Engineering Notes
A Paragraph CMS-native technical blog layout with search, filters, a featured post, and archive rows.
Engineering Notes renders a technical blog listing with a featured post, search, category popover, archive rows, RSS action, empty states, and load-more pagination.
Installation
pnpm dlx shadcn@latest add paragraphcms/shadcn-registry/engineering-notesnpx shadcn@latest add paragraphcms/shadcn-registry/engineering-notesyarn dlx shadcn@latest add paragraphcms/shadcn-registry/engineering-notesbunx --bun shadcn@latest add paragraphcms/shadcn-registry/engineering-notesInstall the runtime dependencies:
pnpm add @paragraphcms/client @tabler/icons-react clsx tailwind-merge @base-ui/react class-variance-authoritynpm install @paragraphcms/client @tabler/icons-react clsx tailwind-merge @base-ui/react class-variance-authorityyarn add @paragraphcms/client @tabler/icons-react clsx tailwind-merge @base-ui/react class-variance-authoritybun add @paragraphcms/client @tabler/icons-react clsx tailwind-merge @base-ui/react class-variance-authorityAdd these registry files to your project:
ui/engineering-notes.tsxui/aspect-ratio.tsxui/badge.tsxui/button.tsxui/input.tsxui/popover.tsxlib/utils.ts
Keep the aliases aligned with your components.json: @/components/ui/* and @/lib/utils.
Prerequisites
You need a Paragraph CMS API key available only on the server:
PARAGRAPH_API_KEY=your_api_keyCreate a server-side Paragraph CMS client:
import { Client } from "@paragraphcms/client";
const apiKey = process.env.PARAGRAPH_API_KEY;
if (!apiKey) {
throw new Error("PARAGRAPH_API_KEY environment variable is not set");
}
export const client = new Client({ apiKey });Engineering Notes expects PageSummaryWithSlug[]. It reads labels from post.labels, author metadata from post.author, and summaries from post.fields.excerpt or post.fields.summary.
Usage
Fetch posts from Paragraph CMS, pass the first item as featured_post, and render the remaining posts as the archive.
import { EngineeringNotes } from "@/components/ui/engineering-notes";
import { client } from "@/paragraph.config";
export default async function BlogPage() {
const { data: posts, error } = await client.pages.list({
collection: "engineering",
requiredSlug: true,
});
if (error) {
throw error;
}
return (
<EngineeringNotes
base_path="/blog"
featured_post={posts[0] ?? null}
posts={posts.slice(1)}
rss_href="/blog/rss.xml"
title="Engineering"
/>
);
}If you omit categories, the component builds its filter menu from the labels present on the fetched posts.