Add Version 7
This commit is contained in:
parent
50060a5f9d
commit
eacdf85629
98
site_test/posts/2025/2025-02-22-version-7.md
Normal file
98
site_test/posts/2025/2025-02-22-version-7.md
Normal file
@ -0,0 +1,98 @@
|
||||
```
|
||||
title = "Version 7"
|
||||
tags = ["meta"]
|
||||
date = "2025-02-22 12:00:42 -0500"
|
||||
short_desc = "Fully rewritten and redesigned."
|
||||
```
|
||||
|
||||
Welcome to version 7 of my website. What started as a rewrite of the backend last fall has since snowballed into a complete redesign of the frontend as well (if you're using an RSS reader, check out the actual site). The previous design dates back to the fall of 2019 and, after doing a bunch of work to rewrite the backend, rebuilding the exact same frontend again wasn't an attractive prospect.
|
||||
|
||||
<!-- excerpt-end -->
|
||||
|
||||
## Redesign
|
||||
|
||||
I don't really know what my design process is. This time, I spent ages trawling through hundreds of other people's personal blogs and websites trying to find anything that I liked, hoping that something cohesive would eventually emerge. It ultimately did, though I have no idea wherefrom.
|
||||
|
||||
### Typography
|
||||
|
||||
The body text is set in [Valkyrie](https://mbtype.com/fonts/valkyrie/) by Matthew Butterick, and the code is set in [`MD IO`](https://mass-driver.com/typefaces/md-io/) by Mass Driver. Valkyrie is a font that I've had my eye on for a while, since reading Butterick's [_Practical Typography_](https://practicaltypography.com). In the old design, I deliberately chose to use a system font (<span style="font-family: Charter, Georgia, serif;">Charter</span>) to avoid the (over-the-wire byte) cost of shipping an entire font. This time, though, I put quite a lot of work into the backend static-site-generator in part so that I could ship [subset fonts](#font-subsetting) without a lot of manual work.
|
||||
|
||||
It took me a while to settle on `MD IO` since it's not one of my usual monospace fonts. But I'm quite happy with it ultimately. It looks nice with Valkyrie when used inline, and looks good in bigger blocks of code. It also has a nice _`italic`_ style, which is important to me since commenets in syntax-highlighted code blocks are italicized. I'm very pleased with how the font choices turned out, I think they both look great—especially on high-DPI displays[^1].
|
||||
|
||||
[^1]: Having put so much time into choosing fonts, I remain disappointed in the state of the external monitor market. My white whale of a 5K, high-DPI, high refresh rate monitor remains elusive. The type on this website looks perfectly good on a regular display, but it looks so much better in high-DPI (like even the screen on my laptop).
|
||||
|
||||
### Markdown Decorations
|
||||
|
||||
One of the more notable touches from the [old design](/2019/reincarnation/#new-theme) was the use of CSS pseudo-elements to render inline Markdown decorations (i.e., underscores around italicized text, making links appear like `[hello](example.com)`, etc.). I still quite like that touch, but after five and a half years, it's time to move on. No more Markdown decorations—but maybe they'll come back in a future redesign.
|
||||
|
||||
### Sidenotes
|
||||
|
||||
One of the side effects of having the content be width-constrained and left-aligned is that I have this big column of empty space on the right to play with. The previous design already used floating asides on wide enough screens, and I wanted to extend that to footnotes as well. I'm the type of person who reads all the footnotes, and having to jump to the end back is somewhat jarring—even if there are backlinks, I often visually lose my place just from the viewport changing. So, sidenotes it is.
|
||||
|
||||
I embarked on an exhaustive survey of someone else's [exhaustive survey](https://gwern.net/sidenote) of existing web libraries for creating sidenotes. Unfortunately, just about all of the options either had limitations that I wasn't willing to accept (e.g., not supporting sidenotes that are numbered like footnotes) or were entirely reliant on JavaScript.
|
||||
|
||||
The solution that I arrived at is a custom combination of some backend processing in the static site generator and a bunch of CSS. When footnotes in Markdown are being converted to HTML, the reference to and the contents of every footnote is output twice: first immediately following the footnote reference in an inline `<span>` element, and then again in the footnotes section at the end. Having the sidenote definition inline immediately after the reference lets me do two things with CSS alone:
|
||||
|
||||
1. Align the sidenote to exactly where it's referenced in the content.
|
||||
2. Highlight the sidenote when the corresponding reference is hovered.
|
||||
|
||||
The second set of definitions are at the end, and are only shown when the screen is viewport is narrow enough for the sidenotes not to fit. Lastly, the two sets of references exist because—although the text of the links is the same—the destinations are different: one jumps to the footnotes section at the end of the page, the other to the sidenote. Which reference is shown is also swapped out using CSS, this time producing a purely functional change since they're visually identical.
|
||||
|
||||
### Color Schemes
|
||||
|
||||
For a couple reasons, there's no longer a dark theme. What you see now is what there is. The first reason is that I've never particularly enjoyed light text on a dark background for reading—I always preferred the light theme of my old design. The second is that, since the page background is now a nice off-white, I found no satisfying way of incorporating that into a dark theme.
|
||||
|
||||
## The Graph
|
||||
|
||||
The impetus for rewriting the backend of my blog can be summed up in one word: graph. Specifically, a dependency graph.
|
||||
|
||||
I should back up first. When building the backends for the last couple versions of my blog, I've done things pretty much the same way: write a bunch of code that reads files and outputs more files. Less a cohesive piece of software, and more of a script. No real consideration was made for incremental compilation or watching files for changes. At various times, this was handled by [separate tools](https://github.com/eradman/entr) and by just watching the entire content directory for changes and rebuilding the whole site. This did work, but it was less than ideal—both because it bothers the perfectionist in me and because regeneration gets inexorably slower as the blog grows.
|
||||
|
||||
So, the path I went down was building the entire static site generator around a giant directed, acyclic graph. Each node in the grah is a unit of computation that produces an output value from the values of some input nodes (or inputs external to the graph, like the filesystem). Powering this ~~monstrosity~~ elegant architecture is a Rust crate I wrote called `compute_graph` which abstracts away dealing with the actual graph and lets you just write code for each of the nodes.
|
||||
|
||||
Actually generating the site, then, is effectively just ("just") topologically sorting the graph and evaluating each node in order. From there, it's not terribly complicated to track which files are read during the process and, when the filesystem watcher detects a change, invalidate _just_ the nodes that read that file and let the graph evaluation take care of the rest. Because the graph also prevents invalidations from propagating downstream when a node's value doesn't change, partial updates can be much more efficient than just regenerating things willy-nilly (e.g., editing the excerpt of a blog post will regenerate both the post page and the homepage, but editing not-the-excerpt won't regenerate the homepage).
|
||||
|
||||
One of the more interesting details of the graph abstraction is that the output value of a node can, itself, be _more_ graph nodes. This means that part of the process of graph evaluation is going back, making sure all the edges are established, then redoing the topological sort and starting the evaluation again.
|
||||
|
||||
And, for fun, here's a diagram of the entire dependency graph—all 964 nodes—of the website as of writing this (you really do want to be in a browser for this, it likely won't work in your RSS reader):
|
||||
|
||||
<style>
|
||||
#svg-container {
|
||||
overflow: scroll;
|
||||
height: 500px;
|
||||
background: white;
|
||||
}
|
||||
</style>
|
||||
<figure>
|
||||
<div id="svg-container">
|
||||
<object type="image/svg+xml" data="/2025/version-7/graph.svg" id="svg"></object>
|
||||
</div>
|
||||
<figcaption>
|
||||
<code id="svg-label"></code>
|
||||
You can scroll, zoom, and click nodes to see connected edges and node labels.
|
||||
<br>
|
||||
<a href="#" id="zoom-out">Zoom Out</a>.
|
||||
<a href="#" id="zoom-in">Zoom In</a>.
|
||||
</figcaption>
|
||||
</figure>
|
||||
<script src="/2025/version-7/graph.js" defer></script>
|
||||
|
||||
### Font Subsetting
|
||||
|
||||
Font subsetting is one of the big features that the whole graph abstraction enables. Subsetting is the process of creating a new font that contains only a subset of the characters/glyphs that the original font has. The simplest way to do this would be do determine what character set you're interested in based on the language the content is written in, and generate a subset font for that.
|
||||
|
||||
If the entire site is typeset in just one style of one font, that process would work fine. But are you also using all of those characters in the bold style of the font? The bold italic one? The monospace bold italic one[^2]? Probably not.
|
||||
|
||||
[^2]: In fact, I'm currently not using the monospace bold italic variant at all. And part of the process about to be described asserts that that is the case and will prevent the site from generating if that changes.
|
||||
|
||||
So here's where the big dependency graph comes in: every node that writes an HTML file to disk also outputs the HTML as a string that's used by further nodes to extract the set of distinct characters used from each font style. This process involves parsing the HTML, walking the tree while keeping track of the current font state (to handle nesting), and accumulating the set of characters. The character sets from each file are merged and then those are inputs to the node responsible for actually subsetting the font. Subsetting is performed by [pyftsubset](https://fonttools.readthedocs.io/en/latest/subset/index.html) from the fontTools project[^3] which is run directly from Rust using [PyO3](https://pyo3.rs/). Finally, the subset fonts are Base 64 encoded and embedded in the compiled CSS.
|
||||
|
||||
[^3]: I would like to be able to subset web fonts directly from Rust, but there are no existing projects that do so. I started writing my own—and got as far as parsing WOFF2—but switched to the Python project in the interest of expediency. To be continued...
|
||||
|
||||
The graph is very useful for this whole thing because it means that everything that needs to be regenerated is whenever necessary, and only when necessary (i.e., editing content in a way that doesn't add new characters to the fonts doesn't re-subset them), without having to write a bunch of code to explicitly keep track of all that information.
|
||||
|
||||
## ActivityPub
|
||||
|
||||
One of the major features of [version 5](/2019/reincarnation/#activity-pub) was making my blog a first-class ActivityPub citizen. This meant you could follow my blog from Mastodon (& co.) and leave comments by replying. This was a cool feature, but I've made the decision to leave it behind this time. It was too great of a code/maintenance burden for too few users. While it didn't break too often, it accounted for the majority of the code in v6 and made deployment somewhat more complicated, since it wasn't just a static site—which now it is.
|
||||
|
||||
I haven't entirely abandoned fediverse integration though: now you can comment on blog posts by replying via ActivityPub to a post from my personal account. I think this is a good middle ground: it's effectively delegating all the ActivityPub responsibility to [Akkoma](https://akkoma.dev), which is actually built for that purpose first and foremost—rather than my hodgepodge—and simply fetching them from the client side. I was also already posting links to blog posts, and almost all of the responses I'd get are directly to that.
|
4069
site_test/static/2025/version-7/graph.dot
Normal file
4069
site_test/static/2025/version-7/graph.dot
Normal file
File diff suppressed because it is too large
Load Diff
104
site_test/static/2025/version-7/graph.js
Normal file
104
site_test/static/2025/version-7/graph.js
Normal file
@ -0,0 +1,104 @@
|
||||
let container, containerRect;
|
||||
let svg;
|
||||
let hasLoaded = false;
|
||||
console.log(document.readyState);
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
container = document.getElementById("svg-container");
|
||||
containerRect = container.getBoundingClientRect();
|
||||
svg = document.getElementById("svg");
|
||||
svg.addEventListener("load", onLoad);
|
||||
// Sometimes the load event just doesn't fire in Safari (race between loading the script and svg?)
|
||||
// So, as a fallback:
|
||||
setTimeout(() => {
|
||||
if (!hasLoaded) {
|
||||
try {
|
||||
onLoad();
|
||||
} catch (e) {
|
||||
console.error("Still failed to load after delay", e);
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
let origWidth, origHeight;
|
||||
function onLoad() {
|
||||
hasLoaded = true;
|
||||
const svgDoc = svg.getSVGDocument();
|
||||
const el = svgDoc.getElementById("node1");
|
||||
origWidth = svgDoc.rootElement.width.baseVal.valueInSpecifiedUnits;
|
||||
origHeight = svgDoc.rootElement.height.baseVal.valueInSpecifiedUnits;
|
||||
scale = Math.pow(0.8, 3);
|
||||
updateSVG(1);
|
||||
const rect = el.getBoundingClientRect();
|
||||
container.scrollTo(
|
||||
rect.x - (containerRect.width - rect.width) / 2,
|
||||
rect.y - (containerRect.height - rect.height) / 2,
|
||||
);
|
||||
|
||||
svgDoc.addEventListener("click", selectNode);
|
||||
}
|
||||
|
||||
function selectNode(e) {
|
||||
const node = findNodeParent(e.target);
|
||||
if (node) {
|
||||
const id = [...node.classList].find((x) => x !== "node");
|
||||
const style = svg.contentDocument.getElementById("node-style");
|
||||
style.innerHTML = `
|
||||
.${id} text,
|
||||
.${id} polygon {
|
||||
fill: red;
|
||||
}
|
||||
.${id} ellipse,
|
||||
.${id} path,
|
||||
.${id} polygon {
|
||||
stroke: red;
|
||||
}`;
|
||||
|
||||
const title = node.querySelector("a").getAttribute("xlink:title");
|
||||
const label = document.getElementById("svg-label");
|
||||
label.innerText = title;
|
||||
label.innerHTML += "<br>";
|
||||
}
|
||||
}
|
||||
|
||||
function findNodeParent(e) {
|
||||
if (e.classList && e.classList.contains("node")) {
|
||||
return e;
|
||||
} else if (e.parentElement) {
|
||||
return findNodeParent(e.parentElement);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
let scale = 1;
|
||||
function updateSVG(oldScale) {
|
||||
const oldRect = svg.getBoundingClientRect();
|
||||
const unscaledTop = -oldRect.top / oldScale;
|
||||
const unscaledLeft = -oldRect.left / oldScale;
|
||||
|
||||
const graph = svg.contentDocument.getElementById("graph0");
|
||||
graph.transform.baseVal[0].setScale(scale, scale);
|
||||
const root = svg.contentDocument.rootElement;
|
||||
const scaledWidth = origWidth * scale;
|
||||
const scaledHeight = origHeight * scale;
|
||||
root.setAttribute("width", `${scaledWidth}pt`);
|
||||
root.setAttribute("height", `${scaledHeight}pt`);
|
||||
root.setAttribute("viewBox", `0 0 ${scaledWidth} ${scaledHeight}`);
|
||||
|
||||
container.scrollTo(unscaledLeft * scale, unscaledTop * scale);
|
||||
}
|
||||
|
||||
document.getElementById("zoom-in").addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
const oldScale = scale;
|
||||
scale *= 1.25;
|
||||
updateSVG(oldScale);
|
||||
});
|
||||
|
||||
document.getElementById("zoom-out").addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
const oldScale = scale;
|
||||
scale *= 0.8;
|
||||
updateSVG(oldScale);
|
||||
});
|
27300
site_test/static/2025/version-7/graph.svg
Normal file
27300
site_test/static/2025/version-7/graph.svg
Normal file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 1.5 MiB |
Loading…
x
Reference in New Issue
Block a user