This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Željko Filipin's personal blog at https://filipin.eu, built with Jekyll and hosted on GitHub Pages (via the CNAME file and github-pages gem). The jekyll-theme-hacker remote theme is applied on top of the default layout.
Serve the site locally at http://127.0.0.1:4000/:
bundle install
bundle exec jekyll s
Bundler is configured via .bundle/config to install gems into vendor/bundle.
No Ruby on the host, so the above won't work directly — use Docker. Build the site into an arbitrary destination (e.g. /tmp/site) with:
docker run --rm -v "$PWD:/w" -w /w -v /tmp:/tmp \
-e PAGES_REPO_NWO=zeljkofilipin/filipin.eu ruby:3.3 bash -c \
'git config --global --add safe.directory /w && \
bundle config set --local path vendor/bundle && \
bundle exec jekyll build -d /tmp/site'
PAGES_REPO_NWO is required because the github-pages gem resolves repo metadata from it; without the env var the build fails. git safe.directory is required because the container runs as root against the host-owned repo. The /tmp mount lets the build output land outside the repo (so .gitignore stays untouched).
Run the linter locally (requires Docker — runs github/super-linter against the repo; matches the GitHub Actions workflow in .github/workflows/linter.yml):
rake
Disabled super-linter validators are declared in .github/workflows/linter.yml (for CI) and mirrored in the Rakefile (for local runs). The Rakefile additionally disables TRIVY, which CI leaves on — locally it only flags a Dockerfile inside vendor/bundle. A .markdownlint.json is present (relaxes line-length, no-inline-html, single-h1), but since VALIDATE_MARKDOWN is off nothing currently runs against it.
_posts/<year>/YYYY-MM-DD-slug.md— blog posts. Posts are bucketed into per-year subdirectories (Jekyll recurses into_posts). Front matter needstags(space-separated) andtitle; thepostlayout is applied automatically via thedefaultsblock in_config.yml._tags/<tag>.md— one file per tag. Each has front mattername: <tag>and optional body content rendered on the tag page. Thetaglayout (_layouts/tag.html) lists all posts whose front mattertagsincludepage.name. Adding a new tag to a post requires creating a matching_tags/<tag>.mdfor the tag page to exist — enforced byscript/check-tags, wired into theLint Code Baseworkflow.index.md,blog.md,tags.md,license.md— top-level pages.blog.mdis the post index;tags.mdlists all tags with post counts._layouts/post.html— overrides the remote theme's post layout. Automatically prepends an estimated reading time (computed from word count at ~200 wpm) and appends a standard feedback footer to every post. Do not add reading time lines or## Feedbacksections to post files — the layout handles both._layouts/tag.html— custom tag layout; lists all posts for a given tag.assets/— images and other static files referenced by posts. Organized by year and post:assets/<year>/<post-slug>/<file>when a post has multiple assets, orassets/<year>/<post-slug>.<ext>when it has exactly one. Assets shared across posts within the same year live atassets/<year>/root; cross-year shared assets live under the earliest year that references them. Site-wide assets (homepage logo, author photo) stay atassets/root.permalink: /:titlein_config.ymlmeans post URLs are/<slug-from-title>not date-based. Changing a post's title will change its URL and break inbound links.
- Tags in the
tags:front matter field are kept alphabetically sorted. - Embed-only tags — add these only when the post actually embeds the content (iframe,
<video>, etc.), not merely links to it:commons,drive,facebook,strava,twitter,video,youtube. image— add when a post contains any visual (screenshot, photo, logo), whether via markdown![]()or HTML<img>. Formerly calledphoto.- Run
script/check-tags(orrake) to verify every tag used in a post has a matching_tags/<tag>.md.
.vscode/settings.json carries a large cSpell.words allowlist of names and domain terms (people, places, tools). When adding posts that mention new proper nouns, add them there to keep the VS Code spellchecker quiet.
- Markdown (~529 files —
_posts/,_tags/, top-level pages): super-linter validatorsVALIDATE_MARKDOWN(markdownlint) andVALIDATE_MARKDOWN_PRETTIER— both disabled - YAML (2 files —
_config.yml,.github/workflows/linter.yml): super-linter validatorVALIDATE_YAML— enabled - HTML (2 files —
_layouts/post.html,_layouts/tag.html): super-linter validatorsVALIDATE_HTMLandVALIDATE_HTML_PRETTIER— both disabled - JSON (4 files —
.markdownlint.json,.vscode/settings.json,.claude/settings.local.json,.devcontainer/devcontainer.json): super-linter validatorVALIDATE_JSON— enabled - Ruby (3 files —
Gemfile,Rakefile,script/check-tags): super-linter validatorVALIDATE_RUBY— disabled
- English — majority of posts; super-linter has
VALIDATE_NATURAL_LANGUAGE(textlint) andVALIDATE_SPELL_CODESPELL(codespell) — both disabled; neither supports Croatian anyway - Croatian — minority of posts; no super-linter validator; see spellchecking item in Tooling backlog
- Keep changes small — each change should be scoped so it can stand on its own as a single commit.
- No Ruby or Node on the host. Run linters and ad-hoc scripts via
docker run --rm -v "$PWD:/w" -w /w ruby:3.3 …or the equivalentnode:20image. Super-linter runs via the pinned image in theRakefile. - One change per turn — apply the edit, run CI locally (
rake), then hand it back for the user to commit. Never make git commits yourself, under any circumstances. - This repo is used on several machines. Anything worth remembering across sessions belongs in this file (CLAUDE.md), not in per-machine Claude memory — memory is local to one installation and will be invisible on the other machines.
For any refactoring — especially changes that rewrite refs across many posts (asset reorgs, slug renames, layout swaps) — build the site before and after and diff the output — the diff must contain only the intended change. A find-and-replace can silently hit text it shouldn't (e.g. a /assets/ in prose or a code block); the build diff is the ground-truth check, much stronger than browser spot-checks. Do this per commit, not batched.
Procedure: build into /tmp/filipin-old using the Docker command above, apply the change, build into /tmp/filipin-new, then diff -rq /tmp/filipin-old /tmp/filipin-new.
Jekyll output contains timestamp lines (article:published_time, schema.org JSON-LD dateModified/datePublished) that reflect build time, so tag pages (_site/tags/*.html) and feed.xml always differ between two builds — ~38 spurious diffs per build. Filter them with sed -E 's/20[0-9]{2}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}[+-][0-9]{2}:[0-9]{2}/TS/g' on both sides before comparing. After filtering, the only differing files should be the posts you touched, and every differing line should contain the token you intended to change.
Converting remaining HTML to Markdown in _posts/, one tag type per commit — all affected files for that tag in a single commit, not one file per commit. Counts are raw tag occurrences across all posts (surveyed 2026-04-24).
-
<code>(21) — inline code →`text`(skip occurrences inside HTML<td>cells) -
<blockquote>(14) — all are Twitter tweet embeds, not content; keep as HTML -
<strong>(6) — all inside SlideShare embed HTML; keep as HTML -
<em>(3) →*text* -
<br>(2) → removed (one split into two paragraphs, one was stray inside a code block) -
<a>(225) →[text](url)where cleanly convertible -
<ul>/<li>(27) → Markdown list syntax - HTML tables (~10 tables in 6 posts) → Markdown pipe tables:
2009/ruby-mail-on-cruby-jruby-and-ironruby.md(1),2010/ruby-mail-and-benchmark-rb-on-cruby-jruby-ironruby-and-rubyinstaller.md(3),2012/page-object-api-should-be-more-similar-to-watir-api.md(1),2010/postfix-logs-bash.md(2, have<code>in cells),2010/two-years-of-watir-podcast.md(1),2007/repeat-feature-of-microsoft-excel.md(2) -
<span>(~15 occurrences in ~7 posts, 2006–2008 era) — Blogger export artifacts (lang="EN-GB",class="entry-author-name", etc.); strip tags, keep content -
<h4>(2 occurrences) — both are inside{% highlight html %}code blocks in2007/extend-watir.mdand2007/find-element-by-xpath.md; no conversion needed - Removable
<div>(2 cases): watermark comment in2024/podcasts-in-2024.mdremoved;2012/watir-nokogiri.mddivs were already inside{% highlight html %}block
<iframe>(280) — embedded YouTube/Facebook, no Markdown equivalent<img>(35) — already handled what's possible; remaining have width/height/class or Wikimedia Commons attribution<script>(18) — social embed scripts<div>— SlideShare attribution (3 posts), Facebook SDK (6 posts, 2015), YouTube wrapper (2017)<p>(47) — remove opportunistically when rewriting surrounding content
Surveyed 2026-04-23. Items marked broken are likely already visible to readers.
- Old image references (~37 posts, mostly 2005–2006 era): malformed image syntax from pre-Jekyll migration. Images are probably not rendering in those posts.
Feed (: verified working; fixed double-slash URLs by removing trailing slash from/feed.xml)urlin_config.yml.- Missing code blocks (
HIGHLIGHTPLACEHOLDER):_posts/2009/2009-12-24-ruby-mail-on-cruby-jruby-and-ironruby.mdand_posts/2010/2010-01-05-ruby-mail-and-benchmark-rb-on-cruby-jruby-ironruby-and-rubyinstaller.mdcontainHIGHLIGHTPLACEHOLDER2ENDPLACEHOLDER/HIGHLIGHTPLACEHOLDER3ENDPLACEHOLDERstrings where code blocks should be. Present since the first commit — original content must be recovered from the live blog or another source.
- No post descriptions: 0 of 487 posts have a
descriptionfield in front matter. Affects SEO and social preview cards (Open Graph). Start with the most-visited posts. - Orphaned assets: ~288 image files in
assets/are never referenced by any post. Could be deleted to reduce repo size (verify before bulk delete).
: deleted — no post used the_tags/TODO.mdTODOtag.- Thin tags:
chess,commons,scratch(handful of posts each). Either write more in those areas or retire the tags.
- No pagination on
blog.md: all 487 posts render on one page. Will get slower as the blog grows; consider adding Jekyll pagination. - Dark/light mode toggle: current
jekyll-theme-hackerhas no runtime toggle. Consider migrating to a theme that supports it — Chirpy (jekyll-theme-chirpy) is the strongest candidate: has a built-in toggle, works asremote_themeon GitHub Pages, and handles tags and reading time. Migration is non-trivial (different front matter conventions and layout structure).
- Markdown linting: enable
VALIDATE_MARKDOWNin super-linter (remove thefalsefrom bothlinter.ymlandRakefile)..markdownlint.jsonis already present. Before enabling, first expand the config to disable false-positive rules, then fix remaining real errors one rule per commit (same approach as HTML cleanup).- Step 1 — disable false-positive rules in
.markdownlint.json:MD041(first-line-heading — posts start with body text, not headings),MD022/MD018/MD023(heading rules — fire on Ruby#comments inside{% highlight %}blocks, which markdownlint can't distinguish from headings),MD024(no-duplicate-heading — same heading in different posts/sections is fine). - Step 2 — fix real errors (1184 before / 1012 after disabling false positives; surveyed 2026-04-24), one rule per commit:
- MD060 table-column-style (472, all in one file) — disabled: rule conflicts with empty cells in data tables; no rendered difference
- MD010 no-hard-tabs (151) — disabled: tabs are inside
{% highlight %}code blocks; replacing them changes rendered indentation in code samples - MD036 emphasis-as-heading (118) —
**bold**standalone lines →##; italic book quotes →>blockquotes; italic asides suppressed withmarkdownlint-disable-next-line - MD034 bare-urls (64) — disabled: 39 of 64 are inside
{% highlight %}blocks (false positives); remaining 25 real prose cases need case-by-case judgment (<url>vs backticks vs[text](url)) — worth fixing separately per post - MD030 list-marker-space (62) — auto-fixed prose; 3 code-block false positives in
rubocop.mdsuppressed withmarkdownlint-disable - MD047 single-trailing-newline (30) — auto-fixed; no rendered diff
- MD028 no-blanks-blockquote (24 — blank lines inside
>blockquotes) - MD032 blanks-around-lists (22 — auto-fixable)
- MD026 no-trailing-punctuation (19 — trailing
!/?in headings) - MD039 no-space-in-links (13 — auto-fixable)
- MD038 no-space-in-code (9 — auto-fixable)
- MD011 no-reversed-links (8 —
(text)[url]→[text](url)) - MD045 no-alt-text (5 — add alt text to images)
- MD012 no-multiple-blanks (5 — auto-fixable)
- MD059 descriptive-link-text (3 — "click here" style links)
- MD049 emphasis-style (2 —
*vs_consistency) - MD027 no-multiple-space-blockquote (2 — auto-fixable)
- MD009 no-trailing-spaces (1 — auto-fixable)
- MD004 ul-style (1 — unordered list marker style)
- MD001 heading-increment (1 — heading level skips)
- Step 3 — enable
VALIDATE_MARKDOWNinlinter.ymlandRakefile.
- Step 1 — disable false-positive rules in
- Spellchecking (English + Croatian): add cspell as a separate CI step (
streetsidesoftware/cspell-action) with English (default) and Croatian (@cspell/dict-hr) dictionaries. Extract the allowlist from.vscode/settings.jsoninto a sharedcspell.config.jsonat the repo root so VS Code and CI use the same config. Options to decide before implementing: (a) which files to check (_posts/only, or all.md); (b) how to handle front matter and Liquid/HTML blocks; (c) whether to also wire it into theRakefilefor local runs.