README.md
11.1 kB · markdown
ssg
- The site builder both sites run, stamped from the kit. One function renders one route; the rest is bookkeeping.
- Generic: it knows routes, inputs, bundles, fingerprints and manifests, never markdown, papers or products.
- The site brings its
site.json, acollect()that lists routes and arender()per route. md.tsis the markdown pipeline:render(md, { link, math, widget }),inline,sheet,front,title,summary,plain,slug,escape.links.tsis the one link resolver andpic.tsdraws a dark and light<picture>pair.- The one dispatch is
../git: agitblock insite.jsonmakesscanappend the repo's own/git/and/raw/routes, andrenderandfingerprintdispatch to that module; no block, nothing git-related runs. - The other dispatch is
blog.ts: abloginput makesscanappend/blog/and every post, andrenderdispatches to that module; no input, no routes. ../serve.tsmaps a path to a route throughrenderandglobals, and../dev.tsserves a site through it; neither reads a file by path, and a route lists inurlswhat it publishes beside its page or, as the slash route above, holds whatever is asked for under it.
SITE.JSON
title(orname) androot, then whatever the site's own chrome reads.inputs:{ name: { path, ext?, deep? } }. Declared, never assumed.site.input(name).filesreads them back.- The site resolves nothing by hand: an undeclared name throws, so every path a build reads is in one block.
kit:{ path, out, hash, files, ext }, one bundle copied intoout/;site.asset(name)gives the href, hashed whenhashis true.- A hashed
.jsfile has every relativeimportandimport()of another listed file rewritten to that file's hashed href, and a hashed.cssfile the same for everyurl()and@import, dependencies first, so the kit needs no bundler. - A cycle throws, and naming a file the bundle does not list throws unless an earlier bundle already placed it, which answers with that file's hashed href.
assets: more bundles of the same shape, for files that must keep their names, such asfonts/andseti/, whose CSS names its faces by relative url.manifest: the webmanifest, written as is.robots:{ disallow }, appended to the wildcard block alone.llms:{ about, links }, optional.aboutis the paragraph llms.txt opens on; a link is{ href, name, note }and is dropped unless the site publishes that route. No block, nollms.txt.
BLOG
blog.tsis the one blog both sites run: a post issite/blog/<slug>/index.mdwith every figure and file beside it.- The input is declared like any other,
"blog": { "path": "blog", "deep": true }; no input, no folder or an empty one and the site gets no routes at all. - Front matter is
title,date(YYYY-MM-DD) andlead; a missing or malformed one throws naming the post, and a slug that is not lowercase words joined by hyphens throws too. - A file loose in the blog folder throws: the shape is one folder per post, never a bare
<slug>.md. /blog/lists the posts newest first, then by slug;/blog/<slug>/is the post, and itslastmodis the front matter date.- Every file beside
index.mdships at/blog/<slug>/<path>with its content type on the output, sopush.tssets the S3 header without re-rendering. - Each one is named in
site.shipsat collect time, repo file to published URL, so the code viewer links that copy and writes no/raw/twin, whatever its type. - Those files ride a hidden route that never enters the sitemap, and each one is named in the link index, so a relative image in the body answers with its served path.
- A markdown sibling ships too but is never indexed, so a link to it falls through the resolver to the
/git/page where the site has one. leaf.imageis the og:image: the first figure in the body when it names a shipped file, its bare name when the body names a site figure, else empty.posts(site)is the parsed list, memoised per site, so a site's owncollectcan read it for its tree, its cards and its home page.- The module never imports the chrome, so
spec.blogcarries it:{ page, md }; nospec.blog.pagemeans the routes are collected and nothing is rendered. page(site, leaf)draws the page, listing and post alike:leaf.kindsays which,leaf.postsis every card, andleaf.outis the outputs the page may add beside itself.md(site, text, from, out)renders the body the site's way, withfromset to the post'sindex.md, so relative links and images resolve beside the post.
EXPORTS
scan(spec): runsprepare, readssite.jsonand the declared inputs, places every bundle, callscollect(), returnsSite.Site:root out config inputs kit routes nav stamp index copies styles serves made ships asset input bytes.copiesare the placed bundle files, whichglobalswrites;stylesis every placed.csshref, sorted, for a chrome that links them all.servesmaps a bundled source file to its href andmadeholds every path the build has written; the code viewer'sservedhook reads both.shipsmaps a repo file the build publishes byte for byte to that URL, filled at collect time, and the code viewer reads it before it asks the hook.render(site, route, spec): pure, returns[{ path, bytes, type? }]for that route alone. A missing input throws here.typeoverrides the content type a path would earn by its extension; it rides in the manifest so a push sets the S3 header without re-rendering.globals(site, spec): the copies, sitemap.xml, robots.txt, llms.txt, the webmanifest, the icons,git/tree.json, the public copy, then the site's own extras.- robots.txt allows everything: an
Allow: /block per named crawler (GPTBot, ClaudeBot, Claude-Web, CCBot, Google-Extended, anthropic-ai, PerplexityBot), then*, then the sitemap line. - sitemap.xml is one
<url>per entry in every route'surls,lastmodfrom the route'sat, so a group route fills the map with the pages it publishes. - llms.txt is the title, the site root, the
aboutparagraph and the declared links; nothing is listed by accident and no route writes itself in. fingerprint(site, route, spec?): sha256 of the route's input bytes, its data, the templates, the navigator and the link index. Never a date, never an absolute path.- A file is named by its declaration,
research/foo.md, not by where the tree sits; an undeclared file is named by its basename, a directory hashes every inner path and byte relative to itself. - So a checkout, a tarball and a lambda fingerprint the same bytes the same way, and one manifest serves them all.
build(spec, { manifest, force, verify }): scan, fingerprint, render only what changed, write only bytes that differ.- Then it drops the outputs of dead routes, prunes every folder that empties, and sweeps each bundle's
out/of any file this build did not write. verifyis on by default and re-renders a route whose outputs went missing fromout/; a build against a remote manifest turns it off, because there the disk is a scratch pad.walk bytes forget digest short escape page today guard label jsonText jsonScript: the small helpers a site would write twice.forget()drops the byte cache, which a watcher calls before it rescans.
SPEC
root out config templates prepare collect render globals inline icons git blog asset.templatesare the dirs whose bytes rebuild every route; the kit itself is always one, so a kit edit re-renders everything.prepareruns first, before the scan reads anything, for a site that bundles its client and then lists the bundle as an asset.collect(site)returns{ routes, nav? };navis the site tree the chrome draws and the code viewer's node joins it when the site has not placed one.inlinelists every inline script a page may carry;buildrefuses a page carrying one it does not know.icons:{ rows, svg }, a square glyph grid of0/1strings and the favicon svg; the builder writes the svg asfavicon.svgand drawsfavicon.png,apple-touch-icon.png,icon-192.pngandicon-512.pngfrom the grid. No field, no icons.gitis the code viewer's hooks,{ page, md, code, served }: the chrome, the markdown pipeline, the highlighter and the mirror seam the module cannot know by itself.blogis the blog's hooks,{ page, md }, the same seam forblog.ts.asset(name, body)may rewrite a bundle file's bytes before it is hashed and placed.Route:{ route, kind, name, data, source, inputs, urls, at, hidden, sitemap }.hiddenkeeps a route out of the navigator and out of every list a reader browses;sitemapputs it back on the map anyway.- The code viewer sets both, so a thousand pages the tree never shows are still crawlable;
/404.htmlsets onlyhiddenand stays off. - One route may be a group:
urlslists the pages it publishes, so a bundler route and a code page still fill the sitemap. rendermay be async, so a route can run a bundler and hand back its bytes before anything is written.- The manifest is
{ route: { hash, at, outputs, types? } }and lives wherever the caller points it. atis the route's own date, else the manifest's while the hash holds, else today, so a tree with no git keeps the dates it was given.
LINKS
resolve(site, from, url)inlinks.tsis the one resolver: every markdown render sends its links through it, andfromis the file the link is written in, absolute or relative to the repo root.https:,http:,mailto:,tel:, a bare#fragmentand a rooted/pathpass through untouched; everything else is a path.- The path resolves against the directory of
from, and its#fragmentor?queryis set aside and put back on whatever the resolver answers. scan()builds the index once per build: every route'ssource, and everysourcea route names in itsurls, mapped to that route under both its absolute path and its declared name, soresearch/core.mdanddemos/spinare keys as much as the full paths are.- A route that wants to be found by a link names the input it publishes in
source; a group route names one per page inurls, which is how a shelf hands each folder its own route. - The index is asked first, for the path, the path plus
.mdand the path with.mdstripped, sobases.md,basesand../demos/spin/all land on the route the site publishes. - A miss falls to the repo:
/raw/<path>for an image or a PDF,/git/<path>for a file,/git/<path>/for a directory, and only when the code viewer carries that route. - A site with git routes but no slug stops there; a site with no git routes falls to
https://github.com/<slug>/blob/<branch>/<path>whensite.jsonnames one, and a site with no git block leaves the link as written. - Anything else is left exactly as written: a target outside the repo, a paper fetched from another tree, a path nothing publishes.
stamp(index)is the index as one string, keyed relative to the repo root, and it rides in every fingerprint, so a page re-renders when a route it could link to appears, renames or disappears.- So the resolver never asks which page is doing the reading, only which file the link was written in, and one README answers the same under
/git/and under/research/.