From 6aa694e81581b939637dad95b9f1964daa989b67 Mon Sep 17 00:00:00 2001 From: youcefzemmar Date: Thu, 28 May 2026 23:45:40 +0100 Subject: [PATCH] docs(cookbook): add blog index recipe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v2 sister PR to #8671 (which targets main/v1), requested by maiieul so the recipe lands on build/v2 without cross-branch merge conflicts. A compiled MDX/MD file's `default` export is a Qwik component, and the Glob Import recipe warns against `eager: true` for Qwik components. This v2 version drops eager: it scopes the glob to the `frontmatter` export (`import: 'frontmatter'`) so the component is never pulled in, and resolves the lazy entries server-side in a routeLoader$. - packages/docs/src/routes/docs/cookbook/blog-index/index.mdx — the recipe - packages/docs/src/routes/demo/cookbook/blog-index/ — runnable demo (index.tsx + 3 posts) - packages/docs/src/routes/docs/cookbook/index.mdx — overview entry (alphabetized) - packages/docs/src/routes/docs/menu.md — sidebar entry Co-Authored-By: Claude Opus 4.7 (1M context) --- .../routes/demo/cookbook/blog-index/index.tsx | 49 ++++++++ .../blog-index/posts/hello-world/index.mdx | 11 ++ .../blog-index/posts/markdown-only/index.md | 11 ++ .../posts/qwik-is-resumable/index.mdx | 11 ++ .../routes/docs/cookbook/blog-index/index.mdx | 105 ++++++++++++++++++ .../docs/src/routes/docs/cookbook/index.mdx | 1 + packages/docs/src/routes/docs/menu.md | 1 + 7 files changed, 189 insertions(+) create mode 100644 packages/docs/src/routes/demo/cookbook/blog-index/index.tsx create mode 100644 packages/docs/src/routes/demo/cookbook/blog-index/posts/hello-world/index.mdx create mode 100644 packages/docs/src/routes/demo/cookbook/blog-index/posts/markdown-only/index.md create mode 100644 packages/docs/src/routes/demo/cookbook/blog-index/posts/qwik-is-resumable/index.mdx create mode 100644 packages/docs/src/routes/docs/cookbook/blog-index/index.mdx diff --git a/packages/docs/src/routes/demo/cookbook/blog-index/index.tsx b/packages/docs/src/routes/demo/cookbook/blog-index/index.tsx new file mode 100644 index 00000000000..d8f7de7b8ae --- /dev/null +++ b/packages/docs/src/routes/demo/cookbook/blog-index/index.tsx @@ -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( + './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 ( +
+

Blog

+
    + {posts.value.map((post) => ( +
  • + {post.title} + — {post.date} + {post.description &&

    {post.description}

    } +
  • + ))} +
+
+ ); +}); diff --git a/packages/docs/src/routes/demo/cookbook/blog-index/posts/hello-world/index.mdx b/packages/docs/src/routes/demo/cookbook/blog-index/posts/hello-world/index.mdx new file mode 100644 index 00000000000..638e17d5638 --- /dev/null +++ b/packages/docs/src/routes/demo/cookbook/blog-index/posts/hello-world/index.mdx @@ -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. diff --git a/packages/docs/src/routes/demo/cookbook/blog-index/posts/markdown-only/index.md b/packages/docs/src/routes/demo/cookbook/blog-index/posts/markdown-only/index.md new file mode 100644 index 00000000000..189a00c8b00 --- /dev/null +++ b/packages/docs/src/routes/demo/cookbook/blog-index/posts/markdown-only/index.md @@ -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. diff --git a/packages/docs/src/routes/demo/cookbook/blog-index/posts/qwik-is-resumable/index.mdx b/packages/docs/src/routes/demo/cookbook/blog-index/posts/qwik-is-resumable/index.mdx new file mode 100644 index 00000000000..25151563eed --- /dev/null +++ b/packages/docs/src/routes/demo/cookbook/blog-index/posts/qwik-is-resumable/index.mdx @@ -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. diff --git a/packages/docs/src/routes/docs/cookbook/blog-index/index.mdx b/packages/docs/src/routes/docs/cookbook/blog-index/index.mdx new file mode 100644 index 00000000000..4d89478dafd --- /dev/null +++ b/packages/docs/src/routes/docs/cookbook/blog-index/index.mdx @@ -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$`: + + +```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( + './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 ( +
+

Blog

+
    + {posts.value.map((post) => ( +
  • + {post.title} + — {post.date} + {post.description &&

    {post.description}

    } +
  • + ))} +
+
+ ); +}); +``` +
+ +## 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`. diff --git a/packages/docs/src/routes/docs/cookbook/index.mdx b/packages/docs/src/routes/docs/cookbook/index.mdx index cd574152894..7970499f07e 100644 --- a/packages/docs/src/routes/docs/cookbook/index.mdx +++ b/packages/docs/src/routes/docs/cookbook/index.mdx @@ -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/) diff --git a/packages/docs/src/routes/docs/menu.md b/packages/docs/src/routes/docs/menu.md index f4bb8ec690a..c20df75ae2c 100644 --- a/packages/docs/src/routes/docs/menu.md +++ b/packages/docs/src/routes/docs/menu.md @@ -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)