Skip to content

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
        }
    )
Terminal
✗ 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:

Terminal
✗ 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
)

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:

Terminal
! 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.

Edit this page