This repo is a working Next.js example of how to embed the Redrover blog CMS at /blog on your own site. Clone it, run it, then port the pattern into your app.
The CMS itself lives at https://cms.tryredrover.com and exposes two POST endpoints that return ready-to-render HTML.
You POST a workspaceId (and optionally a blogId); the CMS responds with a complete HTML page styled for that workspace's appearance settings:
your site (e.g. mysite.com/blog) cms.tryredrover.com
| |
| POST /listblogs |
| { "workspaceId": "..." } |
| ------------------------------------> |
| <!doctype html>... full listing |
| <------------------------------------ |
| |
| POST /getblogcontent |
| { "workspaceId": "...", |
| "blogId": "<slug>" } |
| ------------------------------------> |
| <!doctype html>... full post page |
| <------------------------------------ |
Both endpoints return a complete HTML document (not a fragment). All CSS is inlined; the page is self-contained. You proxy the HTML through to the browser.
| Endpoint | Method | Body | Returns |
|---|---|---|---|
/listblogs |
POST | { "workspaceId": "<uuid>" } |
200 HTML, 400 if workspaceId missing |
/getblogcontent |
POST | { "workspaceId": "<uuid>", "blogId": "<slug>" } |
200 HTML, 404 if post not found, 400 on validation |
There's no API key — just include the workspaceId in the body. Your workspace ID is in the Redrover dashboard URL.
npm install
REDROVER_WORKSPACE_ID=<your-workspace-id> npm run devOpen http://localhost:3000/blog. The default workspace ID is hardcoded in app/blog/config.ts if you want to skip the env var while exploring.
Two env vars override the defaults:
| Var | Default | What it does |
|---|---|---|
REDROVER_WORKSPACE_ID |
(hardcoded sample) | Which workspace's posts to load |
REDROVER_BASE |
https://cms.tryredrover.com |
CMS host (override for staging/local CMS) |
app/
├── layout.tsx
├── page.tsx marketing-style home with a "View the blog" link
└── blog/
├── config.ts workspace ID + CMS base URL
├── lib.ts fetch helper + link rewriter
├── route.ts GET /blog → POST /listblogs
└── [slug]/route.ts GET /blog/<slug> → POST /getblogcontent
Five files total. Read them top-to-bottom in that order — it'll take you about two minutes.
config.ts — Two constants. Workspace ID + CMS base URL. Both env-overridable.
lib.ts — A single fetchHtml() helper that POSTs to the CMS, runs the response through rewriteBlogLinks(), and returns the HTML to the caller. Two thin wrappers — fetchBlogList() and fetchBlogPost(slug) — call it with the right body.
route.ts and [slug]/route.ts — Standard Next.js Route Handlers. Each one is two lines: import the helper, return what it gives you.
The CMS doesn't know what URL path you've mounted the blog under. So it emits:
href="/<slug>"for post linkshref="/"for the blog "home" link
If you mount the blog at /blog/... (like this example does), those hrefs would navigate to the wrong place. rewriteBlogLinks() does a string replace so they become /blog/<slug> and /blog. The order in the function matters — slug rewrite first, then home — see the comment in lib.ts.
If you mount the blog at the root of your domain (e.g. blog.mysite.com/), you can drop the rewriter entirely.
Assuming a Next.js App Router project. Adapt as needed for other frameworks — the CMS is just two HTTP endpoints.
// app/blog/config.ts
export const WORKSPACE_ID = process.env.REDROVER_WORKSPACE_ID!;
export const REDROVER_BASE =
process.env.REDROVER_BASE ?? "https://cms.tryredrover.com";// app/blog/lib.ts
import { REDROVER_BASE, WORKSPACE_ID } from "./config";
async function fetchHtml(path: string, body: unknown): Promise<Response> {
const upstream = await fetch(`${REDROVER_BASE}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
next: { revalidate: 60 }, // cache HTML for 60s — adjust to taste
});
const html = rewriteBlogLinks(await upstream.text());
return new Response(html, {
status: upstream.status,
headers: { "Content-Type": "text/html; charset=utf-8" },
});
}
export const fetchBlogList = () =>
fetchHtml("/listblogs", { workspaceId: WORKSPACE_ID });
export const fetchBlogPost = (blogId: string) =>
fetchHtml("/getblogcontent", { workspaceId: WORKSPACE_ID, blogId });
// Rewrite slug links first, then the home link — flipping the order would
// double-prefix `/` -> `/blog` -> `/blog/blog`.
function rewriteBlogLinks(html: string) {
return html
.replace(/href="\/([^/"#][^"]*)"/g, 'href="/blog/$1"')
.replace(/href="\/"/g, 'href="/blog"');
}// app/blog/route.ts
import { fetchBlogList } from "./lib";
export const GET = () => fetchBlogList();// app/blog/[slug]/route.ts
import { fetchBlogPost } from "../lib";
export async function GET(_req: Request, { params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
return fetchBlogPost(slug);
}REDROVER_WORKSPACE_ID=<your-workspace-uuid>Visit /blog and /blog/<slug>. That's it.
Can I get JSON instead of HTML? Not currently — both endpoints return rendered HTML.
How do I customize the rendering (colors, fonts, layout)? In the Redrover dashboard, under your workspace's blog appearance settings. The CMS reads those at request time, so changes go live without a redeploy.
404 on a post that exists. It's almost always because the post is unpublished. The CMS only serves posts where published = true.
The page renders unstyled / broken. Make sure you're returning the response body as Content-Type: text/html, not as a string in JSON. The CMS includes all CSS inline in the HTML — there's nothing extra to load.
Can I serve the blog at a path other than /blog? Yes — change the app/blog/... directory name and update the rewriter's prefix to match (e.g. /articles/$1). If you serve at the domain root, drop the rewriter.
Does it work outside Next.js? Yes. The CMS is just HTTP. Any backend that can fetch and return HTML works — Express, Hono, FastAPI, Rails, Django, plain Lambda, etc. The example happens to use Next.js Route Handlers because they're the most common case.