From 24245438cf2a456dfe1e520fa4047f76d7bcccc7 Mon Sep 17 00:00:00 2001 From: "shoney.arickathil" Date: Tue, 25 Aug 2026 04:05:00 +0200 Subject: [PATCH] feat(site): logo, favicon, install guide, package catalogue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - layout/logo.wo: the mark as inline SVG — dark tile, two-stroke "W" (white then accent blue). One source: nav brand + /favicon.svg - favicon/controller.wo: GET /favicon.svg, image/svg+xml, day cache - install/: GET /install — toolchain tarball, PATH, verify, first project, build/run, adding a dep. Copy from the real install README - packages/: GET /packages + /packages/:name — catalogue with the [deps] line, what each library gives you, and a usage snippet. Index cards are child components (multi Component) - wo-html: page_head(title, head, body) and a `head` slot on Layout — a favicon link or meta tag had nowhere else to go; page() passes "" - header: Install/Tutorial/Packages/GitHub, brand shows the mark - main.wo: SITE_HOST picks the interface (loopback default), bound address printed at startup - site-accept: 11 -> 17 checks (install, packages x2, 404, favicon, inline logo) Verified on 192.168.0.165:8080 — every route, favicon bytes, and the mark rasterised at 256px and 32px. Gates: oop-accept MET, site 17/0, web-app 46/0, fibers 10/0, db-actor 8/0; shop rebuilt clean. Co-Authored-By: Claude Opus 5 (1M context) --- docs/examples/site/CODE-LOGIC.md | 19 ++++- docs/examples/site/README.md | 17 ++++- docs/examples/site/favicon/controller.wo | 13 ++++ docs/examples/site/install/controller.wo | 12 +++ docs/examples/site/install/view.wo | 73 +++++++++++++++++++ docs/examples/site/layout/app.wo | 1 + docs/examples/site/layout/header.wo | 10 ++- docs/examples/site/layout/logo.wo | 23 ++++++ docs/examples/site/main.wo | 20 ++++- docs/examples/site/packages/controller.wo | 89 +++++++++++++++++++++++ docs/examples/site/packages/view.wo | 61 ++++++++++++++++ docs/examples/wo-html/README.md | 7 +- docs/examples/wo-html/html.wo | 19 ++++- scripts/site-accept.sh | 6 ++ 14 files changed, 357 insertions(+), 13 deletions(-) create mode 100644 docs/examples/site/favicon/controller.wo create mode 100644 docs/examples/site/install/controller.wo create mode 100644 docs/examples/site/install/view.wo create mode 100644 docs/examples/site/layout/logo.wo create mode 100644 docs/examples/site/packages/controller.wo create mode 100644 docs/examples/site/packages/view.wo diff --git a/docs/examples/site/CODE-LOGIC.md b/docs/examples/site/CODE-LOGIC.md index bb70c53..00cf6fc 100644 --- a/docs/examples/site/CODE-LOGIC.md +++ b/docs/examples/site/CODE-LOGIC.md @@ -11,7 +11,11 @@ samples now read the same way. | `types.wo` | MODEL | the `Chapter` `@table`, the `ChapterLink` projection, `Chapters.links()`, and `seed_if_empty()` | | `content.wo` | MODEL (content) | the nine chapter bodies as fragment-returning functions, plus `seed_chapters()` | | `layout/app.wo` | VIEW (chrome) | `AppShell` — the component that fills wo-html's `Layout` — the two named widths, and `html_error` | -| `layout/header.wo`, `layout/footer.wo` | VIEW (chrome) | the shared nav bar and footer | +| `layout/header.wo`, `layout/footer.wo` | VIEW (chrome) | the shared nav bar (brand = mark + wordmark) and footer | +| `layout/logo.wo` | VIEW (chrome) | the mark as inline SVG, plus the `` links | +| `install/view.wo`, `install/controller.wo` | VIEW + CONTROLLER | `/install` — the toolchain guide. Static copy, so `InstallPage` has no fields | +| `packages/view.wo`, `packages/controller.wo` | VIEW + CONTROLLER | `/packages` and `/packages/:name` — the catalogue, its cards, and per-package usage | +| `favicon/controller.wo` | CONTROLLER | `/favicon.svg` — builds its own `Resp` (image/svg+xml) | | `home/view.wo` | VIEW | `HomePage` and the homepage's code showcase | | `home/controller.wo` | CONTROLLER | `Home` — the `/` handler | | `chapter/view.wo` | VIEW | `ChapterNav`, `ChapterPage` | @@ -73,6 +77,19 @@ class — recorded gap #1 — but nothing needs it to.) removed at compile time, `${ }` raw and `{{ }}` auto-escaping. The old "`..` does not straddle newlines, so build accumulator-style" note is obsolete and was removed. +- **The logo is inline SVG, authored once.** `logo_svg(px)` goes in the + nav brand and `favicon_svg()` is served at `/favicon.svg` — a dark tile + with a two-stroke "W", white then accent blue. No asset pipeline, no + binary in the repo, and it stays legible at 16px. The `` link + reaches the document through `Layout`'s `head` slot. +- **A raw literal cannot contain a literal `{{`.** The packages page has + prose ABOUT `{{ }}` holes, and writing it directly would have made it a + hole; it is written with `{` entities instead. This is the same + limitation the chapter code samples hit with `${`, and the reason both + doors exist. +- **`SITE_HOST` picks the interface.** Loopback by default — right behind + a proxy — with the env var for reaching a dev instance across the LAN. + The bound address is printed at startup. - **wo-html's sheet is static.** Tailwind's class NAMES, one hand-written CSS string inlined per page by `page()` — self-contained responses, no toolchain; growing the sheet is appending a line in `tw_css()`. diff --git a/docs/examples/site/README.md b/docs/examples/site/README.md index 57b91b4..7541d28 100644 --- a/docs/examples/site/README.md +++ b/docs/examples/site/README.md @@ -17,7 +17,18 @@ html = { git = "https://github.com/shoneyj/wo-html", rev = "v0. woc . && SITE_TOKEN=change-me WO_DATA=./data ./target/site 8080 ``` -- `GET /` — the chapter index; `GET /ch/` — one chapter. +It binds loopback by default. To reach it from another machine while +developing, name the interface: + +``` +SITE_HOST=0.0.0.0 SITE_TOKEN=change-me WO_DATA=./data ./target/site 8080 +``` + +- `GET /` — the homepage; `GET /ch/` — one chapter. +- `GET /install` — the installation guide; `GET /packages` and + `GET /packages/` — the package catalogue with copy-paste + `[deps]` lines and usage. +- `GET /favicon.svg` — the mark, inline SVG, no asset pipeline. - `POST /admin/ch/` — edit a chapter (`title`/`body`, form-encoded, `authorization: Bearer $SITE_TOKEN`). Edits are WAL-durable under `WO_DATA` and replay on restart — that is chapter 6, demonstrated by @@ -38,7 +49,9 @@ MVC, laid out exactly like the program template | `types.wo` | MODEL — the `Chapter` `@table`, and seed-if-empty | | `content.wo` | the nine chapter bodies + `seed_chapters()` | | `layout/` | the chrome: `AppShell` (+ the two named widths), header, footer, `html_error` | -| `home/`, `chapter/`, `admin/`, `health/` | one module per feature: its `view.wo` (components: fields in, Text out) and its `controller.wo` (query the model, fill the components, answer a `Resp`) | +| `home/`, `chapter/`, `install/`, `packages/` | one module per feature: its `view.wo` (components: fields in, Text out) and its `controller.wo` (query the model, fill the components, answer a `Resp`) | +| `admin/`, `health/`, `favicon/` | controller-only features — a redirect, a text probe, an SVG | +| `layout/logo.wo` | the mark as inline SVG: one source for the nav brand and `/favicon.svg` | | `main.wo` | bootstrap: seed, routes, serve — nothing else | Every `render()` makes its class a component (wo-html's structural diff --git a/docs/examples/site/favicon/controller.wo b/docs/examples/site/favicon/controller.wo new file mode 100644 index 0000000..ff5608b --- /dev/null +++ b/docs/examples/site/favicon/controller.wo @@ -0,0 +1,13 @@ +-- favicon/controller.wo — GET /favicon.svg. The one route that answers +-- something other than HTML or text, so it builds its own Resp. +use framework/http +use layout + +pub class Favicon { + fn handle(req: Req) -> Resp { + let h: map = {}; + h["content-type"] = "image/svg+xml"; + h["cache-control"] = "public, max-age=86400"; + return Resp { status: 200, headers: h, body: favicon_svg() }; + } +} diff --git a/docs/examples/site/install/controller.wo b/docs/examples/site/install/controller.wo new file mode 100644 index 0000000..a4b0cea --- /dev/null +++ b/docs/examples/site/install/controller.wo @@ -0,0 +1,12 @@ +-- install/controller.wo — GET /install. Nothing to query: the page is +-- static copy, so the controller only wraps it in the shell. +use framework/http +use layout + +pub class ShowInstall { + fn handle(req: Req) -> Resp { + let page = InstallPage {}; + let shell = reading_shell("writeonce — install", page.render()); + return ok_html(shell.render()); + } +} diff --git a/docs/examples/site/install/view.wo b/docs/examples/site/install/view.wo new file mode 100644 index 0000000..aeee016 --- /dev/null +++ b/docs/examples/site/install/view.wo @@ -0,0 +1,73 @@ +-- install/view.wo — the VIEW for /install. Static copy: no fields, so +-- the component has none. It is still a component, and still renders +-- through the same interface as every other page. +use html + +pub class InstallPage { + fn render() -> Text { + let head = ` +

