krz/orgo
Lightning fast org-mode static site generator.
clone: git clone https://gitbay.org/krz/orgo.git
1//! User-facing build configuration (`orgo.toml`).
2//!
3//! Everything here was once a constant in the source: the page layout, the nav rule, the
4//! highlighting theme. That made the generator produce exactly one kind of site — a
5//! reasonable place to start from, and a dead end for anyone whose site is not that one.
6//!
7//! Two properties matter beyond the settings themselves:
8//!
9//! 1. **Absent config is a valid config.** Every field has a default, so a directory of
10//! `.org` files with no `orgo.toml` still builds. Configuration is how you change
11//! the output, never how you make it work at all.
12//! 2. **Config is a hash input** (spec §4.1). [`Config`] serializes deterministically and
13//! its hash is folded into every page's render key, so editing `orgo.toml` re-renders
14//! exactly the pages it affects — which for most settings is all of them.
15
16use anyhow::{Context, Result};
17use camino::{Utf8Path, Utf8PathBuf};
18use serde::{Deserialize, Serialize};
19
20/// The config file's name, looked for in the source directory.
21pub const CONFIG_FILE: &str = "orgo.toml";
22
23/// Resolved build configuration. Serialized into the config hash, so field order and
24/// defaults are part of the cache contract.
25#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
26#[serde(default, deny_unknown_fields)]
27pub struct Config {
28 pub site: Site,
29 pub nav: Nav,
30 pub templates: Templates,
31 pub highlight: Highlight,
32 pub html: HtmlOutput,
33 pub build: Build,
34 /// Generated listing pages. Each produces one output file that has no source `.org`
35 /// file behind it — a blog index, an archive, a feed.
36 pub collections: Vec<Collection>,
37 /// Which layout authored pages render through, by source path. Pages matching no
38 /// rule use `base.html`.
39 pub pages: Vec<PageRule>,
40}
41
42/// One layout rule: the pages under `match` render through `template`.
43///
44/// Sections usually want one layout — every blog post carries the same byline and reply
45/// footer — and asking an author to repeat `#+TEMPLATE:` in each of 200 files is asking
46/// them to maintain the same fact 200 times. A rule states it once for the directory; a
47/// page that differs still says so itself with `#+TEMPLATE:`, which wins.
48#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
49#[serde(default, deny_unknown_fields)]
50pub struct PageRule {
51 /// A source path: a directory, matching every page beneath it, or one `.org` file.
52 /// Relative to the source root, like `nav.pages`.
53 #[serde(rename = "match")]
54 pub pattern: Utf8PathBuf,
55 /// Template file name, as it appears in the templates directory.
56 pub template: String,
57}
58
59impl PageRule {
60 /// Does this rule cover `source`? Matching is by path component, so a directory rule
61 /// covers everything beneath it however deep — `blog` matches `blog/2026/post.org`,
62 /// because a section's layout is a property of the section and not of how its files
63 /// happen to be filed — while `blo` matches nothing. An empty `match` covers the
64 /// whole site, which is how you change the default layout's name.
65 pub fn covers(&self, source: &Utf8Path) -> bool {
66 source.starts_with(&self.pattern)
67 }
68
69 /// How specific this rule is, for picking between two that both match. Longer paths
70 /// are more specific, so `blog/notes` beats `blog`.
71 fn specificity(&self) -> usize {
72 self.pattern.components().count()
73 }
74}
75
76/// Which template an authored page renders through: its own `#+TEMPLATE:` if it names
77/// one, else the most specific `[[pages]]` rule covering it, else `base.html`.
78///
79/// A page's own declaration wins because it is the more local statement — the one written
80/// with that page in view.
81pub fn page_template(config: &Config, source: &Utf8Path, keywords: &crate::model::Keywords) -> String {
82 let declared = keywords
83 .entries
84 .iter()
85 .find(|(k, _)| k.eq_ignore_ascii_case("TEMPLATE"))
86 .map(|(_, v)| v.trim())
87 .filter(|v| !v.is_empty());
88 if let Some(name) = declared {
89 return name.to_string();
90 }
91 config
92 .pages
93 .iter()
94 .filter(|rule| rule.covers(source))
95 .max_by_key(|rule| rule.specificity())
96 .map(|rule| rule.template.clone())
97 .unwrap_or_else(|| crate::template::BASE_TEMPLATE_NAME.to_string())
98}
99
100/// A generated page that lists other pages.
101///
102/// This is the one output that is not a translation of some input: a blog index exists
103/// because a set of posts exists, not because someone wrote `index.org`. Keeping it
104/// declarative — a directory in, a file out, through a template — means a feed is the
105/// same mechanism with an XML template rather than a second feature.
106#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
107#[serde(default, deny_unknown_fields)]
108pub struct Collection {
109 /// Directory of source pages to list, relative to the source root. Empty means every
110 /// page in the site.
111 pub source: Utf8PathBuf,
112 /// Where to write the generated page, relative to the output root.
113 pub output: Utf8PathBuf,
114 /// Template file name, as it appears in the templates directory.
115 pub template: String,
116 /// Title for the generated page, available to the template as `page.title`.
117 pub title: String,
118 /// Split the collection into groups and emit one page per group.
119 ///
120 /// `"tags"` groups by `#+FILETAGS:`, where a page belongs to every tag it carries.
121 /// Any other value names a `#+KEYWORD:` and groups by its value, so `"category"`
122 /// buckets pages by `#+CATEGORY:`. Empty means one page for the whole collection.
123 ///
124 /// When set, `output` and `title` may contain `{tag}`, replaced by the group — and
125 /// `output` must, or every group would write to the same file.
126 pub group_by: String,
127 /// Where to write a page listing the groups themselves — a tag index. Empty means
128 /// no such page. Only meaningful with `group_by`.
129 pub index_output: Utf8PathBuf,
130 /// Template for the group-index page. It receives `groups` rather than `pages`.
131 pub index_template: String,
132 pub index_title: String,
133 pub sort: SortKey,
134 pub order: SortOrder,
135 /// Entries per page. `0` means no pagination — the whole collection on one page.
136 ///
137 /// Page 1 stays at `output`, so the canonical URL of a section never moves when the
138 /// number of pages changes. Pages 2 and up go to `paginate_output`.
139 pub paginate: usize,
140 /// Where pages 2..N are written. Must contain `{n}`, the 1-based page number, and
141 /// `{tag}` as well when the collection is grouped — otherwise page 2 of one group
142 /// would overwrite page 2 of another.
143 pub paginate_output: Utf8PathBuf,
144 /// Give the template each entry's rendered HTML as `entry.content`.
145 ///
146 /// Off by default, and only worth turning on for a feed: it renders every listed
147 /// page's body whenever the listing is rebuilt. A reader subscribed to a
148 /// full-content feed and then handed excerpts has lost something, which is the one
149 /// case where that cost is the right trade.
150 pub include_content: bool,
151 /// Add this listing page to the site navigation. This is how a section landing page
152 /// — `/blog/`, `/notes/` — gets into a nav built from top-level pages.
153 pub nav: bool,
154}
155
156impl Default for Collection {
157 fn default() -> Self {
158 Collection {
159 source: Utf8PathBuf::new(),
160 output: Utf8PathBuf::from("index.html"),
161 template: "list.html".to_string(),
162 title: "Index".to_string(),
163 group_by: String::new(),
164 index_output: Utf8PathBuf::new(),
165 index_template: "tags.html".to_string(),
166 index_title: "Tags".to_string(),
167 sort: SortKey::default(),
168 order: SortOrder::default(),
169 paginate: 0,
170 paginate_output: Utf8PathBuf::new(),
171 include_content: false,
172 nav: false,
173 }
174 }
175}
176
177/// The `{tag}` placeholder in a grouped collection's `output` and `title`.
178pub const GROUP_PLACEHOLDER: &str = "{tag}";
179/// The page-number placeholder in `paginate_output`.
180pub const PAGE_PLACEHOLDER: &str = "{n}";
181
182#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
183#[serde(rename_all = "kebab-case")]
184pub enum SortKey {
185 /// By `#+DATE:`, newest first by default. Pages with no parseable date sort last,
186 /// keeping undated drafts out of the way of a dated archive.
187 #[default]
188 Date,
189 Title,
190 /// Output path — stable and predictable when dates are absent or unreliable.
191 Path,
192}
193
194#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
195#[serde(rename_all = "kebab-case")]
196pub enum SortOrder {
197 /// Newest or last first — the useful default for a blog.
198 #[default]
199 Desc,
200 Asc,
201}
202
203#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
204#[serde(default, deny_unknown_fields)]
205pub struct Build {
206 /// Include pages marked `#+DRAFT:` in the build.
207 ///
208 /// Off by default, because the point of marking something a draft is that it is not
209 /// ready to be read. `--drafts` turns it on for a session, which is what you want
210 /// under `watch` while writing one.
211 pub drafts: bool,
212 /// Extra directories whose contents are copied to the *site root*, on top of the
213 /// non-`.org` files found in the source directory. Relative to the source root, and
214 /// allowed to point outside it.
215 ///
216 /// This exists because a site's static files do not always live where its writing
217 /// does: weblorg publishes `theme/static/` to `/`, and a repository migrating from it
218 /// should not have to move `robots.txt` next to its blog posts to keep the URL.
219 pub assets: Vec<Utf8PathBuf>,
220 /// Write `sitemap.xml` listing every published page.
221 ///
222 /// On, but a sitemap requires absolute URLs — the format has nowhere to put a
223 /// relative one — so nothing is written until `site.base_url` is set. That is why a
224 /// zero-config build produces no sitemap and no complaint: there is no URL to give a
225 /// search engine yet.
226 pub sitemap: bool,
227}
228
229impl Default for Build {
230 fn default() -> Self {
231 Build {
232 drafts: false,
233 assets: Vec::new(),
234 sitemap: true,
235 }
236 }
237}
238
239#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
240#[serde(default, deny_unknown_fields)]
241pub struct HtmlOutput {
242 /// How far to push heading levels down: a level-1 org heading becomes
243 /// `<h{1 + heading_offset}>`.
244 ///
245 /// Defaults to 1, matching Emacs' own `org-html-toplevel-hlevel`, because the page
246 /// layout supplies the `<h1>` — the document's title — and section headings sit
247 /// beneath it. Set to 0 if your template renders no title of its own, so the
248 /// document does not start at `<h2>` with nothing above it.
249 pub heading_offset: u8,
250 /// Make each page's table of contents available to templates as `page.toc`.
251 ///
252 /// On by default: it is *data*, and whether it appears is the template's business.
253 /// A document turns it off for itself with org's own `#+OPTIONS: toc:nil`, which is
254 /// how ~2% of the reference corpus does it.
255 pub toc: bool,
256 /// Number headings, `1.`, `1.1.`, and so on.
257 ///
258 /// Off by default, which differs from Emacs — `org-export-with-section-numbers` is
259 /// on there, and the reference site inherits numbered headings from it. Most sites
260 /// do not want them, so the default is the taste rather than the inheritance;
261 /// `#+OPTIONS: num:t` or `section_numbers = true` gets Emacs' behaviour back.
262 pub section_numbers: bool,
263 /// Convert org's special strings in prose: `--` to an en dash, `---` to an em dash,
264 /// `...` to an ellipsis.
265 ///
266 /// On, as in Emacs. A document turns it off for itself with `#+OPTIONS: -:nil`.
267 /// Never applied inside verbatim, code, or a source block.
268 pub special_strings: bool,
269 /// Whether `x^2` and `H_{2}O` become `<sup>`/`<sub>`.
270 ///
271 /// `"yes"` (the default, and Emacs') also treats the braceless `a_b` as a subscript,
272 /// which is what makes `snake_case` in prose render as `snake<sub>case</sub>` —
273 /// surprising, but what Emacs does with the same file. `"braces"` limits it to the
274 /// explicit `a_{b}` form, and `"no"` leaves both alone. A document chooses for itself
275 /// with `#+OPTIONS: ^:nil` or `^:{}`.
276 pub sub_superscript: SubSuperscript,
277 /// Whether `\alpha` and the rest of org's entity table become their characters.
278 ///
279 /// On, as in Emacs. A name org does not know is left as the literal text that was
280 /// typed. A document turns the whole thing off with `#+OPTIONS: e:nil`.
281 pub entities: bool,
282}
283
284/// How `_` and `^` are treated in prose. Mirrors org's `^:` export option.
285#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
286#[serde(rename_all = "kebab-case")]
287pub enum SubSuperscript {
288 /// `a_b` and `a_{b}` both convert.
289 #[default]
290 Yes,
291 /// Only the braced `a_{b}` converts.
292 Braces,
293 /// Neither converts.
294 No,
295}
296
297impl SubSuperscript {
298 /// Read org's `^:` option value: `nil` is off, `{}` is braces-only, anything else on.
299 pub fn from_option(value: &str) -> SubSuperscript {
300 match value.trim() {
301 "nil" | "false" | "no" | "off" => SubSuperscript::No,
302 "{}" => SubSuperscript::Braces,
303 _ => SubSuperscript::Yes,
304 }
305 }
306}
307
308impl Default for HtmlOutput {
309 fn default() -> Self {
310 HtmlOutput {
311 heading_offset: 1,
312 toc: true,
313 section_numbers: false,
314 special_strings: true,
315 sub_superscript: SubSuperscript::Yes,
316 entities: true,
317 }
318 }
319}
320
321/// Site-wide metadata, exposed to templates as `site`.
322#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
323#[serde(default, deny_unknown_fields)]
324pub struct Site {
325 /// Shown in the default layout's header and available as `site.title`.
326 pub title: String,
327 /// Absolute base URL (no trailing slash), for feeds and canonical links. Empty means
328 /// the site is built with relative URLs only, which is the portable default.
329 pub base_url: String,
330 /// Free-form description, available as `site.description`.
331 pub description: String,
332 /// `<html lang="…">` in the default layout.
333 pub language: String,
334 /// A built-in theme name — see [`crate::theme::THEMES`] — written to the output root
335 /// as `theme.css` and linked by the built-in layout and the starter templates.
336 ///
337 /// Empty by default, which emits no stylesheet and leaves the HTML unstyled. A theme
338 /// is a convenience for a site that has not grown its own CSS yet, and defaulting
339 /// one on would restyle every existing site on upgrade and fight the stylesheets
340 /// people already ship as assets.
341 pub theme: String,
342}
343
344impl Default for Site {
345 fn default() -> Self {
346 Site {
347 title: "orgo site".to_string(),
348 base_url: String::new(),
349 description: String::new(),
350 language: "en".to_string(),
351 theme: String::new(),
352 }
353 }
354}
355
356/// Which pages appear in the shared navigation.
357#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
358#[serde(rename_all = "kebab-case")]
359pub enum NavMode {
360 /// Pages at the site root. A nav is a map of the top level, not an index of the
361 /// whole site, and this keeps nav size independent of how many pages exist.
362 #[default]
363 TopLevel,
364 /// Every page. Fine for a small site; note that it makes total output quadratic in
365 /// page count, since each of `n` pages then carries `n` nav links.
366 All,
367 /// Only the pages listed in `nav.pages`, in that order.
368 Explicit,
369 /// No navigation at all.
370 None,
371}
372
373#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
374#[serde(default, deny_unknown_fields)]
375pub struct Nav {
376 pub mode: NavMode,
377 /// Source paths (relative to the source root, e.g. `about.org`) used when
378 /// `mode = "explicit"`. Order is preserved, so this doubles as nav ordering.
379 pub pages: Vec<Utf8PathBuf>,
380}
381
382#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
383#[serde(default, deny_unknown_fields)]
384pub struct Templates {
385 /// Directory of `.html` templates, relative to the source root. Each file is
386 /// registered under its stem, so `base.html` overrides the built-in layout and
387 /// anything else is available to `{% include %}`/`{% extends %}`.
388 pub dir: Utf8PathBuf,
389 /// Give templates a `pages` list of every page's metadata, so a template can build
390 /// an index or archive.
391 ///
392 /// Off by default because it is not free: if any page can read every page's
393 /// metadata, then adding one page can change any page's output, so the whole site
394 /// must re-render on every add, rename or retitle. Turning this on trades that
395 /// incremental precision for the ability to write listing pages.
396 pub expose_page_list: bool,
397}
398
399impl Default for Templates {
400 fn default() -> Self {
401 Templates {
402 dir: Utf8PathBuf::from("templates"),
403 expose_page_list: false,
404 }
405 }
406}
407
408#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
409#[serde(default, deny_unknown_fields)]
410pub struct Highlight {
411 /// Directory of extra `.sublime-syntax` files, relative to the source root.
412 ///
413 /// syntect bundles a long list of languages and this crate adds TOML and Org, but a
414 /// missing language should not need a new release — drop a definition here and it is
415 /// picked up. A file that fails to parse is reported and skipped.
416 pub syntaxes_dir: Utf8PathBuf,
417 /// A syntect built-in theme name — `InspiredGitHub`, `Solarized (dark)`,
418 /// `base16-ocean.dark`, `base16-eighties.dark`, `base16-mocha.dark`,
419 /// `base16-ocean.light`. Highlighting emits CSS classes, and this theme is what the
420 /// generated `syntax.css` colours them with.
421 pub theme: String,
422 /// A second syntect theme for readers whose system asks for dark mode. `syntax.css`
423 /// then carries both, each behind its own `prefers-color-scheme` query, so one
424 /// stylesheet serves both schemes — see [`crate::render::syntax_stylesheet`]. Empty,
425 /// the default, means `theme` colours every reader whatever their scheme.
426 pub theme_dark: String,
427}
428
429impl Default for Highlight {
430 fn default() -> Self {
431 Highlight {
432 syntaxes_dir: Utf8PathBuf::from("syntaxes"),
433 theme: "InspiredGitHub".to_string(),
434 theme_dark: String::new(),
435 }
436 }
437}
438
439impl Config {
440 /// Load `orgo.toml` from `dir`, or return defaults if there is none.
441 ///
442 /// A *missing* config is normal and silent. A *malformed* one is an error: someone
443 /// who wrote a config meant it, and silently building the default site would hide
444 /// their typo behind plausible-looking output.
445 pub fn load(dir: &Utf8Path) -> Result<Config> {
446 Self::load_file(&dir.join(CONFIG_FILE))
447 }
448
449 /// Load a config from an explicit path. Missing is still fine; malformed is not.
450 pub fn load_file(path: &Utf8Path) -> Result<Config> {
451 let text = match std::fs::read_to_string(path) {
452 Ok(text) => text,
453 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Config::default()),
454 Err(e) => return Err(e).with_context(|| format!("reading {path}")),
455 };
456 toml::from_str(&text).with_context(|| format!("parsing {path}"))
457 }
458
459 /// Validate settings that only make sense in combination. Catching these up front
460 /// beats emitting a site with a silently empty nav.
461 pub fn validate(&self) -> Result<()> {
462 if self.nav.mode == NavMode::Explicit && self.nav.pages.is_empty() {
463 anyhow::bail!(
464 "nav.mode is \"explicit\" but nav.pages is empty: list the pages to \
465 include, or use mode = \"top-level\"/\"all\"/\"none\""
466 );
467 }
468 if self.nav.mode != NavMode::Explicit && !self.nav.pages.is_empty() {
469 anyhow::bail!(
470 "nav.pages is set but nav.mode is \"{}\", so it would be ignored; set \
471 mode = \"explicit\" to use it",
472 toml::to_string(&self.nav.mode)
473 .unwrap_or_default()
474 .trim()
475 .trim_matches('"')
476 );
477 }
478 let mut seen: Vec<&Utf8PathBuf> = Vec::new();
479 for collection in &self.collections {
480 let grouped = !collection.group_by.is_empty();
481 if collection.output.as_str().is_empty() && collection.index_output.as_str().is_empty()
482 {
483 anyhow::bail!("a collection has no `output`; it needs a file to write");
484 }
485 if grouped
486 && !collection.output.as_str().is_empty()
487 && !collection.output.as_str().contains(GROUP_PLACEHOLDER)
488 {
489 anyhow::bail!(
490 "collection output {} groups by \"{}\" but has no {GROUP_PLACEHOLDER} in \
491 its path, so every group would overwrite the same file",
492 collection.output,
493 collection.group_by
494 );
495 }
496 if !grouped && collection.output.as_str().contains(GROUP_PLACEHOLDER) {
497 anyhow::bail!(
498 "collection output {} uses {GROUP_PLACEHOLDER} but sets no `group_by`",
499 collection.output
500 );
501 }
502 if !grouped && !collection.index_output.as_str().is_empty() {
503 anyhow::bail!(
504 "collection writes an `index_output` of {} but sets no `group_by`; \
505 there are no groups to index",
506 collection.index_output
507 );
508 }
509 if collection.paginate > 0 {
510 let pattern = collection.paginate_output.as_str();
511 if pattern.is_empty() {
512 anyhow::bail!(
513 "collection output {} sets `paginate` but no `paginate_output`; pages 2 and up need somewhere to go, e.g. \"blog/page/{PAGE_PLACEHOLDER}.html\"",
514 collection.output
515 );
516 }
517 if !pattern.contains(PAGE_PLACEHOLDER) {
518 anyhow::bail!(
519 "collection `paginate_output` {pattern} has no {PAGE_PLACEHOLDER}, so every page after the first would overwrite the same file"
520 );
521 }
522 if grouped && !pattern.contains(GROUP_PLACEHOLDER) {
523 anyhow::bail!(
524 "collection `paginate_output` {pattern} groups by \"{}\" but has no {GROUP_PLACEHOLDER}, so page 2 of one group would overwrite page 2 of another",
525 collection.group_by
526 );
527 }
528 }
529 if collection.paginate == 0 && !collection.paginate_output.as_str().is_empty() {
530 anyhow::bail!(
531 "collection sets `paginate_output` {} but `paginate` is 0, so it would never be used; set `paginate` to a page size",
532 collection.paginate_output
533 );
534 }
535 for path in [&collection.output, &collection.index_output] {
536 if path.as_str().is_empty() || path.as_str().contains(GROUP_PLACEHOLDER) {
537 continue;
538 }
539 if seen.contains(&path) {
540 anyhow::bail!(
541 "two collections both write to {path}; give them different \
542 `output` paths"
543 );
544 }
545 seen.push(path);
546 }
547 }
548 for rule in &self.pages {
549 if rule.template.trim().is_empty() {
550 anyhow::bail!(
551 "the [[pages]] rule matching {:?} names no `template`; it has nothing \
552 to select",
553 rule.pattern.as_str()
554 );
555 }
556 }
557 if !self.site.theme.is_empty() && crate::theme::theme_css(&self.site.theme).is_none() {
558 anyhow::bail!(
559 "unknown site.theme {:?}. Available: {} — or leave it empty for no \
560 stylesheet",
561 self.site.theme,
562 crate::theme::available_themes().join(", ")
563 );
564 }
565 if !self.site.base_url.is_empty() && self.site.base_url.ends_with('/') {
566 anyhow::bail!(
567 "site.base_url must not end with a slash (got {:?}) — URLs are joined \
568 with an explicit separator",
569 self.site.base_url
570 );
571 }
572 Ok(())
573 }
574}
575
576/// The starter config written by `orgo init`, and the documentation of record for
577/// what is configurable. Every value shown is the default — except `site.theme`, which
578/// picks a stylesheet so a new site looks like something on its first build — so
579/// deleting any line is safe.
580pub const STARTER_CONFIG: &str = r#"# orgo configuration. Every setting here is optional and shown at its default — apart
581# from `theme`, noted below — so you can delete any line you do not need, or the whole
582# file.
583
584[site]
585title = "orgo site"
586# Absolute base URL, no trailing slash. Needed for feeds and canonical links, which
587# cannot be relative — set it and uncomment the [[collections]] feed block below.
588base_url = ""
589description = ""
590language = "en"
591# A built-in stylesheet, written to the output as theme.css: "plain" (readable defaults
592# to build your own CSS on), "blog" (serif prose, masthead, styled post lists), "wiki"
593# (wide and dense, contents in the margin, TODO states shown) or "docs" (a guide read in
594# order). The one line here that is not a default: the default is "", which emits no
595# stylesheet at all. Your own base.html can ignore theme.css and link whatever it likes.
596theme = "blog"
597
598[nav]
599# Which pages appear in the shared navigation:
600# "top-level" — pages at the site root (default; keeps nav size independent of site size)
601# "all" — every page (fine when small; output grows quadratically with page count)
602# "explicit" — only nav.pages, in the order listed
603# "none" — no navigation
604mode = "top-level"
605# pages = ["index.org", "about.org"]
606
607[templates]
608# Directory of .html templates, relative to this file. `base.html` replaces the built-in
609# layout; any other file can be pulled in with {% include %} or {% extends %}.
610dir = "templates"
611# Give templates a `pages` list of every page's metadata, so you can build an index or
612# archive. Costs incremental precision: with this on, adding a page re-renders the site.
613expose_page_list = false
614
615# Which layout a page renders through. Without a rule, every page uses base.html.
616# `match` is a source path — a directory (covering everything beneath it) or one .org
617# file — and the most specific rule wins. A page overrides any rule with `#+TEMPLATE:`.
618# [[pages]]
619# match = "blog"
620# template = "post.html"
621
622[highlight]
623# A syntect theme name: InspiredGitHub, Solarized (dark), base16-ocean.dark,
624# base16-eighties.dark, base16-mocha.dark, base16-ocean.light.
625theme = "InspiredGitHub"
626# A second theme for readers in dark mode. Set it and syntax.css carries both themes,
627# each behind its own prefers-color-scheme query. Empty means `theme` colours everyone.
628theme_dark = ""
629# Extra .sublime-syntax files for languages neither syntect nor orgo bundles.
630syntaxes_dir = "syntaxes"
631
632[build]
633# Include pages marked `#+DRAFT:`. Off by default — the point of marking a draft is that
634# it is not ready to be read. `--drafts` turns it on for one run, handy under `watch`.
635drafts = false
636# Extra directories copied to the site root, for static files that live outside the
637# source directory. `assets = ["../theme/static"]` publishes that directory's contents at
638# `/`, not at `/static/`.
639assets = []
640# Write sitemap.xml. Needs site.base_url — a sitemap has nowhere to put a relative URL —
641# so nothing is written until you set one.
642sitemap = true
643
644[html]
645# How far to push heading levels down: a level-1 org heading becomes <h(1 + offset)>.
646# The default of 1 matches Emacs, and assumes your layout renders the page title as the
647# <h1>. Set to 0 if your template renders no title of its own.
648heading_offset = 1
649# Make page.toc available to templates. A document opts out with `#+OPTIONS: toc:nil`.
650toc = true
651# Number headings (1., 1.1., …). Emacs defaults this on; most sites do not.
652# A document overrides with `#+OPTIONS: num:t`.
653section_numbers = false
654# Convert `--` to an en dash, `---` to an em dash and `...` to an ellipsis in prose, as
655# Emacs does. Never inside code. A document overrides with `#+OPTIONS: -:nil`.
656special_strings = true
657# Whether `x^2` and `H_{2}O` become <sup>/<sub>: "yes" (as Emacs, and so `snake_case`
658# becomes snake<sub>case</sub>), "braces" for the `a_{b}` form only, or "no".
659# A document overrides with `#+OPTIONS: ^:nil` or `^:{}`.
660sub_superscript = "yes"
661# Convert `\alpha` and the rest of org's entity table. An unknown name stays literal.
662# A document overrides with `#+OPTIONS: e:nil`.
663entities = true
664
665# Generated listing pages: output files with no source .org behind them. Repeat the
666# [[collections]] block for each one. A feed is the same thing with an XML template.
667[[collections]]
668source = "blog" # directory to list; empty means every page
669output = "blog/index.html" # where to write it
670template = "list.html" # template file name
671title = "Blog"
672sort = "date" # date | title | path
673order = "desc" # desc | asc
674nav = true # put this listing page in the site nav
675# paginate = 10 # entries per page; page 1 stays at `output`
676# paginate_output = "blog/page/{n}.html" # where pages 2..N go; needs {n}
677
678# An RSS feed is a listing page with an XML template. It needs site.base_url above,
679# because a feed is read away from the site that served it and relative links break.
680# [[collections]]
681# source = "blog"
682# output = "feed.xml"
683# template = "feed.xml"
684# title = "Feed"
685
686# One page per tag, plus an index of all tags. `{tag}` in `output`/`title` is replaced
687# by each tag; the index gets `groups` instead of `pages`.
688[[collections]]
689source = "blog"
690group_by = "tags" # "tags", or any #+KEYWORD: name to group by its value
691output = "tags/{tag}.html"
692template = "list.html"
693title = "Tagged: {tag}"
694index_output = "tags/index.html"
695index_template = "tags.html"
696index_title = "Tags"
697nav = true # adds the tag *index*, not every tag
698"#;