Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions docs/guides/accessibility.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// SPDX-License-Identifier: CC-BY-SA-4.0
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
= Accessibility
:toc: left
:toclevels: 2

casket treats accessibility as a property of the *engine*, not a checklist for
the author. This guide separates what casket guarantees from what remains your
responsibility.

== Engine-owned (casket guarantees)

When you use the built-in template and the default pipeline, casket emits:

[cols="2,3"]
|===
| What | WCAG relevance

| `<html lang="…">`, `<meta viewport>`, `color-scheme: light dark`
| 3.1.1 Language of Page; 1.4.10 Reflow; respects OS light/dark

| A skip link (`Skip to content`) and labelled landmarks
(`header` / `nav[aria-label]` / `main#main` / `footer`)
| 2.4.1 Bypass Blocks; 1.3.1 Info & Relationships

| `scope="col"` added to Pandoc table headers automatically
| 1.3.1 Info & Relationships (screen-reader table navigation)

| Valid `<time datetime="…">` (empty `<time>` is dropped)
| 1.3.1; avoids invalid markup

| Mandatory alt-text on every generated badge (`FlexiText`)
| 1.1.1 Non-text Content — *enforced at construction*, cannot be omitted
|===

The `FlexiText` guarantee is structural: the smart constructor rejects empty
alt-text, so a badge without a text alternative cannot be built. See
link:the-gnosis-layer.adoc[The Gnosis layer].

== Author-owned (your responsibility)

casket cannot guarantee accessibility for content and templates you author:

* *Your images* — provide real alt text (`![meaningful description](img.png)`).
* *Custom templates* — if you replace the built-in template, keep the `lang`
attribute, skip link, landmarks, and a logical, unskipped heading order.
* *Custom CSS* — maintain colour contrast (aim ≥ 4.5:1 body text), visible
`:focus-visible`, ≥ 44px touch targets, and honour `prefers-reduced-motion`.
* *Link text* — write meaningful link text, not "click here".
* *Headings* — one `<h1>` per page; don't skip levels.

== Verifying

The engine-owned guarantees ship by default; for the author-owned criteria, run
an automated checker (axe, pa11y, or Lighthouse) over the built `_site/` in CI.
The sibling project https://github.com/hyperpolymath/ddraig-ssg[ddraig] maintains
an `ACCESSIBILITY-CHECKLIST` enumerating engine-owned vs author-owned success
criteria; casket shares that split.

== Direction

The accessibility invariant is being lifted from a constructor guarantee toward
a *proof*: the loss-with-residue `echo-types` `Echo` formalism, where rendered
output retains a witness of its accessible source. `FlexiText` is the current
unverified shadow of that; the dependently-typed sibling ddraig owns the native
proof. See link:../decisions/ADR-0001-content-engine.adoc[ADR-0001].
128 changes: 128 additions & 0 deletions docs/guides/authoring-a-site.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// SPDX-License-Identifier: CC-BY-SA-4.0
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
= Authoring a site with casket
:toc: left
:toclevels: 2

How to write content, build it, and understand the output. For customising the
look see link:templates-and-theming.adoc[Templates & theming]; for the metadata
layer see link:the-gnosis-layer.adoc[The Gnosis layer].

== Source layout

A site is a directory of Markdown plus optional supporting folders:

[source]
----
my-site/
├── content/ # your pages (passed as the build input dir)
│ ├── index.md
│ └── posts/
│ └── 2026-06-23-hello.md
├── templates/ # optional: layouts + partials (theming guide)
│ ├── default.html
│ └── partials/
├── assets/ # optional: copied to /assets in the output
├── public/ # optional: copied verbatim to the OUTPUT ROOT
└── site.conf # optional: site-wide {{site.*}} values
----

Only `content/` (the input directory) is required; everything else has sane
defaults — including a built-in accessible template.

== Writing a page

Each page is Markdown with a `---`-fenced front-matter block:

[source,markdown]
----
---
title: Hello, world
date: 2026-06-23
description: A first page built with casket.
tags: [intro, meta]
---
# Hello

Body text in **Markdown**, rendered by Pandoc: tables, footnotes,
task lists, fenced + highlighted code, smart punctuation.
----

