Editorial Feed
A Paragraph CMS-native editorial feed for React Router projects.
Editorial Feed renders a blog index with search, label filters, featured latest posts, RSS action, author avatars, and empty states.
Installation
pnpm dlx shadcn@latest add paragraphcms/shadcn-registry/editorial-feednpx shadcn@latest add paragraphcms/shadcn-registry/editorial-feedyarn dlx shadcn@latest add paragraphcms/shadcn-registry/editorial-feedbunx --bun shadcn@latest add paragraphcms/shadcn-registry/editorial-feedInstall the runtime dependencies:
pnpm add @paragraphcms/client @tabler/icons-react react-router clsx tailwind-merge @base-ui/react class-variance-authoritynpm install @paragraphcms/client @tabler/icons-react react-router clsx tailwind-merge @base-ui/react class-variance-authorityyarn add @paragraphcms/client @tabler/icons-react react-router clsx tailwind-merge @base-ui/react class-variance-authoritybun add @paragraphcms/client @tabler/icons-react react-router clsx tailwind-merge @base-ui/react class-variance-authorityAdd these registry files to your project:
ui/editorial-feed.tsxui/aspect-ratio.tsxui/avatar.tsxui/badge.tsxui/button.tsxui/input.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 });This component imports Link from react-router, so use it in a React Router route or adapt the link helper for another router.
Usage
Fetch published Paragraph CMS pages on the server and pass the result into EditorialFeed.
import { useLoaderData } from "react-router";
import type { PageSummaryWithSlug } from "@paragraphcms/client";
import { EditorialFeed } from "@/components/ui/editorial-feed";
import { client } from "@/paragraph.config";
export async function loader() {
const { data, error } = await client.pages.list({
collection: "blog",
requiredSlug: true,
});
if (error) {
throw error;
}
return { posts: data };
}
export default function BlogRoute() {
const { posts } = useLoaderData() as {
posts: PageSummaryWithSlug[];
};
return (
<EditorialFeed
basePath="/blog"
posts={posts}
rssHref="/blog/rss.xml"
title="Blog"
/>
);
}EditorialFeed uses post.labels for filters, post.author for metadata, post.heroUrl for cards, and post.slug to build links under basePath.