> For the complete documentation index, see [llms.txt](https://ascribed-dev.com/docs/llms.txt). This documentation describes Ascribe 0.4.4. Anything marked in main, not yet released isn't in that release yet.

# Astro

> How-to · Set up an Astro site that publishes an Ascribe project, with @ascribed/astro.

`@ascribed/astro` publishes an Ascribe project as an [Astro](https://astro.build) site. It runs `ascribe build --emit site` before Astro loads content, gives you a content collection of the built pages with a schema generated from `ascribe.toml`, applies heading ids and image attributes in Astro's own Markdown pipeline (so Astro's table of contents and image processing still work), loads the element library, and serves the files pages link to. In `astro dev`, it rebuilds as you edit.

It supports Astro 7.3.5 and later 7.x releases.

This guide sets a site up. [The `@ascribed/astro` reference](https://ascribed-dev.com/docs/astro/reference.md) has its options, its components, and everything it does in `astro build` and `astro dev`.

## Set up a site

These steps add Ascribe to an Astro project. [`examples/astro-site`](https://github.com/ascribed-dev/ascribe/tree/main/examples/astro-site) is a complete site built this way.

### 1\. Install

A reader sees one of the next 2 sections at a time, chosen by package manager.

#### Package manager: npm

```sh
npm install @ascribed/astro
```

#### Package manager: pnpm

```sh
pnpm add @ascribed/astro
```

It brings `@ascribed/cli`, the `ascribe` command for your platform, and `@ascribed/elements`. Astro's image processing also needs `sharp` (`npm install sharp`) if your project doesn't have it.

### 2\. Write `ascribe.toml`

Put `ascribe.toml` in the Astro project's root, beside `astro.config.mjs`. Its `[consumer]` settings are the site's routing, because Ascribe writes every link: the integration gives Astro its `site`, `base`, and `trailingSlash` from them, so `astro.config.mjs` can leave all three out ([a home page in front of the docs](https://ascribed-dev.com/docs/astro/more-than-docs.md#a-home-page-in-front-of-the-docs) says how `base` can differ from `base-path`):

```toml
spec = "0.1"

[project]
content-root = "content"
output-dir = ".ascribe/build"

# The site's routing: `@ascribed/astro` gives Astro its `site` and
# `trailingSlash` from here, and checks that Astro's `base` is base-path or a
# path above it.
[consumer]
profile = "astro"
site = "https://docs.example.com"
base-path = "/docs/"
trailing-slash = "never"
```

The source pages go in `content/`. If your Ascribe project lives elsewhere, such as in a folder beside the site's, set the integration's `project` option to its directory (step 3). The [`ascribe.toml` reference](https://ascribed-dev.com/docs/content-model.md) has everything else it can declare.

### 3\. Add the integration

```mjs title="astro.config.mjs"
import { defineConfig } from "astro/config";
import ascribe from "@ascribed/astro";

// The integration gives Astro its `site`, `base`, and `trailingSlash` from
// ascribe.toml's [consumer]. Any of them set here must agree with it, or the
// integration fails the build.
export default defineConfig({
  integrations: [ascribe({ build: "site" })],
});
```

`build` names the [build](https://ascribed-dev.com/docs/glossary.md#build) in `ascribe.toml` whose output the site shows. A project without `[builds]` has one, `site`.

### 4\. Write the site's files

A site needs a content collection of the built pages, a route for them, and a layout that loads the elements; this one has a sidebar and a stylesheet too. One command writes the four files, in the Astro project's root:

A reader sees one of the next 2 sections at a time, chosen by package manager.

#### Package manager: npm

```sh
npx @ascribed/astro init
```

#### Package manager: pnpm

```sh
pnpm exec ascribe-astro init
```

It writes `src/content.config.ts`, `src/pages/[...slug].astro`, `src/layouts/Docs.astro`, and `src/styles/docs.css`, and from then on they're yours to change; it never writes over a file, and if any of them is there, it writes nothing. With the Ascribe project elsewhere, say where with `--project ../docs`, and with several builds in `ascribe.toml`, say which the site shows with `--build`. The sections below say what each file does, as `init` writes them for the `ascribe.toml` above.

#### The collection

```ts title="src/content.config.ts"
import { defineCollection } from "astro:content";
import { ascribeCollection } from "@ascribed/astro/content";
// Generated by `ascribe build --emit site`; the integration runs it first.
import { schema } from "../.ascribe/build/site/site/_ascribe/schema.ts";

export const collections = {
  docs: defineCollection(ascribeCollection({ schema })),
};
```

The schema's path is `<output-dir>/<build>/site/_ascribe/schema.ts`, in the directory holding `ascribe.toml`. Importing it by path keeps its exact types, so `entry.data` is typed from your content types. If you change `[project] output-dir`, the build, or the integration's `project`, change this line too.

If the Ascribe project is outside the Astro root (`project: "../docs"`), import the copy of the schema the integration writes inside it instead, `.astro/integrations/_ascribed_astro/schema.ts`, which is `../.astro/integrations/_ascribed_astro/schema.ts` from `src/content.config.ts`, as `init --project` does. The schema imports `astro/zod`, which TypeScript can only find from inside the Astro project, so type-checking (`astro check`, `tsc`) fails on the original there; Astro's own build works with either. The integration updates the copy after each build.

#### The route

A page's URL is the base path plus its entry id, and the root page, `content/index.md` (entry id `index`), is at the base path itself. That's what Ascribe's links point to:

```astro title="src/pages/[...slug].astro"
---
import { getCollection, render } from "astro:content";
import Docs from "../layouts/Docs.astro";

// A page's route is the base path plus its entry id, and the root page (id
// `index`) is at the base path itself: what Ascribe's router computes for
// every link it writes.
export async function getStaticPaths() {
  const entries = await getCollection("docs");
  return entries.map((entry) => ({
    params: { slug: entry.id === "index" ? undefined : entry.id },
    props: { entry },
  }));
}

const { entry } = Astro.props;
const { Content, headings } = await render(entry);
---

<Docs entry={entry} headings={headings}><Content /></Docs>
```

Entry ids are Astro's own: `Guides/My Setup.md` is `guides/my-setup`. The route is at the root of `src/pages/` because the base path is Astro's `base`; with the docs under it, move the route to their folder ([a home page in front of the docs](https://ascribed-dev.com/docs/astro/more-than-docs.md#a-home-page-in-front-of-the-docs)).

#### The layout

A layout loads the element library, and shows the sidebar, the page's [availability](https://ascribed-dev.com/docs/glossary.md#availability), and its table of contents:

```astro title="src/layouts/Docs.astro"
---
import { getCollection, type CollectionEntry } from "astro:content";
import type { ComponentProps } from "astro/types";
import Availability from "@ascribed/astro/Availability.astro";
import Elements from "@ascribed/astro/Elements.astro";
// Everything the head says about the page, from its record.
import Head from "@ascribed/astro/Head.astro";
// With a versioned build, the lines that have the page, and on an older line
// what it is and where the current one is.
import Versions from "@ascribed/astro/Versions.astro";
import VersionBanner from "@ascribed/astro/VersionBanner.astro";
import {
  neighbors,
  sidebar,
  type Formatted,
  type SidebarGroup,
  type SidebarPage,
} from "@ascribed/astro/content";
// The layout's styles. To style the site your own way, change the file, or
// delete it and this line.
import "../styles/docs.css";

interface Props {
  entry: CollectionEntry<"docs">;
  headings: { depth: number; slug: string; text: string }[];
}

/**
 * What the layout reads of a page's frontmatter: `title`, which every content
 * type has, and what a type may have. `title` is plain text; `formatted.title`
 * is the title as HTML, with its code spans as <code>, when the content model
 * sets `inline = "code"` on `title`.
 */
interface Page {
  title: string;
  available?: ComponentProps<typeof Availability>["available"];
  formatted?: { title?: string };
}

const { entry, headings } = Astro.props;
const page = (of: CollectionEntry<"docs">) => of.data as Page;
const escape = (text: string) =>
  text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
/** A page's title as HTML: its formatted form, or its plain text escaped. */
const titleHtml = (of: CollectionEntry<"docs">) =>
  page(of).formatted?.title ?? escape(page(of).title);
/** A sidebar label as HTML: its formatted form, or its plain text escaped. */
const labelHtml = (of: { label: string; formatted?: Formatted }) =>
  of.formatted?.label ?? escape(of.label);
const { available } = page(entry);
// The sidebar: the project's navigation, in ascribe.nav's order or the
// default one, each page with its address and label, and a nested section's
// pages right after its own page, each with its depth. With a versioned build,
// it shows the pages of the line this page is in. Previous and next are the
// pages before and after this one in the navigation's reading order.
//
// The default order puts a section's pages by content type, then title, so
// "Configure" comes before "Install". To choose another, write ascribe.nav
// beside ascribe.toml, or drag the pages in the editor: the docs' "Put your
// pages in order" says how.
const entries = await getCollection("docs");
const groups = sidebar(entries, { entry });
const { previous, next } = neighbors(entry, entries);

/** A sidebar page, with how many sections deep it is: 0 in a top section. */
interface Item {
  page: SidebarPage<CollectionEntry<"docs">>;
  depth: number;
}

/**
 * A group's pages in order, each nested section's right after its own page,
 * or after the group's pages when its own page is its `index.md`.
 */
function items(group: SidebarGroup<CollectionEntry<"docs">>, depth = 0): Item[] {
  const placed = new Set<SidebarGroup<CollectionEntry<"docs">>>();
  const out: Item[] = [];
  for (const page of group.pages) {
    out.push({ page, depth });
    for (const section of group.sections) {
      if (section.page?.entry !== page.entry) continue;
      placed.add(section);
      out.push(...items(section, depth + 1));
    }
  }
  for (const section of group.sections) {
    if (!placed.has(section)) out.push(...items(section, depth + 1));
  }
  return out;
}
---

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <Head entry={entry} />
    <Elements />
  </head>
  <body>
    <details class="menu">
      <summary>Menu</summary>
      <nav aria-label="Docs">
        {
          groups.map((group) => (
            <section>
              <h2 set:html={labelHtml(group)} />
              <ul>
                {items(group).map(({ page: item, depth }) => (
                  <li data-depth={depth}>
                    <a
                      href={item.href}
                      aria-current={item.entry.id === entry.id ? "page" : undefined}
                    >
                      <Fragment set:html={labelHtml(item)} />
                    </a>
                  </li>
                ))}
              </ul>
            </section>
          ))
        }
      </nav>
    </details>
    <main>
      <VersionBanner entry={entry} />
      <h1 set:html={titleHtml(entry)} />
      <Availability available={available} />
      <Versions entry={entry} />
      <div class="page">
        {
          headings.length > 0 && (
            <nav aria-label="On this page">
              <ul>
                {headings.map((heading) => (
                  <li data-depth={heading.depth}>
                    <a href={`#${heading.slug}`}>{heading.text}</a>
                  </li>
                ))}
              </ul>
            </nav>
          )
        }
        <article><slot /></article>
      </div>
      <nav aria-label="Previous and next">
        {
          previous && (
            <a rel="prev" href={previous.href}>
              Previous: <Fragment set:html={labelHtml(previous)} />
            </a>
          )
        }
        {
          next && (
            <a rel="next" href={next.href}>
              Next: <Fragment set:html={labelHtml(next)} />
            </a>
          )
        }
      </nav>
    </main>
  </body>
</html>
```

`<Elements />` loads the element library, and `<Head entry={entry} />` writes the page's head from its record. The layout is yours: replace any part of it with your own components; the table of contents is `headings` from `render`. [The components](https://ascribed-dev.com/docs/astro/reference.md#components) lists what `@ascribed/astro` gives a layout, and what each entry's data has for it.

#### The stylesheet

`src/styles/docs.css`, which the layout imports, gives it a readable column in the system's font, the sidebar beside the page on a wide screen and behind a **Menu** on a phone, the table of contents beside the page where there's room and above it where there isn't, and both color schemes, as the reader's system is. Its colors and spaces are the element library's `--ascribe-*` properties, so setting one restyles the layout and the elements together ([Element library reference](https://ascribed-dev.com/docs/astro/elements.md)); the page's background and text are the browser's own light or dark.

It's a starting point, not a theme: change it as you like. To style the site from scratch, delete the file and its `import` line in the layout; the layout's markup reads in order without it.

#### The sidebar

The layout draws the sidebar from the project's [navigation](https://ascribed-dev.com/docs/navigation.md), with [`sidebar()`](https://ascribed-dev.com/docs/astro/reference.md#the-sidebar), and the links at the page's foot with [`neighbors()`](https://ascribed-dev.com/docs/astro/reference.md#previous-and-next), so the site keeps no list of its own. It lists a nested section's pages in their top section's group, each `<li>` with a `data-depth` for your stylesheet to indent:

![The starter's sidebar, with an indent for each depth: Start, with Loom documentation; Guides, with Set up Loom, Troubleshoot Loom, Work in teams, and, indented under Work in teams, the nested section's page Share a loom, the page being read, in bold; and Reference, with loom.yaml options.](https://ascribed-dev.com/docs/_ascribe/files/images/starter-sidebar-light.png)
![The starter's sidebar, with an indent for each depth: Start, with Loom documentation; Guides, with Set up Loom, Troubleshoot Loom, Work in teams, and, indented under Work in teams, the nested section's page Share a loom, the page being read, in bold; and Reference, with loom.yaml options.](https://ascribed-dev.com/docs/_ascribe/files/images/starter-sidebar-dark.png)

To change the order or a label, write `ascribe.nav`, or drag the pages in the editor: [Put your pages in order](https://ascribed-dev.com/docs/navigation/order.md).

### 5\. Build

```sh
npx astro dev     # rebuilds as you edit
npx astro build
```

`astro dev` prints the address it serves, such as `http://localhost:4321/`. Open it with your `base-path` after it, `http://localhost:4321/docs/` for the `ascribe.toml` above, and the site is there. `astro build` writes it to `dist/`, for your host.

## See also

- [Put the docs in a site with more than the docs](https://ascribed-dev.com/docs/astro/more-than-docs.md): a home page of your own at `/`, with the docs at `/docs/`.
- [Publish for AI agents](https://ascribed-dev.com/docs/astro/agents.md): `llms.txt` and each page's Markdown, with the site.
- [Serve the Markdown versions](https://ascribed-dev.com/docs/astro/serve-markdown.md): what your host does for agents.
- [The `@ascribed/astro` reference](https://ascribed-dev.com/docs/astro/reference.md): the options, the components, and everything the integration does in `astro build` and `astro dev`, with [versions](https://ascribed-dev.com/docs/astro/reference.md#versions) and [permalinks](https://ascribed-dev.com/docs/astro/reference.md#permalinks).
- [Element library reference](https://ascribed-dev.com/docs/astro/elements.md): styling the elements.
