React Router
@citedseo/react-router gives your loaders the feed and your routes their meta tags. Two routes with server loaders, in React Router v7’s framework mode. It needs React Router 7.9 or later, the first version that escapes JSON-LD in meta.
Hand this to your coding agent
Read https://developer.citedseo.ai/llms-full.txt. My CitedSEO site ID is <YOUR_SITE_ID>. Install @citedseo/react-router. Add blog._index and blog.$slug routes with loaders and meta in this React Router v7 app, reading my articles with that package. The feed needs no key. Use only the functions and addresses that file lists.
Install
Section titled “Install”npm install @citedseo/react-routerpnpm add @citedseo/react-routeryarn add @citedseo/react-routerbun add @citedseo/react-routerWhat you’ll add
Section titled “What you’ll add”- .env
Directoryapp
- routes.ts your routes
Directoryroutes
- blog._index.tsx the index
- blog.$slug.tsx one article
- api.citedseo.ts the webhook
-
Set your site ID.
.env CITEDSEO_SITE_ID=YOUR_SITE_ID -
Add the index.
app/routes/blog._index.tsx import { fetchArticles } from '@citedseo/react-router';import { Link } from 'react-router';import type { Route } from './+types/blog._index';export async function loader() {const articles = await fetchArticles({ siteId: process.env.CITEDSEO_SITE_ID! });return {articles: articles.map(({ id, slug, title, description }) => ({ id, slug, title, description })),};}export default function Blog({ loaderData }: Route.ComponentProps) {return (<ul>{loaderData.articles.map((article) => (<li key={article.id}><Link to={`/blog/${article.slug}`}>{article.title}</Link><p>{article.description}</p></li>))}</ul>);} -
Add the article route.
articleMetagives the title, description, canonical link, Open Graph tags and JSON-LD.app/routes/blog.$slug.tsx import { articleMeta, fetchArticle } from '@citedseo/react-router';import { data } from 'react-router';import type { Route } from './+types/blog.$slug';export async function loader({ params }: Route.LoaderArgs) {const article = await fetchArticle({siteId: process.env.CITEDSEO_SITE_ID!,slug: params.slug,});if (!article) throw data(null, { status: 404 });return { article };}export function meta({ data }: Route.MetaArgs) {return data ? articleMeta(data.article) : [];}export default function ArticlePage({ loaderData }: Route.ComponentProps) {const { article } = loaderData;return (<article><h1>{article.title}</h1>{article.cover && <img src={article.cover.src} alt={article.cover.alt} />}<div dangerouslySetInnerHTML={{ __html: article.html }} /></article>);} -
Register the routes. If your app lists its routes in
app/routes.ts, add these three. With@react-router/fs-routesandflatRoutes(), the file names above are found without it.app/routes.ts import { type RouteConfig, route } from '@react-router/dev/routes';export default [// …your other routesroute('blog', 'routes/blog._index.tsx'),route('blog/:slug', 'routes/blog.$slug.tsx'),route('api/citedseo', 'routes/api.citedseo.ts'),] satisfies RouteConfig;
When you publish
Section titled “When you publish”Each request reads the manifest, which is cached for a minute, so a new article shows within a minute with no webhook at all.
If you add your own cache in front of your pages, clear it from a resource route. webhookHandler checks the signature for you:
import { webhookHandler } from '@citedseo/react-router';import type { Route } from './+types/api.citedseo';
const handle = webhookHandler({ secret: process.env.CITEDSEO_WEBHOOK_SECRET!, onPost: async (post) => { // Clear your cache, or store post.data. },});
export const action = ({ request }: Route.ActionArgs) => handle(request);See Update the moment you publish.
Keep the address the same
Section titled “Keep the address the same”Each article’s url is where CitedSEO expects it to live, and its structured data and canonical address say so. If your blog isn’t at /blog, set the site’s blog path in the app’s settings to match your route. See SEO for your article pages.