Files
zeekayandHanzo Dev 9b17185436 brand: one binary, five brands, and no brand wearing another's mark
Hanzo and Lux status pages run the same image digest. Three of the tab-icon
links were brand-templated, but the paths a browser reaches for on its own
were not: /favicon.ico, /apple-touch-icon.png, /logo-192x192.png and
/logo-512x512.png were real files at the web root, and git records this
commit moving them into brands/hanzo/ because that is what they always were.
Every brand got Hanzo's bytes. Saving status.lux.network to an iOS home
screen saved the Hanzo mark, md5-identical.

Artwork now has one home, /brands/<brand>/, and one name to reach it by:
ui.brand. Tab icon, home screen icon, PWA icons and the header logo all
derive from that name, so a deployment cannot title itself one company and
hand out another's mark. The bare root paths are served from the same place
rather than being a second copy that drifts, and a brand missing an asset is
a boot failure instead of a page quietly wearing nobody.

manifest.json was a checked-in static file: it called itself "Status" on a
page titled "Hanzo Status", described itself in upstream Gatus's words, and
splashed #f7f9fb light grey in front of a UI that paints #020817. It is now
rendered from the same config that renders the page, and the theme-color meta
reads the colour the app actually paints instead of a rounder-looking #000000.

The 180/192/512 sizes were rasterised from each brand's own canonical SVG with
rsvg-convert, the same pipeline that produced the existing 16/32 — it
reproduces every shipped icon byte for byte, and Hanzo's icons are unchanged
from what production serves today.

Config drops `logo` and the three-field `favicon` block for the single
`brand` key. TestNoBrandWearsAnothersMark asserts the property that was
silently false: distinct bytes per brand at every size.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-05 16:07:37 -07:00

70 lines
4.3 KiB
JavaScript

/**
* Post-process the Vite build index.html into a Go html/template.
*
* The Go backend (api/spa.go) parses web/static/index.html as a Go template
* and executes it with ViewData{UI: *Config, Theme: string}. This script
* extracts all <script> and <link> tags from the Vite output and inserts
* them into a Go-template-compatible HTML shell that includes window.config
* injection and favicon/theme configuration.
*/
import { readFileSync, writeFileSync } from 'fs'
import { resolve, dirname } from 'path'
import { fileURLToPath } from 'url'
const __dirname = dirname(fileURLToPath(import.meta.url))
const outHtml = readFileSync(resolve(__dirname, '../dist/index.html'), 'utf-8')
const staticDir = resolve(__dirname, '../../static')
// Extract <link> and <script> tags from <head>
const headLinkMatches = [...outHtml.matchAll(/<link\s[^>]*\/?>/g)]
const headScriptMatches = [...outHtml.matchAll(/<script\s[^>]*>[\s\S]*?<\/script>/g)]
// Filter out Vite's default meta/title/favicon -- we replace with Go template versions
const headLinks = headLinkMatches
.map(m => m[0])
.filter(tag => !tag.includes('rel="icon"')) // We inject favicons via Go template
.join('\n')
const headScripts = headScriptMatches.map(m => m[0]).join('\n')
// Extract body scripts (Vite puts the module entry in body)
const bodyMatch = outHtml.match(/<body[^>]*>([\s\S]*)<\/body>/)
const bodyContent = bodyMatch ? bodyMatch[1] : ''
// Collect src from head scripts to deduplicate
const headSrcs = new Set(
[...headScripts.matchAll(/src="([^"]+)"/g)].map(m => m[1])
)
const bodyScripts = [...bodyContent.matchAll(/<script[^>]*>[\s\S]*?<\/script>/g)]
.map(m => m[0])
.filter(tag => {
const srcMatch = tag.match(/src="([^"]+)"/)
if (!srcMatch) return true
return !headSrcs.has(srcMatch[1])
})
.join('\n')
// Build the Go-template-compatible index.html
const template = `<!doctype html><html lang="en" class="{{ .Theme }}"><head><meta charset="utf-8"/><script>window.config = {title: "{{ .UI.Title }}", logo: "{{ .UI.Icons.Logo }}", header: "{{ .UI.Header }}", dashboardHeading: "{{ .UI.DashboardHeading }}", dashboardSubheading: "{{ .UI.DashboardSubheading }}", link: "{{ .UI.Link }}", buttons: [], maximumNumberOfResults: "{{ .UI.MaximumNumberOfResults }}", defaultSortBy: "{{ .UI.DefaultSortBy }}", defaultFilterBy: "{{ .UI.DefaultFilterBy }}"};{{- range .UI.Buttons}}window.config.buttons.push({name:"{{ .Name }}",link:"{{ .Link }}"});{{end}}
// Initialize theme immediately to prevent flash
(function() {
const themeFromCookie = document.cookie.match(/theme=(dark|light);?/)?.[1];
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (themeFromCookie === 'dark' || (!themeFromCookie && prefersDark)) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
})();</script><title>{{ .UI.Title }}</title><meta http-equiv="X-UA-Compatible" content="IE=edge"/><meta name="viewport" content="width=device-width,initial-scale=1"/>{{ if .UI.Brand }}{{ with .UI.Icons }}<link rel="apple-touch-icon" sizes="180x180" href="{{ .Touch180 }}"/><link rel="icon" type="image/svg+xml" href="{{ .SVG }}"/><link rel="icon" type="image/png" sizes="32x32" href="{{ .PNG32 }}"/><link rel="icon" type="image/png" sizes="16x16" href="{{ .PNG16 }}"/><link rel="shortcut icon" href="{{ .ICO }}"/>{{ end }}{{ end }}<link rel="manifest" href="/manifest.json" crossorigin="use-credentials"/><link rel="stylesheet" href="/css/custom.css"/><meta name="description" content="{{ .UI.Description }}"/><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/><meta name="apple-mobile-web-app-title" content="{{ .UI.Title }}"/><meta name="application-name" content="{{ .UI.Title }}"/><meta name="theme-color" content="{{ .UI.Background.Dark }}" media="(prefers-color-scheme: dark)"/><meta name="theme-color" content="{{ .UI.Background.Light }}" media="(prefers-color-scheme: light)"/>
${headLinks}
${headScripts}
</head><body><noscript><strong>Enable JavaScript to view this page.</strong></noscript>
<div id="root"></div>
${bodyScripts}
</body></html>`
writeFileSync(resolve(staticDir, 'index.html'), template)
console.log('[make-template] Wrote Go-template-compatible index.html')