generateItemsTask
Generate HTML pages from a collection of Markdown files (blog posts, portfolio items, etc.).
When to Use
✅ Use generateItemsTask when you have:
- Blog posts as Markdown files
- Portfolio projects with frontmatter
- Any collection of similar content items
❌ Use generatePaginatedItemsTask instead when:
- You need paginated list pages
- Data comes from JSON rather than Markdown
Quick Start
generateItemsTask({
itemsDir: 'src/items/posts',
partialsDir: 'src/partials',
outDir: 'public/posts',
outputVar: 'posts',
})Input: Markdown files in src/items/posts/
Output: HTML pages in public/posts/ + globals.posts array
Configuration
| Option | Type | Required | Description |
|---|---|---|---|
itemsDir |
string | ✅ | Directory containing Markdown files |
partialsDir |
string | ✅ | Directory with Handlebars partials |
outDir |
string | ✅ | Output directory for generated HTML |
outputVar |
string | ✅ | Variable name for items array in globals |
templateExtension |
string | Template file extension (default: .html) |
|
partialExtension |
string | Partial file extension (default: .html) |
|
flatStructure |
boolean | If true, treat all files as items (no subdirs) | |
sortFn |
function | Custom sort function | |
excerptFn |
function | Custom excerpt extractor | |
linkFn |
function | Custom URL generator | |
outputPathFn |
function | Custom output path generator | |
additionalVarsFn |
function | Inject extra template variables (receives ItemisedRenderVars object) |
|
linkRewrite |
object | Link rewriting config for Markdown output | |
frontmatterParser |
function | Custom frontmatter parser | |
markdownRenderer |
function | Custom markdown renderer | |
extractDate |
function | Custom date extraction function |
Type signature
The exact GenerateItemsConfig interface, transcluded from source so it stays in step with the shipping code:
export interface GenerateItemsConfig {
/** Directory containing markdown/HTML items */
itemsDir: string;
/** Directory containing partials */
partialsDir: string;
/** Output directory */
outDir: string;
/** Name of the variable to output the item list as */
outputVar: string;
/** Optional link rewriting config for Markdown output /
linkRewrite?: LinkRewriteConfig;
/* Extension for section templates (default: .html) /
templateExtension?: string;
/* Extension for partials (default: .html) /
partialExtension?: string;
/* If true, treat all files in itemsDir as items (no sections). Default: false */
flatStructure?: boolean;
/** Custom frontmatter parser /
frontmatterParser?: (raw: string) => FrontmatterData;
/* Custom excerpt extractor /
excerptFn?: (raw: string, meta: FrontmatterData) => string | Promise<string>;
/* Custom sorting function for items /
sortFn?: (a: SkierItem, b: SkierItem) => number;
/* Custom link generator /
linkFn?: (args: { section: string; itemName: string; meta: FrontmatterData }) => string;
/* Custom output path generator /
outputPathFn?: (args: { section: string; itemName: string; meta: FrontmatterData }) => string;
/* Custom markdown renderer /
markdownRenderer?: (md: string) => string | Promise<string>;
/* User override for date extraction /
extractDate?: (args: ItemFunctionArgs) => Date | string | undefined;
/* Optional function to inject additional variables per item */
additionalVarsFn?: (
args: ItemisedRenderVars,
) => Record<string, unknown> | Promise<Record<string, unknown>>;
}
Every task's config type together: API Reference → Config Interfaces.
Sorting Items
Default: items are sorted by date (newest first).
Custom sort by title:
generateItemsTask({
// ...
sortFn: (a, b) => a.title.localeCompare(b.title),
})Reverse chronological (explicit):
sortFn: (a, b) => new Date(b.date) - new Date(a.date),Extracting Excerpts
Default: first paragraph, capped at 200 characters.
Use a marker (<!--more-->):
generateItemsTask({
// ...
excerptFn: (content) => {
const [excerpt] = content.split('<!--more-->');
return excerpt.trim();
},
})Output Structure
Default (nested): Each item gets its own directory:
public/posts/
├── hello-world/
│ └── index.html
├── second-post/
│ └── index.html
└── index.html # List pageFlat structure: Items as direct HTML files:
generateItemsTask({
flatStructure: true,
// ...
})public/posts/
├── hello-world.html
├── second-post.html
└── index.htmlReal-World Example
From a blog with categories and reading time:
generateItemsTask({
itemsDir: 'src/items/posts',
partialsDir: 'src/partials',
outDir: 'public/posts',
outputVar: 'posts',
sortFn: (a, b) => new Date(b.date) - new Date(a.date),
excerptFn: (content) => {
const text = content.replace(/<[^>]*>/g, ''); // Strip HTML
return text.slice(0, 200) + '...';
},
additionalVarsFn: (vars) => ({
readingTime: Math.ceil(vars.content.split(' ').length / 200),
relatedPosts: (vars.posts || [])
.filter(p => p.category === vars.category && p.slug !== vars.slug)
.slice(0, 3),
}),
})Template Variables
Each item template receives:
{
// From frontmatter
title: "Hello World",
date: "2024-01-15",
category: "Tech",
tags: ["javascript", "web"],
// Auto-generated
slug: "hello-world",
content: "<p>Rendered HTML...</p>",
excerpt: "First part of content...",
link: "/posts/hello-world/",
// Ordered list of the page's headings, in document order.
// Each heading in `content` also carries a matching `id` anchor.
headings: [
{ level: 2, text: "Getting Started", slug: "getting-started" },
{ level: 3, text: "Install the CLI", slug: "install-the-cli" },
],
// From additionalVarsFn
readingTime: 5,
// All globals
siteTitle: "My Blog",
posts: [/* all posts */],
}Heading Anchors & On-Page TOC
Every heading in rendered Markdown is emitted with a slugged id, so headings
are deep-linkable (e.g. /posts/hello-world/#getting-started). Slugs are
lower-cased, punctuation-stripped, and collision-safe: repeated headings get
-2, -3, … suffixes so every anchor on a page is unique.
The same headings are exposed to the template as the headings array (in
document order), which you can use to build a right-rail table of contents:
<nav class="toc">
<ul>
{{#each headings}}
<li class="toc-level-{{level}}">
<a href="#{{slug}}">{{text}}</a>
</li>
{{/each}}
</ul>
</nav>Each entry carries its level, so a small amount of CSS (e.g. hiding
.toc-level-1, indenting .toc-level-3) is all it takes to render only the
levels you want. Skier's built-in Handlebars environment ships a safe #each
but no comparison helpers, so filter by level in CSS rather than in the
template.
headingsis populated by Skier's built-in Markdown renderer. If you supply a custommarkdownRenderer, it returns HTML only, soheadingswill be empty.
Common Mistakes
❌ Forgetting outputVar:
// Items won't be available in templates!
generateItemsTask({
itemsDir: 'src/posts',
// Missing: outputVar: 'posts'
})❌ Using wrong task order:
// Wrong: generatePagesTask can't access posts yet
generatePagesTask({ /* ... */ }),
generateItemsTask({ outputVar: 'posts' }),❌ Invalid date formats:
# Frontmatter
date: January 15, 2024 # ❌ Won't parse correctly
date: 2024-01-15 # ✅ ISO formatRelated Tasks
- generatePaginatedItemsTask — For paginated lists
- generateFeedTask — Use with
outputVarfor RSS/Atom - generateSitemapTask — Auto-includes generated pages