chore(website): simplify maintenance and detect documentation drift

This commit is contained in:
N0zoM1z0 2026-10-10 19:56:37 +08:00 committed by Ysopwnable
parent 8c7569aca7
commit 5aa30b8b86
32 changed files with 1108 additions and 168 deletions

View File

@ -35,9 +35,5 @@ jobs:
sudo apt-get update
sudo apt-get install --no-install-recommends -y libcairo2 fonts-dejavu-core
python3 -m pip install -r website/requirements.txt
- name: Prepare website assets
run: python3 scripts/prepare-website.py
- name: Check website links, metadata and publishing configuration
run: python3 scripts/verify-website.py
- name: Check sitemap discovery and metadata regressions
run: python3 scripts/test-website.py
- name: Check shared HTML, links, assets and contributor workflows
run: python3 scripts/website.py check

View File

@ -59,10 +59,8 @@ jobs:
sudo apt-get update
sudo apt-get install --no-install-recommends -y libcairo2 fonts-dejavu-core
python3 -m pip install -r website/requirements.txt
- name: Prepare website assets
run: python3 scripts/prepare-website.py
- name: Check website links, metadata and publishing configuration
run: python3 scripts/verify-website.py
- name: Check shared HTML, links, assets and contributor workflows
run: python3 scripts/website.py check
- name: Record the published commit
run: |
python3 - <<'PY'

View File

