website

#astro#js#html#css

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",
    },
  };
}