@eleventy-plugin-themer/theme-base
v0.4.2
Published
A minimal convention-based Eleventy theme with self-describing metadata and extensible features
Maintainers
Readme
@eleventy-plugin-themer/theme-base
A blog theme for Eleventy built on @eleventy-plugin-themer/core. A port of eleventy-base-blog with dark mode, configurable styling, and extensible features.
Features
- Cascade System - User files override theme files (layouts, data, assets, features)
- @theme Alias - Clean imports in Nunjucks templates:
{% extends "@theme/layouts/base.njk" %} - Dark Mode - Configurable light/dark toggle with system preference support
- Extensible Features - Self-contained feature modules loaded per-page via front matter
- CSS Custom Properties - Easy theming via variables for colors, typography, spacing
- Configurable - Override theme defaults via
theme.config.mjs
Installation
npm install @eleventy-plugin-themer/core @eleventy-plugin-themer/theme-base @11ty/eleventy-plugin-syntaxhighlightWith Vite build optimizations:
npm install -D @eleventy-plugin-themer/build-vite @11ty/eleventy-plugin-viteQuick Start
// eleventy.config.mjs
import { eleventyPluginThemer } from '@eleventy-plugin-themer/core';
const THEME_NAME = '@eleventy-plugin-themer/theme-base';
export default async function (eleventyConfig) {
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const { dir } = await eleventyPluginThemer(eleventyConfig, {
theme: THEME_NAME,
projectRoot: __dirname,
input: 'content',
output: '_site',
});
return { dir };
}See the eleventy-starter repository for a complete working example.
Project Structure
your-project/
├── eleventy.config.mjs # Theme + plugin configuration
├── content/ # Your content
│ ├── index.md
│ ├── posts/
│ └── _data/
│ ├── site.js # Site metadata
│ └── theme.js # Theme config overrides
├── overrides/ # Customizations
│ ├── layouts/ # Override/extend theme layouts
│ ├── features/ # Custom or overridden features
│ ├── scripts/
│ │ └── main.js # Your JavaScript entry point
│ └── styles/ # Your styles
└── public/ # Static assets (favicon, etc.)Theme Configuration
Override theme defaults by creating theme.config.mjs:
export default {
themeToggle: {
defaultTheme: 'auto', // 'auto', 'light', or 'dark'
showToggle: true,
},
colors: {
// Grayscale theme with a single reddish-plum accent. Links are grayscale and
// light up to the accent on hover (linkHover defaults to the accent).
light: {
background: '#ffffff',
primary: '#1a1a1a',
accent: '#9b3b54',
text: '#262626',
link: '#1a1a1a',
linkHover: '#9b3b54',
linkVisited: '#595959',
},
dark: {
background: '#171717',
primary: '#f5f5f5',
accent: '#9b3b54',
text: '#e5e5e5',
link: '#fafafa',
linkHover: '#9b3b54',
linkVisited: '#a3a3a3',
},
},
typography: {
fontFamily: '-apple-system, system-ui, sans-serif',
fontFamilyHeading: 'inherit',
fontFamilyMono: "'Consolas', 'Monaco', monospace",
},
logos: {
default: '',
dark: '',
favicon: '/favicon.svg',
},
social: [],
footer: {
copyright: '{year} {site.title}', // supports {year} and {site.title}
startYear: null, // e.g. 2024 -> renders a "2024–<current>" range
alignment: 'center', // footer-bottom row: 'left' | 'center' | 'right'
showPoweredBy: true, // "Built with Eleventy" line
showGitSha: true, // short commit hash (see Build metadata below)
gitHubRepo: '', // e.g. 'https://github.com/you/repo' -> links the commit
},
};All config values are deeply merged with the theme's defaults from theme.json. Use null to explicitly clear a value.
footer.alignment controls only the bottom row (copyright / "Built with" / commit hash); the footer navigation (menu + social links) layout is unaffected.
Customization
Override Layouts
Replace a theme layout:
{# overrides/layouts/post.njk - completely replaces theme's post.njk #}
<article>
<h1>{{ title }}</h1>
{{ content | safe }}
</article>Extend a theme layout:
{# overrides/layouts/custom.njk #}
{% extends "@theme/layouts/base.njk" %}
{% block content %}
<div class="custom">{{ content | safe }}</div>
{% endblock %}Override Data
Create content/_data/site.mjs to replace theme's site data:
export default defineSiteData({
title: 'My Blog',
url: 'https://myblog.com',
language: 'en',
author: { name: 'Your Name', email: '[email protected]' },
social: [{ platform: 'github', account: 'your-handle' }],
});Override Static Assets
Place files in public/ to override theme assets:
public/
├── favicon.svg # Overrides theme's favicon
└── logo.png # Your custom assetFeatures
Features are optional functionality modules loaded per-page via front matter.
Using Features
Add to any page's front matter:
---
title: My Post
feature: code-highlighting
---Available Theme Features
code-highlighting
Syntax highlighting powered by PrismJS (via @11ty/eleventy-plugin-syntaxhighlight). Includes copy button, optional line numbers, diff highlighting, and custom scrollbar.
Usage — add to any page's front matter:
---
feature: code-highlighting
---Configuration — override in theme.config.mjs:
export default {
codeHighlighting: {
prismTheme: 'prism-tomorrow', // PrismJS theme name (default)
diffHighlight: true, // Include diff-highlight plugin (default)
},
};Available PrismJS themes:
| Theme | Style |
| ---------------------- | --------------------------------------- |
| prism | Light, minimal (Lea Verou's default) |
| prism-coy | Light with subtle left border |
| prism-solarizedlight | Light, Solarized palette |
| prism-dark | Dark, muted tones |
| prism-funky | Dark with coloured line backgrounds |
| prism-okaidia | Dark, Monokai-inspired |
| prism-tomorrow | Dark, Tomorrow Night Eighties (default) |
| prism-twilight | Dark, warm greys |
Diff highlighting — use the diff- language prefix with +/- line markers:
```diff-js
+const added = true;
-const removed = false;
const unchanged = null;
```CSS custom properties — enhancements on top of PrismJS (override in your own CSS):
| Property | Default | Description |
| --------------------------- | ------------------------ | ----------------------------- |
| --code-border-radius | 0.5rem | Border radius for code blocks |
| --code-copy-button-bg | var(--color-surface) | Copy button background |
| --code-copy-button-fg | var(--color-text) | Copy button text colour |
| --code-line-number-fg | rgb(255 255 255 / 30%) | Line number colour |
| --code-line-number-width | 3rem | Line number column width |
| --code-scrollbar-thumb-bg | rgb(255 255 255 / 20%) | Scrollbar thumb colour |
back-to-top
A floating button that appears once the page is scrolled past a threshold and smooth-scrolls back to the top. Fully self-contained (no template or data dependencies).
Usage — add to any page's front matter:
---
feature: back-to-top
---Customization — import and call init() manually instead of using the
auto-init variant:
import { init, defaultConfig } from '@theme/features/back-to-top/index.js';
init({ ...defaultConfig, threshold: 800 });CSS custom properties (override in your own CSS):
| Property | Default | Description |
| ---------------------- | ------------------------- | -------------------- |
| --back-to-top-bg | var(--color-primary) | Button background |
| --back-to-top-fg | var(--color-background) | Glyph colour |
| --back-to-top-size | 2.75rem | Button width/height |
| --back-to-top-offset | 1.5rem | Distance from corner |
Creating Custom Features
Create overrides/features/my-feature/index.js (or index.auto.js for auto-initialization):
// index.auto.js - auto-initializes when loaded
console.log('My custom feature loaded');If both index.auto.js and index.js exist in the same directory, the
plugin picks index.auto.js first. Use index.auto.js for self-running
features and index.js for ones the consumer imports and initializes
explicitly.
Then reference in front matter: feature: my-feature
Overriding Theme Features
Create overrides/features/code-highlighting/index.auto.js to replace the theme's version.
Shortcodes
Hero Section
{% hero title="Welcome", subtitle="Build beautiful websites", align="center", height="400px" %}
{% heroButton url="/start", variant="primary" %}Get Started{% endheroButton %}
{% heroButton url="/learn", variant="secondary" %}Learn More{% endheroButton %}
{% endhero %}| Parameter | Description | Default |
| ----------------- | ----------------------------------------- | ---------- |
| title | Main heading | - |
| subtitle | Secondary text | - |
| background | Background image URL | - |
| backgroundColor | Background color (fallback) | - |
| align | Text alignment: left, center, right | 'center' |
| height | Minimum height | 'auto' |
| overlay | Dark overlay on background image | true |
Content Grid
{% contentGrid cols=3, gap="1.5rem" %}
{% box title="Feature 1", link="/about", linkText="Learn More" %}
Description of feature 1.
{% endbox %}
{% endcontentGrid %}Navigation
Uses @11ty/eleventy-navigation for menus. Add pages via front matter:
---
eleventyNavigation:
key: About
order: 2
---Footer navigation uses parent: footer. Hierarchical pages get automatic breadcrumbs.
Build metadata
The theme provides a build global with the current git commit and a build timestamp,
so the footer commit hash works without any setup on your side:
| Field | Example |
| ------------------- | ---------------- |
| build.gitSha | full commit SHA |
| build.gitShaShort | 7-char short SHA |
| build.timestamp | ISO build time |
It runs git rev-parse HEAD in your project at build time (falling back to
unknown/dev outside a git repo). The footer shows build.gitShaShort when
footer.showGitSha is enabled, linking to the commit if site.repository is set.
To override (e.g. inject a CI build number), provide your own
content/_data/build.js — Eleventy's directory data takes precedence over the
theme's global data.
Social links
Configure social links via the social array in content/_data/site.mjs (social is
site data, not theme config). Each entry needs a platform plus either an account
(expanded through a URL template) or a full url:
export default defineSiteData({
social: [
{ platform: 'github', account: 'artislismanis' },
{ platform: 'mastodon', url: 'https://fosstodon.org/@you' },
{ platform: 'linkedin', account: 'your-handle', label: 'LinkedIn' },
],
});accountis expanded by core'ssocialUrlfilter using the framework'sSOCIAL_PLATFORMStable (github,x,linkedin,youtube,instagram,facebook,tiktok,discord,twitch,reddit,bluesky,mastodon). Themes may extend it viatheme.json#socialPlatforms. Mastodon also accepts an@user@instanceaccount.urltakes precedence overaccountand is validated bysafeUrl(see Security helpers).labelis optional (used for thearia-label); it defaults to the capitalized platform name.
Icons
Icons are inline brand SVGs from simple-icons,
rendered with fill="currentColor" so they inherit the link colour (grey, plum on hover).
The platform value must match a simple-icons slug. Browse and search slugs at
simpleicons.org — e.g. the "GitHub" tile has the slug
github, "X" has x. The resolution order per link is:
social.icon— a custom image path you provide (per-link override, wins over everything)- the
simple-iconsbrand SVG matched by slug - a plain text label fallback when no icon is found
Special cases handled in lib/filters.mjs:
twitteris aliased to thexslug (the Twitter glyph was retired upstream), so legacy config keeps working.- A few brands have been removed from simple-icons at the owner's request (notably
LinkedIn). The theme ships fallback glyphs for those in the
SUPPLEMENTAL_ICON_PATHSmap inlib/filters.mjs— add an entry there (keyed by slug, 24×24 path data) to cover any other removed brand, or overrideoverrides/layouts/partials/social/icon.njkto fully customise rendering.
Security helpers
Nunjucks runs with autoescape: false in this framework, so the theme exports a set of escape filters that must be applied explicitly to any dynamic value:
| Filter | Use for |
| ---------------- | ----------------------------------------------------------------------- |
| escapeHtml | Text in HTML body context |
| escapeAttr | Values inside ="…" attributes |
| escapeCssValue | CSS custom-property values (strips quotes, brackets, ;, \, /* */) |
| escapeJsString | Values inside JS string literals (escapes </script>, U+2028/9, etc.) |
| safeUrl | Values used as href / src |
safeUrl() and the related socialUrl() filter use a strict scheme allowlist (http, https, mailto, tel, plus relative URLs). Everything else returns #. The filter also strips control / zero-width / bidi-override chars, rejects https:\/\/ backslash-authority forms, and blocks percent-encoded CR/LF in mailto: / tel: (header smuggling).
Related Packages
- @eleventy-plugin-themer/core - Build-agnostic cascade system
- @eleventy-plugin-themer/build-vite - Vite production optimizations
License
MIT
