自写缓存模块:TTL、stale-while-revalidate、并发未命中合并、失败刷新保留旧值、按标签失效,并有 node:test 测试覆盖
remix-starter-contioreach — Remix 博客模板
作者 contioreach
基于 Remix v2 与 ContioReach 无头 CMS 的博客启动器,包含缓存、按需重新验证、分类归档、SEO 元数据、JSON-LD 与站点地图。
看看你的网站会是什么样
浏览真实页面截图,比较内容排版与手机效果。点击下方缩略图切换,放大查看整页细节。
全部页面与视图 3
截图用于了解视觉与布局,交互效果请以实际项目为准。
值得关注的模板亮点
从已有项目资料中,快速了解它的特点。
POST /api/revalidate/all 签名 webhook,支持按文章 slug 标签单独清除,密钥可放在请求体或 X-API-Key 头
路由页面导出 Cache-Control 头(s-maxage 与 stale-while-revalidate),便于 CDN 缓存渲染后的 HTML
loader 中完成标题锚点与目录生成,两者同源不会漂移,且原始正文不重复下发
统一 SEO helper 生成 canonical、Open Graph、Twitter card 等 meta,并输出 BlogPosting 与 BreadcrumbList JSON-LD
默认关闭搜索引擎索引,避免演示部署与正式站点产生重复内容
模板详细介绍
这是一个用 Remix v2 和 React 18 构建的博客启动器,内容后端为 ContioReach 无头 CMS。项目在 loader 中读取 CMS 数据,并通过自写的缓存模块实现 TTL、stale-while-revalidate、并发请求合并与基于标签的失效;同时提供 POST /api/revalidate/all 签名 webhook,供 CMS 发布时触发按标签清除缓存。路由覆盖博客首页(分页)、文章页、分类归档页,以及 sitemap.xml 和 robots.txt。文章正文在 loader 中完成标题锚点与目录生成,原始 HTML 不再随载荷下发。SEO 方面由统一 helper 生成 canonical、Open Graph、Twitter card 等 meta,并输出 BlogPosting 与 BreadcrumbList 的 JSON-LD。样式使用 Tailwind CSS v4,无 UI 库、无 MDX 管线、无数据库。
需要注意的限制:缓存是进程内内存实现,多实例部署时 webhook 只能清除收到请求的那个实例,需要自行替换为共享存储或配合 CDN 清除;在 serverless 目标上缓存仅存活于热实例期间。项目默认关闭索引(PUBLIC_ALLOW_INDEXING 不为 "true" 时输出 noindex, nofollow 且 robots.txt 全禁),上线前需显式开启。文章无封面图时回退到 /og-default.png,需自行替换。README 也说明 Remix v2 处于维护状态,后续演进为 React Router v7+,Remix v3 是独立重写。
适合你的项目吗?
适用场景
- 以 ContioReach 为内容后端搭建带分类归档与分页的博客站点
- 需要 CMS 发布后按标签精确失效缓存、并配合 CDN 缓存 HTML 的内容站
- 希望参考 Remix v2 中 loader 取数、缓存与 SEO 元数据组织方式的开发项目
- 需要文章目录、JSON-LD 结构化数据与 sitemap 的编辑型内容站点
适合人群
- 使用 ContioReach 无头 CMS 并接受其 API 的团队或个人
- 熟悉 Remix 与 React、能自行调整缓存与部署配置的开发者
- 单实例或 CDN 前置部署、暂不需要共享缓存的博客项目
- 需要 MIT 许可下可修改、可商用的启动代码的开发者
上手与定制指南
结合仓库当前版本的说明,按下面的资料检查内容、配置与运行要求。
- 1
通过 npx degit contioreach/remix-starter-contioreach my-blog 获取代码,安装依赖后复制 .env.example 为 .env
- 2
在 ContioReach 工作区获取 CMS_API_KEY 与 REVALIDATION_SECRET 并填入环境变量,免费计划即可
- 3
PUBLIC_ 前缀是本仓库自定义约定,四个公开值经 root loader 传给客户端,CMS 密钥不会进入该载荷
- 4
上线前将 PUBLIC_ALLOW_INDEXING 设为 "true" 以开启索引,并把 PUBLIC_SITE_URL 指向真实域名
- 5
多实例部署时需将内存缓存替换为共享存储(如 Redis)或在同一 webhook 上触发 CDN 清除
授权与来源资料
使用条件与限制
免费使用 · 保留许可声明本次收录的是公开仓库中采用 MIT 许可的免费版本;免费范围为该版本代码,不代表外部服务或第三方素材免费。
使用时需要遵守
- 允许免费使用、修改、商用和分发该版本代码。
- 分发代码副本或实质性部分时,保留原作者版权声明和完整 MIT 许可声明。
- 代码按现状提供;担保与责任限制以原许可证及适用法律为准。
额外费用与待核对事项
- 使用需注册 ContioReach 账号并获取 CMS_API_KEY 与 REVALIDATION_SECRET;免费计划即可满足,但内容后端依赖该外部服务。
- 缓存为进程内内存实现,多实例部署时 webhook 仅清除收到请求的实例,需自行替换为共享存储或配合 CDN 清除。
- 在 serverless 目标上缓存仅存活于热实例期间,需依赖 Cache-Control 头并视 webhook 为 CDN 清除触发器。
- 默认关闭搜索引擎索引,PUBLIC_ALLOW_INDEXING 不为 "true" 时输出 noindex, nofollow 且 robots.txt 全禁,上线前需显式开启。
- 文章无封面图时回退到 /og-default.png,上线前需自行替换该占位资源。
- 演示图片、字体、图标及其他第三方素材需按各自许可核对,不能仅凭代码的 MIT 许可判断。
- 域名、托管、数据库、支付或 AI 等外部服务可能另外收费。
查看原文依据
Permission is hereby granted, free of charge
许可证原文
Get `CMS_API_KEY` and `REVALIDATION_SECRET` from your ContioReach workspace settings (the free plan is enough).
README 原文
the cache is per-process and in-memory, so a webhook that lands on one instance only invalidates that instance
README 原文
On a serverless target, the in-memory cache lives only as long as a warm instance.
README 原文
**Indexing is off by default.** Until `PUBLIC_ALLOW_INDEXING` is exactly `"true"`
README 原文
https://github.com/contioreach/remix-starter-contioreach/blob/main/LICENSEhttps://github.com/contioreach/remix-starter-contioreach/blob/main/README.mdPosts fall back to `/og-default.png` when they have no cover image — drop your own into `public/` before launch.
README 原文
DeepSeek 根据仓库许可证和 README 整理 ·2026-10-03。结果仅覆盖已读取资料,依赖、图片、字体及后续条款变更以各自原文为准。
项目核对记录与介绍依据
模板资料
- 来源平台
- GitHub
- 资料与维护参考分
- 资料较完整 · 85 / 100
- 技术栈
- Remix · React
- 网站类型
- 博客 · 启动器
这个分数如何计算
本站按许可资料(最高30分)、演示链接(12分)、预览图(10分)、Star数(15分)、记录的维护时间(15分)、描述完整度(8分)、功能记录(5分)和分类资料(5分)计算。它不是代码、安全或性能测试,也不是 Google 评分;有链接不代表已实测可用。
这个项目需要核对什么
- 记录的仓库更新时间
- 2026-10-01
- 截图检查日期
- 2026-10-03
已收录演示链接。请用你的内容与屏幕尺寸检查;有演示不代表表单、认证或支付已经可用。
目录记录的主要技术栈是 Remix。请通过依赖清单和锁文件核对实际版本与托管要求。这些说明基于收录资料,不代表代码审计或部署实测。
查看具体选型方法 →介绍的制作与核对状态
介绍由 AI 辅助整理目录与仓库资料。仓库作者不等于这篇介绍的审核者。
审核声明: [email protected] · 2026-10-03
查看当时使用的来源资料
SHA-256: fcf4d45269690d20d2d92d6e8b172cb57fa87c12c1d46a8b8034d764559ba694
2026-10-03T12:15:34.936Z
来源文件 / 仓库# Remix Headless CMS Starter
A complete, production-shaped blog built with **Remix v2** and a **headless CMS**, using [ContioReach](https://contioreach.com) as the content backend.
Not a toy. It ships the things a real content site actually needs and most examples skip: cached CMS reads with stale-while-revalidate and real tag invalidation, an on-demand revalidation webhook, category archives, pagination, a table of contents generated from the article body, full SEO metadata, JSON-LD, and a sitemap.
```bash
npx degit contioreach/remix-starter-contioreach my-blog
cd my-blog && npm install
cp .env.example .env # add your API key
npm run dev
```
> Looking for another stack? See the [Next.js](https://github.com/contioreach/nextjs-starter-contioreach), [Nuxt](https://github.com/contioreach/nuxtjs-starter-contioreach), [Astro](https://github.com/contioreach/astro-starter-contioreach), [SvelteKit](https://github.com/contioreach/sveltekit-starter-contioreach), [Gatsby](https://github.com/contioreach/gatsby-starter-contioreach), [React](https://github.com/contioreach/react-starter-contioreach) and [Vue](https://github.com/contioreach/vue-starter-contioreach) examples.
### A note on which Remix this is
This is **Remix v2** (`@remix-run/*` 2.x, React 18) — the stable release, and what "Remix" means to most people today. Worth knowing before you start a long-lived project on it:
- Remix v2 is effectively a maintenance branch. Its development continued as **React Router v7+**, which has the same loader/action model; migrating is a documented, mostly mechanical path.
- **Remix v3** is a separate rewrite, in release candidate at the time of writing.
If you are starting fresh and have no attachment to the Remix name, React Router's framework mode is the more future-proof choice, and nearly everything in this repo transfers to it unchanged.
---
## What's in the box
| Feature | Where |
| --- | --- |
| Blog index with pagination | [`app/routes/blog._index.jsx`](app/routes/blog._index.jsx) |
| Article page | [`app/routes/blog.$slug.jsx`](app/routes/blog.%24slug.jsx) |
| Category archives | [`app/routes/blog.category.$slug.jsx`](app/routes/blog.category.%24slug.jsx) |
| CMS client and transforms | [`app/lib/cms.server.js`](app/lib/cms.server.js) |
| Cache: TTL, SWR, single-flight, tags | [`app/lib/cache.server.js`](app/lib/cache.server.js) |
| …and its tests | [`app/lib/cache.test.js`](app/lib/cache.test.js) |
| On-demand revalidation webhook | [`app/routes/api.revalidate.all.jsx`](app/routes/api.revalidate.all.jsx) |
| Table of contents + stable heading anchors | [`app/lib/content.js`](app/lib/content.js) |
| Metadata, Open Graph, JSON-LD | [`app/lib/seo.js`](app/lib/seo.js), [`app/lib/schema.js`](app/lib/schema.js) |
| Sitemap and robots | [`app/routes/sitemap[.]xml.jsx`](app/routes/sitemap%5B.%5Dxml.jsx), [`app/routes/robots[.]txt.jsx`](app/routes/robots%5B.%5Dtxt.jsx) |
| Article typography (raw CMS HTML) | [`app/styles/tailwind.css`](app/styles/tailwind.css) |
Styling is Tailwind CSS v4. No UI library, no MDX pipeline, no database — the CMS is the only backend.
---
## 1. Environment variables
Copy the template and fill it in:
```bash
cp .env.example .env
```
```env
CMS_API_URL=https://cms-api.contioreach.com
CMS_API_KEY=cms_xxxxxxxxxxxxxxxxxxxxxxxx
REVALIDATION_SECRET=revalidate_xxxxxxxxxxxx
PUBLIC_SITE_URL=http://localhost:3000
PUBLIC_ALLOW_INDEXING=false
PUBLIC_SIGNUP_URL=https://app.contioreach.com/signup
PUBLIC_LOGIN_URL=https://app.contioreach.com/login
```
Get `CMS_API_KEY` and `REVALIDATION_SECRET` from your ContioReach workspace settings (the free plan is enough). Every variable is read in one place — [`app/lib/config.server.js`](app/lib/config.server.js) — which throws a named error when one is missing.
**Remix has no `PUBLIC_` convention and no build-time env inlining for the browser.** That prefix is this repo's own, marking which four values are meant to reach the page. They are read on the server and handed to the client through the **root loader** ([`app/root.jsx`](app/root.jsx)); components read them back with the `useSite()` hook ([`app/lib/site.js`](app/lib/site.js)). The CMS key is never part of that payload.
**The API key cannot leak by construction.** [`cms.server.js`](app/lib/cms.server.js), [`cache.server.js`](app/lib/cache.server.js) and [`config.server.js`](app/lib/config.server.js) use Remix's `.server.js` suffix, so the compiler strips them from the browser bundle. (Checked, not assumed: the built `build/client` contains no trace of the key.)
Running the built server:
```bash
npm run build
node --env-file=.env ./node_modules/.bin/remix-serve ./build/server/index.js
```
---
## 2. Fetching content
Every route reads its data in a `loader`, which only ever runs on the server — including across client-side navigations, where Remix fetches the serialized loader data for the next route rather than letting the browser talk to the CMS.
Components never see the API's shape: a transform layer maps it first, so swapping in Contentful, Sanity or Strapi is `apiRequest` plus two `transform*` functions, without touching a component.
The article body is prepared in the loader ([`app/lib/content.js`](app/lib/content.js)): heading anchors added, and the table of contents built from that same pass so the two cannot drift. The raw body is dropped from the payload afterwards, since shipping both would double it.
---
## 3. Caching and revalidation
Like SvelteKit, Remix gives you nothing here — no data cache, no cache tags. (Next has `revalidateTag`; Astro has a tagged response cache; Nitro has cached function groups.) So [`app/lib/cache.server.js`](app/lib/cache.server.js) writes one out, in about sixty lines:
- a value is **fresh** for `maxAge` seconds;
- after that it is served **stale** for up to `swr` seconds while one refresh runs behind the request, so no visitor waits on the CMS;
- concurrent misses on a cold key **share a single load**, so a burst doesn't stampede the CMS;
- a **failing background refresh keeps the stale value**, so a CMS blip doesn't take the page down;
- every entry carries **tags**, and `invalidate(tags)` drops exactly the entries that match.
Because it is hand-written rather than handed to you, those semantics are pinned by tests — [`cache.test.js`](app/lib/cache.test.js), seven of them, run with `npm test`. No test runner to install; it's `node:test`.
On top of that, each page route exports `headers` with `s-maxage` and `stale-while-revalidate`, so a CDN in front of the app can hold the rendered HTML on the same schedule.
**The webhook.** `POST /api/revalidate/all` is what your CMS calls on publish. It checks the shared secret, then drops every entry carrying one of the tags:
```bash
curl -X POST https://your-site.com/api/revalidate/all \
-H 'content-type: application/json' \
-d '{"secret":"revalidate_xxx","post":{"slug":"my-post"}}'
```
Naming a post adds its `blog-<slug>` tag to the purge, so a single article can be invalidated without dropping every listing. The response reports how many entries went. The secret is accepted in the body or as an `X-API-Key` header; anything else gets a 401, and a `GET` gets a 405.
**Before you scale out:** the cache is per-process and in-memory, so a webhook that lands on one instance only invalidates that instance. That is fine for a single server and for CDN-fronted deployments, but with several instances you want either a shared store behind the same `cached()` interface (Redis, say — the module is small and deliberately easy to swap) or a CDN purge on the same webhook.
---
## 4. SEO
- Canonical, Open Graph, Twitter card, robots and keywords come from one helper, [`buildMeta`](app/lib/seo.js), which turns the same arguments every route uses into the flat array Remix's `meta` export expects.
- `BlogPosting` and `BreadcrumbList` JSON-LD from [`app/lib/schema.js`](app/lib/schema.js).
- `/sitemap.xml` is generated from the CMS on request; `/robots.txt` follows the same switch as the meta tags. Both filenames escape the dot (`sitemap[.]xml`) so Remix treats it as a literal rather than a route separator.
- Bots get the fully rendered document rather than a streamed shell — see [`entry.server.jsx`](app/entry.server.jsx).
- Posts fall back to `/og-default.png` when they have no cover image — drop your own into `public/` before launch.
- **Indexing is off by default.** Until `PUBLIC_ALLOW_INDEXING` is exactly `"true"`, every page ships `noindex, nofollow` and `robots.txt` disallows everything — so a demo deployment can't compete with your posts' canonical home.
---
## 5. Project structure
```
app/
components/blog/ Cards, listing, hero, TOC, share bar, article detail
components/layout/ Header, footer, JSON-LD
lib/
*.server.js CMS client, config and the cache — stripped from the
browser bundle by Remix's .server.js convention
content.js Article body prep — anchors + table of contents
format.js Dates, initials, reading time
schema.js JSON-LD builders
seo.js Arguments -> Remix's meta array
site.js useSite(), reading the root loader's public config
routes/ /, /blog, /blog/$slug, /blog/category/$slug,
sitemap[.]xml, robots[.]txt, the webhook
root.jsx HTML shell, public config loader, error boundary
constants.js Values that don't vary by environment
```
---
## 6. Deploying
`npm run build` produces `build/server` and `build/client`; `remix-serve` runs them. Remix also targets Vercel, Netlify, Cloudflare, Fly and any Node host — see [Remix deployment](https://remix.run/docs/en/main/guides/deployment). Set the same environment variables there, with `PUBLIC_SITE_URL` pointing at your real domain, and point the CMS publish webhook at `https://your-domain/api/revalidate/all`.
On a serverless target, the in-memory cache lives only as long as a warm instance. There, lean on the `Cache-Control` headers each route already sets and treat the webhook as a CDN purge trigger.
---
## License
MIT — see [LICENSE](LICENSE). Clone it, strip it back, rebrand it, ship it.
看看同类模板
对比同技术栈与用途的模板,找到更适合的视觉与内容结构。

medusa2-starter
A Turborepo monorepo integrating a Medusa2 backend with a Remix frontend, featuring a Coffee Roast themed dyna…
Remix · 电商

remix-knowledge-base
Knowledge Base starter kit with WYSIWYG, Markdown, GPT, and Multi-language support. Built with Remix, Tailwind…
Remix · 启动器

docs-starter
Remix starter for creating documentation websites with Hygraph
Remix · 文档

directed-stack
Directus + Remix full-stack starter
Remix · 启动器

remix-shadcn-docs
这是一个把文档系统和博客放在同一套 Remix 应用里的起步模板,UI 层使用 Shadcn 组件,样式走 Tailwind CSS,整体用 TypeScript 编写。
Remix · 文档

remix-wasmer-starter
Wasmer Edge JS template for Remix
Remix · 启动器
