Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions packages/docs/src/routes/demo/cookbook/blog-index/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { component$ } from '@qwik.dev/core';
import { Link, routeLoader$ } from '@qwik.dev/router';

type PostFrontmatter = {
title: string;
date: string;
description?: string;
};

// Scope the glob to the `frontmatter` export so it never pulls in each post's
// `default` (a Qwik component). That's also why there's no `eager: true` here —
// eager-importing Qwik components combines their outputs in a way that breaks
// Qwik (see the Glob Import recipe). The lazy entries are resolved server-side
// in the routeLoader$ below.
const postModules = import.meta.glob<PostFrontmatter>(
'./posts/*/index.{md,mdx}',
{
import: 'frontmatter',
}
);

export const useBlogPosts = routeLoader$(async () => {
const posts = await Promise.all(
Object.entries(postModules).map(async ([path, loadFrontmatter]) => {
// './posts/hello-world/index.mdx' -> 'hello-world'
const slug = path.split('/').slice(-2, -1)[0];
return { slug, ...(await loadFrontmatter()) };
})
);
return posts.sort((a, b) => (a.date < b.date ? 1 : -1));
});

export default component$(() => {
const posts = useBlogPosts();
return (
<div>
<h1>Blog</h1>
<ul>
{posts.value.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>
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
title: Hello, Qwik
date: '2024-03-01'
description: A short MDX post that imports a Qwik component.
---

# Hello, Qwik

Welcome to the demo blog. This post lives at `posts/hello-world/index.mdx` and is rendered by Qwik Router's built-in MDX pipeline.

The blog index reads its `frontmatter` export via `import.meta.glob` — no extra plumbing required.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
title: Plain Markdown Works Too
date: '2024-05-20'
description: The glob pattern picks up .md files alongside .mdx.
---

# Plain Markdown Works Too

This file is `.md` rather than `.mdx`, but the same `import.meta.glob` pattern covers both.

Frontmatter parsing is identical — Qwik Router exposes the parsed YAML as a `frontmatter` named export on the module.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
title: Qwik is Resumable
date: '2024-04-15'
description: A short note on why resumability beats hydration for cold starts.
---

# Qwik is Resumable

Instead of replaying component code on the client to reconstruct framework state, Qwik serializes that state into HTML during SSR and resumes from it.

The practical effect: the amount of JS needed before interactivity does not scale with app size.
105 changes: 105 additions & 0 deletions packages/docs/src/routes/docs/cookbook/blog-index/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
---
title: Cookbook | Blog Index
contributors:
- youcefzemmar
---

import CodeSandbox from '../../../../components/code-sandbox/index.tsx';

# 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 Router's MDX pipeline parses YAML frontmatter and re-exports it as a named `frontmatter` export on the compiled module. That makes [`import.meta.glob`](../glob-import/index.mdx) the only primitive you need: glob the post folder, read each module's `frontmatter`, sort, render.

## File layout

```bash
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 Router'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.

```markdown title="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

A compiled MDX/MD file's `default` export is a Qwik component, and the [Glob Import recipe](../glob-import/index.mdx) warns against `eager: true` for Qwik components — it makes Vite combine their outputs in a way that breaks Qwik. We don't need the component here anyway, only the metadata, so we scope the glob to the `frontmatter` export with `import: 'frontmatter'` and resolve the (lazy) entries inside a `routeLoader$`:

<CodeSandbox src="/src/routes/demo/cookbook/blog-index/" maxHeight={300}>
```tsx
import { component$ } from '@qwik.dev/core';
import { Link, routeLoader$ } from '@qwik.dev/router';

type PostFrontmatter = {
title: string;
date: string;
description?: string;
};

const postModules = import.meta.glob<PostFrontmatter>(
'./posts/*/index.{md,mdx}',
{
import: 'frontmatter',
}
);

export const useBlogPosts = routeLoader$(async () => {
const posts = await Promise.all(
Object.entries(postModules).map(async ([path, loadFrontmatter]) => {
// './posts/hello-world/index.mdx' -> 'hello-world'
const slug = path.split('/').slice(-2, -1)[0];
return { slug, ...(await loadFrontmatter()) };
})
);
return posts.sort((a, b) => (a.date < b.date ? 1 : -1));
});

export default component$(() => {
const posts = useBlogPosts();
return (
<div>
<h1>Blog</h1>
<ul>
{posts.value.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>
);
});
```
</CodeSandbox>

## Notes

- `import: 'frontmatter'` keeps the glob from pulling in each post's `default` component, so the index only ships metadata. If you also want to render post bodies on the index (excerpts, etc.), glob the `default` export and resolve it lazily the way the [Glob Import recipe](../glob-import/index.mdx) shows.
- `routeLoader$` runs on the server, so the frontmatter is read during SSR and serialized into the page — the client doesn't refetch the posts to build the list.
- 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`.
1 change: 1 addition & 0 deletions packages/docs/src/routes/docs/cookbook/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ A cookbook contains a collection of useful patterns for solving common problems
Examples:

- [Algolia search](./algolia-search/)
- [Blog Index](./blog-index/)
- [Combine Request Handlers](./combine-request-handlers/)
- [Debouncer](./debouncer/)
- [Deploy with Node using Docker](./node-docker-deploy/)
Expand Down
1 change: 1 addition & 0 deletions packages/docs/src/routes/docs/menu.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@

- [Overview](/docs/cookbook/index.mdx)
- [Algolia Search](/docs/cookbook/algolia-search/index.mdx)
- [Blog Index](/docs/cookbook/blog-index/index.mdx)
- [Combine Handlers](/docs/cookbook/combine-request-handlers/index.mdx)
- [Debouncer](/docs/cookbook/debouncer/index.mdx)
- [Fonts](/docs/cookbook/fonts/index.mdx)
Expand Down