krz/orgo

Lightning fast org-mode static site generator. fast go org-mode static-site-generator

Commit a50bdfe775

a50bdfe77502f0e48848a1e1bf873862fa2ce1c0

parent: ef49ce57a8

Verified · cmc

cmc <hello@cleberg.net> · 2026-08-14 21:17 UTC

Add four built-in site themes

A zero-config build produced a real site with no CSS at all: correct HTML that
nobody would publish without writing a stylesheet first. `site.theme` names one
of four compiled-in stylesheets — `plain`, `blog`, `wiki`, `docs` — and each
build writes it to the output root as `theme.css`, exactly as `syntax.css` is
already written from a syntect theme.

Empty by default, so no existing site is restyled by upgrading and no site that
ships its own CSS finds a second stylesheet competing with it. An unknown name
is an error listing the four. `orgo init` writes `theme = "blog"`, the one line
in the starter config that is not a default.

Templates reach it through a new `theme` variable, guarded and linked *before*
`stylesheet` so the highlighter's colours win over the theme's. The built-in
layout and the starter templates do this; a `base.html` of your own can ignore
`theme.css` entirely. The name lives in `Config`, so it is in the config hash
and switching themes re-renders every page that links it.

A theme styles only markup orgo already emits — headings, tags, TODO keywords,
checkbox lists, footnotes, tables — plus the chrome the layouts put around it.
There is no theme-specific HTML, so switching or removing one touches no
template. Colours are `--orgo-*` properties on `:root`, which is the supported
way to adjust one rather than replace it.

The documentation site is the first user, and switching it over paid for
itself twice:

- `--orgo-code-*` coloured inline `code` as well as code blocks, so a site
  pairing a dark `highlight.theme` with a theme — as the docs do — turned every
  inline `code` into a dark pill on a light page. Inline code now follows the
  page; those three properties colour blocks only. The docs site's `style.css`
  is down from 130 lines to those three.
- A bare URL widened the page to 573px in a 320px viewport. `body` now sets
  `overflow-wrap: break-word` in all four.

Code blocks are the one part that cannot follow `prefers-color-scheme`, since
`syntax.css` is generated from a single syntect theme; a theme's dark mode keeps
its block surface light to stay readable against the default `InspiredGitHub`,
and a dark highlight theme is paired by overriding the three properties.
Everything else — including `TODO`/`DONE`, which were a dark red on a dark
ground until this — resolves per scheme.

Layout: unified · split

