Instruction file imported from bamr87/zer0-mistakes (
.github/instructions/includes.instructions.md). Copyright stays with the author.
Jekyll Includes — _includes/**
Reusable, self-contained Liquid components. Each include does one thing, accepts named parameters, fails gracefully, and degrades on mobile.
1. Directory Layout
_includes/
├── core/ # Page chrome: head, header, footer, scripts
├── components/ # Reusable UI: cards, modals, consent, theme-info
├── navigation/ # Navbar, breadcrumbs, sidebar, pagination
├── analytics/ # Tracking pixels, posthog, GA (consent-gated)
└── content/ # Markdown helpers: backlinks, wiki-graph, embeds
New includes go in the most specific subdirectory. Create a new top-level only if no existing one fits — and update _includes/README.md in the same commit.
2. Required Header
{%- comment -%}
Component: <name>
Path: _includes/<subdir>/<file>.html
Purpose: <one-line>
Params: title (string, required) | class (string, optional, default: "")
Depends on: <other includes, configs, vendor assets>
Notes: <perf, a11y, mobile considerations>
{%- endcomment -%}
3. Parameter Handling
- Always provide defaults for optional params:
{%- assign css_class = include.class | default: "" -%} - Required params: fail loud with a comment if missing:
{%- if include.title == nil -%} {%- comment -%} ERROR: <name> requires `title` {%- endcomment -%} {%- endif -%} - Pass-through content via
include.contentor block content withcapture. - Whitespace: use
{%-and-%}to strip whitespace; keep rendered HTML clean. - Never trim around a conditional HTML attribute.
{%-/-%}strip all adjacent whitespace, including the newlines that separate attributes written one per line, which emits themissing-whitespace-between-attributesparse error (issue #465 — this gluedaria-label/aria-current/titletogether on every navbar link of every consumer site). On its own line between two attributes, write{% if cond %}attr="v"{% endif %}— non-trimming, separator outside the tag — not{%- if cond -%} attr="v"{%- endif -%}, which glues the attribute before it to the one after even when theifemits nothing.test_attribute_whitespace_in_markupintest/test_core.shenforces this across every include and layout.
4. Conditional Loading
Gate environment-sensitive includes:
{%- if jekyll.environment == "production" and site.posthog.enabled -%}
{%- include analytics/posthog.html -%}
{%- endif -%}
Gate consent-required code on a cookieConsent cookie/localStorage flag — never load trackers before consent.
5. Bootstrap 5 Patterns
- Use Bootstrap 5.3.3 utility classes; avoid hand-rolled CSS when a utility exists.
- Components:
card,alert,modal,navbar,accordion,offcanvas— wire them withdata-bs-*attributes. - Icons: Bootstrap Icons via
<i class="bi bi-…"></i>from the local vendor bundle. - Always include ARIA:
role,aria-label,aria-expanded,aria-controls.
Example card:
<div class="card {{ include.class | default: '' }}">
{%- if include.image -%}
<img src="{{ include.image | relative_url }}" class="card-img-top" alt="{{ include.alt | default: include.title }}" loading="lazy">
{%- endif -%}
<div class="card-body">
<h5 class="card-title">{{ include.title }}</h5>
<p class="card-text">{{ include.description }}</p>
</div>
</div>
6. Accessibility (non-negotiable)
- Semantic HTML:
<nav>,<main>,<article>,<section>,<aside>,<header>,<footer>. - Heading hierarchy: only one
<h1>per page; never skip levels. - Every
<img>hasalt(emptyalt=""for decorative). - Every interactive element is keyboard-reachable; visible focus styles.
- Color contrast ≥ WCAG AA.
- Use
aria-livefor dynamic regions;aria-hiddenon decorative icons.
7. Responsive / Mobile-First
- Start mobile, add breakpoints upward (
d-none d-md-block,col-12 col-md-6). - Lazy-load images:
loading="lazy"anddecoding="async". - Use
picture+srcsetfor responsive images when source sizes vary.
8. Security
- Escape user/page content:
{{ value | escape }}for attributes,| strip_htmlfor previews. - Use
relative_url/absolute_urlfor all internal links. - Sanitize HTML in user-supplied frontmatter with
| strip_html | truncate: 200. - Never inline
eval-style JS; never injectinclude.contentinto<script>.
9. SEO Helpers
For SEO/meta includes:
<title>{{ page.title }} | {{ site.title }}</title>
<meta name="description" content="{{ page.description | default: site.description | strip_html | truncate: 160 }}">
<link rel="canonical" href="{{ page.url | absolute_url }}">
10. Error Handling
Wrap risky lookups in defensive checks:
{%- if site.data.navigation and site.data.navigation.main -%}
{%- for item in site.data.navigation.main -%}
…
{%- endfor -%}
{%- else -%}
{%- comment -%} navigation data missing — skipping render {%- endcomment -%}
{%- endif -%}
11. Testing Checklist
Before committing an include:
-
bundle exec jekyll buildis clean (no Liquid warnings) - Renders on a page with and without every optional param
- Mobile (≤ 576px), tablet (768px), desktop (≥ 992px) all look right
- Keyboard navigation works
- Screen-reader landmarks announce correctly (test with VoiceOver / NVDA)
- No console errors in browser devtools
- Updated
_includes/README.mdif added a new include
12. Naming
- Lowercase, hyphen-separated:
cookie-consent.html,feature-card.html. - Match Liquid include path:
{% include components/feature-card.html %}. - One component per file. Split when over ~150 lines.
Related: layouts.instructions.md for layouts · sass.instructions.md for styling · obsidian.instructions.md for wiki/embed includes.