🔌 Adapter development guide
ForcedSkin adapters are authored as declarative JSON formulas ( forcedskin-adapter-formula/v1 ) describing which hostnames to target and which selector layers to tint. The server stores JSON only; the extension ships a fixed interpreter that applies paint rules—never arbitrary JavaScript.
How it works
The core engine starts with CSS-variable + inline overlays for baseline recolouring.
When a hostname hits, adapters run ascending by priority—the smaller the number, the sooner the interpreter executes (recommended site adapters use 100).
Each adapter tweaks targeted nodes via the exposed engineApi helpers; skip transparent overlays/player stacks per the Best practices notes.
The runtime already observes DOM deltas and reapplies adapters throttled—you do not need MutationObserver boilerplate.
Formulas arrive from GET /api/pub/extension-adapters ; every record’s JSON blob is cached locally. Updating the portal copy takes effect once users refresh adapters or reopen the browser—no reinstall required.
Minimal example
{
"schema": "forcedskin-adapter-formula/v1",
"id": "example-site",
"priority": 100,
"match": {
"hostname": [
{ "op": "suffixDomain", "value": "example.com" }
]
},
"layers": [
{
"kind": "surface",
"skipOverlayLike": true,
"selectors": [".site-navbar", ".site-header"]
}
]
}Adapter formula forcedskin-adapter-formula/v1
Submitted JSON must pass server validation—the root schema looks like this:
| Field | Type | Required | Description |
|---|---|---|---|
| schema | string | Required | Literal version string forcedskin-adapter-formula/v1 |
| id | string | Required | Logical adapter id—usually the site codename, e.g. bilibili |
| priority | number | Optional | Execution order (ascending). Site-specific adapters typically use 100 |
| match.hostname | Rule[] | Required | Array of hostname rule objects (see table below) |
| layers | Layer[] | Required | Ordered paint layers— kinds documented in the layer table |
match.hostname rules
| Operator | Meaning |
|---|---|
| equals | Hostname equals value (case-insensitive) |
| suffixDomain | Hostname equals value or ends with .value (covers both example.com and *.example.com) |
Palette keys (richText cssVars / color)
| Field | Description |
|---|---|
| background | Page background |
| foreground | Primary text |
| surface | Card / panel fill |
| surfaceMuted | Muted containers / hover fills |
| border | Border colour |
| muted | Secondary text |
| primary500 | Primary emphasis (links) mapped from theme primary.500 |
| primary700 | Deeper primary state mapped from primary.700 or 800 |
Recommended layer kinds
Think semantically—not just by element names. See the checked-in sample home/server/seeds/bilibili-adapter.formula.json for a full walkthrough.
| Layer kind | Purpose | Typical mapping | markApplied |
|---|---|---|---|
| surface | Panels, list rows, nav shells | background→surface, color→foreground, border→border | bg + text + border |
| accent | Active tabs / current list rows | background+border→primary700, color→background for contrast | bg + text + border |
| canvas | Hero slabs with raster backgrounds | Strip background-image, background→palette background | Usually background only |
| richText | Sites that expose branded web components | Emit required CSS variables + textual colour mapping | text (sometimes + background) |
| svgRecolor | Inline SVG glyphs | Default fill/stroke→currentColor; optional fill/stroke palette keys for fixed colour | Optional marking to avoid interfering with cleanup |
Skip risky regions: The engine already ignores media, canvas, iframe, heavy blend/backdrop layers, and nodes whose classes hint at masks or overlays—extend those guardrails for translucent player chrome.
Host opt-out: Mark subtrees (e.g. previews) with data-gts-ignore so their descendants skip global repainting.
Full sample excerpt
{
"schema": "forcedskin-adapter-formula/v1",
"id": "bilibili",
"priority": 100,
"match": {
"hostname": [
{ "op": "equals", "value": "bilibili.com" },
{ "op": "suffixDomain", "value": "bilibili.com" }
]
},
"layers": [
{
"kind": "surface",
"skipOverlayLike": true,
"selectors": ["[class*='bili-']", "[class*='bpx-']"]
},
{
"kind": "accent",
"selectors": ["[class*='active']", ".bili-dyn-list-tabs__item.active"]
},
{
"kind": "canvas",
"selectors": [".message-bg", ".message-bgc"]
},
{
"kind": "richText",
"selectors": ["bili-rich-text"],
"cssVars": {
"--bili-rich-text-color": "foreground",
"--bili-rich-text-link-color": "primary500",
"--bili-rich-text-link-color-hover": "primary700"
},
"color": "foreground"
},
{
"kind": "svgRecolor",
"selectors": ["svg path", "svg rect", "svg circle"]
}
]
}Best practices
JSON must strictly validate
Invalid schema/layers/host rules bounce during review—fold this guide’s minimal sample & seeds before submitting.
Enable skipOverlayLike on surface
Matches core engine safeguards so translucent HUDs/video stacks are not flattened to solid fills.
Never ship JavaScript
The code field is JSON only; the extension no longer evals strings through new Function.
Submission checklist
🔐 Sign in before filing a submission.
📋 Provide display name, comma-separated domains, and paste the JSON into the adapter code textarea.
⏳ Entries land in pending until staff validate safety + selector scope.
✅ Approved adapters roll out to every extension user who syncs adapters.
❌ Overly broad selectors or invalid JSON return feedback—fix and resubmit.
Target domain format
siteDomain is the human-facing list; matching actually keys off match.hostname like:
[
{ "op": "equals", "value": "bilibili.com" },
{ "op": "suffixDomain", "value": "bilibili.com" }
]Ready? Open the adapter gallery and submit your JSON.
🔌 Submit an adapter