Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Gemfile.lock
# Jekyll cache
.jekyll-cache
.jekyll-metadata
.jektex-cache
_site

# RubyGems
Expand Down
113 changes: 113 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# robotchinwag.com

Personal Jekyll blog built on the [Chirpy](https://github.com/cotes2020/jekyll-theme-chirpy)
theme, consumed as a **gem** (`jekyll-theme-chirpy` in `Gemfile`) — the theme source is *not*
vendored into this repo. Deployed to GitHub Pages via `.github/workflows/pages-deploy.yml`
(Ruby 3.3, `jekyll b` then `htmlproofer`).

## Build & test

```bash
bundle install
JEKYLL_ENV=production bundle exec jekyll b -d _site
bundle exec htmlproofer _site --disable-external \
--ignore-urls "/^http:\/\/127.0.0.1/,/^http:\/\/0.0.0.0/,/^http:\/\/localhost/"
# or: make build_and_test
# dev server: bash tools/run.sh
```

Requires Ruby ≥ 3.1 and < 4.0 (Chirpy pins `~> 3.1`) plus a JS runtime for `execjs`
(Node is fine). On macOS the system Ruby (2.6) is too old — use
`brew install ruby@3.4` and prepend `/opt/homebrew/opt/ruby@3.4/bin` to `PATH`.

## Math rendering: server-side KaTeX (not the theme default)

Chirpy ships **MathJax** rendered client-side. This site replaces it with **KaTeX rendered
at build time** via the [`jektex`](https://github.com/yagarea/jektex) plugin, applying the
approach from [cotes2020/jekyll-theme-chirpy#2603](https://github.com/cotes2020/jekyll-theme-chirpy/pull/2603)
locally (the upstream PR was not merged).

Pieces involved:

| File | Role |
|---|---|
| `Gemfile` | `jektex` in `:jekyll_plugins` |
| `_config.yml` | `math.engine: katex` (read by the include overrides) and a `jektex:` block (cache dir, ignore globs, macros) |
| `_includes/head.html` | **Override** of the theme file — adds the KaTeX stylesheet on `math: true` pages when `math.engine == 'katex'` |
| `_includes/js-selector.html` | **Override** of the theme file — skips the MathJax `<script>` tags when `math.engine == 'katex'` |
| `_plugins/katex-inline-math.rb` | Pre-render hook: rewrites single-dollar inline `$…$` → kramdown's `$$…$$` on pages with `math: true` (skips fenced code blocks). Needed because kramdown only recognises `$$…$$`; the theme's MathJax config previously papered over this in the browser. |
| `assets/css/jekyll-theme-chirpy.scss` | `.katex-display` overflow/padding so wide equations scroll instead of overflowing |
| `.gitignore` | `.jektex-cache/` |

Pipeline at build time:

1. `_plugins/katex-inline-math.rb` (`:pre_render`) — `$x$` → `$$x$$` on `math: true` pages.
2. kramdown converts `$$x$$` → `\(x\)` (inline) / `\[x\]` (block) in the rendered HTML.
3. `jektex` (`:post_render`) — replaces `\(…\)` / `\[…\]` with KaTeX HTML using a bundled
KaTeX JS bundle via `execjs`. Caches results in `.jektex-cache/`.
4. The KaTeX **stylesheet** (CDN, `katex@0.16.9`, with SRI hash) is what makes the markup
render correctly — keep its version aligned with the KaTeX bundled inside `jektex`.

Switching back to MathJax is a one-line change: set `math.engine:` to `mathjax` (or empty)
in `_config.yml`. The include overrides preserve the original MathJax branch.

### Authoring rules for math posts

- Set `math: true` in front matter (per Chirpy convention).
- Block math: `$$ … $$` on its own lines with blank lines around it.
- Inline math: either `$…$` or `$$…$$` — both work; the plugin normalises the former.
- Don't use `\label` / `\eqref` / `\tag` — KaTeX doesn't support them.
- Avoid a bare `$` inside fenced/inline code on `math: true` posts; escape it as `\$` if
you must (the plugin skips fenced blocks, but inline code is not parsed).

## Updating the Chirpy theme version

`_includes/head.html` and `_includes/js-selector.html` are **vendored copies** of the
theme's templates with a small KaTeX patch. They shadow the gem's versions, so when you
bump `jekyll-theme-chirpy` you must re-sync them or you'll silently miss upstream fixes.

Procedure:

1. Bump the version constraint in `Gemfile`, run `bundle update jekyll-theme-chirpy`, and
note the resolved version (`bundle show jekyll-theme-chirpy` or check `Gemfile.lock`).
2. Fetch the new upstream templates for that tag:
```bash
V=v7.6.0 # the resolved version
curl -sL https://raw.githubusercontent.com/cotes2020/jekyll-theme-chirpy/$V/_includes/head.html -o /tmp/head.html
curl -sL https://raw.githubusercontent.com/cotes2020/jekyll-theme-chirpy/$V/_includes/js-selector.html -o /tmp/js-selector.html
```
3. Diff them against the local overrides to see what upstream changed:
```bash
diff /tmp/head.html _includes/head.html
diff /tmp/js-selector.html _includes/js-selector.html
```
4. Replace the local files with the fresh upstream copies, then **re-apply the two KaTeX
patches** (each is marked by a `{% comment %}` header at the top of the file noting the
upstream version it tracks — update that version string too):
- `head.html`: the `{% if page.math and math_engine == 'katex' %}` block that links the
KaTeX CSS, inserted just before `<!-- Scripts -->`.
- `js-selector.html`: the `{% assign math_engine … %}{% if math_engine == 'mathjax' %}`
wrapper around the existing MathJax `<script>` tags inside `{% if page.math %}`.
5. Check whether upstream changed how math is wired (e.g. moved the MathJax block, added a
`mathjax.js` config option, or — best case — merged native KaTeX support, in which case
delete these overrides and the plugin and use the theme's config instead).
6. Rebuild and run `htmlproofer`. Spot-check a math-heavy post (e.g.
`the-tensor-calculus-you-need-for-deep-learning`) and confirm:
- `.katex` spans present, **zero** `.katex-error` elements,
- no `MathJax`/`mathjax` script tags,
- `katex.min.css` linked,
- no leftover raw `$…$` / `\(…\)` / `\[…\]` outside `<code>`/`<pre>`/`<script>`.
7. If you also bump `jektex`, check what KaTeX version it bundles and update the
`katex@<version>` CDN URL + SRI `integrity` hash in `head.html`:
```bash
curl -sL https://cdn.jsdelivr.net/npm/katex@<version>/dist/katex.min.css \
| openssl dgst -sha384 -binary | openssl base64 -A
```

## Other local theme customisations

- `assets/css/jekyll-theme-chirpy.scss` — sidebar avatar sizing, hides "recently
updated"/"trending tags" panel, hides post prev/next nav, mobile topbar title, table
list wrapping, KaTeX overflow.
- `_includes/comments/` — Giscus comment integration override.
- `_plugins/posts-lastmod-hook.rb` — sets `last_modified_at` from git history.
6 changes: 6 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ source "https://rubygems.org"

gem "jekyll-theme-chirpy", "~> 7.4", ">= 7.4.1"

# Server-side KaTeX rendering (replaces client-side MathJax).
# See: https://github.com/cotes2020/jekyll-theme-chirpy/pull/2603
group :jekyll_plugins do
gem "jektex", "~> 0.1.1"
end

gem "html-proofer", "~> 5.0", group: :test

platforms :mingw, :x64_mingw, :mswin, :jruby do
Expand Down
18 changes: 18 additions & 0 deletions _config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,24 @@ social_preview_image: # string, local or CORS resources
# boolean type, the global switch for TOC in posts.
toc: true

# Math equation rendering engine.
# mathjax — client-side rendering (theme default, loads JS in the browser)
# katex — server-side rendering at build time via the jektex plugin (no JS)
# Used by the local _includes/{head,js-selector}.html overrides.
math:
engine: katex

# Jektex (server-side KaTeX) configuration › https://github.com/yagarea/jektex
# Math is written with kramdown's `$$ ... $$` delimiters; the local plugin
# `_plugins/katex-inline-math.rb` also upgrades single-`$` inline math on
# pages with `math: true` so existing posts keep working.
jektex:
cache_dir: ".jektex-cache"
# Skip non-content output (feeds, manifests, generated JS/CSS templates).
ignore: ["*.xml", "*.json", "*.js", "*.scss", "*.css", "*.txt"]
silent: false
macros: []

comments:
# Global switch for the post comment system. Keeping it empty means disabled.
provider: giscus # [disqus | utterances | giscus]
Expand Down
157 changes: 157 additions & 0 deletions _includes/head.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
{%- comment -%}
Local override of the Chirpy theme's _includes/head.html (v7.5.0).
Adds the KaTeX stylesheet when `site.math.engine == 'katex'` so that
jektex-rendered equations are styled. Keep in sync with the upstream file
when bumping the theme version.
See: https://github.com/cotes2020/jekyll-theme-chirpy/pull/2603
{%- endcomment -%}
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#f7f7f7">
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#1b1b1e">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta
name="viewport"
content="width=device-width, user-scalable=no initial-scale=1, shrink-to-fit=no, viewport-fit=cover"
>

{%- capture seo_tags -%}
{% seo title=false %}
{%- endcapture -%}

<!-- Setup Open Graph image -->

{% if page.image %}
{% assign src = page.image.path | default: page.image %}

{% unless src contains '://' %}
{%- capture img_url -%}
{% include media-url.html src=src subpath=page.media_subpath absolute=true %}
{%- endcapture -%}

{%- capture old_url -%}{{ src | absolute_url }}{%- endcapture -%}
{%- capture new_url -%}{{ img_url }}{%- endcapture -%}

{% assign seo_tags = seo_tags | replace: old_url, new_url %}
{% endunless %}

{% elsif site.social_preview_image %}
{%- capture img_url -%}
{% include media-url.html src=site.social_preview_image absolute=true %}
{%- endcapture -%}

{%- capture og_image -%}
<meta property="og:image" content="{{ img_url }}" />
{%- endcapture -%}

{%- capture twitter_image -%}
<meta name="twitter:card" content="summary_large_image" />
<meta property="twitter:image" content="{{ img_url }}" />
{%- endcapture -%}

{% assign old_meta_clip = '<meta name="twitter:card" content="summary" />' %}
{% assign new_meta_clip = og_image | append: twitter_image %}
{% assign seo_tags = seo_tags | replace: old_meta_clip, new_meta_clip %}
{% endif %}

{{ seo_tags }}

{%- if site.social.fediverse_handle %}
<!-- Fediverse handle/creator -->
<meta name="fediverse:creator" content="{{ site.social.fediverse_handle }}">
{% endif %}

<title>
{%- unless page.layout == 'home' -%}
{%- capture title -%}
{%- if page.collection == 'tabs' -%}
{%- assign tab_key = page.title | downcase -%}
{{- site.data.locales[include.lang].tabs[tab_key] -}}
{%- else -%}
{{- page.title -}}
{%- endif -%}
{%- endcapture -%}
{{- title | append: ' | ' -}}
{%- endunless -%}
{{- site.title -}}
</title>

{% include_cached favicons.html %}

<!-- Resource Hints -->
{% unless site.assets.self_host.enabled %}
{% for hint in site.data.origin.cors.resource_hints %}
{% for link in hint.links %}
<link rel="{{ link.rel }}" href="{{ hint.url }}" {{ link.opts | join: ' ' }}>
{% endfor %}
{% endfor %}
{% endunless %}

<!-- Bootstrap -->
{% unless jekyll.environment == 'production' %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css">
{% endunless %}

<!-- Theme style -->
<link rel="stylesheet" href="{{ '/assets/css/:THEME.css' | replace: ':THEME', site.theme | relative_url }}">

<!-- Web Font -->
<link rel="stylesheet" href="{{ site.data.origin[type].webfonts | relative_url }}">

<!-- Font Awesome Icons -->
<link rel="stylesheet" href="{{ site.data.origin[type].fontawesome.css | relative_url }}">

<!-- 3rd-party Dependencies -->

{% if site.toc and page.toc %}
<link rel="stylesheet" href="{{ site.data.origin[type].toc.css | relative_url }}">
{% endif %}

{% if page.layout == 'post' or page.layout == 'page' or page.layout == 'home' %}
<link rel="stylesheet" href="{{ site.data.origin[type]['lazy-polyfill'].css | relative_url }}">
{% endif %}

{% if page.layout == 'page' or page.layout == 'post' %}
<!-- Image Popup -->
<link rel="stylesheet" href="{{ site.data.origin[type].glightbox.css | relative_url }}">
{% endif %}

{% assign math_engine = site.math.engine | default: 'mathjax' %}
{% if page.math and math_engine == 'katex' %}
<!-- KaTeX CSS for server-side rendered math (jektex) -->
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css"
integrity="sha384-n8MVd4RsNIU0tAv4ct0nTaAbDJwPJzDEaqSD1odI+WdtXRGWt2kTvGFasHpSy3SV"
crossorigin="anonymous"
>
{% endif %}

<!-- Scripts -->

<script src="{{ '/assets/js/dist/theme.min.js' | relative_url }}"></script>

{% include js-selector.html lang=lang %}

{% if jekyll.environment == 'production' %}
<!-- PWA -->
{% if site.pwa.enabled %}
<script
defer
src="{{ '/app.min.js' | relative_url }}?baseurl={{ site.baseurl | default: '' }}&register={{ site.pwa.cache.enabled }}"
></script>
{% endif %}

<!-- Web Analytics -->
{% for analytics in site.analytics %}
{% capture str %}{{ analytics }}{% endcapture %}
{% assign platform = str | split: '{' | first %}
{% if site.analytics[platform].id and site.analytics[platform].id != empty %}
{% include analytics/{{ platform }}.html %}
{% endif %}
{% endfor %}
{% endif %}

{% include metadata-hook.html %}
</head>
Loading