同一套 Astro SSR 跑多个域名,我是怎么处理 Host、D1 和缓存串站的

发布时间:2026/8/1 18:08:35
同一套 Astro SSR 跑多个域名,我是怎么处理 Host、D1 和缓存串站的 同一套 Astro SSR 跑多个域名我是怎么处理 Host、D1 和缓存串站的前段时间我在折腾一个 Astro 内容站。最开始想得很简单页面模板都一样只是域名、站名、栏目和配色不同那就让一套代码多绑几个域名省得每个站复制一份项目。真正做起来才发现页面复用反倒是最容易的部分。麻烦的是 Host 怎么识别、Cloudflare 缓存会不会串、Canonical 会不会写到另一个域名以及内容多了以后 Sitemap 怎么拆。现在项目已经跑在 Cloudflare Workers 上目录数据放 D1图片放 R2。我放一个实际页面方便对照本文说的 SSR 输出和链接结构92电影网。这篇不讲完整建站流程只记几个我自己改了不止一次的地方。先把站点差异收进配置不要散落在页面里第一版里我差点在组件里到处写域名判断。写两三个还能看继续加下去首页、详情页和 Sitemap 各有一套判断迟早对不上。后来我把站点相关的东西都收进SiteConfiginterfaceSiteConfig{domain:string;brand:string;routeKey:string;urlKey:string;tagline:string;accent:string;categories:Array{slug:string;label:string;};}域名、品牌名、路由 key、主题色和栏目都是数据。页面组件只关心“当前是哪一个配置”不再关心“当前是哪一个域名”。这个改动不复杂但后面做缓存、SEO 和数据库查询时都省了事因为它们拿到的是同一个site对象。Host 解析放在 Astro Middleware我的处理方式是在请求刚进来时解析 hostname去掉www和端口再到 Map 里查配置。constbyDomainnewMap(sites.map((site)[site.domain,site]));functiongetSiteByDomain(hostname:string){constnormalizedhostname.toLowerCase().split(:)[0].replace(/^www\./,);returnbyDomain.get(normalized);}exportconstonRequestdefineMiddleware(async(context,next){consturlnewURL(context.request.url);constsitegetSiteByDomain(url.hostname);if(!site){returnnewResponse(Unknown site,{status:404,headers:{x-robots-tag:noindex, nofollow,},});}context.locals.sitesite;returnnext();});之后在 Astro 页面里直接读Astro.locals.site。根域名和 HTTP 我统一 301 到https://www。一开始觉得这只是访问习惯问题后来检查页面才意识到如果几个版本都能返回 200Canonical、站内链接和外部抓取很容易出现不一致。if(url.hostnamesite.domain||url.protocol!https:){url.protocolhttps:;url.hostnamewww.${site.domain};returnResponse.redirect(url,301);}Workers 的预览域名我单独放行做测试但会加noindex。生产环境里遇到没有配置过的 Host直接 404不让它套用默认站点。D1 和 R2 的分工尽量简单目录数据在 D1 里保存标题、简介、地区、年份、集数以及图片对象键。图片本身放 R2。wrangler.jsonc中只声明 binding实际项目里的数据库 ID 等信息这里就不贴了{d1_databases:[{binding:DB,database_name:content-catalog}],r2_buckets:[{binding:POSTERS,bucket_name:content-assets}]}页面不直接拼 R2 存储地址而是统一走站内的/media/路由。这样以后换存储方式内容表和页面模板都不用跟着改。图片基本不会变我给/media/一年缓存if(pathname.startsWith(/media/)){returnpublic, max-age31536000, immutable;}HTML 就不能这么干。首页和栏目会更新用短缓存详情页稍长一点搜索页带用户输入直接private, no-store。最容易出问题的其实是缓存键多域名共用一个 Worker 时我最担心的是串站。比如两个域名都请求/如果自己做缓存键时只保留 pathname那么后缓存的页面可能被另一个域名直接拿走。页面看着还能打开但 title、Canonical、品牌名和内链已经全错了这种问题比报错更难发现。我最后没有单独拿 pathname 拼 key而是直接使用原始 RequestconstedgeCacheawaitcaches.open(content-sites-v1);constcachedawaitedgeCache.match(context.request);if(cached){returncached;}constresponseawaitnext();if(response.ok){awaitedgeCache.put(context.request,response.clone());}returnresponse;完整请求 URL 本身带 Host不同域名自然分开。当然也不是所有 GET 都适合进缓存。我排除了搜索页、带 Authorization 的请求、明确要求no-cache的请求以及中转媒体请求。缓存只是加速手段不能反过来改变页面应该有的行为。为了排查方便我还会在响应头里写x-movier-cache: HIT或MISS。浏览器里一眼能看到比凭感觉猜 Worker 到底有没有命中省事。SEO 信息不能写成一套公共常量多域名项目里Canonical 写死是个低级但很实际的错误。我先根据站点配置得到主 OriginfunctionprimaryOrigin(site:SiteConfig){returnhttps://www.${site.domain};}每个页面再用当前路径生成自己的 Canonicalconstcanonical${primaryOrigin(site)}${pathname};首页输出WebSite详情页按内容类型输出TVSeries栏目页输出ItemList。搜索页我设为noindex,follow因为搜索参数组合太多没必要让它们和正式栏目、详情页抢索引。Sitemap 也碰到过同样的问题。内容少时一个 XML 就够了数据多起来后我改成 Sitemap Index固定页面放main.xml详情 URL 每 2000 条拆一个文件。constpagesMath.max(1,Math.ceil(total/2000));constentries[${origin}/sitemaps/main.xml,...Array.from({length:pages},(_,index)${origin}/sitemaps/items-${index1}.xml,),];这里的origin一定来自当前 Host 对应的站点配置。否则 Sitemap 能正常打开里面却可能混着另一个域名检查不仔细很难发现。我现在上线前怎么检查我不会只看首页能不能打开而是同时开两个不同域名请求相同路径对照 title、Canonical、内链和缓存响应头。然后再看根域名是否只跳一次、搜索页有没有进缓存、预览域名有没有noindex以及 Sitemap 里有没有混入其他 Host。另一个容易漏掉的情况是空栏目。数据库暂时没有匹配内容时与其照常输出一个可以索引的空页面不如先设成noindex,follow也不要放进 Sitemap。等有真实内容再开放索引。回头看这个项目最花时间的地方并不是 Astro 组件而是保证“当前站点身份”从请求进来到页面输出始终一致。Host 解析、D1 查询、缓存键、Canonical 和 Sitemap 只要有一处用了另一套判断线上就会出现很别扭的问题。目前这套方案跑下来比较稳定。以后再加栏目或改页面我只动公共组件和站点配置不用维护很多份差不多的代码。对于内容结构接近、又确实需要独立域名和独立 SEO 信息的项目这种做法还是挺省心的。