skier. v1.0.0

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 page

Flat structure: Items as direct HTML files:

generateItemsTask({
  flatStructure: true,
  // ...
})
public/posts/
├── hello-world.html
├── second-post.html
└── index.html

Real-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.

headings is populated by Skier's built-in Markdown renderer. If you supply a custom markdownRenderer, it returns HTML only, so headings will 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 format