Install writeonce

+

Two native binaries — woc, + the compiler, and wovm, the runtime VM. Both depend only on + the system C library. There is no package manager to install, no + language runtime to install on the machines you deploy to, and no + build toolchain beyond these two files.

`; + + let s1 = section("1. Get the toolchain", + `

Download the release tarball and extract + it into /usr/local. Run this as root, or through + sudo:

`, + code_block("rm -rf /usr/local/writeonce\ntar -C /usr/local -xzf writeonce-0.1.0-linux-amd64.tar.gz")); + + let s2 = section("2. Put it on your PATH", + `

Add one line to your + $HOME/.profile (or /etc/profile for every user + on the box), then restart your shell:

`, + code_block("export PATH=$PATH:/usr/local/writeonce/bin")); + + let s3 = section("3. Check it", + `

Both should print the same version. + woc finds wovm beside itself, so a tarball + install needs no further configuration.

`, + code_block("woc version # writeonce 0.1.0 linux/amd64\nwovm --version # wovm 0.1.0")); + + let s4 = section("4. Your first project", + `

A project is a directory with a + wo.toml manifest and one or more .wo files. + Nothing else — no lockfile to create by hand, no scaffolding step.

`, + code_block("mkdir hello && cd hello\n\ncat > wo.toml <<'EOF'\nname = \"hello\"\nversion = \"0.1.0\"\n\n[runtime]\nwo = \">= 0.1\"\nEOF\n\ncat > main.wo <<'EOF'\nfn main() -> Int {\n print(\"hello, writeonce\");\n return 0;\n}\nEOF")); + + let s5 = section("5. Build and run", + `

