This guide will help you set up and develop on pob.dev locally.
- Node.js - Version 24.15.0 or higher (CI uses 24)
- npm - Version 12.0.0 or higher (Node's bundled npm may lag behind; run
npm install -g npm@12to update) - Git - For version control
- Clone the repository
git clone https://github.com/p-ob/pob.dev.git
cd pob.dev- Install dependencies
npm ciThis installs exact versions from package-lock.json.
- Start the development server
npm startThe site will be available at http://localhost:8080 with hot reload enabled.
npm start- Starts Eleventy in watch mode with live reload
- Incremental builds for faster development
- Draft posts are visible
- Serves on
http://localhost:8080
npm run build- Full production build (Eleventy + PageFind search index)
- Minifies CSS
- Optimizes images
- Generates search index
- Outputs to
public/directory
npm run build:11ty- Builds static site only (no search index)
- Faster for testing build output
npm run build:index- Generates PageFind search index only
- Run after
build:11tyto update search
npm run lint- Checks code formatting with Prettier
- Validates JavaScript, Markdown, CSS, JSON, HTML
npm run format- Auto-formats all source files
- Applies Prettier configuration
npm run dev- Starts local Cloudflare Workers environment
- Simulates production deployment locally
- Requires prior
npm run build
npm run deploy- Deploys to Cloudflare Workers
- Requires Cloudflare credentials
- Typically done via CI/CD
npm run clean- Removes build artifacts (
public/directory) - Preserves
.envfile if present - Uses
git cleanto ensure complete cleanup
All source content lives here:
src/
├── _data/ # Global data files (author.js, metadata.js)
├── _includes/ # Layouts and reusable components
├── assets/ # Static assets (CSS, JS)
├── blog/ # Blog posts (organized by year/month)
├── index.njk # Homepage
├── blog.njk # Blog listing
├── reading.njk # RSS feed reader
├── search.njk # Search page
├── feed.njk # Feed listing page
└── about.md # About page
Eleventy plugins that extend functionality:
draft.js- Hides draft posts in productionexternals.js- Manages external dependencies with import mapsfeeds.js- Generates RSS/Atom/JSON feedsfeed-aggregator.js- Aggregates external RSS feedsjson-html.js- Sanitizes JSON for HTML outputtable-of-contents.js- Generates article TOCsyntax-highlight.js- Per-page syntax highlighting with language detection
eleventy.config.js- Main Eleventy configurationwrangler.jsonc- Cloudflare Workers configurationfeeds.json- External RSS feed sources.editorconfig- Editor settings.prettierrc- Code formatting rules
- Create a new markdown file
# Follow the date-based structure
src/blog/YYYY/MM/your-post-title.mdExample: src/blog/2024/11/new-feature.md
- Add frontmatter
---
title: Your Post Title
description: A brief description of your post
date: 2024-11-30 10:00:00 -06:00
tags:
- tag1
- tag2
---
Your content here...-
Required frontmatter fields
title- Post titledescription- Brief description (used in lists and feeds)date- Publication date, always with an explicit time and UTC offset (see "Dates and time zones" below) — a bareYYYY-MM-DDis parsed as UTC midnight and can render as the previous day once formatted in a non-UTC time zone
-
Optional frontmatter fields
tags- Array of tags for categorizationdraft- Set totrueto mark as draft (visible in dev only)updatedDate- Last modification date (displays alongside publish date)
readableDate formats dates with Intl.DateTimeFormat without a fixed timeZone, so it renders in whatever time zone the build/dev machine is running in. A bare date: 2024-11-30 is parsed as 2024-11-30T00:00:00.000Z (UTC midnight) — format that in any time zone behind UTC (e.g. npm start on a laptop set to America/Chicago) and it displays as November 29, one day early.
Always give date (and updatedDate) an explicit time and UTC offset, e.g. 2024-11-30 10:00:00 -06:00. A mid-morning local time keeps the calendar date stable regardless of which time zone the renderer uses. This applies to every content type with a date field (blog posts under src/blog/, talks under src/talks/), not just blog posts.
This site is authored from Milwaukee, WI (America/Chicago). Pick the offset based on whether the date falls in Daylight Saving Time:
-05:00(CDT) — mid-March through early November-06:00(CST) — early November through mid-March
When you update a post significantly, add the updatedDate field:
---
title: My Post
description: A post that was updated
date: 2024-11-30 10:00:00 -06:00
updatedDate: 2025-01-15 10:00:00 -06:00
---This will:
- Display as "November 30, 2024 (Updated January 15, 2025)" on the post
- Add
dateModifiedto Schema.org structured data for SEO - Keep the original
datefor sorting and feeds
To work on a post without publishing:
---
title: Work in Progress
description: Still writing this
date: 2024-11-30 10:00:00 -06:00
draft: true
---Drafts are:
- ✅ Visible in development (
npm start) - ❌ Hidden in production builds (
npm run build) - ❌ Excluded from feeds and collections
Standard markdown plus:
- Footnotes - Via markdown-it-footnote
- Code blocks - With automatic syntax highlighting (see below)
- External links - Automatically open in new tab
- Images - Automatically optimized by Eleventy
Code blocks automatically get syntax highlighting using the <syntax-highlight> element. The system intelligently loads only the languages you use.
Basic usage:
```javascript
function hello() {
console.log("Hello, world!");
}
```Supported languages:
The site supports all Prism languages. Common ones include:
javascript,js- JavaScripttypescript,ts- TypeScriptpython,py- Pythoncsharp,cs- C#bash,sh,shell- Shell scriptshtml,xml- Markupcss,scss- Stylesheetsjson- JSONmarkdown,md- Markdown
Use GitLab-style alert syntax to create styled note boxes. Five types are supported:
Note - General information or reminders:
> [!note]
> This is important information readers should know.Info - Helpful tips or additional context:
> [!info]
> This provides helpful context or tips.Success - Positive outcomes or achievements:
> [!success]
> The operation completed successfully!Warning - Important cautions or considerations:
> [!warning]
> Be careful when doing this operation.Error - Critical errors or failures:
> [!error]
> An error occurred during processing.Multi-line notes:
> [!warning]
> This warning spans multiple lines.
> Each line should start with `> ` to be included.
> The note will display all lines together.Custom labels: You can override the default label with custom text:
> [!warning] Data deletion
> The following instructions will make your data unrecoverable.
> [!info] Pro tip
> Use keyboard shortcuts to speed up your workflow.Notes are rendered as <pob-note> web components with appropriate styling for each type.
Language aliases:
Many languages have aliases that map to the same grammar:
js→javascriptpy→pythoncs→csharpsh,shell→bash
Multiple languages in one post:
```javascript
// JavaScript example
const x = 42;
```
```python
# Python example
x = 42
```
```csharp
// C# example
int x = 42;
```When you use multiple languages, only those specific languages are loaded from the CDN. Pages without code blocks don't load the syntax highlighting library at all, improving performance.
How it works:
- The build process detects which languages are used in your post
- The page loads only those specific languages (plus base languages: markup, css, javascript)
- Syntax highlighting happens at runtime using the CSS Custom Highlight API
- No
<span>elements clutter your HTML – just clean, semantic markup
You can make HTML code blocks interactive by adding the live modifier. This renders the code in a sandboxed iframe that readers can run.
Basic usage:
```html live
<button onclick="alert('Hello!')">Click me</button>
```This will display:
- The syntax-highlighted code block
- A "Run" button in a toolbar below the code
- When clicked, an "Output" panel slides in showing the live rendered HTML
Features:
- Sandboxed execution - Code runs in an iframe with
sandbox="allow-scripts"for security - Dark mode support - The output panel respects the user's color scheme preference
- Auto-sizing - The iframe automatically resizes to fit its content
- One-click run - Click "Run" to see the result; the demo stays visible
Example with CSS and JavaScript:
```html live
<button id="btn">Count: 0</button>
<style>
#btn {
padding: 0.5em 1em;
font-size: 1.2em;
cursor: pointer;
}
</style>
<script>
let count = 0;
document.getElementById('btn').onclick = () => {
count++;
document.getElementById('btn').textContent = `Count: ${count}`;
};
</script>
```Limitations:
- Only works with
htmllanguage blocks - Cannot access the parent page's DOM or styles
- External resources may be blocked by the sandbox
Styling:
Code blocks automatically match your site's theme (light/dark mode) using CSS custom properties. The highlighting styles are defined in src/assets/css/partials/_code.css.
<pob-note type="note">
This is an informational note.
</pob-note>
<pob-note type="warning">
This is a warning.
</pob-note>
<pob-note type="error">
This is an error or critical information.
</pob-note>TOC is automatically generated from heading elements (h2, h3, etc.) when using the post layout.
No action needed - it just works!
CSS is organized using CSS layers:
@layer reset, config, base, utility, interactions, layout;Adding styles:
- Global utilities →
src/assets/css/partials/_base.css - Component styles →
src/assets/css/components/component-name.css - Theme variables →
src/assets/css/partials/_vars.css - Interaction styles →
src/assets/css/partials/_interactions.css
Theme variables are defined in _vars.css:
@property --font-color {
syntax: "<color>";
inherits: true;
initial-value: hsl(0, 0%, 20%);
}
@property --page-background-color {
syntax: "<color>";
inherits: true;
initial-value: hsl(0, 0%, 96%);
}Dark mode variants use prefers-color-scheme:
@media (prefers-color-scheme: dark) {
:root {
--font-color: hsl(0, 0%, 91%);
--page-background-color: hsl(220, 4%, 14%);
}
}Use BEM-like naming:
.component-name { }
.component-name__element { }
.component-name--modifier { }Web components live in src/assets/js/components/:
app.js- Main application shelldemo.js- Live code demo component (used byhtml livecode blocks)note.js- Note/alert componenttile.js- Card/tile component
import { LitElement, html, css } from "lit";
export class MyComponent extends LitElement {
static styles = css`
:host {
display: block;
}
`;
render() {
return html`<div>Hello, World!</div>`;
}
}
customElements.define("my-component", MyComponent);- Import in
app.jsor create new component file - Use in templates:
<my-component></my-component>Components are server-side rendered at build time via @lit-labs/eleventy-plugin-lit.
External feed fetching is off by default (the Reading page renders with no aggregated items) so that ordinary builds and dev servers don't depend on network access to third-party feeds. To actually fetch feeds — e.g. while working on the Reading page — set FETCH_EXTERNAL_FEEDS=true:
FETCH_EXTERNAL_FEEDS=true npm startTo add RSS feeds to the "Reading" page:
- Edit
feeds.json
{
"feeds": [
{
"name": "Example Blog",
"url": "https://example.com/feed.xml",
"siteUrl": "https://example.com"
}
]
}- Configure date filtering (optional)
In eleventy.config.js, you can limit how far back to aggregate posts using ISO 8601 duration strings:
eleventyConfig.addPlugin(FeedAggregatorPlugin, {
configFile: "feeds.json",
durationLimit: "P90D", // Last 90 days
});Common duration examples:
P90D- 90 daysP1Y- 1 yearP1Y6M- 1 year and 6 monthsP2W- 2 weeksP6M- 6 months
Omit durationLimit to include all posts from the feeds.
- Development mode auto-reload
The feed configuration file is automatically watched in development mode. Changes to feeds.json will trigger a rebuild when running npm start.
- Rebuild the site
npm run buildFeeds are fetched at build time and cached in the static output.
Hot reload not working
- Check that you're running
npm start(notnpm run build) - Ensure no other process is using port 8080
Draft posts not showing
- Drafts only show in development mode (
npm start) - Check frontmatter has
draft: true
Search not working
- Run full build:
npm run build - Ensure
build:indexcompleted successfully - Check
public/pagefind/directory exists
CSS not applying
- Check CSS layer order
- Ensure import in
global.css - Clear browser cache
Build failing
- Run
npm run cleanto clear build artifacts - Delete
node_modules/and runnpm ci - Check Node.js version:
node --version(should be 24.15.0+) and npm version:npm --version(should be 12+)
For detailed build information:
DEBUG=Eleventy* npm start- Indentation - Tabs (size 2)
- Line width - 120 characters (200 for Markdown/SCSS)
- Quotes - Consistent use of template literals
- Semicolons - Required
- Braces - Always required, even for single-line statements
// ✅ Good - always use braces
if (condition) {
return null;
}
// ❌ Bad - no braces
if (condition) return null;# Check formatting
npm run lint
# Fix formatting
npm run formatAll code should pass npm run lint before committing.
main- Production branch (auto-deploys)- Feature branches for development
Use clear, descriptive commit messages:
git commit -m "feat: add dark mode toggle"
git commit -m "fix: correct search index generation"
git commit -m "docs: update development guide"Prefixes:
feat:- New featurefix:- Bug fixdocs:- Documentationstyle:- Code style/formattingrefactor:- Code refactoringtest:- Testschore:- Maintenance tasks
Before submitting changes:
- Local development - Test with
npm start - Production build - Test with
npm run build && npm run dev - Multiple browsers - Test in Chrome, Firefox, Safari
- Responsive design - Test on mobile and desktop viewports
- Dark mode - Test with system dark mode enabled
- Search functionality - Verify search works after rebuild
The project has two test suites, both run in CI on every push and pull request:
Unit tests (tests/unit/) - Cover the custom Eleventy plugins in 11ty/ using the Node.js built-in test runner:
npm run test:unitEnd-to-end tests (tests/e2e/) - Playwright browser tests covering the homepage, blog, feeds, reading page, dark mode, and page structure:
npm test # Headless run (starts the dev server automatically)
npm run test:ui # Interactive Playwright UI modeWhen changing a plugin in 11ty/, update its corresponding test in tests/unit/. When changing page structure or behavior, run npm test to catch regressions.
- Only rebuild search index when content changes
- Clear cache with
npm run cleanif builds seem stale npm startintentionally omits--incremental: the Lit SSR plugin (@lit-labs/eleventy-plugin-lit,mode: "worker") only imports component modules once per worker thread, so incremental rebuilds can silently keep serving stale component code after editingsrc/assets/js/components/. A full rebuild spawns a fresh worker every time, so component changes are always picked up without restarting the dev server.
- CSS is automatically minified via clean-css
- Images are optimized via
@11ty/eleventy-img - Minimal JavaScript (only web components)
- Static HTML loads instantly
- No JavaScript required for core functionality
- Web components enhance progressively
- Search runs entirely client-side
- Eleventy Documentation
- Lit Documentation
- PageFind Documentation
- Cloudflare Workers Documentation
- MDN Web Docs - Web standards reference
- Check existing documentation in
/docs/ - Review the architecture documentation
- Check the deployment guide
- Open an issue on GitHub