hello-world
Using emacs and org-publish to generate a static site
Introduction
This post sets up a static website, published via
org-mode. Specifically it sets up this website. This post is both
documentation and template.
If I open the .org source of this post in Emacs by browsing to it in
eww, I can run M-x org-mode to get full org navigation. The buffer
can also be saved to a local file and tangled — bootstrapping a local
version of this site.
I've made this as a way for me to practice and learn Emacs Lisp and
org-mode. So this shouldn't be read as the way to use org-mode
as it is more accurately reflects a novice's experimentation.
I wanted this file to be a self-contained. Capable of bootstrapping a
static site just from having this file open in Emacs and running
org-babel-tangle. The generated site isn't meant to be maintained or
expanded by the same mechanism, just instantiated.[ It isn't entirely self contained, as I do make use of several external programs and various coreutils. I also make several assumptions about the Emacs environment, such that packages like htmlize or the doric-themes are installed. ]
In service of making this a self-contained demo, there are things I am doing in org code blocks that I would typically move to my Emacs config or a separate Elisp library.
For example, the code to generate a CSS color palette from my preferred Emacs theme would be better off in a library.
Document Utilities
To get a this org document generating all of it's own site publication configuration I've defined some utility functions and org blocks in this section. Eventually moving these functions to a library or build script might make sense.
- org block:
stringify-html:var html Take an HTML block as a
:var, strip out any source line references and newlines, then output as a string.This is used in noweb expansions of HTML blocks into Elisp blocks.
ORG: stringify-html
#+begin_src emacs-lisp -r :exports none :results code :var html=html-to-be-stringified[] (replace-regexp-in-string "\n" " " (replace-regexp-in-string "\(ref:[a-z-]*\)" "" html)) #+end_srcExample usage:
The HTML block from the example usage. #+begin_src html -n -r :exports code <p class="foo"> This HTML will be stringified (ref:an-org-ref) </p> <aside> <p>But it can be exported to the HTML document for viewing.</p> </aside> #+end_src The HTML block from the example used as a string. #+begin_src emacs-lisp -n :exports code :noweb yes :noweb-prefix no (format "the HTML can be expanded inline: %s" <<stringify-html(html=html-to-be-stringified[])>>) #+end_srcExample HTML export:
1: <p class="foo"> 2: This HTML will be stringified 3: </p> 4: <aside> 5: <p>But it can be exported to the HTML document for viewing.</p> 6: </aside>1: (format "the HTML can be expanded inline: %s" 2: "<p class=\"foo\"> This HTML will be stringified </p> <aside> <p>But it can be exported to the HTML document for viewing.</p> </aside> " 3: )There are HTML blocks, such as the site's header HTML and the footer HTML, that I want to have in org source blocks with the language set to
html. This allows them to be exported to HTML version of this post with nice syntax highlighting, as well as letting me edit the source as HTML usingC-c '. At the same time, these chunks of HTML also need to be provided as strings to Elisp source blocks. For example, the site header is stored in the variableorg-html-preamble-format, which I configure in thedir-localsblock below.As far as I can figure, HTML blocks don't have an org-babel-execute-src-block function defined. I have created this simple org block that allows me to include HTML blocks as strings in Elisp via a noweb reference. All it does is, given the body of an HTML source block, removes any org code line references, and then formats the body as a string.
I think there is probably a cleaner way to do this, maybe with some specific combination of noweb and tangle flags, however I haven't figured it out yet.
- org block:
details_wrap:var data :var summary :var src Take an org code block and wrap it in an HTML
<details>element.ORG: details_wrap
#+begin_src bash :exports results :results output raw :var nw="no" :var data="" :var summary="expand for details" :var src="fundamental" :cache yes function echo_block() { local summary="${1}" local src="${2}" local data="${3}" echo "#+BEGIN_details" echo "#+HTML: <summary><b>${summary}</b></summary>" echo "#+BEGIN_SRC ${src} -r :noweb ${nw}" echo_data "${data}" "${src}" echo "#+END_SRC" echo "#+END_details" } function echo_data() { local data="$1" local src="$2" # when src is "org", must make sure any lines that starts with # "#" or "*" are escaped with a "," prefix for org source I also # want to escape the link syntax by inserting a zero width space # between [[. This is mostly for bash source blocks, so if I # start wanting the links to render as links when exported I can # adjust to only escape in code blocks. printf -v link_escape '[%s[' $'\u200B' if [[ "$src" = "org" ]]; then readarray -t arr < <(echo "$data") for line in "${arr[@]}"; do if [[ "${line:0:1}" = "#" || "${line:0:1}" = "*" ]]; then line=",${line}" fi if [[ "${line}" =~ '[[' ]]; then line=${line/[[/"$link_escape"} fi echo "${line}" done else echo "$data" fi } if [[ -n "${data}" ]]; then echo_block "${summary}" "${src}" "${data}" fi #+end_srcExample usage:
#+begin_src elisp -r tangle no :exports none (format "This code block should be wrapped on export") #+end_src OR #+begin_src css :tangle somefile.css :post details_wrap(summary="somefile.css", data=*this*, src="css") :exports results :results raw replace :cache yes /* some CSS */ body { color: red; } #+end_srcExample HTML export:
ELISP: example details_wrap usage
(format "This code block should be wrapped on export")OR
somefile.css
/* some CSS */ body { color: red; }Since this document contains all of the code for this site, when the code blocks are rendered to HTML they clutter the documentation with walls of code. I wanted to have longer code blocks be collapsed in a
<details>element in the exported HTML.I can actually just do this in the org file directly. For example, this is completely functional in an org document:
#+BEGIN_details #+HTML: <summary><b>Some summary text</b></summary> #+begin_src elisp -r :exports both :cache yes (format "This elisp can still be edited with %s and the block can still be executed with %s" "C-'" "C-c C-c") #+end_src #+RESULTS[7c7b983b8c69229db7e8891391a1e3cad1ca1bcc]: : This elisp can still be edited with C-' : and the block can still be executed with C-c C-c #+END_detailsWhich ends up getting rendered in the HTML as:
Some summary text
(format "This elisp can still be edited with %s and the block can still be executed with %s" "C-'" "C-c C-c")This elisp can still be edited with C-' and the block can still be executed with C-c C-c
The issue is that it looks a bit ugly in the source org document; the body of the
BEGIN_detailsblock doesn't have any syntax highlighting or even just theorg-blockface. Both verse and quote blocks can have theorg-blockface applied to them, so it might be worth looking into how those work and doing the same fordetails.For now I've opted to use post processing to wrap blocks as needed. The biggest downside of this approach is that, when using block caching, I end up with an org file that contains redundant blocks. It isn't too bad, as the blocks can be collapsed, but I would eventually like to find a better way of doing this.
- org block:
org-pub-demo|metadata Global variables used to set :var header arguments on some code blocks.
This block is evaluated when the org file is opened via a
prognset in a file local variable.ORG: org-pub-demo|metadata-export
#+begin_src emacs-lisp -r :exports none :results silent ;; languages used in this org file (for some reason setting this in the ;; .dir-locals was not working...) (setopt org-babel-load-languages '((emacs-lisp . t) (shell . t) (js . t) (css . t) (org . t))) (defun org-pub-demo|project-root () (or (getenv "XDG_PROJECTS_DIR") (concat (getenv "HOME") "/code"))) (defun org-pub-demo|project-name () (or (car (org-property-values "project_dir")) (make-temp-name "project-"))) (defun org-pub-demo|site-name () (or (car (org-property-values "site_name")) (make-temp-name "site-"))) (defconst org-pub-demo|metadata (let ((metadata (make-hash-table :size 4)) (demo-working-dir (concat (org-pub-demo|project-root) "/" (org-pub-demo|project-name)))) (message "TEMPLATE BUFFER: %s" (buffer-name)) (puthash :project-root (org-pub-demo|project-root) metadata) (puthash :project-name (org-pub-demo|project-name) metadata) (puthash :working-dir demo-working-dir metadata) (puthash :site-name (org-pub-demo|site-name) metadata) metadata) "Stores information, such as the root directory for the generated site, in a hash table. This data is used as org header arguments throughout this document (e.g. `:dir' and `:var')") #+end_srcExample usage:
# Local Variables: # eval: (progn # (org-babel-goto-named-src-block "org-pub-demo|metadata") # (org-babel-execute-src-block) # (org-fold-hide-subtree)) # End:Here I am defining some functions and variables that will be used for various org block header arguments. Initially I used some combination of org properties, org macros, or evaluated source blocks for this.
However, properties, macros and source blocks cannot be used everywhere. An example is the dir header argument, which cannot be a macro, evaluated source block, or noweb expansion. It can be an Elisp expression. In fact, an Elisp expression can be used almost everywhere. One gotcha to look out for when calling functions as part of the HTML export is that care must be taken when relying on the current buffer, because the org publishing internal make use of temporary buffers in some cases[ Calling the function
org-property-valuesin my org code blocks is an example of where I ran into issue. ].- org block:
metadata-value Useful for pulling values out of
org-pub-demo|metadatain noweb expansions.ORG: metadata-value
#+begin_src elisp :var key=":project-name" :results table (eval `(gethash ,(intern key) org-pub-demo|metadata)) #+end_srcExample usage:
#+begin_src elisp :noweb yes :base-directory "<<metadata-value(key=":working-dir")[0,0]>>/src" :publishing-directory "<<metadata-value(key=":working-dir")[0,0]>>/public" #+end_src #+begin_src elisp :exports code :noweb yes ;;; Directory Local Variables -*- no-byte-compile: t -*- ;;; For more information see (info "(emacs) Directory Variables") ((nil . ((org-publish-project-alist . (("website" :components ("public")) ("public" <<a-config-segment>>)))))) #+end_srcExample HTML export:
:base-directory "/home/catsup/code/dotfiles.foo/src" :publishing-directory "/home/catsup/code/dotfiles.foo/public";;; Directory Local Variables -*- no-byte-compile: t -*- ;;; For more information see (info "(emacs) Directory Variables") ((nil . ((org-publish-project-alist . (("website" :components ("public")) ("public" :base-directory "/home/catsup/code/dotfiles.foo/src" :publishing-directory "/home/catsup/code/dotfiles.foo/public"))))))
Setup and configuration
Make project directory
I'll be tangling various org code blocks into various files to get
things up and running. While it isn't strictly necessary to set up
these directories ahead of time — org source blocks have a :mkdirp
header argument that will do this for me — doing so allows me to
generate and embed a nice tree view that gives an overview of the
generated site's layout.
1: project_root="${XDG_PROJECTS_DIR:-$HOME/code}"
2: project_dir="dotfiles.foo"
3:
4: mkdir -p "${project_root}/${project_dir}"/{src,assets/css,assets/images,public}
5: tree --noreport --condense -L 2 -d "${project_root}/${project_dir}" | sed -e "s/$USER/a-user/"
- Line 4
- Uses
mkdirto create the directory structure for the site. - Line 5
- Generates the following tree view of the directory structure (replacing my actual
$USERwith the generic value "a-user").
/home/a-user/code/dotfiles.foo
├── assets (assets)
│ ├── css (css)
│ └── images (images)
├── pages (pages)
├── public (public)
└── src (src)
Configuring org-publish
I'll need a configuration for org-publish. The info manual has
excellent documentation, along with examples of simple and complex
configurations.
I decided to put all of the configuration into a .dirs-locals.el
file in the root of the site's project directory. For actual
day-to-day use, it seems that most people recommend putting the
configuration into a build script that can be passed to Emacs either
on the command line or with a Makefile (e.g. emacs --batch
site-build-script.el ).[ There are several gotchas associated with having the dir-locals.el file generated from the tangling the org file. The major one being that I have to close all of the buffers to reload the variables if I make a change and tangle. An additional complication with the dir-locals block is that it isn't a normal elisp block — it cannot be wrapped in a let, which is what happens if I try inserting the HTML block via a block :var. ]
Some of the configuration settings are simple boolean values (t or
nil), others are are more complex. For example of what I am calling
a "complex" value, html-preamble-format is an alist of locale
strings to a stringified chunk of HTML that will be added to every
page as the site header. For a value like that, I have stubbed out the
value with a noweb reference, (e.g. <<stringified-site-header()>>).
This has some nice advantages over just inserting the value directly:
Easier source editing
When editing the HTML, it can be in it's own source block — or even a separate file — giving me full access to Emacs' code editing facilities. Any future editing of the HTML is also simpler.
Easier to keep documentation and source in sync
The source separate source block can be inserted in the config while at the same time being exported in the HTML page - and both usages will remain in sync
There are two directories I am going to generate the site from, src/
and pages/. The org files in pages/ are things like the about
page. Everything else will go in src/. In terms of the
configuration, the main difference is that pages/ will not have a
table of contents or a sitemap.
:base-directory "/home/catsup/code/dotfiles.foo/src"
:publishing-directory "/home/catsup/code/dotfiles.foo/public"
:recursive t
:exclude "__.*draft.*\.org$"
:makeindex t
:section-numbers nil
:with-toc t
:headline-levels 2
:time-stamp-file nil
:base-directory "/home/catsup/code/dotfiles.foo/assets"
:base-extension "css\\|svg\\|txt"
:publishing-directory "/home/catsup/code/dotfiles.foo/public/assets"
:recursive t
:publishing-function org-publish-attachment
:base-directory "/home/catsup/code/dotfiles.foo/pages"
:publishing-directory "/home/catsup/code/dotfiles.foo/public"
:recursive nil
:auto-sitemap nil
:makeindex nil
:section-numbers nil
:with-toc nil
Besides these base configurations, there are things to configure
sitemap generation, theme generation, and various org-mode settings
that need to be set. More details are in the sections where the noweb
references are defined.
1: ;;; Directory Local Variables -*- no-byte-compile: t -*-
2: ;;; For more information see (info "(emacs) Directory Variables")
3:
4: ((nil . (
5: <<site-css-htmlize>>
6: (org-export-with-broken-links . mark)
7: (org-publish-use-timestamps-flag . nil)
8: <<site-html-document-structure>>
9: (org-html-scripts . <<site-js()>> )
10: (org-publish-project-alist . (("website" :components ("public" "pages" "assets"))
11: ("public"
12: <<base-public-config>>
13: <<site-html-config>>
14: <<sitemap-config>>
15: )
16: ("assets"
17: <<base-asset-config>>
18: )
19: ("pages"
20: <<base-pages-config>>
21: <<site-html-config>>))))))
Site header, footer, and structure
Right out of the box, org will generate HTML that will look great on almost every browser. It is highly backwards compatible by default.
The structure of the html is defined in org-html-divs, and
generates a page with this structure:
<html>
<body>
<div id="preamble" class="status"></div>
<div id="content" class="content"></div>
<div id="postamble" class="status"></div>
</body>
</html>
I have modified org-html-divs slightly to make use of <header>,
<article> and <footer> for the site layout.
; this is used in the .dir-locals.el file
(org-html-divs . ((preamble "header" "preamble")
(content "article" "content")
(postamble "footer" "postamble")))
When it comes to an ox-publish configuration, you can configure any
org export backends (e.g. latex, html, etc.) on a per-project level by
using the available backend setting, simply dropping the org-
prefix. For example, the org-html-postamble variable can be
configured on a per-project level by setting :html-postamble in the
project config alist.
The HTML setting's for this site are:
1: :publishing-function org-html-publish-to-html
2: :html-doctype "html5"
3: :html-html5-fancy t
4: :html-container section
5: :html-head "<link rel=\"stylesheet\" href=\"../assets/css/style.css\" type=\"text/css\"/>"
6: :html-head-include-default-style nil
7: :html-preamble t :html-preamble-format (("en" <<stringified-site-header()>> ))
8: :html-postamble t :html-postamble-format (("en" <<stringified-site-footer()>> ))
9: :html-head-include-scripts t
There are two noweb references here (line 7 and line 8). Those take the two source blocks written below, turns them into an Elisp strings, and puts those strings in place of the noweb references when this file is tangled.
I could also have the references expanded during the export, so the values would appear in the HTML as well. Throughout this document I've mostly opted to leave the references in place, to make things easier to read. As an example, the above references expanded would look like:
1: ;; […]
2: :html-preamble t :html-preamble-format (("en" "<nav class=\"site-nav\"> <ul> <li><a href=\"/\">[index]</a> <li><a href=\"/about.html\">[about]</a> </ul> </nav> <div class=\"form-group\"> <label id=\"theme-switch-label\" for=\"theme-switcher\">Theme:</label> <select id=\"theme-switcher\"> <option id=\"theme-system\" selected>System</option> <option id=\"theme-light\">Light</option> <option id=\"theme-dark\">Dark</option> </select> </div> "
3: ))
4: :html-postamble t :html-postamble-format (("en" "<nav class=\"site-nav\"> <ul> <li><a href=\"/\">[index]</a> <li><a href=\"/about.html\">[about]</a> </ul> </nav> <small> <p>© 2026 | This work is licensed under <a href=\"https://creativecommons.org/licenses/by-sa/4.0/\">CC BY-SA 4.0</a></p> <img alt=\"\" src=\"/assets/images/icons/cc-cc.svg\" loading=\"lazy\" style=\"width:var(--step-0);height:var(--step-0);margin-inline-start:.5ch;padding:0;\"> <img alt=\"\" src=\"/assets/images/icons/cc-by.svg\" loading=\"lazy\" style=\"width:var(--step-0);height:var(--step-0);margin-inline-start:.5ch;padding:0;\"> <img alt=\"\" src=\"/assets/images/icons/cc-sa.svg\" loading=\"lazy\" style=\"width:var(--step-0);height:var(--step-0);margin-inline-start:.5ch;padding:0;\"> </small> "
5: ))
6: ;; […]
Preamble and postamble
The site header and site footer are placed in the "preamble" and
"postamble" sections, respectively.
The site header is just a couple of links (the hompepage andd the about page) along with a light/dark theme switcher toggle.
HTML: site header
<nav class="site-nav">
<ul>
<li><a href="/">[index]</a>
<li><a href="/about.html">[about]</a>
</ul>
</nav>
<div class="form-group">
<label id="theme-switch-label" for="theme-switcher">Theme:</label>
<select id="theme-switcher">
<option id="theme-system" selected>System</option>
<option id="theme-light">Light</option>
<option id="theme-dark">Dark</option>
</select>
</div>
The site footer has the same site nav links as the header, but in place of the theme switcher is a copyright notice and code licensing info.
HTML: site footer
<nav class="site-nav">
<ul>
<li><a href="/">[index]</a>
<li><a href="/about.html">[about]</a>
</ul>
</nav>
<small>
<p>© 2026 | This work is licensed under <a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0</a></p>
<img alt="" src="/assets/images/icons/cc-cc.svg" loading="lazy" style="width:var(--step-0);height:var(--step-0);margin-inline-start:.5ch;padding:0;">
<img alt="" src="/assets/images/icons/cc-by.svg" loading="lazy" style="width:var(--step-0);height:var(--step-0);margin-inline-start:.5ch;padding:0;">
<img alt="" src="/assets/images/icons/cc-sa.svg" loading="lazy" style="width:var(--step-0);height:var(--step-0);margin-inline-start:.5ch;padding:0;">
</small>
The creative commons icons were downloaded into an icon directory. I
used the dir: and :mkdirp header arguments (line
2) to temporarily change the
working directory before running curl.[ I originally base64 encoded the SVGs and embedded them directly in the CSS. Later I opted to lazy load them to save some bytes if the footer is never even rendered. ]
1:
2: #+begin_src sh :dir ../assets/images/icons/ :mkdirp t :eval query :tangle no :exports none :cache yes
3: curl -s https://mirrors.creativecommons.org/presskit/icons/cc.svg > cc-cc.svg
4: curl -s https://mirrors.creativecommons.org/presskit/icons/by.svg > cc-by.svg
5: curl -s https://mirrors.creativecommons.org/presskit/icons/sa.svg > cc-sa.svg
6: #+end_src
The CSS for the article title and subtitle:
Title and subtitle for articles
.content > header:has(.title, .subtitle) {
text-align: left;
grid-column: content;
}
.title {
font: var(--f-title);
color: var(--org-src-org-document-title-fg, red);
}
.subtitle {
font: var(--f-subtitle);
color: var(--color-fg-shadow-subtle,
var(--org-src-org-level-2-fg, red));
margin-block-start: var(--prose-spacing, .8lh);
}
The CSS for the actual preamble and postamble is a bit more
involved. I am recreating the grid that is on the .content section
so that I can line things up on the same named grid lines:
The site header and footer:
The site header and footer
#preamble, #postamble {
font-size: var(--step-0);
display: grid;
grid-template-columns:
[full-width-start] minmax(var(--padding-inline), .2fr)
[breakout-start] minmax(0, var(--breakout-size))
[content-start]
min(50% - var(--padding-inline) * 2, calc(var(--content-max-width) / 2))
[content-mid]
min(50% - var(--padding-inline) * 2, calc(var(--content-max-width) / 2))
[content-end]
minmax(0, var(--breakout-size)) [breakout-end]
minmax(var(--padding-inline), 1fr) [full-width-end];
/* the nav links */
> nav {
grid-column-start: content-start;
grid-column-end: content-mid;
display: grid;
min-inline-size: fit-content;
align-content: center;
justify-content: start;
}
nav > ul {
list-style: none;
display: flex; flex-flow: row wrap;
gap: 0 3ch;
a {
display:block;
min-inline-size: fit-content;
}
}
/* The theme switcher */
.form-group {
grid-column: content-mid / content-end;
justify-self: end;
align-self: end;
display: flex;
gap: 1ch;
flex-flow: row wrap;
align-items: center;
@container(width >= 20ch) {
display: flex; flex-flow: row wrap;
align-items: center;
}
select {
display: grid; gap: 0;
color: var(--color-fg-accent);
background-color: var(--color-bg-accent);
font: var(--f-code-block);
font-size: var(--step-0);
padding: 0; margin: 0;
padding-inline: 1ch;
margin-block-end: .5lh;
text-align: center;
border: 1px solid var(--color-border);
}
}
/* copyright info */
> small {
flex: 1;
display: flex;
min-width: fit-content;
flex-wrap: wrap;
justify-content: center;
padding-inline: var(--padding-inline);
p {
font-size: var(--step--2);
text-wrap: balance;
inline-size: fit-content;
text-align: center;
}
img { box-sizing: border-box; }
}
}
#preamble {
padding-block-start: 1lh;
border-block-end: 0.1lh dashed var(--color-border);
}
#postamble {
border-block-start: 0.1lh dashed var(--color-border);
padding-block: 1lh;
align-items: center;
justify-items: center;
justify-content: center;
align-content: center;
grid-auto-flow: row;
gap: 1lh 0;
> :first-child {
grid-column-start: content-start;
grid-column-end: content-end;
}
> :last-child {
grid-column-start: breakout-start;
grid-column-end: breakout-end;
justify-items: unset;
justify-self: unset;
flex: 1;
display: flex;
min-width: fit-content;
flex-wrap: wrap;
justify-content: center;
}
}
table {
--table-bg: color-mix(var(--org-src-org-table-fg) 7%,var(--org-src-org-block-bg) 80%);
color: var(--org-src-org-table-fg);
background-color: var(--table-bg);
border-collapse:collapse;
width: 100%;
td, th, caption {
padding: .2lh;
text-align: start;
}
caption {
background-color: var(--org-src-org-block-begin-line-bg);
font-size: var(--step--2);
font-weight: bold;
text-transform: capitalize;
color: var(--org-src-org-block-begin-line-fg);
}
th {
background-color: var(--org-src-org-block-bg);
&.org-left { text-align: start; }
}
tr:nth-of-type(2n) { background-color: var(--org-src-org-block-bg); }
}
/* org mode classes */
figure {
img { border: var(--color-fg-shadow-intense) 3px double; }
figcaption { font-size: calc(var(--p-size) - 0.4rem); }
}
/* The # symbol before headline self-links */
h1, h2, h3, h4 {
> a {
--hash-color: hsl(from var(--color-fg-accent) h s l / .2);
text-decoration: none;
color: var(--color-fg-main);
&:hover {
--hash-color: var(--color-fg-accent);
color: var(--color-fg-main);
background-color: transparent;
}
&::before {
content: '# ' / "";
color: var(--hash-color);
transition: color ease-in .2s;
}
}
}
Site CSS layout
The site's CSS is broken into several files. The main file,
style.css is included in the HTML <head> with an org-publish
config setting, and all of the other files are loaded with CSS
@import statements.
style.css
@import "reset.css";
@import "layout.css";
@import "typography.css";
@import "colors.css";
@import "syntax.css";
@import "components.css";
@import "widgets.css";
The first imported file is a small reset:
reset.css
html { font-family: system-ui; scrollbar-gutter: stable; }
img, picture, svg, video { display: block; max-width: 100%; }
form, input, button, label, option, select { font: inherit; }
h1, h2, h3, h4, h5, .subtitle { text-wrap: balance; margin: 0; }
p { text-wrap: pretty; margin: 0; }
body, #content, #header, #footer { margin: 0; padding: 0; max-width: 100%; }
ul, ol { margin: 0 auto; padding: 0; }
For the actual layout, I wanted to use a CSS grid inspired by a Kevin
Powell talk for the site's layout. The ox-html library uses nesting
wrapping divs for layout rather than a flat grid. It was fairly
straightforward to get a flat grid set up within the nested divs as
long as the content remained centered.
Later, I wanted to shift everything left to make room for sidenotes. I
had to do some pretty gnarly CSS to try to get all the nested divs to
have access to the same named gridlines (e.g. full-width,
breakout).[ When the site's content was all centered the CSS didn't require any subgrid usage to get the breakout and full-width grid lines working. All that was needed was to adjust the inline-padding based on the nesting level. Something like --inline-padding:calc((var(--inline-padding)/var(--num-levels)) * var(--current-nesting-level)). ]
layout.css
body {
/* --padding-inline: 2rem; */
--padding-inline: clamp(0.85rem, -0.1748rem + 1.8123vw, 2rem);
--content-max-width: 66ch;
--breakout-max-width: 81ch;
--breakout-size: calc((var(--breakout-max-width) - var(--content-max-width)) / 2);
@media(max-width: 240px) { --padding-inline: 0; }
@media(246px < width < 280px) { --padding-inline: 1rem; }
display: grid; gap: 3lh;
min-block-size: 100vh; min-block-size: 100svh;
grid-template-rows: auto 1fr auto;
margin: 0; padding: 0;
.content {
display: grid;
grid-template-columns:
[full-width-start] minmax(var(--padding-inline), .2fr)
[breakout-start] minmax(0, var(--breakout-size))
[content-start] min(100% - var(--padding-inline) * 2, var(--content-max-width)) [content-end]
minmax(0, var(--breakout-size)) [breakout-end]
minmax(var(--padding-inline), 1fr) [full-width-end];
> * { grid-column: content; }
}
/* In every .outline-N class there is a header, an outline-N-text, and
potentially one or more .outline-N+1 blocks... Anything that we
want to be able to place on the breakout or full-width grid lines
will need to be a grid item.
First, I make all headers align to the content grid columns */
.outline-2 > h2,
.outline-3 > h3,
.outline-4 > h4 { grid-column: content; }
/* Next, I make every .outline-N-text section a full width subgrid. It */
/* needs to be full width to give the children access to all of the */
/* parent grid columns. */
.outline-2,
.outline-3,
.outline-4 {
display: grid;
grid-template-columns: subgrid;
grid-column: full-width;
> h1,h2,h3,h4 { grid-column: content; }
> [class^="outline-text-"] {
display: grid;
grid-template-columns: subgrid;
grid-column: full-width;
> * { grid-column: content; }
}
> .org-breakout-fw {
grid-column: breakout !important;
@media(width <= 70ch) { grid-column: full-width !important; }
}
}
.org-breakout-fw {
grid-column: breakout !important;
@media(width <= 70ch) { grid-column: full-width !important; }
}
.org-breakout {
grid-column: full-width !important;
@media(width <= 70ch) { grid-column: full-width !important; }
}
.org-full-width {
grid-column: full-width !important;
@media(width <= 70ch) { grid-column: full-width !important; }
}
}
/* prose spacing */
.outline-3, .outline-2, [class^="outline-text-"] {
> * + * { margin-block-start: var(--prose-spacing, .8lh); }
li > * + * {
/* border: 1px solid white; */
margin-block-start: var(--prose-spacing, .5em);
}
}
/* spacing between top-level org sections */
.content { gap: var(--section-spacing, 1rem) 0; }
/* spacing between section titles */
h2, h3 { margin-block: 2.1rem 1.4rem; line-height: 1; }
/* spacing between sub-section titles (final heading level) */
h3 { margin-block-start: 2rem; }
Typography
This is one of the hardest parts of setting up a website for me. Personally, I block all web fonts and have Firefox configured to always use my local fonts with my specific size settings, overriding whatever individual websites have set up.
I am also constantly zooming in and out on websites depending on what and how I am reading.
I've just used the calculator at utopia.fyi to set up a basic font size scale.
typography.css
:root {
--step--4: clamp(0.422rem, 0.3815rem + 0.1728vw, 0.5888rem);
--step--3: clamp(0.5064rem, 0.4506rem + 0.2378vw, 0.736rem);
--step--2: clamp(0.6076rem, 0.5318rem + 0.3235vw, 0.92rem);
--step--1: clamp(0.7292rem, 0.627rem + 0.4358vw, 1.15rem);
--step-0: clamp(0.875rem, 0.7385rem + 0.5825vw, 1.4375rem);
--step-1: clamp(1.05rem, 0.8687rem + 0.7735vw, 1.7969rem);
--step-2: clamp(1.26rem, 1.0207rem + 1.0212vw, 2.2461rem);
--step-3: clamp(1.512rem, 1.1975rem + 1.3417vw, 2.8076rem);
--step-4: clamp(1.8144rem, 1.403rem + 1.7555vw, 3.5095rem);
--step-5: clamp(2.1773rem, 1.641rem + 2.2883vw, 4.3869rem);
--step-6: clamp(2.6127rem, 1.9159rem + 2.9731vw, 5.4836rem);
--step-7: clamp(3.1353rem, 2.2326rem + 3.8517vw, 6.8545rem);
--sans-family: "IosevkaTerm Nerd Font Propo", "URW Gothic", "FreeSans",
"BlinkMacSystemFont", "avenir next", "avenir", "segoe ui", "helvetica neue", "Adwaita Sans",
"Cantarell", "Ubuntu", "roboto", "noto", "helvetica", "arial", sans-serif;
--serif-family: "URW Bookman", "FreeSerif",
"Iowan Old Style", "Apple Garamond", "Baskerville", "Times New Roman", "Droid Serif", "Times", "Source Serif Pro", serif,
"Noto Color Emoji", "Noto Emoji", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
"Unifont";
--code-family: "IosevkaTerm Nerd Font Mono", "FreeMono",
"Menlo", "Consolas", "Monaco", "Adwaita Mono", "Liberation Mono", "Lucida Console", monospace;
--font-block-quote: var(--step--2)/var(--step-6) normal serif;
--ff-headings: var(--sans-family);
--ff-content: var(--serif-family);
--ff-technical: var(--code-family);
--f-title: normal normal 900 var(--step-5)/1 var(--ff-headings);
--f-subtitle: normal normal 700 var(--step-4)/1 var(--ff-headings);
--f-content: normal normal 400 var(--step-0)/1.2 var(--ff-content);
--f-headers: normal normal 700 33rem/1.0 var(--ff-headings);
--f-list-heading: normal oblique 600 var(--step-0)/normal var(--ff-headings);
--f-toc-title: normal normal 900 var(--step-3)/1 var(--ff-headings);
--f-toc-links: normal normal 700 var(--step-1)/2cap var(--ff-headings);
--f-code-block: normal normal 400 var(--step--2)/normal var(--ff-technical);
--f-margin-notes: normal normal 400 var(--step--2)/1.2 var(--ff-content);;
/* --f-fig-captions: f; */
/* --f-table: f; */
}
html, p { font: var(--f-content); }
h1, h2, h3, h4, h5 { font: var(--f-headers); }
h1 { font-size: var(--step-4); }
h2 { font-size: var(--step-3); }
h3 { font-size: var(--step-2); }
h4 { font-size: var(--step-1); }
h5 { font-size: var(--step-0); }
/* inline code which is either =foo= or ~bar~ in org */
p code, code {
&:not(pre > &) {
overflow-wrap: anywhere;
font-family: var(--ff-technical);
color: var(--org-src-keyword-fg, red);
font-style: italic;
}
}
.content {
li :first-child:has(b) { font: var(--f-list-heading); }
}
Adding a light and dark theme
In the site header I added a theme switcher. Wiring that up to support a light and dark theme is pretty boilerplate. I set the theme selector up with three options (light, dark, and system). The "system" option is the default, with the toggle being provided to override the OS default if desired.
colors.css
:root {
<<cached-light-theme()>>
}
/* When light-dark() has a bit better browser suport I may just use
that instead:
https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/color_value/light-dark */
:root:has(option[id="theme-system"]:checked) {
color-scheme: light dark;
@media (prefers-color-scheme: dark) {
<<cached-dark-theme()>>
img { filter: brightness(80%); }
}
@media (prefers-color-scheme: light) {
<<cached-light-theme()>>
img { filter: brightness(100%); }
}
}
:root:has(option[id="theme-light"]:checked) {
color-scheme: light;
<<cached-light-theme()>>
img { filter: brightness(100%); }
}
:root:has(option[id="theme-dark"]:checked) {
color-scheme: dark;
<<cached-dark-theme()>>
img { filter: brightness(80%); }
}
The theme switcher works without JavaScript, but in order for the selection to be persisted across pages or when revisiting the site, a bit of JavaScript is needed.
The JS waits for the page to be loaded (to ensure the theme switcher
is on the page), checks localStorage for a previously selected
theme, and sets the theme switcher to that option if it was in
localStorage. It also adds a "change" event listener to the theme
switcher so that any selections update the value in localStorage:
document.addEventListener("DOMContentLoaded", (event) => {
const themeSwitcher = document.getElementById('theme-switcher');
let userTheme = localStorage.getItem('userTheme');
themeSwitcher.addEventListener("change", () => {
localStorage.setItem('userTheme', themeSwitcher.value);
});
if(userTheme) { themeSwitcher.value = userTheme; }
});
With this structure in place, now I just need to generate the CSS
variables for the light and dark themes to expand into those noweb
references in color.css.
I will be using the the doric themes to derive CSS color
variables.[ I've generated the site using the ef-themes as well. Those look pretty great. I might move to using them. ]
The doric themes provide palette variables that are a list of 24
defined colors. The palette variables for each doric theme follow a
naming convention of doric-THEME-NAME-palette. The colors have
standardized names across the palettes, only the values change. An
example from the doric-cherry palette:
| name | value | |
|---|---|---|
| cursor | #a02050 | |
| bg-main | #f7edf1 | |
| fg-main | #35292f | |
| border | #c4a7b0 | |
| bg-shadow-subtle | #e5dde0 | |
| fg-shadow-subtle | #675462 | |
| bg-neutral | #d7c9d0 | |
| etc… | #etc… |
Before making use of the doric theme colors, I need to set up the
htmlize package to generate CSS classes so that there is something
to theme.
Setting up htmlize to generate CSS classes
The htmlize package is used take the Emacs text faces from the org
document that is being exported, and generate the CSS from it.
By default it inlines all of the styles, which is good for one-off HTML documents, but not ideal for this static site as it wouldn't allow for having both a light and dark theme.
In the .dir-locals I have configured org-html-htmlize-output-type
from the default value of inline-css to css.
(org-html-htmlize-output-type . css)
This is a step in the right direction, as the exported HTML elements will now be marked up with CSS classes, which can be styled like so:
/* .org-doc generated from htmlize */
.org-doc { color: #123456; }
By default the class definitions are defined with the values of
whatever theme was loaded when htmlize was run. I would then need
to manually figure out all of the classes generated, and create dark
mode overrides for them all. This is doable, but it would be nicer
if the generated classes all pointed at CSS custom property
(a.k.a. CSS variable). Luckily, others have run into the same issue
and published some code that does just that.
Over at relint.de, the author has provided some code that will
generate a nice css file, with the colors assigned to variables,
and with both a dark and light theme. That creates a CSS file like:
/* The relint.de code generates color variables from the htmlize generated CSS classes */
:root { --org-src-doc-fg: #123456; }
/* then it fills in the CSS classes from htmlize using the variables */
.org-doc { color: var(--org-src-doc-fg); }
This is much closer to what I want. However I wouldn't mind having all of those CSS variables derived from org mode also pointing at CSS variables that map to the colors used in my Emacs theme:
/* Color variables are dumped from the emacs theme, stored in
colors.css */
:root { --color-fg-shadow-subtle: #123456; }
/* a modified version of the relint.de code adds a mapping to the
color's name, with a fallback of red to make missing values easy to
spot. */
:root { --org-src-doc-fg: var(--color-fg-shadow-subtle, red); }
/* this mapping remains the same from the relint.de/htmlize code */
.org-doc { color: var(--org-src-doc-fg); }
Having the org faces decoupled from the color definitions is just a preference I have.
Using the code from relint.de as a starting point, I have modified it
to work with the doric themes specifically, though the general concept
can be applied to any theme that provides named palette variables.[ Now I see why the htmlize implementation is the way it is. Individual themes may implement their palettes in different ways, and htmlize needs to work with everything. ]
First, I'll need a few helper functions.
- function:
org-pub-demo|css-downscale-hexhex Turn a 9 or 12 digit color HEX value into a 6 digit hex value.
ELISP: ~org-pub-demo|css-downscale-hex~
(defun org-pub-demo|css-downscale-hex (hex) "Turn a 9 or 12 digit color HEX value into a 6 digit hex value." (apply 'color-rgb-to-hex `(,@(color-name-to-rgb hex) 2)))Example usage:
(org-pub-demo|css-downscale-hex "#1afa65ec53ef") ;; ⇒ "#1a6553" (org-pub-demo|css-downscale-hex "red") ;; ⇒ "#ff0000"Sometimes a face doesn't map directly to a color name defined in the doric palette variable. An example is the
'diff-refine-addedface, used when rendering code diffs. I didn't read the code to verify, but I suspect these colors are dynamically generated from the palette. i.e. the'diff-refine-addedface might be generated by passing thebg-greencolor to a function such ascolor-lighten-nameto generate its value. There is the added complication that Emacs can support up to 4 digits for each component of an RGB hex color name, and it seems like these derived colors are often in this format.I still want to be able to use these faces on the site, and I do not want to define them manually.
org-pub-demo|css-downscale-hextakes one of these long hex numbers and scales it down.[ I know CSS now supports many different color notations, but I am not well versed in colors and couldn't figure out how to translate a Emacs oklab color value to CSS notation. ]- function:
org-pub-demo|hex-to-theme-csshexcode palette face bg-or-fg Checks PALETTE for HEX and returns a css variable representing that HEX. Falls back to FACE if HEX is not in PALETTE.
ELISP: org-pub-demo|hex-to-theme-css
(defun org-pub-demo|hex-to-theme-css (hex palette face bg-or-fg) "Takes the color `HEX' returned by htmlize and converts it to a css variable that references the corresponding doric name as defined in `PALETTE'. If the `HEX' doesn't map cleanly to a specific doric value (e.g. diff faces use a #RRRRGGGGBBBB format), then just assign variable with the face name in the format --color-FACE-BG|FG." (let ((color-name (car (rassoc (list hex) palette)))) (if color-name (format "var(--color-%s, red)" color-name) (format "var(--color-%s-%s, blue)" face bg-or-fg))))Example usage:
(org-pub-demo|hex-to-theme-css "#a694b1" doric-plum-palette "hl-line" "bg") ;; ⇒ "var(--color-fg-shadow-subtle, red)" (org-pub-demo|hex-to-theme-css "#a694b1" ef-fig-palette "foo" "bg") ;; ⇒ "var(--color-foo-bg, blue)"The
htlmizeprocess is going to be run on every org file that is exported to HTML, assigning the CSS classes to all of the HTML elements. To do this, the library uses thehtmlize-make-face-mapto build a hash table of face names values.The hash table can be used to get a face's foreground and background CSS values. The function will give me back the actual hex values, so I added the
org-pub-demo|hex-to-theme-cssfunction that takes a hex value color and returns a CSS string mapping that back to the actual color name. If there is no direct mapping to a doric theme color, I create a variable using the face name, with a fallback color of blue. Later on I will use the fact that these faces have a fallback of blue to create their definitions.- function:
org-pub-demo|faces-to-exportbuffer-name Generates a list of face names to export for the site from those present in BUFFER-NAME.
ELISP: org-pub-demo|faces-to-export
(defun org-pub-demo|faces-to-export (buffer-name) "Generate a list of face names to export for the site from those present in BUFFER-NAME (plus a few extras). Rather than using a fixed set of face names to generate the syntax css, I use the template post. I grab all of the faces from that buffer." (with-current-buffer buffer-name `(default region ,@(htmlize-faces-in-buffer) ,@(seq-filter (lambda (face) (string-match-p "^diff-.*" (symbol-name face))) (face-list)))))Example usage:
(org-pub-demo|faces-to-export (buffer-name)) ;; ⇒ (default region show-paren-match hl-line diff-refine-added diff-refine-removed diff-refine-changed diff-error diff-nonexistent diff-context diff-function diff-indicator-changed diff-indicator-added ...)To build the
syntax.cssfile, I will need a list of faces I want to export. Eventually this can be a static list of values, but for now I am using the org buffer of this post as the source of all of the faces, plus adding in all the diff related faces as I will likely need them.- function:
org-pub-demo|css-class-def-specsfstruct Generate a string spec from an htmlize FSTRUCT for use in generating CSS class definitions.
ELISP: org-pub-demo|css-class-def-specs
(defun org-pub-demo|css-class-def-specs (fstruct) (let ((css-name (htmlize-fstruct-css-name fstruct))) (when (htmlize-fstruct-foreground fstruct) (setf (htmlize-fstruct-foreground fstruct) (format "var(--org-src-%s-fg)" css-name))) (when (htmlize-fstruct-background fstruct) (setf (htmlize-fstruct-background fstruct) (format "var(--org-src-%s-bg)" css-name))) (htmlize-css-specs fstruct)))Example usage:
See
org-pub-demo|css-insert-class-definitionsThere is an internal function in the
htmlizelibrary,htmlize-css-specs, that takes anhtmlize-fstruct(the library's data object for faces being exported) and converts those attributes to strings of CSS attributes.I was able to wrap that function in my own,
org-pub-demo|css-class-def-specs, that swaps out the background and foreground hex value with a CSS variable.- function:
org-pub-demo|css-org-color-var-specsfstruct clean-face-name &optional theme Generate a string spec from an htmlize FSTRUCT for use in generating CSS org color variables.
ELISP: org-pub-demo|css-org-color-var-specs
(defun org-pub-demo|css-org-color-var-specs (fstruct clean-face-name &optional theme) ;; current enabled theme used by default (unless theme (setq theme (car custom-enabled-themes))) (let ((result) (palette (symbol-value (intern (format "%s-palette" theme)))) (css-name (htmlize-fstruct-css-name fstruct))) (when (htmlize-fstruct-foreground fstruct) (let* ((hex (htmlize-fstruct-foreground fstruct)) (doric (org-pub-demo|hex-to-theme-css hex palette clean-face-name "fg"))) (push (format "--org-src-%s-fg: %s;" css-name doric) result))) (when (htmlize-fstruct-background fstruct) (let* ((hex (htmlize-fstruct-background fstruct)) (doric (org-pub-demo|hex-to-theme-css hex palette clean-face-name "bg"))) (push (format "--org-src-%s-bg: %s;" css-name doric) result))) (nreverse result)))Example usage:
See [BROKEN LINK: syntax-file-helpers-6]
Similair idea to [BROKEN LINK: syntax-file-helpers-4], except I am only concerned with the foreground and background colors here. Since I only need the colors, there is no need to call
htmlize-css-specs. If a THEME is provided, the colors from FSTRUCT will be looked up in THEME, otherwise thecarofcustom-enabled-themesis used instead.This function makes use of the [BROKEN LINK: syntax-file-helpers-2] function from earlier.
- function:
org-pub-demo|output-css-class-definitionsbuffer-faces face-map Iterate through a list of BUFFER-FACES, using an htmlize FACE-MAP to generate and insert CSS for each face in BUFFER-FACES.
ELISP: org-pub-demo|css-insert-class-definitions
(defun org-pub-demo|css-insert-class-definitions (buffer-faces face-map) "Iterate through a list of BUFFER-FACES, using an htmlize FACE-MAP to generate and insert CSS for each face in BUFFER-FACES." (let ((sorted-faces (cl-sort (cl-copy-list buffer-faces) #'string-lessp :key (lambda (f) (htmlize-fstruct-css-name (gethash f face-map))))) (safe-face-name (lambda (f) ; a face (let ((s (prin1-to-string f))) (while (string-match "--" s) (setq s (replace-match "-" t t s))) (while (string-match "\\*/" s) (setq s (replace-match "XX" t t s))) s)))) ;; the color variable definitions, which reference the theme ;; variable stored in color.css (insert ":root {\n ") (dolist (face sorted-faces) (let* ((fstruct (gethash face face-map)) (cleaned-up-face-name (funcall safe-face-name face)) (specs (org-pub-demo|css-org-color-var-specs fstruct cleaned-up-face-name))) ;; skip custom values as they are specific to each post. If a ;; post has custom faces, they will be inserted in the HTML head ;; during export, they are not needed in the syntax.css file. (unless (or (string-match-p "^custom-.*" (htmlize-fstruct-css-name fstruct)) (string-match-p "^custom$" (htmlize-fstruct-css-name fstruct))) (unless (null specs) (insert (mapconcat #'identity specs "\n ")) (insert "\n "))))) (insert "}\n\n") (insert "/* default for org code blocks */\n") (insert ".org-src-container {\n") (insert " color: var(--org-src-org-block-fg);\n") (insert " background-color: var(--org-src-org-block-bg);\n") (insert "}\n\n") ;; the CSS class definitions, which reference the above variables (dolist (face sorted-faces) (let* ((fstruct (gethash face face-map)) (cleaned-up-face-name (funcall safe-face-name face)) (specs (org-pub-demo|css-class-def-specs fstruct))) ;; skip custom values as they are specific to each post. If a ;; post has custom faces, they will be inserted in the HTML head ;; during export, they are not needed in the syntax.css file. (unless (or (string-match-p "^custom-.*" (htmlize-fstruct-css-name fstruct)) (string-match-p "^custom$" (htmlize-fstruct-css-name fstruct))) (insert ".org-" (htmlize-fstruct-css-name fstruct)) (if (null specs) (insert " {}\n") (insert " {\n /* " cleaned-up-face-name " */\n " (mapconcat #'identity specs "\n ")) (insert "\n}\n")))))))Example usage:
By copying and slightly modifying the
css-insert-headfunction from thehtmlizelibrary, I can have a function that writes what will be the contents ofsyntax.cssto a buffer.
Generating syntax.css
Finally, I've defined an org block that I can evaluate manually by
putting my cursor on it and hitting C-c C-c.
It makes sure all of the above helper functions are loaded, and then
outputs the CSS to a cached results buffer. I use the cached results
for exporting and tangling so that the code doesn't need to run again
unless I want to regenerate the syntax.css file — for example if I
want to add more faces to org-pub-demo|faces-to-export.
#+begin_src elisp :var template-buffer="20260307T233612--hello-world__blog_cs_public.org" :exports none :results value replace :cache yes :tangle no
(dotimes (i 6)
(message "syntax-file-helpers-%d" (+ i 1))
(org-babel-goto-named-src-block (format "syntax-file-helpers-%d" (+ i 1)))
(org-babel-execute-src-block))
(defun generate-syntax-file (doric-theme &optional filename dir)
"runs the above code to generate a mapping of org css variable names to values"
(let ((filename (or filename "syntax.css"))
(directory (or dir (concat (gethash :working-dir org-pub-demo|metadata) "/assets/css/")))
(current-themes custom-enabled-themes)
(result))
;; save current theme state before modifying it
(mapcar #'disable-theme custom-enabled-themes)
;; This is needed so that I can map the faces current colors to
;; the doric color names.
(load-theme doric-theme t)
(setq result (with-temp-buffer
(let* ((faces (org-pub-demo|faces-to-export template-buffer))
(htmlize-face-map (htmlize-make-face-map faces)))
(org-pub-demo|css-insert-class-definitions faces htmlize-face-map))
(buffer-string)))
;; reset theme state
(disable-theme doric-theme)
(mapcar (lambda (x) (load-theme x)) (nreverse current-themes))
result))
(generate-syntax-file 'doric-plum)
#+end_src
syntax.css
:root {
--org-src-builtin-fg: var(--color-fg-shadow-subtle, red);
--org-src-comment-fg: var(--color-fg-accent, red);
--org-src-comment-delimiter-fg: var(--color-fg-accent, red);
--org-src-constant-fg: var(--color-fg-main, red);
--org-src-css-property-fg: var(--color-fg-shadow-intense, red);
--org-src-css-selector-fg: var(--color-fg-shadow-subtle, red);
--org-src-default-fg: var(--color-fg-main, red);
--org-src-default-bg: var(--color-bg-main, red);
--org-src-diff-added-fg: var(--color-fg-neutral, red);
--org-src-diff-added-bg: var(--color-diff-added-bg, blue);
--org-src-diff-changed-fg: var(--color-fg-neutral, red);
--org-src-diff-changed-bg: var(--color-diff-changed-bg, blue);
--org-src-diff-changed-unspecified-fg: var(--color-fg-neutral, red);
--org-src-diff-changed-unspecified-bg: var(--color-diff-changed-unspecified-bg, blue);
--org-src-diff-context-fg: var(--color-fg-shadow-subtle, red);
--org-src-diff-error-fg: var(--color-fg-shadow-subtle, red);
--org-src-diff-file-header-fg: var(--color-fg-shadow-subtle, red);
--org-src-diff-function-bg: var(--color-bg-shadow-subtle, red);
--org-src-diff-hunk-header-bg: var(--color-bg-shadow-subtle, red);
--org-src-diff-index-fg: var(--color-fg-shadow-subtle, red);
--org-src-diff-indicator-added-fg: var(--color-fg-neutral, red);
--org-src-diff-indicator-added-bg: var(--color-diff-indicator-added-bg, blue);
--org-src-diff-indicator-changed-fg: var(--color-fg-neutral, red);
--org-src-diff-indicator-changed-bg: var(--color-diff-indicator-changed-bg, blue);
--org-src-diff-indicator-removed-fg: var(--color-fg-neutral, red);
--org-src-diff-indicator-removed-bg: var(--color-diff-indicator-removed-bg, blue);
--org-src-diff-nonexistent-fg: var(--color-fg-shadow-intense, red);
--org-src-diff-refine-added-bg: var(--color-diff-refine-added-bg, blue);
--org-src-diff-refine-changed-bg: var(--color-diff-refine-changed-bg, blue);
--org-src-diff-refine-removed-bg: var(--color-diff-refine-removed-bg, blue);
--org-src-diff-removed-fg: var(--color-fg-neutral, red);
--org-src-diff-removed-bg: var(--color-diff-removed-bg, blue);
--org-src-doc-fg: var(--color-fg-shadow-subtle, red);
--org-src-function-name-fg: var(--color-fg-shadow-intense, red);
--org-src-hl-line-bg: var(--color-bg-accent, red);
--org-src-keyword-fg: var(--color-fg-shadow-intense, red);
--org-src-org-block-bg: var(--color-bg-shadow-subtle, red);
--org-src-org-block-begin-line-fg: var(--color-fg-neutral, red);
--org-src-org-block-begin-line-bg: var(--color-bg-neutral, red);
--org-src-org-block-end-line-fg: var(--color-fg-neutral, red);
--org-src-org-block-end-line-bg: var(--color-bg-neutral, red);
--org-src-org-code-fg: var(--color-fg-shadow-intense, red);
--org-src-org-document-info-fg: var(--color-fg-main, red);
--org-src-org-document-info-keyword-fg: var(--color-fg-shadow-subtle, red);
--org-src-org-document-title-fg: var(--color-fg-accent, red);
--org-src-org-drawer-fg: var(--color-fg-shadow-subtle, red);
--org-src-org-footnote-fg: var(--color-fg-accent, red);
--org-src-org-hide-fg: var(--color-bg-main, red);
--org-src-org-inline-src-block-fg: var(--color-fg-shadow-subtle, red);
--org-src-org-level-1-fg: var(--color-fg-main, red);
--org-src-org-level-2-fg: var(--color-fg-main, red);
--org-src-org-level-3-fg: var(--color-fg-main, red);
--org-src-org-link-fg: var(--color-fg-accent, red);
--org-src-org-meta-line-fg: var(--color-fg-shadow-subtle, red);
--org-src-org-property-value-fg: var(--color-fg-accent, red);
--org-src-org-quote-bg: var(--color-bg-shadow-subtle, red);
--org-src-org-special-keyword-fg: var(--color-fg-shadow-subtle, red);
--org-src-org-table-fg: var(--color-fg-accent, red);
--org-src-org-table-row-fg: var(--color-fg-accent, red);
--org-src-org-tag-fg: var(--color-fg-shadow-subtle, red);
--org-src-org-target-fg: var(--color-fg-main, red);
--org-src-org-todo-fg: var(--color-fg-red, red);
--org-src-org-verbatim-fg: var(--color-fg-shadow-subtle, red);
--org-src-regexp-grouping-backslash-fg: var(--color-fg-shadow-intense, red);
--org-src-regexp-grouping-construct-fg: var(--color-fg-shadow-intense, red);
--org-src-region-fg: var(--color-fg-shadow-intense, red);
--org-src-region-bg: var(--color-bg-shadow-intense, red);
--org-src-shadow-fg: var(--color-fg-shadow-subtle, red);
--org-src-string-fg: var(--color-fg-shadow-subtle, red);
--org-src-type-fg: var(--color-fg-shadow-subtle, red);
--org-src-warning-fg: var(--color-fg-yellow, red);
}
/* default for org code blocks */
.org-src-container {
color: var(--org-src-org-block-fg);
background-color: var(--org-src-org-block-bg);
}
.org-bold {
/* bold */
font-weight: bold;
}
.org-builtin {
/* font-lock-builtin-face */
color: var(--org-src-builtin-fg);
font-weight: bold;
font-style: italic;
}
.org-comment {
/* font-lock-comment-face */
color: var(--org-src-comment-fg);
font-style: italic;
}
.org-comment-delimiter {
/* font-lock-comment-delimiter-face */
color: var(--org-src-comment-delimiter-fg);
font-style: italic;
}
.org-constant {
/* font-lock-constant-face */
color: var(--org-src-constant-fg);
}
.org-css-property {
/* css-property */
color: var(--org-src-css-property-fg);
font-weight: bold;
}
.org-css-selector {
/* css-selector */
color: var(--org-src-css-selector-fg);
font-weight: bold;
font-style: italic;
}
.org-default {
/* default */
color: var(--org-src-default-fg);
background-color: var(--org-src-default-bg);
}
.org-diff-added {
/* diff-added */
color: var(--org-src-diff-added-fg);
background-color: var(--org-src-diff-added-bg);
}
.org-diff-changed {
/* diff-changed */
color: var(--org-src-diff-changed-fg);
background-color: var(--org-src-diff-changed-bg);
}
.org-diff-changed-unspecified {
/* diff-changed-unspecified */
color: var(--org-src-diff-changed-unspecified-fg);
background-color: var(--org-src-diff-changed-unspecified-bg);
}
.org-diff-context {
/* diff-context */
color: var(--org-src-diff-context-fg);
}
.org-diff-error {
/* diff-error */
color: var(--org-src-diff-error-fg);
font-weight: bold;
font-style: italic;
}
.org-diff-file-header {
/* diff-file-header */
color: var(--org-src-diff-file-header-fg);
font-weight: bold;
font-style: italic;
}
.org-diff-function {
/* diff-function */
background-color: var(--org-src-diff-function-bg);
}
.org-diff-header {}
.org-diff-hunk-header {
/* diff-hunk-header */
background-color: var(--org-src-diff-hunk-header-bg);
font-weight: bold;
}
.org-diff-index {
/* diff-index */
color: var(--org-src-diff-index-fg);
font-style: italic;
}
.org-diff-indicator-added {
/* diff-indicator-added */
color: var(--org-src-diff-indicator-added-fg);
background-color: var(--org-src-diff-indicator-added-bg);
}
.org-diff-indicator-changed {
/* diff-indicator-changed */
color: var(--org-src-diff-indicator-changed-fg);
background-color: var(--org-src-diff-indicator-changed-bg);
}
.org-diff-indicator-removed {
/* diff-indicator-removed */
color: var(--org-src-diff-indicator-removed-fg);
background-color: var(--org-src-diff-indicator-removed-bg);
}
.org-diff-nonexistent {
/* diff-nonexistent */
color: var(--org-src-diff-nonexistent-fg);
font-weight: bold;
}
.org-diff-refine-added {
/* diff-refine-added */
background-color: var(--org-src-diff-refine-added-bg);
font-weight: bold;
}
.org-diff-refine-changed {
/* diff-refine-changed */
background-color: var(--org-src-diff-refine-changed-bg);
font-weight: bold;
}
.org-diff-refine-removed {
/* diff-refine-removed */
background-color: var(--org-src-diff-refine-removed-bg);
font-weight: bold;
}
.org-diff-removed {
/* diff-removed */
color: var(--org-src-diff-removed-fg);
background-color: var(--org-src-diff-removed-bg);
}
.org-doc {
/* font-lock-doc-face */
color: var(--org-src-doc-fg);
font-style: italic;
}
.org-function-name {
/* font-lock-function-name-face */
color: var(--org-src-function-name-fg);
}
.org-hl-line {
/* hl-line */
background-color: var(--org-src-hl-line-bg);
}
.org-italic {
/* italic */
font-style: italic;
}
.org-keyword {
/* font-lock-keyword-face */
color: var(--org-src-keyword-fg);
font-weight: bold;
}
.org-org-block {
/* org-block */
background-color: var(--org-src-org-block-bg);
}
.org-org-block-begin-line {
/* org-block-begin-line */
color: var(--org-src-org-block-begin-line-fg);
background-color: var(--org-src-org-block-begin-line-bg);
}
.org-org-block-end-line {
/* org-block-end-line */
color: var(--org-src-org-block-end-line-fg);
background-color: var(--org-src-org-block-end-line-bg);
}
.org-org-code {
/* org-code */
color: var(--org-src-org-code-fg);
font-style: italic;
}
.org-org-document-info {
/* org-document-info */
color: var(--org-src-org-document-info-fg);
}
.org-org-document-info-keyword {
/* org-document-info-keyword */
color: var(--org-src-org-document-info-keyword-fg);
}
.org-org-document-title {
/* org-document-title */
color: var(--org-src-org-document-title-fg);
font-weight: bold;
}
.org-org-drawer {
/* org-drawer */
color: var(--org-src-org-drawer-fg);
}
.org-org-footnote {
/* org-footnote */
color: var(--org-src-org-footnote-fg);
text-decoration: underline;
}
.org-org-formula {}
.org-org-hide {
/* org-hide */
color: var(--org-src-org-hide-fg);
}
.org-org-inline-src-block {
/* org-inline-src-block */
color: var(--org-src-org-inline-src-block-fg);
font-style: italic;
}
.org-org-level-1 {
/* org-level-1 */
color: var(--org-src-org-level-1-fg);
font-weight: bold;
}
.org-org-level-2 {
/* org-level-2 */
color: var(--org-src-org-level-2-fg);
font-weight: bold;
}
.org-org-level-3 {
/* org-level-3 */
color: var(--org-src-org-level-3-fg);
font-weight: bold;
}
.org-org-link {
/* org-link */
color: var(--org-src-org-link-fg);
text-decoration: underline;
}
.org-org-list-dt {
/* org-list-dt */
font-weight: bold;
}
.org-org-meta-line {
/* org-meta-line */
color: var(--org-src-org-meta-line-fg);
}
.org-org-property-value {
/* org-property-value */
color: var(--org-src-org-property-value-fg);
}
.org-org-quote {
/* org-quote */
background-color: var(--org-src-org-quote-bg);
font-style: italic;
}
.org-org-special-keyword {
/* org-special-keyword */
color: var(--org-src-org-special-keyword-fg);
}
.org-org-table {
/* org-table */
color: var(--org-src-org-table-fg);
}
.org-org-table-row {
/* org-table-row */
color: var(--org-src-org-table-row-fg);
}
.org-org-tag {
/* org-tag */
color: var(--org-src-org-tag-fg);
}
.org-org-target {
/* org-target */
color: var(--org-src-org-target-fg);
font-weight: bold;
}
.org-org-todo {
/* org-todo */
color: var(--org-src-org-todo-fg);
font-weight: bold;
}
.org-org-verbatim {
/* org-verbatim */
color: var(--org-src-org-verbatim-fg);
font-style: italic;
}
.org-regexp-grouping-backslash {
/* font-lock-regexp-grouping-backslash */
color: var(--org-src-regexp-grouping-backslash-fg);
font-weight: bold;
}
.org-regexp-grouping-construct {
/* font-lock-regexp-grouping-construct */
color: var(--org-src-regexp-grouping-construct-fg);
font-weight: bold;
}
.org-region {
/* region */
color: var(--org-src-region-fg);
background-color: var(--org-src-region-bg);
}
.org-shadow {
/* shadow */
color: var(--org-src-shadow-fg);
}
.org-string {
/* font-lock-string-face */
color: var(--org-src-string-fg);
}
.org-type {
/* font-lock-type-face */
color: var(--org-src-type-fg);
font-weight: bold;
font-style: italic;
}
.org-variable-name {
/* font-lock-variable-name-face */
font-style: italic;
}
.org-warning {
/* font-lock-warning-face */
color: var(--org-src-warning-fg);
font-weight: bold;
}
Generating colors.css from the doric themes
Now that syntax.css is all sorted, all that remains is to generate
the CSS color variables that syntax.css is referencing. These will
expand in the light theme and dark theme noweb references in
colors.css.
The reason I needed to generate syntax.css first was to detect any
dynamic face colors in use (the variables with a "blue" fallback value
in syntax.css).
I am going to loop through the the colors defined in the palette, generating a list of CSS strings:
(defun org-pub-demo|doric-to-css (palette)
"Take a doric theme PALETTE and return css color variables as a list of strings."
(let ((max-length 0)
(css-colors '())
(calc-gap
(lambda (k v m)
"calculate the gap between the css var name (K) and the value (V) using
the max width (M) found above." (number-to-string (- m (+ (length k) (length v)))))))
(dolist (color-pair palette max-length)
(let* ((css-name (format "--color-%s:" (symbol-name (car color-pair))))
(css-value (if (stringp (cadr color-pair)) (cadr color-pair) (format "var(--color-%s, pink)" (cadr color-pair))))
(length (+ (length css-name) (length css-value) 1)))
(push `(,css-name . ,css-value) css-colors)
(when (> length max-length) (setq max-length length))))
(mapcar
(lambda (elt)
(let ((css-name (car elt))
(css-value (cdr elt)))
(format (concat "%s%-" (funcall calc-gap css-name css-value max-length) "s%s;") css-name " " css-value))) css-colors)))
(org-pub-demo|doric-to-css (symbol-value theme))
ELISP: doric-to-css
(defun org-pub-demo|doric-to-css (palette)
"Take a doric theme PALETTE and return css color variables as a list of strings."
(let ((max-length 0)
(css-colors '())
(calc-gap
(lambda (k v m)
"calculate the gap between the css var name (K) and the value (V) using
the max width (M) found above." (number-to-string (- m (+ (length k) (length v)))))))
(dolist (color-pair palette max-length)
(let* ((css-name (format "--color-%s:" (symbol-name (car color-pair))))
(css-value (if (stringp (cadr color-pair)) (cadr color-pair) (format "var(--color-%s, pink)" (cadr color-pair))))
(length (+ (length css-name) (length css-value) 1)))
(push `(,css-name . ,css-value) css-colors)
(when (> length max-length) (setq max-length length))))
(mapcar
(lambda (elt)
(let ((css-name (car elt))
(css-value (cdr elt)))
(format (concat "%s%-" (funcall calc-gap css-name css-value max-length) "s%s;") css-name " " css-value))) css-colors)))
(org-pub-demo|doric-to-css (symbol-value theme))
Then, because I have a personal preference for this chunk of CSS to be
in two columns, I have another org block that just pipes the first to
the linux column command:
Then I will loop over the syntax.css file, finding all of the face
names that have "blue" fallback values. This indicates that the face
doesn't map directly to a named color in the palette and will need to
have a face-specific variable defined.
ELISP: org-pub-demo|derived-theme-values
(defun blue-faces-list (&optional syntax-file)
"Determine which face in the `SYNTAX-FILE' (syntax.css by default)
generated by `generate-syntax-file' need further processing.
When the syntax file was generated, every face in `exported-faces-list'
was mapped to a CSS variable which in turn maps to a named color in the
themes palette. However, not every face is themed with a named
color. Some faces, such `diff-refine-added' are generated dynamically
from a named palette color.
In order to support those with the site's light/dark theme switcher,
their generated values need to be stored in a CSS variable with a known
name. During syntax file generation, these org face were mapped to a CSS
variable of the format \"var(--color-FACE-NAME-fg|bg, blue)\".
This function finds all of the FACE-NAMEs and returns a list of them, to
be used in the light and dark palette generation."
(let ((faces '())
(file (or syntax-file "~/code/kab/assets/css/syntax.css"))
(re "var(--color-\\(.*\\)-.g, blue);"))
;; collect faces that need definition from syntax file
(with-temp-buffer
(insert-file-contents file)
(goto-char (point-min))
(while (re-search-forward re nil t)
(push (intern (match-string-no-properties 1)) faces)))
(seq-uniq faces)))
(defun org-pub-demo|derived-theme-values (faces theme)
"Take a doric theme PALETTE and return css color variables as a list of strings."
(message "cAllign")
(when faces
(let ((max-length 0)
(css-colors '())
(calc-gap (lambda (k v m)
"calculate the gap between the css var name (K) and the value (V) using
the max width (M) found above." (number-to-string (- m (+ (length k) (length v))))))
(current-themes custom-enabled-themes))
;; disable all current themes and load the theme that the faces
;; will have their CSS generated from
(mapcar #'disable-theme custom-enabled-themes)
(load-theme theme t)
(let ((face-map (htmlize-make-face-map faces)))
;; go through the faces
(dolist (face faces)
(let ((fstruct (gethash face face-map)))
;; foreground
(when (htmlize-fstruct-foreground fstruct)
(let* ((hex (org-pub-demo|css-downscale-hex (htmlize-fstruct-foreground fstruct)))
(css-name (format "--color-%s-fg:" face))
(css-value hex)
(length (+ (length css-name) (length css-value) 1)))
(push `(,css-name . ,css-value) css-colors)
(when (> length max-length) (setq max-length length))))
;; background
(when (htmlize-fstruct-background fstruct)
(let* ((hex (org-pub-demo|css-downscale-hex (htmlize-fstruct-background fstruct)))
(css-name (format "--color-%s-bg:" face))
(css-value hex)
(length (+ (length css-name) (length css-value) 1)))
(push `(,css-name . ,css-value) css-colors)
(when (> length max-length) (setq max-length length)))))))
;; reset theme state
(disable-theme theme)
(mapcar (lambda (x) (load-theme x)) (nreverse current-themes))
(mapcar
(lambda (elt)
(let ((css-name (car elt))
(css-value (cdr elt)))
(format (concat "%s%-" (funcall calc-gap css-name css-value max-length) "s%s;") css-name " " css-value))) css-colors))))
(org-pub-demo|derived-theme-values (blue-faces-list) theme)
Those two can be tied together to tangle the actual CSS file, and if in
the future I want to change the theme, I only need to change the theme
variable I am passing to these blocks [BROKEN LINK: theme-var]:
#+begin_src sh :cache no :exports none :results output code :cache yes
printf '%s' "${palette}" | column -xc 85
printf '%s' "${unnamed}" | column -xc 96
#+end_src
#+begin_src sh :cache no :exports none :results output code :cache yes
printf '%s' "${palette}" | column -xc 85
printf '%s' "${unnamed}" | column -xc 96
#+end_src
Which results in a cached CSS block that gets tangled [ One unfortunate thing about this setup is that htlmize normally cororizes CSS color syntax, so #123456 would be displayed with a background color set to background: #123456. However, it does this by creating one off "org-custom-1" type CSS classes, which don't really work with a changing theme. There might be some way to add the value as a data attribute on the span… ]:
light theme
--color-bg-cyan: #c2ebe8; --color-bg-magenta: #e0c0e7; --color-bg-blue: #bbcce8;
--color-bg-yellow: #e0d9b0; --color-bg-green: #b9e2d0; --color-bg-red: #f2c0c5;
--color-fg-cyan: #005560; --color-fg-magenta: #800060; --color-fg-blue: #203080;
--color-fg-yellow: #704000; --color-fg-green: #006730; --color-fg-red: #a00040;
--color-fg-accent: #a03068; --color-bg-accent: #ecc0e4; --color-fg-shadow-intense: #683455;
--color-bg-shadow-intense: #cc95b7; --color-fg-neutral: #4e4053; --color-bg-neutral: #d7c9d0;
--color-fg-shadow-subtle: #675462; --color-bg-shadow-subtle: #e5dde0; --color-border: #c4a7b0;
--color-fg-main: #35292f; --color-bg-main: #f7edf1; --color-cursor: #a02050;
--color-diff-added-bg: #c0e4d4; --color-diff-added-fg: #4e4053;
--color-diff-changed-bg: #e3dcb7; --color-diff-changed-fg: #4e4053;
--color-diff-changed-unspecified-bg: #e3dcb7; --color-diff-changed-unspecified-fg: #4e4053;
--color-diff-indicator-added-bg: #c0e4d4; --color-diff-indicator-added-fg: #4e4053;
--color-diff-indicator-changed-bg: #e3dcb7; --color-diff-indicator-changed-fg: #4e4053;
--color-diff-indicator-removed-bg: #f3c6ca; --color-diff-indicator-removed-fg: #4e4053;
--color-diff-refine-added-bg: #9bd5bc; --color-diff-refine-changed-bg: #d4cb93;
--color-diff-refine-removed-bg: #ea9ca3; --color-diff-removed-bg: #f3c6ca;
--color-diff-removed-fg: #4e4053;
other color stuff
Apart from setting up the theme colors and the light/darkmode support,
there is some additional CSS in colors.css that makes use of the
themeing for things like links. Maybe that should be moved to the
components.css…eventually.
additional CSS for color.css
::selection {
--c-selection-bg: var(--org-src-region-bg, var(--color-bg-shadow-intense, red));
--c-selection-fg: var(--org-src-region-fg, var(--color-fg-shadow-intense, red));
background-color: var(--c-selection-bg);
color: color-mix(currentColor 80%, var(--c-selection-fg) 30%);
}
html {
color-scheme: light dark;
background-color: var(--color-bg-main);
color: var(--color-fg-main);
scrollbar-color: var(--color-cursor) var(--color-bg-accent);
}
a {
color: currentColor;
text-decoration-color: hsl(from var(--color-fg-accent) h s l / .7);
text-underline-offset: .1lh;
&:visited, &:hover, &:active {
color: currentColor;
text-decoration-color: hsl(from var(--color-fg-accent) h s l / 1);
text-decoration-style:double;
text-underline-offset: unset;
}
}
/* org mode classes */
figure {
img { border: var(--color-fg-shadow-intense) 3px double; }
figcaption { font-size: calc(var(--p-size) - 0.4rem); }
}
.tag { background-color: hsl(from var(--color-bg-shadow-subtle) h s l / .7); }
TODO Sidenotes, marginnotes, and footnotes
Org supports footnotes, and allows for several customisations.
First, I want to change the footnote rendering. By default, the
footnote reference is an HTML <sup> element that contains a link to
the footnote. I don't really like superscript footnotes in HTML —
though I like them in print. Instead, I will render them as bracketed
numbers (e.g. [1] instead of 1)
(setopt org-html-footnote-format "[%s]"
org-html-footnote-separator ",")
The next thing I tried to add was the ability to render footnotes as marginnotes or sidenotes in the style of tufte-css.
I could not figure out how to get this working exactly how I wanted. I tried to have things look good for both a CSS capable browser, and a text-based browser. At the same time, I do not want JavaScript to be required for any of the notes.
I think it will be possible if I ever figure out how to modify
ox-html to allow me to render a flatter-structure, so that the
footnotes are on the same grid level as their references, until then I
have settled on a somewhat brittle solution:
:post
If I want a marginnote, I write it manually with an export block.
The org block would look like: I can get write marginnotes manually. If it becomes tedious, I can move this to a function or a
:postsource block hook.#+begin_export html <p> The org block would look like: <label for="mn-1" class="marginnote-anchor margin-toggle">⊕</label> <input type="checkbox" id="mn-1" class="margin-toggle"> <span class="marginnote">I can get write marginnotes manually. If it becomes tedious, I can move this to a function or a <a href="https://www.gnu.org/software/emacs/manual/html_mono/org.html#Results-of-Evaluation"><code>:post</code></a> source block hook.</span> </p> #+end_exportFootnotes render as sidenotes.
I was almost able to get this working by modifying
org-html-footnote-referencetoconcatthe footnote definition, though I did need to remove the enclosing<p class="footpara">(in a somewhat janky manner) to be able to use an inline<span>.I modified the function with this patch:
diff --git a/lisp/ox-html.el b/lisp/ox-html.el index 5f3666ba1..6ae32d67e 100644 --- a/lisp/ox-html.el +++ b/lisp/ox-html.el @@ -2833,10 +2833,14 @@ CONTENTS is nil. INFO is a plist holding contextual information." `(lambda (ref _) (if ,label (equal (org-element-property :label ref) ,label) - (not (org-element-property :label ref))))))))))) + (not (org-element-property :label ref)))))))))) + (contents (string-join (split-string (org-trim (org-export-data (org-export-get-footnote-definition footnote-reference info) info)) "\n") " ")) + (sidenote-format "<label for=\"sn-%s\" class=\"marginnote-anchor margin-toggle sidenote-number\"></label><input type=\"checkbox\" id=\"sn-%s\" class=\"margin-toggle\"><span class=\"sidenote\">%s</span>") + (html-sidenote-format (format sidenote-format n n (replace-regexp-in-string "^<p class=\"footpara\">\\(.*\\)</p>$" "\\1" contents)))) + (message "contents: %s" html-sidenote-format) (format (plist-get info :html-footnote-format) (org-html--anchor - id n (format " class=\"footref\" href=\"#fn.%s\" role=\"doc-backlink\"" (or label n)) info))))) + id html-sidenote-format (format " class=\"footref\" href=\"#fn.%s\" role=\"doc-backlink\"" (or label n)) info))))) ;;;; HeadlineIn a browser that supports CSS, I hide the footnotes section at the bottom of the page with a
display: none. Unfortunately, if the browser is text-based the note renders inline. It doesn't look too terrible, but I would prefer to have in a blockquote for a text-based browser. However, then I would need to use JavaScript in a mainstream browser to take the contents of the blockquote and put them in a span.Another downside to this patch is that it is wrapped in the footnote reference's
<a>tag, which means I cannot include links in the sidenote.
Then there is some CSS needed for the marginnotes/sidenotes, and also the regular footnote section that is generated:
CSS: sidenotes and marginnotes
body { counter-reset: --sidenote-counter; }
/* The table of contents is the left anchor the start of sidenotes */
.content > #table-of-contents { anchor-name: --toc; }
@supports(anchor-name: --toc) {
/* the checkbox used to toggle sidenotes and marginnotes when the
screen is too narrow to display them in the page margin */
.margin-toggle { display: none; }
/* the <label> */
label[for^="mn-"].marginnote-anchor { display: none; }
.marginnote-anchor {
anchor-name: --margin-note;
anchor-scope: --margin-note;
cursor: pointer;
}
/* The note content */
.marginnote, .sidenote {
margin: 0; padding: 0;
position: absolute;
position-anchor: --margin-note;
left: calc(anchor(--toc right) + var(--breakout-size) / 2);
top: anchor(--margin-note top);
right: var(--padding-inline);
max-inline-size: 50ch;
text-align: left;
font-size: var(--step--2);
}
/* sidenotes have numbers, (marginnotes do not) */
.sidenote {
/* To account for the number */
text-indent: hanging 2.5ch;
&:before {
content: counter(--sidenote-counter) ".";
position: relative;
vertical-align: baseline;
color: var(--color-fg-red);
font-size: var(--step--3);
padding-inline-end: .5ch;
}
}
.sidenote-number {
counter-increment: --sidenote-counter;
a:has(&) { text-decoration: none; }
&:before{
content: counter(--sidenote-counter);
font: var(--f-margin-notes);
color: var(--color-fg-red);
cursor: pointer;
padding-inline: .25ch;
}
}
@media (max-width: 1100px) {
/* on small screens, hide the notes until the anchor is toggled */
label[for^="mn-"].marginnote-anchor { display: inline; }
.sidenote, .marginnote { display: none; inset: auto; }
.margin-toggle:checked + .sidenote,
.margin-toggle:checked + .marginnote {
display: block;
margin: 1rem 2.5%;
vertical-align: baseline;
position: relative;
background: var(--color-bg-shadow-subtle);
color: var(--color-fg-shadow-subtle);
padding: 1em;
}
}
}
CSS: footnotes
.outline-2:has(+ div#footnotes) {
hr:is(:last-child) { --prose-spacing: 6lh; }
}
#footnotes {
--prose-spacing: 1lh;
.footdef {
margin-bottom: var(--prose-spacing);
display: flex;
align-items: start;
.footnum {
align-self: first baseline;
line-height: 1.8;
}
.footpara { margin-inline-start: 1ch; }
p, a { font: var(--f-margin-notes); }
}
/* visually hide the "footnotes" header */
h2.footnotes {
clip: rect(1px, 1px, 1px, 1px);
clip-path: inset(50%);
height: 1px;
width: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
}
/* sidenotes will be used instead */
@supports(anchor-name: --toc) { display: none; }
}
TODO Code references
The org html exporter has an optional script that can be included in the header (excluded by default). As described by the info doc:
In HTML, hovering the mouse over such a link remote-highlights the corresponding code line(4), which is kind of cool.
I do think it is kind of cool. Since I block JavaScript by default, it would be nice to have a CSS only fallback.
To do this with CSS only, I am going to make use of CSS anchors.
First, I need to advise ox-html's export functions to attach
data-attributes to the <code> tags on export.
Elisp: coderef advice
(defun add-css-code-ref-a (org-html-link)
(let ((replace-str "<a data-code-ref-src=\"--coderef-\\1\" href=\"#coderef-\\1\"")
(search "<a href=\"#coderef-\\(.*\\)\""))
(replace-regexp-in-string search replace-str org-html-link)))
(defun add-css-code-ref-source (code)
(let ((replace-str "<span data-code-ref-src=\"--coderef-\\1\" id=\"coderef-\\1\"")
(search "<span id=\"coderef-\\(.*?\\)\""))
(if (string-match search code)
(replace-regexp-in-string search replace-str code)
code)))
(advice-add 'org-html-link :filter-return #'add-css-code-ref-a)
(advice-add 'org-html-do-format-code :filter-return #'add-css-code-ref-source)
Then I will need the CSS to make use of the data attributes I am
attaching. I also want to disable the CSS implementation if JavaScript
is enabled. So I gate the CSS :hover effect with a
:not(.disable-css-cite) check.
a:not(.disable-css-cite).coderef:hover::before { display: block; }
CSS: coderef with anchors
/* for JS enabled browsers */
.code-highlighted {
background-color: var(--color-bg-accent);
> span { background-color: var(--color-bg-accent); }
}
/* A CSS-only implementation */
a:not(.disable-css-cite).coderef:hover::before { display: block; }
a.coderef:hover { background-color: var(--color-bg-accent); }
code span[id^="coderef"] {
anchor-name: attr(data-code-ref-src type(<custom-ident>));
}
.coderef:not(code) {
anchor-name: --reference-to-src;
anchor-scope: --reference-to-src;
--source-referenced: attr(data-code-ref-src type(<custom-ident>));
}
.coderef:not(code)::before {
--sblur: 10px;
--block-padding: .2lh;
--shadow-1: hsl(from var(--org-src-hl-line-bg) h s l / .9);
--shadow-2: hsl(from var(--color-bg-shadow-intense) h s l / .4);
--shadow-3: hsl(from var(--color-bg-shadow-subtle) h s l / .2);
display: none;
content: "";
position: absolute;
border: 2px solid var(--shadow-1);
border-radius: 4px;
top: calc(anchor(top var(--source-referenced)) - var(--block-padding));
right: calc(anchor(end var(--source-referenced)) - 1ch);
left: calc(anchor(left var(--source-referenced)) - .5ch);
bottom: calc(anchor(bottom var(--source-referenced)) - var(--block-padding));
box-shadow: 0px 0px var(--sblur) 1px var(--shadow-1),
0px 0px var(--sblur) 4px var(--shadow-2),
0px 0px var(--sblur) 10px var(--shadow-3);
}
Then I have to slightly modify the existing JavaScript to add the
.disable-css-cite class as part of it's code:
JS: modified coderef code
function CodeHighlightOn(elem, id)
{
// disable the CSS only implementation as JS is running
elem.classList.add("disable-css-cite");
var target = document.getElementById(id);
if(null != target) {
elem.classList.add("code-highlighted");
target.classList.add("code-highlighted");
}
}
function CodeHighlightOff(elem, id)
{
var target = document.getElementById(id);
if(null != target) {
elem.classList.remove("code-highlighted");
target.classList.remove("code-highlighted");
}
}
The CSS-only implementation works ok, but I am accomplishing the effect by placing an element over the referenced code. This means the text legibility would not be great if I tried to do a highlight effect by using a transparent background color. I went with an outline instead, to keep the text readable.[ A better effect might be achieved by messing with z-indexes. Something to experiment with. ]
TODO Table of contents
For the table of contents I originally copied the floating table of
contents from the org wiki. That worked well until I realized it
wasn't usuable with a keyboard — neither TAB nor ' worked in
Firefox.
I have seen other sites put it in the left or right margin as a sticky item. That looks nice and could be updated dynamically to show the current position in the page. However, since I am using sidenotes putting the table of contents in the margins makes things look crowded. For now just a normal list works.
Table of contents
#table-of-contents {
--toc-color: var(--color-fg-shadow-intense,
var(--org-src-keyword-fg, red));
--toc-color-faint: hsl(from var(--toc-color) h s l / .3);
display: grid; gap: 0;
margin-top: 3rem;
font: var(--f-toc-links);
#text-table-of-contents { padding: 0.5em; }
ul, ol { padding-inline-start: 4%; }
h2 {
font: var(--f-toc-title);
color: var(--toc-color-faint);
text-transform: uppercase;
}
}
TODO Source blocks
Org source block styling. I want this to have overflow-x: scroll
eventually, however whenever I try to do that it ends up creating a
site-wide horizontal scroll. Until I can figure that out I just wrap
the lines:
HTML .org-src-container elements
:root {
--padding-source-container: 8pt;
}
pre.src-awk:before { content: 'Awk'; }
pre.src-authinfo::before { content: 'Authinfo'; }
pre.src-c:before { content: 'C'; }
pre.src-C:before { content: 'C'; }
pre.src-css:before { content: 'CSS'; }
pre.src-ditaa:before { content: 'ditaa'; }
pre.src-dot:before { content: 'Graphviz'; }
pre.src-calc:before { content: 'Emacs Calc'; }
pre.src-emacs-lisp:before { content: 'Emacs Lisp'; }
pre.src-gnuplot:before { content: 'gnuplot'; }
pre.src-js:before { content: 'JavaScript'; }
pre.src-latex:before { content: 'LaTeX'; }
pre.src-ledger:before { content: 'Ledger'; }
pre.src-lisp:before { content: 'Lisp'; }
pre.src-lua:before { content: 'Lua'; }
pre.src-org:before { content: 'Org mode'; }
pre.src-plantuml:before { content: 'Plantuml'; }
pre.src-ruby:before { content: 'Ruby'; }
pre.src-scheme:before { content: 'Scheme'; }
pre.src-screen:before { content: 'Gnu Screen'; }
pre.src-sed:before { content: 'Sed'; }
pre.src-sh:before { content: 'shell'; }
pre.src-sql:before { content: 'SQL'; }
pre.src-sqlite:before { content: 'SQLite'; }
pre.src-makefile:before { content: 'Makefile'; }
pre.src-perl:before { content: 'Perl'; }
pre.src-shell:before { content: 'Shell Script'; }
pre.src-bash:before { content: 'bash'; }
pre.src-asm:before { content: 'Assembler'; }
pre.src-html:before { content: 'HTML'; }
pre.src-ps:before { content: 'PostScript'; }
pre.src-prolog:before { content: 'Prolog'; }
pre.src-tex:before { content: 'TeX'; }
pre.src-xml:before { content: 'XML'; }
pre.src-nxml:before { content: 'XML'; }
pre.src-conf:before { content: 'Configuration File'; }
pre.src-elisp:before { content: 'Emacs Lisp'; }
pre.src-diff:before { content: 'Git Diff'; }
pre.src:before {
display: none;
position: absolute;
position-anchor: --org-src-container;
top: anchor(top);
right: anchor(right);
padding: 3px;
color: var(--color-fg-neutral);
background-color: var(--color-bg-neutral);
padding: .25lh var(--padding-source-container, 100px);
border: 1px solid;
border-style: double ridge double ridge;
border-color: var(--color-bg-shadow-intense)
var(--color-fg-shadow-subtle)
var(--color-fg-shadow-intense)
var(--color-bg-shadow-subtle);
}
details, .org-breakout-fw:not(:has( > details)) {
display: grid;
grid-template-columns: 1fr;
overflow: clip;
> .org-src-container {
max-inline-size: calc(100cqi - var(--padding-inline) * 2);
overflow-x: auto;
border: 1px solid var(--color-border);
padding: var(--padding-source-container, 100px);
pre { border: none; padding: 0; overflow: clip; }
}
&:open {
border: 1px solid var(--org-src-org-code-fg, red);
box-shadow: 1px 2px 3px var(--color-bg-shadow-subtle, var(--org-src-org-hide-fg));
> .org-src-container { padding: 0; }
> .org-src-container > pre {
overflow-x: auto;
padding: var(--padding-source-container, 100px);
}
}
}
.org-breakout-fw:not(:has( > details)) > .org-src-container {
/* max-inline-size: 100cqi; */
}
.org-src-container {
font: var(--f-code-block);
anchor-name: --org-src-container;
anchor-scope: --org-src-container;
display: grid;
grid-template-columns: 1fr;
overflow: clip;
pre {
margin: 0;
border: 1px solid var(--color-border);
max-inline-size: calc(100cqi - var(--padding-inline) * 2);
overflow-x: auto;
padding: var(--padding-source-container, 100px);
}
pre.src:hover::before {
margin: 0;
display: block;
}
> label:has(.listing-number) {
color: var(--org-src-org-block-begin-line-fg);
background-color: var(--org-src-org-block-begin-line-bg);
font-size: var(--step--3, smaller);
text-align: start;
padding: .25lh var(--padding-source-container, 100px);
}
}
.example {
max-inline-size: calc(100cqi - var(--padding-inline) * 2);
overflow-x: auto;
overflow-y: clip;
background-color: var(--org-src-org-block-bg,red);
color: var(--org-src-org-code-fg);
font: var(--f-code-block);
padding: var(--padding-source-container, 100px);
}
TODO Various HTML elements
- Lists
<ul>,<ol>,<dl>Lists: ul, ol, and dl
.org-ul, .org-ol, .org-dl { margin-inline: calc(var(--padding-inline, 0) / 2); } .org-dl { font-size: var(--step--1); columns: 2 33ch; &.document-helper-definition { columns: 1; margin-inline: 0; dd { margin-inline-start: 3%; > * { margin-block: .8rem; } p { font-size: var(--step--1) }; } } } .org-ol { --list-item-spacing: 1lh; --first-line-spacing: calc(var(--list-item-spacing) / 2); --list-item-child-spacing: calc(var(--list-item-spacing) / 4); li { &::marker {font: var(--f-list-heading); } &:not(:has(> p))::marker { font-style: normal; font-size: var(--step--1); } > * + * { margin-block-end: var(--list-item-child-spacing); } p:first-of-type { font: var(--f-list-heading); margin-block-end: var(--first-line-spacing); } } }<details>The styling for the
<details>blocks.I've tried to add some animations on the open/close, but it doesn't work in firefox currently.
HTML
<details>elements:root { interpolate-size: allow-keywords; } details { background-color: var(--org-src-org-block-begin-line-bg); box-shadow: 1px 2px 3px var(--color-fg-shadow-intense, var(--org-src-org-link-fg)); transition: box-shadow .5s; summary { display: flow-root list-item; padding-inline-start: 1lh; > * { display: inline; } } } @supports(interpolate-size: allow-keywords) { details::details-content { block-size: 0; transition: block-size .5s, content-visibility .4s; transition-behavior: allow-discrete; } details[open]::details-content { block-size: auto; } }<hr>horizontal rule
HTML
<hr>elementhr { border: none; border-top: 3px double var(--color-border); color: var(--color-border); overflow: visible; text-align: center; height: 5px; width: 100%; &::after { background: var(--color-bg-main); content: "§"; padding: 0 4px; position: relative; top: -13px; } }<blockquote>blockquotes
HTML
<blockquote>elementblockquote { margin: 4% 0 0; padding: 1em; border-left: 3px solid var(--color-fg-accent, var(--org-src-org-table-fg)); padding-block: 1.5em; color: currentColor; background-color: var(--color-bg-shadow-subtle, var(--org-src-org-block-bg)); font: var(--font-blockquote); footer { font-size: var(--step--1); text-align: end; } }<table>HTML tables
HTML
<table>elementtable { --table-bg: color-mix(var(--org-src-org-table-fg) 7%,var(--org-src-org-block-bg) 80%); color: var(--org-src-org-table-fg); background-color: var(--table-bg); border-collapse:collapse; width: 100%; td, th, caption { padding: .2lh; text-align: start; } caption { background-color: var(--org-src-org-block-begin-line-bg); font-size: var(--step--2); font-weight: bold; text-transform: capitalize; color: var(--org-src-org-block-begin-line-fg); } th { background-color: var(--org-src-org-block-bg); &.org-left { text-align: start; } } tr:nth-of-type(2n) { background-color: var(--org-src-org-block-bg); } }- tweaks
small tweaks to the default styles
Tweaking some org defaults
/* Org inserts an empty div after section that are only a header... */ .content { /* :empty will will eventually handle whitespace, but not yet */ div:empty { display: none; } @supports selector(:-moz-only-whitespace) { div:-moz-only-whitespace { display: none; } h2 + div:-moz-only-whitespace + div.outline-3 { margin-block-start: 0; > h3 { margin-block-start: 0; } } h3 + div:-moz-only-whitespace + div.outline-4 { margin-block-start: 0; > h4 { margin-block-start: 0; } } } @supports not selector(:-moz-only-whitespace) { div:not(:has(*)) { display: none !important; } h2 + div:not(:has(*)) { & + div.outline-3 { margin-block-start: 0lh; > h3 { margin-block-start: 0; } } } h3 + div:not(:has(*)) { & + div.outline-4 { margin-block-start: 0; > h4 { margin-block-start: 0; } } } } } /* In org mode "emacs-lisp" and "elisp" are aliases, but in the default CSS only .src-emacs-lisp is defined */ pre.src-elisp:before { content: 'Emacs Lisp'; } @supports(interpolate-size: allow-keywords) { details::details-content { block-size: 0; transition: block-size .5s; } details[open]::details-content { block-size: auto; } } /* org exports help: links as clickable, but they won't work in most browsers. Kind-of-sort-of disable them */ a[href^="help:"] { text-decoration: none; color: var(--org-src-keyword-fg); cursor:not-allowed; pointer-events: none; }
TODO Packaging up the JS
I've stuffed the modified citation code, along with the theme
selection code into the org-html-scripts variable, so that by
setting org-html-head-include-scripts to t the code is included in
the export.
I am also generating and adding the integerity attribute from the
script body. It doesn't actually make a difference in this case
because everything is being served from the same origin, but I just
felt like figuring out how to hash some content in Elisp. [ I eventually want to hash the generated assets, such as the CSS, so that a SHA can be included in their filenames. This would help with caching. ]
(let* ((new-script (format "<script>\n// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later\n%s\n// @license-end\n</script>" (concat theme-js "\n" citation-js)))
(start (+ 8 (string-match "<script>" new-script)))
(end (string-match "</script>" new-script))
(integrity-sha256 (secure-hash 'sha256 new-script start end t))
(integrity-sha384 (secure-hash 'sha384 new-script start end t))
(integrity-sha512 (secure-hash 'sha512 new-script start end t))
(integrity-attr (format "integrity=\"sha256-%s\nsha384-%s\nsha512-%s\""
(base64-encode-string integrity-sha256 t)
(base64-encode-string integrity-sha384 t)
(base64-encode-string integrity-sha512 t))))
(replace-regexp-in-string
"<script>"
(format "<script %s\ncrossorigin=\"anonymous\">" integrity-attr)
new-script))
Pages
/ (homepage)
While a sitemap is not really a homepage, I am going to make use of
the built-in sitemap settings to build an index.html page.
Eventually I might just put an actual homepage.org under the
pages/ directory, and generate the homepage the same way the about
page is generated. For now, I am just hooking into the :auto-sitemap
function.
There are some configuration values I set in .dir-locals
:auto-sitemap t
:sitemap-filename "index.org"
:sitemap-title "src_bash{> ls -gno --time-style=-'+%Y-%m-%d'}"
:sitemap-ignore-case t
:sitemap-function org-pub-demo|sitemap-function
:sitemap-format-entry org-pub-demo|format-sitemap-entry
The org-pub-demo|sitemap function creates the actual org file that
is then exported to HTML as :sitemap-filename. The generated org
file is treated the same as any other org file, so source blocks are
evaluated, noweb references are expanded, etc.
It might make more sense to just write up a homepage.org file, and
let the sitemap continue to be generated as the default
sitemap.org. Then a #+INCLUDE: can be used to make use of the
sitemap.org file. I haven't done that here because I am not sure how
to make sure any homepage.org file is exported after the
sitemap.org has been regenerated.
Elisp: sitemap functions
(defun org-pub-demo|format-sitemap-entry (entry _style project)
"generates the sitemap, which is used as the site's homepage.
modeled off of `ls -gno --block-size=\'1\' --time-style=-\'+%Y-%m-%d\''"
(let* ((dir-fmt "[[file:%s][=dr--r--r-x 2 %s %s=]]")
(file-fmt "[[file:%s][=-rw-r--r-- 1 %s %s=]]")
(exported-file-name (with-temp-buffer
(insert-file-contents-literally entry)
(if (re-search-forward "^#\\+export_file_name:\\(.*\\)$" nil t)
(format "%s.html" (org-trim (match-string 1)))
entry)))
(date (format-time-string
"%Y-%m-%d"
(org-publish-find-date entry project))))
(cond ((not (directory-name-p entry))
(format
file-fmt exported-file-name date (org-publish-find-title entry project)))
(t (format
dir-fmt entry date entry)))))
(defun org-pub-demo|sitemap-function (title list)
"Default site map, as a string.
TITLE is the title of the site map. LIST is an internal
representation for the files to include, as returned by
`org-list-to-lisp'. PROJECT is the current project."
(format "<<sitemap-template()>>"
title
(org-list-to-org list)))
One thing I'd like to figure out is how to better map the org files to
their exported name. I use EXPORT_FILE_NAME keyword to give the
exported HTML file a cleaner URL than the org filename. While there is
an org-publish-find-property function available, the
EXPORT_FILE_NAME option doesn't appear to be available through it.
Instead I am just searching for the keyword in each file.
The template being expanded in org-pub-demo|sitemap-function is:
Org: homepage template
#+TITLE: %s
#+begin_export html
<style>
<<sitemap-css>>
</style>
#+end_export
%s
/about
There isn't much to the generated /about page. Since there isn't a
table of contents to anchor off of, I went ahead and attached to the
first <hr>.
ORG: about page
#+TITLE: About:
#+SUBTITLE: internet personality
#+begin_EXPORT html
<style>
#content > header {
display: flex;
align-items: baseline;
.title, .subtitle { padding: 0; margin: 0; }
}
.content > section.outline-2 hr:first-of-type { anchor-name: --toc; }
.outline-text-2 > .org-ul { margin: 0 var(--padding-inline); }
</style>
#+end_EXPORT
* Contact
+ pgp: [[./assets/pgp.txt][pgp.txt]]
+ email: tbd
+ irc: tbd
-----
* README
:PROPERTIES:
:END:
I worked professionally as a programmer/software engineer/developer for over a decade. [fn:1] I love
working on software that people use; large "legacy" codebases are my happy place.
-----
* Footnotes
[fn:1] Saying "over a decade" sounds more impressive than "thirteen years".
I also needed my pgp key. I've already got one, so for now just outputting that to a text file:
gpg --armour --export "${key_id}" > pgp.txt
Footnotes:
It isn't entirely self contained, as I do make use of several
external programs and various coreutils. I also make several
assumptions about the Emacs environment, such that packages like
htmlize or the doric-themes are installed.
Calling the function org-property-values in my org code
blocks is an example of where I ran into issue.
There are several gotchas associated with having the dir-locals.el
file generated from the tangling the org file. The major one being that I have
to close all of the buffers to reload the variables if I make a change and
tangle. An additional complication with the dir-locals block is that it isn't
a normal elisp block — it cannot be wrapped in a let, which is what
happens if I try inserting the HTML block via a block :var.
I originally base64 encoded the SVGs and embedded them directly in the CSS. Later I opted to lazy load them to save some bytes if the footer is never even rendered.
When the site's content was all centered the CSS didn't require
any subgrid usage to get the breakout and full-width grid lines
working. All that was needed was to adjust the inline-padding based
on the nesting level. Something like
--inline-padding:calc((var(--inline-padding)/var(--num-levels)) * var(--current-nesting-level)).
I've generated the site using the ef-themes as well. Those
look pretty great. I might move to using them.
Now I see why the htmlize implementation is the way it is. Individual themes may implement their palettes in different ways, and htmlize needs to work with everything.
I know CSS now supports many different color notations, but I am not well versed in colors and couldn't figure out how to translate a Emacs oklab color value to CSS notation.
One unfortunate thing about this setup is that htlmize normally
cororizes CSS color syntax, so #123456 would be displayed with a
background color set to background: #123456. However, it does this by
creating one off "org-custom-1" type CSS classes, which don't really
work with a changing theme. There might be some way to add the value
as a data attribute on the span…
A better effect might be achieved by messing with z-indexes. Something to experiment with.
I eventually want to hash the generated assets, such as the CSS, so that a SHA can be included in their filenames. This would help with caching.