A leading SPDX HTML comment before the `---` fence is tolerated.

Recognised keys: `title`, `date`, `description`, `tags`, `layout`, `draft`,
`slug`, and `site`/`brand`. Any *other* key is exposed to templates as
`{{key}}`, so front-matter doubles as page-scoped template data.

== Building

[source,bash]
----
casket-ssg build content _site # input dir, then optional output dir (default _site)
casket-ssg build --drafts content # include `draft: true` pages (or set CASKET_DRAFTS=1)
casket-ssg build --no-clean-urls content
casket-ssg clean _site # remove an output directory
----

== Output structure

By default casket emits *clean URLs*:

[cols="2,2"]
|===
| Source | Output

| `content/index.md` | `_site/index.html`
| `content/about.md` | `_site/about/index.html` (→ `/about/`)
| `content/posts/2026-…-hello.md` | `_site/posts/2026-…-hello/index.html`
|===

* `index.md` always maps to `index.html` in the same directory.
* `slug: my-slug` front-matter overrides the output basename.
* `--no-clean-urls` emits `about.html` instead of `about/index.html`.

== Collections

Any source directory with *two or more dated pages* and no `index.md` of its own
gets a generated listing page (newest first). So `content/posts/` with several
dated posts yields `/posts/` automatically — no configuration.

== Tags

Pages with `tags:` produce a page per tag at `/tags/<tag>/` (slugified) plus a
`/tags/` index counting each tag.

== Feeds & discovery

casket writes, from `site.baseurl`:

* `sitemap.xml` — every page URL (including generated collection/tag pages);
* `feed.xml` — an Atom feed of the 20 most recent *dated* pages.

== Drafts

`draft: true` pages are skipped unless you pass `--drafts` (or set
`CASKET_DRAFTS=1`). Skipped drafts are reported during the build.

== Verbatim files (`public/`)

Anything under `<input>/public/` is copied to the *output root*, so
`public/CNAME`, `public/.well-known/security.txt`, `public/robots.txt`, and
`public/favicon.svg` ship at `/CNAME`, `/.well-known/…`, etc.

== Site configuration (`site.conf`)

A simple `key = value` file (looked up in the CWD, then the input dir), exposed
to templates as `{{site.*}}`:

[source,ini]
----
title = My Casket Site
baseurl = https://example.com
author = Jane Doe
description = Built with casket
nav = Home=/,Posts=/posts/,About=/about/
----

`nav` (optional) drives the primary navigation; without it, casket derives nav
from your top-level pages.
86 changes: 86 additions & 0 deletions docs/guides/templates-and-theming.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// SPDX-License-Identifier: CC-BY-SA-4.0
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
= Templates & theming
:toc: left
:toclevels: 2

How casket turns a rendered page into HTML: layout resolution, partials, the
template variables you can use, and the built-in accessible theme. See also
link:authoring-a-site.adoc[Authoring] and
link:the-gnosis-layer.adoc[The Gnosis layer].

== Layout resolution

For each page casket picks a layout in this order:

. `templates/<layout>.html` — where `<layout>` is the page's `layout:`
front-matter (a bare name gets `.html` appended);
. `templates/default.html` — the site default;
. the *built-in accessible template* — if no `templates/` file exists.

So you can ship zero templates and still get a complete, accessible page.

== Partials

Inside any template, include a partial from `templates/partials/<name>.html`:

[source,html]
----
{{> header}}
{{include:footer}}
----

Both spellings work. Partials may include partials (guarded against cycles). A
missing partial renders an HTML comment rather than failing the build.

== Template variables

Substituted by `fillTemplate`, in this order (so body HTML is never
re-processed):

[cols="1,3"]
|===
| Variable | Value

| `{{site.KEY}}` | Any key from `site.conf` (e.g. `{{site.title}}`, `{{site.baseurl}}`)
| `{{KEY}}` | Any front-matter key on the page (arbitrary custom keys included)
| `{{title}}` | Resolved page title (front-matter `title`, else the file base name)
| `{{content}}` | The rendered HTML body (substituted *last*)
| `{{toc}}` | Table-of-contents `<nav>` from the page headings
| `{{nav}}` | Primary navigation (`site.conf` `nav`, else derived)
| `{{date}}` | Page `date` (empty for generated pages)
| `{{description}}`| Page `description`, else `site.description`
| `{{brand}}` | Page `site`/`brand`, else the site title
|===

