CMS adapters
Integration guides — Next.js, WordPress, Strapi
Production-ready snippets: webhook verification, API fetch, and CMS upsert. Adapt table names to your schema.
All adapters follow the same pattern: verify HMAC on article.ready, pull the full post from Tenant API v1, then upsert into your CMS. Drafts are available immediately — you control when to publish.
Integration flow
- Create API key + webhook URL in GrowthDailySeo Integrations
- Expose a POST endpoint and verify X-GrowthDaily-Signature
- On article.ready, GET /api/v1/sites/{siteId}/posts/{slug}
- Upsert contentHtml, seo, and faqJsonLd — publish on your schedule
See also: Webhooks · API reference
Stack-specific adapters
Copy, adapt env vars and table names, deploy.
App Router route handler + lib helper. Works with Prisma, Drizzle, or any DB client.
Environment variables
# .env.local
GROWTHDAILY_API_BASE=https://app.growthdailyseo.com
GROWTHDAILY_API_KEY=fig_live_...
GROWTHDAILY_WEBHOOK_SECRET=whsec_...
GROWTHDAILY_SITE_ID=your-site-uuid
# YOUR public site (tenant domain — not app.growthdailyseo.com)
SITE_URL=https://www.your-domain.com
# Generate once; host plain-text file at https://www.your-domain.com/{INDEXNOW_KEY}.txt
INDEXNOW_KEY=your-random-indexnow-keyWebhook endpoint
// app/api/growthdaily/webhook/route.ts
import { createHmac, timingSafeEqual } from "crypto";
import { syncGrowthDailyPost } from "@/lib/growthdaily/sync";
export async function POST(req: Request) {
const raw = await req.text();
const ts = req.headers.get("x-growthdaily-timestamp") ?? req.headers.get("x-figrush-timestamp") ?? "";
const sig = req.headers.get("x-growthdaily-signature") ?? req.headers.get("x-figrush-signature") ?? "";
const expected = createHmac("sha256", process.env.GROWTHDAILY_WEBHOOK_SECRET!)
.update(`${ts}.${raw}`)
.digest("hex");
if (expected.length !== sig.length || !timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) {
return new Response("invalid signature", { status: 401 });
}
const event = JSON.parse(raw) as { event: string; siteId: string; slug: string };
if (event.event === "article.ready") {
await syncGrowthDailyPost(event.siteId, event.slug);
}
return Response.json({ ok: true });
}Sync function (fetch + upsert)
// lib/growthdaily/sync.ts
import { db } from "@/lib/db";
type GrowthDailyPost = {
slug: string;
title: string;
excerpt: string | null;
contentHtml: string;
status: "draft" | "published";
seo?: { metaDescription?: string; ogTitle?: string; ogDescription?: string; keywords?: string[] };
faqJsonLd?: Record<string, unknown>;
schemaBundle?: {
article: Record<string, unknown>;
breadcrumb: Record<string, unknown>;
faq?: Record<string, unknown> | null;
} | null;
};
export async function syncGrowthDailyPost(siteId: string, slug: string) {
const res = await fetch(
`${process.env.GROWTHDAILY_API_BASE}/api/v1/sites/${siteId}/posts/${slug}`,
{ headers: { Authorization: `Bearer ${process.env.GROWTHDAILY_API_KEY}` } },
);
if (!res.ok) throw new Error(`GrowthDailySeo fetch failed: ${res.status}`);
const post = (await res.json()) as GrowthDailyPost;
await db.post.upsert({
where: { slug: post.slug },
create: {
slug: post.slug,
title: post.title,
excerpt: post.excerpt,
html: post.contentHtml,
metaTitle: post.seo?.ogTitle ?? post.title,
metaDescription: post.seo?.metaDescription ?? post.excerpt,
faqJsonLd: post.faqJsonLd,
schemaBundle: post.schemaBundle ?? null,
status: post.status === "published" ? "PUBLISHED" : "DRAFT",
},
update: {
title: post.title,
excerpt: post.excerpt,
html: post.contentHtml,
metaTitle: post.seo?.ogTitle ?? post.title,
metaDescription: post.seo?.metaDescription ?? post.excerpt,
faqJsonLd: post.faqJsonLd,
schemaBundle: post.schemaBundle ?? null,
},
});
}Render / backfill
// app/blog/[slug]/page.tsx — render synced HTML + SEO JSON-LD bundle
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await db.post.findUnique({ where: { slug: params.slug } });
if (!post || post.status !== "PUBLISHED") notFound();
const bundle = post.schemaBundle ?? {
article: null,
breadcrumb: null,
faq: post.faqJsonLd ?? null,
};
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.html }} />
{bundle.article ? (
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(bundle.article) }} />
) : null}
{bundle.breadcrumb ? (
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(bundle.breadcrumb) }} />
) : null}
{bundle.faq ? (
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(bundle.faq) }} />
) : null}
</article>
);
}Sitemap feed
// Consume dynamic sitemap (Bearer or ?token= API key)
const res = await fetch(
`${process.env.GROWTHDAILY_API_BASE}/api/v1/sites/${process.env.GROWTHDAILY_SITE_ID}/sitemap.xml`,
{ headers: { Authorization: `Bearer ${process.env.GROWTHDAILY_API_KEY}` } },
);
const xml = await res.text();
// Serve at /sitemap.xml or merge into your sitemap indexIndexNow ping
// 1) Create public/abc123key.txt (content = "abc123key") on YOUR domain
// 2) Set INDEXNOW_KEY=abc123key in .env.local and niche_json.seo.indexnowKey in GrowthDailySeo
// 3) Optional ping after CMS sync (GrowthDailySeo also submits on publish)
const key = process.env.INDEXNOW_KEY!;
const siteOrigin = process.env.SITE_URL!.replace(/\/$/, "");
await fetch('https://api.indexnow.org/indexnow', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
host: new URL(siteOrigin).host,
key,
keyLocation: `${siteOrigin}/${key}.txt`,
urlList: [`${siteOrigin}/blog/${slug}`],
}),
});