Reference
Configuration
[Nordlys] section of skin.conf. No template or JavaScript edits are ever needed.Overview
Any option can also be overridden per station from weewx.conf, in the report’s own section, without touching the skin at all. weewx merges these over the skin’s skin.conf, so weewx.conf wins.
weewx.conf
[StdReport]
[[NordlysReport]]
skin = Nordlys
[[[Nordlys]]]
site_name = My station
[[[[theme]]]]
mode = darkTop level
skin.conf
[Nordlys]
site_name = Aldersundet weather # display name; default: station location
user_css = user.css # optional extra stylesheet
auto_refresh = false # opt out of timed auto-refresh (default true)Pages, rows, tiles
A page is a list of rows. A row is a list of tiles. A tile is a type plus a handful of options. The section name of a tile is its default observation, and the order of the page sections is the order of the navigation.
skin.conf
[[pages]]
[[[today]]] # page id; order = nav order
title = Today
[[[[now]]]] # a row
title = Now # optional row heading
columns = 4 # max columns on wide screens (1-6)
[[[[[outTemp]]]]] # a tile; section name = default obs
type = gauge
[[[week]]]
title = Week
picker = week # period dropdown: week | month | yearRows collapse to two columns under 900 px and one column under 560 px.
Every page is emitted as a real, canonical HTML file - index.html for the first page and <id>.html for the rest - so pages are shareable, bookmarkable, and work with the browser’s back and forward buttons. Navigation links are real <a href> elements that work with JavaScript disabled, using each page’s server-rendered fallback; with JavaScript, clicks navigate in place through the History API from the shared payload.
A page-level picker adds a dropdown on the first row to jump to any past week, month or year. Weeks are real pages generated by Nordlys’s own week generator; months and years come from weewx’s SummaryBy generators.
Tile types
| type | shows | needs |
|---|---|---|
gauge | SVG arc gauge, or a wind compass | obs |
stat | Value with today's min, max and trend | obs |
chart | uPlot chart, wind rose, or calendar heatmap | obs |
table | Stats table or archive records table | obs |
climatology | Per-month and per-year climatological days matrix | - |
celestial | Sun and moon combo, or one almanac section | - |
forecast | Zambretti pressure forecast | - |
reports | Links to archive pages and NOAA reports | - |
history | Cross-year records for this day or month | obs |
text | A static text tile | - |
Common tile keys are type, obs (one name, or a comma list for charts and tables) and title, which defaults to the observation’s label.
gauge
min = -20 # gauge scale bounds (report units)
max = 35
style = compass # wind-direction compass variant
color = accent-2 # token or CSS color for the band and now-marker
cold_below = 0 # switch to --nl-cold below this value
hot_above = 25 # switch to --nl-hot above this valuestat
color, cold_below and hot_above work as above, colouring the value. Add a span (day yesterday week month year alltime archive) and the tile becomes a period stat: the hero value is the period average, or the total for rain, the detail row shows the period minimum and maximum, and there is no trend or live update. Without a span it shows current conditions.
chart
chart = line # line | area | bar | scatter | windrose | calendar
span = day # 24h | day | yesterday | week | month | year
obs = outTemp, dewpoint # one or many series
overlay = rainRate # optional 2nd-axis line (e.g. over rain bars)
colors = accent, accent-3 # optional per-series colors- Spans.
dayis calendar today, midnight to now, andyesterdaythe previous calendar day.24his a trailing 24-hour window.week,monthandyearare rolling windows. - Aggregation. Series are aggregated per span: raw for the day spans, hourly average for a week, three-hourly for a month, daily for a year. Rain is summed instead - hourly buckets on the day spans, daily beyond - so pair it with
chart = bar. - Fills. A single-series
linechart fills its area; charts with several series stay unfilled so they do not obscure each other.areaforces a fill. - Overlay.
overlaydraws one more observation as a line on a second, right-hand axis - a rain rate line over rain bars, say. It is bucket-averaged onto the base series’ timestamps, so it lines up at every span. - Wind rose extras.
bands = 2, 4, 6, 9, 12sets the speed band upper bounds in report units, andcalm_below = 0.5the calm threshold. - Calendar extras.
aggregate = avg(orminmaxsumof each day), coloured between--nl-coldand--nl-hotover a rolling year.
table
table = stats # stats | records
span = month # stats: day | yesterday | week | month | year | alltime
# records: 24h | day | yesterday | week | month | year
obs = outTemp, outHumidity, barometer, windSpeed, rainstats shows minimum, average and maximum with times - for rain, the total and the wettest day. records is a sortable, paginated table of raw archive records, built from the same series data the charts use.
Stats table extras
style = cards # render extremes as record cards (label, value, date)
spans = year, alltime # compare several spans in one card per metricstyle = cards shows each observation’s highest and lowest as record cards instead of a min/avg/max table. Use spans instead of a single span to compare periods: each metric becomes one card with a labelled row per span, such as “This year” over “All-time”.
celestial
section = sunpath # sun | sunpath | moon | seasons | planetsA bare celestial tile with no section is the compact sun-and-moon card. With a section it renders one detailed panel; the Celestial page lays out all five. Everything beyond sunrise, sunset and moon phase - sun path, twilight, moon and planet rise and set, seasons - needs the optional ephem package. Without it those panels show the basic data plus a hint.
reports
stats = outTemp, rain adds at-a-glance period stats - average, or total per observation - to each month and year in the period browser.
history
span = day # day | month (historical window to compare)
obs = outTemp, windGust, rainCross-year “on this day in history” records: for each observation, the record high, low and mean over every matching day in the archive, tagged with the year, and the time for span = day. span = month compares the whole current month across years instead. Summed observations such as rain report the wettest day and no low, and wind, rain rate, UV and radiation carry no low either.
Archive pages
weewx generates one page per calendar month and year, plus NOAA text reports. [[archive]] defines the layout used by all of them - the same rows and tiles as a normal page, with span = archive meaning “this page’s own period”.
skin.conf
[[archive]]
[[[charts]]]
columns = 2
[[[[temperature]]]]
type = chart
chart = line
obs = outTemp, dewpoint
span = archiveArchive series are aggregated three-hourly on month pages and daily on year pages, with rain per day on both. Current-conditions tiles - gauge, stat, celestial and forecast - are not available on archive pages.
Theme
Nordlys is themed entirely through CSS design tokens, the --nl-* custom properties. The dark polar night palette is the hero theme and the light arctic daylight palette is derived from it. There is no shadow DOM and no compiled-in styling: anything you can see, you can re-theme from config.
skin.conf
[[theme]]
mode = auto # auto (default) | dark | light
switcher = true # header theme switch (default true)
[[[dark]]] # --nl-* token overrides for dark mode
accent = "#e86bc1"
[[[light]]] # ... and for light mode
accent = "#7c3aed"mode is the station default. The header carries a three-state switch - auto, light, dark - whose choice persists per browser in localStorage and overrides mode for that visitor, so a forced mode = dark still lets someone pick light. A tiny script in the <head> applies the saved choice before first paint, so there is no flash. Precedence is visitor choice, then mode, then system. Set switcher = false to hide the switch and pin the mode.
Token reference
Defaults below, dark first and light second.
Surfaces and text
| token | dark | light | role |
|---|---|---|---|
bg | #0b1220 | #f6f9fc | page background |
surface | #111a2c | #ffffff | tiles and cards |
border | #1e2a40 | #d7e0eb | 1px hairlines |
text | #e8eef7 | #17263b | primary text |
text-dim | #8fa3bf | #5b6c82 | labels, units, secondary text |
Aurora accents
| token | dark | light | role |
|---|---|---|---|
accent | #3ddc97 | #0e9f6e | primary accent: gauge arcs, links, active nav |
accent-2 | #4cc9f0 | #0e7490 | ice teal |
accent-3 | #a78bfa | #7c3aed | aurora violet |
accent-4 | #e86bc1 | #c2338f | magenta, use sparingly |
warm | #f0b860 | #b07817 | muted amber |
Data semantics
| token | dark | light | role |
|---|---|---|---|
cold | #4cc9f0 | #0e7490 | cold end of thresholds and ramps |
hot | #f08a5c | #c2410c | hot end of thresholds and ramps |
ok | #3ddc97 | #0e9f6e | status: live indicator |
alert | #f27e7e | #c0362c | status: threshold breach, errors |
series-1 … series-6 | aurora palette | darkened | chart series order |
Space, shape and type
| token | default | role |
|---|---|---|
space-0 … space-8 | 4 - 64 px | spacing scale, an 8px grid |
radius | 8px | tile corner radius |
max-width | 1200px | content width |
font | system UI stack | the one typeface |
fs-sm / fs-base / fs-lg / fs-xl | 13 / 15 / 20 / 32 px | type scale |
The full authoritative list is src/theme/tokens.css in the repository.
Guidance
- Keep surfaces monochrome. The design language reserves colour for data, state and interaction. If everything is coloured, nothing is.
- Mind contrast. Body text tokens are chosen for at least 4.5:1 against their backgrounds, and accents for at least 3:1 as non-text elements. If you override
accentin light mode, darken it: the dark-mode value will usually fail contrast on white. - Where tiles colour values, prefer the semantic and accent tokens over raw hex so your palette stays consistent and re-themeable.
Beyond tokens: user.css
For changes tokens cannot express - layout tweaks, hiding elements - ship an extra stylesheet. Put user.css next to the generated site, either by copying it with a [CopyGenerator] copy_once entry or by dropping it in HTML_ROOT. It loads after nordlys.css, so its rules win.
skin.conf
[Nordlys]
user_css = user.cssClimatological days
First-class day-counting definitions, rendered by climatology tiles. A day is counted when its daily aggregate of obs compares true against value.
skin.conf
[[climatological_days]]
span = year # counting window (calendar year)
[[[frost_days]]]
label = Frost days
obs = outTemp
aggregate = min # min | max | avg | sum (of the day)
op = < # < | <= | > | >=
value = 0 # threshold, in report unitsThe shipped defaults follow Norwegian MET conventions: frost (frostdøgn), ice (isdøgn), summer days (sommerdager ≥ 20), tropical days (tropedager ≥ 30), tropical nights (tropenetter), precipitation (nedbørdøgn), growing (vekstdøgn) and storm days. Labels ship in English.
The climatology tile renders these as a matrix: one row per definition, one column per month plus a year total. Months outside the station’s archive coverage render blank rather than a misleading zero.
weewx.conf
[[[Nordlys]]]
[[[[climatological_days]]]]
[[[[[summer_days]]]]] # override a threshold
value = 25 # German/DWD summer day
[[[[[ice_days]]]]] # remove a shipped default
enable = false
[[[[[hot_days]]]]] # add a new one
label = Hot days
obs = outTemp
aggregate = max
op = >=
value = 28With weewx daily summaries, tropical_nights counts the 24-hour minimum at or above 20 °C (tropedøgn). The strict tropenatt, a minimum over 20:00-08:00, is not expressible from daily summaries.
Live updates
skin.conf
[[live]]
broker = wss://example.com:9001 # MQTT over WebSocket
topic = weather/loop # default: weather/loopThe topic must carry JSON loop packets keyed by weewx observation names. Unit suffixes such as outTemp_C are stripped, and dayRain feeds the rain tile. Values must be in the report’s unit system. When configured, the page header shows a live indicator and tiles update in place.
Date and time formats
strftime patterns shared by the search-list extension and the front-end, so extreme times, chart axes, the footer and the records table all format the same way. Defaults are 24-hour time and European dates with abbreviated months.
skin.conf
[[formats]]
time = %H:%M # day-span extremes, records, live
date = %d %b # month/year extremes, chart date ticks
date_year = %d %b %Y # all-time extremes, footer date
datetime = %d %b %Y, %H:%M # footer "Generated"
weekday_time = %a %H:%M # week-span extremes, records rowsThe front-end understands a strftime subset - %H %M %S %d %m %b %a %Y %I %p - with month and day names in English. These keys are separate from weewx’s [Units][[TimeFormats]], which formats Cheetah tags such as NOAA reports rather than the payload.
Search and social
Every page ships a meta description and OpenGraph and Twitter cards automatically. og:url, og:image and sitemap.xml need an absolute site root, resolved as base_url, then weewx’s [Station] station_url, then none. With no base URL those absolute-only parts are skipped.
skin.conf
[[seo]]
base_url = https://weather.example.com/nordlys
description = Custom description for the whole site
image = og-image.png # static fallback card, relative to the site root
dynamic_card = true # live per-station cards when Pillow is present
robots = true # emit robots.txt + sitemap.xml (default)- Descriptions are page-aware by default; override them with
description. robots = falsewrites aDisallow: /robots.txt and an empty sitemap.- A JSON-LD
WebSiteandPlaceblock ships on every page.
Dynamic social cards
With Pillow installed - an optional dependency, like ephem - Nordlys renders the card live per station on every report cycle instead of shipping one generic image. Live pages show current conditions; archive pages get a per-period card summarising that period’s temperature range and rain total.
The card uses the polar night palette and a bundled Inter font, so non-ASCII station names render correctly. The current period is redrawn each cycle and completed historical periods are drawn once and cached. Without Pillow, or with dynamic_card = false, pages fall back to the static image.
Labels and units
skin.conf
[Labels]
[[Generic]]
outTemp = Outside temperatureNordlys uses the report’s unit system; weewx converts everything before serialization. Set it per report in weewx.conf with unit_system = metricwx.
Regeneration
Nordlys generates the whole site every report cycle - there are no write-once pages - so config and theme changes take effect on the next cycle, or immediately with:
weectl report run --config /path/to/weewx.confAuto-refresh
Open pages refresh themselves shortly after each report cycle, timed to weewx’s [StdArchive] archive_interval, so a tab left open stays current. The page re-fetches its own freshly generated HTML and swaps the new numbers, charts and records in place; scroll position, the active page, the theme and any live MQTT connection are all preserved. Refresh pauses while the tab is hidden and catches up when it becomes visible again.
Building on Nordlys rather than configuring it? The data contract documents the JSON payload the skin emits.