Adaptadores CMS

Guias de integração — Next.js, WordPress, Strapi

Snippets prontos para produção: verificação de webhook, fetch via API e upsert no CMS. Adapte os nomes das tabelas ao seu schema.

Todos os adaptadores seguem o mesmo padrão: verificar HMAC em article.ready, buscar o artigo completo na Tenant API v1 e fazer upsert no CMS. Rascunhos ficam disponíveis imediatamente — você decide quando publicar.

Fluxo de integração

  1. Crie chave API e URL de webhook em Integrações do GrowthDailySeo
  2. Exponha um endpoint POST e verifique X-GrowthDaily-Signature
  3. Em article.ready, GET /api/v1/sites/{siteId}/posts/{slug}
  4. Faça upsert de contentHtml, seo e faqJsonLd — publique no seu cronograma

Veja também: Webhooks · Referência da API

Adaptadores por stack

Copie, adapte variáveis de ambiente e nomes de tabelas, faça deploy.

Route handler do App Router + helper em lib. Funciona com Prisma, Drizzle ou qualquer cliente de banco.

Variáveis de ambiente
# .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-key
Endpoint de webhook
// 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 });
}
Função de sync (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,
    },
  });
}
Renderização e preenchimento
// 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>
  );
}
Feed do sitemap
// 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 index
Ping do IndexNow
// 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}`],
  }),
});