QUICKSTARTS BY STACK
The fast path: open the Connect wizard and pick Dynamic site (SSR / on-request rendering). Remix loaders run on every request, so all you need is the env vars — no webhook receiver code.
Index loader
// app/routes/blog._index.tsx
import { json, useLoaderData, type LoaderFunctionArgs } from "react-router";
export async function loader({ context }: LoaderFunctionArgs) {
const res = await fetch(`${process.env.MENTIONWELL_API_URL}/api/public/${process.env.MENTIONWELL_SITE_SLUG}/posts?limit=24`, {
headers: { Authorization: `Bearer ${process.env.MENTIONWELL_API_KEY}` }
});
const { posts } = await res.json();
return json({ posts }, { headers: { "Cache-Control": "public, s-maxage=300, stale-while-revalidate=86400" } });
}
export default function BlogIndex() {
const { posts } = useLoaderData<typeof loader>();
return <ul>{posts.map((p: any) => <li key={p.slug}>{p.title}</li>)}</ul>;
}
Detail loader
// app/routes/blog.$slug.tsx
import { json, useLoaderData, type LoaderFunctionArgs } from "react-router";
export async function loader({ params }: LoaderFunctionArgs) {
const res = await fetch(`${process.env.MENTIONWELL_API_URL}/api/public/${process.env.MENTIONWELL_SITE_SLUG}/posts/${params.slug}`, {
headers: { Authorization: `Bearer ${process.env.MENTIONWELL_API_KEY}` }
});
if (res.status === 404) throw new Response("Not Found", { status: 404 });
const { post } = await res.json();
return json({ post });
}
export default function BlogPost() {
const { post } = useLoaderData<typeof loader>();
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.html }} />
</article>
);
}
Webhook action
// app/routes/api.mentionwell.ts
import { Buffer } from "node:buffer";
import { createHmac, timingSafeEqual } from "node:crypto";
import { json, type ActionFunctionArgs } from "react-router";
export async function action({ request }: ActionFunctionArgs) {
if (request.method !== "POST") throw new Response("Method Not Allowed", { status: 405 });
const raw = await request.text();
const signature = request.headers.get("x-mentionwell-signature") ?? "";
const expected = createHmac("sha256", process.env.MENTIONWELL_WEBHOOK_SECRET!).update(raw).digest("hex");
const verified =
signature.length === expected.length &&
timingSafeEqual(Buffer.from(signature, "utf8"), Buffer.from(expected, "utf8"));
if (!verified) return json({ ok: false }, { status: 401 });
const { post } = JSON.parse(raw) as { post?: { slug?: string } };
// Purge your CDN/app cache for /blog and post?.slug here.
return json({ ok: true, slug: post?.slug ?? null });
}