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
42 changes: 42 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,42 @@
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 }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you try it out on v2? eager: true is not needed there iirc.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If no need for eager:true then could you make a sister PR for V2? We won't be able to resolve it with merge conflicts 🤓

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check #8679

);

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>
);
});
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 City's built-in MDX pipeline.

The blog index reads its `frontmatter` export at build time 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 City 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.
102 changes: 102 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,102 @@
---
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 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`](./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 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.

```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

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.

<CodeSandbox src="/src/routes/demo/cookbook/blog-index/" maxHeight={300}>
```tsx
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>
);
});
```
</CodeSandbox>

## 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`.
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 @@ -41,6 +41,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