@ -0,0 +1,135 @@
"""Optionally audit external website links; network failures stay distinct from missing targets."""
import argparse
from concurrent.futures import ThreadPoolExecutor
from functools import lru_cache
from html.parser import HTMLParser
import json
from pathlib import Path
import re
import subprocess
import sys
from urllib.error import HTTPError, URLError
from urllib.parse import quote, unquote, urlsplit
from urllib.request import Request, urlopen
from website import authored_markdown
@lru_cache(maxsize=None)
def github_api(endpoint):
"""Read GitHub evidence through the caller's gh installation and authentication."""
result = subprocess.run(["gh", "api", endpoint], capture_output=True, text=True, timeout=20)
if result.returncode:
raise ValueError(result.stderr.strip())
return json.loads(result.stdout)
def github_target(url):
"""Resolve repository URLs, including branch names containing slashes."""
parts = [unquote(part) for part in url.path.strip("/").split("/")]
if len(parts) < 2:
return None
repo = f"repos/{parts[0]}/{parts[1]}"
if url.hostname == "raw.githubusercontent.com" and len(parts) >= 4:
return f"{repo}/contents/{'/'.join(parts[3:])}?ref={quote(parts[2], safe='')}"
if len(parts) == 2:
return repo
kind = parts[2]
if kind in ("blob", "tree") and len(parts) >= 4:
ref = parts[3]
# GitHub uses the longest matching branch name before the source path.
if not re.fullmatch(r"[0-9a-f]{40}", ref):
references = github_api(f"{repo}/git/matching-refs/heads/{quote(ref, safe='')}")
references += github_api(f"{repo}/git/matching-refs/tags/{quote(ref, safe='')}")
suffix = "/".join(parts[3:])
matches = [item["ref"].split("/", 2)[2] for item in references]
matches = [name for name in matches if suffix == name or suffix.startswith(name + "/")]
if matches:
ref = max(matches, key=len)
path = "/".join(parts[3:])[len(ref):].lstrip("/")
return f"{repo}/contents/{quote(path, safe='/')}?ref={quote(ref, safe='')}"
if kind in ("issues", "pull") and len(parts) == 4 and parts[3].isdigit():
return f"{repo}/{'pulls' if kind == 'pull' else 'issues'}/{parts[3]}"
if kind == "commit" and len(parts) == 4:
return f"{repo}/commits/{parts[3]}"
if kind == "releases" and len(parts) >= 5 and parts[3] in ("tag", "download"):
return f"{repo}/releases/tags/{quote(parts[4], safe='')}"
return None
def audit_url(item):
"""Classify confirmed missing links separately from blocked or unsupported probes."""
value, sources = item
result = {"url": value, "sources": sorted(set(sources))}
url = urlsplit(value)
try:
if url.hostname in ("github.com", "raw.githubusercontent.com"):
endpoint = github_target(url)
if endpoint is None:
return dict(result, status="unknown", reason="GitHub UI route; check in a browser")
data = github_api(endpoint)
parts = url.path.strip("/").split("/")
if len(parts) >= 6 and parts[2:4] == ["releases", "download"]:
if unquote(parts[5]) not in [asset["name"] for asset in data.get("assets", [])]:
return dict(result, status="broken", reason="Release asset is missing")
return dict(result, status="ok", via="gh api", endpoint=endpoint)
request = Request(value.split("#", 1)[0], headers={
"User-Agent": "REA-Website-Verifier/1.0 (+https://github.com/morluto/rea)",
})
with urlopen(request, timeout=15) as response:
return dict(result, status="ok", http_status=response.status, final_url=response.url)
except HTTPError as error:
return dict(result, status="broken" if error.code in (404, 410) else "unknown", reason=str(error))
except ValueError as error:
return dict(result, status="broken" if "HTTP 404" in str(error) else "unknown", reason=str(error))
except (OSError, URLError, TimeoutError, subprocess.TimeoutExpired) as error:
return dict(result, status="unknown", reason=str(error))
def external_links(root):
"""Discover authored external links, excluding locally checked site and preview URLs."""
references = {}
def add(value, source):
url = urlsplit(value)
if url.scheme in ("http", "https") and url.hostname not in ("rea.tools", "localhost", "127.0.0.1", "::1"):
references.setdefault(value, []).append(str(source.relative_to(root)))
class Links(HTMLParser):
def handle_starttag(self, tag, attrs):
for key, value in attrs:
if key in ("href", "src") and value:
add(value, path)
for path in sorted((root / "website/public").rglob("*.html")):
Links().feed(path.read_text(encoding="utf-8"))
for path in authored_markdown(root):
for value in re.findall(r"(?:\]\(|<)(https?://[^\s<>\)]+)", path.read_text(encoding="utf-8")):
add(value, path)
return references
def main():
"""Run an explicit network audit without making PR checks depend on third-party uptime."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--report", type=Path, help="Write the complete JSON report outside public assets")
args = parser.parse_args()
root = Path(__file__).resolve().parent.parent
with ThreadPoolExecutor(max_workers=4) as pool:
results = list(pool.map(audit_url, external_links(root).items()))
if args.report:
if args.report.resolve().is_relative_to(root / "website/public"):
parser.error("Keep audit reports outside website/public.")
args.report.write_text(json.dumps(results, indent=2) + "\n", encoding="utf-8")
counts = {status: sum(item["status"] == status for item in results) for status in ("ok", "broken", "unknown")}
print(f"External links: {counts['ok']} reachable, {counts['broken']} missing, {counts['unknown']} need manual verification.")
print("GitHub checks establish target existence; external fragment IDs are not checked.")
for item in results:
if item["status"] != "ok":
print(f"{item['status']}: {item['url']} — {item['reason']}")
return 1 if counts["broken"] else 0
if __name__ == "__main__":
sys.exit(main())

View File

@ -1,16 +1,14 @@
"""Prepare downloads, the sitemap and the sharing image for the static website."""
from html.parser import HTMLParser
from io import BytesIO
from pathlib import Path
import re
import sys
from urllib.parse import quote
import xml.etree.ElementTree as ET
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
from website import PageMetadata, page_url
SITE_ORIGIN = "https://rea.tools"
EXAMPLE_FILES = (
"package.json",
"main.js",
@ -21,47 +19,20 @@ EXAMPLE_FILES = (
)
class IndexPolicy(HTMLParser):
"""Read indexing directives from the authored HTML head."""
def __init__(self):
super().__init__()
self.in_head = False
self.noindex = False
def handle_starttag(self, tag, attrs):
if tag == "head":
self.in_head = True
if tag == "meta" and self.in_head:
attributes = dict(attrs)
if (attributes.get("name") or "").lower() in ("robots", "googlebot"):
directives = re.split(
r"[\s,]+", (attributes.get("content") or "").lower()
)
self.noindex |= bool(set(directives) & {"noindex", "none"})
def handle_endtag(self, tag):
if tag == "head":
self.in_head = False
def prepare_sitemap(site):
"""Discover HTML routes automatically, excluding pages marked noindex."""
namespace = "http://www.sitemaps.org/schemas/sitemap/0.9"
ET.register_namespace("", namespace)
sitemap = ET.Element(f"{{{namespace}}}urlset")
for path in sorted(site.rglob("*.html")):
policy = IndexPolicy()
policy = PageMetadata()
policy.feed(path.read_text(encoding="utf-8"))
if policy.noindex:
continue
route = path.relative_to(site).as_posix()
if path.name == "index.html":
route = route.removesuffix("index.html")
entry = ET.SubElement(sitemap, f"{{{namespace}}}url")
ET.SubElement(
entry, f"{{{namespace}}}loc"
).text = f"{SITE_ORIGIN}/{quote(route, safe='/')}"
).text = page_url(site, path)
ET.indent(sitemap, space=" ")
ET.ElementTree(sitemap).write(
site / "sitemap.xml", encoding="utf-8", xml_declaration=True

View File

@ -6,7 +6,11 @@ import io
from pathlib import Path
import shutil
import tempfile
from threading import Thread
import unittest
from http.server import ThreadingHTTPServer
from urllib.error import HTTPError
from urllib.request import urlopen
import xml.etree.ElementTree as ET
@ -23,6 +27,7 @@ def load_script(name):
PREPARE = load_script("prepare-website")
VERIFY = load_script("verify-website")
WEBSITE = load_script("website")
class WebsiteSearchChecks(unittest.TestCase):
@ -126,5 +131,116 @@ class WebsiteSearchChecks(unittest.TestCase):
)
class WebsiteMaintenanceChecks(unittest.TestCase):
"""Exercise contributor edits and the static preview through real consumers."""
def test_shared_edits_preserve_body_and_catch_drift_before_rewriting(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
for name in ("public", "templates"):
shutil.copytree(ROOT / "website" / name, root / "website" / name)
page = root / "website/public/blog/touhou-reconstruction/index.html"
original = page.read_text()
body = original.split('<main id="main"', 1)[1].split("</main>", 1)[0]
template = root / "website/templates/header.html"
template.write_text(template.read_text().replace("Reverse Engineer Anything", "REA test navigation"))
with self.assertRaisesRegex(ValueError, "Shared HTML is stale"):
WEBSITE.sync_pages(root, check=True)
self.assertEqual(page.read_text(), original)
WEBSITE.sync_pages(root)
updated = page.read_text()
self.assertIn("REA test navigation", updated)
self.assertEqual(updated.split('<main id="main"', 1)[1].split("</main>", 1)[0], body)
self.assertEqual(WEBSITE.sync_pages(root), [])
page.write_text(updated.replace(' <!-- website:header:end -->', '', 1))
with self.assertRaisesRegex(ValueError, "needs one header region"):
WEBSITE.sync_pages(root)
def test_new_page_escapes_metadata_stays_a_draft_and_joins_sitemap_when_ready(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
for name in ("public", "templates"):
shutil.copytree(ROOT / "website" / name, root / "website" / name)
site = root / "website/public"
page = WEBSITE.new_page(root, "blog/new-example", 'A <test> & "result"', 'Check "quotes" & <markup>.')
source = page.read_text()
self.assertIn('href="../../blog/" aria-current="page"', source)
self.assertIn('href="../../assets/styles.css"', source)
self.assertIn("A &lt;test&gt; &amp; &quot;result&quot;", source)
with self.assertRaisesRegex(ValueError, "Page already exists"):
WEBSITE.new_page(root, "blog/new-example", "Another page", "Description")
with self.assertRaises(ValueError):
WEBSITE.new_page(root, "../outside", "Title", "Description")
WEBSITE.sync_pages(root, check=True)
PREPARE.prepare_sitemap(site)
self.assertNotIn("https://rea.tools/blog/new-example/", (site / "sitemap.xml").read_text())
page.write_text(source.replace(' <meta name="robots" content="noindex" />\n', ''))
PREPARE.prepare_sitemap(site)
self.assertIn("https://rea.tools/blog/new-example/", (site / "sitemap.xml").read_text())
self.assertEqual(VERIFY.check_site(site)[3], [])
def test_links_in_prompts_fragments_and_copy_controls_are_verified(self):
with tempfile.TemporaryDirectory() as directory:
site = Path(directory) / "public"
shutil.copytree(ROOT / "website/public", site)
page = site / "faq/index.html"
original = page.read_text()
cases = (
('<a href="https://rea.tools/deleted/">Broken</a>', "missing local target"),
('<code>https://rea.tools/guides/native/#deleted</code>', "missing HTML fragment"),
('<code>https://morluto.github.io/rea/examples/notes-web/</code>', "obsolete website URL"),
('<button data-copy="deleted">Copy</button>', "missing copy target"),
('<a href="https://[">Malformed</a>', "invalid URL"),
)
for inserted, message in cases:
with self.subTest(message=message):
page.write_text(original.replace("</main>", inserted + "</main>", 1))
self.assertTrue(any(message in error for error in VERIFY.check_site(site)[3]))
def test_repository_links_reject_missing_files_and_old_markdown_anchors(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / "website/public").mkdir(parents=True)
(root / "docs").mkdir()
(root / "docs/guide.md").write_text("# Guide\n## Set up REA\n")
page = root / "website/README.md"
page.write_text("[Guide](../docs/guide.md#set-up-rea)\n")
(root / "website/.venv").mkdir()
(root / "website/.venv/vendor.md").write_text("[File](missing.md)")
self.assertEqual(VERIFY.check_repository_links(root), [])
page.write_text("[Guide](../docs/guide.md#removed)\n[File](../docs/removed.md)\n")
errors = VERIFY.check_repository_links(root)
self.assertTrue(any("missing Markdown fragment" in error for error in errors))
self.assertTrue(any("missing repository target" in error for error in errors))
def test_preview_preserves_pages_prefix_for_assets_and_directory_redirects(self):
with tempfile.TemporaryDirectory() as directory:
site = Path(directory)
(site / "guide").mkdir()
(site / "guide/index.html").write_text("<h1>Guide</h1>")
(site / "asset.css").write_text("body { color: black; }")
for prefix in ("/", "/rea/"):
with self.subTest(prefix=prefix), ThreadingHTTPServer(("127.0.0.1", 0), WEBSITE.preview_handler(site, prefix)) as server:
worker = Thread(target=server.serve_forever, daemon=True)
worker.start()
try:
base = f"http://127.0.0.1:{server.server_port}"
with urlopen(base + prefix + "guide?test=1") as response:
self.assertEqual(response.url, base + prefix + "guide/?test=1")
self.assertEqual(response.read(), b"<h1>Guide</h1>")
with urlopen(base + prefix + "asset.css") as response:
self.assertIn(b"color: black", response.read())
with self.assertRaises(HTTPError) as error:
urlopen(base + prefix + "missing")
self.assertEqual(error.exception.code, 404)
if prefix != "/":
with self.assertRaises(HTTPError) as error:
urlopen(base + "/guide/")
self.assertEqual(error.exception.code, 404)
finally:
server.shutdown()
worker.join()
if __name__ == "__main__":
unittest.main()

View File

@ -5,13 +5,79 @@ from pathlib import Path
import re
import struct
import sys
from urllib.parse import quote, unquote, urlsplit
import unicodedata
from urllib.parse import unquote, urlsplit
import xml.etree.ElementTree as ET
from zipfile import BadZipFile, ZipFile
from website import NOTES_FIXTURES, SITE_ORIGIN, authored_markdown, page_url, sync_pages
SITE_ORIGIN = "https://rea.tools"
NOTES_FIXTURES = {"examples/notes-electron/index.html", "examples/notes-web/index.html"}
LEGACY_ORIGIN = "morluto.github.io"
SITE_TEXT_URL = re.compile(
r"https?://(?:rea\.tools|morluto\.github\.io)/[^\s<>\"'`]+", re.IGNORECASE
)
def markdown_ids(path):
"""Collect GitHub heading anchors and explicit HTML IDs in local Markdown."""
source = path.read_text(encoding="utf-8")
parser = HtmlReferences()
parser.feed(source)
identifiers = set(parser.ids)
counts = {}
fenced = False
for line in source.splitlines():
if line.lstrip().startswith(("```", "~~~")):
fenced = not fenced
if fenced:
continue
heading = re.match(r"^ {0,3}#{1,6}\s+(.+?)(?:\s+#+)?\s*$", line)
if not heading:
continue
text = re.sub(r"<[^>]*>", "", heading[1]).lower()
slug = "".join(c for c in text if c in "-_ " or unicodedata.category(c)[0] in "LN").replace(" ", "-")
count = counts.get(slug, 0)
identifiers.add(f"{slug}-{count}" if count else slug)
counts[slug] = count + 1
return identifiers
def check_repository_links(root):
"""Check local links in website Markdown and current-repository source links."""
errors = []
sources = {}
for path in authored_markdown(root):
text = re.sub(r"(?ms)^\s*(```|~~~).*?^\s*\1[^\n]*$", "", path.read_text(encoding="utf-8"))
text = re.sub(r"`[^`]*`", "", text)
sources[path] = re.findall(r"\[[^\]\n]*\]\(([^\s)]+)\)", text)
for path in sorted((root / "website/public").rglob("*.html")):
parser = HtmlReferences()
parser.feed(path.read_text(encoding="utf-8"))
sources[path] = parser.references
for path, references in sources.items():
for reference in references:
try:
url = urlsplit(reference)
except ValueError as error:
errors.append(f"{path.relative_to(root)}: invalid URL {reference!r}: {error}")
continue
if url.hostname == LEGACY_ORIGIN and (url.path == "/rea" or url.path.startswith("/rea/")):
errors.append(f"{path.relative_to(root)}: obsolete website URL: {reference}")
continue
if url.hostname == "github.com":
match = re.match(r"^/morluto/rea/(?:blob|tree)/main/(.+)$", url.path)
if not match:
continue
target = (root / unquote(match[1])).resolve()
elif url.scheme or url.netloc or path.suffix == ".html":
continue
else:
target = (path.parent / unquote(url.path)).resolve() if url.path else path
if not target.is_relative_to(root) or not target.exists():
errors.append(f"{path.relative_to(root)}: missing repository target: {reference}")
elif target.suffix == ".md" and url.fragment and unquote(url.fragment) not in markdown_ids(target):
errors.append(f"{path.relative_to(root)}: missing Markdown fragment: {reference}")
return errors
class HtmlReferences(HTMLParser):
@ -30,6 +96,8 @@ class HtmlReferences(HTMLParser):
self.canonicals = []
self.lang = None
self.h1_count = 0
self.text = []
self.copy_targets = []
def handle_starttag(self, tag, attrs):
attributes = dict(attrs)
@ -67,6 +135,8 @@ class HtmlReferences(HTMLParser):
reference = attributes.get(attribute)
if reference is not None:
self.references.append(reference)
if attributes.get("data-copy"):
self.copy_targets.append(attributes["data-copy"])
def handle_endtag(self, tag):
if tag == "head":
@ -75,6 +145,7 @@ class HtmlReferences(HTMLParser):
self.in_title = False
def handle_data(self, data):
self.text.append(data)
if self.in_title:
self.titles[-1] += data
@ -104,15 +175,7 @@ def check_search_metadata(site, pages):
)
if page.noindex():
continue
route = (
path.relative_to(site).parent.as_posix()
if path.name == "index.html"
else label
)
route = "" if route == "." else route
url = f"{SITE_ORIGIN}/{quote(route, safe='/')}" + (
"/" if route and path.name == "index.html" else ""
)
url = page_url(site, path)
indexable.add(url)
if page.canonicals != [url]:
errors.append(f"{label}: needs exactly one head canonical for {url}")
@ -233,15 +296,32 @@ def check_site(site):
label = path.relative_to(site)
for identifier in page.duplicate_ids:
errors.append(f"{label}: duplicate ID {identifier!r}")
for reference in page.references:
url = urlsplit(reference)
if url.scheme or url.netloc:
for identifier in page.copy_targets:
if identifier not in page.ids:
errors.append(f"{label}: missing copy target: {identifier}")
text_urls = [match.group().rstrip(".,;)") for match in SITE_TEXT_URL.finditer(" ".join(page.text))]
for reference in page.references + text_urls:
try:
url = urlsplit(reference)
except ValueError as error:
errors.append(f"{label}: invalid URL {reference!r}: {error}")
continue
references += 1
if url.path.startswith("/"):
if url.hostname == LEGACY_ORIGIN and (url.path == "/rea" or url.path.startswith("/rea/")):
errors.append(f"{label}: obsolete website URL: {reference}; use rea.tools")
continue
if url.hostname == "rea.tools":
if url.scheme != "https" or url.netloc != "rea.tools":
errors.append(f"{label}: website URL must use {SITE_ORIGIN}: {reference}")
continue
target = (site / unquote(url.path).lstrip("/")).resolve()
elif url.scheme or url.netloc:
continue
elif url.path.startswith("/"):
errors.append(f"{label}: use a relative site reference: {reference}")
continue
target = (path.parent / unquote(url.path)).resolve() if url.path else path
else:
target = (path.parent / unquote(url.path)).resolve() if url.path else path
references += 1
if not target.is_relative_to(site):
errors.append(f"{label}: reference leaves the website: {reference}")
continue
@ -323,6 +403,11 @@ def main():
pages, references, indexable, errors = check_site(root / "website/public")
errors.extend(check_example_archive(root / "website/public"))
errors.extend(check_publisher(root))
errors.extend(check_repository_links(root))
try:
sync_pages(root, check=True)
except (OSError, ValueError, KeyError) as error:
errors.append(str(error))
if errors:
print("Website checks failed:\n" + "\n".join(errors), file=sys.stderr)
return 1

245
scripts/website.py Normal file
View File

@ -0,0 +1,245 @@
"""Maintain shared HTML and run the static website's contributor commands."""
import argparse
from functools import partial
from html import escape
from html.parser import HTMLParser
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path, PurePosixPath
from string import Template
import subprocess
import sys
from urllib.parse import quote, urlsplit
SITE_ORIGIN = "https://rea.tools"
NOTES_FIXTURES = {"examples/notes-electron/index.html", "examples/notes-web/index.html"}
NAVIGATION = (
("Showcases", "showcase/"),
("Blog", "blog/"),
("Get started", "get-started/"),
("Guides", "guides/"),
("FAQ", "faq/"),
)
def authored_markdown(root):
"""Discover website documentation while excluding local environments and vendors."""
site = root / "website"
return [
path for path in sorted(site.rglob("*.md"))
if not any(part.startswith(".") or part in ("node_modules", "__pycache__")
for part in path.relative_to(site).parts)
]
class PageMetadata(HTMLParser):
"""Read the authored title, description and indexing policy from the head."""
def __init__(self):
super().__init__()
self.in_head = False
self.in_title = False
self.titles = []
self.descriptions = []
self.noindex = False
def handle_starttag(self, tag, attrs):
if tag == "head":
self.in_head = True
if not self.in_head:
return
if tag == "title":
self.in_title = True
self.titles.append("")
if tag == "meta":
attributes = dict(attrs)
name = (attributes.get("name") or "").lower()
content = attributes.get("content") or ""
if name == "description":
self.descriptions.append(content)
if name in ("robots", "googlebot"):
self.noindex |= bool(
set(content.lower().replace(",", " ").split()) & {"noindex", "none"}
)
def handle_endtag(self, tag):
if tag == "head":
self.in_head = False
if tag == "title":
self.in_title = False
def handle_data(self, data):
if self.in_title:
self.titles[-1] += data
def page_route(site, path):
"""Map an HTML file to its public route without a separate page registry."""
route = path.relative_to(site).as_posix()
return route.removesuffix("index.html") if path.name == "index.html" else route
def page_url(site, path):
"""Return the canonical public URL of an authored HTML file."""
return f"{SITE_ORIGIN}/{quote(page_route(site, path), safe='/')}"
def shared_blocks(root, path, source):
"""Render shared regions while keeping the page's body and metadata authored."""
site = root / "website/public"
metadata = PageMetadata()
metadata.feed(source)
for name, values in (("title", metadata.titles), ("description", metadata.descriptions)):
if len(values) != 1 or not values[0].strip():
raise ValueError(f"{path.relative_to(site)}: needs one nonempty head {name}")
depth = len(path.relative_to(site).parent.parts)
base = "../" * depth or "./"
route = page_route(site, path)
links = []
for label, destination in NAVIGATION:
attributes = ' class="nav-start"' if destination == "get-started/" else ""
if route.startswith(destination):
attributes += ' aria-current="page"'
links.append(f' <a href="{base}{destination}"{attributes}>{label}</a>')
values = {
"base": base,
"navigation": "\n".join(links),
"canonical": escape(page_url(site, path), quote=True),
"origin": SITE_ORIGIN,
"title": escape(metadata.titles[0].strip(), quote=True),
"description": escape(metadata.descriptions[0].strip(), quote=True),
}
templates = root / "website/templates"
return {
name: Template((templates / f"{name}.html").read_text(encoding="utf-8")).substitute(values)
for name in ("sharing", "header", "footer")
}
def sync_pages(root, check=False):
"""Update shared regions, or reject drift without changing any authored file."""
site = root / "website/public"
changes = []
for path in sorted(site.rglob("*.html")):
if path.relative_to(site).as_posix() in NOTES_FIXTURES:
continue
original = path.read_text(encoding="utf-8")
source = original
for name, block in shared_blocks(root, path, source).items():
start = f" <!-- website:{name}:start -->\n"
end = f" <!-- website:{name}:end -->"
if source.count(start) != 1 or source.count(end) != 1:
raise ValueError(f"{path.relative_to(site)}: needs one {name} region; use new-page for a new reading page")
before, _, remaining = source.partition(start)
_, found, after = remaining.partition(end)
if not found:
raise ValueError(f"{path.relative_to(site)}: {name} region is out of order")
source = before + start + block + end + after
if source != original:
changes.append((path, source))
if check and changes:
names = ", ".join(str(path.relative_to(site)) for path, _ in changes)
raise ValueError(f"Shared HTML is stale: {names}. Run python3 scripts/website.py sync and commit the updated pages.")
if not check:
for path, source in changes:
path.write_text(source, encoding="utf-8")
print(f"{'Checked' if check else 'Synced'} shared HTML ({len(changes)} pages changed).")
return [path for path, _ in changes]
def new_page(root, route, title, description):
"""Create a draft reading page with correct depth, metadata and shared regions."""
parts = PurePosixPath(route)
if not route or parts.is_absolute() or ".." in parts.parts or "\\" in route or parts.suffix:
raise ValueError("Use a relative directory route, such as blog/my-article.")
site = root / "website/public"
path = site.joinpath(*parts.parts, "index.html")
if not path.resolve().is_relative_to(site.resolve()):
raise ValueError("The new page must stay inside website/public.")
if path.exists():
raise ValueError(f"Page already exists: {path.relative_to(site)}")
base = "../" * len(parts.parts) or "./"
source = Template((root / "website/templates/page.html").read_text(encoding="utf-8")).substitute(
title=escape(title, quote=True), description=escape(description, quote=True), base=base,
)
# Render before writing, so invalid input cannot leave an incomplete page.
blocks = shared_blocks(root, path, source)
for name, block in blocks.items():
source = source.replace(f" <!-- website:{name}:start -->\n", f" <!-- website:{name}:start -->\n{block}")
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(source, encoding="utf-8")
print(f"Created {path.relative_to(root)}. Write the page, add its index link, then remove the Draft label and noindex directive when ready.")
return path
def run_script(root, name, arguments=()):
"""Use the active Python environment for the existing preparation and checks."""
subprocess.run([sys.executable, str(root / "scripts" / name), *arguments], cwd=root, check=True)
def preview_handler(site, prefix):
"""Serve one static directory at root or a Pages prefix, preserving redirects."""
class Handler(SimpleHTTPRequestHandler):
def send_head(self):
if not urlsplit(self.path).path.startswith(prefix):
self.send_error(404)
return None
return super().send_head()
def translate_path(self, path):
return super().translate_path("/" + path[len(prefix):])
return partial(Handler, directory=str(site))
def main():
"""Expose one small command for synchronizing, checking and previewing the site."""
parser = argparse.ArgumentParser(description=__doc__)
commands = parser.add_subparsers(dest="command", required=True)
sync = commands.add_parser("sync", help="Update shared HTML after editing templates or page metadata")
sync.add_argument("--check", action="store_true", help="Report drift without rewriting pages")
commands.add_parser("check", help="Check shared HTML, prepare assets, verify links and run regression tests")
links = commands.add_parser("links", help="Audit external targets using gh and HTTP; separate from offline checks")
links.add_argument("--report", help="Write the full external-link JSON report")
create = commands.add_parser("new-page", help="Create a draft reading page")
create.add_argument("route")
create.add_argument("--title", required=True)
create.add_argument("--description", required=True)
serve = commands.add_parser("serve", help="Prepare and preview the static site")
serve.add_argument("--port", type=int, default=4173)
serve.add_argument("--base-path", choices=("/", "/rea/"), default="/")
args = parser.parse_args()
root = Path(__file__).resolve().parent.parent
try:
if args.command == "new-page":
new_page(root, args.route, args.title, args.description)
elif args.command == "sync":
sync_pages(root, check=args.check)
elif args.command == "check":
sync_pages(root, check=True)
for name in ("prepare-website.py", "verify-website.py", "test-website.py"):
run_script(root, name)
elif args.command == "links":
run_script(root, "audit-website-links.py", ["--report", args.report] if args.report else [])
else:
sync_pages(root)
run_script(root, "prepare-website.py")
prefix = args.base_path
handler = preview_handler(root / "website/public", prefix)
with ThreadingHTTPServer(("127.0.0.1", args.port), handler) as server:
print(f"Preview: http://127.0.0.1:{server.server_port}{prefix}", flush=True)
server.serve_forever()
except (OSError, ValueError, KeyError) as error:
print(str(error), file=sys.stderr)
return 1
except subprocess.CalledProcessError as error:
return error.returncode
except KeyboardInterrupt:
return 0
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@ -12,6 +12,12 @@ for asset notes and the source of case-study claims.
should explain a new fact or action.
- Reuse the shared styles, prompts, figures and code-step components. Keep a
case study focused on one question, with further examples in native details.
- Edit shared header, footer and sharing markup in `templates/`; run
`python3 scripts/website.py sync` and commit the rendered regions with their
templates. Keep page bodies outside the marked regions. Author each title and
description once; derive sharing fields and canonicals from them. Use
`new-page` for a reading-page scaffold, then add its index link. Keep a draft's
visible label and `noindex` until the content is ready.
- Show what REA returns and how the agent uses it. Preserve source attribution,
target identity and verification scope.
- Blog articles teach methods, decisions and workflows through concrete cases.
@ -27,9 +33,13 @@ for asset notes and the source of case-study claims.
from HTML automatically. Mark example applications `noindex` in the head and
keep them crawlable. Maintain the SVG sharing source, not the generated PNG.
- Check both local root and `/rea/` paths, mobile layouts, expanded details,
copying and downloads. Run `scripts/prepare-website.py` followed by
`scripts/verify-website.py` and `scripts/test-website.py`, using the website
Python environment described in the README.
copying and downloads. Run `python3 scripts/website.py check`, using the Python
environment described in the README. It rejects drift rather than silently
rewriting shared HTML. Use `website.py links` for an optional network audit;
distinguish missing targets from access denials and unverified fragments.
- Check setup and support statements against the current installation/provider
guides and released package. Preserve historical case-study checkpoints and
versions unless a new investigation supplies replacement evidence.
- Keep `website-pages.yml` as the sole, manual production publisher for
Cloudflare and Pages. Default to `both`: both hosts receive the same verified
artifact and pass the published-commit check. Use `pages` only when explicitly

