feat: render venue floor plans as inline SVG - #312
Conversation
The venue floor plans are exported from Affinity Designer and need three things done before they can be inlined into the page: - SVGO optimization (roughly halves them) - id namespacing, since every export names its first clip path `_clip1` and inlining several plans into one document would make them collide - a Noto Sans TC subset covering only the glyphs the plans use, because the exports hard-code a per-glyph x offset computed from that font Run with `pnpm prepare:venue-svg`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The plans were loaded through `<img src="*.svg">`, which puts the SVG in its own document with no access to the page's fonts. Since the exports hard-code a per-glyph x offset computed from Noto Sans TC, the fallback font's glyph widths disagreed with those offsets and the labels came out unevenly spaced. Inlining the markup lets the subsetted fonts.css apply, so the labels keep their intended spacing. The overview tab is imported statically and ships with the page; the rest load their own chunk when their tab is opened. RB-AU stays a webp: its SVG export contains no vector data at all, just a single embedded PNG weighing 1.1 MB against 235 KB for the raster. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughChangesVenue asset pipeline
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant VenuePage
participant PlanConfiguration
participant VenueAssets
VenuePage->>PlanConfiguration: activate venue category
PlanConfiguration->>VenueAssets: select SVG or RB/AU raster asset
VenueAssets-->>VenuePage: provide plan markup or image
VenuePage->>VenuePage: render inline SVG, raster image, or loading placeholder
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1⚔️ Resolve merge conflicts 💡
📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
app/pages/venue.vue (1)
129-150: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse UnoCSS utilities for inline SVG sizing.
Replace the scoped CSS rule with UnoCSS utilities on the container. UnoCSS supports arbitrary selector variants such as
[&>*]:m-1; verify that the project enables the required extractor before using the direct-child variant. (unocss.dev)Proposed refactor
- class="venue-plan w-full" + class="w-full [&>svg]:block [&>svg]:h-full [&>svg]:w-full" @@ -<style scoped> -.venue-plan :deep(svg) { - display: block; - width: 100%; - height: 100%; -} -</style>As per coding guidelines,
**/*.{vue,css,scss}: Use UnoCSS with the Tailwind Wind4 preset and the project theme colorsprimary-50throughprimary-800andcp-green.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/pages/venue.vue` around lines 129 - 150, Replace the scoped .venue-plan :deep(svg) sizing rule with equivalent UnoCSS utilities on the venue-plan container, ensuring the rendered inline SVG remains block-level and fills the container width and height. Use an arbitrary selector variant only after confirming the project’s UnoCSS extractor supports it, and remove the now-unneeded scoped CSS.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@scripts/prepare-venue-svg.mjs`:
- Line 180: Update the font-family declaration generation in
prepare-venue-svg.mjs to emit lint-compliant CSS identifiers without the
problematic quotes, then regenerate app/assets/venue/fonts.css so all generated
font-family declarations use the corrected format.
- Around line 68-83: Update the SVGO plugin configuration in prepare-venue-svg
to explicitly include removeScripts before writing generated SVG assets. Ensure
script elements, event-handler attributes, and script URLs are stripped while
preserving the existing cleanupIds, prefixIds, and removeDimensions behavior.
---
Nitpick comments:
In `@app/pages/venue.vue`:
- Around line 129-150: Replace the scoped .venue-plan :deep(svg) sizing rule
with equivalent UnoCSS utilities on the venue-plan container, ensuring the
rendered inline SVG remains block-level and fills the container width and
height. Use an arbitrary selector variant only after confirming the project’s
UnoCSS extractor supports it, and remove the now-unneeded scoped CSS.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 92ae7b9e-0bf0-4097-adf9-0a79d19c5331
⛔ Files ignored due to path filters (17)
app/assets/venue/TR2F.svgis excluded by!**/*.svgapp/assets/venue/TR309.svgis excluded by!**/*.svgapp/assets/venue/TR312.svgis excluded by!**/*.svgapp/assets/venue/TR3F.svgis excluded by!**/*.svgapp/assets/venue/TR409-1.svgis excluded by!**/*.svgapp/assets/venue/TR4F.svgis excluded by!**/*.svgapp/assets/venue/TR5F.svgis excluded by!**/*.svgapp/assets/venue/overview.svgis excluded by!**/*.svgapp/assets/venue/raw/TR2F.svgis excluded by!**/*.svgapp/assets/venue/raw/TR309.svgis excluded by!**/*.svgapp/assets/venue/raw/TR312.svgis excluded by!**/*.svgapp/assets/venue/raw/TR3F.svgis excluded by!**/*.svgapp/assets/venue/raw/TR409-1.svgis excluded by!**/*.svgapp/assets/venue/raw/TR4F.svgis excluded by!**/*.svgapp/assets/venue/raw/TR5F.svgis excluded by!**/*.svgapp/assets/venue/raw/overview.svgis excluded by!**/*.svgpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (12)
AGENTS.mdapp/assets/venue/TR2F.webpapp/assets/venue/TR3F.webpapp/assets/venue/TR409-1.webpapp/assets/venue/TR4F.webpapp/assets/venue/TR5F.webpapp/assets/venue/fonts.cssapp/assets/venue/overview.webpapp/pages/venue.vueeslint.config.mjspackage.jsonscripts/prepare-venue-svg.mjs
| plugins: [ | ||
| { | ||
| name: 'preset-default', | ||
| params: { | ||
| overrides: { | ||
| // prefixIds does the renaming; letting cleanupIds also minify them | ||
| // first only makes the diff noisier. | ||
| cleanupIds: false, | ||
| }, | ||
| }, | ||
| }, | ||
| // Namespace every id so multiple plans can coexist in one document. | ||
| { name: 'prefixIds', params: { prefix: id, delim: '__', prefixClassNames: false } }, | ||
| // Drop width/height="100%" so CSS controls the size; viewBox stays. | ||
| 'removeDimensions', | ||
| ], |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== file list =="
git ls-files | sed -n '1,120p'
echo "== target files =="
for f in scripts/prepare-venue-svg.mjs app/pages/venue.vue; do
if [ -e "$f" ]; then
echo "--- $f ($(wc -l < "$f") lines) ---"
sed -n '1,220p' "$f"
else
echo "MISSING $f"
fi
done
echo "== package svgo refs =="
(git ls-files | rg '(^|/)package(-lock)?\.json$|(^|/)pnpm-lock\.yaml$|(^|/)yarn\.lock$' || true) | while read -r f; do
echo "--- $f ---"
rg -n "svgo|`@svgo`|node-svgo|vue|v-html" "$f" || true
done
echo "== usage of venue SVGs / raw assets =="
rg -n "prepare-venue-svg|venue-svg|removeDimensions|prefixIds|v-html|src=\"[^\"]*\\.svg|svg" app scripts 2>/dev/null || true
echo "== lockfile svgo version candidates =="
rg -n "svgo" -S pnpm-lock.yaml package-lock.json yarn.lock 2>/dev/null | head -80 || trueRepository: COSCUP/2026
Length of output: 50368
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== package manifests / svgo refs concise =="
for f in package.json scripts/prepare-venue-svg.mjs app/pages/venue.vue; do
if [ -e "$f" ]; then
echo "--- $f ---"
wc -l "$f"
rg -n "svgo|optimize|preset-default|v-html|removeScripts|onload|onclick|<script|href=|src=" "$f" || true
else
echo "MISSING $f"
fi
done
echo "== venue route page concise =="
sed -n '1,220p' app/pages/venue.vue
echo "== prepare script targeted sections =="
sed -n '1,220p' scripts/prepare-venue-svg.mjs
echo "== lockfile SVGO concise =="
for f in package-lock.json pnpm-lock.yaml yarn.lock; do
if [ -e "$f" ]; then
echo "--- $f svgo ---"
rg -n "svgo|`@svgo`|core-js|node-svgo" "$f" | head -120 || true
fi
done
echo "== raw SVG executable-looking attributes concise =="
rg -n -i --glob '*.svg' '(\b(on[a-z]+|onclick|load|error|i[\s]*load|begin\b)|<\s*script\b|href\s*=|src\s*=)' app/assets/venue 2>/dev/null || true
echo "== package scripts =="
python3 - <<'PY'
import json, pathlib, re
p=pathlib.Path('package.json')
if p.exists():
try:
data=json.loads(p.read_text())
except Exception as e:
print("package.json parse error", e)
data={}
print(json.dumps(data.get("scripts", {}), indent=2, sort_keys=True))
PYRepository: COSCUP/2026
Length of output: 50368
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== exact relevant source slices =="
sed -n '1,220p' scripts/prepare-venue-svg.mjs
sed -n '1,220p' app/pages/venue.vue
echo "== package and lockfile concise svgo versions =="
sed -n '/"scripts",/,/^\}/p' package.json 2>/dev/null | sed -n '1,220p' || true
for f in package-lock.json pnpm-lock.yaml yarn.lock; do
[ -e "$f" ] || continue
echo "--- $f svgo summary ---"
rg -n -C 2 -i 'svgo|`@svgo/core`|`@svgo/vue-loader`|node-svgo|remove-scripts|removeScripts' "$f" | head -200 || true
done
echo "== executable-looking SVG attributes in raw assets concise =="
python3 - <<'PY'
import pathlib, re
root=pathlib.Path('app/assets/venue')
pat=re.compile(r'(?i)\b(?:on[a-z]+|load|error|i[\s]*load|begin\b)|<\s*script\b|(?:href|src)\s*=\s*["\'][^"\']*javascript:', re.S)
for p in sorted(root.glob('*/*.svg'))+sorted(root.glob('*')):
if not p.is_file() or p.suffix.lower()!='.svg':
continue
text=p.read_text(errors='replace')
if pat.search(text):
lines=[(i+1,l) for i,l in enumerate(text.splitlines()) if pat.search(l)]
print(f"{p} {len(lines)} matches:")
for line, txt in lines[:5]:
print(f" {line}: {txt[:200]}")
PYRepository: COSCUP/2026
Length of output: 14970
🌐 Web query:
SVGO 4.0.2 removeScripts preset-default plugins
💡 Result:
In SVGO 4.0.2, the removeScripts plugin is not included in the preset-default configuration [1][2]. It must be explicitly added to your SVGO configuration if you wish to use it [1][3]. The removeScripts plugin (formerly named removeScriptElement in versions prior to v4.0.0) is designed to remove script elements, SVG event attributes (such as onload and onclick), and script URIs from the document [1][3][4]. Because it is not part of the default preset, it is disabled by default [2]. To enable it, you can add it to your configuration file (e.g., svgo.config.mjs): export default { plugins: [ 'preset-default', 'removeScripts' ] } Note that in SVGO 4.0.2, the removeScripts plugin received security updates to improve its handling of namespaces and case-insensitive matching for script URIs to better prevent XSS vulnerabilities when processing untrusted SVGs [5][2].
Citations:
- 1: https://svgo.dev/docs/plugins/removeScripts/
- 2: GHSA-2p49-hgcm-8545
- 3: https://github.com/svg/svgo/releases/tag/v4.0.0
- 4: https://svgo.dev/docs/migrations/migration-from-v3-to-v4/
- 5: https://github.com/svg/svgo/releases/tag/v4.0.2
XSS (CWE-79): Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
Reachability: Internal
Strip executable SVG content before writing inline assets.
scripts/prepare-venue-svg.mjs processes raw SVG assets and writes app/assets/venue/*.svg, which app/pages/venue.vue renders with v-html. SVGO 4.0.2’s preset-default does not include removeScripts, so SVG script elements, event attributes such as onload, and script URLs can remain in the generated markup. Add removeScripts explicitly before emitting SVGs.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/prepare-venue-svg.mjs` around lines 68 - 83, Update the SVGO plugin
configuration in prepare-venue-svg to explicitly include removeScripts before
writing generated SVG assets. Ensure script elements, event-handler attributes,
and script URLs are stripped while preserving the existing cleanupIds,
prefixIds, and removeDimensions behavior.
| const font = await fetchSubset(family, weight, chars) | ||
| faces.push( | ||
| `@font-face {\n` + | ||
| ` font-family: '${token}';\n` + |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Generate lint-compliant font family names.
This line emits quoted CSS identifiers. Stylelint reports errors for all three generated font-family declarations in app/assets/venue/fonts.css. Update the generator, then regenerate the file.
Proposed fix
- ` font-family: '${token}';\n` +
+ ` font-family: ${token};\n` +📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ` font-family: '${token}';\n` + | |
| ` font-family: ${token};\n` + |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/prepare-venue-svg.mjs` at line 180, Update the font-family
declaration generation in prepare-venue-svg.mjs to emit lint-compliant CSS
identifiers without the problematic quotes, then regenerate
app/assets/venue/fonts.css so all generated font-family declarations use the
corrected format.
Source: Linters/SAST tools
這個 PR 做什麼
把會場地圖從
<img src="*.webp">改成 inline SVG,並加入處理原始匯出檔的腳本。為什麼
原本 TR309 / TR312 用
<img src="*.svg">載入,文字的字距不一。原因是這些 SVG 由 Affinity Designer 匯出,每個字都帶有一組硬寫死的絕對
x座標,那是用 Noto Sans TC 的字寬算出來的:而
<img>載入的 SVG 處於瀏覽器的安全靜態模式 —— 它是獨立文件,拿不到頁面的 CSS 與 web font。沒安裝 Noto Sans TC 的使用者會退回系統字型,字形實際寬度跟預留座標對不上,於是有的字擠在一起、有的空一大格。改成 inline 之後頁面的
fonts.css就能生效,字距回到設計者當初看到的樣子。改了什麼
新增
scripts/prepare-venue-svg.mjs(pnpm prepare:venue-svg)--multipassprefixIds_clip1→TR309___clip1prefixIds是必要的:每個匯出檔都把第一個 clip path 命名為_clip1,各自是獨立<img>文件時沒事,一旦 inline 到同一份 HTML,clip-path="url(#_clip1)"會全部指到第一個,圖會破。字型只嵌入實際用到的字元。
URW DIN與DIN 2014是商用授權字型,且只出現在攤位編號這類單字元標籤(不帶 per-glyph 位移),所以不嵌入。venue.vue改為 inlineoverview靜態 import 隨頁面送出;其餘用import.meta.glob動態載入,切 tab 才拉對應 chunk。所有圖同為7083:4753,用單一aspect-ratio讓 skeleton 與實圖同尺寸,避免 CLS。RB-AU 維持 webp
它的 SVG 匯出檔有 0 個
<path>—— 整份就是一張 base64 PNG 包在<svg>裡,1.1MB 對上 webp 的 235KB,SVGO 完全壓不動。腳本的 SKIP 清單裡有它,原始檔也沒有進版控。測試
pnpm lintpnpm typecheckpnpm build(372 條路由)fonts.css→venue.BdhJjKBo.css(29KB),只在 venue 頁載入/venue三個 tab 確認標籤字距,特別是 TR309 / TR312 / TR3F 這三張有<text>的圖🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Chores