From a569f84d8abe6eeb7bc1760249b4f10cb16dc1f4 Mon Sep 17 00:00:00 2001 From: Tam Nguyen Duc <1218621+tamnd@users.noreply.github.com> Date: Mon, 15 Jun 2026 15:40:07 +0700 Subject: [PATCH] Match each token in a multi-value link rel (#17) A attribute is a space-separated token list, but kage matched the whole string against its set of downloadable rels. So a tag like matched neither "preload" nor "stylesheet" and its stylesheet was left on the live web, absolute and undownloaded. VitePress ships exactly this form, and it is the only stylesheet on the page, so a cloned Vue docs site (and any other VitePress site) rendered completely unstyled offline. Tokenize the rel and match if any single token is a known asset rel, the way the HTML spec reads it. This also subsumes the old combined "shortcut icon" entry, since the "icon" token alone now matches. --- asset/asset_test.go | 2 ++ asset/html.go | 24 +++++++++++++++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/asset/asset_test.go b/asset/asset_test.go index 672cc64..d347111 100644 --- a/asset/asset_test.go +++ b/asset/asset_test.go @@ -69,6 +69,7 @@ body { background: url(../img/bg.png); } const htmlDoc = ` + @@ -98,6 +99,7 @@ func TestRewriteHTML(t *testing.T) { checks := map[string]bool{ `href="_kage/ex.com/css/main.css"`: true, // stylesheet localised + `href="_kage/ex.com/css/vp.css"`: true, // multi-value "preload stylesheet" rel localised `href="_kage/ex.com/favicon.ico"`: true, // icon localised `href="https://ex.com/canon"`: true, // canonical left alone `href="docs/intro/index.html"`: true, // internal page → local diff --git a/asset/html.go b/asset/html.go index d939327..ddbe040 100644 --- a/asset/html.go +++ b/asset/html.go @@ -9,13 +9,28 @@ import ( "golang.org/x/net/html/atom" ) -// stylesheetRels are values whose href kage downloads as an asset. -var stylesheetRels = map[string]bool{ - "stylesheet": true, "icon": true, "shortcut icon": true, +// assetRels are the individual tokens whose href kage downloads as +// an asset. A rel attribute is a space-separated token list, so a single known +// token in it (for example the "stylesheet" in "preload stylesheet") is enough. +var assetRels = map[string]bool{ + "stylesheet": true, "icon": true, "apple-touch-icon": true, "apple-touch-icon-precomposed": true, "mask-icon": true, "manifest": true, "preload": true, "prefetch": true, } +// linkRelDownloadable reports whether a names a resource kage should +// download. It treats rel as the space-separated token list the HTML spec +// defines, so "preload stylesheet", "shortcut icon", or a bare "stylesheet" all +// match on a single recognised token. +func linkRelDownloadable(rel string) bool { + for _, tok := range strings.Fields(strings.ToLower(rel)) { + if assetRels[tok] { + return true + } + } + return false +} + // RewriteHTML walks the parsed document and rewrites every resource and link // reference through sink, resolving relative URLs against base. It mutates the // tree in place; the caller renders it afterwards. References kage cannot handle @@ -40,8 +55,7 @@ func rewriteElement(n *html.Node, base *url.URL, sink RefSink) { case atom.Iframe, atom.Frame: rewriteAttr(n, "src", base, sink, pageOrAsset) case atom.Link: - rel := strings.ToLower(strings.TrimSpace(attrVal(n, "rel"))) - if stylesheetRels[rel] { + if linkRelDownloadable(attrVal(n, "rel")) { rewriteAttr(n, "href", base, sink, alwaysAsset) } case atom.Img: