Site adaptersDevelopment guide

🔌 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 recoloring.

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:

FieldTypeRequiredDescription
schemastringRequiredLiteral version string forcedskin-adapter-formula/v1
idstringRequiredLogical adapter id—usually the site codename, e.g. bilibili
prioritynumberOptionalExecution order (ascending). Site-specific adapters typically use 100
match.hostnameRule[]RequiredArray of hostname rule objects (see table below)
layersLayer[]RequiredOrdered paint layers— kinds documented in the layer table

match.hostname rules

OperatorMeaning
equalsHostname equals value (case-insensitive)
suffixDomainHostname equals value or ends with .value (covers both example.com and *.example.com)

Palette keys (richText cssVars / color)

FieldDescription
backgroundPage background
foregroundPrimary text
surfaceCard / panel fill
surfaceMutedMuted containers / hover fills
borderBorder color
mutedSecondary text
primary500Primary emphasis (links) mapped from theme primary.500
primary700Deeper 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 kindPurposeTypical mappingmarkApplied
surfacePanels, list rows, nav shellsbackground→surface, color→foreground, border→borderbg + text + border
accentActive tabs / current list rowsbackground+border→primary700, color→background for contrastbg + text + border
canvasHero slabs with raster backgroundsStrip background-image, background→palette backgroundUsually background only
richTextSites that expose branded web componentsEmit required CSS variables + textual color mappingtext (sometimes + background)
svgRecolorInline SVG glyphsDefault fill/stroke→currentColor; optional fill/stroke palette keys for fixed colorOptional 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