website
git clone https://git.pyrossh.dev/website
木 Personal website of pyrossh. Built with astrojs, shiki, vite.
eleventy.config.js
| 44991dd | 1 | import { readFileSync, rmSync, writeFileSync } from "node:fs"; |
| 44991dd | 2 | import path from "node:path"; |
| 3c79a68 | 3 | import syntaxHighlight from "@11ty/eleventy-plugin-syntaxhighlight"; |
| 3c79a68 | 4 | import pluginRss from "@11ty/eleventy-plugin-rss"; |
| 3931c4d | 5 | import { fileIcon, folderIcon, iconSpriteDefs } from "./src/_helpers/fileIcons.js"; |
| 1c2fa8e | 6 | import { discoverLocalRepos, findReadmeImagePaths } from "./src/_helpers/gitRepos.js"; |
| 3c79a68 | 7 | |
| ada6e53 | 8 | // theme-switcher (src/components/theme-switcher.js) used to be a Web |
| ada6e53 | 9 | // Component (via @elenajs/core + an @elenajs/ssr build-time transform |
| ada6e53 | 10 | // that expanded it on every generated page). A stateless click-to-toggle |
| ada6e53 | 11 | // button doesn't need a component framework for that: its markup is now |
| ada6e53 | 12 | // static HTML baked directly into site-header.njk, and theme-switcher.js |
| ada6e53 | 13 | // is a plain script (loaded with `defer` below) that just wires up the |
| ada6e53 | 14 | // click handler by id once the DOM is parsed. |
| ada6e53 | 15 | // |
| 44991dd | 16 | // Every page used to pick and choose its own subset of these via `css`/ |
| 44991dd | 17 | // `extraCss` frontmatter (e.g. repo pages loading repos.css + markdown.css |
| 44991dd | 18 | // + diff2html.min.css, the homepage loading just home.css). Now that |
| 44991dd | 19 | // everything below is @scope'd (see shared.css/repos.css/markdown.css/ |
| 44991dd | 20 | // workers/home.css), there's no naming-collision reason to keep them |
| 44991dd | 21 | // apart, so they're concatenated into one cacheable stylesheet every page |
| 44991dd | 22 | // loads — fewer render-blocking requests than 2-4 separate <link>s. |
| 44991dd | 23 | // |
| 44991dd | 24 | // issues-detail.css/issues-index.css/posts-index.css/only-bible-app*.css |
| 44991dd | 25 | // are deliberately left out — none of them are unscoped-safe. The |
| 44991dd | 26 | // issues-* pair is leftover from a page that never made it into this |
| 44991dd | 27 | // rewrite. The other three are fully superseded by inline styles on their |
| 44991dd | 28 | // own pages now (posts.njk, only-bible-app/*.njk have zero matching |
| 44991dd | 29 | // classes left) — merging them site-wide used to be harmless because |
| 44991dd | 30 | // each was the only stylesheet on its own page, but their unscoped bare- |
| 44991dd | 31 | // tag selectors (a plain `a {...}`, `h1 {...}`, etc., not `.some-class |
| 44991dd | 32 | // a`) would otherwise leak onto every other page's matching tags once |
| 44991dd | 33 | // bundled together, which is exactly what broke tools/interests links on |
| 44991dd | 34 | // the homepage — inherited from posts-index.css's `ul li a` rule matching |
| 44991dd | 35 | // any list of links, not just the post list it was meant for. |
| 44991dd | 36 | const CSS_FILES_TO_MERGE = [ |
| 44991dd | 37 | "assets/css/shared.css", |
| 44991dd | 38 | "assets/css/repos.css", |
| 44991dd | 39 | "assets/css/markdown.css", |
| 44991dd | 40 | "assets/css/diff2html.min.css", |
| 44991dd | 41 | "assets/css/workers/home.css", |
| 44991dd | 42 | "assets/css/workers/cv.css", |
| 44991dd | 43 | ]; |
| 3c79a68 | 44 | |
| 3c79a68 | 45 | export default function (eleventyConfig) { |
| 3c79a68 | 46 | eleventyConfig.addPassthroughCopy({ assets: "assets" }); |
| 3c79a68 | 47 | eleventyConfig.addPassthroughCopy({ |
| 3c79a68 | 48 | "node_modules/diff2html/bundles/css/diff2html.min.css": "assets/css/diff2html.min.css", |
| 3c79a68 | 49 | }); |
| 3c79a68 | 50 | |
| 3c79a68 | 51 | // htmx powers no-full-page-reload tab switching on repo pages (Readme/ |
| 3c79a68 | 52 | // Commits/Files, Contents/History/Blame) — hx-select pulls #repo-content |
| 3c79a68 | 53 | // or #file-content out of the fetched static page and swaps it in place. |
| 3c79a68 | 54 | // Vendored locally rather than a CDN <script> so the build has no |
| 3c79a68 | 55 | // runtime network dependency. |
| 3c79a68 | 56 | eleventyConfig.addPassthroughCopy({ |
| 3c79a68 | 57 | "node_modules/htmx.org/dist/htmx.min.js": "assets/vendor/htmx.min.js", |
| 3c79a68 | 58 | }); |
| 3c79a68 | 59 | |
| 1c2fa8e | 60 | // README images referenced by a repo-relative path (e.g. `shots/shot1.png`) |
| 1c2fa8e | 61 | // only render on GitHub because GitHub serves the file itself. Copying |
| 1c2fa8e | 62 | // just the handful of files each README actually references to a `raw/` |
| 1c2fa8e | 63 | // route keeps them real, loadable URLs without inlining every screenshot |
| 1c2fa8e | 64 | // as base64 into the README's HTML (see resolveReadmeImages in |
| 1c2fa8e | 65 | // gitRepos.js, which points <img> at these same paths). |
| 1c2fa8e | 66 | for (const repo of discoverLocalRepos()) { |
| 1c2fa8e | 67 | for (const relPath of findReadmeImagePaths(repo.readme, repo.dir)) { |
| 1c2fa8e | 68 | eleventyConfig.addPassthroughCopy({ |
| 1c2fa8e | 69 | [path.join(repo.dir, relPath)]: `repos/${repo.id}/raw/${relPath}`, |
| 1c2fa8e | 70 | }); |
| 1c2fa8e | 71 | } |
| 1c2fa8e | 72 | } |
| 1c2fa8e | 73 | |
| 3c79a68 | 74 | eleventyConfig.addPlugin(syntaxHighlight); |
| 3c79a68 | 75 | eleventyConfig.addPlugin(pluginRss); |
| 3c79a68 | 76 | |
| 3c79a68 | 77 | eleventyConfig.addFilter("readableDate", (value) => |
| 3c79a68 | 78 | new Date(value).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" }), |
| 3c79a68 | 79 | ); |
| 3c79a68 | 80 | eleventyConfig.addFilter("fileIcon", fileIcon); |
| 3c79a68 | 81 | eleventyConfig.addFilter("folderIcon", folderIcon); |
| 3931c4d | 82 | // Nunjucks' built-in `selectattr` only checks truthiness of the named |
| 3931c4d | 83 | // attribute — it has no Jinja2-style `equalto` test, so a third argument |
| 3931c4d | 84 | // is silently ignored. These do the actual equality filtering in JS. |
| 3931c4d | 85 | eleventyConfig.addFilter("filterByRepo", (entries, repoId) => entries.filter((e) => e.repoId === repoId)); |
| 3931c4d | 86 | eleventyConfig.addFilter("defaultFileEntry", (entries) => { |
| 3931c4d | 87 | if (!entries.length) return null; |
| 3931c4d | 88 | return entries.find((e) => e.file.path.toLowerCase() === "readme.md") ?? entries[entries.length - 1]; |
| 3931c4d | 89 | }); |
| 3c79a68 | 90 | // A folder should start open only when the active file lives inside it — |
| 3c79a68 | 91 | // otherwise every directory in the tree defaults to expanded, which on a |
| 3c79a68 | 92 | // repo of any size is just a wall of open folders with nothing collapsed. |
| 3c79a68 | 93 | eleventyConfig.addFilter( |
| 3c79a68 | 94 | "isAncestorDir", |
| 3c79a68 | 95 | (dirPath, activePath) => !!activePath && (activePath === dirPath || activePath.startsWith(`${dirPath}/`)), |
| 3c79a68 | 96 | ); |
| 3c79a68 | 97 | eleventyConfig.addFilter("formatBytes", (bytes) => { |
| 3c79a68 | 98 | if (!bytes) return "0B"; |
| 3c79a68 | 99 | const units = ["B", "KB", "MB", "GB"]; |
| 3c79a68 | 100 | const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1); |
| 3c79a68 | 101 | const value = bytes / 1024 ** i; |
| 3c79a68 | 102 | return `${i === 0 ? value : value.toFixed(1)}${units[i]}`; |
| 3c79a68 | 103 | }); |
| 44991dd | 104 | // Runs after passthrough copy has placed every file in CSS_FILES_TO_MERGE |
| 44991dd | 105 | // under dist/, so it reads the already-copied output rather than the |
| 44991dd | 106 | // assets/ source tree (that's also where diff2html.min.css lands, copied |
| 44991dd | 107 | // from node_modules above — there's no separate "source" copy of it). |
| 44991dd | 108 | eleventyConfig.on("eleventy.after", ({ dir }) => { |
| 44991dd | 109 | const outputDir = dir.output; |
| 44991dd | 110 | const merged = CSS_FILES_TO_MERGE.map((file) => readFileSync(path.join(outputDir, file), "utf8")).join("\n"); |
| 44991dd | 111 | writeFileSync(path.join(outputDir, "assets/css/bundle.css"), merged); |
| 44991dd | 112 | for (const file of CSS_FILES_TO_MERGE) rmSync(path.join(outputDir, file)); |
| 1980b5c | 113 | |
| 1980b5c | 114 | // Written once here rather than inlined per page (see fileIcons.js) — |
| 1980b5c | 115 | // every repo page's fileIcon()/folderIcon() <use> just points at this |
| 1980b5c | 116 | // one shared file instead of shipping the ~30-icon sprite body again. |
| 1980b5c | 117 | writeFileSync(path.join(outputDir, "assets/icons/sprite.svg"), iconSpriteDefs()); |
| 44991dd | 118 | }); |
| 44991dd | 119 | |
| 3c79a68 | 120 | return { |
| 3c79a68 | 121 | markdownTemplateEngine: false, |
| 3c79a68 | 122 | dir: { |
| 3c79a68 | 123 | input: "src", |
| 3c79a68 | 124 | output: "dist", |
| 3c79a68 | 125 | }, |
| 3c79a68 | 126 | }; |
| 3c79a68 | 127 | } |