The idiom `<time>{{date}}</time>` is rewritten to a valid
`<time datetime="…">…</time>` (or dropped when there is no date), so empty
`<time>` elements never ship.

== Assets

These directories, if present in the CWD, are copied into the output:
`assets/`, `static/`, `css/`, `js/`, `images/`. casket additionally writes the
syntax-highlighting stylesheet to `/assets/highlight.css` — link it from your
template:

[source,html]
----
<link rel="stylesheet" href="/assets/style.css">
<link rel="stylesheet" href="/assets/highlight.css">
----

(For files that must land at the site root, use `public/` — see
link:authoring-a-site.adoc[Authoring].)

== The built-in accessible theme

When you provide no template, casket's fallback is accessible by construction:
`<html lang>`, a skip link, labelled `header`/`nav`/`main#main`/`footer`
landmarks, `<meta viewport>`, and `color-scheme: light dark`. casket also adds
`scope="col"` to Pandoc's table headers (WCAG 1.3.1).

If you author your *own* template, you own its accessibility — keep the skip
link, landmarks, `lang`, and a logical heading order. See
link:accessibility.adoc[the accessibility guide] for the engine-owned vs
author-owned split.
89 changes: 89 additions & 0 deletions docs/guides/the-gnosis-layer.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// SPDX-License-Identifier: CC-BY-SA-4.0
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
= The Gnosis layer
:toc: left
:toclevels: 2

Gnosis is casket's optional metadata-driven rendering layer. It runs over the
Markdown *before* Pandoc, so you can substitute typed values, branch and loop on
metadata, and emit accessible badges — all from a structured data context. See
link:authoring-a-site.adoc[Authoring] for the basics.

== The context

Gnosis renders against a *context* — a map of keys to `FlexiText` values, where
each value pairs a *visual* with mandatory *alt-text*. The context is built from:

* the page's front-matter (highest priority), merged over
* the repository's descriptive metadata (the six `.a2ml` files under
`.machine_readable/descriptiles/`).

[NOTE]
====
*Current state:* the loader (`Gnosis.SixSCM`) still reads the older flat
`.machine_readable/*.scm` layout, so today the context is driven mainly by
front-matter. Wiring it to `descriptiles/*.a2ml` is the first migration step in
link:../decisions/ADR-0001-content-engine.adoc[ADR-0001].
====

== Placeholders — `(:key)`

`(:key)` is replaced by the value of `key` in the context, rendered as an
*accessible Shields.io badge* (the value as the badge, `key` as the label, with
the FlexiText alt-text). A missing key renders `(:MISSING:key)` so gaps are
visible, never silent.

[source,markdown]
----
Project phase: (:phase)
Health: (:health-score)
----

=== Filters

Chain filters with `|`:

[source,markdown]
----
Downloads: (:downloads | thousands-separator)
Name: (:name | uppercase)
----

Built-in filters: `thousands-separator`, `relativeTime`, `uppercase`,
`lowercase`, `capitalize`, `round`. An unknown filter passes the value through
unchanged.

== Conditionals & loops (DAX)

DAX adds block logic, evaluated before placeholders.

[source,markdown]
----
{{#if phase == production}}
**Stable.** Safe to depend on.
{{/if}}

{{#if status != archived}}
Actively maintained.
{{/if}}
----

Loops iterate a comma-separated list value; inside the body, `(:var)` refers to
the current item:

[source,markdown]
----
{{#for tag in tags}}
- (:tag)
{{/for}}
----

Supported comparisons are `==` and `!=`; an unparseable condition renders
nothing.

== Why FlexiText

Every Gnosis value is a `FlexiText` — a visual paired with alt-text whose smart
constructor *rejects empty alt-text* (a `Left`, the "Tri-Guard" rule). That is
why placeholder badges always carry an accessible description: it is impossible
to construct one that does not. See link:accessibility.adoc[Accessibility].
Loading