tosijs-ui/site — static, pre-rendered, hydrating doc sites
A build system that turns a project's markdown (.md files + /*# block
comments in source) into a fast, SEO/AI-friendly documentation site that
works with no JavaScript and then upgrades itself into the interactive
<tosi-doc-system> doc browser when the bundle loads.
The output is a plain folder of static files — drop it on GitHub Pages, Firebase Hosting, or any static host.
Status: extraction in progress. The runtime component lives here in
src/doc-system/; the build tooling still lives inbin/(site-config.ts,generate-site.ts,generate-css.ts,generate-og.ts,build-dom-shim.ts,docs.ts) and is being consolidated intosrc/doc-system/behind the importabletosijs-ui/siteentry described here. See "Current layout" at the bottom.
What you get
- One pre-rendered
/{slug}/index.htmlper doc (README → site root) with real<head>metadata:<title>, description, canonical, Open Graph, Twitter card, andschema.orgTechArticleJSON-LD. - No-JS readable: the markdown is already rendered to HTML and every nav
item is a real
<a>, so crawlers and AI agents see full content and links. - Zero-flash hydration: the theme is burned into a static stylesheet, so
pages are styled before any JS runs; then
<tosi-doc-system>hydrates the page into the live doc browser (search, live examples, locale switching). sitemap.xml+robots.txt(whenbaseUrlis set), and host files (.nojekyll,CNAME, …) appropriate to the chosen host.
How it works (pipeline)
extractDocs(docPaths) → docs.json (markdown corpus)
generateSite(config, docs) → /{slug}/index.html + docs.json + sitemap + robots
generate-css(theme) → doc-system.css (burned-in, no FOUC)
bundle(bundleEntry | iife.js) → the JS that hydrates the pages
host preset → .nojekyll / CNAME / firebase.json
Static and hydrated output share the same slug + markdown rendering
(src/doc-system/routing + render) so the page never reflows on hydration.
Quick start (adopting in your project)
1. site.config.ts at your repo root:
import { defineSiteConfig } from 'tosijs-ui/site'
export default defineSiteConfig({
name: 'my-lib',
description: 'What my library does.',
baseUrl: 'https://my-lib.example.com',
host: 'github-pages', // emits .nojekyll + CNAME (domain from baseUrl)
bundleEntry: 'demo/site.ts', // omit to use tosijs-ui's published iife.js
navbarLinks: [
{ href: 'https://github.com/me/my-lib', label: 'github', icon: 'github' },
],
})
2. bin/site.ts — the only build file you write:
import { buildSite, devServer } from 'tosijs-ui/site'
import config from '../site.config'
process.argv.includes('--build') ? buildSite(config) : devServer(config)
3. scripts in package.json:
{ "scripts": { "start": "bun bin/site.ts", "build": "bun bin/site.ts --build" } }
Bundles & live examples (read this)
The static pages are inert HTML until a JS bundle loads and registers the
custom elements (and powers live js/test examples). You pick one of two
modes:
bundleEntry— bring your own (recommended for any project with custom components). The build bundles your entrypoint to IIFE and pages load it. Your entrypoint must import everything your pages and live examples reference — typically:// demo/site.ts import 'tosijs-ui' // registers tosi-* elements + the doc-system import * as tosijs from 'tosijs' import * as mylib from '../src/index' // your own components // expose anything your inline examples import: globalThis.tosijs = tosijs globalThis.mylib = mylibWithout these imports your custom elements won't upgrade and live examples that
import { x } from 'mylib'won't resolve.scriptUrlfallback — use a prebuilt bundle. OmitbundleEntryand pages loadscriptUrl(default/iife.js, i.e. tosijs-ui's published bundle). Good for a pure docs site with no custom elements of its own.
Configuration reference
All fields are optional except name. See bin/site-config.ts for the
authoritative typed definition.
Identity & SEO
| field | default | purpose |
|---|---|---|
name |
— | brand name; <title> suffix, og:site_name |
description |
— | site-level meta + structured-data fallback |
baseUrl |
— | absolute origin for canonical/OG/sitemap URLs |
lang |
'en' |
<html lang> |
favicon |
/favicon.svg |
favicon href |
ogImage |
— | default share image (per-page overridable) |
headExtra |
— | raw lines injected into every <head> |
Branding & chrome
| field | default | purpose |
|---|---|---|
projectLinks |
— | logo + view-source links |
navbarLinks |
— | header-bar icon links |
theme |
— | base colors (palette derived from accent) |
localizedStrings |
— | TSV table for the language picker |
Doc sources
| field | default | purpose |
|---|---|---|
docPaths |
['src', 'README.md'] |
dirs scanned for /*# + .md files (list root .md files explicitly) |
Bundle
| field | default | purpose |
|---|---|---|
bundleEntry |
— | your IIFE entrypoint; omit to use the fallback bundle |
bundleExternals |
— | modules left external, e.g. ['jolt-physics'] |
scriptUrl |
/iife.js |
bundle URL pages load (fallback + output name) |
Static assets
| field | default | purpose |
|---|---|---|
staticDirs |
['demo/static'] or ['static'] |
dirs copied to the web root |
Hosting
| field | default | purpose |
|---|---|---|
host |
'static' |
'github-pages' | 'firebase' | 'static' preset |
domain |
derived from baseUrl |
custom domain → CNAME (github-pages); implies basePath: '/' |
basePath |
'/' |
URL prefix; set '/<repo>' for a GitHub project page without a custom domain |
Build toggles & dev server
| field | default | purpose |
|---|---|---|
generateIcons |
false |
run icon-data generation (tosijs-ui-specific) |
llmsTxt |
true |
emit llms.txt discoverability index |
outputDir |
'docs' |
served web-root output dir |
port |
8787 |
dev-server port |
watchPaths |
— | extra dev-server watch dirs |
Host presets & custom domains
host |
.nojekyll |
CNAME |
basePath |
other |
|---|---|---|---|---|
github-pages + domain |
✅ | domain |
/ |
— |
github-pages, no domain |
✅ | — | set '/<repo>' yourself |
— |
firebase |
— | — | / |
optional firebase.json rewrites |
static (default) |
— | — | / |
nothing host-specific |
domain is derived from baseUrl's hostname when omitted (and
host: 'github-pages'), so the common case needs no extra config; set it
explicitly to override (apex vs www, or a domain that differs from the
canonical origin). A custom domain always serves from root, so it forces
basePath: '/'.
Doc format
.mdfiles are included whole./*#…*/block comments in.ts/.js/.cssare extracted as markdown. The first heading is the page title.- Metadata via a JSON block —
<!--{ "pin": "top" }-->(html) or/*{ "pin": "bottom" }*/(ts/js/css) — controls nav ordering; per-page SEO overrides (description,keywords,image,noindex,headTitle) live in the same block. - Consecutive
js/html/css/testcode blocks become one live example (see the main project's "Live example code blocks" docs).
Notes & gotchas
- Build-time only. The orchestrator and generators run under Bun and never
enter a browser bundle. Only the runtime
<tosi-doc-system>component ships to the page (and is tree-shaken away for consumers that don't use it). - Dependency direction for
tosijsitself. If the coretosijsrepo uses this to build its docs, that's a build-time-only dependency on tosijs-ui — the publishedtosijslibrary still depends on nothing upstream. It is not circular, but CI must build/resolve tosijs-ui first. - Not every site fits. This is for reference/doc sites built from markdown.
A bespoke scroll-driven marketing page (e.g.
tosijs-product) wants a different page model — use tosijs-ui's components there, not this doc system (or host its API docs as a separate site).
Current layout (extraction in progress)
| concern | today | target |
|---|---|---|
config type + defineSiteConfig |
bin/site-config.ts |
src/site/ |
| orchestrator (prebuild/build/serve) | bin/dev.ts |
src/site/ (buildSite/devServer) |
| static page generator | bin/generate-site.ts |
src/site/ |
| theme → static CSS | bin/generate-css.ts |
src/site/ |
| OG image generation | bin/generate-og.ts |
src/site/ |
| doc extraction | bin/docs.ts |
src/site/ |
| runtime component | src/doc-system/ |
unchanged (ships in the bundle) |