每次构建我的110 个网站时,我都会渲染大约 3300 个页面 – 可能有点过头了。大约 615 个艺术家页面、1200 多个电影页面、500 个节目页面、500 个书籍页面和约 35 个流派页面。呼。构建时间徘徊在大约一分钟(仍然非常令人印象深刻!),但只会增加。
所有这些页面的数据都存储在Supabase的 postgres 中,并通过Directus [1]进行管理。每次我的网站构建时,它都会使用分页模板为每个类别中的每个条目生成一页。足够简单,但效率不高。
我拥有所有内容数据,每种数据类型都有一个 slug(因此,无论如何呈现,URL 都是稳定的),我所要做的就是更有效地呈现它们 – 也就是说,不是一次性全部呈现在构建时。
我的网站托管在Cloudflare上,并且已经大量使用工作人员,因此让工作人员处理这些不断增长的路线的渲染是有意义的:
name = "dynamic-media-worker" main = "./index.js" compatibility_date = "2023-01-01" account_id = "${CF_ACCOUNT_ID}" workers_dev = true [ observability ] enabled = true [ env.production ] name = "dynamic-media-worker-production" routes = [ { pattern = "https://coryd.dev/watching/movies/*" , zone_id = "${CF_ZONE_ID}" } , { pattern = "https://coryd.dev/watching/shows/*" , zone_id = "${CF_ZONE_ID}" } , { pattern = "https://coryd.dev/music/artists/*" , zone_id = "${CF_ZONE_ID}" } , { pattern = "https://coryd.dev/music/genres/*" , zone_id = "${CF_ZONE_ID}" } , { pattern = "https://coryd.dev/books/*" , zone_id = "${CF_ZONE_ID}" } , ]
这里没有什么令人兴奋的——路由被分配给工作人员并且日志记录被启用。接下来,我向站点基本模板中的标签添加了一系列data-dynamic
属性。每个属性都有一个值来指示标签是什么,以便可以动态更新 – title
、 description
、 page
等。好吧,现在来说说工人:
import { createClient } from "@supabase/supabase-js" ; import { fetchDataByUrl , fetchGlobals } from "./utils/fetchers.js" ; import { generateArtistHTML , generateBookHTML , generateGenreHTML , generateMetadata , generateWatchingHTML , } from "./utils/generators.js" ; import { updateDynamicContent } from "./utils/updaters.js" ; const BASE_URL = "https://coryd.dev" ; const NOT_FOUND_URL = ` ${ BASE_URL } /404 ` ; export default { async fetch ( request , env ) { const url = new URL ( request . url ) ; const path = url . pathname . replace ( / \/$ / , "" ) ; const supabaseUrl = env . SUPABASE_URL || process . env . SUPABASE_URL ; const supabaseKey = env . SUPABASE_KEY || process . env . SUPABASE_KEY ; const supabase = createClient ( supabaseUrl , supabaseKey ) ; let data , type ; if ( path === "/books" || path === "/books/" ) return fetch ( ` ${ BASE_URL } /books/ ` ) ; if ( path . startsWith ( "/books/years/" ) ) return fetch ( ` ${ BASE_URL } ${ path } ` ) ; if ( path . startsWith ( "/watching/movies/" ) ) { data = await fetchDataByUrl ( supabase , "optimized_movies" , path ) ; type = "movie" ; } else if ( path . startsWith ( "/watching/shows/" ) ) { data = await fetchDataByUrl ( supabase , "optimized_shows" , path ) ; type = "show" ; } else if ( path . startsWith ( "/music/artists/" ) ) { data = await fetchDataByUrl ( supabase , "optimized_artists" , path ) ; type = "artist" ; } else if ( path . startsWith ( "/music/genres/" ) ) { data = await fetchDataByUrl ( supabase , "optimized_genres" , path ) ; type = "genre" ; } else if ( path . startsWith ( "/books/" ) ) { data = await fetchDataByUrl ( supabase , "optimized_books" , path ) ; type = "book" ; } else { return Response . redirect ( NOT_FOUND_URL , 302 ) ; } if ( ! data ) return Response . redirect ( NOT_FOUND_URL , 302 ) ; const globals = await fetchGlobals ( supabase ) ; let mediaHtml ; switch ( type ) { case "artist" : mediaHtml = generateArtistHTML ( data , globals ) ; break ; case "genre" : mediaHtml = generateGenreHTML ( data , globals ) ; break ; case "book" : mediaHtml = generateBookHTML ( data , globals ) ; break ; default : mediaHtml = generateWatchingHTML ( data , globals , type ) ; break ; } const templateResponse = await fetch ( ` ${ BASE_URL } ` ) ; const template = await templateResponse . text ( ) ; const metadata = generateMetadata ( data , type , globals ) ; const html = updateDynamicContent ( template , metadata , mediaHtml ) ; const headers = new Headers ( { "Content-Type" : "text/html" , "Cache-Control" : "public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400" , } ) ; return new Response ( html , { headers } ) ; } , } ;
我导入了许多生成 HTML 的生成器函数和一个更新一些 HTML 的更新器。工作线程的核心处理请求,检查请求的路径并根据所述路径处理数据。如果路径无效,用户将被发送到 404 页面。
通过查询与请求匹配的 slug 从 Supabase 获取数据:
data = await fetchDataByUrl ( supabase , "optimized_artists" , path ) ; ... export const fetchDataByUrl = async ( supabase , table , url ) => { const { data , error } = await supabase . from ( table ) . select ( "*" ) . eq ( "url" , url ) . single ( ) ; if ( error ) { console . error ( ` Error fetching from ${ table } : ` , error ) ; return null ; } return data ; } ;
如果获取艺术家页面,工作人员将使用generateArtistHTML
生成该页面的HTML:
export const generateArtistHTML = ( artist , globals ) => { const playLabel = artist ?. [ "total_plays" ] === 1 ? "play" : "plays" ; const concertsList = artist [ "concerts" ] ?. length ? ` <hr /> <p id="concerts" class="concerts"> ${ ICON_MAP [ "deviceSpeaker" ] } I've seen this artist live! </p> <ul> ${ artist [ "concerts" ] . map ( generateConcertModal ) . join ( "" ) } </ul> ` : "" ; const albumsTable = artist [ "albums" ] ?. length ? ` <table> <tr><th>Album</th><th>Plays</th><th>Year</th></tr> ${ artist [ "albums" ] . map ( ( album ) => ` <tr> <td> ${ album [ "name" ] } </td> <td> ${ album [ "total_plays" ] || 0 } </td> <td> ${ album [ "release_year" ] } </td> </tr> ` ) . join ( "" ) } </table> <p><em>These are the albums by this artist that are in my collection, not necessarily a comprehensive discography.</em></p> ` : "" ; return ` <a class="icon-link" href="/music"> ${ ICON_MAP . arrowLeft } Back to music</a> <article class="artist-focus"> <div class="artist-display"> <img srcset=" ${ globals [ "cdn_url" ] } ${ artist [ "image" ] } ?class=w200&type=webp 200w, ${ globals [ "cdn_url" ] } ${ artist [ "image" ] } ?class=w600&type=webp 400w, ${ globals [ "cdn_url" ] } ${ artist [ "image" ] } ?class=w800&type=webp 800w " sizes="(max-width: 450px) 200px, (max-width: 850px) 400px, 800px" src=" ${ globals [ "cdn_url" ] } ${ artist [ "image" ] } ?class=w200&type=webp" alt=" ${ artist [ "name" ] } / ${ artist [ "country" ] } " loading="eager" decoding="async" width="200" height="200" /> <div class="artist-meta"> <p class="title"><strong> ${ artist [ "name" ] } </strong></p> <p class="sub-meta country"> ${ ICON_MAP [ "mapPin" ] } ${ parseCountryField ( artist [ "country" ] ) } </p> ${ artist [ "favorite" ] ? ` <p class="sub-meta favorite"> ${ ICON_MAP [ "heart" ] } This is one of my favorite artists!</p> ` : "" }
${ artist [ "tattoo" ] ? ` <p class="sub-meta tattoo"> ${ ICON_MAP [ "needle" ] } I have a tattoo inspired by this artist!</p> ` : "" }
${ artist [ "total_plays" ] ? ` <p class="sub-meta"><strong class="highlight-text"> ${ artist [ "total_plays" ] } ${ playLabel } </strong></p> ` : "" } <p class="sub-meta"> ${ artist [ "genre" ] ? ` <a href=" ${ artist [ "genre" ] [ "url" ] } "> ${ artist [ "genre" ] [ "name" ] } </a> ` : "" } </p> </div> </div> ${ generateAssociatedMediaHTML ( artist ) }
${ artist [ "description" ] ? ` <h2>Overview</h2> <div data-toggle-content class="text-toggle-hidden"> ${ md . render ( artist [ "description" ] ) } </div> <button data-toggle-button>Show more</button> ` : "" }
${ concertsList }
${ albumsTable } </article> ` ; } ;
这是一大堆笨拙的模板,到处都插入了标记值。根据响应的形状,有条件地呈现不同的元素。读起来相当多,但并非难以管理。 [2]
我收到了一个请求,生成了 HTML,并且收到了data-dynamic
数据属性。接下来,我获取我的主页(尽管任何页面都可以),因为它已由 11ty 处理和输出,并且允许我重新使用构建渲染模板的核心。
我generateMetadata
(包括为请求的视图构建标题、描述和图像路径)并使用我的基本模板、元数据和生成的 HTML 来构建最终页面:
import { parseHTML } from "linkedom" ; export const updateDynamicContent = ( html , metadata , mediaHtml ) => { const { document } = parseHTML ( html ) ; const titleTag = document . querySelector ( 'title[data-dynamic="title"]' ) ; if ( titleTag ) titleTag [ "textContent" ] = metadata [ "title" ] ; const dynamicMetaSelectors = [ { selector : 'meta[data-dynamic="description"]' , attribute : "content" , value : metadata [ "description" ] , } , { selector : 'meta[data-dynamic="og:title"]' , attribute : "content" , value : metadata [ "og:title" ] , } , { selector : 'meta[data-dynamic="og:description"]' , attribute : "content" , value : metadata [ "og:description" ] , } , { selector : 'meta[data-dynamic="og:image"]' , attribute : "content" , value : metadata [ "og:image" ] , } , { selector : 'meta[data-dynamic="og:url"]' , attribute : "content" , value : metadata [ "canonical" ] , } , ] ; dynamicMetaSelectors . forEach ( ( { selector , attribute , value } ) => { const element = document . querySelector ( selector ) ; if ( element ) element . setAttribute ( attribute , value ) ; } ) ; const canonicalLink = document . querySelector ( 'link[rel="canonical"]' ) ; if ( canonicalLink ) canonicalLink . setAttribute ( "href" , metadata [ "canonical" ] ) ; const pageElement = document . querySelector ( '[data-dynamic="page"]' ) ; if ( pageElement ) pageElement . innerHTML = mediaHtml ; return document . toString ( ) ; } ;
parseHTML
为我们提供了一个易于导航的 DOM 树,我们将每个元标记映射为数组中的对象进行更新,更新每个标记并将媒体 HTML 插入到文档中,然后将其全部作为字符串返回。
最后(最后)这一切都在带有一些Cache-Control
标头的响应中发送出去。加载页面需要稍微长一点的时间,然后页面会被缓存,我的构建时间会停止无情地增长,而且 – 因为我对每种页面类型都有 slugs – 我可以为静态和动态页面构建搜索索引和站点地图我的网站。 [3]
原文: https://coryd.dev/posts/2024/dynamic-pages-with-11ty-and-cloudflare-workers