CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Overview
Jekyll personal blog on GitHub Pages, with posts in English and Chinese. Pushing to master triggers the GitHub Pages build and deploy; there is no CI config in the repo. The site is dark by default with a light-mode toggle.
Commands
Tests are the verification that works with only Node installed (Node >= 18, jsdom pinned in package.json):
npm ci
npm test # node --test tests/site.test.cjs
node --test --test-name-pattern="theme" tests/site.test.cjs # run a single test by title
SITE_URL=https://wizna.github.io/ npm test # also runs the deployed-TOC test (skipped otherwise)
Jekyll:
bundle exec jekyll serve --livereload # add --drafts to preview drafts
bundle exec jekyll build
bundle exec jekyll doctor # permalink/config problems
Gemfile and Gemfile.lock are gitignored and absent from this checkout. Plugins (jekyll-paginate, jekyll-sitemap, jekyll-feed) are declared under plugins: in _config.yml.
A local build works on the macOS system Ruby (2.6.10), but the newest gems need Ruby 3+, so pin the versions that still support 2.6. This combination built the whole site (47 files) and matched the live HTML apart from the removed image script (checked on one post):
# Pre-pin the dependencies that dropped Ruby 2.6 support, one gem per call (gem rejects -v with several gems).
# Assembled from the steps that worked here; not re-run as a single script.
for g in ffi:1.15.5 i18n:1.14.8 public_suffix:4.0.7 jekyll:3.10.0 jekyll-paginate:1.1.0 kramdown-parser-gfm:1.1.0; do
gem install --user-install --no-document "$g"
done
export PATH="$HOME/.gem/ruby/2.6.0/bin:$PATH" # user-installed binaries are not on PATH by default
jekyll build --config _config.yml,/tmp/local.yml --destination /tmp/site-out
/tmp/local.yml is a throwaway override. Its plugins: list replaces the one in _config.yml, so list only jekyll-paginate (jekyll-sitemap and jekyll-feed are slow to install and irrelevant to UI checks). Add the two PDFs and assets/summary to exclude:. Add a defaults: entry that gives type: posts the layout post. GitHub Pages does this implicitly with its jekyll-default-layout plugin; a plain local build does not, so posts come out without the site chrome. Serve the output with python3 -I -m http.server --bind 127.0.0.1 --directory /tmp/site-out. Expect harmless “Liquid syntax error” warnings for `` through _includes/optimize-images.html, which adds decoding="async" and loading="lazy" to every <img> except the first one on the page, and appends size parameters to Pexels (auto=compress&cs=tinysrgb&w=800) and Unsplash (w=800&q=80) URLs that have none. It must stay in Liquid: the browser requests each src while parsing, so a script that rewrites src or loading afterwards cannot stop the first request and makes the browser fetch the image twice. Only images in content are processed; the header avatar sets its own attributes. The include only matches <img followed by a space and a src="..." attribute, so other spellings are left untouched.
Theming
assets/js/theme.jsloads synchronously in<head>, before the stylesheet. It setsdata-themeon<html>(defaultdark;localStorage['site-theme'] === 'light'switches it), updatesmeta[name=theme-color], and wires[data-theme-toggle]. That button is renderedhiddenand revealed by the script.- A toggle dispatches a
site:themechangeevent.assets/js/diagrams.jslistens to it to re-render Mermaid in the matching theme. - While the theme changes,
theme.jsputs.theme-switchingon<html>._sass/_site.scssuses it to turn everytransitionoff, because per-element colour transitions (links, nav, buttons) otherwise lag behind the instantly changing background. Withdocument.startViewTransitionthe page cross-fades instead, unless reduced motion is requested. The saved choice is written immediately, but the DOM changes inside the transition callback. Any new colour transition is covered automatically; do not add!importantoverrides that outrank.theme-switching. - All colors are CSS custom properties defined in
:root(dark) and:root[data-theme="light"]in_sass/_site.scss.bootstrap/css/directory.cssconsumes the same variables. Add new colors as variables with a value in both themes rather than hard-coding them.
Sass
style.scss contains only @imports (reset, variables, highlights, svg-icons, site). The real styles, including the search modal, are in _sass/_site.scss. _sass/_variables.scss ($blue, etc.) is legacy. _sass/_svg-icons.scss is ~54 KB of base64 SVG, so avoid reading it. bootstrap/css/directory.css is separate and linked from directory.html.
Directory page (Liquid -> JS)
directory.html (permalink /directory/, layout page) emits hidden <span class="post-direct" data-title data-url> elements, one per post, with the post URL as text. bootstrap/js/directory.js builds a tree from the URL path segments and renders Bootstrap 3 collapsible panels.
- Tree position comes from the span text; the link
hrefcomes fromdata-url, which isrelative_url-aware. - Collapse IDs are hex-encoded UTF-16 of the full path, so same-named subcategories and names differing only by case or punctuation stay unique.
- Titles and URLs go through
escapeHtml. Tests assert that markup in titles stays text. directory.jsis loaded on every page and does two jobs. It initializes Tipue search withminimumLength: 1(so single CJK characters work), and it builds the tree only when#directory-treeexists.
Search (Tipue, locally forked)
assets/tipuesearch/tipuesearch_content.jshas front matter, so Jekyll processes it as a Liquid template that emitsvar tipuesearch = {pages: [...]}from all ofsite.posts(title, text, tags, url).- The Tipue files are a locally maintained fork. Per
README.md, upstream is unreachable and the newest cdnjs build (5.0.0) is older. The fork adds Chinese substring matching, literal handling of regex punctuation, text-only rendering of titles and queries, and modal integration. Do not replace it with a CDN or upstream copy. - Tipue loads on every page because
_layouts/default.htmlitself setstipue_search_active: truein its front matter. The per-page flag documented elsewhere is effectively redundant. - The header search form opens the Bootstrap modal
#searchModaland renders results into#tipue_search_content.site-ui.jshandles focus return to the opener, sets the subtitle text, and appliesprefers-reduced-motion: it drops the modal’s.fadeand finishes collapses withbsTransitionEnd. Active collapses finish when the preference changes; new ones finish on the next task, after Bootstrap attaches its handlers. This avoids waiting for atransitionendthat the reduced-motion CSS removes. Keep$.support.transitionintact: Bootstrap’s pending fallback timers still read its.endproperty, so changing it mid-animation can throw and leave a collapse stuck. Do not patchbootstrap/; fix this kind of problem from our own scripts.
Tests
tests/site.test.cjs loads the real source files into jsdom with win.eval (jQuery, then Bootstrap, then site-ui.js / Tipue / directory.js / theme.js). Those files must stay classic scripts with no module syntax and no new dependencies beyond jQuery/Bootstrap. The tests/ folder and node_modules are in exclude, so they are not published. The deployed-TOC test hard-codes four post URLs, so renaming or moving those posts breaks it.
Third-party resources
| Resource | Where | Loading |
|---|---|---|
| MathJax 2.7.9 (cdnjs) | _includes/meta.html |
Every page; $...$ inline only, skips pre |
| Mermaid 10.6.1 (jsdelivr) | assets/js/diagrams.js |
Only when .language-mermaid / .mermaid nodes exist |
| Disqus | _includes/disqus.html |
post.html only |
| Google Analytics | _includes/analytics.html |
Every page |
| Bootstrap 3.3.7, jQuery 3.7.1 | bootstrap/ (vendored) |
Every page; do not use Bootstrap 4/5 APIs |
Local scripts and stylesheets are referenced with ?v=1794231208 for cache busting. Keep that when adding assets.
Jekyll configuration gotchas
- Dual permalinks: the global
permalink: /:title/applies to pages (about.md->/about/). Thepostscollection overrides it with/posts/:path/, so the folder path under_posts/is part of the URL. Moving a post changes its URL. post.categoriesis empty becausepostsis declared explicitly undercollections:. Derive categories frompost.path(index.htmlalready does this).- Pagination is 5 posts per page at
/page:num/.
Content
- New post:
_posts/<Category>/<Subcategory>/YYYY-MM-DD-title.md. Reuse the existing category folders (Coding/*,Handbook,Learning/*) and match their casing. - Front matter is mostly omitted. The title is derived from the filename. Add front matter only to override (
title:,excerpt:,exclude_from_search: true). - Table of contents: put
* TOCfollowed by{:toc}at the top of the post. The deployed test checks that#markdown-toclinks resolve and that no literal “TOC” list item remains. - Diagrams use a
mermaidcode fence. Math uses$...$. - Do not read
assets/Simple-Java.pdf,assets/paper.pdf, or the PDFs inassets/summary/.
Style and commits
- Four-space indentation in HTML, Liquid, and JS.
- Recent commits use conventional prefixes (
feat:,fix:); content usespost: add <title>. Keep the first line short and imperative. - Include screenshots in PRs that change navigation, the directory tree, the search modal, or either theme.
