krz/orgo

Lightning fast org-mode static site generator.

clone: git clone https://gitbay.org/krz/orgo.git

v0.20.2: src/config.rs · raw

  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}
335
336impl Default for Site {
337    fn default() -> Self {
338        Site {
339            title: "orgo site".to_string(),
340            base_url: String::new(),
341            description: String::new(),
342            language: "en".to_string(),
343        }
344    }
345}
346
347/// Which pages appear in the shared navigation.
348#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
349#[serde(rename_all = "kebab-case")]
350pub enum NavMode {
351    /// Pages at the site root. A nav is a map of the top level, not an index of the
352    /// whole site, and this keeps nav size independent of how many pages exist.
353    #[default]
354    TopLevel,
355    /// Every page. Fine for a small site; note that it makes total output quadratic in
356    /// page count, since each of `n` pages then carries `n` nav links.
357    All,
358    /// Only the pages listed in `nav.pages`, in that order.
359    Explicit,
360    /// No navigation at all.
361    None,
362}
363
364#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
365#[serde(default, deny_unknown_fields)]
366pub struct Nav {
367    pub mode: NavMode,
368    /// Source paths (relative to the source root, e.g. `about.org`) used when
369    /// `mode = "explicit"`. Order is preserved, so this doubles as nav ordering.
370    pub pages: Vec<Utf8PathBuf>,
371}
372
373#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
374#[serde(default, deny_unknown_fields)]
375pub struct Templates {
376    /// Directory of `.html` templates, relative to the source root. Each file is
377    /// registered under its stem, so `base.html` overrides the built-in layout and
378    /// anything else is available to `{% include %}`/`{% extends %}`.
379    pub dir: Utf8PathBuf,
380    /// Give templates a `pages` list of every page's metadata, so a template can build
381    /// an index or archive.
382    ///
383    /// Off by default because it is not free: if any page can read every page's
384    /// metadata, then adding one page can change any page's output, so the whole site
385    /// must re-render on every add, rename or retitle. Turning this on trades that
386    /// incremental precision for the ability to write listing pages.
387    pub expose_page_list: bool,
388}
389
390impl Default for Templates {
391    fn default() -> Self {
392        Templates {
393            dir: Utf8PathBuf::from("templates"),
394            expose_page_list: false,
395        }
396    }
397}
398
399#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
400#[serde(default, deny_unknown_fields)]
401pub struct Highlight {
402    /// Directory of extra `.sublime-syntax` files, relative to the source root.
403    ///
404    /// syntect bundles a long list of languages and this crate adds TOML and Org, but a
405    /// missing language should not need a new release — drop a definition here and it is
406    /// picked up. A file that fails to parse is reported and skipped.
407    pub syntaxes_dir: Utf8PathBuf,
408    /// A syntect built-in theme name — `InspiredGitHub`, `Solarized (dark)`,
409    /// `base16-ocean.dark`, `base16-eighties.dark`, `base16-mocha.dark`,
410    /// `base16-ocean.light`. Highlighting emits CSS classes, and this theme is what the
411    /// generated `syntax.css` colours them with.
412    pub theme: String,
413}
414
415impl Default for Highlight {
416    fn default() -> Self {
417        Highlight {
418            syntaxes_dir: Utf8PathBuf::from("syntaxes"),
419            theme: "InspiredGitHub".to_string(),
420        }
421    }
422}
423
424impl Config {
425    /// Load `orgo.toml` from `dir`, or return defaults if there is none.
426    ///
427    /// A *missing* config is normal and silent. A *malformed* one is an error: someone
428    /// who wrote a config meant it, and silently building the default site would hide
429    /// their typo behind plausible-looking output.
430    pub fn load(dir: &Utf8Path) -> Result<Config> {
431        Self::load_file(&dir.join(CONFIG_FILE))
432    }
433
434    /// Load a config from an explicit path. Missing is still fine; malformed is not.
435    pub fn load_file(path: &Utf8Path) -> Result<Config> {
436        let text = match std::fs::read_to_string(path) {
437            Ok(text) => text,
438            Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Config::default()),
439            Err(e) => return Err(e).with_context(|| format!("reading {path}")),
440        };
441        toml::from_str(&text).with_context(|| format!("parsing {path}"))
442    }
443
444    /// Validate settings that only make sense in combination. Catching these up front
445    /// beats emitting a site with a silently empty nav.
446    pub fn validate(&self) -> Result<()> {
447        if self.nav.mode == NavMode::Explicit && self.nav.pages.is_empty() {
448            anyhow::bail!(
449                "nav.mode is \"explicit\" but nav.pages is empty: list the pages to \
450                 include, or use mode = \"top-level\"/\"all\"/\"none\""
451            );
452        }
453        if self.nav.mode != NavMode::Explicit && !self.nav.pages.is_empty() {
454            anyhow::bail!(
455                "nav.pages is set but nav.mode is \"{}\", so it would be ignored; set \
456                 mode = \"explicit\" to use it",
457                toml::to_string(&self.nav.mode)
458                    .unwrap_or_default()
459                    .trim()
460                    .trim_matches('"')
461            );
462        }
463        let mut seen: Vec<&Utf8PathBuf> = Vec::new();
464        for collection in &self.collections {
465            let grouped = !collection.group_by.is_empty();
466            if collection.output.as_str().is_empty() && collection.index_output.as_str().is_empty()
467            {
468                anyhow::bail!("a collection has no `output`; it needs a file to write");
469            }
470            if grouped
471                && !collection.output.as_str().is_empty()
472                && !collection.output.as_str().contains(GROUP_PLACEHOLDER)
473            {
474                anyhow::bail!(
475                    "collection output {} groups by \"{}\" but has no {GROUP_PLACEHOLDER} in \
476                     its path, so every group would overwrite the same file",
477                    collection.output,
478                    collection.group_by
479                );
480            }
481            if !grouped && collection.output.as_str().contains(GROUP_PLACEHOLDER) {
482                anyhow::bail!(
483                    "collection output {} uses {GROUP_PLACEHOLDER} but sets no `group_by`",
484                    collection.output
485                );
486            }
487            if !grouped && !collection.index_output.as_str().is_empty() {
488                anyhow::bail!(
489                    "collection writes an `index_output` of {} but sets no `group_by`; \
490                     there are no groups to index",
491                    collection.index_output
492                );
493            }
494            if collection.paginate > 0 {
495                let pattern = collection.paginate_output.as_str();
496                if pattern.is_empty() {
497                    anyhow::bail!(
498                        "collection output {} sets `paginate` but no `paginate_output`;                          pages 2 and up need somewhere to go, e.g. \"blog/page/{PAGE_PLACEHOLDER}.html\"",
499                        collection.output
500                    );
501                }
502                if !pattern.contains(PAGE_PLACEHOLDER) {
503                    anyhow::bail!(
504                        "collection `paginate_output` {pattern} has no {PAGE_PLACEHOLDER},                          so every page after the first would overwrite the same file"
505                    );
506                }
507                if grouped && !pattern.contains(GROUP_PLACEHOLDER) {
508                    anyhow::bail!(
509                        "collection `paginate_output` {pattern} groups by \"{}\" but has no                          {GROUP_PLACEHOLDER}, so page 2 of one group would overwrite page 2                          of another",
510                        collection.group_by
511                    );
512                }
513            }
514            if collection.paginate == 0 && !collection.paginate_output.as_str().is_empty() {
515                anyhow::bail!(
516                    "collection sets `paginate_output` {} but `paginate` is 0, so it would                      never be used; set `paginate` to a page size",
517                    collection.paginate_output
518                );
519            }
520            for path in [&collection.output, &collection.index_output] {
521                if path.as_str().is_empty() || path.as_str().contains(GROUP_PLACEHOLDER) {
522                    continue;
523                }
524                if seen.contains(&path) {
525                    anyhow::bail!(
526                        "two collections both write to {path}; give them different \
527                         `output` paths"
528                    );
529                }
530                seen.push(path);
531            }
532        }
533        for rule in &self.pages {
534            if rule.template.trim().is_empty() {
535                anyhow::bail!(
536                    "the [[pages]] rule matching {:?} names no `template`; it has nothing \
537                     to select",
538                    rule.pattern.as_str()
539                );
540            }
541        }
542        if !self.site.base_url.is_empty() && self.site.base_url.ends_with('/') {
543            anyhow::bail!(
544                "site.base_url must not end with a slash (got {:?}) — URLs are joined \
545                 with an explicit separator",
546                self.site.base_url
547            );
548        }
549        Ok(())
550    }
551}
552
553/// The starter config written by `orgo init`, and the documentation of record for
554/// what is configurable. Every value shown is the default, so deleting any line is safe.
555pub const STARTER_CONFIG: &str = r#"# orgo configuration. Every setting here is optional and shown at its default,
556# so you can delete any line you do not need — or the whole file.
557
558[site]
559title = "orgo site"
560# Absolute base URL, no trailing slash. Needed for feeds and canonical links, which
561# cannot be relative — set it and uncomment the [[collections]] feed block below.
562base_url = ""
563description = ""
564language = "en"
565
566[nav]
567# Which pages appear in the shared navigation:
568#   "top-level" — pages at the site root (default; keeps nav size independent of site size)
569#   "all"       — every page (fine when small; output grows quadratically with page count)
570#   "explicit"  — only nav.pages, in the order listed
571#   "none"      — no navigation
572mode = "top-level"
573# pages = ["index.org", "about.org"]
574
575[templates]
576# Directory of .html templates, relative to this file. `base.html` replaces the built-in
577# layout; any other file can be pulled in with {% include %} or {% extends %}.
578dir = "templates"
579# Give templates a `pages` list of every page's metadata, so you can build an index or
580# archive. Costs incremental precision: with this on, adding a page re-renders the site.
581expose_page_list = false
582
583# Which layout a page renders through. Without a rule, every page uses base.html.
584# `match` is a source path — a directory (covering everything beneath it) or one .org
585# file — and the most specific rule wins. A page overrides any rule with `#+TEMPLATE:`.
586# [[pages]]
587# match = "blog"
588# template = "post.html"
589
590[highlight]
591# A syntect theme name: InspiredGitHub, Solarized (dark), base16-ocean.dark,
592# base16-eighties.dark, base16-mocha.dark, base16-ocean.light.
593theme = "InspiredGitHub"
594# Extra .sublime-syntax files for languages neither syntect nor orgo bundles.
595syntaxes_dir = "syntaxes"
596
597[build]
598# Include pages marked `#+DRAFT:`. Off by default — the point of marking a draft is that
599# it is not ready to be read. `--drafts` turns it on for one run, handy under `watch`.
600drafts = false
601# Extra directories copied to the site root, for static files that live outside the
602# source directory. `assets = ["../theme/static"]` publishes that directory's contents at
603# `/`, not at `/static/`.
604assets = []
605# Write sitemap.xml. Needs site.base_url — a sitemap has nowhere to put a relative URL —
606# so nothing is written until you set one.
607sitemap = true
608
609[html]
610# How far to push heading levels down: a level-1 org heading becomes <h(1 + offset)>.
611# The default of 1 matches Emacs, and assumes your layout renders the page title as the
612# <h1>. Set to 0 if your template renders no title of its own.
613heading_offset = 1
614# Make page.toc available to templates. A document opts out with `#+OPTIONS: toc:nil`.
615toc = true
616# Number headings (1., 1.1., …). Emacs defaults this on; most sites do not.
617# A document overrides with `#+OPTIONS: num:t`.
618section_numbers = false
619# Convert `--` to an en dash, `---` to an em dash and `...` to an ellipsis in prose, as
620# Emacs does. Never inside code. A document overrides with `#+OPTIONS: -:nil`.
621special_strings = true
622# Whether `x^2` and `H_{2}O` become <sup>/<sub>: "yes" (as Emacs, and so `snake_case`
623# becomes snake<sub>case</sub>), "braces" for the `a_{b}` form only, or "no".
624# A document overrides with `#+OPTIONS: ^:nil` or `^:{}`.
625sub_superscript = "yes"
626# Convert `\alpha` and the rest of org's entity table. An unknown name stays literal.
627# A document overrides with `#+OPTIONS: e:nil`.
628entities = true
629
630# Generated listing pages: output files with no source .org behind them. Repeat the
631# [[collections]] block for each one. A feed is the same thing with an XML template.
632[[collections]]
633source = "blog"            # directory to list; empty means every page
634output = "blog/index.html" # where to write it
635template = "list.html"     # template file name
636title = "Blog"
637sort = "date"              # date | title | path
638order = "desc"             # desc | asc
639nav = true                 # put this listing page in the site nav
640# paginate = 10            # entries per page; page 1 stays at `output`
641# paginate_output = "blog/page/{n}.html"   # where pages 2..N go; needs {n}
642
643# An RSS feed is a listing page with an XML template. It needs site.base_url above,
644# because a feed is read away from the site that served it and relative links break.
645# [[collections]]
646# source = "blog"
647# output = "feed.xml"
648# template = "feed.xml"
649# title = "Feed"
650
651# One page per tag, plus an index of all tags. `{tag}` in `output`/`title` is replaced
652# by each tag; the index gets `groups` instead of `pages`.
653[[collections]]
654source = "blog"
655group_by = "tags"            # "tags", or any #+KEYWORD: name to group by its value
656output = "tags/{tag}.html"
657template = "list.html"
658title = "Tagged: {tag}"
659index_output = "tags/index.html"
660index_template = "tags.html"
661index_title = "Tags"
662nav = true                   # adds the tag *index*, not every tag
663"#;