From 5aa30b8b867836df578639e824d8b47bdc54f861 Mon Sep 17 00:00:00 2001 From: N0zoM1z0 <161784452+N0zoM1z0@users.noreply.github.com> Date: Sat, 10 Oct 2026 19:56:37 +0800 Subject: [PATCH] chore(website): simplify maintenance and detect documentation drift --- .github/workflows/website-check.yml | 8 +- .github/workflows/website-pages.yml | 6 +- scripts/audit-website-links.py | 135 ++++++++++ scripts/prepare-website.py | 37 +-- scripts/test-website.py | 116 +++++++++ scripts/verify-website.py | 121 +++++++-- scripts/website.py | 245 ++++++++++++++++++ website/AGENTS.md | 16 +- website/README.md | 98 +++++-- website/public/blog/index.html | 10 +- .../blog/touhou-reconstruction/index.html | 26 +- website/public/examples/dino-lab/index.html | 19 +- website/public/faq/index.html | 16 +- website/public/first-investigation/index.html | 19 +- website/public/get-started/index.html | 51 +++- website/public/guides/browser/index.html | 23 +- website/public/guides/index.html | 23 +- website/public/guides/javascript/index.html | 13 +- website/public/guides/native/index.html | 23 +- website/public/index.html | 16 +- website/public/showcase/aegis/index.html | 16 +- website/public/showcase/ctf/index.html | 20 +- website/public/showcase/dx-ball/index.html | 10 +- website/public/showcase/index.html | 10 +- website/public/showcase/notion/index.html | 14 +- website/public/showcase/th04/index.html | 14 +- website/style-guide.md | 6 +- website/templates/footer.html | 16 ++ website/templates/header.html | 71 +++++ website/templates/page.html | 36 +++ website/templates/sharing.html | 40 +++ website/wrangler.toml | 2 +- 32 files changed, 1108 insertions(+), 168 deletions(-) create mode 100644 scripts/audit-website-links.py create mode 100644 scripts/website.py create mode 100644 website/templates/footer.html create mode 100644 website/templates/header.html create mode 100644 website/templates/page.html create mode 100644 website/templates/sharing.html diff --git a/.github/workflows/website-check.yml b/.github/workflows/website-check.yml index 26270ce73..676a9c83a 100644 --- a/.github/workflows/website-check.yml +++ b/.github/workflows/website-check.yml @@ -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 diff --git a/.github/workflows/website-pages.yml b/.github/workflows/website-pages.yml index 7dec1ea47..3d20f7ea1 100644 --- a/.github/workflows/website-pages.yml +++ b/.github/workflows/website-pages.yml @@ -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' diff --git a/scripts/audit-website-links.py b/scripts/audit-website-links.py new file mode 100644 index 000000000..cff49da0b --- /dev/null +++ b/scripts/audit-website-links.py @@ -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()) diff --git a/scripts/prepare-website.py b/scripts/prepare-website.py index af4f2d9b9..848b6fbc4 100644 --- a/scripts/prepare-website.py +++ b/scripts/prepare-website.py @@ -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 diff --git a/scripts/test-website.py b/scripts/test-website.py index 121dec8ae..43d913548 100644 --- a/scripts/test-website.py +++ b/scripts/test-website.py @@ -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('
", 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('
", 1)[0], body) + self.assertEqual(WEBSITE.sync_pages(root), []) + page.write_text(updated.replace(' ', '', 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 & "result"', 'Check "quotes" & .') + source = page.read_text() + self.assertIn('href="../../blog/" aria-current="page"', source) + self.assertIn('href="../../assets/styles.css"', source) + self.assertIn("A <test> & "result"", 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(' \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 = ( + ('Broken', "missing local target"), + ('https://rea.tools/guides/native/#deleted', "missing HTML fragment"), + ('https://morluto.github.io/rea/examples/notes-web/', "obsolete website URL"), + ('', "missing copy target"), + ('Malformed', "invalid URL"), + ) + for inserted, message in cases: + with self.subTest(message=message): + page.write_text(original.replace("
", inserted + "
", 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("

Guide

") + (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"

Guide

