Trois intégrations prêtes à adapter, une par façon de brancher votre site (voir l'API des articles). Toutes lisent l'API avec la clé d'accès du site ; celles qui reçoivent une notification vérifient sa signature.
Chacune traite aussi la mise à jour d'un article déjà en ligne : un article dont replaces n'est pas null n'a pas de page à lui, il remplace le contenu de votre page qui porte ce slug. Les exemples Next.js et build statique relisent l'API à chaque rendu ou build : si la mise à jour est réécrite ou supprimée dans takotak, la page reprend son contenu d'avant ; l'extension WordPress, elle, a remplacé le contenu de l'article, qui le garde.
Next.js avec revalidation à la demande
Aucun build à relancer : la notification invalide la réponse gardée en cache, et la page se refait à la visite suivante. C'est ce que fait takotak.ai.
Variables du projet : TAKOTAK_ARTICLES_TOKEN (clé d'accès) et TAKOTAK_WEBHOOK_SECRET (clé de signature). Réglez ensuite l'adresse à prévenir sur la route /api/revalidate de votre site.
// lib/articles.ts : lu au rendu, gardé en cache jusqu'à la prochaine notification.
export const ARTICLES_TAG = "takotak-articles";
export async function getArticles() {
const res = await fetch("https://app.takotak.ai/api/v1/articles?include=content", {
headers: { authorization: `Bearer ${process.env.TAKOTAK_ARTICLES_TOKEN}` },
cache: "force-cache", // obligatoire : Next ne garde jamais seul une requête qui porte Authorization
next: { tags: [ARTICLES_TAG] }, // c'est cette étiquette que la route ci-dessous invalide
});
if (!res.ok) throw new Error(`API takotak : HTTP ${res.status}`); // la page déjà en cache reste servie
return (await res.json()).articles;
}
// Les articles qui ont leur page sous /blog : une mise à jour (replaces) n'en a pas.
export const getBlogArticles = async () => (await getArticles()).filter((a) => !a.replaces);
// La mise à jour d'une de vos pages, à rendre à la place de son contenu dans le gabarit de cette page.
export const getUpdate = async (slug: string) => (await getArticles()).find((a) => a.replaces && a.slug === slug) ?? null;
// app/blog/[slug]/page.tsx : un article paru après le déploiement doit pouvoir être rendu.
export const dynamicParams = true;
// app/api/revalidate/route.ts : l'adresse à prévenir, https://www.mon-site.fr/api/revalidate
import { revalidateTag } from "next/cache";
import { after } from "next/server";
import { verify } from "@/lib/takotak-signature"; // la fonction verify() de la page Webhook
import { ARTICLES_TAG } from "@/lib/articles";
export async function POST(req: Request) {
const raw = await req.text(); // le corps brut, avant JSON.parse
if (!verify(raw, req.headers.get("x-takotak-signature") ?? "", process.env.TAKOTAK_WEBHOOK_SECRET!)) {
return Response.json({ error: "signature" }, { status: 401 });
}
const { event, article } = JSON.parse(raw);
if (event === "article.test") return Response.json({ ok: true }); // l'essai ne désigne aucun vrai article
revalidateTag(ARTICLES_TAG, "max"); // Next 16 ; en Next 15 : revalidateTag(ARTICLES_TAG)
// Une mise à jour garde l'adresse de la page qu'elle remplace.
const url = article.replaces?.url ?? `${new URL(req.url).origin}/blog/${article.slug}`;
// « max » sert encore l'ancienne page à la première visite, le temps de la refaire : faites cette visite vous-même.
after(() => Promise.all([fetch(url), fetch(new URL("/blog", req.url))]).catch(() => {}));
return Response.json({ ok: true, ...(event === "article.published" ? { url } : {}) });
}
Attention
Trois pièges : sans dynamicParams = true, un article paru après le déploiement reste en 404 même revalidé, car sa page n'existait pas au build ; sans cache: "force-cache", la réponse n'est jamais gardée, donc lue à chaque visite ; et une liste, une page /blog/[slug], un sitemap ou un flux RSS qui lit getArticles() au lieu de getBlogArticles() donne une seconde adresse à chaque mise à jour.
Si la clé est réglée mais l'API injoignable au moment du build, laissez le build échouer : votre hébergeur garde le déploiement précédent en ligne, ce qui vaut mieux qu'un blog publié sans ses articles.
Un build qui lit l'API
Pour un site statique (Astro, Hugo, Eleventy, export statique) : un script lancé avant le build lit tous les articles en un appel et range les images chez vous. Réglez le « build hook » de votre hébergeur comme adresse à prévenir.
// Script de build (Node 18+) : lit tous les articles en un appel et range les images chez vous.
import { mkdir, writeFile } from "node:fs/promises";
const API = "https://app.takotak.ai/api/v1/articles";
const TOKEN = process.env.TAKOTAK_ARTICLES_TOKEN;
if (!TOKEN) process.exit(0); // pas configuré : le build continue sans
const res = await fetch(`${API}?include=content`, { headers: { authorization: `Bearer ${TOKEN}` } });
if (!res.ok) throw new Error(`API takotak : HTTP ${res.status}`); // un build raté garde le site en ligne tel quel
const { articles } = await res.json();
for (const a of articles) {
await mkdir(`public/blog/${a.slug}`, { recursive: true });
let html = a.html;
for (const img of a.images) {
const file = `public/blog/${a.slug}/${img.file}`;
await writeFile(file, Buffer.from(await (await fetch(img.url)).arrayBuffer()));
html = html.replaceAll(img.url, `/blog/${a.slug}/${img.file}`).replace(/ srcset="[^"]*"/g, "");
}
// Une mise à jour (replaces) n'a pas de page à elle : le gabarit de la page qui porte ce slug lit
// content/updates/<slug>.json à la place de son contenu, s'il existe.
await mkdir(a.replaces ? "content/updates" : "content/blog", { recursive: true });
await writeFile(`${a.replaces ? "content/updates" : "content/blog"}/${a.slug}.json`, JSON.stringify({ ...a, html }));
}
WordPress
Une extension de quelques lignes : elle vérifie la signature, lit l'article par l'API, range les images dans la médiathèque, crée ou met à jour l'article par son identifiant (une mise à jour d'un article déjà en ligne : par son slug) et renvoie son adresse. Réglez ensuite l'adresse à prévenir sur https://www.mon-site.fr/wp-json/takotak/v1/articles.
Dans wp-config.php, définissez TAKOTAK_WEBHOOK_SECRET (clé de signature) et TAKOTAK_API_TOKEN (clé d'accès), et reprenez la fonction takotak_verify() de la page Webhook.
<?php
// Extension minimale : wp-content/plugins/takotak-articles/takotak-articles.php
/* Plugin Name: takotak articles */
add_action('rest_api_init', function () {
register_rest_route('takotak/v1', '/articles', [
'methods' => 'POST', 'permission_callback' => '__return_true',
'callback' => function (WP_REST_Request $req) {
$raw = $req->get_body();
if (!takotak_verify($raw, $req->get_header('x-takotak-signature') ?? '', TAKOTAK_WEBHOOK_SECRET)) {
return new WP_REST_Response(['error' => 'signature'], 401);
}
$p = json_decode($raw, true);
$id = $p['article']['id'];
// Une mise à jour d'un article déjà en ligne (replaces) remplace l'article ou la page qui porte son slug : jamais un second article.
$replaces = !empty($p['article']['replaces']);
$existing = $replaces
? get_posts(['post_type' => ['post', 'page'], 'post_status' => 'any', 'name' => $p['article']['slug'], 'numberposts' => 1])
: get_posts(['post_type' => 'post', 'post_status' => 'any', 'meta_key' => 'takotak_id', 'meta_value' => $id, 'numberposts' => 1]);
if ($p['event'] === 'article.removed') {
if ($existing) wp_update_post(['ID' => $existing[0]->ID, 'post_status' => 'draft']);
return new WP_REST_Response(['ok' => true], 200);
}
if ($replaces && !$existing) return new WP_REST_Response(['error' => 'aucun article à ce slug'], 404);
// L'article, dans son dernier état, se lit par l'API.
$res = wp_remote_get($p['article']['url'], ['headers' => ['Authorization' => 'Bearer ' . TAKOTAK_API_TOKEN], 'timeout' => 15]);
// Une mise à jour réécrite ou supprimée depuis n'existe plus (404) : l'article garde son contenu.
if ($replaces && wp_remote_retrieve_response_code($res) === 404) return new WP_REST_Response(['ok' => true], 200);
if (wp_remote_retrieve_response_code($res) === 410) {
if ($existing) wp_update_post(['ID' => $existing[0]->ID, 'post_status' => 'draft']);
return new WP_REST_Response(['ok' => true], 200);
}
if (wp_remote_retrieve_response_code($res) !== 200) return new WP_REST_Response(['error' => 'api'], 502);
$a = json_decode(wp_remote_retrieve_body($res), true)['article'];
// Les images rejoignent la médiathèque ; leur adresse remplace celle de takotak dans le HTML.
require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/image.php';
$html = preg_replace('/ (srcset|sizes)="[^"]*"/', '', $a['html']); $hero_id = 0;
foreach ($a['images'] as $img) {
$att = media_sideload_image($img['url'], 0, $img['alt'], 'id');
if (is_wp_error($att)) continue;
if ($img['role'] === 'hero') $hero_id = $att;
$html = str_replace($img['url'], wp_get_attachment_url($att), $html);
}
$post = ['post_title' => $a['title'], 'post_name' => $a['slug'], 'post_content' => $html, 'post_excerpt' => $a['description'],
'post_status' => $p['event'] === 'article.test' ? 'draft' : 'publish', 'post_date_gmt' => gmdate('Y-m-d H:i:s', strtotime($a['publishedAt'])), 'post_type' => 'post'];
// Une mise à jour garde la date de publication, le statut et le type (article ou page) de ce qu'elle remplace.
if ($replaces) unset($post['post_date_gmt'], $post['post_status'], $post['post_type']);
if ($existing) { $post['ID'] = $existing[0]->ID; $post_id = wp_update_post($post); } else { $post_id = wp_insert_post($post); add_post_meta($post_id, 'takotak_id', $id, true); }
if ($hero_id) set_post_thumbnail($post_id, $hero_id);
return new WP_REST_Response(['url' => get_permalink($post_id)], 200);
},
]);
});
Une fois l'article publié chez vous, pensez à prévenir Bing.