@ascribed/astro reference

What the Astro integration takes, what it does to a site, and what it writes for search engines, crawlers, and review.

For AI agents: the documentation index is at llms.txt, and this page is available as Markdown.

Everything @ascribed/astro takes and does, once a site is set up: its options, what it adds to Astro’s build and dev server, the components a layout uses, several projects, versions, and permalinks in one site, the head, sitemap, robots.txt, and response headers it writes, review in the site preview, and styling. Astro sets a site up.

Options

Option Meaning
build The build whose site output is the collection: a build name in ascribe.toml. Required, unless projects is given.
project The directory holding ascribe.toml, relative to the Astro root. By default, the root.
projects Several projects: a list of entries, each { name, build, project }, in place of build and project.
binary The ascribe binary to run, relative to the Astro root. By default, the ASCRIBE_BIN environment variable, then the binary @ascribed/cli installed.
anchors Mark each block of the page with the source file and lines it came from (data-ascribe-source; see the site-render contract), for review: "dev" in astro dev only, true in astro build too. Use "dev" unless the build is for reviewers: true puts source file paths, fragments’ included, in the published pages. By default, false, though review turns them on in astro dev.
review Review in the site preview: the Ascribe review app in astro dev’s toolbar. false leaves it out. By default, true. astro build never has it. With projects, it reviews one entry: the one an entry’s name names (review: "docs"), or the first for true.
codeTitles Show a code block’s title above it, and what a tested example was tested with (code block titles). false leaves both out, for a site that shows them itself. By default, true.
sitemap { exclude: [...] }: routes of the site’s own pages, from the host’s root, to leave out of the sitemap, such as ["/drafts"].

What the integration does

  • Fails the Astro build when ascribe build reports an error (the compiler’s report is the error), and when ascribe.toml’s [consumer] site, base-path, or trailing-slash disagrees with Astro’s site, base, or trailingSlash. In ascribe 0.2.0, Astro’s trailingSlash: "ignore" agrees with either value; since ascribe 0.3.0, the integration replaces it, as below. base-path agrees with Astro’s base when it’s that path or, since ascribe 0.3.0, a path under it (A home page in front of the docs).
  • Gives Astro its site, base, and trailingSlash from [consumer] when astro.config.mjs leaves them out, so the site’s address is written once: site, base-path, and trailing-slash. A setting astro.config.mjs does give must still agree. With projects, base stays the site’s. Astro can’t tell an integration a setting left out from one set to its default, so base: "/" and trailingSlash: "ignore" are replaced too.
  • Writes each page where a static host serves it at its address, by Astro’s build.format: with trailing-slash = "never", it gives Astro build.format: "file", so a page is written as x.html and served at x, not as x/index.html, which a host serves at x/ and answers x with a redirect. With "always", a page stays x/index.html. A build.format that disagrees fails the build; Astro’s default, "directory", is replaced for "never", since it can’t be told from a format left out. The home page is the output folder’s index.html either way, so under a base path a host may still serve it at /docs/, not /docs.
  • Adds its Markdown plugin to Astro’s Markdown processor, to apply heading ids, image attributes, glossary terms’ data-ascribe-term, and source anchors: to the default Sätteri processor’s hastPlugins, or to a unified() processor’s rehypePlugins. Both plugins are exported, as @ascribed/astro/satteri and @ascribed/astro/rehype, for a processor you configure yourself. Their types come from satteri and @types/hast, optional peer dependencies: a Sätteri processor already has the first, and a unified() one the second.
  • Serves the files pages link to (other than pages and images) at <base-path>_ascribe/files/, in astro dev and in the built site.
  • Publishes the outputs for agents, on by default with a [consumer] site: llms.txt and each page’s Markdown, in astro dev and in the built site (Publish for AI agents).
  • Gives each permalink an address that forwards to its page or heading, in astro dev and in the built site (permalinks), and serves each line’s help entry for it as JSON.
  • Writes the site’s sitemap and robots.txt from every project’s page records, and gives a layout each page’s head (the head, sitemap, and robots.txt), in astro dev and in the built site.
  • Sends each Markdown version’s response headers in astro dev, and with [consumer] host = "netlify" writes them into the built site’s _headers, or fails the build when the site’s own public/_headers lacks them (response headers).
  • Writes the redirects with [consumer] host = "netlify": each project’s _redirects, from the moves ascribe.addresses records, merged into the built site’s, ahead of the site’s own public/_redirects, and fails the build when a redirect’s address is a page the site publishes (the host’s files).
  • Serves every line of a versioned build from one collection, builds every line in astro build and astro dev, and serves each line’s linked files at its base path (versions). Each entry’s data has line, the line it’s from, and current; the generated schema doesn’t type them. A permalink forwards to the current line, and each line’s outputs for agents are published at its base path, with its headers.
  • Shows code block titles and tested lines, with a Shiki transformer it adds to Astro’s code highlighting (code block titles), and gives each highlighted block’s <code> its language as a class (code block languages).
  • Copies the generated schema into .astro/integrations/_ascribed_astro/schema.ts after each build, for a project outside the Astro root (the collection).