woc <dir> emits ONE + standalone binary at target/<name>. Copy that file to a + server and run it — the VM is inside it, and so is the database.

`, + code_block("woc .\n./target/hello # hello, writeonce")); + + let s6 = section("6. Add a dependency", + `

Dependencies are git repositories pinned + by revision. The [deps] KEY is the module name your code + uses. woc writes a wo.lock with the + exact revision, and a locked build works offline. See + Packages for what is available.

`, + code_block("[deps]\nhtml = { git = \"https://github.com/shoneyj/wo-html\", rev = \"v0.1.0\" }")); + + let note = el("div", "bg-white rounded-lg border shadow-sm p-6 mt-8", + el("h2", "text-lg font-bold mb-2", "Where things go") .. + `

The tarball installs to + /usr/local/writeonce: bin/woc, + bin/wovm, and a VERSION file. To point + woc at a different VM, set $WO_RUNTIME or add a + [build] runtime = "..." key to wo.toml. A + manifest may also require a minimum toolchain with + [runtime] wo = ">= 0.1"; woc refuses to build + a project that needs a newer toolchain than itself.

`); + + return head .. s1 .. s2 .. s3 .. s4 .. s5 .. s6 .. note; + } +} + +-- One numbered step: heading, prose, and the commands to run. +fn section(title: Text, prose: Text, code: Text) -> Text { + return el("div", "mb-8", + el("h2", "text-2xl font-bold mb-2", title) .. prose .. code); +} diff --git a/docs/examples/site/layout/app.wo b/docs/examples/site/layout/app.wo index d367269..0b39ee5 100644 --- a/docs/examples/site/layout/app.wo +++ b/docs/examples/site/layout/app.wo @@ -17,6 +17,7 @@ pub class AppShell { fn render() -> Text { let l = Layout { title: self.title, + head: head_links(), nav: header(), content: el("div", self.container, self.content), footer: footer() diff --git a/docs/examples/site/layout/header.wo b/docs/examples/site/layout/header.wo index 4adee3c..323523f 100644 --- a/docs/examples/site/layout/header.wo +++ b/docs/examples/site/layout/header.wo @@ -2,8 +2,12 @@ use html pub fn header() -> Text { - let links = link("/ch/hello", "text-gray-700", "Tutorial"); - links = links .. link("/health", "text-gray-700", "Health"); + let links = link("/install", "text-gray-700", "Install"); + links = links .. link("/ch/hello", "text-gray-700", "Tutorial"); + links = links .. link("/packages", "text-gray-700", "Packages"); links = links .. link("https://github.com/shoneyj", "text-gray-700", "GitHub"); - return nav_bar("/", "writeonce.de", links); + -- The brand is the mark plus the wordmark, one inline-flex row so the + -- tile and the text share a baseline at any font size. + let brand = el("span", "flex items-center gap-2", logo_svg(28) .. "writeonce.de"); + return nav_bar("/", brand, links); } diff --git a/docs/examples/site/layout/logo.wo b/docs/examples/site/layout/logo.wo new file mode 100644 index 0000000..1c2f2aa --- /dev/null +++ b/docs/examples/site/layout/logo.wo @@ -0,0 +1,23 @@ +-- layout/logo.wo — the mark, authored as inline SVG so it needs no asset +-- pipeline and no second request: a dark tile with a two-stroke "W", the +-- first half white and the second the site's accent blue. One glyph, two +-- colours, still legible at 16px. +-- +-- One source, two consumers: the nav brand embeds it, and +-- `favicon/controller.wo` serves the same bytes at /favicon.svg. + +pub fn logo_svg(px: Int) -> Text { + return ``; +} + +-- The favicon is the same mark without intrinsic width/height, so the +-- browser scales it to whatever the tab needs. +pub fn favicon_svg() -> Text { + return ``; +} + +-- What goes in . Inline SVG, no request, no cache question. +pub fn head_links() -> Text { + return ` + `; +} diff --git a/docs/examples/site/main.wo b/docs/examples/site/main.wo index 65b5c89..8286675 100644 --- a/docs/examples/site/main.wo +++ b/docs/examples/site/main.wo @@ -5,6 +5,7 @@ -- final chapter: /ch/serving shows this file's shape. -- -- SITE_TOKEN=... WO_DATA=./data ./site 8080 +-- SITE_HOST=0.0.0.0 ... ./site 8080 (reachable from the network) -- -- Behind nginx/caddy for writeonce.de: the proxy terminates TLS and -- forwards to 127.0.0.1:8080 (the framework speaks HTTP/1.1 keep-alive). @@ -17,8 +18,11 @@ use framework use framework/router use home use chapter +use install +use packages use admin use health +use favicon fn main(args: multi Text) -> Int { if len(args) < 1 { @@ -36,13 +40,27 @@ fn main(args: multi Text) -> Int { return 2; } + -- Bind to loopback unless SITE_HOST says otherwise. Behind a proxy + -- loopback is right; SITE_HOST=0.0.0.0 (or a LAN address) is how you + -- reach it from another machine while developing. + let host = "127.0.0.1"; + let h = env.get("SITE_HOST"); + if h != nil { + host = "${h}"; + } + seed_if_empty(); let app = App { middleware: [], routes: [] }; app.use_mw(Mw { m: Logging {} }); app.get("/", Home {}); + app.get("/install", ShowInstall {}); + app.get("/packages", ShowPackages {}); + app.get("/packages/:name", ShowPackage {}); app.get("/health", Health {}); + app.get("/favicon.svg", Favicon {}); app.get("/ch/:slug", ShowChapter {}); app.post("/admin/ch/:slug", AdminEdit { token: "${token}" }); - return app.serve("127.0.0.1", port); + print_err("site: listening on ${host}:${port}"); + return app.serve(host, port); } diff --git a/docs/examples/site/packages/controller.wo b/docs/examples/site/packages/controller.wo new file mode 100644 index 0000000..c456134 --- /dev/null +++ b/docs/examples/site/packages/controller.wo @@ -0,0 +1,89 @@ +-- packages/controller.wo — GET /packages and /packages/:name. +-- +-- The catalogue is static data, not rows, so it lives here on the DATA +-- side of the feature rather than in the view: the components are handed +-- values exactly as they would be if this were a table one day. +use framework/http +use html +use layout + +typedef PackageInfo = { + name: Text, + summary: Text, + git: Text, + rev: Text, + what: Text, + usage: Text +} + +pub class ShowPackages { + fn handle(req: Req) -> Resp { + let cards: multi Component = []; + for p in catalogue() { + push(cards, PackageCard { name: p.name, summary: p.summary }); + } + let page = PackagesPage { cards: cards }; + let shell = reading_shell("writeonce — packages", page.render()); + return ok_html(shell.render()); + } +} + +pub class ShowPackage { + fn handle(req: Req) -> Resp { + let name = req.params["name"]; + if name == nil { + return html_error(404, "No such package", "The address names no package."); + } + for p in catalogue() { + if p.name == name { + let page = PackagePage { + name: p.name, summary: p.summary, git: p.git, rev: p.rev, + what: p.what, usage: p.usage + }; + let shell = reading_shell("writeonce — ${p.name}", page.render()); + return ok_html(shell.render()); + } + } + return html_error(404, "No such package", "Nothing is published under that name."); + } +} + +-- The two libraries this site runs on. Both are `kind = "library"` +-- projects: no `fn main`, imported through `[deps]`. +fn catalogue() -> multi PackageInfo { + let out: multi PackageInfo = []; + + push(out, PackageInfo { + name: "framework", + summary: "A web framework written in writeonce: an HTTP/1.1 keep-alive server core, a router with :param captures, and Handler/Middleware structural interfaces.", + git: "https://github.com/shoneyj/writeonce-framework", + rev: "v0.1.0", + what: ` +
    +
  • App — the route table and the middleware chains; app.get/post/delete_, use_mw, use_after, mount, serve.
  • +
  • Handler, Middleware, After — structural interfaces. A handler is a CLASS; its fields are the closure this language does not have.
  • +
  • Req/Resp plus builders: ok_text, ok_html, ok_json, created_json, not_found, bad_request, unauthorized, conflict, redirect.
  • +
  • Auth MECHANISM only — bearer_token, basic_credentials, constant-time ct_eq. Which routes are gated stays your policy.
  • +
  • Bodies: form_values, multipart_parts, content negotiation, ETags, security headers, CORS, WebSocket frames.
  • +
