Configuration API YouTube
Obtenir une clé API YouTube
Rendez-vous sur Google Cloud Console
Créez un nouveau projet
Activez YouTube Data API v3
Créez des identifiants → Clé API
Copiez votre clé API
Créer un compte Cloudflare
Rendez-vous sur Cloudflare
Créez un compte gratuit
Allez dans Workers & Pages
Cliquez sur Create application
Déployer le Worker
Ajouter le secret de la clé API
Ouvrez votre Worker dans Cloudflare
Allez dans Settings → Variables and Secrets
Cliquez sur Add → Secret
Nommez le secret : YOUTUBE_API_KEY
Collez votre clé API YouTube comme valeur
Enregistrez et redéployez le Worker
Créez un worker et collez ce code à l'intérieur :
function decodeHTMLEntities(str) {
if (typeof str !== 'string') return str
return str
.replace(/"/g, '"')
.replace(/'/g, "'")
.replace(/'/g, "'")
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/&#(\d+);/g, (_, num) => String.fromCharCode(parseInt(num, 10)))
.replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
}
function decodeSnippetFields(data) {
if (!data || !Array.isArray(data.items)) return data
data.items = data.items.map(item => {
if (!item.snippet) return item
const s = item.snippet
if (s.title) s.title = decodeHTMLEntities(s.title)
if (s.description) s.description = decodeHTMLEntities(s.description)
if (s.channelTitle) s.channelTitle = decodeHTMLEntities(s.channelTitle)
return item
})
return data
}
export default {
async fetch(request, env) {
// Gérer les CORS
if (request.method === 'OPTIONS') {
return new Response(null, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
}
})
}
// Seulement GET autorisé
if (request.method !== 'GET') {
return new Response(JSON.stringify({ error: 'Method not allowed' }), {
status: 405,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
})
}
try {
const url = new URL(request.url)
const channelId = url.searchParams.get('channelId')
let maxResults = url.searchParams.get('maxResults') || '10'
const allowShorts = url.searchParams.get('allowShorts') || 'false'
// Validation channelId
if (!channelId) {
return new Response(JSON.stringify({ error: 'channelId parameter is required' }), {
status: 400,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
})
}
// Validation format channelId (alphanumérique, tirets, underscores uniquement)
if (!/^[a-zA-Z0-9_-]+$/.test(channelId)) {
return new Response(JSON.stringify({ error: 'Invalid channelId format' }), {
status: 400,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
})
}
// Validation et limitation maxResults
maxResults = parseInt(maxResults, 10)
if (isNaN(maxResults) || maxResults < 1) {
maxResults = 10
}
// Limiter à 50 max (limite API YouTube)
if (maxResults > 50) {
maxResults = 50
}
maxResults = String(maxResults)
// Validation allowShorts
const safeAllowShorts = allowShorts === 'true'
// Récupérer la clé API depuis les secrets Cloudflare
const apiKey = env.YOUTUBE_API_KEY
if (!apiKey) {
return new Response(JSON.stringify({ error: 'API key not configured' }), {
status: 500,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
})
}
// Cache Cloudflare 24h : retourner la réponse en cache si présente
const cachedResponse = await caches.default.match(request)
if (cachedResponse) {
return cachedResponse
}
// Encoder les paramètres pour éviter l'injection
const encodedChannelId = encodeURIComponent(channelId)
const encodedMaxResults = encodeURIComponent(maxResults)
// Timeout de 10 secondes pour les requêtes API
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 10000)
try {
if (safeAllowShorts) {
const apiUrl = `https://www.googleapis.com/youtube/v3/search?part=snippet&channelId=${encodedChannelId}&maxResults=${encodedMaxResults}&order=date&type=video&videoDuration=short&key=${apiKey}`
const apiResponse = await fetch(apiUrl, { signal: controller.signal })
clearTimeout(timeoutId)
if (!apiResponse.ok) {
throw new Error(`YouTube API error: ${apiResponse.status}`)
}
const data = await apiResponse.json()
if (!data || typeof data !== 'object' || !Array.isArray(data.items)) {
throw new Error('Invalid response format from YouTube API')
}
decodeSnippetFields(data)
const response = new Response(JSON.stringify(data), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'public, max-age=86400'
}
})
await caches.default.put(request, response.clone())
return response
} else {
const [mediumResponse, longResponse] = await Promise.all([
fetch(`https://www.googleapis.com/youtube/v3/search?part=snippet&channelId=${encodedChannelId}&maxResults=${encodedMaxResults}&order=date&type=video&videoDuration=medium&key=${apiKey}`, { signal: controller.signal }),
fetch(`https://www.googleapis.com/youtube/v3/search?part=snippet&channelId=${encodedChannelId}&maxResults=${encodedMaxResults}&order=date&type=video&videoDuration=long&key=${apiKey}`, { signal: controller.signal })
])
clearTimeout(timeoutId)
if (!mediumResponse.ok || !longResponse.ok) {
const failedResponse = !mediumResponse.ok ? mediumResponse : longResponse
throw new Error(`YouTube API error: ${failedResponse.status}`)
}
const [mediumData, longData] = await Promise.all([
mediumResponse.json(),
longResponse.json()
])
if (!mediumData || typeof mediumData !== 'object' || !Array.isArray(mediumData.items)) {
throw new Error('Invalid response format from YouTube API (medium)')
}
if (!longData || typeof longData !== 'object' || !Array.isArray(longData.items)) {
throw new Error('Invalid response format from YouTube API (long)')
}
const combinedItems = [...(mediumData.items || []), ...(longData.items || [])]
combinedItems.sort((a, b) => {
const dateA = new Date(a.snippet?.publishedAt || 0)
const dateB = new Date(b.snippet?.publishedAt || 0)
return dateB - dateA
})
const limitedItems = combinedItems.slice(0, parseInt(maxResults, 10))
const outputData = { ...mediumData, items: limitedItems }
decodeSnippetFields(outputData)
const response = new Response(JSON.stringify(outputData), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'public, max-age=86400'
}
})
await caches.default.put(request, response.clone())
return response
}
} catch (fetchError) {
clearTimeout(timeoutId)
if (fetchError.name === 'AbortError') {
throw new Error('Request timeout')
}
throw fetchError
}
} catch (error) {
const errorMessage = error.message || 'Internal server error'
return new Response(JSON.stringify({
error: 'Internal server error',
message: errorMessage.includes('API key') ? 'Configuration error' : 'An error occurred'
}), {
status: 500,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
})
}
}
}Configuration Webflow
Ajoutez ceci dans le Head Code de votre page Webflow :
<!-- BeBranded Contents -->
<script async src="https://cdn.jsdelivr.net/npm/@bebranded/bb-contents@latest/bb-contents.js"></script>
<!-- Youtube Integration by bb-contents -->
<script>
window._bbContentsConfig = {
youtubeEndpoint: 'YOUR CLOUDFLARE WORKER URL HERE'
};
</script>Créez un div wrapper de grille dans Webflow.
Ajoutez bb-youtube-channel="UC_YOUR_CHANNEL_ID" sur le wrapper.
À l'intérieur, ajoutez un élément template avec bb-youtube-item.
Dans le template, marquez la miniature, le titre, la description, la date, etc.
Voir la section 4 pour tous les attributs disponibles.
Cloner le projet Webflow prêt à l'emploi (recommandé)
Le moyen le plus rapide de démarrer : clonez notre projet Made in Webflow.
La structure de la grille, les attributs et le style sont déjà configurés.
Clonez-le, collez l'URL de votre Worker dans le Head Code, mettez à jour votre ID de chaîne, et publiez.
https://webflow.com/made-in-webflow/website/bb-contents-youtube-grid
Après le clonage : ouvrez Project Settings → Custom Code → Head Code, collez le script ci-dessus et remplacez l'URL du Worker.
Vous pouvez aussi copier des sections individuelles dans votre propre projet.
Personnalisation du module
Comment fonctionnent les attributs
Ajoutez des attributs personnalisés sur les éléments Webflow (Paramètres de l'élément → Attributs personnalisés).
Les attributs de niveau grille vont sur le wrapper extérieur. Les attributs du template vont à l'intérieur de bb-youtube-item.
Le module récupère les vidéos via votre Worker Cloudflare et remplit automatiquement chaque template cloné.
ID de chaîne (wrapper de grille)
Valeur : un ou plusieurs ID de chaîne YouTube, séparés par des virgules.
Trouvez votre ID de chaîne dans YouTube Studio → Paramètres → Chaîne → Paramètres avancés.
Ne pas confondre avec bb-youtube-channel dans le template (celui-ci affiche le nom de la chaîne).
Nombre de vidéos dans la grille
Par défaut : 10 si omis. Tout nombre positif fonctionne.
Le Worker limite les requêtes à 50 (limite de l'API YouTube).
Ignorer les premières vidéos
Exemple : bb-youtube-skip="1" masque la dernière publication et affiche les suivantes.
Utile pour épingler une vidéo mise en avant séparément ou exclure du contenu récent.
Autoriser les Youtube Shorts ?
false (comportement par défaut) : uniquement les vidéos medium et long — les Shorts sont exclus.
true : inclut les YouTube Shorts dans la grille.
Langue du module
Langues supportées : Français (fr) — par défaut, Anglais (en).
Conteneur de grille (optionnel)
Le module clone le template à l'intérieur de cet élément conteneur.
Laissez vide si bb-youtube-item est directement dans le wrapper de grille.
Template vidéo (requis)
Ajoutez bb-youtube-item sur l'élément qui sera dupliqué pour chaque vidéo.
Fonctionne sur un link block, un div ou tout autre wrapper.
Si le template est un link block, l'URL YouTube est définie automatiquement sur href.
Masquez ce template dans Webflow (display: none) — le module le clone pour chaque vidéo.
Miniature vidéo
Aucune valeur nécessaire — src et alt sont remplis automatiquement.
Utilise la meilleure qualité de miniature disponible (jusqu'à 1280×720).
Titre de la vidéo
Affiche le titre de la vidéo depuis l'API YouTube.
Les entités HTML (&, ', etc.) sont décodées automatiquement.
Description de la vidéo
Affiche la description de la vidéo depuis YouTube.
Les longues descriptions peuvent nécessiter du CSS (line-clamp, max-height) pour contrôler la mise en page.
Date de publication
Affiche une date relative (ex. « il y a 3 jours »).
La langue suit bb-youtube-language.
Nom de la chaîne (dans le template)
À ajouter sur un élément Text pour afficher le nom de la chaîne YouTube.
Différent de bb-youtube-channel sur le wrapper de grille.





