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需要精確控制每個字型與文字的頁面,例如字型展示、預覽器
PathUse for
webfont-api.jsCustomer sites. Scans page text automatically, no request code needed
/api/fonts/subsetPages needing per-font control, such as specimen or preview pages
同一個頁面不要同時使用兩種。兩者都會注入 @font-face,會互相覆蓋。
Do not use both on the same page. Each injects @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) 指定範圍
modeOn loadAfterwards
autoScans and requestsWatches the whole page with a MutationObserver and requests again as content changes
manualScans and requestsNothing. Call TWINTYPE.refresh() when you need it
observeScans and requestsNothing 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
hideBodytrue字型套用前先隱藏內容,避免看到字型替換的跳動
debounce450合併連續 DOM 變動的延遲毫秒數
maxChars1200單次請求的字元上限
api官方端點覆寫 subset API 位址
KeyDefaultDescription
mode"auto"auto / manual / observe. Unrecognised values fall back to auto
hideBodytrueHide content until fonts apply, avoiding a visible swap
debounce450Milliseconds to coalesce consecutive DOM changes
maxChars1200Character ceiling per request
apiOfficial endpointOverride 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()目前模式
MethodDescription
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 物件,不會覆蓋你先掛上的回呼。
The script reuses an existing 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

欄位型別必填說明
tokenstring是專案 Token
familystring是字型的 family 名稱
textstring是要包含的字元。重複字元會被去除
sitestring是請求來源,用於比對允許網域。通常傳 location.origin
weightnumber否CSS 字重。省略時視為 0,會挑最接近的字重
FieldTypeRequiredDescription
tokenstringyesProject token
familystringyesFont family name
textstringyesCharacters to include. Duplicates are removed
sitestringyesRequest origin, matched against allowed domains. Usually location.origin
weightnumbernoCSS 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是否來自快取。兩者都計費
FieldDescription
subset_urlSubset file URL, usable directly as an @font-face src
cachedWhether 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.

未設定任何允許網域的 Token 會被一律拒絕。
A token with no allowed domains configured is always rejected.

字型解析順序

Font resolution order

  1. 該 Token 已套用的自訂字型中,family 相符者
  2. 該 Token 所屬方案的字型庫中,family 相符者
  1. Custom fonts applied to this token whose family matches
  2. 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.

自訂字型必須在主控台「套用」到該專案才會生效。僅上傳而未套用的字型不會被提供,即使屬於同一個帳號。
A custom font must be applied to the project in the console. Uploading alone is not enough — an unapplied font is never served, even to its own owner.

節流建議

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 requestJSON 無法解析,或欄位型別不符(例如 weight 傳了字串)
missing family, text or token三個必填欄位有缺
errorCause
invalid requestJSON could not be parsed, or a field has the wrong type (for example weight sent as a string)
missing family, text or tokenOne of the three required fields is absent

401

error原因
missing tokentoken 為空
missing sitesite 為空。嵌入腳本會自動帶 location.origin,直接呼叫 API 時要自己帶
invalid token找不到這個 Token
domain not allowedsite 正規化後不在該 Token 的允許網域清單內
font family not found查詢字型時發生資料庫錯誤
font family empty沒有任何字型使用這個 family 名稱
this plan does not include this font familyfamily 存在,但不在該 Token 所屬方案的字型庫內
errorCause
missing tokentoken was empty
missing sitesite was empty. The embed script sends location.origin automatically; direct API callers must supply it
invalid tokenNo such token
domain not allowedNormalised site is not in the token's allowed domains
font family not foundDatabase error while looking up the family
font family emptyNo font uses this family name
this plan does not include this font familyThe 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找不到對應的訂閱紀錄。屬於設定不完整,請聯繫我們
errorCause
no remaining PVs or plan expiredQuota exhausted or the subscription has expired
no subscription record for this user/planNo matching subscription row. This indicates incomplete setup — contact us

500

error原因
failed to download font from storage原始字型檔在儲存空間中不存在或無法讀取
subset failed: ...子集程序失敗,訊息含實際輸出
failed to upload subset font子集檔上傳失敗
errorCause
failed to download font from storageThe source font file is missing or unreadable in storage
subset failed: ...The subsetting process failed; the message carries its output
failed to upload subset fontUploading the subset failed

字型沒有變化

Fonts do not change

API 回 success: true 但畫面沒變,依序檢查:

When the API returns success: true but nothing renders differently, check in order:

  1. CSS 的 font-family 是否與字型的 family 名稱完全相同,包含空白與全角字元
  2. 是否有更後面的 CSS 規則覆蓋了 font-family
  3. 是否同時載入了嵌入腳本又自己注入 @font-face,兩者互相覆蓋
  4. 自訂字型是否已在主控台套用到這個專案
  1. Whether the CSS font-family matches the family name exactly, including spaces and full-width characters
  2. Whether a later CSS rule overrides font-family
  3. Whether the embed script and your own @font-face injection are both present, overwriting each other
  4. Whether the custom font has been applied to this project in the console
family 名稱以字型檔的 name 表為準,可能包含全角字元或版本號,例如 pingting。請從主控台複製,不要自己輸入。
Family names come from the font's name table and may contain full-width characters or version numbers, such as pingting. Copy them from the console rather than typing them.