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..1c4aed2c09c --- /dev/null +++ b/packages/docs/src/routes/demo/cookbook/blog-index/index.tsx @@ -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( + './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 ( +
+

Blog

+ +
+ ); +}); 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..25ab0767c5a --- /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 City's built-in MDX pipeline. + +The blog index reads its `frontmatter` export at build time 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..aaa74247c06 --- /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 City 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..b84bd3b2a8b --- /dev/null +++ b/packages/docs/src/routes/docs/cookbook/blog-index/index.mdx @@ -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. + + +```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( + './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 ( +
+

Blog

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

    {post.description}

    } +
  • + ))} +
+
+ ); +}); +``` +
+ +## 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`. diff --git a/packages/docs/src/routes/docs/cookbook/index.mdx b/packages/docs/src/routes/docs/cookbook/index.mdx index aac523233b0..f4b97e0ca7f 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 2520f475ff0..a9bb4231c1f 100644 --- a/packages/docs/src/routes/docs/menu.md +++ b/packages/docs/src/routes/docs/menu.md @@ -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)