`, + usage: code_block("use framework\nuse framework/http\nuse framework/router\n\nclass Hello {\n fn handle(req: Req) -> Resp {\n return ok_text(\"hello\");\n }\n}\n\nfn main(args: multi Text) -> Int {\n let app = App { middleware: [], routes: [] };\n app.use_mw(Mw { m: Logging {} });\n app.get(\"/\", Hello {});\n return app.serve(\"127.0.0.1\", 8080);\n}") + }); + + push(out, PackageInfo { + name: "html", + summary: "Server-rendered HTML as plain Text: escaping, element builders, a component layer, and a Tailwind-style utility stylesheet inlined into every page.", + git: "https://github.com/shoneyj/wo-html", + rev: "v0.1.0", + what: ` +
    +
  • esc() — escapes & < > ". A {{ }} hole in a raw text literal compiles to a call to it, so display data is escaped by construction. (Written with entities here: inside a raw literal, a real {{ would BE a hole.)
  • +
  • Component — a class with fields and fn render() -> Text, satisfied structurally. multi Component holds children directly; render_all renders them in order.
  • +
  • Layout — content projection: title, head, nav, content and footer as pre-rendered slots.
  • +
  • Builders: el, link, card, nav_bar, code_block, form_post, text_input, text_area, submit_btn, btn_link.
  • +
  • tw_css() + page() — one hand-written utility sheet, inlined, so a page is one self-contained response. No CDN, no build step, no JS.
  • +
