
Munder Difflin 博客工程实战用 Eleventy 子项目实现双主题、单一媒体清单与一键发布【免费下载链接】munder-difflinA local multi-agent harness that works with your existing Claude Code, Codex subscriptions, allows you to run an office of agents项目地址: https://gitcode.com/GitHub_Trending/mu/munder-difflinMunder Difflin 仓库里的blog/目录是一个完全独立的 Eleventy 静态站点子项目它用自己的package.json、独立构建到docs/blog/复用仓库现有的 GitHub Pages 部署直接出现在站点的/blog路径下。本文以 BLOG_README 为主干结合 eleventy.config.js、theme.js、media.json 与 .github/workflows/blog.yml 等源码完整讲清这个博客的工程架构双主题构建期切换、由单一 JSON 清单驱动的配图/插图/视频管线、完整的 frontmatter 规范以及 sitemap 归属与 CI 校验机制。读完你可以照搬到自己的仓库搭出同样“零部署配置改动”的独立博客。子项目定位独立的 Eleventy 工程输出即发布博客被刻意做成一个自包含的子项目而不是主应用的一部分。选择 Eleventy 的理由在 BLOG_README 中写明它对定制 HTML 有完全控制权无需 Ruby、比 Astro 轻同时保留“纯 markdown frontmatter 写作 内置 collections”的最低写作成本。这个定位在 package.json 中直接落地——博客拥有自己独立的devDependencies11ty/eleventy ^3.0.0、markdown-it、luxon等与主应用的 Electron 依赖互不干扰且标记为private: true不会发布。输出路径是整个部署策略的关键构建结果写入../docs/blog/见 eleventy.config.js 中的OUT变量而仓库现有的 Pages 部署本来就是“分支 →/docsCNAME自定义域名”所以博客不需要任何 Pages 配置改动构建完就出现在munderdiffl.in/blog。从源码结构看URL 前缀的处理也有讲究。eleventy.config.js 顶部注释解释了为什么不直接用 Eleventy 的pathPrefix当pathPrefix与url过滤器组合时HTML 自动改写会把/blog前缀叠加两次。因此项目改用自定义的u过滤器显式拼接前缀// 根相对 URL带 /blog base绝对 URL 原样返回 eleventyConfig.addFilter(u, (p) { if (p undefined || p null || p ) return BASE /; if (/^https?:\/\//.test(String(p))) return p; const path String(p).startsWith(/) ? p : / p; return (BASE path).replace(/([^:])\/{2,}/g, $1/); });BASE和输出目录都支持环境变量覆盖BLOG_BASE/BLOG_OUT这正是主题预览构建能输出到docs/blog-preview-*的基础见下节。双主题体系构建期用 BLOG_THEME 一键切换博客有独立的设计身份刻意与营销站点分开README 提到 Reddit 反馈等宽/新粗野主义皮肤对长文阅读不友好。内置两套主题构建期通过BLOG_THEME环境变量选择逻辑在 theme.js 中只有几行主题定位字体对应样式文件press默认大胆编辑风杂志感呼应应用配色奶油纸、墨色、强调黄、无红受 The Verge 2022 改版启发Fraunces 展示衬线 Source Serif 4 正文blog/src/assets/blog-press.csssunroom友好圆润糖果色受 Josh W. Comeau 博客启发Bricolage Grotesque 标题 Nunito Sans 正文blog/src/assets/blog-sunroom.csstheme.js 的判定是BLOG_THEME sunroom ? sunroom : press——也就是说任何未设置或非 sunroom 的取值都会落到press与“press 是线上默认主题”一致。该文件还导出 Google Fonts 的字体 URLFONTS和预览开关当BLOG_PREVIEW1时模板会注入meta namerobots contentnoindex保证并排预览页不会与真实博客在搜索里互相竞争。并排对比两套主题的命令来自 package.jsonnpm run previews # 实际展开为两条 # rimraf ../docs/blog-preview-sunroom BLOG_THEMEsunroom BLOG_PREVIEW1 \ # BLOG_BASE/blog-preview-sunroom BLOG_OUT../docs/blog-preview-sunroom eleventy # rimraf ../docs/blog-preview-press BLOG_THEMEpress BLOG_PREVIEW1 \ # BLOG_BASE/blog-preview-press BLOG_OUT../docs/blog-preview-press eleventy构建出的 noindexed 副本放在docs/blog-preview-sunroom/与docs/blog-preview-press/gitignore 目录随时可删。开发侧还有两个端口区分的 dev 脚本npm run dev8088与dev:press8089。单一媒体清单一个 media.json 驱动全部配图、插图与视频整个博客的媒体管理只靠一个文件src/_data/media.json当前约 4000 行覆盖全部文章。eleventy.config.js 在配置加载阶段直接把它读进来// 单一媒体清单每篇文章的 hero、inline 图、video 都在这一个文件里 const media JSON.parse(readFileSync(src/_data/media.json, utf8));清单结构以真实条目为例见 media.json{ agent-security-and-sandboxing: { title: Running AI Agents Safely: Permission Modes and Sandboxing, category: guides, hero: { file: assets/media/agent-security-and-sandboxing/hero.png, alt: …, prompt: …, status: ready }, inline: { note-1: { file: …/note-1.png, alt: …, status: ready }, note-2: { file: …/note-2.png, alt: …, status: ready } }, youtube: [] } }每个条目都有alt、生成用prompt和status三个字段状态机只有两档placeholder与ready。模板侧的渲染函数renderFigureeleventy.config.js保证“永不出现破图”status为ready时输出带loadinglazy decodingasync的figureimg否则输出一块按文章 topic 着色、带aria-label的设计感占位块“illustration on its way”。图片生成脚本只负责写文件并把status翻成ready模板零改动——这是“现在发布、稍后补图”工作流的核心。两条图片生产管线1. 手绘插画管线hero 的主路径。README 明确 hero 是“画出来的不是 AI 生成的”scene-lib.js 提供零件库 16 种场景原型spec.js 记录每篇帖子选用哪个原型及标注二者在浏览器里通过blog/media-src/render.html?slugslug渲染成 1600x900 页面对该页面截图即可重生成任意 hero。风格契约统一的暖色手绘编辑插画、像素风办公室世界写在清单顶部的_style键里README 另指向.claude/skills下的插画 SKILL.md 作为本地覆盖。2. 可选的图像模型管线。generate-images.mjs 是一条按需启用的 OpenAI Images 管线几个工程细节值得注意源码固定模型gpt-image-2、尺寸1536x1024接近博客的 16:9 比例质量分档定价{ low: 0.005, medium: 0.041, high: 0.165 }每次成功写出一张图后立即重写 media.json所以崩溃、配额错误或 Ctrl-C 都不会丢进度重跑即断点续传支持--posts a,b,c/--all/--quality/--inline/--dry-run/--force/--max-cost默认 25 美元熔断/--suffix对比出图不翻转 status等参数。新增文章后先跑npm run media刷新清单已有条目保留该脚本在 package.json 中定义。markdown 里的两个媒体 shortcode在文章 markdown 中插入清单驱动的媒体用两个自定义 shortcode实现见 eleventy.config.js{% img note-1, 可选图注 %} {% youtube VIDEO_ID, 标题 %}{% img %}按当前页的fileSlug查media[slug].inline[slot-id]槽位未声明时静默渲染为空而不是报错。{% youtube %}是“点击再加载”嵌入先渲染缩略图 播放按钮页面无第三方 JS 请求点击后才换成youtube-nocookie.com的 iframe加载脚本在src/_includes/base.njk中空 ID 或以TODO开头的 ID 渲染“video on its way”占位块与图片的“先发布后补料”流程一致。清单还承担社交卡片职责heroOg过滤器eleventy.config.js在 hero 就绪后直接读取 PNG 文件头IHDR 块取出宽高让 Facebook/LinkedIn 首次分享就能正确画卡无需先抓取图片文件。新增一篇帖子从文件名到全站自动收录把 markdown 文件丢进blog/src/posts/文件名即 URL slugmy-post.md→/blog/my-post/。最小 frontmatter 与完整写法如下继承自 BLOG_README--- title: Your post title description: One-sentence summary used for SEO the card the feed. date: 2026-06-10 category: guides # 集群 slug: guides | orchestration | memory | internals # | concepts | comparisons | use-cases | story categoryLabel: Guides # 人类可读标签用于 chips/面包屑 JSON-LD articleSection type: Technical # Technical | Non-technical来自 BLOG_IDEAS.md 的分类 primaryKeyword: your focus keyword secondaryKeywords: [supporting term, another] tags: [Getting Started, Hive] --- 正文用 markdown。## H2 与 ### H3 会自动生成目录TOC和深链锚点。Frontmatter 完整字段表字段必填说明title是文章标题。description是SEO meta description 卡片副标 feed 摘要约 150 字符。date是YYYY-MM-DD驱动排序、上一篇/下一篇与 feed 时间戳。category是集群 slug决定文章聚合到哪个/topics/category/页。categoryLabel是集群展示标签如Engineering。tags否自由标签数组每个标签生成/tags/slug/归档页。updated否YYYY-MM-DD最后修改日用于 JSON-LD 与 sitemaplastmod。author否{ name, initials }默认 Munder Difflin / MD。ogImage否自定义社交图绝对 URL默认site.defaultOgImage。seoTitle否覆盖title否则为title — Munder Difflin Blog。ogTitle否仅覆盖 OG/Twitter 卡标题。canonicalUrl否覆盖 canonical很少需要默认从 URL 推导。thumb否卡片缩略图 URL否则用着色标签瓦片。faq否{ q, a }数组 → 输出FAQPageJSON-LD。draft否true时从所有 collections 和构建产物中隐藏。noindex否true时加robots: noindex404 页在用。集群clusters在 site.js 中列出guides / orchestration / memory / internals / concepts / comparisons / use-cases / story各带 technical / non-technical 属性同时又从文章实时派生——所以带新category的帖子“直接就能用”不需要先改配置。构建期自动派生collections 与锚点“写完只需npm run build文章自动进首页、topic 页、各 tag 页、sitemap 和 RSS”这句话的支撑是 eleventy.config.js 中的五组 collectionposts全部非 draft 文章最新在前pinnedpinned: true的支柱文章按pinOrder排序置顶在首页与 topic 页顶部保证快速更新频率不会把参考性长文挤出首屏unpinned其余文章首页 featured 位取其中最旧的一篇最新文categories按category分组并附文章数数量多的集群排前tagList带计数的扁平标签列表。目录与深链则是两级协作markdown-it 挂载markdown-it-anchor配置为 H2/H3 生成id和隐藏的#锚点链接toc过滤器再从渲染后的 HTML 正则提取出{ level, id, text }列表供模板渲染 TOC。另有几个实用细节readingTime过滤器按 225 词/分钟估算阅读时长L129-L134所有 markdown 表格被包进.table-scroll滚动容器L34-L38避免宽表格在手机上撑破 sticky 页头。一个真实例子是 munder-difflin-faq.mdfrontmatter 里声明了 14 条{ q, a }的faq数组构建后自动变成FAQPage结构化数据 正文可见的 QA 块样式见 blog.cssfrontmatter 的author、tags、secondaryKeywords用法与上表完全对应。SEO 管线从关键词策略文件到 JSON-LD 落地SEO 工作按“两份输入文件 → 三处落地”的契约运转SEO 输入落地位置站点级 title/description/OG 默认值site.jsorigin、baseUrl、description、defaultOgImage等关键词集群 技术/非技术分线site.js的clusters 每篇的category每页title/ description / canonical / OG各文章 frontmatterseoTitle、description、ogImage…JSON-LDBlogPosting、BreadcrumbList、FAQPagepost.njkFAQ 由faqfrontmatter 触发sitemap / robots / RSSsitemap.njk、feed.njk自动生成选题 backlog变成src/posts/下新的.md两份输入文件在仓库中是 SEO_METADATA.md关键词策略 每页元数据 JSON-LD 计划和 BLOG_IDEAS.md带 type / 关键词 / 意图 / 摘要的选题清单。构建侧“已接线”的 SEO 能力均可在源码中逐条对上每页独立title、meta description、canonical基于site.baseUrl推导OpenGraph Twitter 卡默认 OG 图取自site.defaultOgImage单篇可用ogImage覆盖配合前述heroOg的 PNG 头宽高读取JSON-LD首页输出BlogBreadcrumbListindex.njk每篇输出BlogPostingpost.njkBreadcrumbListL37声明faq时追加FAQPageL49自动生成的根sitemap.xml与 Atomfeed.xmlfeed 取最新 60 篇feed.njk可访问性与性能语义化 HTML、单h1、skip-link、:focus-visible描边、prefers-reduced-motion支持、图片懒加载。robots.txt归营销站点所有docs/robots.txt随docs/index.html一起维护指向munderdiffl.in/sitemap.xml——这与下一节的 sitemap 归属策略直接挂钩。构建、部署与 sitemap 归属输出提交入库 CI 校验门Quick startcd blog npm install npm run dev # 带热重载的 dev server端口 8088路径 /blog/ npm run build # 一次性构建到 ../docs/blognpm run build的完整链路是cleanrimraf 旧输出→eleventy→node scripts/postbuild.mjs见 package.json。需要注意部署模型docs/blog/下的构建产物是提交进仓库的——Pages 直接服务仓库里的docs/不存在独立的 Actions 发布物。Build blog workflow 的真实形态BLOG_README 描述的是“blog/**变化时自动重建并提交docs/blog”的思路而当前 .github/workflows/blog.yml 的实现已经演化为一种校验门文件头注释交代了原因main 分支要求 status checksActions 的推送无法满足检查而被拒绝导致新文章无法上线。现行为触发条件pull_request与push到main且paths限定在blog/**、docs/blog/**、docs/sitemap.xml及 workflow 自身权限仅contents: read行为从零重建若与已提交的docs/blog产物不一致则失败同一棵树两次构建产物应逐字节相同失败即说明输出过期它从不推送。因此实际发布流程是改blog/源码 → 本地cd blog npm ci npm run build→ 把docs/blog/与docs/sitemap.xml连同源码一起提交 → PR 中 Build blog 校验通过即上线。README 中的手动方式build 后提交docs/blog/并推送仍然成立workflow 只是把“产物必须与源码一致”变成了强制检查。Sitemap 的根目录归属Option A博客构建拥有整站根docs/sitemap.xml构建生成的是“营销首页 全部博客页”的完整 sitemap营销页不会成为孤儿sitemap.njk 里明确标注了这一归属约定。由于 Eleventy 只能写自己的输出目录postbuild.mjs 在构建结束后把docs/blog/sitemap.xml改名搬到docs/sitemap.xml——正是docs/robots.txt指引爬虫的位置。两条纪律帖子的lastmod一律取真实发布/更新日期而非构建时间docs/sitemap.xml禁止手改每次构建都会重新生成。RSS 输出在docs/blog/feed.xml对外为/blog/feed.xml与营销站点head中已有的link relalternate对齐。项目结构速览以下为仓库实际结构在 README 树基础上补入了theme.js等真实文件blog/ eleventy.config.js # 配置、过滤器、collections、/blog base、markdown 锚点 package.json # 独立依赖 build/dev/previews/media 脚本 scripts/ postbuild.mjs # 把生成的 sitemap 搬到 docs/sitemap.xml站点根 generate-images.mjs # 可选的图像模型管线断点续传 成本熔断 add-inline-notes.mjs media-src/ scene-lib.js # hero 插画零件库 16 种场景原型 spec.js # 每篇文章的原型选择与标注 render.html # ?slugslug 浏览器渲染 1600x900 页面 src/ _data/ site.js # 站点级 SEO 默认值 clusters主题色板、origin 等 theme.js # BLOG_THEME 切换、字体 URL、BLOG_PREVIEW noindex media.json # 单一媒体清单hero/inline/youtubestatus 状态机 _includes/ base.njk # headSEO、OG、字体、导航、页脚壳 youtube 点击加载脚本 page.njk # 静态页布局about post.njk # 文章布局JSON-LD、TOC、署名、上/下篇、相关文章 nav.njk footer.njk card.njk assets/ blog.css blog-press.css blog-sunroom.css # 三套设计系统 media/slug/… # 各文章的 hero 与插图 posts/ # markdown 文章目录文件名即 slug index.njk # 博客首页featured 网格 集群过滤 topics-index.njk # /topics topic.njk # /topics/cluster/按 category 分页 tag.njk # /tags/tag/按 tag 分页 about.md 404.njk feed.njk sitemap.njk → 构建输出 ../docs/blog/ 站点根 ../docs/sitemap.xml这套结构的工程要点可以概括为三句话用独立package.json把博客隔离成零依赖冲突的子项目用media.json单一清单把“先发布、后补媒体”变成状态机而非人工流程用“产物提交入库 CI 校验门”替代脆弱的 Actions 自动推送让docs/blog的任何过期都在 PR 红灯中暴露。【免费下载链接】munder-difflinA local multi-agent harness that works with your existing Claude Code, Codex subscriptions, allows you to run an office of agents项目地址: https://gitcode.com/GitHub_Trending/mu/munder-difflin创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考