jekyll-site-dev

Development reference for the alexgude.com Jekyll site. Use when modifying plugins in _plugins/, writing or editing content in _posts/ or _books/, working with the markdown output pipeline, book famil

SKILL.md

GitHub

Jekyll site development

Use this skill to locate the authoritative implementation notes, policy guides, tests, and verification commands for the alexgude.com Jekyll site. The site runs Jekyll in Docker. Run site operations through make; do not run jekyll or bundle directly.

Start with the relevant source of truth

Most subsystem documentation lives next to the code it describes. Use the following order when investigating a change:

  1. Read this file for the stable architecture and workflow overview.
  2. Read the relevant code-resident documentation for implementation behavior.
  3. Read the relevant policy reference for authoring or operational rules.
  4. Read the matching tests and their shared helper before changing behavior.
  5. Run the narrowest relevant check, then the full checks required by the change.

Code comments and tests are authoritative for implementation behavior. This skill summarizes where to find them; do not duplicate volatile implementation details here.

Query code-resident documentation

Custom YARD tags declared in .yardopts mark documentation that belongs with the code:

Tag Use it for
@validator Build validators, the data errors they catch, and their fatal failures.
@pipeline The stages and data flow of a processing pipeline.
@pattern Reusable implementation and testing patterns.
@gotcha Non-obvious behavior that can cause subtle bugs.

List all tagged objects:

make doc-index
make doc-index QUERY='has_tag?(:gotcha)'

Display one object’s full docstring:

make doc-show OBJ=Jekyll::Books::Core::BookLinkResolver
make doc-show OBJ='Jekyll::Books::Core::BookLinkResolver#resolve'

For a top-level method, use OBJ='#method_name'. A constant, class method, or instance method can be passed by its full object path. Reading the source file reported by the index is equivalent when that is easier.

The skill-doc tests keep these pointers current:

  • _tests/src/test_skill_docs_yard_objects.rb checks every referenced tag and YARD object.
  • _tests/src/test_skill_docs_paths.rb checks every referenced repository path.

Repository map

  • _posts/ contains blog posts.
  • _books/ contains the books collection and book reviews.
  • _layouts/ and _includes/ define page structure and shared markup.
  • _plugins/src/ contains the plugins, organized by domain:
    • infrastructure/ contains low-level utilities, text and URL handling, the link cache, generated static files, and markdown formatting helpers.
    • ui/ contains reusable cards, ratings, citations, and markdown-card components.
    • content/ contains domain logic for books, posts, authors, series, and markdown output.
    • seo/ contains JSON-LD, SEO metadata, and standard.site output.
  • _tests/ mirrors _plugins/src/ for plugin tests. _tests/bin/ mirrors _bin/ for CI-gate tests.
  • _scripts/atproto/ contains the AT Protocol publishing tools.

The link cache is built by LinkCacheGenerator and stored in site.data['link_cache']. Resolvers use it for O(1) lookups of books and authors. Keep it serializable; live document objects belong elsewhere.

The SEO subsystems communicate through _includes/head.html:

  • JsonLdInjector populates site.data['generated_json_ld_scripts'] with <script type="application/ld+json"> tags. Its dispatch is keyed by layout through LAYOUT_GENERATORS; an unknown layout raises.
  • SeoMetaInjector populates site.data['seo_meta'] with title, Open Graph, Twitter, description, canonical, and robots values. Its layout knowledge is limited to title suffixes (LAYOUT_TITLE_SUFFIX), article classification (ARTICLE_LAYOUTS), and indexing (NOINDEX_LAYOUTS, NO_CANONICAL_LAYOUTS). The test_every_known_layout_has_article_classification test requires every layout in LAYOUT_GENERATORS to be explicitly classified.
  • StandardSiteWellKnownGenerator emits .well-known/site.standard.publication for AT Protocol / Bluesky verification. The matching link tags use standard_site.publication_uri from configuration and _data/standard_site.json generated by CI.

