Architecture¶
How Folia is built, for someone who wants to change it. For how to set up and test a change, see development setup.
The crates¶
The Cargo workspace has one crate per collection (the code calls a collection a mode) and a few
shared ones. Each collection crate depends on folia-core only, never on another collection.
crates/core(folia-core): the content model.content::route:Route, a URL and the one policy that maps it to an output file.content::source,content::front_matter: source documents and their front matter, read with Jekyll's conventions.content::slug,content::date: Jekyll-compatible slugs, the site clock, dated file names.content::git:LastCommits, each source file's last commit time, formodified.content::modified: a document's last modification (front matter, then git, then its date) and where it came from.content::presentation: front matter only templates read (table of contents, cover, the frame's switches), resolved with its defaults.content::markdown: Markdown to HTML as Jekyll's kramdown renders it, parsed with pulldown-cmark and adjusted by passes, one per subject:wiki_link,html(raw HTML as kramdown reads it),ial,lists,typography,heading(IDs),render(kramdown's HTML and layout) andhighlight(syntect with bat's grammars fromtwo-face, mapped to Rouge's Pygments classes);excerpt, with the page description made of it; andanalyze, the headings and the table of contents built from them.publish: what a mode hands to the site builder:PlannedOutput(routes), andModeOutputwith itsEntrySummarys andPageSpecs.
crates/blog,crates/micro,crates/garden,crates/pages: one crate per collection. Each module comment holds the collection's route table.folia_garden::WikiLinksresolves wiki-links by title or file name, as the Jekyll wiki-link plugin the original site used did, andfolia_garden::build(modulenote) builds notes' pages, with their backlinks, and the note graph;folia_pages::buildbuilds pages;folia_blog::category_countsgives the categories every page shows, andfolia_blog::build(modulepost) builds posts' pages. Eachbuilddocuments the context its templates see.crates/folia(folia): the site builder and thefolia buildcommand (main.rs).configreadsfolia.toml,discoverclassifies the site's files, andsiteplans every output in one route table, checks it for duplicate URLs and colliding output files, and writes redirect stubs.buildis the pipeline (below).renderrenders pages with MiniJinja from the site's templates (_folia/templatesin the site),redirectsserializes/redirects.jsonasjekyll-redirect-fromwrites it, andoutputwrites the destination whole or not at all.serveisfolia serve: it builds the whole site again on every changewatchreports, with the site's URL set to the server's, and serves it (http), mapping URLs to files as the host does (files);reloadis the live reload, whose script goes into pages as they are served, never into the files. Its destination's record saysfolia servewrote it (output::claim_for_serve), so that serve never replaces whatfolia buildwrote, and recovers its own interrupted builds.- Preview flags.
buildandservetake--futureand--unpublished(main.rs), which travel as aPreview(folia_core::content). They reach the modes through theSiteClock(with_future,with_unpublished), which the modes already receive; the sass check inbuild.rsreads it too. A preview build's summary line and warning are printed bymain.rs, andoutputwrites its/folia futureand/folia unpublishedlines into the record, whichfolia buildchecks before it replaces a destination. crates/parity: the dev-only parity tool and the content lint. It is described in the parity governance page, and is not part of thefoliabinary.
From files to output¶
folia build (crates/folia/src/build.rs) runs one pipeline:
- Plan every output of the site into one route table, and check it for duplicate URLs and
colliding output files (
site::plan). - Build the documents, each by its collection's crate: pages, posts, notes (with the note graph) and micro entries, dated with their last git commits. A document Folia does not support yet is skipped, with the reason; a document that is supported but fails (a missing template, an unknown author) fails the whole build. Nothing supported is skipped silently.
- Assemble the site that templates see: the site's identity and authors, the build's year and the blog's categories.
- Render: check every page's template, then render each page with MiniJinja.
- Generate: a redirect stub for each
redirect_fromURL, the files the collections generate (the note graph), the sitemap, the feeds, the search script,redirects.json, and a copy of every static file. - Write it all to the destination, whole or not at all (
output).
Every URL the plan reserves and no step produced is counted as not built yet; today there is none, and the summary line says so only when there is one (see the command-line reference). Warnings, such as a broken wiki-link, do not stop a build.
Design decisions¶
Choices that shape the rest of the code, and that you will want to know before changing it.
- Collections stay apart. Each knows its own routes, front matter and template context, and
hands the builder
PageSpecs andPlannedOutputs. The builder knows no collection's rules, which is why adding a URL clash check or a feed does not touch a collection. - One route table. Every output, including generated ones, is planned first, so a clash is found before anything is rendered or written.
- All or nothing. A build writes to a sibling directory and swaps it in, and records what it wrote, so a failed build leaves the destination as it was and Folia never deletes a file it does not own.
- Strict, not forgiving. An unknown front matter key, an undefined template value or an unknown author is an error. Jekyll ignores such things; Folia would rather stop than publish a page that differs from what its author meant.
- Jekyll-compatible on purpose. Markdown, slugs, dates, feeds, the sitemap and the redirect list are made as Jekyll and its plugins made them, because the first site Folia built was migrated from Jekyll. This is also why some parts look odd to someone who never used Jekyll.
How each part is built¶
Front matter Folia does not know is refused, because a misspelt key would otherwise leave a
page silently different from what its author meant. Jekyll ignores such keys, so a published site
can have some. The exceptions are listed, one exact key each, with why, in
folia_pages::IGNORED_TYPOS. So far there is one, owner decision: narrow-post (for
narrow_post), set to true or false, on pages only, is accepted and changes nothing. No
template reads it, so Jekyll published the page at its usual width, and Folia builds it the same
way. Posts and other modes still refuse it, and every other unknown key is still refused.
Authors are the configuration's [authors.<key>] tables, typed in
folia::config::Author, with the fields listed in its module: names, picture, pages, the
author page's backgrounds ([[authors.<key>.covers]]) and an account on each service the site
links to. An unset field is left out; an empty one is refused. Templates see every field as
site.authors.<key>, and covers_json, the backgrounds as the author page's script reads them,
serialized by Folia as Jekyll's jsonify wrote them. A page names its author with its front
matter author; one that names an author the configuration does not have is not rendered.
Micro entries are built by kind, the directory they sit in (notes, likes, bookmarks,
rsvps), with the defaults the site's Jekyll configuration gave each kind: layout and category,
the frame's navigation and titles hidden, a corner link back to /micro, no previous and next
row, categories and tags hidden (see folia_micro's entry module). Their author is the
configuration's [micro] author, a key of [authors]; without one, they have none.
Micro listings are the site's pages that list micro entries, which micro claims: each
[[micro.listings]] names its source, the template that renders it and the kinds it lists.
The page keeps its route, redirects and front matter, its body is not rendered, and the template
gets page.items, the entries newest first (same dates by path, descending, as Jekyll sorted them),
each with its excerpt, its text and its first 25 words.
Blog listings are built by the blog (folia_blog::listings) when the configuration's [blog]
names them: index, the claimed source of /blog/ (its source and template), whose pages
list 9 posts each (not hidden), the first also the featured ones; and archive-template, which
renders an archive per category. Each post is a card: its cover, title, the first 30 words of its
excerpt, author and date. Archives are not dark, as the site's front matter defaults never
reached them (use_dark_layout false). [blog]'s categories and tags claim the pages listing
the posts by category and by tag (their source and template): each gets page.groups, every
group by name in byte order, with its posts' cards newest first.
Feeds are the Atom feeds the modes plan (the blog's, one per tag, micro's, one per
micro kind, and the garden's), written by folia::feed when the configuration has [feeds]:
its lang, the generator the feeds name (name, uri, version) and how many items each
lists (limit, 10). Each mode gives a FeedItem per entry it builds; a feed lists the newest of
those whose pages were rendered, serialized as jekyll-feed 0.17 wrote them, without a template.
Its <updated> is its latest item's, never the build time. Notes name the
configuration's [garden] author, as micro entries name [micro]'s, and their summaries show
wiki-links resolved. A tag's feed is /blog/tag/:tag.xml for every tag that can be a
file name; one with a space is written with it (stem cells.xml) and linked percent-encoded
(stem%20cells.xml; the group's feed, null for a category, which has none). The combined
feed is a page of the site ([feeds.combined]: its source, such as feed.xml, and the
collections it lists): the newest items of those collections, titled as Jekyll's
layout titled it (the site's title, then | and nothing, trailing separator kept), with a
summary and no content.
The sitemap, /sitemap.xml, is written by folia::sitemap as jekyll-sitemap 1.4 wrote it:
every page rendered but /404.html (redirect stubs are not pages), by URL, then the static
.htm, .html, .xhtml and .pdf files. A page's <lastmod> is its entry's modified or
date, or its claimed source's last commit; a page without a source, such as a category archive,
has none. A static file's is its last commit, where Jekyll used the file's modification time.
The search script is written by folia::search when the configuration has [search]: it
claims the script's source (assets/js/lunrsearchengine.js) and writes, at its route, the
documents array the site's Liquid made, followed by fragment, the script's code after the
array, read as it is from the site (_folia/). The records are the site's pages
by URL (with the stubs and redirects.json; not .xml, assets, category or tag URLs),
then the RSVPs, likes and bookmarks, the notes and the posts, oldest first, each with its body's
text by the script's filter chain, except that a line break in a body is a space, not nothing as
in Jekyll (differences from Jekyll). Micro entries are listed
(an RSVP's body is its answer and event), and strings are JSON-escaped, with no
markdownify. A listing page's body is the text of its template's block named by
content-block ([search]), so the navigation, the footer and the rest of the frame are not
indexed; without it, the rendered page. A redirect stub has no body.