콘텐츠로 이동

Configuration reference

이 콘텐츠는 아직 번역되지 않았습니다.

A single window.RANKING_CONFIG object, loaded via a <script> tag before core.js. Unknown or unset keys fall back to sensible defaults. Edit this file to change schema, filters, sort options, data sources, and redirect URLs — no core changes required.

The authoritative definition is src/types.ts, published as dist/types.d.ts. With the JSDoc annotation at the top of ranking-config.js, your editor validates the whole object as you type.


configVersion: 4,

The schema version this file targets. Core warns when a config declares a version newer than it understands, so a stale core.js after a CDN major bump says so in the console instead of silently half-working. An older config raises no warning: it still works. Optional, but recommended.

version added
1 the original schema
2 scale on the rank column, and the stars and score-badge renderers
3 chrome: the header and footer moved out of index.html and into the config
4 the named form of languages, which the language button’s tooltip is built from

One entry per device category. Each key becomes a toggle button and a valid ?type= URL param.

types: {
earphone: {
label: { default: 'Earphones', i18n: { ko: '이어폰' } },
source: { kind: 'csv', url: 'https://docs.google.com/.../pub?output=csv' },
phonebook: '../data/phone_book.json',
measurementUrl: '../?share={file}',
measurementsPageUrl: '../', // header "measurements" icon link
defaults: { Style: 'Open' }, // fills blank cells on this type
rowFilter: { field: 'Category', values: ['iem'] }, // optional, see below
},
}
  • source.url — published Google Sheet CSV URL (File → Share → Publish to web → CSV).
  • phonebook — CrinGraph phone_book.json. Omit it and measurement links are simply never shown.
  • measurementUrl — template; {file} is replaced with the URL-encoded phonebook filename.
  • measurementsPageUrl — where the top-right measurements icon sends the user for this type. Omit it and the icon is hidden.

By default each type fetches its own CSV. To serve several types from a single sheet, give them the same source.url and add a rowFilter:

earphone: { rowFilter: { field: 'Category', values: ['iem', 'earbud'] }, /* ... */ },
headphone: { rowFilter: { field: 'Category', values: ['headphone'] }, /* ... */ },

Matching is case-insensitive. Rows whose Category matches nothing appear under no type.


An ordered list. Each column declares its data source, filter, sort behavior and renderer independently. Card order follows this list.

{
id: 'rank', // unique; referenced by sort keys and filter state
source: 'Rank', // CSV header (optional for template-only columns)
role: 'rank', // semantic hint; see below
label: { default: 'Rank', i18n: { ko: '등급' } },
sortable: true,
scale: [ // rank columns only; see below
{ value: 'S', score: 5, color: '#6c63ff' },
{ value: 'A', score: 4, color: '#00bfff' },
],
filter: { kind: 'select' },
render: { kind: 'rank-badge' },
showForTypes: ['headphone'], // optional allowlist
placement: 'meta', // optional slot override
}

Roles are how core finds a column without hardcoding a header name. Rename Brand to Maker in your sheet and everything keeps working, as long as the role stays put.

role used for
rank Badge ordering, the rank chart, the scale, and the primary tie-breaker
brand Phonebook brand matching, second tie-breaker
model Phonebook model matching, third tie-breaker
score Numeric sorting and the average readout; falls back to the rank scale

A rank used to be spread across four lists that had to stay index-aligned by hand: the filter values, the badge classMap, stats.chartColors, and a Score formula in the sheet. scale replaces all four. It is an ordered list, best first, and it is the only place a rank is defined.

{
id: 'rank', source: 'Rank', role: 'rank', label: 'Rank',
sortable: true,
scale: [
{ value: 'S', score: 5, color: '#6c63ff' },
{ value: 'A', score: 4, color: '#00bfff' },
{ value: 'B', score: 3, color: '#8bc34a' },
],
filter: { kind: 'select' },
render: { kind: 'rank-badge' },
}

From that one list core derives the dropdown options, the sort order, the badge color, the chart bar colors, and the score each grade is worth. Add a grade by adding one line.

key effect
value the cell value as written in the sheet
score what the grade is worth; feeds the score sort and the average readout
color badge background, and the chart bar for this step
textColor badge text; defaults to white or near-black, whichever is readable
label display text for the badge and the dropdown; defaults to value
class an extra CSS class on the badge, for styling beyond a flat color

Three consequences worth knowing:

  • The Score column becomes optional. When a row has no numeric Score cell, its score is the one its grade carries. A sheet with only Brand, Model and Rank still sorts by score and still shows an average.
  • A numeric scale tolerates values between steps. On an all-numeric scale, 8.6 counts in the 9 bar and shows as 8.6. Letter scales never snap: an unknown grade stays unranked.
  • filter.values and stats.chartColors become optional. Set either one to override what the scale supplies.
render: { kind: 'stars', max: 5 } // 4.5 draws 4 and a half
render: { kind: 'score-badge', min: 0, max: 10, decimals: 1 } // colored 0-to-10 pill

stars clips a row of star icons to a percentage, so any fraction works without half-star artwork. score-badge interpolates its color across render.colors between min and max, so a 0-to-100 scale needs no per-value color list.

