fix(web): vendor Inter so image builds skip Google Fonts
BUG-430 loaded Inter via next/font/google, which failed Gitea publish run 2408 when the Docker build could not reach fonts.googleapis.com. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
# 印度占星 Skill 更新日志
|
||||
|
||||
## 2026-09-05 — 发布构建不再从网上拉 Inter
|
||||
|
||||
西文正文字体改成仓库里的 Inter 文件,镜像构建不再访问 Google Fonts。页面上看起来还是同一套字。Skill 版本未变。
|
||||
|
||||
## 2026-09-04 — 入学毕业不再连问两道相同的上大学发挥题
|
||||
|
||||
候选区分阶段,同一套发挥选项只问一次。入学和毕业如果切分相同,只问入学那年;毕业不再套「调剂 / 发挥失常」这道题。Skill 版本仍是 10.0.14。
|
||||
|
||||
+21
-4
@@ -6578,15 +6578,15 @@
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-08-29
|
||||
- 最近更新:2026-08-29
|
||||
- 最近更新:2026-09-05
|
||||
- 影响面:全站 `--font-body`、后台 antd `fontFamily`
|
||||
- 用户现象:设计稿写了 Inter,实际请求里没有 woff2;非苹果系统落到 Segoe UI / 微软雅黑。
|
||||
- 触发条件:打开任意带根布局的页面。
|
||||
- 根因:仓库 0 个字体文件、0 处 `@font-face`、0 处 `next/font`,CSS 与 admin token 却把 Inter 写在 StyreneB 之后。
|
||||
- 修复:根布局用 `next/font/google` 加载 Inter(`display: "swap"`、`--font-inter`)。`--font-body` 与 admin `fontFamily` 改为 `var(--font-inter, Inter)`。Tiempos / 宋体栈未改。构建产物与 `/` HTML 可见 woff2 preload。
|
||||
- 修复:根布局用 `next/font/local` 加载仓内 `InterVariable-latin.woff2`(`display: "swap"`、`--font-inter`)。早期曾用 `next/font/google`,构建期会访问 Google Fonts,见 BUG-543。`--font-body` 与 admin `fontFamily` 改为 `var(--font-inter, Inter)`。Tiempos / 宋体栈未改。构建产物与 `/` HTML 可见 woff2 preload。
|
||||
- 验证:`frontend/tests/site-style-isolation-contract.test.ts`;`next build` 的 `/` HTML 含 Inter woff2 preload。
|
||||
- 防复发:字体栈里出现的西文家族必须有 `next/font` 或 `@font-face`;缺授权的展示字体(Tiempos)不得用同一套办法偷偷补上。
|
||||
- 相关记录:无
|
||||
- 防复发:字体栈里出现的西文家族必须有 `next/font/local`(仓内字体文件)或 `@font-face`;不得用 `next/font/google`。缺授权的展示字体(Tiempos)不得用同一套办法偷偷补上。
|
||||
- 相关记录:BUG-543
|
||||
- 复发自:无
|
||||
- 修复版本:待发布
|
||||
|
||||
@@ -8372,3 +8372,20 @@
|
||||
- 相关记录:BUG-390、BUG-540
|
||||
- 复发自:BUG-390(质量探针只对学业发出,但学业内 kind 未收紧)
|
||||
- 修复版本:待发布
|
||||
|
||||
## BUG-543 | staging 镜像构建因 Google Fonts 拉不到 Inter 失败
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-09-05
|
||||
- 最近更新:2026-09-05
|
||||
- 影响面:Gitea `backend-quality-gate.yml` `publish`、`deploy/railway-web.Dockerfile` 的 `RUN npm run build`、`frontend/src/app/layout.tsx`
|
||||
- 用户现象:无终端用户可见现象。门禁 validate 通过后 publish 失败,staging 不发布新镜像。
|
||||
- 触发条件:推送触及门禁路径后,publish 在 Docker 内执行 `npm run build`。构建环境访问不到 `fonts.googleapis.com`。
|
||||
- 根因:BUG-430 用 `next/font/google` 在构建期下载 Inter。validate 跑在 hostexecutor,能出网;publish 的镜像构建不能。Gitea run 2408(SHA `40c623ed`)报 `Failed to fetch Inter from Google Fonts`,`railway-web.Dockerfile:26` 退出 1。
|
||||
- 修复:把 latin 可变 Inter(OFL)放进 `frontend/src/app/fonts/InterVariable-latin.woff2`,根布局改 `next/font/local`。不改 workflow、不设构建期代理。字体栈与 `--font-inter` 不变。
|
||||
- 验证:`site-style-isolation-contract` 锁定 `next/font/local`、仓内 woff2 魔数、以及 `frontend/src` 不再出现 `next/font/google` / `fonts.googleapis.com`。本机 `npx tsx --test tests/site-style-isolation-contract.test.ts`;`tsc --noEmit`。
|
||||
- 防复发:不得把西文正文字体改回 `next/font/google`。新字体必须是仓内文件 + `next/font/local` 或 `@font-face`。
|
||||
- 相关记录:BUG-430
|
||||
- 复发自:BUG-430(加载方式在无 Google Fonts 网络的镜像构建里不成立)
|
||||
- 修复版本:待发布
|
||||
|
||||
|
||||
@@ -42,3 +42,13 @@
|
||||
| `run_quality_gate.py --profile quick` 内 `npm test` | 2682 tests / 2673 pass / **9 fail**,全部是 Docker 迁移 `database migration failed`(`database-foundation` / `database-local-business` / redeem / identity / secret-refs / v9×3 / Skill migration)。与本单无关,属既有环境缺口。 |
|
||||
|
||||
未跑全量 `npm test`(时间);未做浏览器真人走查(无登录态)。清单:`docs/testing/rectification-quality-probe-dedupe-20260904.md`。
|
||||
|
||||
## 门禁修复(BUG-543)
|
||||
|
||||
Gitea run 2408:`validate` 通过,`publish` 在 `railway-web.Dockerfile` `RUN npm run build` 失败。报错 `Failed to fetch Inter from Google Fonts`。根因是 BUG-430 的 `next/font/google` 在镜像构建里访问不到 Google。未改 workflow。
|
||||
|
||||
| 项 | 原值 | 新值 | 原因 |
|
||||
| --- | --- | --- | --- |
|
||||
| `site-style-isolation-contract` Inter 加载断言 | `import { Inter } from "next/font/google"` | `import localFont from "next/font/local"` + 仓内 woff2 魔数 + `frontend/src` 禁止 `from "next/font/google"` / `fonts.googleapis.com` | 构建期不得出网拉字体 |
|
||||
|
||||
本机补充:`npx tsx --test tests/site-style-isolation-contract.test.ts` **4/4**;`tsc --noEmit` 0 错;改动文件 eslint 0 error。本机 `npm run build` 被 worktree 里 `frontend/node_modules` 指向仓外的 symlink 挡住(Turbopack `points out of the filesystem root`),属既有环境缺口;CI publish 镜像内是 `npm ci`,无此 symlink。编号:BUG-543(未占用预留的 BUG-542)。未改 `.gitea/workflows/**`。
|
||||
|
||||
+1
-1
@@ -136,7 +136,7 @@ is read through an external store so a change in one tab reaches the others.
|
||||
### Font stacks
|
||||
|
||||
- Display: `"Tiempos Headline", "Songti SC", "STSong", "Noto Serif CJK SC", Georgia, serif`. The licensed Copernicus/Tiempos files are unavailable; the Chinese Song serif stack is the declared production substitute.
|
||||
- Body/UI: `StyreneB, var(--font-inter, Inter), -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif`. Inter is loaded with `next/font/google` (`display: "swap"`, CSS variable `--font-inter`) so Windows/Linux no longer silently fall back past a never-requested Inter. Tiempos Headline remains unloaded; the Songti stack is still the declared production substitute.
|
||||
- Body/UI: `StyreneB, var(--font-inter, Inter), -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif`. Inter is loaded with `next/font/local` from `src/app/fonts/InterVariable-latin.woff2` (`display: "swap"`, CSS variable `--font-inter`) so Windows/Linux no longer silently fall back past a never-requested Inter, and image builds do not call fonts.googleapis.com. Tiempos Headline remains unloaded; the Songti stack is still the declared production substitute.
|
||||
- Code/data: `"JetBrains Mono", "SFMono-Regular", Consolas, monospace`, exposed as `--font-mono`.
|
||||
- Root boundary pages (`error.tsx`, `not-found.tsx`, `forbidden.tsx`, `global-error.tsx`) sit in the shared root layout segment and must not import `globals.css` — importing it would drag the chat stylesheet onto every admin route. They therefore cannot read `--font-mono` or any token, and inline their own values: a system stack for body text and `ui-monospace, SFMono-Regular, Menlo, monospace` for code. Keep those literals in step with the stacks above by hand.
|
||||
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,93 @@
|
||||
Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter) Inter-Italic[opsz,wght].ttf: Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,5 @@
|
||||
Inter latin variable (wght axis)
|
||||
File: InterVariable-latin.woff2
|
||||
Taken from @fontsource-variable/inter@5.3.0 files/inter-latin-wght-normal.woff2
|
||||
Upstream: https://github.com/rsms/inter
|
||||
License: SIL Open Font License 1.1 (OFL.txt)
|
||||
@@ -1,14 +1,17 @@
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import localFont from "next/font/local";
|
||||
import Script from "next/script";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { StaleClientRecovery } from "@/components/stale-client-recovery";
|
||||
import { themePreferenceBootScript } from "@/lib/theme-preference";
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
// Vendored Inter latin variable (OFL). Do not switch back to next/font/google;
|
||||
// the publish image cannot download fonts at build time.
|
||||
const inter = localFont({
|
||||
src: "./fonts/InterVariable-latin.woff2",
|
||||
display: "swap",
|
||||
variable: "--font-inter",
|
||||
weight: "100 900",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { readdirSync, readFileSync } from "node:fs";
|
||||
import { extname, join } from "node:path";
|
||||
import test from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const layout = readFileSync(new URL("../src/app/layout.tsx", import.meta.url), "utf8");
|
||||
const siteStyles = readFileSync(new URL("../src/app/site-styles.ts", import.meta.url), "utf8");
|
||||
@@ -10,7 +12,9 @@ const homePage = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "
|
||||
const loginPage = readFileSync(new URL("../src/app/login/page.tsx", import.meta.url), "utf8");
|
||||
|
||||
test("Inter is loaded through next/font and applied as a CSS variable", () => {
|
||||
assert.match(layout, /import \{ Inter \} from "next\/font\/google"/);
|
||||
assert.match(layout, /import localFont from "next\/font\/local"/);
|
||||
assert.doesNotMatch(layout, /from ["']next\/font\/google["']/);
|
||||
assert.match(layout, /src: "\.\/fonts\/InterVariable-latin\.woff2"/);
|
||||
assert.match(layout, /display: "swap"/);
|
||||
assert.match(layout, /variable: "--font-inter"/);
|
||||
assert.match(layout, /className=\{inter\.variable\}/);
|
||||
@@ -21,6 +25,31 @@ test("Inter is loaded through next/font and applied as a CSS variable", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("Inter is vendored locally so production builds do not fetch Google Fonts", () => {
|
||||
const font = readFileSync(new URL("../src/app/fonts/InterVariable-latin.woff2", import.meta.url));
|
||||
assert.equal(Buffer.from(font.subarray(0, 4)).toString(), "wOF2");
|
||||
assert.ok(font.byteLength > 20_000);
|
||||
const srcRoot = fileURLToPath(new URL("../src/", import.meta.url));
|
||||
const stack = [srcRoot];
|
||||
while (stack.length > 0) {
|
||||
const dir = stack.pop()!;
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
const fullPath = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
stack.push(fullPath);
|
||||
continue;
|
||||
}
|
||||
if (![".ts", ".tsx", ".js", ".jsx", ".css"].includes(extname(entry.name))) continue;
|
||||
const source = readFileSync(fullPath, "utf8");
|
||||
assert.doesNotMatch(
|
||||
source,
|
||||
/from ["']next\/font\/google["']|fonts\.googleapis\.com/,
|
||||
`${fullPath} must not fetch Google Fonts at build time`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("site chrome owns globals.css; admin does not import it", () => {
|
||||
assert.match(siteStyles, /import "\.\/globals\.css"/);
|
||||
assert.match(siteStyles, /import "\.\/birth-time-choice\.css"/);
|
||||
|
||||
Reference in New Issue
Block a user