website

#astro#js#html#css

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
}