Three ready-made configurations live in presets/: letter (S through F), stars (five stars in half steps), and score (0 to 10). Each ships its own TEMPLATE.csv matching that scale. Copy the pair you want over ranking-config.js and TEMPLATE.csv.

configVersion: 1 configs, which spell out filter.values, render.classMap and stats.chartColors, keep working unchanged. classMap still takes precedence over a scale color when both are set, so an operator styling badges from their own stylesheet loses nothing. A version-1 core cannot read a version-2 config, and says so in the console rather than failing quietly.

kind default slot purpose
rank-badge rank colored badge; color comes from scale
stars rank a row of stars out of render.max, halves included
score-badge rank a number in a pill colored along render.colors
title title heading; uses render.template with {Header} placeholders
meta-chip meta chip in the meta row; consecutive chips get separators
text / numeric meta plain span; numeric also sorts numerically
link actions anchor; uses render.hrefTemplate or render.href
block body a paragraph in the card body; see below
tags body splits the cell on separator and renders pills
measurement-link actions anchor resolved via the type’s phonebook
none — not rendered; still filterable, sortable and searchable

A block renders one cell as a paragraph, preserving the cell’s line breaks.

{ id: 'pros', source: 'Pros', label: 'Pros', render: { kind: 'block', style: 'up' } }
style appearance
plain body text, no icon (the main comment)
up green block with a plus icon
down red block with a minus icon
note muted text with a question icon
muted muted text with an asterisk icon

A blank cell renders nothing at all, so every block is optional per row and order is set by the config, not by the operator’s typing.

Add blockLabel to prefix the block with a bold heading (blockLabel: { default: 'Pros' }). Omit it and only the icon distinguishes the block, which is usually enough.

rank (left block) · title (top of header) · meta (chip row) · actions (button row) · body (right content column). Override per column via placement.

  • text — substring match on filter.match (array of CSV headers) or the column’s own source.
  • select — exact-match dropdown built from filter.values, or from the column’s scale when values is omitted.
  • select-auto — exact-match dropdown whose options are collected from the loaded rows, sorted alphabetically. Good for Driver and Style columns where the value set changes as the sheet grows.
i18nSource: { en: 'Comment', ko: 'Comment_KR' },

The page reads the header for the active language, and falls back to source when that cell is blank, so a half-translated sheet degrades per row rather than showing gaps. The header names are yours: Comment_KR is what the template happens to use, not something core looks for. See languages and i18n for the rest of what a second language needs.


search: {
enabled: true,
// Omit `fields` to search every header any column declares, in both languages.
fields: ['Brand', 'Model', 'Comment', 'Pros', 'Cons', 'Tags'],
label: { default: 'Search', i18n: { ko: '검색' } },
},
sort: {
default: 'rank-asc',
options: ['rank-asc', 'rank-desc', 'score-desc', 'brand-asc'],
labels: { 'rank-asc': { default: 'Rank (Best First)', i18n: { ko: '등급순 (높은 순)' } } },
},
stats: {
enabled: true,
average: { source: 'Score', denominator: '5.00' },
chartColors: ['#6c63ff', /* ... */], // optional; defaults to the scale's colors
chartLibUrl: 'https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js',
},
deepLink: {
template: '{Brand}-{Model}',
slugify: 'lowercase-hyphen', // '#apple-airpods-max-usb-c'
},

Sort keys are {columnId}-{asc|desc}. A key with no matching label gets one generated from the column label. Rows with a blank or unrecognized value in the sorted column always sink to the bottom, in both directions.

Chart.js is fetched the first time the statistics modal is opened, so it never delays the first render. Set stats.enabled: false to hide the button entirely.


The page shell. index.html is three empty landmarks — #ranking-header, #ranking-content and #ranking-footer — and core builds everything inside them, so branding the page never means editing markup.

chrome: {
title: { default: 'SquigRanking', i18n: { ko: '랭킹' } },
subtitle: 'IEM and headphone rankings',
titleUrl: '../',
links: [
{ href: 'https://example.com/blog', label: 'Blog', icon: 'external', newTab: true },
],
footer: {
note: { default: 'Rankings reflect my own listening.', i18n: { ko: '...' } },
links: [{ href: 'https://example.com', label: 'My site', newTab: true }],
},
},
key what it does
title Header title. An I18nString, or false for no title. Omitted renders nothing.
subtitle A second line under the title.
titleUrl Wraps the title in a link, e.g. back to your measurement page.
themeToggle Show the light/dark button. Default true.
languageToggle Show the language button. Defaults to on when more than one language is offered.
measurementsLink Show the header measurement icon. Default true; it appears only for types declaring measurementsPageUrl.
links Extra header links, before the built-in buttons.
footer.note The disclaimer under the list. Pass an array for several paragraphs.
footer.links Links along the footer’s bottom row.

A link takes href, an optional label (an I18nString), an optional icon (measurements, external or info), an optional title for the tooltip and accessible name, and newTab.