") + 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() diff --git a/scripts/verify-website.py b/scripts/verify-website.py index d67a89284..889d77394 100644 --- a/scripts/verify-website.py +++ b/scripts/verify-website.py @@ -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 diff --git a/scripts/website.py b/scripts/website.py new file mode 100644 index 000000000..77099d5bf --- /dev/null +++ b/scripts/website.py @@ -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' {label}') + 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" \n" + end = f" " + 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" \n", f" \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()) diff --git a/website/AGENTS.md b/website/AGENTS.md index e97181ec6..92824a294 100644 --- a/website/AGENTS.md +++ b/website/AGENTS.md @@ -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 diff --git a/website/README.md b/website/README.md index 298bc4b80..d5fbf121d 100644 --- a/website/README.md +++ b/website/README.md @@ -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 . 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 +. + +## 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 `` 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 diff --git a/website/public/blog/index.html b/website/public/blog/index.html index 90bf245ad..e56e26449 100644 --- a/website/public/blog/index.html +++ b/website/public/blog/index.html @@ -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> diff --git a/website/public/blog/touhou-reconstruction/index.html b/website/public/blog/touhou-reconstruction/index.html index 10201ec25..0343995ad 100644 --- a/website/public/blog/touhou-reconstruction/index.html +++ b/website/public/blog/touhou-reconstruction/index.html @@ -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> diff --git a/website/public/examples/dino-lab/index.html b/website/public/examples/dino-lab/index.html index 02452bda9..63165d5a4 100644 --- a/website/public/examples/dino-lab/index.html +++ b/website/public/examples/dino-lab/index.html @@ -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> diff --git a/website/public/faq/index.html b/website/public/faq/index.html index 3ab51b9d3..893534374 100644 --- a/website/public/faq/index.html +++ b/website/public/faq/index.html @@ -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> diff --git a/website/public/first-investigation/index.html b/website/public/first-investigation/index.html index 0db427248..8c4414dc5 100644 --- a/website/public/first-investigation/index.html +++ b/website/public/first-investigation/index.html @@ -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> diff --git a/website/public/get-started/index.html b/website/public/get-started/index.html index cc1070c92..050232525 100644 --- a/website/public/get-started/index.html +++ b/website/public/get-started/index.html @@ -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> diff --git a/website/public/guides/browser/index.html b/website/public/guides/browser/index.html index 1611ecba3..859faceab 100644 --- a/website/public/guides/browser/index.html +++ b/website/public/guides/browser/index.html @@ -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> diff --git a/website/public/guides/index.html b/website/public/guides/index.html index 0ac92c277..610a5d172 100644 --- a/website/public/guides/index.html +++ b/website/public/guides/index.html @@ -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> diff --git a/website/public/guides/javascript/index.html b/website/public/guides/javascript/index.html index da04c775b..d4cae40de 100644 --- a/website/public/guides/javascript/index.html +++ b/website/public/guides/javascript/index.html @@ -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> diff --git a/website/public/guides/native/index.html b/website/public/guides/native/index.html index 304c2e9f0..3bb436ead 100644 --- a/website/public/guides/native/index.html +++ b/website/public/guides/native/index.html @@ -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> diff --git a/website/public/index.html b/website/public/index.html index 0f618cc93..0e90f4449 100644 --- a/website/public/index.html +++ b/website/public/index.html @@ -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> diff --git a/website/public/showcase/aegis/index.html b/website/public/showcase/aegis/index.html index de2227842..81087b367 100644 --- a/website/public/showcase/aegis/index.html +++ b/website/public/showcase/aegis/index.html @@ -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> diff --git a/website/public/showcase/ctf/index.html b/website/public/showcase/ctf/index.html index 42084b828..f4278c941 100644 --- a/website/public/showcase/ctf/index.html +++ b/website/public/showcase/ctf/index.html @@ -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'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'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> diff --git a/website/public/showcase/dx-ball/index.html b/website/public/showcase/dx-ball/index.html index 04926e8ba..1b6d6eae1 100644 --- a/website/public/showcase/dx-ball/index.html +++ b/website/public/showcase/dx-ball/index.html @@ -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> diff --git a/website/public/showcase/index.html b/website/public/showcase/index.html index 2329ee42f..204141cf3 100644 --- a/website/public/showcase/index.html +++ b/website/public/showcase/index.html @@ -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> diff --git a/website/public/showcase/notion/index.html b/website/public/showcase/notion/index.html index 161cb1204..7ee0857b0 100644 --- a/website/public/showcase/notion/index.html +++ b/website/public/showcase/notion/index.html @@ -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's clipboard API through Electron'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's clipboard API through Electron'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> diff --git a/website/public/showcase/th04/index.html b/website/public/showcase/th04/index.html index 701065aaf..4f1490716 100644 --- a/website/public/showcase/th04/index.html +++ b/website/public/showcase/th04/index.html @@ -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'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'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> diff --git a/website/style-guide.md b/website/style-guide.md index 77f517784..6c1f70060 100644 --- a/website/style-guide.md +++ b/website/style-guide.md @@ -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 diff --git a/website/templates/footer.html b/website/templates/footer.html new file mode 100644 index 000000000..db66ffc5c --- /dev/null +++ b/website/templates/footer.html @@ -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> diff --git a/website/templates/header.html b/website/templates/header.html new file mode 100644 index 000000000..10d08a081 --- /dev/null +++ b/website/templates/header.html @@ -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> diff --git a/website/templates/page.html b/website/templates/page.html new file mode 100644 index 000000000..36741f39a --- /dev/null +++ b/website/templates/page.html @@ -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

+

$description

+
+
+
+

The question

+

Introduce one concrete question, then show the evidence and result.

+
+
+
+ + + + diff --git a/website/templates/sharing.html b/website/templates/sharing.html new file mode 100644 index 000000000..dc4a4c69e --- /dev/null +++ b/website/templates/sharing.html @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + diff --git a/website/wrangler.toml b/website/wrangler.toml index 7ebf92a64..f00a67ff1 100644 --- a/website/wrangler.toml +++ b/website/wrangler.toml @@ -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.