README.md +4 −1
@@ -48,7 +48,9 @@ Open <http://127.0.0.1:3000>. Edit `my-site/index.org`, save, and the page reloa
4848own — that is the loop you will spend your time in.
4949
5050`init` writes a starter post, a page layout you can edit, and a config file with every
51setting explained in comments. It never overwrites a file you already have.
51setting explained in comments. It never overwrites a file you already have. It also picks
52one of the four built-in themes — `plain`, `blog`, `wiki`, `docs` — so the site is styled
53from the first build; change `theme` in `orgo.toml`, or empty it and write your own CSS.
5254
5355## Org-Mode, Anywhere
5456
@@ -78,6 +80,7 @@ Each of these is a few lines of config, and each has a page in the guide:
7880| Tag pages, and an index of tags | [Collections](https://krazywarez.github.io/orgo/guide/03-collections.html) |
7981| An RSS feed | [Collections](https://krazywarez.github.io/orgo/guide/03-collections.html) |
8082| Numbered pages when a list gets long | [Collections](https://krazywarez.github.io/orgo/guide/03-collections.html) |
83| A built-in theme for a blog, a wiki or a doc site | [Configuration](https://krazywarez.github.io/orgo/guide/02-configuration.html) |
8184| Your own design, in ordinary HTML templates | [Templates](https://krazywarez.github.io/orgo/guide/04-templates.html) |
8285| Drafts that stay unpublished until you say so | [Authoring](https://krazywarez.github.io/orgo/guide/06-authoring.html) |
8386| A table of contents on long posts | [Authoring](https://krazywarez.github.io/orgo/guide/06-authoring.html) |
docs/guide/02-configuration.org +50
@@ -18,6 +18,7 @@ title = "orgo site"
1818base_url = ""
1919description = ""
2020language = "en"
21theme = ""
2122
2223[nav]
2324mode = "top-level"
@@ -52,6 +53,55 @@ Plus any number of =[[collections]]= blocks, documented in [[file:03-collections
5253| =base_url= | ="" | Absolute origin, *no trailing slash*. |
5354| =description= | ="" | Available as ={{ site.description }}=. |
5455| =language= | ="en"= | Goes in =<html lang>= in the built-in layout. |
56| =theme= | ="" | A built-in stylesheet, written to the output as =theme.css=. |
57
58** theme
59
60Four themes are compiled into the binary. Name one and each build writes it to the
61output root as =theme.css=, which the built-in layout and the templates =orgo init=
62writes both link.
63
64| Theme | Shape | For |
65|-------+-------+-----|
66| ="plain"= | Narrow, system fonts, hairline rules. | Readable defaults to build your own CSS on. |
67| ="blog"= | Serif prose, a centred masthead, styled post lists. | Dated writing. |
68| ="wiki"= | Wide and dense, contents in the margin, TODO states as badges. | Notes, a reference site. |
69| ="docs"= | Narrow, a contents card, quote blocks as notes, =#+LEDE:=. | A guide read in order. |
70
71All four follow =prefers-color-scheme=, so a site gets a dark mode without a toggle, a
72setting or a line of JavaScript — and all four reflow from a 320px phone up, with tables
73and code blocks scrolling inside their own box rather than widening the page.
74
75The default is empty: no stylesheet is written and no page links one, so the output is
76unstyled HTML. That is deliberate — a site that already ships CSS of its own should not
77find a second stylesheet competing with it, and upgrading orgo should never restyle a
78site. An unknown name is an error listing the four.
79
80A theme styles the markup orgo already emits — headings, tags, TODO keywords, checkbox
81lists, footnotes, tables — plus the chrome the built-in layout puts around it. There is
82no theme-specific HTML, so switching or removing one touches no template.
83
84Every colour is a custom property on =:root=, named =--orgo-*=. To adjust rather than
85replace a theme, ship a stylesheet of your own as an asset, link it after =theme.css=,
86and redefine the handful you care about:
87
88#+BEGIN_SRC css
89:root {
90 --orgo-accent: #7a1fa2;
91 --orgo-measure: 46rem;
92}
93#+END_SRC
94
95Code /blocks/ are the one part a theme cannot make follow =prefers-color-scheme=:
96=syntax.css= is generated from a single syntect theme, so a theme's dark mode keeps its
97block surface light to stay readable against the default =InspiredGitHub=. Pair a dark
98=highlight.theme= with overrides of =--orgo-code-bg=, =--orgo-code-fg= and
99=--orgo-code-rule=. Those three colour blocks only — inline =~code~= follows the page's
100own scheme, so it stays legible whichever highlight theme you use. The documentation site
101does exactly this; its =style.css= is those three lines and nothing else.
102
103When you outgrow a theme, drop =theme= from the config and write =templates/base.html=
104against your own CSS. Nothing else changes.
55105
56106** base_url
57107
docs/guide/04-templates.org +14
@@ -150,6 +150,20 @@ depth:
150150URL of the generated =syntax.css=, relative to this page. Link it or code blocks are
151151unstyled.
152152
153** theme
154
155URL of =theme.css=, relative to this page — the [[file:02-configuration.org][built-in theme]] named by
156=site.theme=. Empty when there is none, which is the default, so guard it and link it
157*before* =stylesheet= or the theme's code colours would override the highlighter's:
158
159#+BEGIN_SRC html
160{% if theme %}<link rel="stylesheet" href="{{ theme }}">{% endif %}
161{% if stylesheet %}<link rel="stylesheet" href="{{ stylesheet }}">{% endif %}
162#+END_SRC
163
164A layout that ignores it is a layout with its own CSS, which is the point at which you
165have outgrown the setting.
166
153167** pages, group, groups, paginator
154168
155169Present on generated pages; see [[file:03-collections.org][Collections]]. =pages= is also
docs/guide/10-deploying.org +2 −2
@@ -14,8 +14,8 @@ Two differences from the build you run while writing:
1414 bad build fails rather than shipping.
1515- No =--drafts=, so pages marked =#+DRAFT:= stay out.
1616
17Everything in =_site= is the site: HTML, the generated =syntax.css=, and every asset
18copied from the source. There is no runtime, no server requirement and no build step
17Everything in =_site= is the site: HTML, the generated =syntax.css=, =theme.css= if the
18config names a [[file:02-configuration.org][theme]], and every asset copied from the source. There is no runtime, no server requirement and no build step
1919downstream.
2020
2121* Set base_url for production
docs/guide/11-versioning.org +8 −1
@@ -12,7 +12,7 @@ Changing any of this incompatibly requires a major version.
1212| Stable | What that covers |
1313|--------+------------------|
1414| =orgo.toml= keys | Their names, types and meaning. |
15| Template context | =page=, =site=, =nav=, =root=, =pages=, =group=, =groups=, =paginator=, =stylesheet=, and the =absolute=, =rfc822= and =truncate= filters. |
15| Template context | =page=, =site=, =nav=, =root=, =pages=, =group=, =groups=, =paginator=, =stylesheet=, =theme=, and the =absolute=, =rfc822= and =truncate= filters. |
1616| The CLI | Command names, flags and exit codes. |
1717| URLs | How a source path becomes an output path, =#+SLUG:= included. |
1818
@@ -42,6 +42,13 @@ The class names the documentation names are the ones to write CSS against:
4242=post-list=, =post-list-item=, =figure-number=, =table-number=, =section-number-N=,
4343=footnote-ref=, =verbatim=, and the =on=/=off=/=trans= classes on checkbox items.
4444
45** What a built-in theme looks like
46
47The names — =plain=, =blog=, =wiki=, =docs= — and the fact that the chosen one is written
48to =theme.css= are stable. Its CSS is not: a theme is a starting point that improves
49between releases, and a site that cannot afford that should copy the stylesheet it likes
50into its own assets and stop naming a theme.
51
4552** The Rust API
4653
4754The crate is on crates.io so the binary can be installed with =cargo install=. The library
docs/orgo.toml +5
@@ -10,6 +10,9 @@ language = "en"
1010# Left empty so the docs build with relative URLs and open from the filesystem. Set it to
1111# your real origin to enable canonical links and feeds.
1212base_url = ""
13# The built-in theme written for exactly this: a guide read in order, with a contents
14# block that matters. The site's own CSS is six lines of overrides on top of it.
15theme = "docs"
1316
1417[nav]
1518# Explicit, because the header already links home: listing index.org here as well would
@@ -25,6 +28,8 @@ dir = "templates"
2528# A dark theme, with code blocks styled dark in both colour schemes. syntax.css is
2629# generated from a single syntect theme and cannot respond to prefers-color-scheme, so
2730# the page CSS matches the theme rather than leaving code unreadable in one of them.
31# That match is style.css, which overrides the site theme's --orgo-code-* properties and
32# nothing else — the documented way to adjust a built-in theme instead of replacing it.
2833theme = "base16-ocean.dark"
2934
3035[html]
docs/quickstart.org +1 −1
@@ -19,7 +19,7 @@ already has content is safe and additive.
1919
2020#+BEGIN_EXAMPLE
2121my-site/
22 orgo.toml every setting, at its default, commented
22 orgo.toml every setting, commented — all at their defaults but `theme`
2323 index.org the home page
2424 blog/first-post.org a post, to show the collection working
2525 templates/
docs/style.css +11 −125
@@ -1,130 +1,16 @@
1/* Documentation site styling.
1/* Documentation site styling: the `docs` built-in theme, adjusted rather than replaced.
22 *
33 * A plain asset, copied through the build untouched — which is also how any other CSS,
4 * image or font in a source directory reaches the output. */
4 * image or font in a source directory reaches the output. It is linked after theme.css,
5 * which is what lets these definitions win.
6 *
7 * Only the code surface is changed. syntax.css is generated from one syntect theme and
8 * cannot follow prefers-color-scheme, so a site pairing a dark `highlight.theme` with a
9 * theme has to say what colour code sits on — here, dark in both schemes, matching
10 * base16-ocean.dark. Everything else is the theme's. */
511
612:root {
7 --ink: #1c1f24;
8 --muted: #5b6472;
9 --rule: #dfe3e8;
10 --accent: #0b5fa5;
11 --surface: #f6f8fa;
12 --measure: 42rem;
13}
14
15@media (prefers-color-scheme: dark) {
16 :root {
17 --ink: #dee3ea;
18 --muted: #9aa4b2;
19 --rule: #2b3138;
20 --accent: #79b8ff;
21 --surface: #171a1f;
22 }
23 body { background: #0f1216; }
24}
25
26* { box-sizing: border-box; }
27
28body {
29 margin: 0;
30 color: var(--ink);
31 font: 16px/1.65 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
32}
33
34header.site {
35 border-bottom: 1px solid var(--rule);
36 padding: 1rem 1.5rem;
37 display: flex;
38 flex-wrap: wrap;
39 gap: 1rem 1.5rem;
40 align-items: baseline;
13 --orgo-code-bg: #2b303b;
14 --orgo-code-fg: #c0c5ce;
15 --orgo-code-rule: #1f232b;
4116}
42
43header.site .site-title {
44 font-weight: 700;
45 font-size: 1.05rem;
46 color: var(--ink);
47 text-decoration: none;
48}
49
50header.site nav { display: flex; gap: 1.25rem; flex-wrap: wrap; }
51header.site nav a { color: var(--muted); text-decoration: none; }
52header.site nav a:hover { color: var(--accent); }
53
54main {
55 max-width: var(--measure);
56 margin: 0 auto;
57 padding: 2.5rem 1.5rem 5rem;
58}
59
60h1 { font-size: 2rem; line-height: 1.2; margin: 0 0 .5rem; letter-spacing: -0.02em; }
61h2 { font-size: 1.35rem; margin: 2.5rem 0 .75rem; letter-spacing: -0.01em; }
62h3 { font-size: 1.1rem; margin: 2rem 0 .5rem; }
63
64p.page-date, p.lede { color: var(--muted); }
65p.lede { font-size: 1.1rem; margin-top: 0; }
66
67a { color: var(--accent); }
68
69code {
70 font: 0.875em/1.5 ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
71 background: var(--surface);
72 padding: .1em .35em;
73 border-radius: 3px;
74}
75
76/* Code blocks are dark in both colour schemes, matching the syntect theme named in
77 orgo.toml. One generated stylesheet cannot follow prefers-color-scheme, so the page
78 commits to the theme's palette instead of leaving highlighted code unreadable in one
79 of the two. */
80pre {
81 background: #2b303b;
82 color: #c0c5ce;
83 border: 1px solid #1f232b;
84 border-radius: 6px;
85 padding: .9rem 1rem;
86 overflow-x: auto;
87}
88
89pre code { background: none; padding: 0; }
90
91blockquote {
92 margin: 1.5rem 0;
93 padding: .25rem 0 .25rem 1rem;
94 border-left: 3px solid var(--rule);
95 color: var(--muted);
96}
97
98table { border-collapse: collapse; width: 100%; margin: 1.25rem 0; display: block; overflow-x: auto; }
99th, td { text-align: left; padding: .5rem .75rem; border-bottom: 1px solid var(--rule); }
100th { font-size: .85rem; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
101
102hr { border: 0; border-top: 1px solid var(--rule); margin: 2.5rem 0; }
103
104/* Table of contents, emitted from page.toc */
105nav.toc {
106 background: var(--surface);
107 border: 1px solid var(--rule);
108 border-radius: 6px;
109 padding: .75rem 1.25rem 1rem;
110 margin: 1.5rem 0 2.5rem;
111}
112nav.toc h2 { font-size: .8rem; text-transform: uppercase; letter-spacing: .06em; margin: .25rem 0 .5rem; color: var(--muted); }
113nav.toc ul { margin: 0; padding-left: 1.1rem; }
114nav.toc li { margin: .15rem 0; }
115
116ul.post-list { list-style: none; padding: 0; }
117ul.post-list > li { padding: 1rem 0; border-bottom: 1px solid var(--rule); }
118ul.post-list a { font-weight: 600; font-size: 1.05rem; }
119p.excerpt { margin: .35rem 0 .2rem; color: var(--muted); }
120span.reading-time { font-size: .85rem; color: var(--muted); }
121
122footer.site {
123 border-top: 1px solid var(--rule);
124 padding: 1.5rem;
125 color: var(--muted);
126 font-size: .9rem;
127 text-align: center;
128}
129
130.tag { font-size: .75rem; background: var(--surface); border: 1px solid var(--rule); border-radius: 999px; padding: .1em .6em; color: var(--muted); }
docs/templates/base.html +4 −1
@@ -8,10 +8,13 @@
88<link rel="canonical" href="{{ page.url | absolute }}">
99{%- endif %}
1010<meta name="description" content="{{ page.excerpt | truncate(150) }}">
11<link rel="stylesheet" href="{{ root }}style.css">
11{%- if theme %}
12<link rel="stylesheet" href="{{ theme }}">
13{%- endif %}
1214{%- if stylesheet %}
1315<link rel="stylesheet" href="{{ stylesheet }}">
1416{%- endif %}
17<link rel="stylesheet" href="{{ root }}style.css">
1518</head>
1619<body>
1720<header class="site">
src/config.rs +29 −3
@@ -331,6 +331,14 @@ pub struct Site {
331331 pub description: String,
332332 /// `<html lang="…">` in the default layout.
333333 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,
334342}
335343
336344impl Default for Site {
@@ -340,6 +348,7 @@ impl Default for Site {
340348 base_url: String::new(),
341349 description: String::new(),
342350 language: "en".to_string(),
351 theme: String::new(),
343352 }
344353 }
345354}
@@ -539,6 +548,14 @@ impl Config {
539548 );
540549 }
541550 }
551 if !self.site.theme.is_empty() && crate::theme::theme_css(&self.site.theme).is_none() {
552 anyhow::bail!(
553 "unknown site.theme {:?}. Available: {} — or leave it empty for no \
554 stylesheet",
555 self.site.theme,
556 crate::theme::available_themes().join(", ")
557 );
558 }
542559 if !self.site.base_url.is_empty() && self.site.base_url.ends_with('/') {
543560 anyhow::bail!(
544561 "site.base_url must not end with a slash (got {:?}) — URLs are joined \
@@ -551,9 +568,12 @@ impl Config {
551568}
552569
553570/// 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.
571/// what is configurable. Every value shown is the default — except `site.theme`, which
572/// picks a stylesheet so a new site looks like something on its first build — so
573/// deleting any line is safe.
574pub const STARTER_CONFIG: &str = r#"# orgo configuration. Every setting here is optional and shown at its default — apart
575# from `theme`, noted below — so you can delete any line you do not need, or the whole
576# file.
557577
558578[site]
559579title = "orgo site"
@@ -562,6 +582,12 @@ title = "orgo site"
562582base_url = ""
563583description = ""
564584language = "en"
585# A built-in stylesheet, written to the output as theme.css: "plain" (readable defaults
586# to build your own CSS on), "blog" (serif prose, masthead, styled post lists), "wiki"
587# (wide and dense, contents in the margin, TODO states shown) or "docs" (a guide read in
588# order). The one line here that is not a default: the default is "", which emits no
589# stylesheet at all. Your own base.html can ignore theme.css and link whatever it likes.
590theme = "blog"
565591
566592[nav]
567593# Which pages appear in the shared navigation:
src/lib.rs +1
@@ -20,5 +20,6 @@ pub mod resolve;
2020pub mod serve;
2121pub mod site;
2222pub mod template;
23pub mod theme;
2324pub mod util;
2425pub mod watch;
src/main.rs +13 −1
@@ -10,7 +10,7 @@ use orgo::parser::parse;
1010use orgo::config::{self, Config};
1111use orgo::render::{self, render, Html, SyntectHighlighter};
1212use orgo::resolve::ResolvedDoc;
13use orgo::site::{build_site, BuildOptions, SYNTAX_STYLESHEET};
13use orgo::site::{build_site, BuildOptions, SYNTAX_STYLESHEET, THEME_STYLESHEET};
1414use orgo::template::{PageContext, RenderContext, SiteContext, Templater};
1515
1616#[derive(Parser)]
@@ -325,6 +325,13 @@ fn build_file(input: &Utf8Path, output: &Utf8Path) -> Result<()> {
325325 };
326326 let mut ctx = RenderContext::new(&site, &page_ctx, &[], SYNTAX_STYLESHEET, "");
327327 ctx.body = &fragment;
328 // The page and its stylesheets are written side by side here, so the link is a bare
329 // filename rather than a path back to a site root that does not exist.
330 ctx.theme = if config.site.theme.is_empty() {
331 ""
332 } else {
333 THEME_STYLESHEET
334 };
328335 // `#+TEMPLATE:` and `[[pages]]` apply here too, so `build one.org` and a whole-site
329336 // build put the same page through the same layout.
330337 let name = config::page_template(
@@ -339,5 +346,10 @@ fn build_file(input: &Utf8Path, output: &Utf8Path) -> Result<()> {
339346
340347 let css = output.with_file_name(SYNTAX_STYLESHEET);
341348 fs::write(&css, css_text).with_context(|| format!("writing stylesheet {css}"))?;
349
350 if let Some(theme_css) = orgo::theme::theme_css(&config.site.theme) {
351 let path = output.with_file_name(THEME_STYLESHEET);
352 fs::write(&path, theme_css).with_context(|| format!("writing stylesheet {path}"))?;
353 }
342354 Ok(())
343355}
src/site.rs +26
@@ -32,6 +32,7 @@ use crate::template::{
3232 GroupContext, NavItem, PageContext, Paginator, PaginatorPage, RenderContext, SiteContext,
3333 Templater,
3434};
35use crate::theme;
3536use crate::util::{
3637 document_text, first_paragraph, is_draft, iso_date, iso_time, option_enabled,
3738 output_path, output_url,
@@ -882,8 +883,10 @@ fn render_page(
882883 // Relative to the *output* path, since `#+SLUG:` can move a page between depths.
883884 let root = relative_root(&p.output);
884885 let stylesheet = format!("{root}{SYNTAX_STYLESHEET}");
886 let theme = theme_href(config, &root);
885887 let mut ctx = RenderContext::new(site, &p.context, &p.nav, &stylesheet, &root);
886888 ctx.body = &fragment;
889 ctx.theme = &theme;
887890 ctx.pages = pages;
888891 templater
889892 .render(&p.template, &ctx)
@@ -893,6 +896,20 @@ fn render_page(
893896/// Site-root-relative name of the generated syntax stylesheet. Every page links to it.
894897pub const SYNTAX_STYLESHEET: &str = "syntax.css";
895898
899/// Site-root-relative name of the built-in theme's stylesheet, written only when
900/// `site.theme` names one.
901pub const THEME_STYLESHEET: &str = "theme.css";
902
903/// Where a page at `root` finds `theme.css` — empty when the site has no theme, which
904/// is how a template knows to link nothing at all.
905fn theme_href(config: &Config, root: &str) -> String {
906 if config.site.theme.is_empty() {
907 String::new()
908 } else {
909 format!("{root}{THEME_STYLESHEET}")
910 }
911}
912
896913/// Site-root-relative name of the generated sitemap.
897914pub const SITEMAP: &str = "sitemap.xml";
898915
@@ -1132,7 +1149,9 @@ pub fn build_site(src: &Utf8Path, out: &Utf8Path, opts: &BuildOptions) -> Result
11321149 let with_content = listing
11331150 .include_content
11341151 .then(|| entries_with_content(&listing.entries, &preps, &highlighter, &cfg));
1152 let theme = theme_href(&cfg, &root);
11351153 let mut ctx = RenderContext::new(&site, &page_ctx, &nav, &stylesheet, &root);
1154 ctx.theme = &theme;
11361155 ctx.pages = Some(with_content.as_deref().unwrap_or(&listing.entries));
11371156 ctx.group = listing.group.as_ref();
11381157 ctx.groups = &listing.groups;
@@ -1167,6 +1186,13 @@ pub fn build_site(src: &Utf8Path, out: &Utf8Path, opts: &BuildOptions) -> Result
11671186 fs::write(out.join(SYNTAX_STYLESHEET), &syntax_css)
11681187 .with_context(|| format!("writing {SYNTAX_STYLESHEET} under {out}"))?;
11691188
1189 // The built-in theme, on the same terms: compiled in, written every build, and named
1190 // in the config hash so switching themes re-renders the pages that link it.
1191 if let Some(css) = theme::theme_css(&cfg.site.theme) {
1192 fs::write(out.join(THEME_STYLESHEET), css)
1193 .with_context(|| format!("writing {THEME_STYLESHEET} under {out}"))?;
1194 }
1195
11701196 // A sitemap covers every page the build emits, authored and generated alike, so it is
11711197 // written here rather than declared as a collection: a collection lists the pages it
11721198 // was pointed at, and this one has to know about all of them including itself.
src/template.rs +16
@@ -90,6 +90,9 @@ const BASE_TEMPLATE: &str = r##"<!DOCTYPE html>
9090{%- if page.description %}
9191<meta name="description" content="{{ page.description }}">
9292{%- endif %}
93{%- if theme %}
94<link rel="stylesheet" href="{{ theme }}">
95{%- endif %}
9396{%- if stylesheet %}
9497<link rel="stylesheet" href="{{ stylesheet }}">
9598{%- endif %}
@@ -267,6 +270,7 @@ impl Templater {
267270 body => ctx.body,
268271 nav => ctx.nav,
269272 stylesheet => ctx.stylesheet,
273 theme => ctx.theme,
270274 root => ctx.root,
271275 pages => ctx.pages,
272276 group => ctx.group,
@@ -338,6 +342,9 @@ pub struct RenderContext<'a> {
338342 pub nav: &'a [NavItem],
339343 /// URL of the syntax stylesheet, relative to this page.
340344 pub stylesheet: &'a str,
345 /// Path to the built-in theme's `theme.css`, relative to this page — empty when
346 /// `site.theme` names no theme, which is the default.
347 pub theme: &'a str,
341348 /// `../`-prefix back to the site root from this page.
342349 pub root: &'a str,
343350 /// The pages this listing shows, or every page when `expose_page_list` is on.
@@ -366,6 +373,9 @@ impl<'a> RenderContext<'a> {
366373 body: "",
367374 nav,
368375 stylesheet,
376 // Assigned after construction, like `body`: most callers have no theme, and
377 // an empty one is exactly "link no theme stylesheet".
378 theme: "",
369379 root,
370380 pages: None,
371381 group: None,
@@ -383,6 +393,9 @@ pub const STARTER_TAGS_TEMPLATE: &str = r#"<!DOCTYPE html>
383393<meta charset="utf-8">
384394<meta name="viewport" content="width=device-width, initial-scale=1">
385395<title>{{ page.title }} &middot; {{ site.title }}</title>
396{%- if theme %}
397<link rel="stylesheet" href="{{ theme }}">
398{%- endif %}
386399{%- if stylesheet %}
387400<link rel="stylesheet" href="{{ stylesheet }}">
388401{%- endif %}
@@ -418,6 +431,9 @@ pub const STARTER_LIST_TEMPLATE: &str = r#"<!DOCTYPE html>
418431<meta charset="utf-8">
419432<meta name="viewport" content="width=device-width, initial-scale=1">
420433<title>{{ page.title }} &middot; {{ site.title }}</title>
434{%- if theme %}
435<link rel="stylesheet" href="{{ theme }}">
436{%- endif %}
421437{%- if stylesheet %}
422438<link rel="stylesheet" href="{{ stylesheet }}">
423439{%- endif %}
src/theme.rs added +43
@@ -0,0 +1,43 @@
1//! Built-in site themes: whole stylesheets compiled into the binary.
2//!
3//! A theme is one CSS file and nothing else. It styles the markup the RENDER stage
4//! emits — org's headings, tags, TODO keywords, checkbox lists, footnotes — plus the
5//! chrome the built-in layout and the starter templates put around it. There is no
6//! theme-specific HTML, so a theme can be switched, or removed, without touching a
7//! template.
8//!
9//! Compiled in for the same reason the syntax definitions are: `cargo install orgo`
10//! gives you one binary, and a site that needs a stylesheet fetched from somewhere else
11//! before it looks like anything is not that. The chosen theme is written to the output
12//! root as `theme.css` on every build, the way [`crate::render::syntax_css`] writes
13//! `syntax.css`.
14//!
15//! Nothing here is a wrapper you have to work through: `site.theme` empty emits no
16//! stylesheet at all, and a `base.html` of your own can ignore `theme.css` and link
17//! whatever it likes.
18
19/// Every built-in theme, as `(name, stylesheet)`, in the order they are offered.
20///
21/// - `plain` — readable defaults with no design opinion, to build your own CSS on.
22/// - `blog` — dated writing: serif prose, a masthead, styled listing pages.
23/// - `wiki` — a dense reference site: wide, sidebar contents, tables and TODO states.
24/// - `docs` — a guide read in order: prominent contents, code-forward, `#+LEDE:`.
25pub const THEMES: &[(&str, &str)] = &[
26 ("plain", include_str!("../themes/plain.css")),
27 ("blog", include_str!("../themes/blog.css")),
28 ("wiki", include_str!("../themes/wiki.css")),
29 ("docs", include_str!("../themes/docs.css")),
30];
31
32/// The stylesheet for a built-in theme, or `None` if no theme goes by that name.
33pub fn theme_css(name: &str) -> Option<&'static str> {
34 THEMES
35 .iter()
36 .find(|(theme, _)| *theme == name)
37 .map(|(_, css)| *css)
38}
39
40/// Every theme name [`theme_css`] accepts, for error messages and documentation.
41pub fn available_themes() -> Vec<&'static str> {
42 THEMES.iter().map(|(name, _)| *name).collect()
43}
tests/config.rs +110
@@ -2499,3 +2499,113 @@ fn well_known_is_published_but_other_dot_entries_are_not() {
24992499 assert!(!out.join(".git/config").exists(), ".git stays out");
25002500 assert!(!out.join(".env").exists(), ".env stays out");
25012501}
2502
2503// ---------------------------------------------------------------------------
2504// Built-in themes
2505// ---------------------------------------------------------------------------
2506
2507/// A theme is one compiled-in stylesheet: named in the config, written to the output
2508/// root, linked from every page at whatever depth that page sits.
2509#[test]
2510fn a_built_in_theme_is_written_once_and_linked_from_every_depth() {
2511 let root = tmpdir("theme-site");
2512 let src = root.join("src");
2513 std::fs::create_dir_all(&src).unwrap();
2514 write_site(&src);
2515 std::fs::write(src.join("orgo.toml"), "[site]\ntheme = \"wiki\"\n").unwrap();
2516 let out = root.join("out");
2517 build(&src, &out);
2518
2519 let css = std::fs::read_to_string(out.join("theme.css")).expect("theme.css is written");
2520 assert_eq!(
2521 css,
2522 orgo::theme::theme_css("wiki").unwrap(),
2523 "verbatim, not a rebuilt approximation of it"
2524 );
2525
2526 assert!(
2527 page(&out, "index.html").contains("<link rel=\"stylesheet\" href=\"theme.css\">"),
2528 "a root page links it directly"
2529 );
2530 assert!(
2531 page(&out, "blog/post.html").contains("<link rel=\"stylesheet\" href=\"../theme.css\">"),
2532 "a nested page reaches back up to it"
2533 );
2534}
2535
2536/// The theme has to come before `syntax.css`, or a theme's `pre code` colour would
2537/// override the highlighter's and code blocks would render in one flat colour.
2538#[test]
2539fn the_theme_is_linked_ahead_of_the_syntax_stylesheet() {
2540 let root = tmpdir("theme-order");
2541 let src = root.join("src");
2542 std::fs::create_dir_all(&src).unwrap();
2543 write_site(&src);
2544 std::fs::write(src.join("orgo.toml"), "[site]\ntheme = \"docs\"\n").unwrap();
2545 let out = root.join("out");
2546 build(&src, &out);
2547
2548 let home = page(&out, "index.html");
2549 let theme = home.find("theme.css").expect("theme link");
2550 let syntax = home.find("syntax.css").expect("syntax link");
2551 assert!(theme < syntax, "theme first, highlighting on top of it: {home}");
2552}
2553
2554/// No theme is the default. An existing site upgrading must not find itself restyled,
2555/// and a site with a stylesheet of its own must not have a second one competing with it.
2556#[test]
2557fn no_theme_is_the_default_and_emits_no_stylesheet() {
2558 let root = tmpdir("theme-none");
2559 let src = root.join("src");
2560 std::fs::create_dir_all(&src).unwrap();
2561 write_site(&src);
2562 let out = root.join("out");
2563 build(&src, &out);
2564
2565 assert!(!out.join("theme.css").exists(), "nothing to write");
2566 assert!(
2567 !page(&out, "index.html").contains("theme.css"),
2568 "and nothing to link"
2569 );
2570}
2571
2572/// A misspelled theme name would otherwise emit an unstyled site with no complaint,
2573/// which looks exactly like the theme setting doing nothing.
2574#[test]
2575fn an_unknown_site_theme_is_rejected_with_the_available_ones() {
2576 let root = tmpdir("theme-unknown");
2577 let src = root.join("src");
2578 std::fs::create_dir_all(&src).unwrap();
2579 write_site(&src);
2580 std::fs::write(src.join("orgo.toml"), "[site]\ntheme = \"blogg\"\n").unwrap();
2581
2582 let err = build_site(&src, &root.join("out"), &BuildOptions::default())
2583 .expect_err("unknown theme must fail");
2584 let message = format!("{err:#}");
2585 assert!(message.contains("blogg"), "names the bad theme: {message}");
2586 for name in orgo::theme::available_themes() {
2587 assert!(message.contains(name), "lists {name}: {message}");
2588 }
2589}
2590
2591/// Switching themes changes every page's `<head>`, so every page has to be re-rendered.
2592/// The theme name lives in the config hash, which is what makes that happen.
2593#[test]
2594fn switching_the_theme_re_renders_the_pages_that_link_it() {
2595 let root = tmpdir("theme-switch");
2596 let src = root.join("src");
2597 std::fs::create_dir_all(&src).unwrap();
2598 write_site(&src);
2599 std::fs::write(src.join("orgo.toml"), "[site]\ntheme = \"plain\"\n").unwrap();
2600 let out = root.join("out");
2601 build(&src, &out);
2602
2603 std::fs::write(src.join("orgo.toml"), "[site]\ntheme = \"blog\"\n").unwrap();
2604 let report = build(&src, &out);
2605 assert!(report.skipped.is_empty(), "no page may keep the old head");
2606 assert_eq!(
2607 std::fs::read_to_string(out.join("theme.css")).unwrap(),
2608 orgo::theme::theme_css("blog").unwrap(),
2609 "and the stylesheet on disk is the new one"
2610 );
2611}
themes/blog.css added +298
@@ -0,0 +1,298 @@
1/* orgo built-in theme: blog
2 *
3 * A personal site of dated writing. Serif prose at a narrow measure, a masthead rather
4 * than a toolbar, and listing pages — the blog index, a tag page, a feed's HTML
5 * counterpart — treated as first-class rather than as an afterthought.
6 *
7 * Every colour is a custom property on :root, so a stylesheet of your own loaded after
8 * this one can retheme the site by redefining a handful of values. */
9
10:root {
11 --orgo-ink: #23201d;
12 --orgo-muted: #6b645d;
13 --orgo-rule: #e5e0d8;
14 --orgo-accent: #9a3412;
15 --orgo-surface: #faf7f2;
16 --orgo-bg: #fdfcfa;
17 --orgo-measure: 36rem;
18 --orgo-font: Iowan Old Style, Charter, Palatino, Georgia, "Times New Roman", serif;
19 --orgo-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
20 --orgo-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
21 /* These three colour code *blocks* only; inline code follows the page. A block keeps
22 a light surface in both colour schemes because syntax.css is generated from one
23 syntect theme (`highlight.theme`, InspiredGitHub by default) and cannot follow
24 prefers-color-scheme. Pair a dark highlight theme with overrides of these three. */
25 --orgo-todo: #b02a37;
26 --orgo-done: #2c7a4b;
27 --orgo-code-bg: #f5f2ec;
28 --orgo-code-fg: #323232;
29 --orgo-code-rule: #e5e0d8;
30}
31
32@media (prefers-color-scheme: dark) {
33 :root {
34 --orgo-ink: #e6e1da;
35 --orgo-muted: #a49c93;
36 --orgo-rule: #322e2a;
37 --orgo-accent: #f0a066;
38 --orgo-surface: #1b1917;
39 --orgo-todo: #f0909a;
40 --orgo-done: #74c795;
41 --orgo-bg: #141312;
42 }
43}
44
45* { box-sizing: border-box; }
46
47body {
48 margin: 0;
49 background: var(--orgo-bg);
50 color: var(--orgo-ink);
51 font: 18px/1.7 var(--orgo-font);
52 -webkit-font-smoothing: antialiased;
53 /* A bare URL or a long identifier must wrap, not widen the page on a phone. */
54 overflow-wrap: break-word;
55}
56
57/* Masthead ----------------------------------------------------------------- */
58
59body > header {
60 max-width: var(--orgo-measure);
61 margin: 0 auto;
62 padding: 3rem 1.5rem 1.5rem;
63 text-align: center;
64 display: flex;
65 flex-direction: column;
66 align-items: center;
67 gap: .75rem;
68}
69
70.site-title {
71 font-size: 1.5rem;
72 font-weight: 400;
73 letter-spacing: .02em;
74 color: var(--orgo-ink);
75 text-decoration: none;
76}
77
78body > header nav {
79 display: flex;
80 flex-wrap: wrap;
81 justify-content: center;
82 gap: 1.5rem;
83 font: .8rem/1 var(--orgo-ui);
84 text-transform: uppercase;
85 letter-spacing: .1em;
86}
87body > header nav a { color: var(--orgo-muted); text-decoration: none; }
88body > header nav a:hover { color: var(--orgo-accent); }
89
90/* Page body ---------------------------------------------------------------- */
91
92main {
93 max-width: var(--orgo-measure);
94 margin: 0 auto;
95 padding: 2rem 1.5rem 5rem;
96}
97
98h1 {
99 font-size: 2.1rem;
100 font-weight: 400;
101 line-height: 1.25;
102 letter-spacing: -0.01em;
103 margin: 0 0 .5rem;
104}
105h2 { font-size: 1.4rem; font-weight: 600; margin: 2.5rem 0 .75rem; }
106h3 { font-size: 1.15rem; font-weight: 600; margin: 2rem 0 .5rem; }
107h4, h5, h6 { font-size: 1rem; font-weight: 600; margin: 1.5rem 0 .5rem; }
108
109a { color: var(--orgo-accent); text-underline-offset: .15em; }
110
111/* The byline the built-in layout prints under the title. */
112p.page-date {
113 font: .8rem/1 var(--orgo-ui);
114 text-transform: uppercase;
115 letter-spacing: .1em;
116 color: var(--orgo-muted);
117 margin: 0 0 2.5rem;
118}
119
120img, video { max-width: 100%; height: auto; }
121
122figure { margin: 2rem 0; }
123figcaption {
124 font: .85rem/1.5 var(--orgo-ui);
125 color: var(--orgo-muted);
126 margin-top: .5rem;
127 text-align: center;
128}
129.figure-number, .table-number { font-weight: 600; }
130
131blockquote {
132 margin: 2rem 0;
133 padding: 0 1.5rem;
134 border-left: 2px solid var(--orgo-accent);
135 font-style: italic;
136 color: var(--orgo-muted);
137}
138
139hr {
140 border: 0;
141 margin: 3rem auto;
142 width: 6rem;
143 border-top: 1px solid var(--orgo-rule);
144}
145
146.center { text-align: center; }
147.verse { font-family: var(--orgo-mono); font-size: .95rem; white-space: pre-wrap; }
148
149dt { font-weight: 600; margin-top: .75rem; }
150dd { margin: 0 0 0 1.5rem; }
151
152/* Code --------------------------------------------------------------------- */
153
154/* Inline code is prose furniture, so it follows the page rather than the code blocks. */
155code {
156 font: .85em/1.5 var(--orgo-mono);
157 background: var(--orgo-surface);
158 color: inherit;
159 padding: .1em .35em;
160 border-radius: 3px;
161}
162
163pre {
164 background: var(--orgo-code-bg);
165 color: var(--orgo-code-fg);
166 border: 1px solid var(--orgo-code-rule);
167 border-radius: 4px;
168 padding: 1rem 1.1rem;
169 font-size: .9rem;
170 line-height: 1.55;
171 overflow-x: auto;
172}
173
174pre code { background: none; color: inherit; padding: 0; }
175
176/* Tables ------------------------------------------------------------------- */
177
178table {
179 border-collapse: collapse;
180 margin: 2rem 0;
181 font: .95rem/1.5 var(--orgo-ui);
182 display: block;
183 overflow-x: auto;
184}
185caption { text-align: left; color: var(--orgo-muted); font-size: .9rem; padding-bottom: .5rem; }
186th, td { text-align: left; padding: .5rem .85rem; border-bottom: 1px solid var(--orgo-rule); }
187th {
188 font-size: .75rem;
189 text-transform: uppercase;
190 letter-spacing: .06em;
191 color: var(--orgo-muted);
192}
193
194/* Org-specific markup ------------------------------------------------------ */
195
196.tag {
197 font: .7rem/1.6 var(--orgo-ui);
198 text-transform: uppercase;
199 letter-spacing: .08em;
200 color: var(--orgo-muted);
201 background: var(--orgo-surface);
202 border: 1px solid var(--orgo-rule);
203 border-radius: 999px;
204 padding: .1em .7em;
205 vertical-align: middle;
206}
207
208.todo, .done { font: .7em/1 var(--orgo-mono); letter-spacing: .05em; }
209.todo { color: var(--orgo-todo); }
210.done { color: var(--orgo-done); }
211.priority { color: var(--orgo-muted); font-family: var(--orgo-mono); font-size: .8em; }
212
213time.timestamp { font: .9em/1 var(--orgo-ui); color: var(--orgo-muted); }
214
215li.on, li.trans { color: var(--orgo-muted); }
216li.on { text-decoration: line-through; }
217
218.footnotes {
219 margin-top: 4rem;
220 font-size: .9rem;
221 color: var(--orgo-muted);
222}
223.footnote-ref a { text-decoration: none; }
224
225/* Table of contents, from page.toc ----------------------------------------- */
226
227nav.toc {
228 border-top: 1px solid var(--orgo-rule);
229 border-bottom: 1px solid var(--orgo-rule);
230 padding: 1rem 0;
231 margin: 2rem 0 3rem;
232 font: .95rem/1.6 var(--orgo-ui);
233}
234nav.toc h2 {
235 font-size: .7rem;
236 text-transform: uppercase;
237 letter-spacing: .1em;
238 color: var(--orgo-muted);
239 margin: 0 0 .5rem;
240}
241nav.toc ul { margin: 0; padding-left: 1.1rem; }
242nav.toc li { margin: .15rem 0; }
243
244/* Listing pages ------------------------------------------------------------ */
245
246ul.post-list { list-style: none; padding: 0; margin: 2rem 0 0; }
247ul.post-list > li { padding: 1.75rem 0; border-bottom: 1px solid var(--orgo-rule); }
248ul.post-list > li:first-child { padding-top: 0; }
249ul.post-list time {
250 display: block;
251 font: .75rem/1 var(--orgo-ui);
252 text-transform: uppercase;
253 letter-spacing: .1em;
254 color: var(--orgo-muted);
255 margin-bottom: .4rem;
256}
257ul.post-list a {
258 font-size: 1.3rem;
259 line-height: 1.3;
260 color: var(--orgo-ink);
261 text-decoration: none;
262}
263ul.post-list a:hover { color: var(--orgo-accent); }
264p.excerpt { margin: .5rem 0 .35rem; color: var(--orgo-muted); font-size: .95rem; }
265span.reading-time { font: .75rem/1 var(--orgo-ui); letter-spacing: .06em; color: var(--orgo-muted); }
266
267ul.tag-list {
268 list-style: none;
269 padding: 0;
270 display: flex;
271 flex-wrap: wrap;
272 gap: .75rem 1.25rem;
273 font: .95rem/1.6 var(--orgo-ui);
274}
275
276nav.pagination {
277 display: flex;
278 justify-content: space-between;
279 align-items: baseline;
280 gap: 1rem;
281 margin-top: 3rem;
282 font: .85rem/1 var(--orgo-ui);
283 text-transform: uppercase;
284 letter-spacing: .08em;
285 color: var(--orgo-muted);
286}
287
288/* Footer ------------------------------------------------------------------- */
289
290body > footer {
291 max-width: var(--orgo-measure);
292 margin: 0 auto;
293 border-top: 1px solid var(--orgo-rule);
294 padding: 1.5rem;
295 font: .85rem/1.6 var(--orgo-ui);
296 color: var(--orgo-muted);
297 text-align: center;
298}
themes/docs.css added +247
@@ -0,0 +1,247 @@
1/* orgo built-in theme: docs
2 *
3 * A documentation site: a guide read in order, with a contents block that matters, code
4 * blocks that carry as much of the meaning as the prose, and a `#+LEDE:` line under the
5 * title. Cooler and more technical than `blog`, narrower and more designed than `wiki`.
6 *
7 * Every colour is a custom property on :root, so a stylesheet of your own loaded after
8 * this one can retheme the site by redefining a handful of values. */
9
10:root {
11 --orgo-ink: #1c1f24;
12 --orgo-muted: #5b6472;
13 --orgo-rule: #dfe3e8;
14 --orgo-accent: #0b5fa5;
15 --orgo-surface: #f6f8fa;
16 --orgo-bg: #ffffff;
17 --orgo-measure: 42rem;
18 --orgo-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
19 --orgo-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
20 /* These three colour code *blocks* only; inline code follows the page. A block keeps
21 a light surface in both colour schemes because syntax.css is generated from one
22 syntect theme (`highlight.theme`, InspiredGitHub by default) and cannot follow
23 prefers-color-scheme. Pair a dark highlight theme with overrides of these three. */
24 --orgo-todo: #b02a37;
25 --orgo-done: #2c7a4b;
26 --orgo-code-bg: #f6f8fa;
27 --orgo-code-fg: #323232;
28 --orgo-code-rule: #e3e7ec;
29}
30
31@media (prefers-color-scheme: dark) {
32 :root {
33 --orgo-ink: #dee3ea;
34 --orgo-muted: #9aa4b2;
35 --orgo-rule: #2b3138;
36 --orgo-accent: #79b8ff;
37 --orgo-surface: #171a1f;
38 --orgo-todo: #f0909a;
39 --orgo-done: #74c795;
40 --orgo-bg: #0f1216;
41 }
42}
43
44* { box-sizing: border-box; }
45
46body {
47 margin: 0;
48 background: var(--orgo-bg);
49 color: var(--orgo-ink);
50 font: 16px/1.65 var(--orgo-font);
51 /* A bare URL or a long identifier must wrap, not widen the page on a phone. */
52 overflow-wrap: break-word;
53}
54
55/* Header ------------------------------------------------------------------- */
56
57body > header {
58 position: sticky;
59 top: 0;
60 z-index: 1;
61 background: var(--orgo-bg);
62 border-bottom: 1px solid var(--orgo-rule);
63 padding: 1rem 1.5rem;
64 display: flex;
65 flex-wrap: wrap;
66 gap: .5rem 1.5rem;
67 align-items: baseline;
68}
69
70.site-title {
71 font-weight: 700;
72 font-size: 1.05rem;
73 color: var(--orgo-ink);
74 text-decoration: none;
75}
76
77body > header nav { display: flex; flex-wrap: wrap; gap: 1.25rem; }
78body > header nav a {
79 color: var(--orgo-muted);
80 text-decoration: none;
81 padding-bottom: .15rem;
82 border-bottom: 2px solid transparent;
83}
84body > header nav a:hover { color: var(--orgo-ink); border-bottom-color: var(--orgo-accent); }
85
86/* Page body ---------------------------------------------------------------- */
87
88main {
89 max-width: var(--orgo-measure);
90 margin: 0 auto;
91 padding: 2.5rem 1.5rem 5rem;
92}
93
94h1 { font-size: 2rem; line-height: 1.2; letter-spacing: -0.02em; margin: 0 0 .5rem; }
95h2 { font-size: 1.35rem; letter-spacing: -0.01em; margin: 2.75rem 0 .75rem; }
96h3 { font-size: 1.1rem; margin: 2rem 0 .5rem; }
97h4, h5, h6 { font-size: 1rem; margin: 1.5rem 0 .5rem; }
98
99[class^="section-number-"] { color: var(--orgo-muted); font-weight: 400; }
100
101a { color: var(--orgo-accent); }
102
103/* `#+LEDE:` reaches the layout as page.keywords.lede; page.date as the byline. */
104p.lede { font-size: 1.1rem; color: var(--orgo-muted); margin-top: 0; }
105p.page-date { color: var(--orgo-muted); font-size: .9rem; }
106
107img, video { max-width: 100%; height: auto; }
108
109figure { margin: 1.75rem 0; }
110figcaption { color: var(--orgo-muted); font-size: .9rem; margin-top: .4rem; }
111.figure-number, .table-number { font-weight: 600; }
112
113/* A quote block reads as a note or a caution in a documentation site. */
114blockquote {
115 margin: 1.5rem 0;
116 padding: .75rem 1rem;
117 background: var(--orgo-surface);
118 border-left: 3px solid var(--orgo-accent);
119 border-radius: 0 4px 4px 0;
120 color: var(--orgo-muted);
121}
122blockquote > :first-child { margin-top: 0; }
123blockquote > :last-child { margin-bottom: 0; }
124
125hr { border: 0; border-top: 1px solid var(--orgo-rule); margin: 2.5rem 0; }
126
127.center { text-align: center; }
128.verse { font-family: var(--orgo-mono); white-space: pre-wrap; }
129
130dt { font-weight: 600; margin-top: .75rem; font-family: var(--orgo-mono); font-size: .95rem; }
131dd { margin: 0 0 0 1.5rem; }
132
133/* Code --------------------------------------------------------------------- */
134
135/* Inline code is prose furniture, so it follows the page rather than the code blocks. */
136code {
137 font: .875em/1.5 var(--orgo-mono);
138 background: var(--orgo-surface);
139 color: inherit;
140 padding: .1em .35em;
141 border-radius: 3px;
142}
143
144pre {
145 background: var(--orgo-code-bg);
146 color: var(--orgo-code-fg);
147 border: 1px solid var(--orgo-code-rule);
148 border-radius: 6px;
149 padding: .9rem 1.1rem;
150 font-size: .9rem;
151 line-height: 1.55;
152 overflow-x: auto;
153}
154
155pre code { background: none; color: inherit; padding: 0; }
156
157/* Tables ------------------------------------------------------------------- */
158
159table {
160 border-collapse: collapse;
161 width: 100%;
162 margin: 1.5rem 0;
163 display: block;
164 overflow-x: auto;
165}
166caption { text-align: left; color: var(--orgo-muted); font-size: .9rem; padding-bottom: .4rem; }
167th, td { text-align: left; padding: .5rem .75rem; border-bottom: 1px solid var(--orgo-rule); }
168th {
169 font-size: .8rem;
170 text-transform: uppercase;
171 letter-spacing: .05em;
172 color: var(--orgo-muted);
173}
174
175/* Org-specific markup ------------------------------------------------------ */
176
177.tag {
178 font: .72rem/1.6 var(--orgo-mono);
179 color: var(--orgo-muted);
180 background: var(--orgo-surface);
181 border: 1px solid var(--orgo-rule);
182 border-radius: 999px;
183 padding: .05em .6em;
184 vertical-align: middle;
185}
186
187.todo, .done { font: .72rem/1.6 var(--orgo-mono); letter-spacing: .04em; }
188.todo { color: var(--orgo-todo); }
189.done { color: var(--orgo-done); }
190.priority { color: var(--orgo-muted); font-family: var(--orgo-mono); font-size: .8em; }
191
192time.timestamp { color: var(--orgo-muted); font-family: var(--orgo-mono); font-size: .9em; }
193
194li.on, li.trans { color: var(--orgo-muted); }
195li.on { text-decoration: line-through; }
196
197.footnotes { margin-top: 3rem; font-size: .9rem; color: var(--orgo-muted); }
198.footnote-ref a { text-decoration: none; }
199
200/* Table of contents: a card, because in a guide it is navigation ----------- */
201
202nav.toc {
203 background: var(--orgo-surface);
204 border: 1px solid var(--orgo-rule);
205 border-radius: 6px;
206 padding: .75rem 1.25rem 1rem;
207 margin: 1.75rem 0 2.5rem;
208}
209nav.toc h2 {
210 font-size: .8rem;
211 text-transform: uppercase;
212 letter-spacing: .06em;
213 color: var(--orgo-muted);
214 margin: .25rem 0 .5rem;
215}
216nav.toc ul { margin: 0; padding-left: 1.1rem; }
217nav.toc li { margin: .15rem 0; }
218
219/* Listing pages: a guide's contents page, a tag index ---------------------- */
220
221ul.post-list { list-style: none; padding: 0; }
222ul.post-list > li { padding: 1rem 0; border-bottom: 1px solid var(--orgo-rule); }
223ul.post-list a { font-weight: 600; font-size: 1.05rem; }
224ul.post-list time { color: var(--orgo-muted); font-size: .9rem; }
225p.excerpt { margin: .35rem 0 .2rem; color: var(--orgo-muted); }
226span.reading-time { font-size: .85rem; color: var(--orgo-muted); }
227
228ul.tag-list { list-style: none; padding: 0; display: flex; flex-wrap: wrap; gap: .6rem 1.25rem; }
229
230nav.pagination {
231 display: flex;
232 gap: 1rem;
233 align-items: baseline;
234 margin-top: 2.5rem;
235 color: var(--orgo-muted);
236 font-size: .9rem;
237}
238
239/* Footer ------------------------------------------------------------------- */
240
241body > footer {
242 border-top: 1px solid var(--orgo-rule);
243 padding: 1.5rem;
244 color: var(--orgo-muted);
245 font-size: .9rem;
246 text-align: center;
247}
themes/plain.css added +205
@@ -0,0 +1,205 @@
1/* orgo built-in theme: plain
2 *
3 * Readable defaults and nothing else. System fonts, one measure, hairline rules — the
4 * styling a page needs to be read comfortably, with no design opinion for a site to
5 * grow out of. Start here when you intend to write your own CSS eventually.
6 *
7 * Every colour is a custom property on :root, so a stylesheet of your own loaded after
8 * this one can retheme the site by redefining a handful of values. */
9
10:root {
11 --orgo-ink: #1c1f24;
12 --orgo-muted: #5b6472;
13 --orgo-rule: #dfe3e8;
14 --orgo-accent: #0b5fa5;
15 --orgo-surface: #f6f8fa;
16 --orgo-bg: #ffffff;
17 --orgo-measure: 40rem;
18 --orgo-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
19 --orgo-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
20 /* These three colour code *blocks* only; inline code follows the page. A block keeps
21 a light surface in both colour schemes because syntax.css is generated from one
22 syntect theme (`highlight.theme`, InspiredGitHub by default) and cannot follow
23 prefers-color-scheme. Pair a dark highlight theme with overrides of these three. */
24 --orgo-todo: #b02a37;
25 --orgo-done: #2c7a4b;
26 --orgo-code-bg: #f6f8fa;
27 --orgo-code-fg: #323232;
28 --orgo-code-rule: #e3e7ec;
29}
30
31@media (prefers-color-scheme: dark) {
32 :root {
33 --orgo-ink: #dee3ea;
34 --orgo-muted: #9aa4b2;
35 --orgo-rule: #2b3138;
36 --orgo-accent: #79b8ff;
37 --orgo-surface: #171a1f;
38 --orgo-todo: #f0909a;
39 --orgo-done: #74c795;
40 --orgo-bg: #0f1216;
41 }
42}
43
44* { box-sizing: border-box; }
45
46body {
47 margin: 0;
48 background: var(--orgo-bg);
49 color: var(--orgo-ink);
50 font: 16px/1.65 var(--orgo-font);
51 /* A bare URL or a long identifier must wrap, not widen the page on a phone. */
52 overflow-wrap: break-word;
53}
54
55/* Header and navigation ---------------------------------------------------- */
56
57body > header {
58 border-bottom: 1px solid var(--orgo-rule);
59 padding: 1rem 1.5rem;
60 display: flex;
61 flex-wrap: wrap;
62 gap: .5rem 1.5rem;
63 align-items: baseline;
64}
65
66.site-title { font-weight: 700; color: var(--orgo-ink); text-decoration: none; }
67
68body > header nav { display: flex; flex-wrap: wrap; gap: 1.25rem; }
69body > header nav a { color: var(--orgo-muted); text-decoration: none; }
70body > header nav a:hover { color: var(--orgo-accent); text-decoration: underline; }
71
72/* Page body ---------------------------------------------------------------- */
73
74main {
75 max-width: var(--orgo-measure);
76 margin: 0 auto;
77 padding: 2.5rem 1.5rem 4rem;
78}
79
80h1 { font-size: 1.9rem; line-height: 1.2; margin: 0 0 1rem; }
81h2 { font-size: 1.35rem; margin: 2.25rem 0 .75rem; }
82h3 { font-size: 1.1rem; margin: 1.75rem 0 .5rem; }
83h4, h5, h6 { font-size: 1rem; margin: 1.5rem 0 .5rem; }
84
85a { color: var(--orgo-accent); }
86
87p.page-date { color: var(--orgo-muted); margin-top: -.5rem; }
88
89img, video { max-width: 100%; height: auto; }
90
91figure { margin: 1.5rem 0; }
92figcaption { color: var(--orgo-muted); font-size: .9rem; margin-top: .4rem; }
93.figure-number, .table-number { font-weight: 600; }
94
95blockquote {
96 margin: 1.5rem 0;
97 padding-left: 1rem;
98 border-left: 3px solid var(--orgo-rule);
99 color: var(--orgo-muted);
100}
101
102hr { border: 0; border-top: 1px solid var(--orgo-rule); margin: 2rem 0; }
103
104.center { text-align: center; }
105.verse { font-family: var(--orgo-mono); white-space: pre-wrap; }
106
107dt { font-weight: 600; margin-top: .75rem; }
108dd { margin: 0 0 0 1.5rem; }
109
110/* Code --------------------------------------------------------------------- */
111
112/* Inline code is prose furniture, so it follows the page rather than the code blocks. */
113code {
114 font: .875em/1.5 var(--orgo-mono);
115 background: var(--orgo-surface);
116 color: inherit;
117 padding: .1em .35em;
118 border-radius: 3px;
119}
120
121pre {
122 background: var(--orgo-code-bg);
123 color: var(--orgo-code-fg);
124 border: 1px solid var(--orgo-code-rule);
125 border-radius: 4px;
126 padding: .9rem 1rem;
127 overflow-x: auto;
128}
129
130pre code { background: none; color: inherit; padding: 0; }
131
132/* Tables ------------------------------------------------------------------- */
133
134table { border-collapse: collapse; margin: 1.25rem 0; display: block; overflow-x: auto; }
135caption { text-align: left; color: var(--orgo-muted); font-size: .9rem; padding-bottom: .4rem; }
136th, td { text-align: left; padding: .45rem .75rem; border-bottom: 1px solid var(--orgo-rule); }
137th { font-weight: 600; }
138
139/* Org-specific markup ------------------------------------------------------ */
140
141.tag {
142 font-size: .75rem;
143 font-family: var(--orgo-mono);
144 color: var(--orgo-muted);
145 border: 1px solid var(--orgo-rule);
146 border-radius: 999px;
147 padding: .05em .55em;
148 vertical-align: middle;
149}
150
151.todo, .done { font-size: .75em; font-family: var(--orgo-mono); letter-spacing: .04em; }
152.todo { color: var(--orgo-todo); }
153.done { color: var(--orgo-done); }
154.priority { color: var(--orgo-muted); font-family: var(--orgo-mono); font-size: .8em; }
155
156time.timestamp { color: var(--orgo-muted); font-family: var(--orgo-mono); font-size: .9em; }
157
158li.on, li.trans { color: var(--orgo-muted); }
159li.on { text-decoration: line-through; }
160
161.footnotes { margin-top: 3rem; font-size: .9rem; color: var(--orgo-muted); }
162.footnote-ref a { text-decoration: none; }
163
164/* Table of contents, from page.toc ----------------------------------------- */
165
166nav.toc { margin: 1.5rem 0 2.5rem; }
167nav.toc h2 {
168 font-size: .8rem;
169 text-transform: uppercase;
170 letter-spacing: .06em;
171 color: var(--orgo-muted);
172 margin: 0 0 .35rem;
173}
174nav.toc ul { margin: 0; padding-left: 1.1rem; }
175nav.toc li { margin: .1rem 0; }
176
177/* Listing pages: collections, tag indexes, pagination ---------------------- */
178
179ul.post-list { list-style: none; padding: 0; }
180ul.post-list > li { padding: .9rem 0; border-bottom: 1px solid var(--orgo-rule); }
181ul.post-list a { font-weight: 600; }
182ul.post-list time { color: var(--orgo-muted); font-size: .9rem; }
183p.excerpt { margin: .3rem 0 .2rem; color: var(--orgo-muted); }
184span.reading-time { font-size: .85rem; color: var(--orgo-muted); }
185
186ul.tag-list { list-style: none; padding: 0; display: flex; flex-wrap: wrap; gap: .5rem 1rem; }
187
188nav.pagination {
189 display: flex;
190 gap: 1rem;
191 align-items: baseline;
192 margin-top: 2rem;
193 color: var(--orgo-muted);
194 font-size: .9rem;
195}
196
197/* Footer ------------------------------------------------------------------- */
198
199body > footer {
200 border-top: 1px solid var(--orgo-rule);
201 padding: 1.5rem;
202 color: var(--orgo-muted);
203 font-size: .9rem;
204 text-align: center;
205}
themes/wiki.css added +275
@@ -0,0 +1,275 @@
1/* orgo built-in theme: wiki
2 *
3 * A dense reference site: notes, a personal knowledge base, a manual. Wide and
4 * information-first rather than roomy — a table of contents parked in the margin on a
5 * large screen, compact headings with visible structure, tables and task lists styled
6 * because a wiki is mostly made of them, and org's TODO keywords shown as badges since
7 * in a notes site they are content rather than decoration.
8 *
9 * Every colour is a custom property on :root, so a stylesheet of your own loaded after
10 * this one can retheme the site by redefining a handful of values. */
11
12:root {
13 --orgo-ink: #1f2328;
14 --orgo-muted: #656d76;
15 --orgo-rule: #d8dee4;
16 --orgo-accent: #0969da;
17 --orgo-surface: #f6f8fa;
18 --orgo-bg: #ffffff;
19 --orgo-measure: 46rem;
20 --orgo-wide: 74rem;
21 --orgo-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
22 --orgo-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
23 /* These three colour code *blocks* only; inline code follows the page. A block keeps
24 a light surface in both colour schemes because syntax.css is generated from one
25 syntect theme (`highlight.theme`, InspiredGitHub by default) and cannot follow
26 prefers-color-scheme. Pair a dark highlight theme with overrides of these three. */
27 --orgo-todo: #b02a37;
28 --orgo-done: #2c7a4b;
29 --orgo-code-bg: #f6f8fa;
30 --orgo-code-fg: #323232;
31 --orgo-code-rule: #d8dee4;
32}
33
34@media (prefers-color-scheme: dark) {
35 :root {
36 --orgo-ink: #e6edf3;
37 --orgo-muted: #8d96a0;
38 --orgo-rule: #30363d;
39 --orgo-accent: #6cb6ff;
40 --orgo-surface: #161b22;
41 --orgo-bg: #0d1117;
42 }
43}
44
45* { box-sizing: border-box; }
46
47body {
48 margin: 0;
49 background: var(--orgo-bg);
50 color: var(--orgo-ink);
51 font: 15px/1.6 var(--orgo-font);
52 /* A bare URL or a long identifier must wrap, not widen the page on a phone. */
53 overflow-wrap: break-word;
54}
55
56/* Header ------------------------------------------------------------------- */
57
58body > header {
59 background: var(--orgo-surface);
60 border-bottom: 1px solid var(--orgo-rule);
61 padding: .7rem 1.5rem;
62 display: flex;
63 flex-wrap: wrap;
64 gap: .5rem 1.5rem;
65 align-items: baseline;
66}
67
68.site-title {
69 font-weight: 700;
70 font-size: 1rem;
71 color: var(--orgo-ink);
72 text-decoration: none;
73}
74
75body > header nav { display: flex; flex-wrap: wrap; gap: 1.1rem; font-size: .9rem; }
76body > header nav a { color: var(--orgo-muted); text-decoration: none; }
77body > header nav a:hover { color: var(--orgo-accent); text-decoration: underline; }
78
79/* Page body ---------------------------------------------------------------- */
80
81main {
82 max-width: var(--orgo-measure);
83 margin: 0 auto;
84 padding: 2rem 1.5rem 4rem;
85}
86
87h1 {
88 font-size: 1.8rem;
89 line-height: 1.25;
90 margin: 0 0 1rem;
91 padding-bottom: .4rem;
92 border-bottom: 1px solid var(--orgo-rule);
93}
94h2 {
95 font-size: 1.3rem;
96 margin: 2rem 0 .6rem;
97 padding-bottom: .25rem;
98 border-bottom: 1px solid var(--orgo-rule);
99}
100h3 { font-size: 1.05rem; margin: 1.5rem 0 .4rem; }
101h4, h5, h6 { font-size: .95rem; margin: 1.25rem 0 .4rem; }
102
103/* Section numbers, when html.section_numbers is on. */
104[class^="section-number-"] { color: var(--orgo-muted); font-weight: 400; }
105
106a { color: var(--orgo-accent); text-decoration: none; }
107a:hover { text-decoration: underline; }
108
109p.page-date { color: var(--orgo-muted); font-size: .9rem; margin-top: -.5rem; }
110
111p, ul, ol { margin: .6rem 0; }
112li { margin: .15rem 0; }
113
114img, video { max-width: 100%; height: auto; }
115
116figure { margin: 1.25rem 0; }
117figcaption { color: var(--orgo-muted); font-size: .85rem; margin-top: .35rem; }
118.figure-number, .table-number { font-weight: 600; }
119
120blockquote {
121 margin: 1rem 0;
122 padding: .1rem 0 .1rem 1rem;
123 border-left: 3px solid var(--orgo-rule);
124 color: var(--orgo-muted);
125}
126
127hr { border: 0; border-top: 1px solid var(--orgo-rule); margin: 1.75rem 0; }
128
129.center { text-align: center; }
130.verse { font-family: var(--orgo-mono); white-space: pre-wrap; }
131
132dt { font-weight: 600; margin-top: .5rem; }
133dd { margin: 0 0 0 1.5rem; color: var(--orgo-muted); }
134
135/* Code --------------------------------------------------------------------- */
136
137/* Inline code is prose furniture, so it follows the page rather than the code blocks. */
138code {
139 font: .875em/1.5 var(--orgo-mono);
140 background: var(--orgo-surface);
141 color: inherit;
142 border: 1px solid var(--orgo-rule);
143 padding: .05em .3em;
144 border-radius: 3px;
145}
146
147pre {
148 background: var(--orgo-code-bg);
149 color: var(--orgo-code-fg);
150 border: 1px solid var(--orgo-code-rule);
151 border-radius: 4px;
152 padding: .8rem 1rem;
153 font-size: .875rem;
154 overflow-x: auto;
155}
156
157pre code { background: none; color: inherit; border: 0; padding: 0; }
158
159/* Tables — a wiki lives in them, so they get zebra rows and a sticky-looking head. */
160
161table {
162 border-collapse: collapse;
163 margin: 1rem 0;
164 font-size: .9rem;
165 display: block;
166 overflow-x: auto;
167}
168caption { text-align: left; color: var(--orgo-muted); font-size: .85rem; padding-bottom: .35rem; }
169th, td { text-align: left; padding: .35rem .7rem; border: 1px solid var(--orgo-rule); }
170th { background: var(--orgo-surface); font-weight: 600; }
171tbody tr:nth-child(even) td { background: var(--orgo-surface); }
172
173/* Org-specific markup ------------------------------------------------------ */
174
175.tag {
176 font: .7rem/1.6 var(--orgo-mono);
177 color: var(--orgo-muted);
178 background: var(--orgo-surface);
179 border: 1px solid var(--orgo-rule);
180 border-radius: 3px;
181 padding: .05em .45em;
182 vertical-align: middle;
183}
184
185/* TODO state is content in a notes site, not decoration. */
186.todo, .done {
187 font: .7rem/1.6 var(--orgo-mono);
188 letter-spacing: .04em;
189 border-radius: 3px;
190 padding: .05em .45em;
191 vertical-align: middle;
192 color: #ffffff;
193}
194.todo { background: var(--orgo-todo); }
195.done { background: var(--orgo-done); }
196.priority { color: var(--orgo-todo); font-family: var(--orgo-mono); font-size: .8em; }
197
198time.timestamp { color: var(--orgo-muted); font-family: var(--orgo-mono); font-size: .9em; }
199time.inactive { opacity: .75; }
200
201/* Checkbox lists: the three org states, including `[-]`, which HTML has no input for.
202 The `[X]` marker is the bullet, so the list's own is dropped. */
203li.on, li.off, li.trans { list-style: none; }
204li.on { color: var(--orgo-muted); text-decoration: line-through; }
205li.trans { color: var(--orgo-muted); }
206
207.footnotes { margin-top: 2.5rem; font-size: .9rem; color: var(--orgo-muted); }
208.footnote-ref a { text-decoration: none; }
209
210/* Table of contents: in the margin once there is a margin to put it in ------ */
211
212nav.toc {
213 background: var(--orgo-surface);
214 border: 1px solid var(--orgo-rule);
215 border-radius: 4px;
216 padding: .6rem 1rem .8rem;
217 margin: 0 0 1.5rem;
218 font-size: .875rem;
219}
220nav.toc h2 {
221 font-size: .7rem;
222 text-transform: uppercase;
223 letter-spacing: .07em;
224 color: var(--orgo-muted);
225 margin: 0 0 .35rem;
226 padding: 0;
227 border: 0;
228}
229nav.toc ul { margin: 0; padding-left: 1rem; }
230nav.toc li { margin: .1rem 0; }
231
232@media (min-width: 62rem) {
233 main { max-width: var(--orgo-wide); }
234 /* Floated rather than gridded: the layout puts the contents block wherever it likes
235 in the document, and a float lands in the margin from any of those positions. */
236 nav.toc {
237 float: right;
238 width: 16rem;
239 margin: 0 0 1.5rem 2.5rem;
240 }
241 /* Prose keeps its measure even though the page is wide; the things that actually
242 want the extra room — code, tables, figures — are let out of it. */
243 main > * { max-width: var(--orgo-measure); }
244 main > pre, main > table, main > figure, main > nav.toc { max-width: none; }
245}
246
247/* Listing pages ------------------------------------------------------------ */
248
249ul.post-list { list-style: none; padding: 0; }
250ul.post-list > li { padding: .6rem 0; border-bottom: 1px solid var(--orgo-rule); }
251ul.post-list a { font-weight: 600; }
252ul.post-list time { color: var(--orgo-muted); font-family: var(--orgo-mono); font-size: .85rem; }
253p.excerpt { margin: .25rem 0; color: var(--orgo-muted); font-size: .9rem; }
254span.reading-time { font-size: .8rem; color: var(--orgo-muted); }
255
256ul.tag-list { list-style: none; padding: 0; display: flex; flex-wrap: wrap; gap: .5rem 1.25rem; }
257
258nav.pagination {
259 display: flex;
260 gap: 1rem;
261 align-items: baseline;
262 margin-top: 1.5rem;
263 font-size: .9rem;
264 color: var(--orgo-muted);
265}
266
267/* Footer ------------------------------------------------------------------- */
268
269body > footer {
270 border-top: 1px solid var(--orgo-rule);
271 padding: 1.25rem 1.5rem;
272 color: var(--orgo-muted);
273 font-size: .85rem;
274 text-align: center;
275}