On this page
  1. The re-exported /delivery/data surface
  2. createPublicRoutes
  3. Route-data types
  4. PublicRoutesConfig
  5. PublicRoutes
  6. EntryData
  7. CairnHead

Delivery (@glw907/cairn-cms/delivery)

This subpath is the public read model for a SvelteKit site. It carries the catch-all route loader, the public route-data types, and the feed, sitemap, and robots responders. Import it from a +server.ts or a +page.server.ts in the reader-facing site. The matching head component, CairnHead, lives one level down at /delivery/head. Anything proposed here must be a SvelteKit-route-facing reader, or already live on /delivery/data; a pure projection with no kit dependency belongs on /delivery/data alone, and the .svelte head component stays split onto /delivery/head so neither pulls Svelte into a plain-Node build.

import { createPublicRoutes } from '@glw907/cairn-cms/delivery';

The TypeScript types in src/lib are the source of truth, and the export-coverage gate checks every name here against them.


The re-exported /delivery/data surface

/delivery re-exports the entire /delivery/data surface: the index builders, the feed, sitemap, and robots builders and responders, the SEO head builder, and the small pure helpers. Those symbols are documented on the delivery-data reference and are not repeated here. This page covers only the names /delivery adds on top of that surface: the createPublicRoutes loader factory and its route-data types.

A SvelteKit site usually imports the shared symbols through this barrel. The feed.xml, feed.json, sitemap.xml, and robots.txt showcase servers all reach rssResponse, sitemapResponse, and robotsResponse through @glw907/cairn-cms/delivery.


createPublicRoutes

Stability tier: Scaffold API.

function createPublicRoutes(deps: PublicRoutesConfig): {
  entryLoad: (event: { url: URL }) => Promise<EntryData>;
  entries: () => { path: string }[];
  markdownEntries: () => { path: string }[];
  markdownLoad: (event: { url: URL }) => Promise<{ body: string }>;
};

createPublicRoutes exports its return type by name as PublicRoutes.

Build the public route loader for a site’s unified index. Pass the PublicRoutesConfig: the built site resolver, the render function, the origin, and the SEO defaults. The returned object carries entryLoad, the one loader the catch-all route calls, and entries, the prerender enumerator. entryLoad resolves one entry by request path and folds in the rendered html, the SEO head, and the hero; it throws error(404) on a miss.

markdownEntries and markdownLoad build the raw-markdown twin: one .md-suffixed path per entry whose frontmatter robots field doesn’t carry noindex, and a loader that resolves the same .md-suffixed request path back to the entry’s stored body, unrendered. markdownLoad throws error(404) on a miss, the same as entryLoad, and on a noindex entry, so the loader and the enumerator agree whether or not the site’s route is prerendered. Both read only through the injected SiteResolver, so a request for a path the resolver doesn’t carry gets error(404), and nothing outside the resolver’s own committed content can reach a response. Pair markdownEntries/markdownLoad with markdownResponse in a prerendered +server.ts, never a runtime one, so the served set is always what a build against main produced.

The showcase wires entryLoad and entries into its [...path] catch-all server. The +page.server.ts calls entryLoad and the +page.svelte renders the entry directly.

import type { PageServerLoad, EntryGenerator } from './$types';
import { createPublicRoutes } from '@glw907/cairn-cms/delivery';
import { site, ORIGIN, SITE_DESCRIPTION } from '$lib/content';
import { cairn, siteConfig } from '$lib/cairn.config';

export const prerender = true;

const routes = createPublicRoutes({
  site,
  render: cairn.rendering.render,
  origin: ORIGIN,
  siteName: siteConfig.siteName,
  description: SITE_DESCRIPTION,
  defaultImage: ORIGIN + '/og/default.png',
  feeds: { rss: ORIGIN + '/feed.xml', json: ORIGIN + '/feed.json' },
});

export const entries: EntryGenerator = () => routes.entries();

