[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fOhGOkgvW-9Xb2kFhTBDArJqCfshy1I_niicRl_Ahl_0":3},{"item":4,"related":44},{"id":5,"type":6,"title":7,"slug":8,"summary":9,"body":10,"coverUrl":11,"productScreenshots":12,"productLinks":13,"authorName":14,"authorUrl":15,"authorSubject":16,"category":17,"tags":22,"sourceLabel":35,"sourceName":36,"sourceUrl":36,"status":37,"seoTitle":36,"seoDescription":36,"canonicalUrl":36,"isFeatured":38,"sno":39,"sortOrder":40,"publishedAt":41,"updatedAt":42,"createdAt":43},"9fc7876e-6196-491d-aef4-4610112ec643","article","Foundit架构解析","foundit-analysis","基于项目源代码的架构拆解——Foundit 为什么比传统博客\u002FCMS 更聪明、更安全、更\"被看见\"","> *本文基于项目源码撰写。该项目已**从 Cloudflare Workers + Supabase 迁移至国内腾讯云服务器自托管**：Nginx 反向代理 + Nuxt SSR（Node）+ 本地 PostgreSQL + 本地文件存储，身份服务由 Srces Auth 提供。*\n\n如果你曾经搭过个人网站、技术博客，或者公司内容站，大概率踩过这些坑：文章发出去搜索引擎半年没收录；后台登录形同虚设，改个 URL 就能绕过；服务器月月烧钱；换台手机排版就崩了。\n\n**Foundit** 这个项目，正是针对这些\"老毛病\"给出的一个近乎教科书式的答案。它不是一个臃肿的系统，而是一套用现代工具链拼起来的、克制而精密的内容站。下面我们用拆解一台精密仪器的眼光，看看它的内部构造。\n\n## 一、它到底是什么？\n\n用一句话概括：\n\n> **Foundit 是一个部署在腾讯云服务器上、基于 Nuxt（SSR）、本地 PostgreSQL（数据库）+ 本地文件存储（媒体）以及 Srces Auth（统一登录）的科技内容知识站。**\n\n它把内容分成四种形态——**文章、产品、想法、专题**，对外提供公开阅读（首页、列表、详情、搜索、RSS、Sitemap），对内提供一个由管理员权限保护的内容后台。\n\n它的技术栈可以用\"五个方面军\"来理解：\n\n```mermaid\nflowchart LR\n    读者([公开访问者]) --> NGX[Nginx 反向代理\u003Cbr\u002F>TLS + 缓存 + 安全头]\n    NGX --> Nuxt[Nuxt SSR（Node）\u003Cbr\u002F>页面 \u002F 后台 \u002F 接口]\n    Nuxt --> PG[(PostgreSQL 本地实例\u003Cbr\u002F>内容 \u002F 分类 \u002F 标签 \u002F 专题)]\n    Nuxt --> FS[(本地磁盘 \u002Fuploads\u002F\u003Cbr\u002F>图片 \u002F 附件)]\n    管理员([管理员]) --> SDK[Srces Auth SDK\u003Cbr\u002F>OAuth + PKCE]\n    SDK --> NGX\n    NGX --> JWKS[JWKS 验证 JWT\u003Cbr\u002F>RS256 + admin 角色]\n    JWKS --> PG\n```\n\n这张图里藏着 Foundit 的第一个聪明之处：**浏览器永远不直接碰数据库钥匙**。所有写入都要经过 Nuxt 服务端这一道\"门房\"，而\"门房\"只认经过密码学签名的令牌。整条链路（Nginx、Node、PostgreSQL、磁盘）都跑在同一台腾讯云 CVM 的内网回环上，**只有 Nginx 监听公网 443**，数据库与 Node 进程绝不暴露在公网。\n\n如果把镜头再拉近一点，按\"分层\"的视角看，各个组件的上下游关系会更清晰——公开读取和管理员写入走的是两条泾渭分明的通道，最终都汇聚到 PostgreSQL，但沿途经过的\"安检\"完全不同：\n\n```mermaid\nflowchart TB\n    subgraph Public[\"公开访问者\"]\n        P[浏览器]\n    end\n    subgraph Edge[\"Nginx + Nuxt 运行时（腾讯云 CVM）\"]\n        NGX[Nginx 反向代理 \u002F TLS \u002F 静态缓存]\n        Node[Nuxt SSR \u002F Nitro 运行时]\n        Cache[(SWR 缓存)]\n    end\n    subgraph App[\"Nuxt 应用层\"]\n        Pages[Pages \u002F Layouts \u002F Components]\n        Composables[Composables]\n        API[Server API \u002F Routes]\n    end\n    subgraph Data[\"数据与安全\"]\n        PG[(PostgreSQL 本地实例\u003Cbr\u002F>服务端按状态过滤)]\n        FS[(本地磁盘 \u002Fuploads\u002F\u003Cbr\u002F>public-media)]\n        AUTH[Srces Auth\u003Cbr\u002F>RS256 JWT + JWKS]\n    end\n\n    P -->|HTTPS :443| NGX\n    NGX --> Node\n    NGX -->|\u002F_nuxt\u002F \u002Fuploads\u002F 静态直出| Cache\n    Node --> Cache\n    Node --> Pages\n    Pages --> Composables\n    Pages -->|useFetch \u002Fapi\u002Fcontent| API\n    API -->|数据库连接串（服务端）| PG\n    API -->|只读媒体 \u002Fuploads\u002F| FS\n    API -->|公开内容（status=published 过滤）| PG\n\n    subgraph Admin[\"管理员\"]\n        A[浏览器 + SrcesAuth SDK]\n    end\n    A -->|OAuth + PKCE| AUTH\n    AUTH -->|Access Token| NGX\n    NGX --> Node\n    Node -->|JWKS 验签 + admin 角色| AUTH\n    API -->|写操作 + 审计| PG\n    API -->|上传\u002F删除| FS\n```\n\n注意左右两侧的对称：左边\"公开访问者\"只能通过服务端过滤后的只读通道拿到已发布内容；右边\"管理员\"必须先过 Srces Auth 的 JWT 验签、再由服务端持数据库连接串才能写入，并且每一次写入都会留下审计记录。\n\n## 二、目录结构：像图书馆一样井井有条\n\n一个项目好不好维护，先看它的\"房间怎么分\"。Foundit 的目录非常符合直觉：\n\n| 目录 \u002F 文件 | 职责 | 科普类比 |\n|---|---|---|\n| `pages\u002F` | 19 个页面（前台 + 后台） | 对外开放的\"展厅\"和内部的\"办公室\" |\n| `components\u002F` | 7 个可复用组件 | 标准化的\"家具\"：列表、轮播、编辑器 |\n| `composables\u002F` | 3 个组合式逻辑（认证、图片压缩、主题） | 可插拔的\"功能模块\" |\n| `server\u002Fapi\u002F` | 公共接口 + 后台接口 | 对外的\"服务窗口\" |\n| `server\u002Futils\u002F` | 数据访问层 + 鉴权 + 审计 + 媒体存储 | 后厨：备菜、安检、记账、储物 |\n| `server\u002Froutes\u002F` | `robots.txt` \u002F `sitemap.xml` \u002F `rss.xml` \u002F `llms.txt` | 给搜索引擎的\"地图和告示\" |\n| `database\u002F` | `schema.sql` + `migrations\u002F*.sql` | 仓库的\"建筑图纸\"与\"加建记录\" |\n| `layouts\u002F` | 前台布局 + 后台布局 | 两套\"装修风格\"但同源 |\n| `config\u002F`、`types\u002F`、`utils\u002F` | 配置、类型、纯函数工具 | 字典和工具箱 |\n| `scripts\u002F` | 一键部署 \u002F 升级 \u002F 数据迁移脚本 | 标准化的\"施工手册\" |\n\n最值得称道的一点：**公共接口（`\u002Fapi\u002Fcontent`）与后台接口（`\u002Fapi\u002Fadmin\u002F*`）严格分离**。前者的数据访问层叫 `content-repository`，后者叫 `admin-repository`。\n\n## 三、六大优点与特性\n\n### 1. 生来就被搜索引擎\"看得懂\"——SSR + SEO + GEO\n\n很多现代网站为了酷炫，正文靠 JavaScript 在浏览器里现拉现渲染。结果就是：关掉 JS，页面一片空白；搜索引擎爬虫看不懂；AI 问答工具也抓不到。\n\nFoundit 反其道而行。它用 **SSR（服务端渲染）**，用户或爬虫拿到的就是一份**完整的 HTML 正文**。项目里还专门做了三件事：\n\n- **结构化数据（JSON-LD）**：每篇文章\u002F产品页都输出机器可读的 `Article` \u002F `SoftwareApplication` 标签，相当于给内容贴了\"身份证\"，方便 Google、Bing 以及各类 AI 准确理解。\n- **Sitemap + RSS + robots.txt + llms.txt**：全部由服务端动态生成，内容一发布就自动更新\"目录\"。\n- **GEO（生成式引擎优化）**：专门为\"被 AI 引用\"做了设计——首屏直出核心结论、事实与观点分开、标注作者与来源时间、提供参考资料链接。\n\n那么一次\"打开文章\"的请求，在幕后到底经历了什么？下面这张时序图，把从浏览器发起请求，到 Nginx 反向代理、服务端拉数据、Markdown 渲染、注入 JSON-LD，最后吐出完整 HTML 的全过程摊开看：\n\n```mermaid\nsequenceDiagram\n    participant B as 浏览器\n    participant NGX as Nginx\n    participant N as Nuxt SSR（Node）\n    participant API as \u002Fapi\u002Fcontent\n    participant PG as PostgreSQL（本地）\n    participant AUTH as Srces Auth (JWKS)\n\n    B->>NGX: GET \u002Farticle\u002F{slug}  (HTTPS :443)\n    NGX->>N: 反向代理\n    N->>N: SWR 缓存命中?\n    alt 缓存命中\n        N-->>B: 直接返回缓存 HTML（Stale-While-Revalidate）\n    else 未命中\n        N->>API: useFetch('\u002Fapi\u002Fcontent?type=article&slug=...')\n        API->>PG: listContents（数据库连接串，服务端过滤 status='published'）\n        PG-->>API: ContentItem\n        API-->>N: JSON\n        N->>N: markdown-it 渲染正文 + 注入 JSON-LD\n        N-->>B: 完整 HTML（含正文\u002Fmeta\u002Fcanonical）\n    end\n```\n\n关键在于：真正决定\"正文长什么样\"的那一步（渲染 + 注入结构化数据）发生在**服务端**，而不是浏览器。所以无论是搜索引擎爬虫、AI 抓取器，还是关掉了 JS 的读者，拿到的都是同一份完整、可读、带\"身份证\"的 HTML。\n\n### 2. 多层安全防线——\"隐藏菜单\"骗不了人\n\n很多 CMS 的\"权限\"只是前端把按钮藏起来。Foundit 的态度是：**前端隐藏只是体验，真正的安全必须发生在服务端。**\n\n它的防线是这样的：\n\n1. **统一身份（OIDC + PKCE）**：登录走标准的授权码 + PKCE 流程，不存客户端密钥。\n2. **密码学令牌（RS256 JWT）**：服务端用 `jose` 库 + 远程 JWKS 公钥，**逐条校验**签名算法、签发者（issuer）、接收方（audience）、过期时间，并确认角色含 `admin`。\n3. **服务端兜底**：每个 `\u002Fapi\u002Fadmin\u002F*` 都调用同一个 `requireAdmin`，前端无论怎么改都绕不过去。\n4. **服务端数据访问层强制过滤**：公开读取只返回 `status='published'` 的内容，草稿、待审、归档天然不可见；数据库凭据（`NUXT_DATABASE_URL`）只在服务端环境变量中，**不存在可被公开访问的数据库角色**——没有任何一个公网入口能直接触达数据库。\n5. **密钥隔离**：高权限的数据库连接串与 `adminSessionSecret` 只存在于服务器环境变量，**绝不进前端产物**。\n\n具体到\"登录\"这件事，Foundit 用了一套巧妙的**双令牌机制**：管理员先从 Srces Auth 拿到一枚有效期仅 10 分钟的 RS256 令牌（用于向服务端证明身份），服务端验签通过后，再签发一枚 8 小时的 HttpOnly 会话 Cookie（免去每次都携带外部令牌）。整个握手过程如下：\n\n```mermaid\nsequenceDiagram\n    participant B as 浏览器\n    participant AUTH as Srces Auth\n    participant S as Foundit 服务端\n\n    B->>AUTH: auth.login()（OAuth + PKCE）\n    AUTH-->>B: RS256 Access Token（有效期 10 分钟，iss=auth.srces.cn, aud=foundit）\n    B->>S: POST \u002Fapi\u002Fadmin\u002Fsession  (Authorization: Bearer \u003CSrces token>)\n    S->>AUTH: createRemoteJWKSet + jwtVerify（RS256, iss, aud=foundit, exp）\n    S->>S: toAuthUser：校验 roles 含 'admin'，否则 403\n    S-->>B: 签发 HS256 本地会话 Cookie（foundit_admin_session，8h，HttpOnly, path=\u002Fapi\u002Fadmin）\n```\n\n这里有两个容易被忽略的细节：其一，本地会话 Cookie 的 `path` 被限定为 `\u002Fapi\u002Fadmin`，意味着它**只在后台请求时才会被发送**，不会泄漏到普通页面；其二，无论前端如何伪造，服务端 `requireAdmin` 都会重新验签并检查 `admin` 角色——**前端隐藏菜单，永远绕不过这道服务端的门。**\n\n一句话总结它的安全哲学：**\"永远不要相信浏览器送来的任何东西。\"**\n\n### 3. 一套设计语言，前后台\"长得很像\"\n\nFoundit 附带了一份极其详尽的 UI 设计规范（40 多节）。它的核心只有几个字：**克制、安静、留白、内容优先**。\n\n- 色彩只有黑、白、浅灰三色体系；\n- 视觉层级靠**字体和间距**建立，而不是靠色块和阴影；\n- 前后台共用同一套字体、按钮、表单风格，后台不再是\"花花绿绿的 SaaS 仪表盘\"；\n- 连动效都限制时长（150–220ms），禁止\"为了高级感而高级感\"。\n\n这种设计的好处是**长期可读、跨设备一致、维护成本低**。它像一本排版考究的书，而不是一面喧闹的广告墙。\n\n### 4. 智能缓存：既快又不会\"显示旧文章\"\n\nFoundit 的缓存是**两层协作**的：\n\n- **Nuxt Nitro `routeRules`**：在应用层把页面缓存策略写得很细（见下表），基于 SWR（Stale-While-Revalidate）。\n- **Nginx 静态缓存**：构建产物 `_nuxt\u002F` 静态资源设 `expires 1y, immutable` 并关闭访问日志；上传的媒体文件 `\u002Fuploads\u002F` 设 `expires 30d, public` 并开启 gzip——这些静态内容由 Nginx 直接吐出，根本不进 Node 进程。\n\n| 页面 | 缓存策略（Nitro routeRules） | Nginx 静态层 |\n|---|---|---|\n| 首页 | SWR 5 分钟 | — |\n| 文章 \u002F 产品 \u002F 专题详情 | SWR 1 小时 | — |\n| `\u002F_nuxt\u002F` 静态资源 | — | `1y` immutable（直出，不落 Node） |\n| 上传媒体 `\u002Fuploads\u002F` | — | `30d` public + gzip（直出） |\n| 登录回调 \u002F 后台 \u002F 后台接口 | `no-store`（绝不缓存） | 反代透传，带 `noindex` 头 |\n\n`SWR`（Stale-While-Revalidate）是个聪明机制：先立刻把缓存的老页面给用户（快），同时后台悄悄刷新（新）。而涉及认证和敏感数据的页面，则**坚决不缓存**，避免把别人的后台响应留在节点上；`\u002Fauth\u002F**`、`\u002Fadmin\u002F**`、`\u002Fapi\u002Fadmin\u002F**` 还会被打上 `x-robots-tag: noindex, nofollow` 防止被搜索引擎收录。\n\n### 5. 云服务器部署：数据主权与成本可控\n\nFoundit 自2026年8月1日起整体迁移至**一台腾讯云 CVM（云服务器）**，：\n\n```text\n互联网 ── HTTPS :443 ── Nginx ── Nuxt SSR（Node）── PostgreSQL\n                                └── \u002Fuploads\u002F → 服务器本地磁盘\n```\n\n这带来几个实打实的好处：\n\n- **数据留在国内**：数据库与媒体文件都在腾讯云，访问国内访客延迟低，也更符合数据驻留与合规要求；不再依赖海外第三方 SaaS。\n- **没有供应商锁定**：PostgreSQL 是标准关系型数据库，本地磁盘就是文件，哪天要迁走，导出 SQL + 打包 `\u002Fuploads\u002F` 即可，不绑定 Supabase 专有能力。\n- **成本可预期**：一台按量或包年 CVM 的账单清清楚楚，不存在\"请求数暴涨导致边缘函数账单失控\"的风险。\n- **证书与域名可控**：TLS 证书直接由腾讯云 SSL 证书控制台下载 Nginx 格式部署，域名走自有 DNS。\n\n> 这对个人创作者尤其友好：**运维负担被一键脚本压到了最低**——`scripts\u002Fsetup-foundit-windows.ps1` \u002F `setup-nginx.ps1` 把装环境、建库、构建、注册开机自启服务、配置 Nginx 与防火墙全部标准化、可重复执行；代码升级只需重新跑一遍脚本。\n\n代价也要说清楚：它不再是\"无限扩展的边缘网络\"——**单台服务器是单点（SPOF）**，需要自行负责系统更新、监控与扩容（垂直升配，或在前面加负载均衡 + 多台）。但内容站本就是\"读多写少\"，单台 2–4 核 CVM + Nginx 足以支撑相当大的流量。\n\n### 6. 数据模型：为\"生长\"而设计\n\n数据库 schema 不是拍脑袋写的。它用枚举约束内容类型与状态（`draft\u002Fpending_review\u002Fpublished\u002Farchived`），用 `jsonb` 存产品截图与链接，用 GIN 索引支持全文搜索，还预留了 `revisions`（修订记录）、`audit_logs`（审计日志）、`auth_users`（用户映射）等\"未来扩展位\"。媒体表 `media` 的 `bucket` 字段现在固定为 `'local'`，表示文件落在服务器本地磁盘（区别于旧 Supabase 的 `public-media` \u002F `private-files`）。\n\n把这些表之间的关系画出来，就能看到这套\"地基\"的全貌——内容表居于核心，分类\u002F标签\u002F专题围绕它展开，而修订、审计、用户映射则像预埋的钢筋，静静等待未来的功能生长上去：\n\n```mermaid\nerDiagram\n    contents ||--o| categories : \"category_id\"\n    contents ||--o{ content_tags : \"多对多\"\n    content_tags }o--|| tags : \"\"\n    topics ||--o{ topic_contents : \"position 排序\"\n    topic_contents }o--|| contents : \"\"\n    media ||--o{ contents : \"cover_url（逻辑关联）\"\n    revisions ||--o| contents : \"content_id\"\n\n    categories { uuid id PK }\n    tags { uuid id PK }\n    contents { uuid id PK }\n    content_tags { uuid content_id PK }\n    topics { uuid id PK }\n    topic_contents { uuid topic_id PK }\n    media { uuid id PK }\n    revisions { uuid id PK }\n    settings { text key PK }\n    audit_logs { uuid id PK }\n    auth_users { uuid id PK }\n```\n\n这意味着：**今天它是个博客，明天它想加评论、加多作者、加付费墙，地基已经留好了。**\n\n## 四、Foundit和传统方案对比\n\n| 维度 | 传统 WordPress \u002F 自建后台 | 普通 SPA（如纯前端框架） | **Foundit** |\n|---|---|---|---|\n| 搜索引擎可见性 | 依赖插件，易出坑 | 差（JS 渲染） | **原生 SSR + 结构化数据** |\n| 安全模型 | 插件质量参差 | 前端路由即\"伪权限\" | **服务端 JWT 校验 + 数据层状态过滤双层** |\n| 运维成本 | 需常驻服务器 + 数据库 | 静态托管但功能受限 | **单台云服务器自托管，运维可控且合规** |\n| 内容形态 | 单一\"文章\" | 自己造轮子 | **文章\u002F产品\u002F想法\u002F专题原生支持** |\n| 设计一致性 | 主题市场鱼龙混杂 | 看团队水平 | **统一设计系统约束** |\n| AI 可发现性 | 基本没考虑 | 基本没考虑 | **内建 GEO 优化** |\n| 数据驻留与合规 | 取决于主机商，常在海外 | 取决于托管地 | **数据库与媒体均在国内腾讯云，合规可控** |\n\n用一个比喻：传统方案像\"自己盖房子，水电自己接，锁自己装\"；Foundit 像\"采用现代装配式建筑——结构、安防、节能标准都是出厂即合规的\"，只不过现在这栋房子稳稳地落在了自家（腾讯云）的地基上。\n\n## 最后\n\nFoundit 给我们的最大启发是：**好的工程不是堆功能，而是把\"正确的事\"变成默认。** 内容该被看见，所以默认 SSR；权限该被守住，所以默认服务端校验；数据该留在国内、成本该被压低，所以默认托管在自有云服务器。\n\n它像一台调校得当的相机——没有花哨的灯，但每一次按下快门，都能稳定地、清晰地把\"内容\"拍下来，递到读者和机器面前。\n\n> *\"页面不主动争夺注意力，而是让内容自然地被看见。\"* —— 这正是 Foundit 设计系统里最动人的一句话，也是它整个架构的底层逻辑。\n","https:\u002F\u002Foxqtewbrpuiouqqjrvdv.supabase.co\u002Fstorage\u002Fv1\u002Fobject\u002Fpublic\u002Fpublic-media\u002F2026-07-17\u002F7c1bf642-5019-452c-a3f0-8eecdd233278.jpg",[],[],"Foundit AI","https:\u002F\u002Ffoundit.cn","f39339b1-aaa6-4e86-b0c2-a6e6a21113b5",{"id":18,"name":19,"slug":20,"description":21},"c523f1c9-338c-4618-add9-9ce67a39b2a0","研究","research","研究成果与启发",[23,27,31],{"id":24,"name":25,"slug":26},"7c76bfc2-f80f-4ee0-a95d-27bd8708b434","技术","slug",{"id":28,"name":29,"slug":30},"63b56667-dcdb-4b8b-bcbe-c405143a7ec2","测评","test",{"id":32,"name":33,"slug":34},"93e74788-a474-46ff-a185-5bdb45ab3b02","Srces工作室产品","srces-product","资料来源",null,"published",false,55,0,"2026-08-01T00:00:00.000Z","2026-08-10T06:26:42.433Z","2026-07-17T06:35:15.766Z",[45,55,65],{"id":46,"type":6,"title":47,"slug":48,"summary":49,"coverUrl":50,"authorName":51,"sno":52,"publishedAt":53,"createdAt":54},"c17c736f-efbe-433b-ad2c-e7c9e24f6108","MCP Tasks：工具调用为什么也需要“任务状态机”？","mcp-tasks-long-running-tool-calls","MCP Tasks 为长时间运行、可轮询、可取消或需要补充输入的工具调用定义了任务状态机。本文解释任务调用与普通 tools\u002Fcall 的区别、能力协商、状态生命周期、TTL 和工程边界。","\u002Fuploads\u002F2026-09-12\u002Fc1ac67e1-5074-4faa-97c3-c3215edd0646.jpg","Foundit",54,"2026-09-12T00:00:00.000Z","2026-09-12T03:56:41.060Z",{"id":56,"type":6,"title":57,"slug":58,"summary":59,"coverUrl":60,"authorName":61,"sno":62,"publishedAt":63,"createdAt":64},"91a515bb-7b16-4b1a-9a13-8a579d689e00","软件复杂度分配：开发者和用户各自该承担什么","software-complexity","当开发者选择“不做某些事情”时，这些事情的复杂度并不会自动消失，而是会以另一种形式转嫁到用户身上","\u002Fuploads\u002F2026-08-31\u002F00da97de-89e8-46ad-babb-19e7e09a5ff0.jpg","Srces工作室",1,"2026-08-31T00:00:00.000Z","2026-08-31T06:12:08.608Z",{"id":66,"type":6,"title":67,"slug":68,"summary":69,"coverUrl":70,"authorName":51,"sno":62,"publishedAt":71,"createdAt":72},"14a78d02-d08a-4cc1-b4b4-5f86f632d90c","DeepSeek-V4-Flash：跑得快没奖励，跑得慢有惩罚","deepseek-v4-flash","DeepSeek-V4-Flash 正式版，是怎么把整个 AI 行业逼到墙角的","\u002Fuploads\u002F2026-08-05\u002Fc4b0e276-bfcb-4796-8da7-aa8ff5df8253.jpg","2026-08-05T00:00:00.000Z","2026-08-05T03:24:39.350Z"]