There are no defaults for the wording: a config with no chrome.title renders no title, and one with no footer renders neither a note nor links. The shipped presets set both, so a fresh download has them. The footer bar itself is always there, because it carries the built with squigRanking credit on its left; your footer.links sit opposite it on the right.

What stays in index.html is the <head>: <title>, the og: tags, the canonical URL and the favicon. Those are read by crawlers and link previews before any script runs, so they cannot come from a config.


languages declares what the language toggle cycles through, in order:

languages: { en: 'English', ko: 'Korean', ja: 'Japanese' },

Name them. The language button’s tooltip names the language it moves to, and core builds that sentence out of these names — “View in Japanese” on the Korean page, “View in English” on the Japanese one.

i18n overrides the interface strings themselves. They ship inside the bundle in English and Korean, so this is for a third language, or for rewording one of the two:

i18n: {
ja: { filterAndSort: 'フィルターと並べ替え', resetFilters: 'リセット' },
},

Any string you do not override falls back to the built-in value, then to English, one string at a time — a language with two keys filled in works, with the rest in English. Available keys: filterAndSort, resetFilters, search, sortBy, all, statsTitle, averageScore, deviceCount, closeStats, openStats, measurementsPage, toggleTheme, toggleLanguage, scrollTop, noResults, loadError, ascending, descending.

toggleLanguage is the one you rarely need. It is derived from the names above, in English; set it only to word the tooltip in the language itself — i18n: { ja: { toggleLanguage: '英語で表示' } }. An explicit value always wins over the derived one.

Nothing in the page enumerates languages — a tag is supported exactly as far as your config carries it. For a language beyond the built-in two:

  • languages — the tag and its name, in cycle order.
  • columns[].i18nSource — the CSV header holding that language’s text, for each translated column.
  • Every I18nString in the file — column label and blockLabel, scale[].label, type labels, sort.labels, search.label, chrome.title, footer.note, link labels — takes i18n: { ja: '...' } alongside its default.
  • i18n — the interface strings, if you want them out of English.

The config editor writes all of it: add a tag, a name and a column suffix in its language step and it fills in languages, the i18nSource entries, and a box for every piece of wording and every interface string. Korean comes pre-filled; anything else starts empty and falls back to English until you type in it.

Stored choice from a previous visit, else the first entry in languages matching navigator.languages (ja-JP matches a declared ja), else the first entry in the list. The choice is kept in localStorage under preferred-lang.

Declaring a single language hides the language toggle, since there is nothing to switch to. Set chrome.languageToggle: true to show it anyway.

Only strings core writes itself live in i18n. Your own wording — the header title, the footer note — belongs in chrome, which takes an I18nString for each and so carries its own translations.

If you add markup of your own to index.html, data-i18n="key" on an element fills it from this table; data-i18n-title and data-i18n-label do the same for the title and aria-label attributes.


Where the page loads its own build from. Read by loader.js before the bundle exists, so unlike every other key here it never reaches core. Every field is optional, and most deploys set none of them.

cdn: {
majorVersion: 1,
},
key default meaning
source 'auto' 'auto' follows loader.js: a copy in your own folder means the build is there too, the CDN copy means fetch the published one. 'local' and 'cdn' force it.
majorVersion highest published Major version to track. Its newest patch is loaded on every visit.
version — An exact build, e.g. '1.4.2'. Freezes the deploy and skips the version lookup.
base the project’s jsDelivr URL CDN base. Point it at your own mirror of the cdn branch.
versionsUrl derived from base Full URL to versions.json.
debug false Load the readable core.js instead of core.min.js.

Setting majorVersion is the usual choice: bug fixes arrive on their own, and a major bump never does. See Deploying.

Add a Price header to your sheet, then:

{
id: 'price',
source: 'Price',
label: { default: 'Price', i18n: { ko: '가격' } },
sortable: true,
filter: { kind: 'text' },
render: { kind: 'numeric' },
},

Add 'price-asc' to sort.options if you want it in the dropdown. No code changes.

Edit types.*.measurementUrl. Operators without CrinGraph can point at arbitrary URLs, or drop phonebook to hide measurement links.

Add showForTypes: ['headphone'].

Inline on the column: label: { default: 'Rank', i18n: { ko: '등급', ja: 'ランク' } }.

Name the tag in languages, give each translated column an i18nSource entry, and add the tag to every I18nString. languages and i18n walks through it.


  • URL: .../ranking/?type=earphone#apple-airpods-max-usb-c
  • ?type= selects the tab; #<slug> selects the tab that contains the card, scrolls to it, and highlights it.
  • Slug format is controlled by deepLink.template + deepLink.slugify. CrinGraph’s listAugment.js and modernGraphTool’s PhoneSelector.svelte build links in this format — see the CrinGraph guide and the modernGraphTool guide.

A modernGraphTool deploy can point its RANKING.CONFIG_URL at this file to show grades from your sheet. It reads only:

  • types[<type>].source.url and types[<type>].rowFilter
  • the role: 'rank' column’s source and scale
  • the role: 'brand' and role: 'model' columns’ source
  • deepLink

Everything else here is free to change without affecting it. Dropping role: 'rank' from the rank column, or renaming any key above, silently stops those grades appearing. See the modernGraphTool guide.