View File

@ -7,8 +7,9 @@
An English static website with explanatory figures, worked guides and Aegis, DX-Ball, Notion, TH04 and CTF investigations.
The public files are in `website/public/`. The site uses HTML, CSS and small
scripts for copying code, following code comparisons and playing the dinosaur
speed reconstruction. Python prepares the example ZIP, sitemap and sharing
image; there is no frontend bundler or npm dependency.
speed reconstruction. Small Python commands maintain shared HTML, prepare
downloads and check the site. There is no frontend framework, bundler or npm
dependency. The checked-in HTML works as ordinary static files.
[style-guide.md](style-guide.md) explains the writing, page structure, figures,
visual system and review process. Read it before adding or revising a page.
@ -21,8 +22,7 @@ From the repository root:
python3 -m venv website/.venv
source website/.venv/bin/activate
python3 -m pip install -r website/requirements.txt
python3 scripts/prepare-website.py
python3 -m http.server 4173 --bind 127.0.0.1 --directory website/public
python3 scripts/website.py serve
```
Open <http://127.0.0.1:4173/>. Refresh the browser after editing a file.
@ -32,9 +32,77 @@ install them with `sudo apt-get install libcairo2 fonts-dejavu-core`. For other
systems, see [CairoSVG's installation instructions](https://cairosvg.org/documentation/#installation).
CI installs both packages explicitly before rendering the card.
After adding, moving or removing a page, or changing its indexing policy, rerun
`python3 scripts/prepare-website.py`. It regenerates the sitemap from the current
HTML files. No URL list needs to be maintained.
To test the GitHub Pages prefix, use
`python3 scripts/website.py serve --port 4174 --base-path /rea/` and open
<http://127.0.0.1:4174/rea/>.
## Contribute a page or correction
Edit the page's HTML in `public/`. Keep the question, evidence and result easy to
follow. The writing and design guidance in [style-guide.md](style-guide.md)
applies to every page.
| Change | Edit | Then run |
| ------------------------------ | ----------------------------------------------------------- | ----------------------------------------------- |
| Page text, examples or figures | The page's HTML outside the marked shared regions | `python3 scripts/website.py check` |
| Search title or description | The page's `<title>` and first description meta tag | `python3 scripts/website.py sync`, then `check` |
| Main navigation | `NAVIGATION` in `scripts/website.py` | `sync`, then `check` |
| Header, social links or footer | `templates/header.html` or `templates/footer.html` | `sync`, then `check` |
| Shared link previews | `templates/sharing.html` or `public/assets/social-card.svg` | `sync`, then `check` |
Create a reading page with:
```sh
python3 scripts/website.py new-page blog/my-article \
--title "My article title" \
--description "One sentence explaining the article."
```
The command creates `public/blog/my-article/index.html` with the correct relative
paths, navigation and sharing metadata. It starts as a visible draft with
`noindex`. Write the article, add its link to the Blog index, then remove the
Draft label and robots meta tag when it is ready. The same command accepts
routes under `guides/` and `showcase/`. New pages still need their own content;
the scaffold supplies the common structure.
The regions marked `website:sharing`, `website:header` and `website:footer` are
generated from the templates. Edit the template rather than a generated copy.
`sync` updates only those regions; page bodies, figures and scripts remain
authored HTML. Commit the updated pages alongside the template. Titles and
descriptions are written once per page; their sharing fields are derived from
them. Routes come from file paths, including the sitemap, so there is no second
page list to register.
Before opening a PR:
```sh
python3 scripts/website.py check
```
This checks shared HTML without rewriting it, prepares generated assets, and
checks page links, copied-text targets, repository documentation links, SEO
metadata and the publishing configuration. It also runs the maintenance
regressions. A stale shared region fails with the command needed to fix it.
PR checks and the manual publisher run this same command. The checks work
offline after the Python dependencies are installed.
For an optional external-link audit, with GitHub CLI available:
```sh
python3 scripts/website.py links --report /tmp/rea-website-links.json
```
GitHub source links are checked through `gh`, including pinned commits and
branch names containing slashes. Other external links use HTTP. Missing targets
fail the command; access denials, timeouts and GitHub UI routes are reported as
unknown and need manual verification. External fragment IDs are not checked.
This network audit runs separately so third-party uptime does not block every PR.
Keep its report outside public assets.
Links are only part of a content review. When updating setup or support claims,
compare them with [installation.md](../docs/installation.md), the relevant
provider guide and the released package. A case study's pinned version and date
describe its recorded investigation; update them only when repeating that work.
## Pages
@ -67,11 +135,11 @@ navigation and asset references remain relative so both hosts and local previews
keep working.
Maintain a descriptive `<title>` and a short meta description in each page's
`<head>`. The title names the page's subject; visible headings keep the wording
that best explains it to a reader. Open Graph and Twitter titles/descriptions
match those fields. `og:url` matches the canonical, and sharing images use
absolute rea.tools URLs. Search results and link previews should follow the same
clear, concise writing standard as the page.
`<head>`, then run `sync`. The title names the page's subject; visible headings
keep the wording that best explains it to a reader. The script derives Open
Graph and Twitter fields and the canonical from those inputs and the file's
route. Search results and link previews should follow the same clear, concise
writing standard as the page.
`scripts/prepare-website.py` scans `public/**/*.html` and generates
`public/sitemap.xml` with absolute rea.tools URLs. An `index.html` maps to its
@ -101,8 +169,7 @@ canonicals, lost fixture exclusions, stale sitemap entries and missing previews.
Run it after preparing assets:
```sh
python3 scripts/verify-website.py
python3 scripts/test-website.py
python3 scripts/website.py check
```
### After publishing an SEO change
@ -404,8 +471,7 @@ dependencies. The same asset preparation and verification run before each manual
deployment. With the preview environment active, run them locally:
```sh
python3 scripts/prepare-website.py
python3 scripts/verify-website.py
python3 scripts/website.py check
```
`.github/workflows/pages.yml` is a separate, manual-only VitePress build. It

View File

@ -8,6 +8,7 @@
name="description"
content="Thoughts on reverse engineering, drawing on practical experience with REA and AI agents."
/>
<!-- website:sharing:start -->
<link rel="canonical" href="https://rea.tools/blog/" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="REA" />
@ -48,6 +49,7 @@
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../assets/styles.css" />
@ -55,6 +57,7 @@
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../" aria-label="REA home"
@ -63,8 +66,8 @@
>
<nav class="navigation" aria-label="Main navigation">
<a href="../showcase/">Showcases</a>
<a href="./" aria-current="page">Blog</a>
<a class="nav-start" href="../get-started/">Get started</a>
<a href="../blog/" aria-current="page">Blog</a>
<a href="../get-started/" class="nav-start">Get started</a>
<a href="../guides/">Guides</a>
<a href="../faq/">FAQ</a>
</nav>
@ -130,6 +133,7 @@
</div>
</div>
</header>
<!-- website:header:end -->
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb"><a href="../">Home</a> / Blog</p>
@ -154,6 +158,7 @@
</section>
</div>
</main>
<!-- website:footer:start -->
<footer class="site-footer">
<div class="container footer-inner">
<p>REA · Reverse Engineer Anything</p>
@ -170,5 +175,6 @@
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
<!-- website:footer:end -->
</body>
</html>

View File

@ -8,10 +8,8 @@
name="description"
content="N0zoM1z0 reflects on manual reverse engineering, Touhou reconstruction with AI agents, and how knowledge compounds across TH08, TH095 and PC-98 TH04."
/>
<link
rel="canonical"
href="https://rea.tools/blog/touhou-reconstruction/"
/>
<!-- website:sharing:start -->
<link rel="canonical" href="https://rea.tools/blog/touhou-reconstruction/" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="REA" />
<meta
@ -22,10 +20,7 @@
property="og:description"
content="N0zoM1z0 reflects on manual reverse engineering, Touhou reconstruction with AI agents, and how knowledge compounds across TH08, TH095 and PC-98 TH04."
/>
<meta
property="og:url"
content="https://rea.tools/blog/touhou-reconstruction/"
/>
<meta property="og:url" content="https://rea.tools/blog/touhou-reconstruction/" />
<meta
property="og:image"
content="https://rea.tools/assets/social-card.png"
@ -54,6 +49,7 @@
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../../assets/styles.css" />
@ -61,6 +57,7 @@
</head>
<body id="top" class="blog-article">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../../" aria-label="REA home"
@ -69,8 +66,8 @@
>
<nav class="navigation" aria-label="Main navigation">
<a href="../../showcase/">Showcases</a>
<a href="../" aria-current="true">Blog</a>
<a class="nav-start" href="../../get-started/">Get started</a>
<a href="../../blog/" aria-current="page">Blog</a>
<a href="../../get-started/" class="nav-start">Get started</a>
<a href="../../guides/">Guides</a>
<a href="../../faq/">FAQ</a>
</nav>
@ -136,6 +133,7 @@
</div>
</div>
</header>
<!-- website:header:end -->
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb">
@ -1010,6 +1008,7 @@
</article>
</div>
</main>
<!-- website:footer:start -->
<footer class="site-footer">
<div class="container footer-inner">
<p>REA · Reverse Engineer Anything</p>
@ -1023,8 +1022,9 @@
</div>
</footer>
<p class="sr-only" id="copy-status" role="status" aria-live="polite"></p>
<a class="back-to-top" href="#top" aria-label="Back to top"
><span aria-hidden="true">↑</span> Top</a
>
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
<!-- website:footer:end -->
</body>
</html>

View File

@ -8,6 +8,7 @@
name="description"
content="Read the dinosaur game’s speed rule with REA, rebuild it, then change the speed and play."
/>
<!-- website:sharing:start -->
<link rel="canonical" href="https://rea.tools/examples/dino-lab/" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="REA" />
@ -48,6 +49,7 @@
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../../assets/styles.css" />
@ -56,6 +58,7 @@
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../../" aria-label="REA home"
@ -64,9 +67,10 @@
>
<nav class="navigation" aria-label="Main navigation">
<a href="../../showcase/">Showcases</a>
<a href="../../blog/">Blog</a
><a class="nav-start" href="../../get-started/">Get started</a
><a href="../../guides/">Guides</a><a href="../../faq/">FAQ</a>
<a href="../../blog/">Blog</a>
<a href="../../get-started/" class="nav-start">Get started</a>
<a href="../../guides/">Guides</a>
<a href="../../faq/">FAQ</a>
</nav>
<div class="header-social">
<a
@ -130,6 +134,7 @@
</div>
</div>
</header>
<!-- website:header:end -->
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb">
@ -396,6 +401,7 @@
</article>
</div>
</main>
<!-- website:footer:start -->
<footer class="site-footer">
<div class="container footer-inner">
<p>REA · Reverse Engineer Anything</p>
@ -409,8 +415,9 @@
</div>
</footer>
<p class="sr-only" id="copy-status" role="status" aria-live="polite"></p>
<a class="back-to-top" href="#top" aria-label="Back to top"
><span aria-hidden="true">↑</span> Top</a
>
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
<!-- website:footer:end -->
</body>
</html>

View File

@ -8,6 +8,7 @@
name="description"
content="Answers to common REA questions: installation, updates, analysis tools, coding agents, troubleshooting and sharing examples."
/>
<!-- website:sharing:start -->
<link rel="canonical" href="https://rea.tools/faq/" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="REA" />
@ -48,6 +49,7 @@
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../assets/styles.css" />
@ -55,6 +57,7 @@
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../" aria-label="REA home"
@ -64,9 +67,9 @@
<nav class="navigation" aria-label="Main navigation">
<a href="../showcase/">Showcases</a>
<a href="../blog/">Blog</a>
<a class="nav-start" href="../get-started/">Get started</a>
<a href="../get-started/" class="nav-start">Get started</a>
<a href="../guides/">Guides</a>
<a href="./" aria-current="page">FAQ</a>
<a href="../faq/" aria-current="page">FAQ</a>
</nav>
<div class="header-social">
<a
@ -130,6 +133,7 @@
</div>
</div>
</header>
<!-- website:header:end -->
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb"><a href="../">Home</a> / FAQ</p>
@ -495,6 +499,7 @@
</article>
</div>
</main>
<!-- website:footer:start -->
<footer class="site-footer">
<div class="container footer-inner">
<p>REA · Reverse Engineer Anything</p>
@ -508,8 +513,9 @@
</div>
</footer>
<p class="sr-only" id="copy-status" role="status" aria-live="polite"></p>
<a class="back-to-top" href="#top" aria-label="Back to top"
><span aria-hidden="true">↑</span> Top</a
>
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
<!-- website:footer:end -->
</body>
</html>

View File

@ -8,6 +8,7 @@
name="description"
content="Try REA on a small Notes app. Set up your agent, ask where exported notes go, and check the CSV formatter with a sample title."
/>
<!-- website:sharing:start -->
<link rel="canonical" href="https://rea.tools/first-investigation/" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="REA" />
@ -48,6 +49,7 @@
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../assets/styles.css" />
@ -55,6 +57,7 @@
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../" aria-label="REA home"
@ -63,9 +66,10 @@
>
<nav class="navigation" aria-label="Main navigation">
<a href="../showcase/">Showcases</a>
<a href="../blog/">Blog</a
><a class="nav-start" href="../get-started/">Get started</a
><a href="../guides/">Guides</a><a href="../faq/">FAQ</a>
<a href="../blog/">Blog</a>
<a href="../get-started/" class="nav-start">Get started</a>
<a href="../guides/">Guides</a>
<a href="../faq/">FAQ</a>
</nav>
<div class="header-social">
<a
@ -129,6 +133,7 @@
</div>
</div>
</header>
<!-- website:header:end -->
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb">
@ -433,6 +438,7 @@ JS</code></pre>
</article>
</div>
</main>
<!-- website:footer:start -->
<footer class="site-footer">
<div class="container footer-inner">
<p>REA · Reverse Engineer Anything</p>
@ -446,8 +452,9 @@ JS</code></pre>
</div>
</footer>
<p class="sr-only" id="copy-status" role="status" aria-live="polite"></p>
<a class="back-to-top" href="#top" aria-label="Back to top"
><span aria-hidden="true">↑</span> Top</a
>
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
<!-- website:footer:end -->
</body>
</html>

View File

@ -8,6 +8,7 @@
name="description"
content="Connect REA to your coding agent or inspect a local JavaScript or Electron app from the terminal. Setup, first questions and native provider guides."
/>
<!-- website:sharing:start -->
<link rel="canonical" href="https://rea.tools/get-started/" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="REA" />
@ -48,6 +49,7 @@
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../assets/styles.css" />
@ -55,6 +57,7 @@
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../" aria-label="REA home"
@ -63,9 +66,10 @@
>
<nav class="navigation" aria-label="Main navigation">
<a href="../showcase/">Showcases</a>
<a href="../blog/">Blog</a
><a class="nav-start" href="./" aria-current="page">Get started</a
><a href="../guides/">Guides</a><a href="../faq/">FAQ</a>
<a href="../blog/">Blog</a>
<a href="../get-started/" class="nav-start" aria-current="page">Get started</a>
<a href="../guides/">Guides</a>
<a href="../faq/">FAQ</a>
</nav>
<div class="header-social">
<a
@ -129,6 +133,7 @@
</div>
</div>
</header>
<!-- website:header:end -->
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb"><a href="../">Home</a> / Get started</p>
@ -291,7 +296,9 @@
<div>
<p>
Use an existing Ghidra 12.1.x installation and a 64-bit
JDK 21 or newer on Linux x64 or macOS x64/arm64. Set the
JDK 21 or newer on Linux or macOS, on x64 or arm64. Your
Ghidra installation needs a native decompiler for your
host architecture. Set the
paths before running setup:
</p>
<details class="evidence-details">
@ -316,8 +323,38 @@ npx rea-agents@latest setup</code></pre>
<p>
Replace both paths with your existing installations.
Setup checks them and records valid settings for your
selected client. On macOS, use a Ghidra package with the
native decompiler matching your host architecture.
selected client. On Linux arm64, you may need to build
Ghidra's native decompiler separately.
</p>
</details>
<details class="evidence-details">
<summary>Connect Ghidra on Windows</summary>
<p>
Windows x64 has experimental, read-only support for
native x86 and x64 PE applications and DLLs on fixed local
NTFS drives. Use an existing Ghidra 12.1.x installation
and a 64-bit JDK 21 or newer:
</p>
<div class="code-block">
<div class="code-header">
<span>Ghidra connection · Windows PowerShell</span>
<button
class="copy-button"
type="button"
data-copy="ghidra-windows-connect"
aria-label="Copy Ghidra connection for Windows PowerShell"
>
Copy
</button>
</div>
<pre><code id="ghidra-windows-connect">$env:GHIDRA_INSTALL_DIR = "C:/tools/ghidra_12.1.4_PUBLIC"
$env:JAVA_HOME = "C:/tools/jdk-21"
npx -y rea-agents@latest doctor --provider ghidra --json
npx rea-agents@latest setup</code></pre>
</div>
<p>
Replace the two paths with your installations, then review
setup's plan.
</p>
</details>
</div>
@ -492,6 +529,7 @@ npx rea-agents@latest setup</code></pre>
</article>
</div>
</main>
<!-- website:footer:start -->
<footer class="site-footer">
<div class="container footer-inner">
<p>REA · Reverse Engineer Anything</p>
@ -508,5 +546,6 @@ npx rea-agents@latest setup</code></pre>
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
<!-- website:footer:end -->
</body>
</html>

View File

@ -8,10 +8,14 @@
name="description"
content="Observe a Notes export with REA: select a browser page, capture a click, inspect the request and follow the export script."
/>
<!-- website:sharing:start -->
<link rel="canonical" href="https://rea.tools/guides/browser/" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="REA" />
<meta property="og:title" content="Browser and Network Analysis — REA" />
<meta
property="og:title"
content="Browser and Network Analysis — REA"
/>
<meta
property="og:description"
content="Observe a Notes export with REA: select a browser page, capture a click, inspect the request and follow the export script."
@ -29,7 +33,10 @@
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Browser and Network Analysis — REA" />
<meta
name="twitter:title"
content="Browser and Network Analysis — REA"
/>
<meta
name="twitter:description"
content="Observe a Notes export with REA: select a browser page, capture a click, inspect the request and follow the export script."
@ -42,6 +49,7 @@
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../../assets/styles.css" />
@ -49,6 +57,7 @@
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../../" aria-label="REA home"
@ -57,9 +66,10 @@
>
<nav class="navigation" aria-label="Main navigation">
<a href="../../showcase/">Showcases</a>
<a href="../../blog/">Blog</a
><a class="nav-start" href="../../get-started/">Get started</a
><a href="../">Guides</a><a href="../../faq/">FAQ</a>
<a href="../../blog/">Blog</a>
<a href="../../get-started/" class="nav-start">Get started</a>
<a href="../../guides/" aria-current="page">Guides</a>
<a href="../../faq/">FAQ</a>
</nav>
<div class="header-social">
<a
@ -123,6 +133,7 @@
</div>
</div>
</header>
<!-- website:header:end -->
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb">
@ -416,6 +427,7 @@ link.click();</code></pre>
</article>
</div>
</main>
<!-- website:footer:start -->
<footer class="site-footer">
<div class="container footer-inner">
<p>REA · Reverse Engineer Anything</p>
@ -432,5 +444,6 @@ link.click();</code></pre>
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
<!-- website:footer:end -->
</body>
</html>

View File

@ -8,10 +8,14 @@
name="description"
content="Learn REA through short worked examples: a native calculation, an Electron CSV export and a browser request."
/>
<!-- website:sharing:start -->
<link rel="canonical" href="https://rea.tools/guides/" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="REA" />
<meta property="og:title" content="Reverse Engineering Guides — REA" />
<meta
property="og:title"
content="Reverse Engineering Guides — REA"
/>
<meta
property="og:description"
content="Learn REA through short worked examples: a native calculation, an Electron CSV export and a browser request."
@ -29,7 +33,10 @@
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Reverse Engineering Guides — REA" />
<meta
name="twitter:title"
content="Reverse Engineering Guides — REA"
/>
<meta
name="twitter:description"
content="Learn REA through short worked examples: a native calculation, an Electron CSV export and a browser request."
@ -42,6 +49,7 @@
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../assets/styles.css" />
@ -49,6 +57,7 @@
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../" aria-label="REA home"
@ -57,9 +66,10 @@
>
<nav class="navigation" aria-label="Main navigation">
<a href="../showcase/">Showcases</a>
<a href="../blog/">Blog</a
><a class="nav-start" href="../get-started/">Get started</a
><a href="./" aria-current="page">Guides</a><a href="../faq/">FAQ</a>
<a href="../blog/">Blog</a>
<a href="../get-started/" class="nav-start">Get started</a>
<a href="../guides/" aria-current="page">Guides</a>
<a href="../faq/">FAQ</a>
</nav>
<div class="header-social">
<a
@ -123,6 +133,7 @@
</div>
</div>
</header>
<!-- website:header:end -->
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb"><a href="../">Home</a> / Guides</p>
@ -199,6 +210,7 @@
</section>
</div>
</main>
<!-- website:footer:start -->
<footer class="site-footer">
<div class="container footer-inner">
<p>REA · Reverse Engineer Anything</p>
@ -215,5 +227,6 @@
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
<!-- website:footer:end -->
</body>
</html>

View File

@ -8,6 +8,7 @@
name="description"
content="Trace a Notes CSV export through a renderer API, preload and IPC handler with REA. Includes downloadable example files and a verified analysis result."
/>
<!-- website:sharing:start -->
<link rel="canonical" href="https://rea.tools/guides/javascript/" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="REA" />
@ -48,6 +49,7 @@
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../../assets/styles.css" />
@ -55,6 +57,7 @@
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../../" aria-label="REA home"
@ -63,9 +66,10 @@
>
<nav class="navigation" aria-label="Main navigation">
<a href="../../showcase/">Showcases</a>
<a href="../../blog/">Blog</a
><a class="nav-start" href="../../get-started/">Get started</a
><a href="../">Guides</a><a href="../../faq/">FAQ</a>
<a href="../../blog/">Blog</a>
<a href="../../get-started/" class="nav-start">Get started</a>
<a href="../../guides/" aria-current="page">Guides</a>
<a href="../../faq/">FAQ</a>
</nav>
<div class="header-social">
<a
@ -129,6 +133,7 @@
</div>
</div>
</header>
<!-- website:header:end -->
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb">
@ -418,6 +423,7 @@
</article>
</div>
</main>
<!-- website:footer:start -->
<footer class="site-footer">
<div class="container footer-inner">
<p>REA · Reverse Engineer Anything</p>
@ -434,5 +440,6 @@
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
<!-- website:footer:end -->
</body>
</html>

View File

@ -8,10 +8,14 @@
name="description"
content="Recover a native calculation with REA: inspect instructions, follow a caller, read constants and check reconstructed C."
/>
<!-- website:sharing:start -->
<link rel="canonical" href="https://rea.tools/guides/native/" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="REA" />
<meta property="og:title" content="Native Binary Analysis — REA" />
<meta
property="og:title"
content="Native Binary Analysis — REA"
/>
<meta
property="og:description"
content="Recover a native calculation with REA: inspect instructions, follow a caller, read constants and check reconstructed C."
@ -29,7 +33,10 @@
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Native Binary Analysis — REA" />
<meta
name="twitter:title"
content="Native Binary Analysis — REA"
/>
<meta
name="twitter:description"
content="Recover a native calculation with REA: inspect instructions, follow a caller, read constants and check reconstructed C."
@ -42,6 +49,7 @@
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../../assets/styles.css" />
@ -49,6 +57,7 @@
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../../" aria-label="REA home"
@ -57,9 +66,10 @@
>
<nav class="navigation" aria-label="Main navigation">
<a href="../../showcase/">Showcases</a>
<a href="../../blog/">Blog</a
><a class="nav-start" href="../../get-started/">Get started</a
><a href="../">Guides</a><a href="../../faq/">FAQ</a>
<a href="../../blog/">Blog</a>
<a href="../../get-started/" class="nav-start">Get started</a>
<a href="../../guides/" aria-current="page">Guides</a>
<a href="../../faq/">FAQ</a>
</nav>
<div class="header-social">
<a
@ -123,6 +133,7 @@
</div>
</div>
</header>
<!-- website:header:end -->
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb">
@ -348,6 +359,7 @@ return (DxBallInt)pan;</code></pre>
</article>
</div>
</main>
<!-- website:footer:start -->
<footer class="site-footer">
<div class="container footer-inner">
<p>REA · Reverse Engineer Anything</p>
@ -364,5 +376,6 @@ return (DxBallInt)pan;</code></pre>
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
<!-- website:footer:end -->
</body>
</html>

View File

@ -8,6 +8,7 @@
name="description"
content="Reverse engineer apps, binaries and browser behavior with your coding agent. Follow clear guides and try interactive examples with REA."
/>
<!-- website:sharing:start -->
<link rel="canonical" href="https://rea.tools/" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="REA" />
@ -48,6 +49,7 @@
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="assets/styles.css" />
@ -57,6 +59,7 @@
</head>
<body class="home-page" id="top">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="./" aria-label="REA home"
@ -64,11 +67,11 @@
><span class="brand-caption">Reverse Engineer Anything</span></a
>
<nav class="navigation" aria-label="Main navigation">
<a href="showcase/">Showcases</a>
<a href="blog/">Blog</a>
<a class="nav-start" href="get-started/">Get started</a>
<a href="guides/">Guides</a>
<a href="faq/">FAQ</a>
<a href="./showcase/">Showcases</a>
<a href="./blog/">Blog</a>
<a href="./get-started/" class="nav-start">Get started</a>
<a href="./guides/">Guides</a>
<a href="./faq/">FAQ</a>
</nav>
<div class="header-social">
<a
@ -132,6 +135,7 @@
</div>
</div>
</header>
<!-- website:header:end -->
<details class="home-page-nav" data-home-nav open>
<summary class="container">
<span>On this page</span>
@ -1095,6 +1099,7 @@ SPEED: 6</code></pre>
</div>
</section>
</main>
<!-- website:footer:start -->
<footer class="site-footer">
<div class="container footer-inner">
<p>REA · Reverse Engineer Anything</p>
@ -1111,5 +1116,6 @@ SPEED: 6</code></pre>
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
<!-- website:footer:end -->
</body>
</html>

View File

@ -8,6 +8,7 @@
name="description"
content="Inspect the Aegis Android APK with REA, recover its six-digit login-code calculation and try a reconstruction with an adjustable clock."
/>
<!-- website:sharing:start -->
<link rel="canonical" href="https://rea.tools/showcase/aegis/" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="REA" />
@ -48,6 +49,7 @@
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../../assets/styles.css" />
@ -56,6 +58,7 @@
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../../" aria-label="REA home"
@ -63,9 +66,9 @@
><span class="brand-caption">Reverse Engineer Anything</span></a
>
<nav class="navigation" aria-label="Main navigation">
<a href="../">Showcases</a>
<a href="../../showcase/" aria-current="page">Showcases</a>
<a href="../../blog/">Blog</a>
<a class="nav-start" href="../../get-started/">Get started</a>
<a href="../../get-started/" class="nav-start">Get started</a>
<a href="../../guides/">Guides</a>
<a href="../../faq/">FAQ</a>
</nav>
@ -131,6 +134,7 @@
</div>
</div>
</header>
<!-- website:header:end -->
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb">
@ -731,6 +735,7 @@ npx rea-agents@latest inspect-android-package \
</article>
</div>
</main>
<!-- website:footer:start -->
<footer class="site-footer">
<div class="container footer-inner">
<p>REA · Reverse Engineer Anything</p>
@ -744,8 +749,9 @@ npx rea-agents@latest inspect-android-package \
</div>
</footer>
<p class="sr-only" id="copy-status" role="status" aria-live="polite"></p>
<a class="back-to-top" href="#top" aria-label="Back to top"
><span aria-hidden="true">↑</span> Top</a
>
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
<!-- website:footer:end -->
</body>
</html>

View File

@ -8,6 +8,7 @@
name="description"
content="Use REA to solve DownUnderCTF's masked squares flag checker. Find the checking functions, extract the masks and compare a recovered flag with the original program."
/>
<!-- website:sharing:start -->
<link rel="canonical" href="https://rea.tools/showcase/ctf/" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="REA" />
@ -17,7 +18,7 @@
/>
<meta
property="og:description"
content="Use REA to solve DownUnderCTF's masked squares flag checker. Find the checking functions, extract the masks and compare a recovered flag with the original program."
content="Use REA to solve DownUnderCTF&#x27;s masked squares flag checker. Find the checking functions, extract the masks and compare a recovered flag with the original program."
/>
<meta property="og:url" content="https://rea.tools/showcase/ctf/" />
<meta
@ -38,7 +39,7 @@
/>
<meta
name="twitter:description"
content="Use REA to solve DownUnderCTF's masked squares flag checker. Find the checking functions, extract the masks and compare a recovered flag with the original program."
content="Use REA to solve DownUnderCTF&#x27;s masked squares flag checker. Find the checking functions, extract the masks and compare a recovered flag with the original program."
/>
<meta
name="twitter:image"
@ -48,6 +49,7 @@
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../../assets/styles.css" />
@ -55,6 +57,7 @@
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../../" aria-label="REA home"
@ -62,9 +65,9 @@
><span class="brand-caption">Reverse Engineer Anything</span></a
>
<nav class="navigation" aria-label="Main navigation">
<a href="../">Showcases</a>
<a href="../../showcase/" aria-current="page">Showcases</a>
<a href="../../blog/">Blog</a>
<a class="nav-start" href="../../get-started/">Get started</a>
<a href="../../get-started/" class="nav-start">Get started</a>
<a href="../../guides/">Guides</a>
<a href="../../faq/">FAQ</a>
</nav>
@ -130,6 +133,7 @@
</div>
</div>
</header>
<!-- website:header:end -->
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb">
@ -701,6 +705,7 @@ chmod +x ms_flag_checker
</article>
</div>
</main>
<!-- website:footer:start -->
<footer class="site-footer">
<div class="container footer-inner">
<p>REA · Reverse Engineer Anything</p>
@ -714,8 +719,9 @@ chmod +x ms_flag_checker
</div>
</footer>
<p class="sr-only" id="copy-status" role="status" aria-live="polite"></p>
<a class="back-to-top" href="#top" aria-label="Back to top"
><span aria-hidden="true">↑</span> Top</a
>
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
<!-- website:footer:end -->
</body>
</html>

View File

@ -8,6 +8,7 @@
name="description"
content="Follow a DX-Ball reconstruction from binary analysis through REA to maintained C, original-x86 tests and matching compiled function bytes."
/>
<!-- website:sharing:start -->
<link rel="canonical" href="https://rea.tools/showcase/dx-ball/" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="REA" />
@ -48,6 +49,7 @@
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../../assets/styles.css" />
@ -55,6 +57,7 @@
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../../" aria-label="REA home"
@ -62,9 +65,9 @@
><span class="brand-caption">Reverse Engineer Anything</span></a
>
<nav class="navigation" aria-label="Main navigation">
<a href="../">Showcases</a>
<a href="../../showcase/" aria-current="page">Showcases</a>
<a href="../../blog/">Blog</a>
<a class="nav-start" href="../../get-started/">Get started</a>
<a href="../../get-started/" class="nav-start">Get started</a>
<a href="../../guides/">Guides</a>
<a href="../../faq/">FAQ</a>
</nav>
@ -130,6 +133,7 @@
</div>
</div>
</header>
<!-- website:header:end -->
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb">
@ -709,6 +713,7 @@
</article>
</div>
</main>
<!-- website:footer:start -->
<footer class="site-footer">
<div class="container footer-inner">
<p>REA · Reverse Engineer Anything</p>
@ -725,5 +730,6 @@
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
<!-- website:footer:end -->
</body>
</html>

View File

@ -8,6 +8,7 @@
name="description"
content="Follow REA investigations through code, figures and demos: an Android authenticator, DX-Ball, Notion, TH04 and a DownUnderCTF flag checker."
/>
<!-- website:sharing:start -->
<link rel="canonical" href="https://rea.tools/showcase/" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="REA" />
@ -48,6 +49,7 @@
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../assets/styles.css" />
@ -55,6 +57,7 @@
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../" aria-label="REA home"
@ -62,9 +65,9 @@
><span class="brand-caption">Reverse Engineer Anything</span></a
>
<nav class="navigation" aria-label="Main navigation">
<a href="./" aria-current="page">Showcases</a>
<a href="../showcase/" aria-current="page">Showcases</a>
<a href="../blog/">Blog</a>
<a class="nav-start" href="../get-started/">Get started</a>
<a href="../get-started/" class="nav-start">Get started</a>
<a href="../guides/">Guides</a>
<a href="../faq/">FAQ</a>
</nav>
@ -130,6 +133,7 @@
</div>
</div>
</header>
<!-- website:header:end -->
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb"><a href="../">Home</a> / Showcases</p>
@ -241,6 +245,7 @@
<a class="text-link" href="../guides/">Read the guides</a>
</section>
</main>
<!-- website:footer:start -->
<footer class="site-footer">
<div class="container footer-inner">
<p>REA · Reverse Engineer Anything</p>
@ -257,5 +262,6 @@
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
<!-- website:footer:end -->
</body>
</html>

View File

@ -8,6 +8,7 @@
name="description"
content="Follow Notion's clipboard API through Electron's preload, IPC wrappers and main process, with short code excerpts and a look at its rich clipboard format."
/>
<!-- website:sharing:start -->
<link rel="canonical" href="https://rea.tools/showcase/notion/" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="REA" />
@ -17,7 +18,7 @@
/>
<meta
property="og:description"
content="Follow Notion's clipboard API through Electron's preload, IPC wrappers and main process, with short code excerpts and a look at its rich clipboard format."
content="Follow Notion&#x27;s clipboard API through Electron&#x27;s preload, IPC wrappers and main process, with short code excerpts and a look at its rich clipboard format."
/>
<meta property="og:url" content="https://rea.tools/showcase/notion/" />
<meta
@ -38,7 +39,7 @@
/>
<meta
name="twitter:description"
content="Follow Notion's clipboard API through Electron's preload, IPC wrappers and main process, with short code excerpts and a look at its rich clipboard format."
content="Follow Notion&#x27;s clipboard API through Electron&#x27;s preload, IPC wrappers and main process, with short code excerpts and a look at its rich clipboard format."
/>
<meta
name="twitter:image"
@ -48,6 +49,7 @@
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../../assets/styles.css" />
@ -55,6 +57,7 @@
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../../" aria-label="REA home"
@ -62,9 +65,9 @@
><span class="brand-caption">Reverse Engineer Anything</span></a
>
<nav class="navigation" aria-label="Main navigation">
<a href="../">Showcases</a>
<a href="../../showcase/" aria-current="page">Showcases</a>
<a href="../../blog/">Blog</a>
<a class="nav-start" href="../../get-started/">Get started</a>
<a href="../../get-started/" class="nav-start">Get started</a>
<a href="../../guides/">Guides</a>
<a href="../../faq/">FAQ</a>
</nav>
@ -130,6 +133,7 @@
</div>
</div>
</header>
<!-- website:header:end -->
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb">
@ -547,6 +551,7 @@ mimeTypes:
</article>
</div>
</main>
<!-- website:footer:start -->
<footer class="site-footer">
<div class="container footer-inner">
<p>REA · Reverse Engineer Anything</p>
@ -563,5 +568,6 @@ mimeTypes:
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
<!-- website:footer:end -->
</body>
</html>

View File

@ -8,6 +8,7 @@
name="description"
content="Inspect TH04's original 16-bit DOS code with REA. Follow the assembly and recovered C++ to see how a ring of bullets is spaced and aimed at the player."
/>
<!-- website:sharing:start -->
<link rel="canonical" href="https://rea.tools/showcase/th04/" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="REA" />
@ -17,7 +18,7 @@
/>
<meta
property="og:description"
content="Inspect TH04's original 16-bit DOS code with REA. Follow the assembly and recovered C++ to see how a ring of bullets is spaced and aimed at the player."
content="Inspect TH04&#x27;s original 16-bit DOS code with REA. Follow the assembly and recovered C++ to see how a ring of bullets is spaced and aimed at the player."
/>
<meta property="og:url" content="https://rea.tools/showcase/th04/" />
<meta
@ -38,7 +39,7 @@
/>
<meta
name="twitter:description"
content="Inspect TH04's original 16-bit DOS code with REA. Follow the assembly and recovered C++ to see how a ring of bullets is spaced and aimed at the player."
content="Inspect TH04&#x27;s original 16-bit DOS code with REA. Follow the assembly and recovered C++ to see how a ring of bullets is spaced and aimed at the player."
/>
<meta
name="twitter:image"
@ -48,6 +49,7 @@
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="../../assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="../../assets/styles.css" />
@ -55,6 +57,7 @@
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="../../" aria-label="REA home"
@ -62,9 +65,9 @@
><span class="brand-caption">Reverse Engineer Anything</span></a
>
<nav class="navigation" aria-label="Main navigation">
<a href="../">Showcases</a>
<a href="../../showcase/" aria-current="page">Showcases</a>
<a href="../../blog/">Blog</a>
<a class="nav-start" href="../../get-started/">Get started</a>
<a href="../../get-started/" class="nav-start">Get started</a>
<a href="../../guides/">Guides</a>
<a href="../../faq/">FAQ</a>
</nav>
@ -130,6 +133,7 @@
</div>
</div>
</header>
<!-- website:header:end -->
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb">
@ -724,6 +728,7 @@ no_aim:
</article>
</div>
</main>
<!-- website:footer:start -->
<footer class="site-footer">
<div class="container footer-inner">
<p>REA · Reverse Engineer Anything</p>
@ -740,5 +745,6 @@ no_aim:
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>
<!-- website:footer:end -->
</body>
</html>

View File

@ -9,8 +9,10 @@ evidence the reader can inspect, and a useful next step. A clean, refined
appearance comes from precise wording, readable figures, consistent typography
and deliberate spacing.
Use this guide for new pages and revisions. Start from an existing page and
reuse `public/assets/styles.css` and `public/assets/main.js`. Local preview and
Use this guide for new pages and revisions. Create the common page structure
with `python3 scripts/website.py new-page`, then reuse the relevant components
from an existing page, `public/assets/styles.css` and `public/assets/main.js`.
Keep shared regions in sync with `templates/`. Contribution, preview and
publication commands are in [README.md](README.md).
## What a reader should understand

View File

@ -0,0 +1,16 @@
<footer class="site-footer">
<div class="container footer-inner">
<p>REA · Reverse Engineer Anything</p>
<div class="footer-links">
<a href="https://github.com/morluto/rea">Source</a
><a href="https://github.com/morluto/rea/blob/main/LICENSE"
>MIT license</a
><a href="https://discord.gg/GkcryMnJDM">Join the community</a
><a href="https://x.com/reatools">Follow us on X</a>
</div>
</div>
</footer>
<p class="sr-only" id="copy-status" role="status" aria-live="polite"></p>
<a class="back-to-top" href="#top" aria-label="Back to top">
<span aria-hidden="true">↑</span> Top
</a>

View File

@ -0,0 +1,71 @@
<header class="site-header">
<div class="container header-inner">
<a class="brand" href="$base" aria-label="REA home"
><span class="brand-name">REA</span
><span class="brand-caption">Reverse Engineer Anything</span></a
>
<nav class="navigation" aria-label="Main navigation">
$navigation
</nav>
<div class="header-social">
<a
class="nav-github"
href="https://github.com/morluto/rea"
aria-label="REA on GitHub"
title="REA on GitHub"
>
<svg
width="22"
height="22"
viewBox="0 0 16 16"
aria-hidden="true"
focusable="false"
>
<path
fill="currentColor"
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82a7.65 7.65 0 0 1 2-.27c.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z"
/>
</svg>
<span class="nav-github-count" hidden aria-hidden="true"></span>
</a>
<a
class="nav-discord"
href="https://discord.gg/GkcryMnJDM"
aria-label="Join REA on Discord"
title="Join REA on Discord"
>
<svg
width="22"
height="22"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path
fill="currentColor"
d="M20.317 4.3698a19.7913 19.7913 0 0 0-4.8851-1.5152.0741.0741 0 0 0-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 0 0-.0785-.037 19.7363 19.7363 0 0 0-4.8852 1.515.0699.0699 0 0 0-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 0 0 .0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 0 0 .0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 0 0-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 0 1-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 0 1 .0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 0 1 .0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 0 1-.0066.1276 12.2986 12.2986 0 0 1-1.873.8914.0766.0766 0 0 0-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 0 0 .0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 0 0 .0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 0 0-.0312-.0286ZM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189Zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"
/>
</svg>
</a>
<a
class="nav-x"
href="https://x.com/reatools"
aria-label="Follow REA on X"
title="Follow REA on X"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
>
<path
fill="currentColor"
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
/>
</svg>
</a>
</div>
</div>
</header>

View File

@ -0,0 +1,36 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>$title</title>
<meta name="description" content="$description" />
<meta name="robots" content="noindex" />
<!-- website:sharing:start -->
<!-- website:sharing:end -->
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="${base}assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="${base}assets/styles.css" />
<script src="${base}assets/main.js" defer></script>
</head>
<body id="top">
<a class="skip-link" href="#main">Skip to content</a>
<!-- website:header:start -->
<!-- website:header:end -->
<main id="main" class="container">
<div class="page-heading">
<p class="breadcrumb"><a href="$base">Home</a> / Draft</p>
<h1>$title</h1>
<p class="lede">$description</p>
</div>
<article class="article">
<section class="article-section" id="question">
<h2>The question</h2>
<p>Introduce one concrete question, then show the evidence and result.</p>
</section>
</article>
</main>
<!-- website:footer:start -->
<!-- website:footer:end -->
</body>
</html>

View File

@ -0,0 +1,40 @@
<link rel="canonical" href="$canonical" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="REA" />
<meta
property="og:title"
content="$title"
/>
<meta
property="og:description"
content="$description"
/>
<meta property="og:url" content="$canonical" />
<meta
property="og:image"
content="$origin/assets/social-card.png"
/>
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta
property="og:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>
<meta name="twitter:card" content="summary_large_image" />
<meta
name="twitter:title"
content="$title"
/>
<meta
name="twitter:description"
content="$description"
/>
<meta
name="twitter:image"
content="$origin/assets/social-card.png"
/>
<meta
name="twitter:image:alt"
content="REA — Find out how software works. Reverse engineering with your coding agent."
/>

View File

@ -11,7 +11,7 @@ not_found_handling = "none"
[build]
cwd = ".."
command = "python3 scripts/prepare-website.py && python3 scripts/verify-website.py"
command = "python3 scripts/website.py check"
watch_dir = "./public/examples/notes-electron"
# Public HTTPS depends on zone activation and certificate readiness.