TwinType Webfont
依頁面實際用到的字元動態產生字型子集。瀏覽器只下載該頁需要的字,而不是整套字型檔。
Generates font subsets from the characters a page actually uses. The browser downloads only the glyphs on that page instead of a complete font file.
需要準備
Prerequisites
- 一個專案 Token(在主控台建立專案後取得)
- 該專案的允許網域清單中,包含你要嵌入的網站網域
- A project token, issued when you create a project in the console
- The site's domain listed in that project's allowed domains
最小可用範例
Minimal setup
<script src="https://api.twintype.co/webfont-api.js?token=YOUR_TOKEN"></script>
貼在 <head>。腳本會在載入時掃描頁面文字、向 API 請求對應的子集,並注入
@font-face。
Place it in <head>. On load the script scans the page text, requests the
matching subset, and injects @font-face.
字型的套用仍由你的 CSS 決定:
Applying the font is still up to your CSS:
body {
font-family: 'pingting', sans-serif;
}
font-family 的值必須與字型的 family 名稱一致。方案內的字型可在主控台的字型預覽頁查到;
自訂字型的 family 顯示在「我的字體」。
The font-family value must match the font's family name exactly. Plan fonts are
listed on the console's font preview page; family names for your own uploads appear under
My Fonts.
兩種整合方式
Two integration paths
| 方式 | 適用 |
|---|---|
webfont-api.js | 客戶網站。自動掃描頁面文字,不需自己寫請求 |
/api/fonts/subset | 需要精確控制每個字型與文字的頁面,例如字型展示、預覽器 |
| Path | Use for |
|---|---|
webfont-api.js | Customer sites. Scans page text automatically, no request code needed |
/api/fonts/subset | Pages needing per-font control, such as specimen or preview pages |
@font-face,會互相覆蓋。
@font-face and they overwrite
each other.
計費單位
Billing
每一次子集請求消耗 1 PV,快取命中也計算。額度依方案,用完後 API 回傳 402。
Each subset request consumes 1 PV, including cache hits. Quota depends on the plan; the API returns 402 once it is exhausted.
嵌入腳本
Embed Script
<script>
window.TWINTYPE_CONFIG = {
mode: "auto",
hideBody: true
};
</script>
<script src="https://api.twintype.co/webfont-api.js?token=YOUR_TOKEN"></script>
TWINTYPE_CONFIG 必須在腳本標籤之前宣告,否則不會被讀到。
TWINTYPE_CONFIG must be declared before the script tag or it will not be read.
Token 從腳本的 src query string 取得。若同一頁有多個腳本標籤,第一個帶
token= 的會被採用。
The token is taken from the script's src query string. With multiple script
tags, the first one carrying token= wins.
模式
Modes
三種模式在載入時都會掃描頁面並請求一次。差別在載入之後還會不會自動再請求。
All three modes scan the page and issue one request on load. They differ in whether anything happens afterwards.
| mode | 載入時 | 之後 |
|---|---|---|
auto | 掃描並請求 | 以 MutationObserver 持續監看整頁,內容變動就補字 |
manual | 掃描並請求 | 不監看。需要時自己呼叫 TWINTYPE.refresh() |
observe | 掃描並請求 | 不監看,直到呼叫 TWINTYPE.observe(el) 指定範圍 |
| mode | On load | Afterwards |
|---|---|---|
auto | Scans and requests | Watches the whole page with a MutationObserver and requests again as content changes |
manual | Scans and requests | Nothing. Call TWINTYPE.refresh() when you need it |
observe | Scans and requests | Nothing until you call TWINTYPE.observe(el) to scope it |
manual 不代表「不請求」。載入時的那一次請求一樣會發出,也一樣計費。
若在 manual 下額外呼叫 refresh(),會多發一次請求。
manual does not mean "no request". The initial request still fires and is still
billed. Calling refresh() on top of it produces a second request.
設定項
Configuration
| 鍵 | 預設 | 說明 |
|---|---|---|
mode | "auto" | auto / manual / observe。無法識別的值會退回 auto |
hideBody | true | 字型套用前先隱藏內容,避免看到字型替換的跳動 |
debounce | 450 | 合併連續 DOM 變動的延遲毫秒數 |
maxChars | 1200 | 單次請求的字元上限 |
api | 官方端點 | 覆寫 subset API 位址 |
| Key | Default | Description |
|---|---|---|
mode | "auto" | auto / manual / observe. Unrecognised values fall back to auto |
hideBody | true | Hide content until fonts apply, avoiding a visible swap |
debounce | 450 | Milliseconds to coalesce consecutive DOM changes |
maxChars | 1200 | Character ceiling per request |
api | Official endpoint | Override the subset API URL |
window.TWINTYPE_API 也可覆寫 API 位址,優先於 CONFIG.api。
window.TWINTYPE_API also overrides the API URL and takes precedence over
CONFIG.api.
狀態 class
State classes
腳本會在 <html> 上維護目前狀態,可用於自訂載入樣式。
The script maintains the current state on <html> for styling load
transitions.
| class | 狀態State |
|---|---|
tw-loading | 首次載入中Initial load in progress |
tw-reloading | 重新載入中Reload in progress |
tw-active | 字型已套用Fonts applied |
tw-inactive | 沒有取得任何字型No fonts returned |
html.tw-loading .content { visibility: hidden; }
JS API
腳本載入後掛在 window.TWINTYPE。
Available on window.TWINTYPE once the script has loaded.
| 方法 | 說明 |
|---|---|
refresh() | 取消待處理的 debounce,立即重新掃描並請求 |
flush() | refresh() 的別名 |
observe(el) | 把掃描範圍限定到 el,開始監看它,並立即請求一次。傳 null 代表回到整頁 |
unobserve() | 停止監看 DOM 變動。已注入的字型不受影響 |
setText(text) | 直接指定要子集的文字,跳過頁面掃描。傳 null 清除 |
setDebounce(ms) | 調整 debounce 毫秒數,回傳實際採用的值 |
getDebounce() | 目前的 debounce 毫秒數 |
getRoot() | 目前的掃描範圍元素,整頁時為 null |
getMode() | 目前模式 |
| Method | Description |
|---|---|
refresh() | Cancels any pending debounce, rescans and requests immediately |
flush() | Alias for refresh() |
observe(el) | Scopes scanning to el, starts watching it, and requests once. Pass null to return to the whole page |
unobserve() | Stops watching DOM changes. Already injected fonts are unaffected |
setText(text) | Subsets the given text directly, bypassing the page scan. Pass null to clear |
setDebounce(ms) | Sets the debounce in milliseconds and returns the value applied |
getDebounce() | Current debounce in milliseconds |
getRoot() | Current scan scope element, null when scanning the whole page |
getMode() | Current mode |
setText
setText
用於預覽器一類的介面:文字來自輸入框,而不是頁面內容。
For preview interfaces where the text comes from an input rather than the page.
input.addEventListener('input', () => {
TWINTYPE.setText(input.value);
});
setText 設定後會一直生效,頁面掃描不再影響請求內容。要恢復掃描,傳
null。
Once set, setText stays in effect and page scanning no longer affects requests.
Pass null to resume scanning.
unobserve
unobserve
在 auto 模式下呼叫,等同於「只吃第一次掃描的結果」:
Called under auto, this makes the first scan the only one:
window.addEventListener('load', () => {
TWINTYPE.unobserve();
});
回呼
Callbacks
指派為 window.TWINTYPE 上的函式。回呼中拋出的例外會被忽略,不會中斷字型載入。
Assign functions on window.TWINTYPE. Exceptions thrown from a callback are
swallowed and will not interrupt font loading.
| 回呼Callback | 觸發時機Fires when |
|---|---|
onPreload | 首次載入開始Initial load begins |
onReload | 重新載入開始Reload begins |
onActived | 字型已套用Fonts have been applied |
onInactived | 沒有取得任何字型No fonts were returned |
TWINTYPE.onActived = () => {
document.body.classList.add('fonts-ready');
};
回呼必須在腳本載入之後指派。若要在載入前就準備好,先建立物件:
Callbacks must be assigned after the script loads. To have them ready earlier, create the object first:
window.TWINTYPE = window.TWINTYPE || {};
window.TWINTYPE.onActived = () => { /* ... */ };
window.TWINTYPE 物件,不會覆蓋你先掛上的回呼。
window.TWINTYPE object on init, so callbacks
assigned beforehand are preserved.
Subset API
POST https://api.twintype.co/api/fonts/subset
Content-Type: application/json
請求
Request
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
token | string | 是 | 專案 Token |
family | string | 是 | 字型的 family 名稱 |
text | string | 是 | 要包含的字元。重複字元會被去除 |
site | string | 是 | 請求來源,用於比對允許網域。通常傳 location.origin |
weight | number | 否 | CSS 字重。省略時視為 0,會挑最接近的字重 |
| Field | Type | Required | Description |
|---|---|---|---|
token | string | yes | Project token |
family | string | yes | Font family name |
text | string | yes | Characters to include. Duplicates are removed |
site | string | yes | Request origin, matched against allowed domains. Usually location.origin |
weight | number | no | CSS weight. Treated as 0 when omitted, which selects the nearest weight |
{
"token": "YOUR_TOKEN",
"site": "https://example.com",
"family": "pingting",
"weight": 600,
"text": "天地玄黃 宇宙洪荒"
}
回應
Response
{
"success": true,
"subset_url": "https://api.twintype.co/fonts/subset/subset_<hash>.woff2",
"cached": false
}
| 欄位 | 說明 |
|---|---|
subset_url | 子集檔位址,可直接放進 @font-face 的 src |
cached | 是否來自快取。兩者都計費 |
| Field | Description |
|---|---|
subset_url | Subset file URL, usable directly as an @font-face src |
cached | Whether it came from cache. Both are billed |
注入字型
Injecting the font
function injectFontFace(family, weight, url) {
const id = `tt-${family}-${weight}`;
let el = document.getElementById(id);
if (!el) {
el = document.createElement('style');
el.id = id;
document.head.appendChild(el);
}
el.textContent = `
@font-face {
font-family: '${family}';
font-weight: ${weight};
font-display: block;
src: url('${url}') format('woff2');
}
`;
}
用固定的 id 覆寫同一個 <style>,避免重複請求累積出大量
@font-face 規則。
Overwriting a <style> with a fixed id keeps repeated requests
from accumulating @font-face rules.
網域比對
Domain matching
site 會正規化後與專案的允許網域逐一比對,必須完全相等。正規化會去除協定、路徑與通訊埠。
因此 https://example.com:8443/page 與 example.com 相符。
子網域不會自動涵蓋:www.example.com 與 example.com
是兩個不同的值,都要分別加入。
site is normalised and compared against each allowed domain for an exact match.
Normalisation strips the scheme, path and port, so
https://example.com:8443/page matches example.com. Subdomains are
not covered automatically: www.example.com and example.com are
distinct values and both must be listed.
字型解析順序
Font resolution order
- 該 Token 已套用的自訂字型中,family 相符者
- 該 Token 所屬方案的字型庫中,family 相符者
- Custom fonts applied to this token whose family matches
- Fonts in the token's plan library whose family matches
兩者皆以 family 相符為前提,再從候選中挑字重最接近 weight 的一支。
自訂字型優先,因此以相同 family 上傳的自訂字型會覆蓋方案字型。
Both require a family match; the nearest weight to weight is then chosen from
the candidates. Custom fonts take priority, so uploading a custom font under an existing
family name overrides the plan font.
節流建議
Throttling
使用者輸入時不要每個按鍵都發請求。以 300–450 毫秒 debounce 合併,再一次送出。
Do not issue a request per keystroke. Coalesce with a 300–450 ms debounce and send once.
let timer;
input.addEventListener('input', () => {
clearTimeout(timer);
timer = setTimeout(() => loadPreviewFont(input.value), 450);
});
錯誤參考
Error Reference
失敗一律回傳 success: false 與 error 字串。
Failures return success: false and an error string.
{ "success": false, "error": "domain not allowed" }
400
| error | 原因 |
|---|---|
invalid request | JSON 無法解析,或欄位型別不符(例如 weight 傳了字串) |
missing family, text or token | 三個必填欄位有缺 |
| error | Cause |
|---|---|
invalid request | JSON could not be parsed, or a field has the wrong type (for example weight sent as a string) |
missing family, text or token | One of the three required fields is absent |
401
| error | 原因 |
|---|---|
missing token | token 為空 |
missing site | site 為空。嵌入腳本會自動帶 location.origin,直接呼叫 API 時要自己帶 |
invalid token | 找不到這個 Token |
domain not allowed | site 正規化後不在該 Token 的允許網域清單內 |
font family not found | 查詢字型時發生資料庫錯誤 |
font family empty | 沒有任何字型使用這個 family 名稱 |
this plan does not include this font family | family 存在,但不在該 Token 所屬方案的字型庫內 |
| error | Cause |
|---|---|
missing token | token was empty |
missing site | site was empty. The embed script sends location.origin automatically; direct API callers must supply it |
invalid token | No such token |
domain not allowed | Normalised site is not in the token's allowed domains |
font family not found | Database error while looking up the family |
font family empty | No font uses this family name |
this plan does not include this font family | The family exists but is not in the token's plan library |
domain not allowed 是最常見的錯誤。檢查順序:Token 是否有設定允許網域、
site 的主機名稱是否與清單完全相符、是否漏了 www 之類的子網域。
domain not allowed is the most common failure. Check in order: whether the token
has any allowed domains, whether the site host matches one exactly, and whether
a subdomain such as www is missing.
402
| error | 原因 |
|---|---|
no remaining PVs or plan expired | 額度用完,或訂閱已過期 |
no subscription record for this user/plan | 找不到對應的訂閱紀錄。屬於設定不完整,請聯繫我們 |
| error | Cause |
|---|---|
no remaining PVs or plan expired | Quota exhausted or the subscription has expired |
no subscription record for this user/plan | No matching subscription row. This indicates incomplete setup — contact us |
500
| error | 原因 |
|---|---|
failed to download font from storage | 原始字型檔在儲存空間中不存在或無法讀取 |
subset failed: ... | 子集程序失敗,訊息含實際輸出 |
failed to upload subset font | 子集檔上傳失敗 |
| error | Cause |
|---|---|
failed to download font from storage | The source font file is missing or unreadable in storage |
subset failed: ... | The subsetting process failed; the message carries its output |
failed to upload subset font | Uploading the subset failed |
字型沒有變化
Fonts do not change
API 回 success: true 但畫面沒變,依序檢查:
When the API returns success: true but nothing renders differently, check in
order:
- CSS 的
font-family是否與字型的 family 名稱完全相同,包含空白與全角字元 - 是否有更後面的 CSS 規則覆蓋了
font-family - 是否同時載入了嵌入腳本又自己注入
@font-face,兩者互相覆蓋 - 自訂字型是否已在主控台套用到這個專案
- Whether the CSS
font-familymatches the family name exactly, including spaces and full-width characters - Whether a later CSS rule overrides
font-family - Whether the embed script and your own
@font-faceinjection are both present, overwriting each other - Whether the custom font has been applied to this project in the console
pingting。請從主控台複製,不要自己輸入。
pingting. Copy them from the console rather than
typing them.