Qwik v2 betaLighter, faster, better.

Blog Index

A blog index is a route that lists every post in a folder, with its title, date, and a link to the post itself. The posts are MDX (or plain Markdown) files โ€” no CMS, no database.

Qwik City's MDX pipeline parses YAML frontmatter and re-exports it as a named frontmatter export on the compiled module. That makes import.meta.glob the only primitive you need: glob the post folder, read each module's frontmatter, sort, render.

File layout

src/
โ””โ”€โ”€ routes/
    โ””โ”€โ”€ blog/
        โ”œโ”€โ”€ index.tsx                       # the index page (this recipe)
        โ””โ”€โ”€ posts/
            โ”œโ”€โ”€ hello-world/index.mdx       # /blog/posts/hello-world/
            โ”œโ”€โ”€ qwik-is-resumable/index.mdx # /blog/posts/qwik-is-resumable/
            โ””โ”€โ”€ markdown-only/index.md      # /blog/posts/markdown-only/

Each post sits in its own folder so Qwik City's directory-based routing turns it into a real, navigable page.

A post

The frontmatter is plain YAML. Anything you put there is available on the module's frontmatter export.

src/routes/blog/posts/hello-world/index.mdx
---
title: Hello, Qwik
date: '2024-03-01'
description: A short MDX post.
---
 
# Hello, Qwik
 
Welcome to the demo blogโ€ฆ

The index page

The index uses import.meta.glob with eager: true because we only need metadata at build time โ€” no need to defer-load each post just to read its title.

import { component$ } from '@builder.io/qwik';
import { Link } from '@builder.io/qwik-city';
 
type PostFrontmatter = {
  title: string;
  date: string;
  description?: string;
};
 
type PostModule = {
  frontmatter: PostFrontmatter;
};
 
const postModules = import.meta.glob<PostModule>(
  './posts/*/index.{md,mdx}',
  { eager: true }
);
 
const posts = Object.entries(postModules)
  .map(([path, mod]) => {
    // './posts/hello-world/index.mdx' -> 'hello-world'
    const slug = path.split('/').slice(-2, -1)[0];
    return { slug, ...mod.frontmatter };
  })
  .sort((a, b) => (a.date < b.date ? 1 : -1));
 
export default component$(() => {
  return (
    <div>
      <h1>Blog</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.slug}>
            <Link href={`./posts/${post.slug}/`}>{post.title}</Link>
            <small> โ€” {post.date}</small>
            {post.description && <p>{post.description}</p>}
          </li>
        ))}
      </ul>
    </div>
  );
});

Notes

  • eager: true inlines the matched modules into the index chunk. That's fine for listing metadata. If you also want to render the post body on the index (excerpts, etc.), the modules also expose a default component you can drop in directly.
  • The glob is resolved at build time, so adding a new post is a matter of creating the folder โ€” no registration step.
  • The date field is a string in YYYY-MM-DD form so lexicographic sort matches chronological sort. If you'd rather use real Date objects, parse them inside the .map.
  • To paginate, slice the sorted array; to filter by tag, narrow the frontmatter type and add a .filter step before .sort.

Contributors

Thanks to all the contributors who have helped make this documentation better!

  • youcefzemmar