Blog automatisé avec l'IA — 3/3 : Admin, Google Indexing & déploiement
Finalise le projet : interface admin pour piloter à la main, Google Indexing API pour une indexation en moins de 24h, et déploiement sur Vercel.
Partie 3/3 — Admin, Google Indexing & déploiement
Ce que tu vas obtenir à la fin de cette partie
- Une interface admin pour générer/publier/supprimer à la main
- Google Indexing API connectée → tes articles indexés en <24h
- Le projet déployé sur Vercel avec les crons actifs
- Une checklist complète de mise en production
Temps estimé : 45 minutes
Pré-requis : un compte Google Cloud (gratuit) et un compte Vercel (gratuit ou Pro pour les crons)
Étape 1 — Créer l'interface admin
Une page protégée pour piloter manuellement quand tu en as besoin (ex. publier en urgence un article, en supprimer un mauvais).
Crée src/app/admin/blog/actions.ts :
"use server";
import { revalidatePath } from "next/cache";
import { cookies } from "next/headers";
import { createClient } from "@supabase/supabase-js";
import { generateSEOArticle } from "@/lib/seo/article-generator";
import { requestIndexing, pingGoogleSitemap } from "@/lib/seo/google-indexing";
async function ensureAdmin() {
const cookieStore = await cookies();
const token = cookieStore.get("admin-token")?.value;
if (token !== process.env.ADMIN_SECRET) throw new Error("Unauthorized");
}
function adminClient() {
return createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);
}
// Publier un article scheduled immédiatement
export async function publishNow(id: number) {
await ensureAdmin();
const supabase = adminClient();
const now = new Date().toISOString();
const { data: article } = await supabase
.from("seo_articles").select("slug").eq("id", id).single();
await supabase.from("seo_articles")
.update({ status: "published", published_at: now })
.eq("id", id);
await requestIndexing(article!.slug);
await pingGoogleSitemap();
revalidatePath("/blog");
revalidatePath(`/blog/${article!.slug}`);
}
// Supprimer un article (mauvaise génération, etc.)
export async function deleteArticle(id: number) {
await ensureAdmin();
await adminClient().from("seo_articles").delete().eq("id", id);
revalidatePath("/blog");
}
// Insérer les mots-clés du fichier seed-keywords.ts
export async function seedKeywords() {
await ensureAdmin();
const { KEYWORDS } = await import("@/lib/seo/seed-keywords");
const supabase = adminClient();
let inserted = 0;
for (const k of KEYWORDS) {
const { error } = await supabase
.from("seo_keywords")
.insert(k);
if (!error) inserted++;
}
return { inserted, total: KEYWORDS.length };
}
Les 3 actions exposées :
| Action | Quand l'utiliser |
|---|---|
publishNow(id) |
Publier un scheduled sans attendre le cron de 7h |
deleteArticle(id) |
Article généré nul → poubelle |
seedKeywords() |
Au premier setup, pour remplir la file d'attente |
💡 La page admin elle-même (UI) est libre à toi : tableau des articles + boutons. Le minimum vital ce sont les actions ci-dessus.
Étape 2 — Configurer Google Indexing API
Sans Google Indexing API, ton article peut mettre plusieurs semaines à être découvert. Avec l'API : indexé en moins de 24h.
2.1 — Créer un Service Account Google Cloud
- Va sur console.cloud.google.com → New Project (ex.
blog-indexing) - APIs & Services → Library → cherche Web Search Indexing API → Enable
- APIs & Services → Credentials → Create Credentials → Service Account
- Donne-lui un nom (ex.
blog-indexer) → Create and Continue → skip les rôles → Done - Clique sur le service account créé → onglet Keys → Add Key → JSON → télécharge le fichier
Le JSON ressemble à ça :
{
"type": "service_account",
"client_email": "blog-indexer@xxx.iam.gserviceaccount.com",
"private_key": "-----BEGIN PRIVATE KEY-----\nMII...\n-----END PRIVATE KEY-----"
}
2.2 — Autoriser le Service Account dans Search Console
- Va sur search.google.com/search-console
- Vérifie ton domaine (méthode DNS recommandée)
- Settings → Users and permissions → Add user
- Email = le
client_emaildu JSON - Permission = Owner (obligatoire pour l'Indexing API)
2.3 — Ajouter les credentials dans .env.local
# Google Indexing API
GOOGLE_INDEXING_CLIENT_EMAIL=blog-indexer@xxx.iam.gserviceaccount.com
GOOGLE_INDEXING_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMII...\n-----END PRIVATE KEY-----"
NEXT_PUBLIC_SITE_URL=https://ton-domaine.com
# Admin (pour la page admin)
ADMIN_SECRET=remplace-par-un-autre-secret-aleatoire
⚠️ La
private_keydoit garder ses\nlittéraux. C'estreplace(/\\n/g, "\n")dans le code qui les retransforme en vrais sauts de ligne.
Étape 3 — Le module Google Indexing
Crée src/lib/seo/google-indexing.ts. Pas de dépendance externe : on signe le JWT à la main avec le module crypto natif de Node.
async function getAccessToken(): Promise<string> {
const now = Math.floor(Date.now() / 1000);
// 1. Construire le JWT (header + payload)
const header = Buffer
.from(JSON.stringify({ alg: "RS256", typ: "JWT" }))
.toString("base64url");
const payload = Buffer
.from(JSON.stringify({
iss: process.env.GOOGLE_INDEXING_CLIENT_EMAIL,
scope: "https://www.googleapis.com/auth/indexing",
aud: "https://oauth2.googleapis.com/token",
exp: now + 3600,
iat: now,
}))
.toString("base64url");
// 2. Signer avec la clé privée
const privateKey = process.env.GOOGLE_INDEXING_PRIVATE_KEY!
.replace(/\\n/g, "\n");
const { createSign } = await import("crypto");
const sign = createSign("RSA-SHA256");
sign.update(`${header}.${payload}`);
const signature = sign.sign(privateKey, "base64url");
const jwt = `${header}.${payload}.${signature}`;
// 3. Échanger le JWT contre un access_token
const res = await fetch("https://oauth2.googleapis.com/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion: jwt,
}),
});
const { access_token } = await res.json();
return access_token;
}
// Notifier Google qu'un article a été publié/mis à jour
export async function requestIndexing(slug: string) {
const token = await getAccessToken();
await fetch("https://indexing.googleapis.com/v3/urlNotifications:publish", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
url: `${process.env.NEXT_PUBLIC_SITE_URL}/blog/${slug}`,
type: "URL_UPDATED",
}),
});
}
// Pinger le sitemap après une publication batch
export async function pingGoogleSitemap() {
await fetch(
`https://www.google.com/ping?sitemap=${process.env.NEXT_PUBLIC_SITE_URL}/sitemap.xml`
);
}
Maintenant, branche-le dans le cron de publication (Partie 2, Étape 5) :
import { requestIndexing, pingGoogleSitemap } from "@/lib/seo/google-indexing";
// ... dans la boucle for (const article of articles)
await requestIndexing(article.slug);
// Après la boucle
if (articles?.length) await pingGoogleSitemap();
Étape 4 — Sitemap dynamique
Google a besoin d'un sitemap pour découvrir les nouveaux articles. Next.js le génère pour toi avec un fichier app/sitemap.ts :
import { MetadataRoute } from "next";
import { createClient } from "@supabase/supabase-js";
export const revalidate = 3600;
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
const { data: articles } = await supabase
.from("seo_articles")
.select("slug, updated_at")
.eq("status", "published");
const base = process.env.NEXT_PUBLIC_SITE_URL!;
return [
{ url: base, changeFrequency: "weekly", priority: 1 },
{ url: `${base}/blog`, changeFrequency: "daily", priority: 0.9 },
...(articles || []).map(a => ({
url: `${base}/blog/${a.slug}`,
lastModified: new Date(a.updated_at),
changeFrequency: "weekly" as const,
priority: 0.7,
})),
];
}
Tu trouves ton sitemap à https://ton-domaine.com/sitemap.xml.
Étape 5 — Déployer sur Vercel
- Push ton code sur GitHub
- Sur vercel.com/new → import du repo → Deploy
- Une fois déployé, va dans Settings → Environment Variables et ajoute toutes les variables de ton
.env.local:
| Variable | Source |
|---|---|
NEXT_PUBLIC_SUPABASE_URL |
Partie 1 |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Partie 1 |
SUPABASE_SERVICE_ROLE_KEY |
Partie 1 |
ANTHROPIC_API_KEY |
Partie 2 |
CRON_SECRET |
Partie 2 |
GOOGLE_INDEXING_CLIENT_EMAIL |
Partie 3 |
GOOGLE_INDEXING_PRIVATE_KEY |
Partie 3 |
NEXT_PUBLIC_SITE_URL |
Partie 3 |
ADMIN_SECRET |
Partie 3 |
- Redéploie (Deployments → ⋯ → Redeploy) pour que les variables soient prises en compte
- Vérifie que les crons sont bien actifs : Settings → Crons → tu dois voir tes 2 entrées de
vercel.json
Étape 6 — Premier test en production
- Va dans Search Console → Sitemaps → soumets
https://ton-domaine.com/sitemap.xml - Dans ton admin (ou directement via Supabase) → bouton "Seeder mots-clés" → tu dois voir tes mots-clés dans
seo_keywords - Déclenche le cron de génération à la main avec
curl:
curl -H "Authorization: Bearer ton-cron-secret" \
https://ton-domaine.com/api/cron/seo-blog/generate
- Vérifie dans Supabase qu'un article apparaît avec
status = scheduled - Le lendemain, le cron de publication le passera en
publishedet le notifiera à Google
✅ Checklist finale de mise en production
Base de données (Partie 1)
- Projet Supabase créé
- Tables
seo_articles+seo_keywordsen place - RLS activée
Génération (Partie 2)
- Clé API Anthropic configurée
-
article-generator.tstesté en local -
seed-keywords.tsrempli avec ≥ 20 mots-clés - 2 crons écrits et
vercel.jsonconfiguré
Indexation & déploiement (Partie 3)
- Service Account Google Cloud créé
- Service Account ajouté en Owner sur Search Console
-
google-indexing.tsbranché dans le cron de publication - Sitemap soumis à Google Search Console
- Variables d'env sur Vercel (les 9)
- Crons Vercel actifs
- Premier article généré en production
📊 Résultat attendu
| Métrique | Valeur |
|---|---|
| Articles publiés | 4 par semaine |
| Délai indexation Google | < 24h |
| Temps de maintenance | 0 (sauf relecture optionnelle) |
| Coût mensuel | ~1 € (Claude) + 0-20 € (Vercel) |
Après 3 mois : 50+ articles indexés, premiers trafics organiques sur la longue traîne.
📦 Ce que contient le pack à télécharger
README.md— ce guideadmin-actions.ts— server actions admin (Étape 1).env.example— variables Google + admin (Étape 2)google-indexing.ts— module d'indexation (Étape 3)sitemap.ts— sitemap dynamique (Étape 4)trigger-prod.sh— script de test en production (Étape 6)
🎉 Bravo, ton blog tourne tout seul.