web-visual-assets
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWeb Visual Assets
网站视觉资产
Schema authority: theprimitive shape (Illustrationand{name, kind, alt, spec}) and the per-unit{kind: 'waived', reason}Coverage Floor are defined inillustrations[]§11._shared/domain-primitives.md
This skill produces the visual layer of a teaching site. Four asset sources cover virtually every need, each with different tradeoffs.
Schema 权威定义:基础结构(Illustration和{name, kind, alt, spec})以及每个单元的{kind: 'waived', reason}覆盖标准在illustrations[]第11节中定义。_shared/domain-primitives.md
此技能用于生成教学网站的视觉层。四种资产来源几乎能覆盖所有需求,各有不同的权衡。
Asset Source Decision Tree
视觉资产来源决策树
What do you need?
├── Screenshot of a real product/website → Source 1: Playwright scraping
├── A conceptual scene (workflow, metaphor) → Source 2: AI image generation
├── A simple diagram (boxes, arrows, labels) → Source 3: Hand-drawn SVG
└── A scannable code / functional artifact → Source 4: Code generator (QRCode, etc.)What do you need?
├── Screenshot of a real product/website → Source 1: Playwright scraping
├── A conceptual scene (workflow, metaphor) → Source 2: AI image generation
├── A simple diagram (boxes, arrows, labels) → Source 3: Hand-drawn SVG
└── A scannable code / functional artifact → Source 4: Code generator (QRCode, etc.)Source 1: Playwright Scraping
来源1:Playwright 爬取
Use for screenshots of tools, websites, profile cards. The example workshop scrapes 6 AI tool homepages and a YouTube instructor channel.
用于工具、网站、名片的截图示例工作坊爬取了6个AI工具首页和一个YouTube讲师频道。
Standard headless flow
标准无头流程
js
// scripts/scrape-tools.mjs
import { chromium } from 'playwright';
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
await page.goto(url, { waitUntil: 'networkidle' });
await page.screenshot({ path: `data/tools/${id}.png`, fullPage: false });
await browser.close();js
// scripts/scrape-tools.mjs
import { chromium } from 'playwright';
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
await page.goto(url, { waitUntil: 'networkidle' });
await page.screenshot({ path: `data/tools/${id}.png`, fullPage: false });
await browser.close();When anti-bot blocks you (CDP mode)
遇到反机器人拦截时(CDP模式)
Some sites (Cloudflare, modern OpenAI properties) detect headless Chromium and block. Workaround: connect to a real Chrome via CDP:
powershell
undefined部分网站(如Cloudflare、现代OpenAI产品)会检测无头Chromium并拦截。解决方法:通过CDP连接到真实Chrome:
powershell
undefinedscripts/start-cdp-chrome.ps1
scripts/start-cdp-chrome.ps1
Start-Process chrome.exe -ArgumentList '--remote-debugging-port=9222', '--user-data-dir=C:\tmp\cdp-profile'
```js
const browser = await chromium.connectOverCDP('http://localhost:9222');
const page = (await browser.contexts()[0].pages())[0];
// ... drive an existing real-browser sessionRun with flags so the human can complete any CAPTCHA, then press Enter to continue.
--cdp --pauseStart-Process chrome.exe -ArgumentList '--remote-debugging-port=9222', '--user-data-dir=C:\tmp\cdp-profile'
```js
const browser = await chromium.connectOverCDP('http://localhost:9222');
const page = (await browser.contexts()[0].pages())[0];
// ... drive an existing real-browser session运行时添加 参数,以便人工完成验证码,然后按Enter继续。
--cdp --pauseSubset-merge pattern (don't overwrite siblings)
子集合并模式(不覆盖其他内容)
When re-scraping one item out of many, merge with existing data instead of overwriting:
js
async function writeMerged(jsonPath, updates) {
const existing = JSON.parse(await fs.readFile(jsonPath, 'utf8').catch(() => '{}'));
await fs.writeFile(jsonPath, JSON.stringify({ ...existing, ...updates }, null, 2));
}CLI: only touches those two.
node scrape-tools.mjs --ids codex,notebooklm当重新爬取多个项目中的某一个时,应与现有数据合并而非覆盖:
js
async function writeMerged(jsonPath, updates) {
const existing = JSON.parse(await fs.readFile(jsonPath, 'utf8').catch(() => '{}'));
await fs.writeFile(jsonPath, JSON.stringify({ ...existing, ...updates }, null, 2));
}命令行: 仅处理这两个项目。
node scrape-tools.mjs --ids codex,notebooklmYouTube channel quirks
YouTube频道特殊处理
- The channel page is a SPA. URL query strings like are stripped. Sort client-side after fetching the latest N videos.
?sort=p&view=0 - Virtual scrolling strips from out-of-view
src. Derive thumbnail URLs from<img>:videoId. Don't rely on DOMhttps://img.youtube.com/vi/{videoId}/hqdefault.jpg.src
- 频道页面是单页应用(SPA),URL查询参数如 会被移除。获取最新N个视频后在客户端进行排序。
?sort=p&view=0 - 虚拟滚动会将不可见的 的
<img>清空。需从src推导缩略图URL:videoId,不要依赖DOM中的https://img.youtube.com/vi/{videoId}/hqdefault.jpg属性。src
Source 2: AI Image Generation
来源2:AI图像生成
Use for scenario illustrations, day heroes, conceptual scenes. The example workshop uses Gemini's image generation; any text-to-image API works.
用于场景插图、主视觉图、概念场景。示例工作坊使用Gemini的图像生成功能,任何文本转图像API均可适用。
Prompt design rules
提示词设计规则
- Specify a consistent visual style across the whole course (e.g. "flat illustration, soft pastel palette, no text, no people"). Inconsistent illustrations look amateurish.
- Avoid faces and brand logos — AI image generators struggle with both and produce uncanny results.
- Store prompts alongside the images:
assets/illustrations/
├── day1-token-prediction.png
├── day1-token-prediction.prompt.md ← regenerate-ready prompt
└── ...This lets you regenerate at higher quality later or tweak style.
- 指定全课程统一的视觉风格(例如:"扁平化插图、柔和淡色调、无文字、无人物")。风格不一致的插图会显得不专业。
- 避免人脸和品牌标志——AI图像生成器在这两方面表现不佳,会产生怪异的结果。
- 将提示词与图片一起存储:
assets/illustrations/
├── day1-token-prediction.png
├── day1-token-prediction.prompt.md ← 可重新生成的提示词
└── ...这样后续可以更高质量重新生成或调整风格。
PNG + SVG fallback render
PNG + SVG降级渲染
AI generation occasionally fails (rate limits, content filters, model issues). Pair every AI PNG with a hand-drawn SVG fallback at the same path stem:
js
function renderIllustration(name) {
const img = el('img', { src: `assets/illustrations/${name}.png`, alt: '' });
img.onerror = () => { img.src = `assets/illustrations/${name}.svg`; img.onerror = null; };
return img;
}The browser tries PNG; on 404 swaps to SVG. The fallback ships even if you never use it — defensive against link rot.
AI生成偶尔会失败(速率限制、内容过滤、模型问题)。应为每个AI生成的PNG搭配手绘SVG降级方案,路径前缀保持一致:
js
function renderIllustration(name) {
const img = el('img', { src: `assets/illustrations/${name}.png`, alt: '' });
img.onerror = () => { img.src = `assets/illustrations/${name}.svg`; img.onerror = null; };
return img;
}浏览器会先尝试加载PNG;若返回404则切换为SVG。即使从未使用过降级方案,也需随项目一同发布——这是应对链接失效的防御性措施。
Source 3: Hand-drawn SVG
来源3:手绘SVG
Use for diagrams that benefit from precise control: arrows between named boxes, step-by-step flows, labelled UI mockups, the classroom map.
Hand-coded SVG (or exported from Figma/Excalidraw) is preferable to AI for:
- Text labels (AI can't reliably render Chinese text inside images)
- Arrow + box flows (AI gets layout wrong)
- Anything that needs to update when course details change
Style hint: wrap in a translucent container + filter to match the site's overall aesthetic, even if the SVG itself is line-art.
drop-shadow用于需要精确控制的图表:带命名的箭头框图、分步流程、带标签的UI原型、教室地图。
手动编写的SVG(或从Figma/Excalidraw导出)相比AI更适合:
- 文本标签(AI无法可靠地在图像中渲染中文文本)
- 箭头+框流程(AI容易布局错误)
- 任何需要随课程细节更新的内容
风格提示:将SVG包裹在半透明容器中,并添加 滤镜,以匹配网站的整体视觉风格,即使SVG本身是线稿。
drop-shadowSource 4: Generated Codes (QR)
来源4:代码生成(二维码)
js
import QRCode from 'qrcode';
await QRCode.toFile('assets/qr/workshop-url.png', 'https://your-workshop.example/', {
width: 512,
margin: 1,
color: { dark: '#000000', light: '#FFFFFF' }
});For dark-themed sites, generate a dark-on-white code, not white-on-dark — most QR scanners require dark foreground on light background.
js
import QRCode from 'qrcode';
await QRCode.toFile('assets/qr/workshop-url.png', 'https://your-workshop.example/', {
width: 512,
margin: 1,
color: { dark: '#000000', light: '#FFFFFF' }
});对于深色主题的网站,生成黑底白码而非白底黑码——大多数二维码扫描器需要深色前景、浅色背景。
Asset Folder Convention
资产文件夹规范
assets/
├── tools/ ← Source 1: scraped tool/product screenshots
├── illustrations/ ← Source 2 (PNG) + Source 3 (SVG fallback)
├── scenarios/ ← Source 2: per-unit scene illustrations
├── cases/ ← Source 2: shared case visuals
├── characters/ ← Source 2: persona portraits
├── qr/ ← Source 4: QR codes
└── maps/ ← Source 1 (screenshot) or Source 3 (SVG)assets/
├── tools/ ← 来源1:爬取的工具/产品截图
├── illustrations/ ← 来源2(PNG) + 来源3(SVG降级方案)
├── scenarios/ ← 来源2:每个单元的场景插图
├── cases/ ← 来源2:共享案例视觉素材
├── characters/ ← 来源2:人物肖像
├── qr/ ← 来源4:二维码
└── maps/ ← 来源1(截图)或来源3(SVG)Wiring Assets into the SPA
将资产接入SPA
In , every unit carries an array (1–3 entries) populated from the blocks written in Stage 2:
course-data.jsillustrations[]圖片需求js
{
id: 'u-3',
title: '...',
illustrations: [
{ name: 'day1-u3-hero.png', kind: 'hero', alt: '...', spec: '...' },
{ name: 'day1-u3-flow.svg', kind: 'diagram', alt: '...', spec: '...' },
{ name: 'day1-u3-example.png', kind: 'screenshot', alt: '...', spec: '...' } // optional 3rd
],
// ...
}renderUnitunit.illustrationsrenderIllustration(entry)Legacy single- field: older files use (single string). Treat it as and migrate to the array form when convenient. Don't rely on the legacy shape for new sites.
illustrationcourse-data.jsillustration: 'foo.png'illustrations: [{ name: 'foo.png', kind: 'hero' }]在 中,每个单元都包含一个 数组(1-3个条目),数据来自阶段2编写的「圖片需求」块:
course-data.jsillustrations[]js
{
id: 'u-3',
title: '...',
illustrations: [
{ name: 'day1-u3-hero.png', kind: 'hero', alt: '...', spec: '...' },
{ name: 'day1-u3-flow.svg', kind: 'diagram', alt: '...', spec: '...' },
{ name: 'day1-u3-example.png', kind: 'screenshot', alt: '...', spec: '...' } // 可选第三个条目
],
// ...
}renderUnitunit.illustrationsrenderIllustration(entry)遗留的单个字段:旧版 文件使用 (单个字符串)。需将其视为 ,方便时迁移到数组格式。新网站不要依赖遗留格式。
illustrationcourse-data.jsillustration: 'foo.png'illustrations: [{ name: 'foo.png', kind: 'hero' }]Coverage Floor (Hard Rule — 1–3 illustrations per unit)
覆盖标准(硬性规则——每个单元1-3张插图)
A teaching site that ships with only a cover image looks like an unfinished draft — Stage 5 must hit a minimum coverage before declaring the site feature-complete:
- Every unit in has
course-data.js(andillustrations.length >= 1).<= 3 - At least one entry per unit has a real file under (PNG or SVG). Pure-stub units (
assets/everywhere) fail.{ kind: 'placeholder' } - Genuine no-image units carry an explicit waiver: . Silence — i.e. an empty array — fails the floor.
illustrations: [{ kind: 'waived', reason: '...' }] - Cover image (or equivalent) exists in addition to per-unit images. Cover does NOT count toward any unit's 1–3 quota.
assets/cover.png
When the floor is unmet, the site cannot be declared "feature-complete" and Stages 5b / 6 must NOT be dispatched yet — see Stage 5 Image Coverage Floor for the orchestrator-level enforcement.
teaching-site/SKILL.md仅带有封面图的教学网站看起来像未完成的草稿——阶段5必须达到最低覆盖标准才能宣布网站功能完整:
- 中的每个单元都满足
course-data.js(且illustrations.length >= 1)。<= 3 - 每个单元至少有一个条目在 下有真实文件(PNG或SVG)。全占位符单元(所有条目均为
assets/)不达标。{ kind: 'placeholder' } - 确实不需要图片的单元需明确声明豁免:。空数组(即未声明)不达标。
illustrations: [{ kind: 'waived', reason: '...' }] - 除了每个单元的图片外,封面图(或等效文件)必须存在。封面图不计入任何单元的1-3张配额。
assets/cover.png
若未达到覆盖标准,网站不能被宣布为“功能完整”,且不得启动阶段5b/6——详见 阶段5图片覆盖标准的编排器级强制要求。
teaching-site/SKILL.mdBatch-generation pattern (recommended)
批量生成模式(推荐)
Don't generate images one unit at a time. Build a manifest from and run a batch:
course-data.jsjs
// scripts/generate-illustrations.mjs
const manifest = [];
for (const dayKey of ['day1', 'day2', 'day3', 'day4']) {
for (const unit of window.COURSE[dayKey].units) {
for (const ill of (unit.illustrations || [])) {
if (ill.kind === 'waived') continue;
manifest.push({ name: ill.name, prompt: ill.spec, kind: ill.kind });
}
}
}
// → feed manifest to AI image API in parallel batches of 4–6Why batch: style consistency. Single-unit ad-hoc generation drifts. A batch with the same style prefix produces a coherent visual system.
不要逐个单元生成图片。从 构建清单并批量运行:
course-data.jsjs
// scripts/generate-illustrations.mjs
const manifest = [];
for (const dayKey of ['day1', 'day2', 'day3', 'day4']) {
for (const unit of window.COURSE[dayKey].units) {
for (const ill of (unit.illustrations || [])) {
if (ill.kind === 'waived') continue;
manifest.push({ name: ill.name, prompt: ill.spec, kind: ill.kind });
}
}
}
// → 将清单以4-6个为一批并行传入AI图像API为什么要批量:保证风格一致性。逐个单元临时生成会导致风格偏差。带有相同风格前缀的批量生成能产生连贯的视觉系统。
Verification
验证
Two verification concerns, each with its own skill:
- "Do referenced assets exist on disk?" (cross-file reference check) → invoke . Its Audit 1 (Cross-Artifact Reference Resolution) and the asset coverage audit cover this exactly.
web-content-audit - "Do assets actually render in the browser without 404s?" (runtime check) → invoke . A multi-viewport verify script that collects
web-visual-verificationwill surface brokenconsole.errorrequests automatically.<img>
Run both before each deployment. The audit catches references to files that don't exist; the verify catches assets that exist but are mis-pathed in the rendered HTML. They fail in different ways and need different scripts.
有两个验证关注点,各对应一个技能:
- “引用的资产是否存在于磁盘上?”(跨文件引用检查)→ 调用 。其审计1(跨工件引用解析)和资产覆盖审计恰好涵盖此内容。
web-content-audit - “资产在浏览器中是否能正常渲染,无404错误?”(运行时检查)→ 调用 。一个多视口验证脚本会收集
web-visual-verification,自动发现失效的console.error请求。<img>
每次部署前都要运行这两个验证。审计会捕获指向不存在文件的引用;验证会捕获文件存在但在渲染的HTML中路径错误的情况。两者失效方式不同,需要不同的脚本。
Anti-Patterns
反模式
- Generating images one-by-one without style consistency — write the style spec once, paste it as prefix for every prompt.
- AI-generated text in images — almost always broken; use SVG for any image that needs accurate text.
- No SVG fallback for AI PNGs — when the PNG link breaks (file deletion, rename mistake), the site shows broken-image icons; SVG fallback degrades gracefully.
- Storing screenshots without a re-scrape script — six months later someone wants fresh screenshots and there's no record of how the originals were taken.
- 逐个生成图片且不保证风格一致性——只需编写一次风格规范,将其作为前缀粘贴到每个提示词中。
- AI生成的图片中包含文本——几乎总是出错;如需准确文本,使用SVG。
- AI生成的PNG没有SVG降级方案——当PNG链接失效(文件删除、重命名错误)时,网站会显示破损图片图标;SVG降级方案能优雅降级。
- 存储截图但不保留重新爬取脚本——六个月后有人想要最新截图时,没有记录原始截图是如何获取的。
Hand-off
交接
Tell the user: "visual assets in place. Run to confirm nothing's missing. The site is now feature-complete. Next stage () can turn the same content into a PDF/DOCX deliverable — invoke that only when the web version is stable."
node scripts/verify-assets.mjscourse-ebook-publishing告知用户:“视觉资产已部署完成。运行 确认无缺失内容。网站现已功能完整。下一阶段()可将相同内容转换为PDF/DOCX交付物——仅当网页版本稳定后再调用该阶段。”
node scripts/verify-assets.mjscourse-ebook-publishing