`, + usage: code_block("use html\n\nclass Card {\n name: Text\n fn render() -> Text {\n return `\n
\n

{{ self.name }}

\n
`;\n }\n}\n\n-- in a handler:\nlet c = Card { name: user_supplied };\nreturn ok_html(page(\"Hello\", c.render()));") + }); + + return out; +} diff --git a/docs/examples/site/packages/view.wo b/docs/examples/site/packages/view.wo new file mode 100644 index 0000000..5e9af1f --- /dev/null +++ b/docs/examples/site/packages/view.wo @@ -0,0 +1,61 @@ +-- packages/view.wo — the VIEW for /packages and /packages/:name. +-- +-- Three components: a card for the index, the index itself (holding its +-- cards as CHILDREN through the structural interface), and the detail +-- page. None of them knows where the catalogue came from. +use html + +pub class PackageCard { + name: Text + summary: Text + + fn render() -> Text { + return ` +
+

{{ self.name }}

+

{{ self.summary }}

+
`; + } +} + +pub class PackagesPage { + cards: multi Component + + fn render() -> Text { + return ` +

Packages

+

A package is a git repository with a + wo.toml that says kind = "library". There is no + registry and no publish step: you depend on a URL and a revision, and + wo.lock pins exactly what you built against. These are the + libraries this site itself is built on.

+
${render_all(self.cards)}
`; + } +} + +pub class PackagePage { + name: Text + summary: Text + git: Text + rev: Text + what: Text + usage: Text + + fn render() -> Text { + let dep = code_block("[deps]\n" .. self.name .. " = { git = \"" .. self.git .. "\", rev = \"" .. self.rev .. "\" }"); + let head = ` +

{{ self.name }}

+

{{ self.summary }}

+

Install

+

Add it to your wo.toml. The + KEY is the module name — that is what use imports.

`; + let what = ` +

What you get

+ ${self.what}`; + let usage = ` +

Usage

+ ${self.usage}`; + let back = el("p", "mt-8", link("/packages", "", "← all packages")); + return head .. dep .. what .. usage .. back; + } +} diff --git a/docs/examples/wo-html/README.md b/docs/examples/wo-html/README.md index cd3030b..68f39ba 100644 --- a/docs/examples/wo-html/README.md +++ b/docs/examples/wo-html/README.md @@ -53,12 +53,15 @@ pub class ProductListPage { record is needed (the framework's `Mw`/`Aw` wrappers are not a language requirement). `render_all(cs)` renders children in order. -`Layout { title, nav, content, footer }` is content projection — +`Layout { title, head, nav, content, footer }` is content projection — Angular's `` with the slots as ordinary pre-rendered `Text`. The caller passes `child.render()`, a raw literal, or a builder's output; the layout never learns which, which is precisely why it never needs the child's type. A page wanting a fixed-width column wraps its content -before handing it over — deliberately no container knob here. +before handing it over — deliberately no container knob here. `head` is +the one slot that is not body markup: a favicon link or a meta tag has +nowhere else to go, and `""` is the ordinary value (`page()` passes it +for you). `Layout` renders through `page()`, so it inlines the utility sheet. An app that links a real stylesheet instead writes its own two-slot shell diff --git a/docs/examples/wo-html/html.wo b/docs/examples/wo-html/html.wo index 20a372b..76b2f8c 100644 --- a/docs/examples/wo-html/html.wo +++ b/docs/examples/wo-html/html.wo @@ -166,6 +166,14 @@ pub fn tw_css() -> Text { -- One full document: the sheet inlined, viewport set, body handed in. -- Self-contained by construction — view-source shows everything. pub fn page(title: Text, body: Text) -> Text { + return page_head(title, "", body); +} + +-- The same document with extra markup: a favicon link, a meta +-- description, whatever the app needs up there. Kept RAW (`${}`) — head +-- content is markup the app built, not data — and defaulted to "" by +-- `page()` so no consumer has to care. +pub fn page_head(title: Text, head: Text, body: Text) -> Text { -- The whole document as one literal. Every newline here lands -- inside , where whitespace is insignificant; the body hole -- and its closing tags share one line so nothing is inserted into @@ -174,6 +182,7 @@ pub fn page(title: Text, body: Text) -> Text { {{ title }} + ${head} ${body}`; } @@ -213,16 +222,18 @@ pub fn render_all(cs: multi Component) -> Text { -- builder's output — the layout never knows which, which is exactly why -- it never needs to know the child's type. -- --- Deliberately four slots and no container/width field: a page that --- wants its content in a fixed-width column wraps it before passing it --- in. One less knob here beats one more. +-- Deliberately no container/width field: a page that wants its content +-- in a fixed-width column wraps it before passing it in. `head` is the +-- one non-body slot — a favicon link or a meta tag has nowhere else to +-- go, and "" is the ordinary value. pub class Layout { title: Text + head: Text nav: Text content: Text footer: Text fn render() -> Text { - return page(self.title, self.nav .. self.content .. self.footer); + return page_head(self.title, self.head, self.nav .. self.content .. self.footer); } } diff --git a/scripts/site-accept.sh b/scripts/site-accept.sh index 42c666b..6243242 100755 --- a/scripts/site-accept.sh +++ b/scripts/site-accept.sh @@ -93,6 +93,12 @@ expect "tailwind sheet inlined" "$(hit /)" 200 ".btn{" expect "chapter renders a code sample" "$(hit /ch/hello)" 200 "fn main" expect "escaped interpolation visible" "$(hit /ch/values)" 200 '${port}' expect "unknown chapter is a 404 page" "$(hit /ch/nope)" 404 "No such chapter" +expect "install guide renders" "$(hit /install)" 200 "tar -C /usr/local" +expect "packages index lists both" "$(hit /packages)" 200 "/packages/framework" +expect "package detail shows its dep" "$(hit /packages/html)" 200 "wo-html" +expect "unknown package is a 404 page" "$(hit /packages/nope)" 404 "No such package" +expect "favicon is served as svg" "$(hit /favicon.svg)" 200 "