JSON-LD entity identity (JsonLdUtils::ENTITY_KINDS): a bare page URL identifies the web page. An entity the page describes gets the page URL plus a fragment, so one URI never names both a page and a thing:

Entity @id url
Reviewed Book book-family URL + #book book-family URL
Book author author page URL + #person author page URL
BookSeries series page URL + #series series page URL
Site owner author.url (_config.yml) + #person author.url
WebSite site root + #website site root

Only /resume/ and author pages have page nodes with an @id. These nodes use the bare page URL and carry page text (name, description). A Review or BlogPosting identifies its page through mainEntityOfPage; other page nodes carry only url. Person nodes carry identity only.

author.url is required: JsonLdUtils.site_owner_ref stops the build if it is missing. /resume/ is a ProfilePage whose mainEntity is the site owner. An author page is a CollectionPage whose about is the book author. Google's ProfilePage classification is for a person affiliated with the site; the book author wrote the books, not the site, so that classification does not apply.

The book-family URL comes from BookIdentityResolver, so archived reviews share the canonical review's Book @id. A book's place in its series is the Book's position, never the shared BookSeries node's. A book whose series has no series page stops the build, like an unresolved author.

Every book and author page requires wikidata_qid, the only field that stores its QID. JsonLdUtils.same_as_list builds the Wikidata URL from this QID and places it first in the Book's or author Person's sameAs; same_as_urls holds the other links. The link cache carries the author's QID so book reviews build the same sameAs for credited authors. FrontMatterValidator stops the build if the QID is missing or malformed, if a non-null same_as_urls value is not a list, or if same_as_urls contains a Wikidata URL. On a book, it also stops the build if isbn is set but not a string: YAML 1.1 reads an unquoted isbn: 0306406152 as an octal Integer with different digits, so quote the ISBN or write it with hyphens.

An archived review is distinct content, so its HTML canonical link points to its own URL. The front-matter canonical_url only groups reviews into a book family for book links and the Book @id.

Code-resident topics

Read the named exemplar before modifying the corresponding subsystem:

  • Build validators (@validator): classes that raise Jekyll::Errors::FatalException for invalid data. Read these when adding a validator, debugging a FatalException, or determining why a build failed.
  • Markdown output pipeline (@pipeline): the complete pre-render → post-render → llms.txt flow begins at Jekyll::MarkdownOutput::MarkdownBodyHook. Read it when changing render_mode, markdown output assembly, or the llms.txt index.
  • Plugin patterns (@pattern): Jekyll::Infrastructure::Links::LinkTagBase documents the thin-tag and delegate structure; Jekyll::UI::DisplayTagRenderable documents render-mode branching; Jekyll::Infrastructure::Links::LinkResolverSupport and Jekyll::Infrastructure::Links::LinkResolverSkeleton document the two link-resolver mixins; and Jekyll::Infrastructure::PluginLoggerUtils.log_liquid_failure documents the error-logging convention.
  • Gotchas (@gotcha): read the tagged code comments for document URL access, page-payload snapshots, cache pollution, canonical URL filtering, generate_link_cache in tests, and other behavior that is easy to miss.
  • Testing: the header of _tests/test_helper.rb documents test structure, naming, MockDocument, MockSite, factory methods, common patterns, and SimpleCov behavior. Factory methods have their own @param and @return documentation. Use the production pattern docs when writing tests for Finder/Renderer orchestration or render-mode behavior. _tests/src/seo/test_json_ld_rendered_site.rb builds a small fixture site with the real layouts, includes, and plugins in a separate Ruby process and parses the written HTML; copy it when a test must cross the hook and template boundary.
  • CI/CD and hooks: the header of .github/workflows/jekyll.yml documents workflow stages. The header of _bin/pre-commit.sh documents hook stages and the git stash --keep-index mechanism. Read them before modifying a workflow or commit hook. The hook restores unstaged work from the stash commit it created, never by stash position, and leaves the stash with recovery commands when the restore fails. _scripts/tests/test_pre_commit.py runs it in a temporary repository with stub docker and uv executables; run it with make test-scripts after any hook change.
  • CI gates: _bin/check_* scripts run as real subprocesses against throwaway fixture directories. The header of _tests/bin/bin_script_helper.rb documents the fixture builders and test approach. test_bin_scripts_are_wired_up.rb fails when a new check_* script has no test, or when no step's run: command in jekyll.yml invokes it. A mention in a comment or step name does not count.
  • External links: .github/workflows/link-check.yml runs Lychee monthly with a JSON report. _scripts/ci/lychee_report.py decides the result: it drops the at:// parse errors, writes the issue body, and exits 0 (clean), 1 (broken links), or 2 (no usable report). Its tests run against real Lychee reports in _scripts/tests/fixtures/lychee/; regenerate them with the Lychee version that lycheeverse/lychee-action@v2 installs.

