Skip to content

feat: render venue floor plans as inline SVG - #312

Open
yoyo930021 wants to merge 2 commits into
mainfrom
inline-venue-floor-plans
Open

feat: render venue floor plans as inline SVG#312
yoyo930021 wants to merge 2 commits into
mainfrom
inline-venue-floor-plans

Conversation

@yoyo930021

@yoyo930021 yoyo930021 commented Aug 5, 2026

Copy link
Copy Markdown
Member

這個 PR 做什麼

把會場地圖從 <img src="*.webp"> 改成 inline SVG,並加入處理原始匯出檔的腳本。

為什麼

原本 TR309 / TR312 用 <img src="*.svg"> 載入,文字的字距不一

原因是這些 SVG 由 Affinity Designer 匯出,每個字都帶有一組硬寫死的絕對 x 座標,那是用 Noto Sans TC 的字寬算出來的:

<text style="font-family:'NotoSansTC-Medium',...">福<tspan
  x="219.402px 297.095px 316.563px 363.694px ...">岡 Engineer Cafe:打造全球開源社群典範</tspan></text>

<img> 載入的 SVG 處於瀏覽器的安全靜態模式 —— 它是獨立文件,拿不到頁面的 CSS 與 web font。沒安裝 Noto Sans TC 的使用者會退回系統字型,字形實際寬度跟預留座標對不上,於是有的字擠在一起、有的空一大格。

改成 inline 之後頁面的 fonts.css 就能生效,字距回到設計者當初看到的樣子。

改了什麼

新增 scripts/prepare-venue-svg.mjspnpm prepare:venue-svg

app/assets/venue/raw/*.svg  ← 設計師的 Affinity 匯出檔
  → app/assets/venue/*.svg  ← 壓縮 + id 命名空間化後的成品
  → app/assets/venue/fonts.css
處理 結果
SVGO --multipass 3125K → 1647K(−47.3%
prefixIds _clip1TR309___clip1
Noto Sans TC subset 內嵌 Thin/Medium/Bold 三個 weight、147 字、21KB

prefixIds 是必要的:每個匯出檔都把第一個 clip path 命名為 _clip1,各自是獨立 <img> 文件時沒事,一旦 inline 到同一份 HTML,clip-path="url(#_clip1)" 會全部指到第一個,圖會破。

字型只嵌入實際用到的字元。URW DINDIN 2014 是商用授權字型,且只出現在攤位編號這類單字元標籤(不帶 per-glyph 位移),所以不嵌入。

venue.vue 改為 inline

overview 靜態 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 lint
  • pnpm typecheck
  • pnpm build(372 條路由)
  • 首屏 HTML 不含非當前 tab 的圖(各前綴 grep 均為 0),每張圖獨立 chunk
  • fonts.cssvenue.BdhJjKBo.css(29KB),只在 venue 頁載入
  • 字距的視覺確認 — 尚未在瀏覽器實際檢視過,合併前請 reviewer 開 /venue 三個 tab 確認標籤字距,特別是 TR309 / TR312 / TR3F 這三張有 <text> 的圖

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Venue floor plans now load faster and render more reliably, with inline vector support and a raster fallback for certain plans.
    • Venue text now uses improved embedded fonts for more consistent display.
  • Documentation

    • Added guidance for preparing and committing venue floor-plan assets.
  • Chores

    • Added a preparation step for generating optimized venue assets and updated build/tooling settings.

yoyo930021 and others added 2 commits August 5, 2026 21:35
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>
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Venue asset pipeline

Layer / File(s) Summary
SVG optimization and font generation
scripts/prepare-venue-svg.mjs, package.json
The new preparation script optimizes venue SVGs, skips RB-AU.svg, namespaces IDs, extracts supported glyphs, fetches Google Fonts WOFF2 subsets, and generates fonts.css. The package adds the preparation command and svgo.
Inline venue plan rendering
app/pages/venue.vue, app/assets/venue/fonts.css, eslint.config.mjs
The venue page uses shared plan configuration, dynamic inline SVG loading, raster rendering for RB/AU, loading placeholders, and embedded venue fonts. ESLint ignores the generated stylesheet.
Asset workflow documentation
AGENTS.md
The documentation describes venue source and generated assets, the preparation command, commit requirements, inline rendering constraints, and the RB/AU WebP exception.

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
Loading

Possibly related PRs

  • COSCUP/2026#302: Introduced the venue page that this PR extends with shared configuration and inline SVG rendering.
  • COSCUP/2026#311: Refactored venue floor-plan configuration and included the RB/AU plan handled here.

Suggested reviewers: rileychh

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: rendering venue floor plans as inline SVG.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch inline-venue-floor-plans
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch inline-venue-floor-plans

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
app/pages/venue.vue (1)

129-150: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use 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 colors primary-50 through primary-800 and cp-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

📥 Commits

Reviewing files that changed from the base of the PR and between 5e24526 and b858bcc.

⛔ Files ignored due to path filters (17)
  • app/assets/venue/TR2F.svg is excluded by !**/*.svg
  • app/assets/venue/TR309.svg is excluded by !**/*.svg
  • app/assets/venue/TR312.svg is excluded by !**/*.svg
  • app/assets/venue/TR3F.svg is excluded by !**/*.svg
  • app/assets/venue/TR409-1.svg is excluded by !**/*.svg
  • app/assets/venue/TR4F.svg is excluded by !**/*.svg
  • app/assets/venue/TR5F.svg is excluded by !**/*.svg
  • app/assets/venue/overview.svg is excluded by !**/*.svg
  • app/assets/venue/raw/TR2F.svg is excluded by !**/*.svg
  • app/assets/venue/raw/TR309.svg is excluded by !**/*.svg
  • app/assets/venue/raw/TR312.svg is excluded by !**/*.svg
  • app/assets/venue/raw/TR3F.svg is excluded by !**/*.svg
  • app/assets/venue/raw/TR409-1.svg is excluded by !**/*.svg
  • app/assets/venue/raw/TR4F.svg is excluded by !**/*.svg
  • app/assets/venue/raw/TR5F.svg is excluded by !**/*.svg
  • app/assets/venue/raw/overview.svg is excluded by !**/*.svg
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (12)
  • AGENTS.md
  • app/assets/venue/TR2F.webp
  • app/assets/venue/TR3F.webp
  • app/assets/venue/TR409-1.webp
  • app/assets/venue/TR4F.webp
  • app/assets/venue/TR5F.webp
  • app/assets/venue/fonts.css
  • app/assets/venue/overview.webp
  • app/pages/venue.vue
  • eslint.config.mjs
  • package.json
  • scripts/prepare-venue-svg.mjs

Comment on lines +68 to +83
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',
],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 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 || true

Repository: 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))
PY

Repository: 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]}")
PY

Repository: 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:


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` +

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

Suggested change
` 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant