feat(site): logo, favicon, install guide, package catalogue
- 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) <noreply@anthropic.com>
This commit is contained in:
parent
23550e7021
commit
3afdafa5c4
14 changed files with 357 additions and 13 deletions
|
|
@ -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 `<head>` 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 `<head>` 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()`.
|
||||
|
|
|
|||
|
|
@ -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/<slug>` — 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/<slug>` — one chapter.
|
||||
- `GET /install` — the installation guide; `GET /packages` and
|
||||
`GET /packages/<name>` — the package catalogue with copy-paste
|
||||
`[deps]` lines and usage.
|
||||
- `GET /favicon.svg` — the mark, inline SVG, no asset pipeline.
|
||||
- `POST /admin/ch/<slug>` — 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
|
||||
|
|
|
|||
13
docs/examples/site/favicon/controller.wo
Normal file
13
docs/examples/site/favicon/controller.wo
Normal file
|
|
@ -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<Text, Text> = {};
|
||||
h["content-type"] = "image/svg+xml";
|
||||
h["cache-control"] = "public, max-age=86400";
|
||||
return Resp { status: 200, headers: h, body: favicon_svg() };
|
||||
}
|
||||
}
|
||||
12
docs/examples/site/install/controller.wo
Normal file
12
docs/examples/site/install/controller.wo
Normal file
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
73
docs/examples/site/install/view.wo
Normal file
73
docs/examples/site/install/view.wo
Normal file
|
|
@ -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 = `
|
||||
<h1 class="text-3xl font-bold mb-4">Install writeonce</h1>
|
||||
<p class="leading-relaxed mb-6">Two native binaries — <code>woc</code>,
|
||||
the compiler, and <code>wovm</code>, 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.</p>`;
|
||||
|
||||
let s1 = section("1. Get the toolchain",
|
||||
`<p class="leading-relaxed mb-4">Download the release tarball and extract
|
||||
it into <code>/usr/local</code>. Run this as root, or through
|
||||
<code>sudo</code>:</p>`,
|
||||
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",
|
||||
`<p class="leading-relaxed mb-4">Add one line to your
|
||||
<code>$HOME/.profile</code> (or <code>/etc/profile</code> for every user
|
||||
on the box), then restart your shell:</p>`,
|
||||
code_block("export PATH=$PATH:/usr/local/writeonce/bin"));
|
||||
|
||||
let s3 = section("3. Check it",
|
||||
`<p class="leading-relaxed mb-4">Both should print the same version.
|
||||
<code>woc</code> finds <code>wovm</code> beside itself, so a tarball
|
||||
install needs no further configuration.</p>`,
|
||||
code_block("woc version # writeonce 0.1.0 linux/amd64\nwovm --version # wovm 0.1.0"));
|
||||
|
||||
let s4 = section("4. Your first project",
|
||||
`<p class="leading-relaxed mb-4">A project is a directory with a
|
||||
<code>wo.toml</code> manifest and one or more <code>.wo</code> files.
|
||||
Nothing else — no lockfile to create by hand, no scaffolding step.</p>`,
|
||||
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",
|
||||
`<p class="leading-relaxed mb-4"><code>woc <dir></code> emits ONE
|
||||
standalone binary at <code>target/<name></code>. Copy that file to a
|
||||
server and run it — the VM is inside it, and so is the database.</p>`,
|
||||
code_block("woc .\n./target/hello # hello, writeonce"));
|
||||
|
||||
let s6 = section("6. Add a dependency",
|
||||
`<p class="leading-relaxed mb-4">Dependencies are git repositories pinned
|
||||
by revision. The <code>[deps]</code> KEY is the module name your code
|
||||
<code>use</code>s. <code>woc</code> writes a <code>wo.lock</code> with the
|
||||
exact revision, and a locked build works offline. See
|
||||
<a href="/packages">Packages</a> for what is available.</p>`,
|
||||
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") ..
|
||||
`<p class="leading-relaxed">The tarball installs to
|
||||
<code>/usr/local/writeonce</code>: <code>bin/woc</code>,
|
||||
<code>bin/wovm</code>, and a <code>VERSION</code> file. To point
|
||||
<code>woc</code> at a different VM, set <code>$WO_RUNTIME</code> or add a
|
||||
<code>[build] runtime = "..."</code> key to <code>wo.toml</code>. A
|
||||
manifest may also require a minimum toolchain with
|
||||
<code>[runtime] wo = ">= 0.1"</code>; <code>woc</code> refuses to build
|
||||
a project that needs a newer toolchain than itself.</p>`);
|
||||
|
||||
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);
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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) .. "<span>writeonce.de</span>");
|
||||
return nav_bar("/", brand, links);
|
||||
}
|
||||
|
|
|
|||
23
docs/examples/site/layout/logo.wo
Normal file
23
docs/examples/site/layout/logo.wo
Normal file
|
|
@ -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 `<svg xmlns="http://www.w3.org/2000/svg" width="${px}" height="${px}" viewBox="0 0 64 64" role="img" aria-label="writeonce"><rect width="64" height="64" rx="14" fill="#111827"/><path d="M14 20 L22 44 L32 28" fill="none" stroke="#ffffff" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><path d="M32 28 L42 44 L50 20" fill="none" stroke="#2563eb" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
|
||||
}
|
||||
|
||||
-- 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 `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="#111827"/><path d="M14 20 L22 44 L32 28" fill="none" stroke="#ffffff" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/><path d="M32 28 L42 44 L50 20" fill="none" stroke="#2563eb" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
|
||||
}
|
||||
|
||||
-- What goes in <head>. Inline SVG, no request, no cache question.
|
||||
pub fn head_links() -> Text {
|
||||
return `<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<meta name="description" content="writeonce — one language, one runtime, one database, one binary.">`;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
89
docs/examples/site/packages/controller.wo
Normal file
89
docs/examples/site/packages/controller.wo
Normal file
|
|
@ -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: `
|
||||
<ul class="list-disc pl-6 leading-relaxed">
|
||||
<li class="mb-2"><code>App</code> — the route table and the middleware chains; <code>app.get</code>/<code>post</code>/<code>delete_</code>, <code>use_mw</code>, <code>use_after</code>, <code>mount</code>, <code>serve</code>.</li>
|
||||
<li class="mb-2"><code>Handler</code>, <code>Middleware</code>, <code>After</code> — structural interfaces. A handler is a CLASS; its fields are the closure this language does not have.</li>
|
||||
<li class="mb-2"><code>Req</code>/<code>Resp</code> plus builders: <code>ok_text</code>, <code>ok_html</code>, <code>ok_json</code>, <code>created_json</code>, <code>not_found</code>, <code>bad_request</code>, <code>unauthorized</code>, <code>conflict</code>, <code>redirect</code>.</li>
|
||||
<li class="mb-2">Auth MECHANISM only — <code>bearer_token</code>, <code>basic_credentials</code>, constant-time <code>ct_eq</code>. Which routes are gated stays your policy.</li>
|
||||
<li class="mb-2">Bodies: <code>form_values</code>, <code>multipart_parts</code>, content negotiation, ETags, security headers, CORS, WebSocket frames.</li>
|
||||
</ul>`,
|
||||
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: `
|
||||
<ul class="list-disc pl-6 leading-relaxed">
|
||||
<li class="mb-2"><code>esc()</code> — escapes <code>& < > "</code>. A <code>{{ }}</code> 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 <code>{{</code> would BE a hole.)</li>
|
||||
<li class="mb-2"><code>Component</code> — a class with fields and <code>fn render() -> Text</code>, satisfied structurally. <code>multi Component</code> holds children directly; <code>render_all</code> renders them in order.</li>
|
||||
<li class="mb-2"><code>Layout</code> — content projection: title, head, nav, content and footer as pre-rendered slots.</li>
|
||||
<li class="mb-2">Builders: <code>el</code>, <code>link</code>, <code>card</code>, <code>nav_bar</code>, <code>code_block</code>, <code>form_post</code>, <code>text_input</code>, <code>text_area</code>, <code>submit_btn</code>, <code>btn_link</code>.</li>
|
||||
<li class="mb-2"><code>tw_css()</code> + <code>page()</code> — one hand-written utility sheet, inlined, so a page is one self-contained response. No CDN, no build step, no JS.</li>
|
||||
</ul>`,
|
||||
usage: code_block("use html\n\nclass Card {\n name: Text\n fn render() -> Text {\n return `\n <div class=\"card\">\n <h3>{{ self.name }}</h3>\n </div>`;\n }\n}\n\n-- in a handler:\nlet c = Card { name: user_supplied };\nreturn ok_html(page(\"Hello\", c.render()));")
|
||||
});
|
||||
|
||||
return out;
|
||||
}
|
||||
61
docs/examples/site/packages/view.wo
Normal file
61
docs/examples/site/packages/view.wo
Normal file
|
|
@ -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 `
|
||||
<div class="bg-white rounded-lg border shadow-sm p-6">
|
||||
<h3 class="text-lg font-bold mb-2"><a href="/packages/{{ self.name }}">{{ self.name }}</a></h3>
|
||||
<p class="leading-relaxed text-gray-700">{{ self.summary }}</p>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
pub class PackagesPage {
|
||||
cards: multi Component
|
||||
|
||||
fn render() -> Text {
|
||||
return `
|
||||
<h1 class="text-3xl font-bold mb-4">Packages</h1>
|
||||
<p class="leading-relaxed mb-6">A package is a git repository with a
|
||||
<code>wo.toml</code> that says <code>kind = "library"</code>. There is no
|
||||
registry and no publish step: you depend on a URL and a revision, and
|
||||
<code>wo.lock</code> pins exactly what you built against. These are the
|
||||
libraries this site itself is built on.</p>
|
||||
<div class="grid grid-cols-2 gap-6 mb-8">${render_all(self.cards)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
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 = `
|
||||
<h1 class="text-3xl font-bold mb-2">{{ self.name }}</h1>
|
||||
<p class="leading-relaxed text-gray-700 mb-6">{{ self.summary }}</p>
|
||||
<h2 class="text-2xl font-bold mb-2">Install</h2>
|
||||
<p class="leading-relaxed mb-4">Add it to your <code>wo.toml</code>. The
|
||||
KEY is the module name — that is what <code>use</code> imports.</p>`;
|
||||
let what = `
|
||||
<h2 class="text-2xl font-bold mb-2 mt-8">What you get</h2>
|
||||
${self.what}`;
|
||||
let usage = `
|
||||
<h2 class="text-2xl font-bold mb-2 mt-8">Usage</h2>
|
||||
${self.usage}`;
|
||||
let back = el("p", "mt-8", link("/packages", "", "← all packages"));
|
||||
return head .. dep .. what .. usage .. back;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 `<ng-content>` 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
|
||||
|
|
|
|||
|
|
@ -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 <head> 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 <head>, 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 {
|
|||
<!doctype html><html><head><meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>{{ title }}</title>
|
||||
${head}
|
||||
<style>${tw_css()}</style>
|
||||
</head><body>${body}</body></html>`;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 "<svg"
|
||||
expect "the logo is inline in the nav" "$(hit /)" 200 "aria-label=\"writeonce\""
|
||||
expect "admin without token is 401" "$(hit /admin/ch/hello POST "title=X")" 401 "unauthorized"
|
||||
expect "admin edit answers a redirect" "$(hit /admin/ch/hello POST "title=Hello v2" s3cr3t)" 302 ""
|
||||
expect "the edit is live" "$(hit /ch/hello)" 200 "Hello v2"
|
||||
|
|
|
|||
Loading…
Reference in a new issue