Content System
Two explicit responsibilities
ContentSource is the boundary for finding and reading source data. It owns scanning, reading, identity, and source metadata such as mtime, size, ETag, or hashes. FileSystemContentSource is the normal local implementation.
ContentManager owns the meaning of that data: parsing, Markdown/HTML pipeline execution, post processing, plugin orchestration, manifest creation, and content-graph construction. It should not grow direct filesystem behavior that bypasses ContentSource.
Processing model
scan/read source
-> resolve public locations (default resolver + plugin hooks)
-> parse post -> process post -> create manifest -> graph
|
`--- plugin lifecycle/content hooks ---'
Public locations are resolved before any content that needs a URL is processed, so a consumer never has to invent one. Plugin hooks observe or extend defined phases such as configuration resolution, content loaded, parsed/processed posts, manifest creation, and build start/end. Keep source I/O, semantic interpretation, and rendering distinct so a remote source can replace the filesystem source without changing Core policy.
Public location and URLs
A content entry separates three notions of identity:
- slug — the internal lookup key used by
contentIndex, the manifestbySlugmap, the content graph, and application-level selection keys such as/explore?note=<slug>. - permalink — the resolved canonical public URL used in article links, feeds, sitemaps, and metadata.
- content ID — an optional, source-authored stable identity exposed as
ContentManifestEntry.contentIdand indexed byContentManifest.byContentId. It is independent of both the slug and all public locations.
They are distinct. A consumer that needs a public URL reads ContentManifestEntry.permalink (also available as entry.publicLocation); it must not build a URL from a slug or filesystem path. Turning a slug into a URL is the Core default resolver's job alone.
Resolution is a single, stateless pipeline:
- Core seeds every entry with the official default resolver,
resolveDefaultContentLocation(content), wherecontentis aContentLocationInput(slug,path,markdown). The default policy isindex->/and every other entry ->/{slug}. This is the Core default public-location policy, not a compatibility fallback. - Each enabled plugin may replace locations through the optional
resolveContentLocationshook, which receives theContentLocationInputlist and returnsContentPublicLocationvalues. Plugin-specific URL strategies stay inside the plugin. ContentManager.getContentLocations()returns the resolvedReadonlyMap<string, ContentPublicLocation>. AContentPublicLocationcarries the canonicalpermalink, optionalredirects, and optional opaquemetadatathat Core does not interpret.
The manifest stores the resolved result: ContentManifestEntry.permalink and .publicLocation, plus the byPermalink index and the redirects map. The content graph and readOnlyContentGraph(source, locations) consume those resolved entries rather than deriving URLs. If a public location is not resolved for an entry, Core raises an explicit error instead of falling back to a slug-derived URL.
Stable content IDs
Set the standard id frontmatter field when content needs an identity that
survives a rename, permalink change, alias, or redirect. IDs are optional, so
existing content without id has no generated substitute and keeps its current
behavior. Core never uses a slug, path, permalink, alias, or redirect as a
stable ID.
---
id: note-7f4e9b
---
Values must be non-empty, trimmed strings, and each explicit ID must be unique within a manifest; invalid or duplicate IDs fail the manifest build rather than silently selecting an identity.
Manifest, graph, and runtime
The manifest is the generated content representation used by the application. The content graph represents relationships and can be extended through the plugin graph contract. Reading a runtime manifest is not an explicit build. Incremental build state belongs only to the explicit build path and is never a mutable Worker runtime dependency.
Content queries
Core exposes a portable query layer over resolved manifest entries:
queryContentEntries(entries, spec)filters by tags, folder, frontmatter, and date range, applies one or more sort keys, and slices the result withlimit/offset.queryContentPage(entries, spec)applies the same selection and returns the page slice together withpagemetadata (page,pageCount,hasPrevious,hasNext);resolveContentQueryPagination(total, spec)computes that metadata alone.groupContentEntries(entries, groupBy, options)runs the same selection and groups the result by tags, folder, date granularity (year/month/day), or a frontmatter field.
Both functions operate on ContentManifestEntry values, so links use the resolved permalink; a query never builds a public content URL from a slug. Applications and plugins compose these functions to build listing pages and taxonomy views, while Core keeps ownership of manifest and graph construction rather than routing.
Content collections
buildContentCollections(entries, definitions) turns the same query selection into listing collections. A definition declares a kind, a groupBy (tags, folder, date, or a frontmatter field), a site-local basePath, optional filter/sort/order values, and optional resolveTitle/resolvePath builders. Every generated ContentCollection carries the group value, the resolved path, a title, and its entries in query order.
This is the shared mechanism behind taxonomy, folder, and archive listings. A tag definition groups by tags under /tags; an archive definition groups by date under /archive; both are produced by the same call. Routing stays in the application, while the collection contract and the query engine stay in Core. Listing entries still link through ContentManifestEntry.permalink and never construct a URL from a slug.
A definition may set pageSize to split a collection across pages. Each page is emitted as its own ContentCollection whose path is the collection path plus /page/<n> for later pages, and its page metadata carries current, count, size, total, previousPath, and nextPath for building navigation.
Correctness rules
- Preserve canonical content identity across source, manifest, and graph.
- Treat slug and permalink as separate concepts: obtain public URLs only from the resolved
ContentPublicLocation. - Treat source metadata as change evidence, not universally reliable truth.
- Make publication/exclusion policy visible in configuration.
- Return diagnostics for recoverable user-facing problems; do not silently omit content.
- Keep graph extensions deterministic for identical inputs.
See Configuration, Build system, and Plugin system.