Youtube Grid
BeBranded Contents brings powerful, no-code features to your Webflow sites through simple HTML attributes. Add social sharing, dynamic favicons and more with just a single script. Designed in France, built for the global Webflow community.
Designed for Webflow
Easy to implement

Obtain a Google API for Youtube
Go to this website: https://console.cloud.google.com/
Create an API key 'YouTube Data API v3'
Copy the key in the worker code below
Create an API key 'YouTube Data API v3'
Copy the key in the worker code below
Create a worker on Cloudflare
And paste this code inside it
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': '*'
}
})
}
}
}Script to paste into your Webflow project → Head Code
<!-- 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>