In astro dev

  • Saving a page, a fragment, an image or other file under the content root, an asset elsewhere in the project that a page uses, or ascribe.toml rebuilds. Changes are batched, and builds run one at a time.
  • After a successful rebuild, Astro reloads the collection and the page. A versioned build rebuilds every line.
  • After a failed one, the diagnostics are in Astro’s terminal, and pages answer with HTTP 503 until a save builds cleanly again, so you never see a stale page.
  • A change to [project] output-dir needs a restart of astro dev, and says so.
  • It writes where it’s running (url and build) to .ascribe/dev.json in the project, for the editor’s Open Site Preview, and removes the file when it stops. The url is the project’s base path on the dev server.

Components

A layout imports these from @ascribed/astro, each given the page’s collection entry or a part of it. The layout that init writes uses every one but MissingPermalink, which goes on the site’s 404 page.

Component What it renders
Elements.astro <Elements />: the element library’s stylesheet, and the small script <ascribe-tabs> needs.
Head.astro <Head entry={entry} />: everything the page’s head says about the page, from its record: its <title>, description, canonical link, robots rule, link to its Markdown version, link preview, and breadcrumbs (the head).
Availability.astro <Availability available={entry.data.available} />: the page’s available frontmatter as a badge, the same markup the site output gives a section’s @available, which the element library styles. Nothing for a page with no availability.
Versions.astro <Versions entry={entry} />: the lines of a versioned build, those that have the page, or the page it replaces or the page that replaces it, as links, the others unlinked, and the archived lines that have an address. Nothing for a build that isn’t versioned.
VersionBanner.astro <VersionBanner entry={entry} />: on an older line’s page, which version it documents, with a link to the current line’s. Nothing on the current line.
MissingPermalink.astro <MissingPermalink />, on the site’s 404 page: a permalink that isn’t here.

An entry’s data, besides its frontmatter:

  • formatted, when the content model sets inline = "code" on a field: the field as HTML, with each code span a <code>, for the heading and navigation (set:html). The example’s model sets it on title, so a title can show code, as “`loom.yaml` options”: entry.data.title is plain text, for <title> and search, and entry.data.formatted.title the HTML. See code in a field.
  • source, the page’s path from the content root, with /, as guides/install.md. An “Edit this page” link is your repository’s edit address, the content root’s folder in it, and source, wherever Ascribe’s build folder is.
  • available, the page’s available frontmatter: a list of targets, each with the text to show.

Several projects

With projects, one integration builds several Ascribe projects into one site, each under its own base path. Put the docs in a site with more than the docs sets one up.

  • An entry’s name is what everything else says: the collection, review, the folder of the entry’s schema, .astro/integrations/_ascribed_astro/<name>/schema.ts, and an error message. It has letters, digits, -, and _, starts with a letter, and is the entry’s own. Each collection names its entry, ascribeCollection({ schema, name }).
  • Astro’s site and trailingSlash, when astro.config.mjs leaves them out, come from the first entry whose project sets them; every project must agree with them anyway, and the build fails, naming the entry, when one doesn’t. Astro’s base is the site’s, the root every base path is under: the integration never sets it from a project.
  • Two entries can name one project with two builds, such as a collection for each release line. They share the project’s ascribe.toml, so they share its base path: route one of them at it, and use the other’s collection as you like. Where both publish a file or a permalink, the first entry’s wins.
  • In astro dev, a change rebuilds the project it’s in, and each project’s folder gets its own .ascribe/dev.json, with its own base path. Review covers one project: the entry review names, or the first entry when it’s true. Each removed permalink’s page links to the root of the project that retired it, and MissingPermalink offers the root of the first project with permalinks.

What the projects share, the integration checks, and it fails the build, naming the entries, when:

  • Two projects have one base path. Each needs its own. One project at / with another beneath it at /docs/ is fine.
  • A page of one project is under another’s base path, such as a page at /docs/extra/ in the project at /.
  • Their permalinks are published at two paths. One route serves every project’s permalinks, so set permalink-path to one path in each project that declares any. A project with none can leave it.
  • Two projects declare one permalink. Neither project’s build can see the other’s, so the integration reports it; rename or retire it in one of them.
  • Two entries have one name, or a name has other characters.

Versions

With a versioned build, one site serves every live line: the current line’s pages at the base path, and each older line’s under its number, /docs/3.4/, from one collection, with nothing to change in astro.config.mjs or src/content.config.ts. The Astro components says what the collection, the layout, and astro dev do with them. Each line’s llms.txt and Markdown are published at its base path, an older line’s pages name the current line’s as canonical, and the sitemap lists each page once. A permalink always forwards to the current line.

