đ ProblĂ©matique
đŻ Objectifs
- Renouveler automatiquement le token Facebook, sans intervention manuelle, en le stockant dans Vercel Edge Config.
- Publier automatiquement les nouveaux articles enrichis avec les métadonnées OpenGraph (titre, description, image).
- Centraliser ces opérations dans des API routes Next.js.
- Piloter lâexĂ©cution avec des tĂąches planifiĂ©es via Vercel Cron.
đ Gestion automatique du token Facebook
-
Une API route
/api/cron/refresh-facebook-token
est appelée chaque jour par une tùche cron Vercel. - Elle vérifie si un token est présent dans Edge Config.
- Si oui, elle appelle lâAPI Graph de Facebook pour renouveler le token.
- Le nouveau token est stocké dans Edge Config.
- Les erreurs éventuelles sont loguées pour suivi.
typeScript /src/app/api/cron/refresh facebook token/route.ts
// /app/api/refresh-facebook-token/route.ts
import { NextResponse } from "next/server";
import { getAll, has, get } from '@vercel/edge-config';
// On récupÚre les identifiants de l'app Facebook
const facebook_app_id = process.env.FACEBOOK_APP_ID!;
const facebook_app_secret = process.env.FACEBOOK_APP_SECRET!;
// Route GET appelée par une tùche cron Vercel
export async function GET() {
try {
// Vérifie si un token existe déjà dans l'Edge Config
const hasFacebookToken = await has('facebook_token');
if (hasFacebookToken) {
const oldToken = await get('facebook_token') as string;
// On tente de renouveler le token
const result = await updateFacebookToken(oldToken);
if (result?.success) {
return NextResponse.json({
facebookToken: result.newToken,
status: 'success',
message: 'Token Facebook renouvelé.'
});
} else {
return NextResponse.json({
status: 'error',
message: 'Ăchec du renouvellement du token Facebook.'
}, { status: 500 });
}
}
// Aucun token Facebook trouvĂ© â retour d'Ă©tat
const configItems = await getAll();
return NextResponse.json({
configItems,
status: 'ok',
message: 'Aucun token Facebook Ă rafraĂźchir.'
});
} catch (err: any) {
console.error('Erreur lors du rafraĂźchissement du token :', err);
return NextResponse.json({
status: 'error',
message: 'Erreur serveur lors de la tentative de rafraĂźchissement.'
}, { status: 500 });
}
}
// Fonction qui appelle l'API Facebook pour renouveler le token
async function updateFacebookToken(oldToken: string): Promise<{ success: boolean, newToken?: string }> {
try {
// Appel à Facebook pour échanger le token
const res = await fetch(`https://graph.facebook.com/v21.0/oauth/access_token?grant_type=fb_exchange_token&client_id=${facebook_app_id}&client_secret=${facebook_app_secret}&fb_exchange_token=${oldToken}`);
const data = await res.json();
if (!data.access_token) {
console.error('Réponse invalide de Facebook :', data);
return { success: false };
}
// Mise Ă jour dans lâEdge Config
const updateRes = await fetch(
`https://api.vercel.com/v1/edge-config/${process.env.VERCEL_EDGE_ID}/items?teamId=${process.env.VERCEL_TEAM_ID}`,
{
method: 'PATCH',
headers: {
Authorization: `Bearer ${process.env.VERCEL_API_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
items: [
{
operation: 'update',
key: 'facebook_token',
value: data.access_token,
},
],
}),
}
);
const result = await updateRes.json();
if (updateRes.ok) {
return { success: true, newToken: data.access_token };
} else {
console.error('Ăchec de la mise Ă jour Vercel Edge:', result);
return { success: false };
}
} catch (error) {
console.error('Erreur dans updateFacebookToken:', error);
return { success: false };
}
}
đ° Publication automatique des articles rĂ©cents
/api/cron/social-share
, qui automatise la diffusion des articles récents.
Son fonctionnement est le suivant :
- Récupérer les articles publiés dans les derniÚres 24 heures.
-
Extraire les métadonnées OpenGraph via
open-graph-scraper
. - Générer les publications enrichies (titre, description, image).
-
Diffuser automatiquement :
- Sur LinkedIn (via API UGC v2).
- Sur Facebook (via Graph API Feed).
typeScript /src/app/api/cron/social share/route.ts
import { baseURL } from '@/app/resources';
import { getPosts } from '@/app/utils/serverActions';
import { NextResponse } from 'next/server';
import { postToFacebook } from './postToFacebook';
import { postToLinkedIn } from './postToLinkedIn';
const env = process.env.NODE_ENV
export async function GET(req: Request) {
if (!(env === 'development') && req.headers.get('Authorization') !== `Bearer ${process.env.CRON_SECRET}`) {
return NextResponse.json({ status: 'Unauthorized', code: 401 });
}
// 1. RécupÚre les articles publiés dans les derniÚres 24h
const articles = await getPosts({ limit: 1 });
// 2. Pour chaque article, publier sur les médias sociaux
for (const article of articles) {
if (article.metadata.publishedAt && isLessThan24HoursOld(article.metadata.publishedAt)) {
const postData = {
title: article.metadata.title as string,
description: article.metadata.description as string,
url: `${baseURL}/blog/${article.slug}`
};
await Promise.all([
// postToLinkedIn(postData),
postToFacebook(postData)
]);
console.log(`Partage de l'article ${article.slug} sur les média sociaux.`);
return NextResponse.json({ status: 'done', count: articles.length });
} else {
console.log(`L'article ${article.slug} a plus de 24 heures il ne sera pas diffusé !.`);
return NextResponse.json({ status: 'skipped', message: articles.length + ' article(s) ils ont plus de 24h et ont déjà était diffusés.' });
}
}
}
function isLessThan24HoursOld(dateString: string): boolean {
const publishedDate = new Date(dateString);
const now = new Date();
const diffMs = now.getTime() - publishedDate.getTime();
const diffHours = diffMs / (1000 * 60 * 60);
return diffHours < 24;
}
Exemple LinkedIn
typeScript /src/app/api/cron/social share/postToLinkedIn.ts
import { get } from '@vercel/edge-config';
import ogs from 'open-graph-scraper';
export async function postToLinkedIn(article: any) {
const accessToken = (await get('linkedin_token')) as string;
const personURN = process.env.LINKEDIN_AUTHOR_URN!; // ex: urn:li:person:abc123
// 1ïžâŁ RĂ©cupĂ©rer les donnĂ©es OpenGraph
const ogResult = await ogs({ url: article.url });
const ogData = ogResult.result;
// console.log('OpenGraph Data:', ogData);
// fallback en cas de champs manquants
const ogTitle = ogData.ogTitle || article.title;
const ogDescription = ogData.ogDescription || article.description;
let ogImage: string | undefined;
if (ogData.ogImage && Array.isArray(ogData.ogImage) && typeof ogData.ogImage[0] === 'object' && ogData.ogImage[0] !== null && 'url' in ogData.ogImage[0]) {
ogImage = ogData.ogImage[0].url;
}
// 2ïžâŁ Construire le post UGC avec OG
const postBody = {
author: personURN,
lifecycleState: 'DRAFT',
specificContent: {
'com.linkedin.ugc.ShareContent': {
shareCommentary: {
text: ogTitle
},
shareMediaCategory: 'ARTICLE',
media: [
{
status: 'READY',
description: {
text: ogDescription
},
originalUrl: article.url,
title: {
text: ogTitle
},
...(ogImage && {
thumbnails: [
{ resolvedUrl: ogImage }
]
})
}
]
}
},
visibility: {
'com.linkedin.ugc.MemberNetworkVisibility': 'PUBLIC'
}
};
// 3ïžâŁ Envoi vers LinkedIn
const res = await fetch('https://api.linkedin.com/v2/ugcPosts', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'X-Restli-Protocol-Version': '2.0.0',
'Content-Type': 'application/json'
},
body: JSON.stringify(postBody)
});
if (!res.ok) {
const error = await res.text();
console.error(`LinkedIn post failed: ${res.status}`, error);
throw new Error(`LinkedIn post failed: ${res.status} - ${error}`);
}
console.log('LinkedIn post succeeded!');
}
Exemple Facebook
typeScript /src/app/api/cron/social share/postToFacebook.ts
import { get } from '@vercel/edge-config';
export async function postToFacebook(article: any) {
const accessToken = await get('facebook_token') as string;
const pageId = process.env.FACEBOOK_PAGE_ID!;
const url = `https://graph.facebook.com/${pageId}/feed`;
const params = new URLSearchParams({
message:
`${article.title}
${article.description}`,
link: article.url,
access_token: accessToken
});
const res = await fetch(`${url}?${params.toString()}`, {
method: 'POST'
});
if (!res.ok) {
const error = await res.text();
console.error(`Facebook post failed: ${res.status}`, error);
throw new Error(`Facebook post failed: ${res.status} - ${error}`);
}
}
âïž Orchestration avec Vercel Cron
-
/api/cron/refresh-facebook-token
â tous les jours Ă 4h (renouvellement du token Facebook). -
/api/cron/social-share
â tous les jours Ă 5h (publication des articles).
JSON /vercel.json
{
"crons": [
{
"path": "/api/cron/social-share",
"schedule": "0 7 * * *"
},
{
"path": "/api/cron/refresh-facebook-token",
"schedule": "0 4 20 * *"
}
]
}
đ RĂ©sultats obtenus
- Le token Facebook reste toujours valide.
- Les articles récents sont automatiquement publiés sur les réseaux.
- Les publications sont enrichies avec des métadonnées OpenGraph.
- Le systĂšme fonctionne de maniĂšre autonome, fiable et invisible.
đ Perspectives dâĂ©volution
- ImplĂ©menter un fallback pour les erreurs dâAPI.
- Ătendre le systĂšme Ă dâautres plateformes (Mastodon, X...).
- Ajouter la publication rĂ©troactive dâanciens articles.
En rĂ©sumĂ© : un systĂšme automatisĂ©, robuste et lĂ©ger, qui simplifie la gestion des publications tout en amĂ©liorant la visibilitĂ© SEO du portfolio đ.
Ă quoi sert le systĂšme dâautomatisation prĂ©sentĂ© ?
Comment le token Facebook est-il renouvelé automatiquement ?
Comment les articles sont-ils partagés sur les réseaux sociaux ?
Comment Vercel Cron est-il configuré ?
Et si le token expire ou quâil y a une erreur ?
Peut-on Ă©tendre ce systĂšme Ă dâautres plateformes ?
Quâapporte ce dispositif en termes de SEO ?
Powered by wisp
Sources :
Commentaires

⚠Wisp.blog : Publier sur son blog sans friction ni déploiement
28 mai 2025
Créer un site Headless avec WordPress et Next.js
12 mai 2025
đ SEO & IA GĂ©nĂ©rative : Comment prĂ©parer votre site pour le futur des moteurs de recherche
22 juin 2025