Policy and operational references

These documents describe policy rather than implementation. Read the one that matches the change:

  • Build Performance — measured decisions about HTML compression and build caches. Required when changing build or serve targets.

  • Book Families & canonical_url — re-review workflow and canonical URL rules. Required for book reviews, especially re-reviews.

  • Content Authoring — excerpts, Liquid tags, links, and front matter enforced by the AT Protocol pipeline. Required for blog posts and book reviews.

  • Markdown Output — pre-render hook behavior, page payload updates, and fatal generation failures. Required for the Markdown output pipeline.

  • Links, Backlinks, and Previews — raw Liquid scanning, related-book scoring, canonical URL filtering, hover and footnote previews, reference-link validation, and strict-Liquid constraints. Required for link resolution, the link cache, previews, check_reference_links.rb, or check_strict.rb.

  • AT Protocol / standard.site — domain-as-handle DNS wiring, the publish script, the well-known generator, link tags, CI flow, and operational procedures for orphaned records, outages, and secret rotation. Required for _scripts/atproto/, the standard.site plugin, or publish steps in CI.

Content and plugin rules

Keep these boundaries intact when changing the site:

  • Tags in tags/ are thin wrappers. They select render_mode and delegate.
  • Utilities in [domain]/[util].rb orchestrate logic.
  • Finders fetch data; renderers generate HTML.
  • Use PluginLoggerUtils.log_liquid_failure for Liquid failures.
  • Add a matching test in _tests/ for every new class.
  • When an invariant is violated, raise Jekyll::Errors::FatalException. Do not allow incorrect output to ship silently.

When changing plugin architecture, CI workflow, hooks, or the markdown-output pipeline, update the matching document in references/.

Verification

Run commands from the repository root. The content checks are separate; no single command runs all of them:

Command Checks
make test All Ruby tests in _tests/. Fails below 95% line or 90% branch coverage.
make test TEST=_tests/src/path/to/test.rb One Ruby test file. The coverage thresholds apply to full runs only, so the exit status reflects the tests alone.
make check-links Builds the site, then runs html-proofer for broken links, missing images, and empty alt attributes.
make check-refs Finds undefined, duplicate, and orphaned Markdown reference links.
make check-liquid Renders documents with strict Liquid checking.
make check Alias for make check-links; it does not run the other content checks.
make lint Runs the repository lint checks.
make format-md Formats every Markdown file in the repository. Do not use it to format a change; it rewrites unrelated files.
make prettier FILE.md ... Formats the named Markdown files with Prettier.

For a content or plugin change, run the targeted test first and then the checks that cover the changed behavior. Do not merge a branch straight into main. Open a pull request, or run make check-links, make check-refs, and make check-liquid locally before merging. These checks must run before the change can publish.

The AT Protocol publish step runs on main only. Its PDS is the state store; the repository does not contain AT-URIs for individual documents. Every other branch runs publish --dry-run in the test job as a blocking step. It needs no secrets, so a bsky.social outage fails branch CI; rerun the job. Read AT Protocol / standard.site before changing publish behavior or operating the workflow.