Each build lists its permalinks in the site output, _ascribe/permalinks.json (output-layout contract §1.2), and the integration publishes a page at each address, in astro build and in astro dev, where a permalink you add or remove is served on the next rebuild, with no restart. The page:

  • forwards at once with a <meta http-equiv="refresh">, without a script, keeping a heading’s anchor;
  • with a script, first takes the reader’s choices from the address’s parameters;
  • links to the same place, for a browser that doesn’t follow;
  • is marked noindex, with a canonical link to where it leads, so search engines list the page and not the address.

So it works on any static host. With [consumer] host = "netlify", the host forwards each address with a 301 instead, and serves the page only for an address with a dimension‘s parameter. The page names its section’s Markdown and JSON as its alternates, <link rel="alternate">, for an agent that reads it. The addresses end the way the site’s routes do (trailing-slash), and aren’t pages of the collection, llms.txt, or the Markdown for agents. A retired permalink’s page says what was there was removed from the docs, gives the reason, and links to the docs’ home page, unless it was retired --to another page, where it forwards.

The parameters are named for the content model’s dimensions, as a product links to a permalink:

  • A parameter that names a dimension, with one of its values, sets the reader’s choice for that dimension, as if they’d picked the tab, and the choice holds on the pages they read next.
  • Anything else is ignored: a name that isn’t a dimension, or a value the dimension doesn’t have. A link with only unknown parameters works as one with none.
  • The choice needs a script and the browser’s storage. With scripts off, the address forwards to the section with the tabs as they were; with storage blocked, the choice doesn’t reach the next page.
  • A version isn’t a parameter yet. self-managed=3.4 is ignored like any unknown one.

permalink-path moves the addresses, as a path from the site’s root, such as "/go/" when the docs move under /docs/ but the addresses your product prints shouldn’t. It must be under Astro’s base, since that’s all an Astro site serves; the integration fails the build when it isn’t.

Another generator reads the same list and publishes a page like these at path and each name, forwarding to route, and # and fragment for a heading. For the parameters, its script calls @ascribed/elements’ choose for each one that names a dimension in the list’s dimensions with one of its values, then forwards. A host’s redirect rules can do the same as the page without its script, but a redirect drops nothing only if the host keeps the anchor.

Help entries

Each build writes each permalink’s section to _ascribe/help.json (output-layout contract §1.7), for a product to show in itself. The integration serves each entry at an address of its own, in astro build and in astro dev, so a product can fetch its line’s:

  • The current line’s at the permalink’s address with .json in place of its end: /docs/go/connect.json, beside /docs/go/connect/.
  • An older line’s at go/ under the line’s base path: /docs/3.4/go/connect.json. A permalink’s own address always forwards to the current line, so the line is in the path.

Each answers with the file narrowed to that one entry: version, build, and line as the line’s file has them, and help with the entry under its name, so a product reads a fetched entry and its bundled file the same way. A line that has left the window has no address, and an alias has its own. astro dev answers with Access-Control-Allow-Origin: *, and with [consumer] host = "netlify" the build’s _headers gives each address the same, since the build’s headers list has a rule for each.

Beside each, with .md in place of .json, /docs/go/connect.md, the integration serves the entry’s section as Markdown: its title as a heading, then its text, the section as the plain output writes it. An agent reads the section a permalink names in one fetch, and a host that answers content negotiation by trying the address with .md finds it for the permalink’s address too. The JSON names it as its alternate, with a Link header in astro dev and in the build’s headers list.

Another generator serves the same: for each name in a line’s help.json, the file with that entry alone, at <path><name>.json for the current line, where path is _ascribe/permalinks.json’s, and at <base>go/<name>.json for an older line, where base is the line’s in _ascribe/versions.json, and the section as Markdown at each with .md.

A product released before a section was renamed, or after one was added, may link to a name these docs don’t have. The host answers with the site’s 404 page, as for any missing address. Put MissingPermalink on it:

---
// src/pages/404.astro
import MissingPermalink from "@ascribed/astro/MissingPermalink.astro";
---

<h1>Page not found</h1>
<MissingPermalink />

On an address under the permalinks’ path, it says the link came from a product that expected a section of the docs there, names it, and offers the docs’ home page. Anywhere else, and without a script, it shows nothing. home sets where it sends the reader, the docs’ base path by default; children replace its last paragraph, such as to point at your search:

<MissingPermalink>
  <p>Search the docs for what you need, or go back to the <a href="/docs">home page</a>.</p>
</MissingPermalink>

Search engines and crawlers

From every project’s page records, _ascribe/pages.json, the integration writes what a search engine reads: each page’s head, through Head.astro; the site’s sitemap.xml and robots.txt; and each Markdown version’s response headers, which astro dev sends and host = "netlify" writes into _headers. The head, sitemap, robots.txt, and headers says what each holds, and Publishing what a project tells search engines and agents.

sidebar(entries, options), from @ascribed/astro/content, gives a layout its sidebar: the project’s navigation, from the build’s _ascribe/nav.json (navigation contract), each page joined to its collection entry. entries is the collection, await getCollection("docs").

Option Meaning
entry The page the sidebar is on. With a versioned build, the sidebar shows that page’s line; without it, the current line’s.
name Which project’s collection, with several projects, as ascribeCollection takes it. By default, the project the entries were loaded from.
nav A site’s own list of groups, { label, pages }, each page a source file from the content root, which overrides the project’s navigation. The build fails when the list leaves out a page of the collection, names a file that isn’t one, or names a page twice.

It returns the groups in order. The content root’s pages are the first group, labeled Start unless ascribe.nav says otherwise, and each top section is a group of its own; a site’s list gives its groups as they are.

Field Meaning
label The group’s label, as plain text.
formatted.label The label as HTML, when it’s a title with code (inline = "code").
folder The section’s folder, from the content root. Absent in a site’s own list.
page The section’s own page.
pages Its pages in order, each { entry, href, label, formatted? }: the collection entry, its address, the one Ascribe’s links use, and its label. A section’s index.md is first among them; an own page beside the folder, such as checks/diagnostics.md for checks/diagnostics/, is among the parent’s pages instead, and the nested group sits right after it. A top section’s own page beside its folder, such as guides.md for guides/, is first in that section’s group, not in the root’s.
sections Its nested sections, as groups, in order. Empty in a site’s own list. A layout that draws one level leaves them out.

The build fails when the navigation names a page the collection has no entry for, which happens only when the site output is from another build than the collection; build again.

A site that passed its list first, sidebar(nav, entries, options), passes it as an option: sidebar(entries, { ...options, nav }).

A page’s label is its nav-label when it has one, and its title otherwise.

Previous and next

neighbors(entry, entries, options), from @ascribed/astro/content, gives a page’s previous and next: the pages before and after it in the navigation’s reading order, depth first, so a nested section’s pages follow its own page. entry is the page’s collection entry and entries the collection; the options are name and nav, as sidebar() takes them. With a versioned build, both are in the line entry is on.

---
import { getCollection } from "astro:content";
import { neighbors } from "@ascribed/astro/content";

const { previous, next } = neighbors(entry, await getCollection("docs"));
---
{previous && <a rel="prev" href={previous.href}>{previous.label}</a>}
{next && <a rel="next" href={next.href}>{next.label}</a>}

It returns { previous?, next? }, each { entry, href, label, formatted? }, as a page of a sidebar group is. The first page has no previous, and the last no next; a page the navigation leaves out, such as one that’s unlisted, has neither. With a site’s own list, nav, they’re the pages before and after it in the list’s groups, in order.

Styling

The elements render into the page (no shadow DOM), so your site’s styles apply to them, and they’re themed with CSS custom properties such as --ascribe-tip-color and --ascribe-tab-active-color. The element library reference lists them, and shows how to style your own note types and lifecycle states. Their names don’t change, but their default colors are Ascribe’s and may change in a release, as the changelog notes, so set the ones your site needs fixed.

Code block titles

A code block’s title, such as a @snippet’s title, is in its info string (```mjs title="astro.config.mjs"). So is a tested example’s tested, what it was tested with. Astro’s highlighting ignores both, so the integration adds a Shiki transformer that puts such a block in a figure with the title as its caption, and the tested line under it in a <div class="ascribe-code-tested">, whose “Tested with” lead-in is the element library’s stylesheet’s:

<figure class="code-title">
  <figcaption>astro.config.mjs</figcaption>
  <pre class="astro-code">…</pre>
</figure>

The figure has your browser’s default margins, and the caption no style of its own; style figure.code-title and its figcaption to suit your site. Titles need Astro’s default highlighting, Shiki; with syntaxHighlight: "prism" or false, they aren’t shown.

Code block languages

Astro’s highlighting marks a block’s language only as the <pre>’s data-language. A tool that converts a page’s HTML to text or Markdown, as many AI agents do before they read it, looks for a language-… class on the <code>, as Markdown renders it before highlighting, and without one writes the block with no language. So the integration adds a second Shiki transformer, whatever codeTitles is, that gives the <code> that class: <code class="language-sh">. A block with no language, or one in a language Shiki doesn’t know, which Astro highlights as plaintext, gets none.

Other Markdown processors

The integration supports Astro’s default processor (Sätteri) and unified(). Any other processor is an error at startup, since heading ids and image attributes would be lost.