website
git clone https://git.pyrossh.dev/website
木 Personal website of pyrossh. Built with astrojs, shiki, vite.
eleventy.config.js
import { readFileSync, rmSync, writeFileSync } from "node:fs";
import path from "node:path";
import syntaxHighlight from "@11ty/eleventy-plugin-syntaxhighlight";
import pluginRss from "@11ty/eleventy-plugin-rss";
import { fileIcon, folderIcon, iconSpriteDefs } from "./src/_helpers/fileIcons.js";
import { discoverLocalRepos, findReadmeImagePaths } from "./src/_helpers/gitRepos.js";
// theme-switcher (src/components/theme-switcher.js) used to be a Web
// Component (via @elenajs/core + an @elenajs/ssr build-time transform
// that expanded it on every generated page). A stateless click-to-toggle
// button doesn't need a component framework for that: its markup is now
// static HTML baked directly into site-header.njk, and theme-switcher.js
// is a plain script (loaded with `defer` below) that just wires up the
// click handler by id once the DOM is parsed.
//
// Every page used to pick and choose its own subset of these via `css`/
// `extraCss` frontmatter (e.g. repo pages loading repos.css + markdown.css
// + diff2html.min.css, the homepage loading just home.css). Now that
// everything below is @scope'd (see shared.css/repos.css/markdown.css/
// workers/home.css), there's no naming-collision reason to keep them
// apart, so they're concatenated into one cacheable stylesheet every page
// loads — fewer render-blocking requests than 2-4 separate <link>s.
//
// issues-detail.css/issues-index.css/posts-index.css/only-bible-app*.css
// are deliberately left out — none of them are unscoped-safe. The
// issues-* pair is leftover from a page that never made it into this
// rewrite. The other three are fully superseded by inline styles on their
// own pages now (posts.njk, only-bible-app/*.njk have zero matching
// classes left) — merging them site-wide used to be harmless because
// each was the only stylesheet on its own page, but their unscoped bare-
// tag selectors (a plain `a {...}`, `h1 {...}`, etc., not `.some-class
// a`) would otherwise leak onto every other page's matching tags once
// bundled together, which is exactly what broke tools/interests links on
// the homepage — inherited from posts-index.css's `ul li a` rule matching
// any list of links, not just the post list it was meant for.
const CSS_FILES_TO_MERGE = [
"assets/css/shared.css",
"assets/css/repos.css",
"assets/css/markdown.css",
"assets/css/diff2html.min.css",
"assets/css/workers/home.css",
"assets/css/workers/cv.css",
];
export default function (eleventyConfig) {
eleventyConfig.addPassthroughCopy({ assets: "assets" });
eleventyConfig.addPassthroughCopy({
"node_modules/diff2html/bundles/css/diff2html.min.css": "assets/css/diff2html.min.css",
});
// htmx powers no-full-page-reload tab switching on repo pages (Readme/
// Commits/Files, Contents/History/Blame) — hx-select pulls #repo-content
// or #file-content out of the fetched static page and swaps it in place.
// Vendored locally rather than a CDN <script> so the build has no
// runtime network dependency.
eleventyConfig.addPassthroughCopy({
"node_modules/htmx.org/dist/htmx.min.js": "assets/vendor/htmx.min.js",
});
// README images referenced by a repo-relative path (e.g. `shots/shot1.png`)
// only render on GitHub because GitHub serves the file itself. Copying
// just the handful of files each README actually references to a `raw/`
// route keeps them real, loadable URLs without inlining every screenshot
// as base64 into the README's HTML (see resolveReadmeImages in
// gitRepos.js, which points <img> at these same paths).
for (const repo of discoverLocalRepos()) {
for (const relPath of findReadmeImagePaths(repo.readme, repo.dir)) {
eleventyConfig.addPassthroughCopy({
[path.join(repo.dir, relPath)]: `repos/${repo.id}/raw/${relPath}`,
});
}
}
eleventyConfig.addPlugin(syntaxHighlight);
eleventyConfig.addPlugin(pluginRss);
eleventyConfig.addFilter("readableDate", (value) =>
new Date(value).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" }),
);
eleventyConfig.addFilter("fileIcon", fileIcon);
eleventyConfig.addFilter("folderIcon", folderIcon);
// Nunjucks' built-in `selectattr` only checks truthiness of the named
// attribute — it has no Jinja2-style `equalto` test, so a third argument
// is silently ignored. These do the actual equality filtering in JS.
eleventyConfig.addFilter("filterByRepo", (entries, repoId) => entries.filter((e) => e.repoId === repoId));
eleventyConfig.addFilter("defaultFileEntry", (entries) => {
if (!entries.length) return null;
return entries.find((e) => e.file.path.toLowerCase() === "readme.md") ?? entries[entries.length - 1];
});
// A folder should start open only when the active file lives inside it —
// otherwise every directory in the tree defaults to expanded, which on a
// repo of any size is just a wall of open folders with nothing collapsed.
eleventyConfig.addFilter(
"isAncestorDir",
(dirPath, activePath) => !!activePath && (activePath === dirPath || activePath.startsWith(`${dirPath}/`)),
);
eleventyConfig.addFilter("formatBytes", (bytes) => {
if (!bytes) return "0B";
const units = ["B", "KB", "MB", "GB"];
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
const value = bytes / 1024 ** i;
return `${i === 0 ? value : value.toFixed(1)}${units[i]}`;
});
// Runs after passthrough copy has placed every file in CSS_FILES_TO_MERGE
// under dist/, so it reads the already-copied output rather than the
// assets/ source tree (that's also where diff2html.min.css lands, copied
// from node_modules above — there's no separate "source" copy of it).
eleventyConfig.on("eleventy.after", ({ dir }) => {
const outputDir = dir.output;
const merged = CSS_FILES_TO_MERGE.map((file) => readFileSync(path.join(outputDir, file), "utf8")).join("\n");
writeFileSync(path.join(outputDir, "assets/css/bundle.css"), merged);
for (const file of CSS_FILES_TO_MERGE) rmSync(path.join(outputDir, file));
// Written once here rather than inlined per page (see fileIcons.js) —
// every repo page's fileIcon()/folderIcon() <use> just points at this
// one shared file instead of shipping the ~30-icon sprite body again.
writeFileSync(path.join(outputDir, "assets/icons/sprite.svg"), iconSpriteDefs());
});
return {
markdownTemplateEngine: false,
dir: {
input: "src",
output: "dist",
},
};
}