SSG output primitives#
Custom hosts that set ssg: false keep their own page templates. They can
still ask Ox Content to plan and emit:
- content-addressed resource fingerprinting and URL rewriting
- self-hosted font and Iconify asset files
- Markdown companion files for host-rendered HTML pages
- RSS / Atom / JSON feeds and sitemap metadata
- git-derived
lastmod
None of this requires the default theme or buildSsg(). The same option
objects used by oxContent() / buildSsg() configure the composable path.
import {
planSsgOutputs,
renderFeedFiles,
writeResourceFiles,
writeMarkdownCompanions,
writeFeedFiles,
writeSiteMapFiles,
writeSelfHostedAssets,
} from "@ox-content/vite-plugin";
const plan = planSsgOutputs({
outDir,
srcDir,
root,
options: {
ssg: {
enabled: false,
markdownSource: true,
lastUpdated: true,
siteUrl: "https://example.com",
siteName: "Docs",
},
resources: { dedupe: true },
feeds: true,
siteMaps: true,
},
pages: [
{
inputPath: path.join(srcDir, "guide.md"),
urlPath: "guide",
outputPath: path.join(outDir, "guide", "index.html"),
html: hostRenderedHtml,
source: markdownSource,
title: "Guide",
},
],
});
await writeResourceFiles(plan.resources);
await writeSelfHostedAssets(plan.selfHostedAssets);
await writeMarkdownCompanions(plan.markdownCompanions);
const feedFiles = await renderFeedFiles(plan.feeds);
await writeFeedFiles(plan.feeds);
await writeSiteMapFiles(plan.siteMaps);
ssg: false (the boolean) turns SSG off and also clears markdownSource,
lastUpdated, and siteUrl. Use ssg: { enabled: false, ... } when those
fields should still resolve.
Collection assets with custom aliases#
planCollectionAssets() handles files that do not belong to a Markdown page's
resource flow. Give it explicit collection source paths and the public aliases
your host owns. It hashes and deduplicates the content target, URL-encodes
aliases safely, and keeps the same manifest for production and development.
import {
createCollectionAssetsMiddleware,
planCollectionAssets,
rewriteCollectionAssetUrls,
writeCollectionAssets,
} from "@ox-content/vite-plugin";
const collectionAssets = await planCollectionAssets({
root,
assets: [
{
sourcePath: "src/content/showcase/project-cover.jpg",
publicPath: ["/works/showcase/assets/project-cover.jpg", "/works/showcase/cover.jpg"],
},
],
});
await writeCollectionAssets({ manifest: collectionAssets, outDir });
viteServer.middlewares.use(createCollectionAssetsMiddleware(collectionAssets));
const rewritten = rewriteCollectionAssetUrls({
html: hostRenderedHtml,
pagePath: "/works/showcase/",
manifest: collectionAssets,
});
writeCollectionAssets() writes each distinct content hash below contentDir
("/assets/content" by default) once, then hard-links aliases with a copy
fallback. sourcePath must stay beneath root; malformed URL encoding, path
traversal, and aliases outside the output directory are rejected.
rewriteCollectionAssetUrls() is the pure HTML step for custom renderers. It
parses an HTML fragment by default, resolves href, src, and poster
against the supplied page path, and rewrites known aliases to the manifest's
content-addressed target. Query strings and fragments are kept. Unknown aliases,
external origins, fragment-only links, non-HTTP schemes such as data:,
mailto:, and javascript:, and malformed attributes are left unchanged. Pass
origin to treat same-origin absolute URLs like root-relative paths; rewritten
values are still emitted as URL paths. Pass document: true when the input is a
full HTML document.
External feeds in a custom host#
The built-in blog renderer and a custom blog index can share the same RSS/Atom
loader without building pages. loadBlogFeedEntries() accepts the public blog
feed configuration, keeps the existing timeout, redirect, size, and safe-network
checks, and returns normalized external entries plus warning/fatal diagnostics.
import { loadBlogFeedEntries, mergeBlogFeedEntries } from "@ox-content/vite-plugin";
const external = await loadBlogFeedEntries({
sources: [
{ url: "https://example.com/feed.xml", language: "en", author: "Ada" },
{ url: "https://example.jp/atom.xml", language: "ja", onError: "warn" },
],
});
if (external.fatals.length) {
throw new Error(external.fatals.join("\n"));
}
const entries = mergeBlogFeedEntries(localEntries, external.entries);
Entries include title, url, stable id, optional canonical, date,
language, author, summary, external, and sourceUrl. Empty sources make
no network request, repeated source URLs are fetched once per call, and warning
sources do not discard unrelated successful sources. Merge order matches the
built-in blog: local entries win on duplicate canonical URL or stable id, then
items sort newest first.
Redirect outputs without built-in pages#
Custom hosts can plan and explicitly write redirect outputs with the same configuration the built-in SSG uses. Planning is side-effect free; writing emits only the selected redirect files and will not replace an existing host-rendered HTML page.
import { planRedirectOutputs, writeRedirectOutputs } from "@ox-content/vite-plugin";
const redirectInput = {
redirects: {
provider: "cloudflare",
html: false,
map: { "/old-guide": "/guide" },
},
routes: [{ path: "/guide", aliases: ["/old"], redirect: "/retired" }],
occupiedPaths: hostPagePaths,
base: "/docs/",
} as const;
const redirectPlan = planRedirectOutputs(redirectInput);
await writeRedirectOutputs({ ...redirectInput, outDir });
planRedirectOutputs() accepts the public redirects option, custom host
routes, occupied host paths, base, and optional CI env. Outputs are
discriminated as html, provider, headers, or json; provider outputs name
cloudflare or netlify instead of exposing the internal file-plan field.
_redirects, _headers, and redirects.json are root host files. Ox Content
writes them only when requested by this API or the built-in SSG, so a custom host
that already owns those files should merge or choose one owner before writing.
API#
| Function | Role |
|---|---|
planSsgOutputs |
Build writer inputs from host pages and the same option objects buildSsg() reads. |
writeResourceFiles |
Fingerprint page-bundle assets and rewrite host HTML URLs. |
writeSelfHostedAssets |
Write self-hosted __ox_icons__ and __ox_fonts__ files for a custom host. |
planCollectionAssets |
Plan content-addressed targets from explicit collection source-to-public mappings. |
writeCollectionAssets |
Write deduplicated collection targets and public hard-link/copy aliases. |
createCollectionAssetsMiddleware |
Serve the planned aliases and content targets in a development host. |
rewriteCollectionAssetUrls |
Rewrite host-rendered HTML aliases to collection content targets. |
loadBlogFeedEntries |
Load public RSS/Atom entries for a custom blog index without page rendering. |
mergeBlogFeedEntries |
Merge host entries and external entries with built-in blog precedence. |
planRedirectOutputs |
Preview redirect HTML, provider, headers, and JSON outputs without writing files. |
writeRedirectOutputs |
Emit selected redirect outputs beside host-rendered pages. |
writeMarkdownCompanions |
Write original Markdown beside host-rendered pages. Reuses the copy-as-markdown writer. |
renderFeedFiles |
Render RSS / Atom / JSON feed files without filesystem writes. |
writeFeedFiles |
Write RSS / Atom / JSON feeds, including named feeds. |
writeSiteMapFiles |
Write sitemap.xml, robots.txt, and llms.txt. |
resolveGitLastmod |
Return a file's latest git commit time in milliseconds, or undefined. |
lastUpdated on a page is used as-is. When it is omitted and ssg.lastUpdated
or siteMaps is on, the planner calls resolveGitLastmod(inputPath, root).
Hosts can skip the planner and call a writer with the same resolved option
objects buildSsg() already uses (resolveResourcesOptions,
resolveFeedsOptions, resolveSiteMapsOptions,
resolveMarkdownSourceOptions). Use resolveSelfHostedAssetManifest() when a
custom renderer needs the matching stylesheet and preload tags for <head>.