Skip to content

Content

Remotes

remotes(site) creates nav, meta, page and search as Kit remotes, prerendered by default and live when you ask. The doc body is baked into a region ticket, so the corpus stays on the server.

On this page

The site value (docs on this page) holds the corpus, so it is server-only, yet the page still needs its data. remotes() creates the crossing: the sidebar tree, the shell bundle, the view of one page, and search, as Kit remotes in a .remote.ts. The browser imports only these remotes, and the collection never comes along with them.

Every site() site needs them, because they are the one supported path from the server-side site to a page. Rendering the nav or a body without them would mean importing the collection into a client module, which is exactly the leak the server-module rule exists to prevent.

Create them

import { remotes } from '@ogygia/content/server';
import { docs } from './site.server';

export const { nav, meta, page, search } = remotes(docs, { base: '/docs' });

This gives you four remotes, each a plain async function that the page awaits:

  • nav(slug?) returns the sidebar tree with hrefs resolved for base. On a dimensioned site, pass the current slug (or a bare coordinate prefix such as 'v1/') so that the tree reflects that coordinate.
  • meta(slug?) returns the full shell bundle { nav, switcher, data } in one prerendered call. Pass it straight to <DocsShell {meta}>, and the layout never imports the corpus.
  • page(slug) returns the view of one page: data, meta, headings, prev/next, and the baked body.
  • search(q) returns ranked hits from search.

Prefer `meta` over `nav` for shells

nav exists for custom chrome that needs only the tree. A shell needs the tree, the switcher and the site facts, and meta delivers all three in one prerendered payload, which means one await and one cache entry.

The body crosses as a ticket

A body is a live region rendered in the same SSR pass, so it cannot be serialized as it is. page awaits it, which bakes its SSR HTML into the region ticket (HTML only, with no source and no corpus), and the page renders the ticket with <Region> through <Doc>. Content collections explains the baking in more detail.

<script>
	import { Doc } from '@ogygia/content';
	import * as docs from '$lib/site.remote';

	const view = (await docs.page(slug))!;   // view.entry.body is a baked ticket
</script>

<Doc {view} />

This is why the docs corpus can be megabytes while each page ships only its own baked HTML.

Prerender or live

By default nav, meta and page prerender, meaning they are static payloads computed at build time. Some sites need them to be dynamic, for an authenticated preview or content that varies per request, and the modes option sets this per remote:

export const { nav, meta, page, search } = remotes(docs, { base: '/docs' });
// nav/meta/page are baked at build and served static; search runs per request
export const { nav, meta, page, search } = remotes(docs, {
	base: '/docs',
	modes: { page: 'query', nav: 'query' }   // per-request; the site's `context` gates what renders
});
  • 'prerender' (the default for nav / meta / page) bakes the payload at build time and serves it statically. It is required when the pages that await it are prerendered.
  • 'query' runs on the server for each request, and pairs with site({ context }) for previews and roles.

search is always a query, since a search is dynamic by nature. For a fully static search box, ship the index instead and query it in a client worker, in which case you don’t create search at all.

Prerender entries

The route still needs to know which slugs to bake. docs.entries() returns every leaf in the outline, including declared old addresses, so redirect stubs are baked too:

import { docs } from '$lib/site.server';

export const prerender = true;
export const entries = docs.entries;
export const load = docs.load;   // the 404 guard + alias redirects + per-page checks

docs.load is the piece people tend to forget. It turns an unknown slug into a real 404 rather than an empty page, serves a baked 308 for every address an entry declares in redirect_from, and runs the per-page checks in dev.

Do / don’t

  • Do create remotes in a .remote.ts and import only those from pages, which is all it takes to keep the corpus safe.
  • Do let page bake the body, because the await is what turns a live region into something that can cross the wire.
  • Don’t return a raw entry.body from a remote or a server load. It is a same-pass render, and ogygia stops you with an error that says so.
  • Don’t use query mode by default. Prerendered pages are faster and cheaper, so switch a remote to query only when its content really varies per request.