From dce8f26ccfcc6d182c54c618ae907b5dd61f4bbd Mon Sep 17 00:00:00 2001 From: Tiago Moraes Date: Tue, 28 Jul 2026 15:04:52 -0300 Subject: [PATCH] feat(site): drop .html from the routes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pages already resolves /docs to docs.html — it answers 200 with the page, no redirect — so the site was linking to an implementation detail it never had to expose. Links, canonicals, og:urls and the sitemap now use /claude and /docs. The old .html URLs keep answering, so anything already linked, including the Community listing, still lands. Two things had to follow the routes rather than break behind them. CI's link check resolved an href against the literal path only, so every extensionless link would have failed the deploy; it now accepts the .html twin. And `npm run site` was python's http.server, which does not do that resolution at all, so the nav would have 404'd locally while working in production — it is now a small node server that resolves a request the way Pages does. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NJ5rqmRHgZn76zd5ytr2Zh --- .github/workflows/site.yml | 4 ++- DESIGN.md | 6 ++-- LISTING.md | 2 +- README.md | 2 +- package.json | 2 +- serve-site.mjs | 63 ++++++++++++++++++++++++++++++++++++++ site/assets/css/claude.css | 2 +- site/assets/css/tokens.css | 2 +- site/claude.html | 16 +++++----- site/docs.html | 14 ++++----- site/index.html | 12 ++++---- site/sitemap.xml | 4 +-- 12 files changed, 97 insertions(+), 32 deletions(-) create mode 100644 serve-site.mjs diff --git a/.github/workflows/site.yml b/.github/workflows/site.yml index a7fb86a..7d15878 100644 --- a/.github/workflows/site.yml +++ b/.github/workflows/site.yml @@ -40,11 +40,13 @@ jobs: cd site fail=0 # Local hrefs only: strip anchors and query, map "/" to index.html. + # Routes are extensionless — Pages serves /docs from docs.html — so a + # link resolves if either the literal path or its .html twin exists. for f in $(find . -name '*.html'); do grep -oE 'href="/[^"#?]*"' "$f" | sed 's/href="//;s/"//' | sort -u | while read -r href; do target=".${href}" case "$href" in */) target=".${href}index.html" ;; esac - if [ ! -e "$target" ]; then + if [ ! -e "$target" ] && [ ! -e "${target}.html" ]; then echo "::error file=site/${f#./}::broken internal link -> $href" exit 1 fi diff --git a/DESIGN.md b/DESIGN.md index 2660adc..ea7cbc1 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -80,11 +80,11 @@ them carries meaning. `site.css` never names a ground colour again. **Clay is the third meaning.** tofig's biggest use case is a design Claude generated, and that journey has two halves. Violet is the tofig/Figma side, clay is the Claude side, and the handoff is where they meet. It appears on the -Claude section of the landing page and on `/claude.html`, and nowhere else. +Claude section of the landing page and on `/claude`, and nowhere else. `--c-500` on `--void` measures 5.2:1. White on `--c-600` measures only 4.17:1, so the interactive fill is `--c-700`, the same correction violet needed. -**On `/claude.html` the handoff runs at page scale.** The atmospheric field +**On `/claude` the handoff runs at page scale.** The atmospheric field is a background layer on the body rather than a fixed overlay, so it is measured against the whole document: clay pools where the work starts, drains through the middle, and violet gathers where the file lands. The @@ -118,7 +118,7 @@ White on `--v-500` measures 4.49:1 — one step under AA. Interactive fills therefore sit on `--v-600` and move *up* to `--v-500` on hover. **Tint intensity is a share of the gamut, not a chroma number.** -`/claude.html` shifts its whole neutral ramp to clay's side of the wheel, +`/claude` shifts its whole neutral ramp to clay's side of the wheel, and the first version did it by copying violet's chroma figures across to hue 38. The page came out brown. The sRGB chroma ceiling is not the same at every hue: at L 0.20 it is 0.118 at 288 and only 0.063 at 40, so the diff --git a/LISTING.md b/LISTING.md index dc9522b..52ce063 100644 --- a/LISTING.md +++ b/LISTING.md @@ -147,7 +147,7 @@ Still fully local: it drives your own Chrome, it just isn't sandbox-restricted. Free and open source, MIT licensed. Built and maintained by acaso. Site: tofig.aca.so -Claude → Figma walkthrough: tofig.aca.so/claude.html +Claude → Figma walkthrough: tofig.aca.so/claude Code: github.com/aca-so/tofig Built on @builder.io/html-to-figma (MIT); bundles React/ReactDOM (MIT). ``` diff --git a/README.md b/README.md index 229949d..a9e47c3 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ No server, no browser extension, no account — just a Figma plugin. there. Then paste your HTML and hit **Convert**. Reopen it later with right-click → **Plugins** → **tofig**. -Guides for using it, including the [Claude walkthrough](https://tofig.aca.so/claude.html), +Guides for using it, including the [Claude walkthrough](https://tofig.aca.so/claude), are at [tofig.aca.so](https://tofig.aca.so). The rest of this README is about building and running it from source. diff --git a/package.json b/package.json index b055cb1..152fd1e 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "build": "node esbuild.mjs", "watch": "node esbuild.mjs --watch", "render": "node bin/tofig-render.mjs", - "site": "python3 -m http.server 8777 --bind 127.0.0.1 --directory site", + "site": "node serve-site.mjs", "typecheck": "tsc --noEmit -p tsconfig.json", "lint": "eslint ." }, diff --git a/serve-site.mjs b/serve-site.mjs new file mode 100644 index 0000000..7dfa277 --- /dev/null +++ b/serve-site.mjs @@ -0,0 +1,63 @@ +// Local preview for site/, matching how GitHub Pages resolves a request. +// +// It exists because the routes are extensionless: Pages serves /docs from +// docs.html, and `python3 -m http.server` does not, so the nav links that work +// in production 404'd locally. A preview server that disagrees with the deploy +// about what a URL means is worse than no preview server. +// +// node serve-site.mjs [port] +import { createServer } from "node:http"; +import { readFile, stat } from "node:fs/promises"; +import { extname, join, normalize } from "node:path"; + +const ROOT = new URL("./site/", import.meta.url).pathname; +const PORT = Number(process.argv[2]) || 8777; + +const TYPES = { + ".html": "text/html; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".js": "text/javascript; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".svg": "image/svg+xml", + ".png": "image/png", + ".jpg": "image/jpeg", + ".webp": "image/webp", + ".woff2": "font/woff2", + ".xml": "application/xml; charset=utf-8", + ".txt": "text/plain; charset=utf-8", +}; + +const isFile = async (p) => { + try { + return (await stat(p)).isFile(); + } catch { + return false; + } +}; + +// Pages' order: the literal path, then its .html twin, then a directory index. +async function resolve(pathname) { + const rel = normalize(decodeURIComponent(pathname)).replace(/^(\.\.[/\\])+/, ""); + const base = join(ROOT, rel); + for (const candidate of [base, `${base}.html`, join(base, "index.html")]) { + if (await isFile(candidate)) return candidate; + } + return null; +} + +createServer(async (req, res) => { + const { pathname } = new URL(req.url, `http://localhost:${PORT}`); + const file = await resolve(pathname); + if (!file) { + res.writeHead(404, { "content-type": "text/plain; charset=utf-8" }); + res.end(`404 ${pathname}\n`); + return; + } + res.writeHead(200, { + "content-type": TYPES[extname(file)] ?? "application/octet-stream", + "cache-control": "no-store", + }); + res.end(await readFile(file)); +}).listen(PORT, "127.0.0.1", () => { + console.log(`tofig.aca.so preview → http://127.0.0.1:${PORT}`); +}); diff --git a/site/assets/css/claude.css b/site/assets/css/claude.css index 191683f..3ce2143 100644 --- a/site/assets/css/claude.css +++ b/site/assets/css/claude.css @@ -1,5 +1,5 @@ /* ────────────────────────────────────────────────────────────────── - tofig.aca.so/claude.html — Claude to Figma + tofig.aca.so/claude — Claude to Figma Page-scoped layer on top of tokens.css + site.css. Everything here is prefixed `cl-` or scoped under `.p-claude`, so nothing leaks diff --git a/site/assets/css/tokens.css b/site/assets/css/tokens.css index d2bbdf1..e99cbb7 100644 --- a/site/assets/css/tokens.css +++ b/site/assets/css/tokens.css @@ -41,7 +41,7 @@ --void rather than on it, and the nav wash sits level with it. They are tokens rather than literals because a page may move its - whole neutral ramp to another hue — /claude.html rides clay's 44 + whole neutral ramp to another hue — /claude rides clay's 44 instead of violet's 288 — and a component that hardcodes one hue turns into a cold rectangle on a warm page. Every sunken surface takes its ground from here; only the accent painted on top of it diff --git a/site/claude.html b/site/claude.html index 88d3731..fc30a90 100644 --- a/site/claude.html +++ b/site/claude.html @@ -5,10 +5,10 @@ Claude to Figma | tofig - + - + @@ -34,7 +34,7 @@ "@type": "TechArticle", "headline": "Claude to Figma: turn a generated design into editable layers", "description": "How to export a Claude artifact as self-contained HTML and rebuild it in Figma as native, editable layers with tofig.", - "url": "https://tofig.aca.so/claude.html", + "url": "https://tofig.aca.so/claude", "inLanguage": "en", "author": { "@type": "Organization", "name": "acaso", "url": "https://aca.so" }, "publisher": { "@type": "Organization", "name": "acaso", "url": "https://aca.so" }, @@ -78,11 +78,11 @@

@@ -605,11 +605,11 @@

Renderer CLI

Source

diff --git a/site/docs.html b/site/docs.html index 984b3d7..8255db6 100644 --- a/site/docs.html +++ b/site/docs.html @@ -5,9 +5,9 @@ Documentation | tofig - + - + @@ -50,11 +50,11 @@