There are two layers of customization for a self-hosted instance:

  1. Project configuration in uvproj.yaml (and Translations/ui18n-config.yaml) — branding, canonical URL and the external service URLs. These are substituted into the source at build time by the UVKBuildTool, so they apply to both the HTML templates and the JS.
  2. Runtime tuning constants scattered across a few JS files in CONSTANT BLOCK comment fences — grading, animation timings, writer sizing and the character-database download behaviour.

Project configuration (uvproj.yaml)

The variables: section of uvproj.yaml is the single source of truth for branding and URLs. Each var/val pair is substituted wherever the template token appears in any processed file.

Variable Default What it controls
trademark Youyin The product/brand name. Substituted via in the HTML chrome (page titles, Open Graph/Twitter meta) and via the {brand} placeholder inside translation strings.
cannonical_url https://youyin.madladsquad.com The deployed site's base URL. CI rewrites https://madladsquad.com/ links to this and it feeds the canonical/OG/sitemap URLs.
marketplace_url https://github.com/MadLadSquad/YouyinPublicDeckRepository Human-facing link to the public deck repository.
marketplace_cdn_url https://cdn.jsdelivr.net/gh/MadLadSquad/YouyinPublicDeckRepository@latest jsDelivr base the marketplace page fetches deck lists/decks from (MARKETPLACE_CDN in marketplace.js, also pre-cached by sw.js).
char_data_url https://cdn.jsdelivr.net/gh/MadLadSquad/hanzi-writer-data-youyin@latest jsDelivr base for the character stroke database. Builds CHARACTER_MANIFEST_URL and CHARACTER_CHUNK_URL_BASE in character-database.js.
additional_cdn_hosts none Extra hosts to add to the service worker's pre-cache list (sw.js). The literal string none disables the block.

Renaming the site

Because the brand appears in two different substitution engines, renaming requires editing two files:

Never hardcode the name inside a translation string or template — always write {brand} / so a rename stays a two-line change. Other {name}-style placeholders (e.g. {streak}) are filled at runtime by JS, not at build time.

Runtime tuning constants

These live inside // --- CONSTANT BLOCK EDIT IF RUNNING ON A CUSTOM SYSTEM --- fences. They are no longer all in one file — each block sits next to the code that uses it.

scripts/index.js — core

window.MAX_KNOWLEDGE_LEVEL = 4;

window.HOUR_UNIX   = 3600000;   // ms in an hour
window.MINUTE_UNIX = 60000;
window.SECOND_UNIX = 1000;

scripts/pages/main-page.js — grading, session size & writer timing

window.MAX_POINTS_ON_CHARACTER = 0.05;
window.ADD_POINTS_ON_ERROR_3_4 = 0.0375;   // 3/4 of 0.05
window.ADD_POINTS_ON_ERROR_1_2 = 0.025;    // 1/2 of 0.05
window.ADD_POINTS_ON_ERROR_1_4 = 0.0125;   // 1/4 of 0.05

// Hard cap on cards (and, separately, phrases) revised per play session. Larger
// decks are shuffled and only the first entries are drawn, so each session is a
// random subset of at most this many cards and this many phrases.
window.MAX_SESSION_REVISION_ITEMS = 8;

window.WRITER_SLEEP_AFTER_COMPLETE   = 1200;  // ms to admire a finished character
window.WRITER_FLY_TO_COUNTER_DURATION = 650;  // ms of the "fly into the counter" animation

window.WRITER_SHOW_HINT_ON_ERRORS       = 3;  // misses before a hint at normal levels
window.WRITER_SHOW_HINT_ON_ERRORS_LVL_3 = 1;  // misses before a hint at the advanced level

scripts/components/writer.js — writer sizing

window.CARD_WRITER_SIZE        = 100;  // single-character widget
window.PHRASE_CARD_WRITER_SIZE = 50;   // per-character widget inside a phrase card
window.CARD_WRITER_STROKE_ANIMATION_SPEED = 1.25;
window.CARD_WRITER_DELAY_BETWEEN_STROKES  = 50;
window.WRITER_PADDING = 5;

The writer colours (WRITER_RADICAL_COLOUR, WRITER_STROKE_COLOUR, WRITER_OUTLINE_COLOUR) are not constants here — they are owned by scripts/data/theme.js and set from the active theme's accent/stroke/outline colours.

scripts/pages/deck-new.js — card-editor preview defaults

window.CARD_DEFAULT_CHARACTER    = "是";
window.CARD_DEFAULT_PREVIEW_NAME = "Preview Name";

scripts/data/character-database.js — stroke-database download

window.CHARACTER_MANIFEST_URL   = "/character-map-chunks.json";
window.CHARACTER_CHUNK_URL_BASE = "/character-map-chunks/character-map-full-";
window.CHARACTER_CHUNK_BATCH_SIZE = 5;     // chunks fetched per batch
window.CHARACTER_CHUNK_COOLDOWN_MS = 300;  // pause between batches
window.CHARACTER_FETCH_RETRIES      = 4;   // retries per manifest/chunk fetch
window.CHARACTER_FETCH_RETRY_DELAY_MS = 600;

The two URLs are derived from the char_data_url build variable — to point at a different stroke dataset, change char_data_url in uvproj.yaml rather than editing these lines.

A note on the grading constants

Messing with the grading settings may lead to logical breakages in the application. Setting MAX_KNOWLEDGE_LEVEL above 4 will simply raise the upper points limit without changing functionality; lowering it disables the advanced levels.