之前我做了一个纯静态的个人 Geek 博客只包含index.html、assets、posts这些静态文件。部署到 Cloudflare Pages 后访问速度和维护成本都很舒服。但静态博客有一个明显问题每次修改个人信息、联系方式、项目介绍、文章内容都要手动改文件再重新部署。于是我把它改造成了一个带后台管理页面的在线博客系统前台仍然是 Cloudflare Pages 托管后端使用 Cloudflare Pages Functions数据存储使用 Cloudflare D1后台通过/admin/管理内容支持文章、个人资料、联系方式、项目和模板切换最终效果是不需要自己买服务器也不需要传统后端框架就能拥有一个轻量级 CMS。C:\Users\86182\Desktop\我的博客可以上瘾了一、项目整体结构改造后的目录结构如下geek-blog/ ├─ index.html ├─ post.html ├─ wrangler.toml ├─ schema.sql ├─ package.json ├─ assets/ │ ├─ main.js │ ├─ render.js │ └─ style.css ├─ admin/ │ ├─ index.html │ ├─ admin.js │ └─ admin.css ├─ functions/ │ └─ api/ │ ├─ _shared.js │ ├─ site.js │ ├─ posts/ │ │ └─ [slug].js │ └─ admin/ │ ├─ _auth.js │ ├─ login.js │ ├─ profile.js │ ├─ links.js │ ├─ projects.js │ ├─ template.js │ └─ posts/ │ └─ index.js └─ tests/ ├─ api.test.mjs ├─ admin-api.test.mjs └─ render.test.mjs这个版本已经不是单纯的静态页面而是一个 Cloudflare 原生全栈应用。二、为什么选择 Cloudflare Pages Functions D1这个博客的需求并不复杂管理个人资料管理联系方式管理项目列表管理文章切换博客模板不想维护服务器如果用传统方案可能会引入 Node.js 服务、数据库、登录系统、反向代理等一整套东西。但对个人博客来说这有点重。Cloudflare 的组合更轻Cloudflare Pages 托管前端静态资源 Pages Functions 提供 /api/* 后端接口 Cloudflare D1 存储博客内容 Pages Secret 保存后台管理 token Wrangler 部署和管理资源前台页面、后台页面、API、数据库都在 Cloudflare 上完成不需要额外服务器。三、Cloudflare 配置文件核心配置在wrangler.tomlname geek-blog pages_build_output_dir . compatibility_date 2026-06-29 [[d1_databases]] binding DB database_name geek_blog_db database_id 1471df18-9d11-48bc-b105-fd6670547b7d这里最关键的是 D1 绑定binding DB在 Pages Functions 中就可以通过env.DB访问数据库。例如awaitenv.DB.prepare(SELECT key, value FROM settings).all();这也是 Cloudflare Functions 很舒服的地方不需要自己管理数据库连接池也不需要配置复杂的 ORM。四、D1 数据表设计数据库初始化文件是schema.sql。一共设计了四张表CREATETABLEIFNOTEXISTSsettings(keyTEXTPRIMARYKEY,valueTEXTNOTNULL);settings用来保存全局配置例如当前模板个人资料因为这些配置结构变化可能比较灵活所以采用key-value JSON的方式。联系方式表CREATETABLEIFNOTEXISTSlinks(idINTEGERPRIMARYKEYAUTOINCREMENT,labelTEXTNOTNULL,urlTEXTNOTNULL,sort_orderINTEGERNOTNULLDEFAULT0);项目表CREATETABLEIFNOTEXISTSprojects(idINTEGERPRIMARYKEYAUTOINCREMENT,nameTEXTNOTNULL,descriptionTEXTNOTNULL,urlTEXTNOTNULL,tagsTEXTNOTNULLDEFAULT[],sort_orderINTEGERNOTNULLDEFAULT0);文章表CREATETABLEIFNOTEXISTSposts(idINTEGERPRIMARYKEYAUTOINCREMENT,slugTEXTNOTNULLUNIQUE,titleTEXTNOTNULL,excerptTEXTNOTNULLDEFAULT,contentTEXTNOTNULL,statusTEXTNOTNULLDEFAULTpublished,published_atTEXTNOTNULL,updated_atTEXTNOTNULLDEFAULTCURRENT_TIMESTAMP);文章使用slug作为公开访问标识例如/post.html?slughello-world这样比直接暴露数据库 id 更适合博客系统。五、前台 API聚合首页数据首页需要展示当前模板个人资料联系方式项目列表文章列表所以我设计了一个聚合接口GET /api/site对应源码是functions/api/site.jsexportasyncfunctiononRequestGet({env}){constsettingsRowsawaitenv.DB.prepare(SELECT key, value FROM settings).all();constsettingsObject.fromEntries(settingsRows.results.map((row)[row.key,parseJson(row.value,row.value),]));constlinksawaitenv.DB.prepare(SELECT label, url FROM links ORDER BY sort_order, id).all();constprojectsawaitenv.DB.prepare(SELECT name, description, url, tags FROM projects ORDER BY sort_order, id).all();constpostsawaitenv.DB.prepare(SELECT slug, title, excerpt, published_at FROM posts WHERE status published ORDER BY published_at DESC, id DESC).all();returnjson({template:settings.template||terminal,profile:settings.profile||{},links:links.results,projects:projects.results.map(normalizeProject),posts:posts.results,});}这个接口的好处是前台只需要请求一次就能拿到首页需要的所有数据。返回结构大致如下{template:terminal,profile:{name:YOUR_NAME,role:Engineer},links:[],projects:[],posts:[]}六、文章详情 API文章详情页使用动态路由GET /api/posts/:slugCloudflare Pages Functions 支持文件名动态参数functions/api/posts/[slug].js核心代码exportasyncfunctiononRequestGet({env,params}){constpostawaitenv.DB.prepare(SELECT slug, title, excerpt, content, published_at FROM posts WHERE slug ? AND status published).bind(params.slug).first();if(!post){returnjson({error:Post not found},404);}returnjson(post);}这里使用了 D1 的参数绑定.bind(params.slug)避免直接拼 SQL减少注入风险。七、后台鉴权设计后台不是公开接口所以需要鉴权。我没有引入完整用户系统而是采用了一个轻量方案ADMIN_TOKEN这个 token 保存在 Cloudflare Pages Secret 中。后台登录时输入 token前端保存到sessionStorage后续请求带上Authorization: Bearer token鉴权逻辑封装在functions/api/admin/_auth.jsexportfunctionisAuthorized(request,env){constheaderrequest.headers.get(authorization)||;consttokenheader.replace(/^Bearer\s/i,);returnBoolean(env.ADMIN_TOKENtokentokenenv.ADMIN_TOKEN);}exportfunctionrequireAdmin(request,env){if(!isAuthorized(request,env)){returnjson({error:Unauthorized},401);}returnnull;}这种方式不适合大型多人系统但对个人博客后台非常够用。八、后台文章管理接口文章管理接口在functions/api/admin/posts/index.js支持GET /api/admin/posts POST /api/admin/posts PUT /api/admin/posts DELETE /api/admin/posts写入前会先清洗数据functioncleanPost(body){return{slug:String(body?.slug||).trim(),title:String(body?.title||).trim(),excerpt:String(body?.excerpt||).trim(),content:String(body?.content||).trim(),status:body?.statusdraft?draft:published,published_at:String(body?.published_at||newDate().toISOString().slice(0,10)),};}再做基础校验functionvalidatePost(post){returnBoolean(post.slugpost.titlepost.contentpost.published_at);}创建文章时awaitenv.DB.prepare(INSERT INTO posts (slug, title, excerpt, content, status, published_at) VALUES (?, ?, ?, ?, ?, ?)).bind(post.slug,post.title,post.excerpt,post.content,post.status,post.published_at).run();删除文章时awaitenv.DB.prepare(DELETE FROM posts WHERE slug ?).bind(slug).run();整体保持了一个原则后台 API 不做复杂业务只负责数据校验和持久化。九、前台渲染层拆分为了让前台逻辑更清楚我把渲染函数单独拆到了assets/render.js例如渲染个人资料exportfunctionrenderProfile(profile{}){constnameprofile.name||YOUR_NAME;constrows[[role,profile.role],[stack,profile.stack],[location,profile.location],[status,profile.status],].filter(([,value])value);returnp你好我是 strong${escapeHtml(name)}/strong。/p p${escapeHtml(profile.intro||一名喜欢折腾的开发者。)}/p ul classkv${rows.map(([key,value])lispan classk${escapeHtml(key)}/spanspan classv${escapeHtml(value)}/span/li).join()}/ul;}这里有一个很重要的函数exportfunctionescapeHtml(value){returnString(value??).replace(/[]/g,(char)({:amp;,:lt;,:gt;,:quot;,:#39;,})[char]);}因为后台内容最终会渲染到页面上所以必须做 HTML 转义。否则如果文章标题、项目描述、个人简介里出现 HTML就可能污染页面结构甚至产生 XSS 风险。十、首页如何动态加载数据首页脚本是assets/main.js。核心逻辑asyncfunctionloadSite(){try{constresponseawaitfetch(/api/site);if(!response.ok)thrownewError(site api failed);renderSite(awaitresponse.json());}catch{constpostsawaitfetch(posts/index.json).then((response)(response.ok?response.json():[])).catch(()[]);renderSite({template:terminal,profile:{name:YOUR_NAME,role:Software Engineer,stack:TypeScript / Go / Rust / Linux,location:中国,status:online,},projects:[],links:[],posts,});}}这里做了一个降级策略优先请求/api/site如果 API 不可用则回退到静态posts/index.json这让前台在本地静态预览时也不至于完全空白。真正渲染时functionrenderSite(data){document.body.dataset.templatedata.template||terminal;setHtml(profile,renderProfile(data.profile));setHtml(project-list,renderProjects(data.projects));setHtml(link-list,renderLinks(data.links));setHtml(post-list,renderPosts(data.posts));}模板切换的关键就是document.body.dataset.templatedata.template||terminal;CSS 根据body[data-templatexxx]应用不同主题。十一、模板切换实现当前内置了三个模板terminal minimal cyberCSS 中通过属性选择器控制body[data-templateminimal]{--bg:#f7f7f4;--fg:#151515;--muted:#686868;--line:#deded8;background:var(--bg);}赛博风模板body[data-templatecyber]{--bg:#05070a;--fg:#eaf7ff;--muted:#7b8da1;--accent:#00e0ff;--line:#143344;}这种设计的优点是不需要切换 HTML不需要重新部署后台只保存一个模板名称前台读取配置后自动应用样式后台保存模板时调用PUT /api/admin/template接口会限制模板值只能是constTEMPLATESnewSet([terminal,minimal,cyber]);这是一个小但重要的白名单校验。十二、后台页面设计后台入口是/admin/页面文件admin/index.html admin/admin.css admin/admin.js后台分为五个模块资料联系方式项目文章模板前端保存 tokensessionStorage.setItem(adminToken,token);统一 API 请求函数asyncfunctionapi(path,options{}){constresponseawaitfetch(path,{method:options.method||GET,headers:{content-type:application/json,...(state.token?{authorization:Bearer${state.token}}:{}),},body:options.body?JSON.stringify(options.body):undefined,});if(!response.ok)thrownewError(awaitresponse.text());returnresponse.json();}所有后台写操作都会带上Authorization: Bearer token这样后端只需要复用requireAdmin()即可。十三、Markdown 文章渲染文章正文使用 Markdown 存储在 D1 的posts.content字段中。前台文章页是post.html?slughello-world它请求constresponseawaitfetch(/api/posts/${encodeURIComponent(slug)});然后使用renderMarkdown()渲染正文。当前 Markdown 渲染器支持一级到三级标题段落行内代码链接代码块代码块处理逻辑constcodeMatchline.match(/^(\w)?\s*$/);如果检测到代码块开始就进入inCode状态再次遇到 时输出html.push(precode classlanguage-${escapeAttr(codeLang)}${escapeHtml(code.join(\n))}/code/pre);这不是完整 Markdown 引擎但对个人技术博客第一版已经够用。十四、测试设计这个版本加入了 Node.js 内置测试{scripts:{test:node --test}}测试分为三类tests/api.test.mjs tests/admin-api.test.mjs tests/render.test.mjs覆盖内容包括/api/site是否返回首页聚合数据/api/posts/:slug是否返回文章详情后台登录 token 是否有效未授权写操作是否被拒绝模板、资料、文章写入是否正确前端渲染函数是否正确转义 HTMLMarkdown 渲染是否支持标题、链接、代码块实际运行结果# tests 12 # pass 12 # fail 0这点我觉得很重要个人项目也应该有测试尤其是带后台写入能力后接口行为需要可回归验证。十五、部署流程部署使用 Wrangler。创建 D1npx wrangler d1 create geek_blog_db初始化数据库npx wrangler d1 execute geek_blog_db--remote--fileschema.sql设置后台 tokennpx wrangler pages secret put ADMIN_TOKEN --project-name geek-blog部署 Pagesnpx wrangler pages deploy.--project-name geek-blog部署后访问https://geek-blog-aw8.pages.dev/后台地址https://geek-blog-aw8.pages.dev/admin/十六、这个架构的优点和限制优点不需要服务器不需要传统后端框架Cloudflare Pages 自动托管静态资源Pages Functions 直接提供 APID1 能满足轻量博客数据存储后台功能在线可用模板切换不需要重新部署成本很低适合个人博客限制当前登录系统只是单 token不适合多人协作Markdown 渲染器比较简单后台没有图片上传没有文章搜索没有版本历史后台 token 存在sessionStorage安全性够个人使用但不是企业级方案后续可以继续扩展接入 Cloudflare R2 做图片上传加文章搜索加草稿预览加访问统计加自定义域名加 GitHub 自动备份使用更完整的 Markdown 渲染库总结这次改造的核心思路是静态博客 ↓ Cloudflare Pages 托管前台 ↓ Pages Functions 提供 API ↓ Cloudflare D1 存储内容 ↓ /admin 后台在线管理相比传统博客系统这个方案更轻也更适合个人开发者。它不像 WordPress 那样庞大也不像纯静态博客那样每次都要改文件部署。它介于两者之间保留静态站点的轻量和速度同时拥有在线后台的便利。对于个人主页、作品集、技术博客、小型知识库来说Cloudflare Pages Functions D1 是一个非常值得尝试的组合。
从静态博客到在线 CMS:基于 Cloudflare Pages Functions + D1 改造个人 Geek 博客
之前我做了一个纯静态的个人 Geek 博客只包含index.html、assets、posts这些静态文件。部署到 Cloudflare Pages 后访问速度和维护成本都很舒服。但静态博客有一个明显问题每次修改个人信息、联系方式、项目介绍、文章内容都要手动改文件再重新部署。于是我把它改造成了一个带后台管理页面的在线博客系统前台仍然是 Cloudflare Pages 托管后端使用 Cloudflare Pages Functions数据存储使用 Cloudflare D1后台通过/admin/管理内容支持文章、个人资料、联系方式、项目和模板切换最终效果是不需要自己买服务器也不需要传统后端框架就能拥有一个轻量级 CMS。C:\Users\86182\Desktop\我的博客可以上瘾了一、项目整体结构改造后的目录结构如下geek-blog/ ├─ index.html ├─ post.html ├─ wrangler.toml ├─ schema.sql ├─ package.json ├─ assets/ │ ├─ main.js │ ├─ render.js │ └─ style.css ├─ admin/ │ ├─ index.html │ ├─ admin.js │ └─ admin.css ├─ functions/ │ └─ api/ │ ├─ _shared.js │ ├─ site.js │ ├─ posts/ │ │ └─ [slug].js │ └─ admin/ │ ├─ _auth.js │ ├─ login.js │ ├─ profile.js │ ├─ links.js │ ├─ projects.js │ ├─ template.js │ └─ posts/ │ └─ index.js └─ tests/ ├─ api.test.mjs ├─ admin-api.test.mjs └─ render.test.mjs这个版本已经不是单纯的静态页面而是一个 Cloudflare 原生全栈应用。二、为什么选择 Cloudflare Pages Functions D1这个博客的需求并不复杂管理个人资料管理联系方式管理项目列表管理文章切换博客模板不想维护服务器如果用传统方案可能会引入 Node.js 服务、数据库、登录系统、反向代理等一整套东西。但对个人博客来说这有点重。Cloudflare 的组合更轻Cloudflare Pages 托管前端静态资源 Pages Functions 提供 /api/* 后端接口 Cloudflare D1 存储博客内容 Pages Secret 保存后台管理 token Wrangler 部署和管理资源前台页面、后台页面、API、数据库都在 Cloudflare 上完成不需要额外服务器。三、Cloudflare 配置文件核心配置在wrangler.tomlname geek-blog pages_build_output_dir . compatibility_date 2026-06-29 [[d1_databases]] binding DB database_name geek_blog_db database_id 1471df18-9d11-48bc-b105-fd6670547b7d这里最关键的是 D1 绑定binding DB在 Pages Functions 中就可以通过env.DB访问数据库。例如awaitenv.DB.prepare(SELECT key, value FROM settings).all();这也是 Cloudflare Functions 很舒服的地方不需要自己管理数据库连接池也不需要配置复杂的 ORM。四、D1 数据表设计数据库初始化文件是schema.sql。一共设计了四张表CREATETABLEIFNOTEXISTSsettings(keyTEXTPRIMARYKEY,valueTEXTNOTNULL);settings用来保存全局配置例如当前模板个人资料因为这些配置结构变化可能比较灵活所以采用key-value JSON的方式。联系方式表CREATETABLEIFNOTEXISTSlinks(idINTEGERPRIMARYKEYAUTOINCREMENT,labelTEXTNOTNULL,urlTEXTNOTNULL,sort_orderINTEGERNOTNULLDEFAULT0);项目表CREATETABLEIFNOTEXISTSprojects(idINTEGERPRIMARYKEYAUTOINCREMENT,nameTEXTNOTNULL,descriptionTEXTNOTNULL,urlTEXTNOTNULL,tagsTEXTNOTNULLDEFAULT[],sort_orderINTEGERNOTNULLDEFAULT0);文章表CREATETABLEIFNOTEXISTSposts(idINTEGERPRIMARYKEYAUTOINCREMENT,slugTEXTNOTNULLUNIQUE,titleTEXTNOTNULL,excerptTEXTNOTNULLDEFAULT,contentTEXTNOTNULL,statusTEXTNOTNULLDEFAULTpublished,published_atTEXTNOTNULL,updated_atTEXTNOTNULLDEFAULTCURRENT_TIMESTAMP);文章使用slug作为公开访问标识例如/post.html?slughello-world这样比直接暴露数据库 id 更适合博客系统。五、前台 API聚合首页数据首页需要展示当前模板个人资料联系方式项目列表文章列表所以我设计了一个聚合接口GET /api/site对应源码是functions/api/site.jsexportasyncfunctiononRequestGet({env}){constsettingsRowsawaitenv.DB.prepare(SELECT key, value FROM settings).all();constsettingsObject.fromEntries(settingsRows.results.map((row)[row.key,parseJson(row.value,row.value),]));constlinksawaitenv.DB.prepare(SELECT label, url FROM links ORDER BY sort_order, id).all();constprojectsawaitenv.DB.prepare(SELECT name, description, url, tags FROM projects ORDER BY sort_order, id).all();constpostsawaitenv.DB.prepare(SELECT slug, title, excerpt, published_at FROM posts WHERE status published ORDER BY published_at DESC, id DESC).all();returnjson({template:settings.template||terminal,profile:settings.profile||{},links:links.results,projects:projects.results.map(normalizeProject),posts:posts.results,});}这个接口的好处是前台只需要请求一次就能拿到首页需要的所有数据。返回结构大致如下{template:terminal,profile:{name:YOUR_NAME,role:Engineer},links:[],projects:[],posts:[]}六、文章详情 API文章详情页使用动态路由GET /api/posts/:slugCloudflare Pages Functions 支持文件名动态参数functions/api/posts/[slug].js核心代码exportasyncfunctiononRequestGet({env,params}){constpostawaitenv.DB.prepare(SELECT slug, title, excerpt, content, published_at FROM posts WHERE slug ? AND status published).bind(params.slug).first();if(!post){returnjson({error:Post not found},404);}returnjson(post);}这里使用了 D1 的参数绑定.bind(params.slug)避免直接拼 SQL减少注入风险。七、后台鉴权设计后台不是公开接口所以需要鉴权。我没有引入完整用户系统而是采用了一个轻量方案ADMIN_TOKEN这个 token 保存在 Cloudflare Pages Secret 中。后台登录时输入 token前端保存到sessionStorage后续请求带上Authorization: Bearer token鉴权逻辑封装在functions/api/admin/_auth.jsexportfunctionisAuthorized(request,env){constheaderrequest.headers.get(authorization)||;consttokenheader.replace(/^Bearer\s/i,);returnBoolean(env.ADMIN_TOKENtokentokenenv.ADMIN_TOKEN);}exportfunctionrequireAdmin(request,env){if(!isAuthorized(request,env)){returnjson({error:Unauthorized},401);}returnnull;}这种方式不适合大型多人系统但对个人博客后台非常够用。八、后台文章管理接口文章管理接口在functions/api/admin/posts/index.js支持GET /api/admin/posts POST /api/admin/posts PUT /api/admin/posts DELETE /api/admin/posts写入前会先清洗数据functioncleanPost(body){return{slug:String(body?.slug||).trim(),title:String(body?.title||).trim(),excerpt:String(body?.excerpt||).trim(),content:String(body?.content||).trim(),status:body?.statusdraft?draft:published,published_at:String(body?.published_at||newDate().toISOString().slice(0,10)),};}再做基础校验functionvalidatePost(post){returnBoolean(post.slugpost.titlepost.contentpost.published_at);}创建文章时awaitenv.DB.prepare(INSERT INTO posts (slug, title, excerpt, content, status, published_at) VALUES (?, ?, ?, ?, ?, ?)).bind(post.slug,post.title,post.excerpt,post.content,post.status,post.published_at).run();删除文章时awaitenv.DB.prepare(DELETE FROM posts WHERE slug ?).bind(slug).run();整体保持了一个原则后台 API 不做复杂业务只负责数据校验和持久化。九、前台渲染层拆分为了让前台逻辑更清楚我把渲染函数单独拆到了assets/render.js例如渲染个人资料exportfunctionrenderProfile(profile{}){constnameprofile.name||YOUR_NAME;constrows[[role,profile.role],[stack,profile.stack],[location,profile.location],[status,profile.status],].filter(([,value])value);returnp你好我是 strong${escapeHtml(name)}/strong。/p p${escapeHtml(profile.intro||一名喜欢折腾的开发者。)}/p ul classkv${rows.map(([key,value])lispan classk${escapeHtml(key)}/spanspan classv${escapeHtml(value)}/span/li).join()}/ul;}这里有一个很重要的函数exportfunctionescapeHtml(value){returnString(value??).replace(/[]/g,(char)({:amp;,:lt;,:gt;,:quot;,:#39;,})[char]);}因为后台内容最终会渲染到页面上所以必须做 HTML 转义。否则如果文章标题、项目描述、个人简介里出现 HTML就可能污染页面结构甚至产生 XSS 风险。十、首页如何动态加载数据首页脚本是assets/main.js。核心逻辑asyncfunctionloadSite(){try{constresponseawaitfetch(/api/site);if(!response.ok)thrownewError(site api failed);renderSite(awaitresponse.json());}catch{constpostsawaitfetch(posts/index.json).then((response)(response.ok?response.json():[])).catch(()[]);renderSite({template:terminal,profile:{name:YOUR_NAME,role:Software Engineer,stack:TypeScript / Go / Rust / Linux,location:中国,status:online,},projects:[],links:[],posts,});}}这里做了一个降级策略优先请求/api/site如果 API 不可用则回退到静态posts/index.json这让前台在本地静态预览时也不至于完全空白。真正渲染时functionrenderSite(data){document.body.dataset.templatedata.template||terminal;setHtml(profile,renderProfile(data.profile));setHtml(project-list,renderProjects(data.projects));setHtml(link-list,renderLinks(data.links));setHtml(post-list,renderPosts(data.posts));}模板切换的关键就是document.body.dataset.templatedata.template||terminal;CSS 根据body[data-templatexxx]应用不同主题。十一、模板切换实现当前内置了三个模板terminal minimal cyberCSS 中通过属性选择器控制body[data-templateminimal]{--bg:#f7f7f4;--fg:#151515;--muted:#686868;--line:#deded8;background:var(--bg);}赛博风模板body[data-templatecyber]{--bg:#05070a;--fg:#eaf7ff;--muted:#7b8da1;--accent:#00e0ff;--line:#143344;}这种设计的优点是不需要切换 HTML不需要重新部署后台只保存一个模板名称前台读取配置后自动应用样式后台保存模板时调用PUT /api/admin/template接口会限制模板值只能是constTEMPLATESnewSet([terminal,minimal,cyber]);这是一个小但重要的白名单校验。十二、后台页面设计后台入口是/admin/页面文件admin/index.html admin/admin.css admin/admin.js后台分为五个模块资料联系方式项目文章模板前端保存 tokensessionStorage.setItem(adminToken,token);统一 API 请求函数asyncfunctionapi(path,options{}){constresponseawaitfetch(path,{method:options.method||GET,headers:{content-type:application/json,...(state.token?{authorization:Bearer${state.token}}:{}),},body:options.body?JSON.stringify(options.body):undefined,});if(!response.ok)thrownewError(awaitresponse.text());returnresponse.json();}所有后台写操作都会带上Authorization: Bearer token这样后端只需要复用requireAdmin()即可。十三、Markdown 文章渲染文章正文使用 Markdown 存储在 D1 的posts.content字段中。前台文章页是post.html?slughello-world它请求constresponseawaitfetch(/api/posts/${encodeURIComponent(slug)});然后使用renderMarkdown()渲染正文。当前 Markdown 渲染器支持一级到三级标题段落行内代码链接代码块代码块处理逻辑constcodeMatchline.match(/^(\w)?\s*$/);如果检测到代码块开始就进入inCode状态再次遇到 时输出html.push(precode classlanguage-${escapeAttr(codeLang)}${escapeHtml(code.join(\n))}/code/pre);这不是完整 Markdown 引擎但对个人技术博客第一版已经够用。十四、测试设计这个版本加入了 Node.js 内置测试{scripts:{test:node --test}}测试分为三类tests/api.test.mjs tests/admin-api.test.mjs tests/render.test.mjs覆盖内容包括/api/site是否返回首页聚合数据/api/posts/:slug是否返回文章详情后台登录 token 是否有效未授权写操作是否被拒绝模板、资料、文章写入是否正确前端渲染函数是否正确转义 HTMLMarkdown 渲染是否支持标题、链接、代码块实际运行结果# tests 12 # pass 12 # fail 0这点我觉得很重要个人项目也应该有测试尤其是带后台写入能力后接口行为需要可回归验证。十五、部署流程部署使用 Wrangler。创建 D1npx wrangler d1 create geek_blog_db初始化数据库npx wrangler d1 execute geek_blog_db--remote--fileschema.sql设置后台 tokennpx wrangler pages secret put ADMIN_TOKEN --project-name geek-blog部署 Pagesnpx wrangler pages deploy.--project-name geek-blog部署后访问https://geek-blog-aw8.pages.dev/后台地址https://geek-blog-aw8.pages.dev/admin/十六、这个架构的优点和限制优点不需要服务器不需要传统后端框架Cloudflare Pages 自动托管静态资源Pages Functions 直接提供 APID1 能满足轻量博客数据存储后台功能在线可用模板切换不需要重新部署成本很低适合个人博客限制当前登录系统只是单 token不适合多人协作Markdown 渲染器比较简单后台没有图片上传没有文章搜索没有版本历史后台 token 存在sessionStorage安全性够个人使用但不是企业级方案后续可以继续扩展接入 Cloudflare R2 做图片上传加文章搜索加草稿预览加访问统计加自定义域名加 GitHub 自动备份使用更完整的 Markdown 渲染库总结这次改造的核心思路是静态博客 ↓ Cloudflare Pages 托管前台 ↓ Pages Functions 提供 API ↓ Cloudflare D1 存储内容 ↓ /admin 后台在线管理相比传统博客系统这个方案更轻也更适合个人开发者。它不像 WordPress 那样庞大也不像纯静态博客那样每次都要改文件部署。它介于两者之间保留静态站点的轻量和速度同时拥有在线后台的便利。对于个人主页、作品集、技术博客、小型知识库来说Cloudflare Pages Functions D1 是一个非常值得尝试的组合。