This is the authoritative description of how the al-folio v1 starter and its plugin gems fit together. It is written for humans and coding agents alike. AGENTS.md is the short entry point and links here; BOUNDARIES.md is the authoritative area-to-gem ownership table. Everything in this file should exist in exactly one place — if you find it restated elsewhere, link here instead of copying.
- Architecture (v1.x)
al-folio v1.x is a thin Jekyll starter, not a theme. This repo owns only:
- starter wiring (
Gemfile,_config.yml,_data/featured_plugins.yml), - example content (
_pages,_posts,_projects,_news,_teachings,_books,_bibliography), - documentation (
docs/), - cross-gem integration tests (
test/integration_*.sh) and visual parity tests (test/visual/).
All runtime — layouts, includes, Sass, Liquid tags, filters, and feature JS — lives in versioned gems published independently on RubyGems and developed under the al-org-dev organization.
The single most common mistake is editing runtime here. If a change is a layout, include, tag, filter, or feature behavior, it belongs in the owning gem. See the routing table in AGENTS.md and the full ownership table in BOUNDARIES.md.
These three are the reason most "I changed it and nothing happened" reports exist. None of them raises a build error.
Feature gating is two-layered, and a feature renders only when both layers agree:
- Site-wide config flags in
_config.yml:search_enabled,enable_math,enable_cookie_consent,enable_darkmode,al_folio.features.cv.enabled,al_folio.features.distill.enabled, and the provider IDs underanalytics:. - Per-page front matter:
images:,tikzjax,chart.*,mermaid.*,giscus_comments,layout: distill,layout: cv.
al_folio_core ships thin wrappers in _includes/plugins/*.liquid that call custom Liquid tags defined by the sibling gems. When the owning gem is not in the plugin list, or the flag is off, the tag emits an empty string. There is no warning, no missing-tag error, and no visual placeholder — the feature simply is not there.
When debugging a feature that "does nothing", check in this order:
- Is the gem in both the
Gemfileand theplugins:list in_config.yml? (See below.) - Is the site-wide flag on?
- Does the page's front matter opt in?
- Is the relevant
third_party_librariesentry present with its SRI hash?
Plugin activation requires two edits, in two files:
Gemfile,group :al_folio_plugins— the pinned dependency (for examplegem 'al_folio_core', '= 1.0.11')._config.yml, theplugins:list — the Jekyll activation entry.
A gem present in only one of them is inert. In the Gemfile only, Jekyll never loads it; in plugins: only, Bundler never installs it. Adding or removing a plugin means editing both. Note the spelling difference: repo directories use hyphens (al-folio-core), gem and plugin ids use underscores (al_folio_core).
The demo site is published as a project page at https://alshedivat.github.io/al-folio/, so _config.yml already sets baseurl: /al-folio. A plain build therefore picks it up — deploy.yml, broken-links-site.yml and axe.yml all run bundle exec jekyll build with no flag. What matters is that the effective baseurl stays /al-folio; passing it explicitly is redundant but harmless, and the command set spells it out so the served path is unambiguous:
bundle exec jekyll build --baseurl /al-folio
bundle exec jekyll serve # http://localhost:4000/al-folio/ (note the path)What breaks the site is blanking the baseurl out — build with an empty baseurl and every asset and internal link resolves one path segment too high. The Docker entry point serves under /al-folio too. A build that "works" but renders unstyled is almost always a baseurl mismatch. In your own site this is different: personal and organization sites (username.github.io) must leave baseurl empty but present; project sites set baseurl: /<project-name>/. See FAQ.
al_folio_core is the hub: _config.yml sets theme: al_folio_core, and the gem ships every base _layouts/*.liquid and _includes/*.liquid, the base theme JS/CSS, the details and file_exists tags, and the hideCustomBibtex and remove_accents filters. Its _includes/plugins/*.liquid wrappers delegate to tags owned by sibling gems:
| Wrapper / call site | Liquid tag | Owning gem |
|---|---|---|
| search assets | al_search_assets |
al_search (Cmd-K ninja-keys palette; index built at build time from content) |
| comments | al_comments |
al_comments (Giscus + Disqus, front-matter gated) |
| cookie banner | al_cookie_styles / al_cookie_scripts |
al_cookie (consent-mode gating of analytics) |
icon <link>s |
al_icons_styles |
al_icons (FontAwesome/Academicons/Scholar Icons from CDN) |
| analytics | al_analytics_scripts |
al_analytics (GA/Cronitor/Pirsch/OpenPanel) |
| math | al_math_styles / al_math_scripts |
al_math (MathJax, pseudocode.js, TikZJax) |
| charts | al_charts_scripts |
al_charts (Mermaid/Chart.js/ECharts/Plotly/Vega/Leaflet/diff2html) |
| image tools | al_img_tools_styles / al_img_tools_scripts |
al_img_tools (zoom, lightbox, sliders, galleries) |
| newsletter | al_newsletter_form / al_newsletter_scripts |
al_newsletter (Loops.so signup) |
layout: cv |
al_folio_cv_render |
al_folio_cv (RenderCV YAML + JSONResume) |
layout: distill |
al_folio_distill_render |
al_folio_distill (vendored, hash-pinned distillpub runtime) |
| citation badges | google_scholar_citations / inspirehep_citations |
al_citations |
| external posts | (generator, no tag) | al_ext_posts (RSS/URL ingestion into synthetic posts) |
| legacy Bootstrap behavior | (opt-in assets) | al_folio_bootstrap_compat |
| upgrade/audit CLI | bundle exec al-folio … |
al_folio_upgrade |
Most feature gems are Jekyll Generators that inject their JS/CSS as static files at build time, only when the feature is enabled. Consequences worth knowing:
- Those assets are not committed to this repo; you will not find them under
assets/in a fresh checkout, and they appear in_site/only for enabled features. - Several load from pinned CDN URLs with Subresource Integrity hashes read from the
third_party_libraries:block in_config.yml. Bumping a library version means bumping itsintegrityhash in the same block. - Do not vendor icon fonts or runtime JS back into starter paths to "fix" a missing asset — that is the silent-gating symptom in failure mode 1, not a packaging bug.
_config.yml must keep the al_folio contract keys:
al_folio.api_version: 1al_folio.style_engine: tailwindal_folio.tailwind.{version,css_entry,preflight}al_folio.distill.{engine,source}
This is enforced twice: as build-time warnings by al_folio_core's :after_init hook, and as blocking findings by bundle exec al-folio upgrade audit (which CI runs in upgrade-check.yml). Do not remove these keys.
These two cases have opposite rules, and conflating them is a recurring source of confusion.
In your own site (a repo created from this template): local overrides are fully supported. You may shadow any gem-owned file by adding the same path in your repo — _layouts/bib.liquid, _includes/repository/repo.liquid, _sass/_variables.scss, and so on. Your copy wins over the gem's. Track them so future gem updates can flag drift:
bundle exec al-folio upgrade overrides audit
bundle exec al-folio upgrade overrides diff <path>
bundle exec al-folio upgrade overrides accept <path>overrides audit records the owning gem, its version, and the upstream/local SHA256 in .al-folio-overrides.yml. Commit that file. When a later bundle update changes the upstream file, the audit marks your override stale. Fixes that would benefit everyone should be ported to the owning gem instead of kept as a local override.
In this starter repo (alshedivat/al-folio itself): those directories must not exist. npm run lint:style-contract fails the build if the starter contains _includes/, _layouts/, _sass/, _scripts/, assets/tailwind/, tailwind.config.js, assets/webfonts/, or icon-font artifacts. This is the automated enforcement of the thin-starter boundary and applies to contributions to al-folio, not to user sites.
Note for maintainers:
test/style_contract.jsandunit-tests.ymlship to every site created from this template, so a user who adds a perfectly legal local override will see the starter's own contract check fail in their fork. Whether to re-scope that check to the upstream repo only is an open maintainer decision; it is deliberately unchanged here.
al_folio.compat.bootstrap.enabled: true (default false) activates al_folio_bootstrap_compat, which restores legacy data-toggle and Bootstrap-class behavior on the Tailwind-first v1 core.
- Supported through
v1.2 - Deprecated in
v1.3 - Removed in
v2.0
Migrate content off Bootstrap markup before then. See FAQ.
Plugin gems are developed as sibling checkouts next to this repo. Clone the gem repo beside your al-folio checkout, then point the Gemfile at your working copy and reinstall:
gem "al_folio_core", path: "../al-folio-core" # or git: / branch:bundle install
bundle exec jekyll build --baseurl /al-folioRevert the Gemfile to the pinned released version before committing — the pins in Gemfile are starter wiring and test/style_contract.js asserts some of them.