export const load: PageServerLoad = async ({ url }) => {
  // entryLoad resolves one entry by request path and throws error(404) on a miss. The returned
  // payload carries the rendered html, the SEO head, and the hero.
  return routes.entryLoad({ url });
};

Route-data types

The shapes the public loaders return and consume. A template reads the loaded data; a server passes the deps.

PublicRoutesConfig

Stability tier: Extension API.

interface PublicRoutesConfig {
  site: SiteResolver;
  render: SiteRender;
  origin: string;
  siteName: string;
  description: string;
  feeds?: { rss?: string; json?: string };
  defaultImage?: string;
  resolveMedia?: MediaResolve;
}

The injected dependencies for the public loaders. render turns an entry’s markdown into html, origin and feeds build the absolute URLs in the head, and description and defaultImage are the site-wide fallbacks for an entry that declares none. resolveMedia resolves a frontmatter media: hero reference to its delivery path; the site builds it from its committed media.json exactly as it builds the body resolver, and when it is absent no heroImage projection is derived.

PublicRoutes

Stability tier: Scaffold API.

What createPublicRoutes returns: the entry loader, the prerender path enumerator, and the markdown twin’s loader and enumerator, shown expanded in createPublicRoutes.

EntryData

Stability tier: Extension API.

interface EntryData {
  concept: string;
  entry: ContentEntry;
  html: string;
  canonicalUrl: string;
  seo: SeoMeta;
  newer?: ContentSummary;
  older?: ContentSummary;
  heroImage?: { url: string; absoluteUrl?: string; alt: string; caption?: string };
}

One entry’s data: the detail entry, its rendered html, its canonical URL, the SEO head, and the adjacent entries for prev and next links. entryLoad returns this shape. heroImage is a derived projection of the frontmatter image field, resolved through resolveMedia: url is the root-relative path for an <img> and absoluteUrl the origin-anchored form for the og:image. The canonical token is left untouched, so entry.frontmatter.image.src stays the media: token, and heroImage is undefined when no hero is set, media is off, or the reference does not resolve.


CairnHead

/delivery/head carries exactly the one Svelte component. A plain-data SEO helper, SeoMeta included, belongs on /delivery/data instead, even one this component itself consumes, so a plain-Node tool never resolves a component just to reach the data it renders.

Stability tier: Extension API.

import { CairnHead } from '@glw907/cairn-cms/delivery/head';
<CairnHead
  seo={SeoMeta}
  title={string | false}
  titleTemplate={(title: string) => string}
  markdownUrl={string}
/>

Render a page’s SEO head from a SeoMeta object into <svelte:head>: a title, meta tags, link tags, and one escaped JSON-LD script. The title renders from seo.title by default; title={false} lets the site own the <title>, and a string overrides it. titleTemplate carries the site’s own title-suffix convention (for example (t) => ${t} · 907.life\) and applies to seo.title only when title is left undefined, so an explicit title or title={false} still wins. markdownUrl, when passed, adds a rel="alternate" type="text/markdown" link pointing at the entry’s raw-markdown twin (markdownResponse); a site that has not wired the twin route, or an entry with no twin (a noindex entry, which markdownEntries excludes), passes nothing and the link is omitted. The component carries no CSS, so it pulls in no admin styles. The showcase mounts it from the seo field the catch-all loader returns.

<script lang="ts">
  import type { PageData } from './$types';
  import { CairnHead } from '@glw907/cairn-cms/delivery/head';

  let { data }: { data: PageData } = $props();
</script>

<CairnHead seo={data.seo} />

<article>
  <h1>{data.entry.title}</h1>
  {@html data.html}
</article>

CairnHead imports from @glw907/cairn-cms/delivery/head, the component-free split that keeps the data surface node-safe. A .svelte component would pull Svelte into the module graph, so the plain-data builders live on /delivery/data and the one component lives on its own /delivery/head entry. A plain-Node tool can import the builders without ever resolving a component.

Edit this page on GitHub(opens in a new tab)