Content and collections
Content lives in collections. A collection says where files come from, what front matter they must have, where their pages end up and how they are rendered. You write all of that in F#, so the compiler checks it.
let docs = Theme.docs theme "content"
You can have as many as you need: guides in one, a changelog in another, an API reference generated by a plugin in a third. They are independent - different front matter, different layout, different routes.
Front matter is typed
Every collection declares the shape its pages must have, and a page that does not match stops the build:
type DocFrontMatter =
{
Title: string
Description: string option
Order: int option
}
let decoder: Decoder<DocFrontMatter> =
Decode.object (fun get ->
{
Title = get.Required.Field "title" Decode.string
Description = get.Optional.Field "description" Decode.string
Order = get.Optional.Field "order" Decode.int
}
)
✗ content/guide/writing.md(2,1): error nacara/front-matter-invalid: Missing required field 'title' (at 'title')
The line it names is the line in your file. A layout then reads context.FrontMatter.Title and knows it is there.
Theme.docs uses the front-matter type that comes with the default theme - see its fields. Write your own when you need more than it offers.
Which files a collection reads
A collection reads every file under its directory that a plugin knows how to read. You do not declare which: register the markdown plugin and .md files become content, register the literate plugin and .fsx files do too.
Use Collection.source when you want something narrower - a directory that holds code samples beside its pages, for example:
|> Collection.source "content" [ "**/*.md" ] // only markdown, even with literate registered
The formats come from plugins. The markdown plugin claims .md, the literate plugin claims .fs and .fsx, and a file whose extension nothing claims fails the build:
✗ error nacara/unknown-front-matter-format: Nothing knows how to read front matter for '.md'
From a file to a URL
A file's path under the collection root becomes its route. The extension goes, index disappears into the directory holding it, and each segment is slugged:
content/guide/getting-started.md → /guide/getting-started/
content/guide/index.md → /guide/
content/index.md → /
The page is then written as guide/getting-started/index.html, so its URL ends in a slash. That is the default, and most sites never touch it.
Moving a whole collection
Collection.routePrefix puts every route of the collection under a path, whatever decides those routes:
Theme.docs theme "content"
|> Collection.routePrefix "docs" // /guide/… becomes /docs/guide/…
Deciding a route yourself
Collection.route replaces the mapping. It is a function from what is known about a page - its path, its locale, its front matter - to the route to publish it at, so it can read anything the page carries:
|> Collection.route (fun page ->
// Slug here is a field of this collection's own front matter type.
match page.FrontMatter.Slug with
| Some slug -> Route.create page.Locale [ "blog"; slug ]
| None -> Collection.defaultRoute page
)
Collection.defaultRoute is the mapping described above, so a rule of your own can fall back to it rather than reimplement it. Two pages landing on the same route fail the build with nacara/duplicate-route.
Route.file publishes a page at a literal path, for the files a host expects to find exactly there - 404.html, robots.txt - where 404/index.html would not be found. Put it behind a condition, or every page in the collection ends up at that same path:
|> Collection.route (fun page ->
if RelativePath.value page.RelativePath = "404.md" then
Route.file page.Locale "404.html"
else
Collection.defaultRoute page
)
Links follow files, not URLs
A link in a page names the file it points at, the way it would on GitHub, and the engine rewrites it to that page's URL when it builds:
See [the layout](project-layout.md) and [its front matter](project-layout.md#front-matter).
<a href="/Nacara/guide/project-layout/">the layout</a>
<a href="/Nacara/guide/project-layout/#front-matter">its front matter</a>
The base URL, a route prefix and a version prefix are all applied for you, and moving a page fixes its incoming links instead of breaking them. A link the engine cannot resolve is reported instead of written out:
! warning markdown/link-target-missing: This link points at an unknown page 'project-layout.md'
hint: A link names a file: one beside this page, or one from the project root with a leading '/'.
Anchors are checked too - markdown/anchor-missing when the heading a link names is not on the page it points at. Set StrictLinks to turn both warnings into errors.
Ordering
Collection.order reads a position out of front matter, and the sidebar and the previous/next links follow it:
|> Collection.order (fun frontMatter -> frontMatter.Order |> Option.defaultValue 0)
A section falls back to this order when nothing else says otherwise. Declare a menu for a section and the menu decides the order instead - in the sidebar and in the previous/next links - so those pages need no order at all. These docs declare menus for their two sections and use none.
Content that is not on disk
A collection can generate its pages instead of reading them. That is how the changelog plugin publishes CHANGELOG.md files:
|> Collection.producer "api" (fun context ->
assemblies
|> List.map (fun assembly ->
GeneratedContent.create $"{assembly.Name}.md" (render assembly)
|> GeneratedContent.dependsOn [ assembly.Path ]
)
)
Generated pages go through everything a file does: the same transforms, routing, link checking and layout. Use dependsOn so watch mode rebuilds them when their source changes.
Table of contents
Headings are collected while a page renders and handed to the layout, which is where the right-hand column of this page comes from. The markdown plugin decides which levels are collected, and how a page overrules the site's choice - see the markdown plugin.