web-visual-assets

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Web Visual Assets

网站视觉资产

Schema authority: the
Illustration
primitive shape (
{name, kind, alt, spec}
and
{kind: 'waived', reason}
) and the per-unit
illustrations[]
Coverage Floor are defined in
_shared/domain-primitives.md
§11.
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[]
覆盖标准在
_shared/domain-primitives.md
第11节中定义。
此技能用于生成教学网站的视觉层。四种资产来源几乎能覆盖所有需求,各有不同的权衡。

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
undefined

scripts/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 session
Run with
--cdp --pause
flags so the human can complete any CAPTCHA, then press Enter to continue.
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 session
运行时添加
--cdp --pause
参数,以便人工完成验证码,然后按Enter继续。

Subset-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:
node scrape-tools.mjs --ids codex,notebooklm
only touches those two.
当重新爬取多个项目中的某一个时,应与现有数据合并而非覆盖:
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,notebooklm
仅处理这两个项目。

YouTube channel quirks

YouTube频道特殊处理

  • The channel page is a SPA. URL query strings like
    ?sort=p&view=0
    are stripped. Sort client-side after fetching the latest N videos.
  • Virtual scrolling strips
    src
    from out-of-view
    <img>
    . Derive thumbnail URLs from
    videoId
    :
    https://img.youtube.com/vi/{videoId}/hqdefault.jpg
    . Don't rely on DOM
    src
    .
  • 频道页面是单页应用(SPA),URL查询参数如
    ?sort=p&view=0
    会被移除。获取最新N个视频后在客户端进行排序。
  • 虚拟滚动会将不可见的
    <img>
    src
    清空。需从
    videoId
    推导缩略图URL:
    https://img.youtube.com/vi/{videoId}/hqdefault.jpg
    ,不要依赖DOM中的
    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

提示词设计规则

  1. Specify a consistent visual style across the whole course (e.g. "flat illustration, soft pastel palette, no text, no people"). Inconsistent illustrations look amateurish.
  2. Avoid faces and brand logos — AI image generators struggle with both and produce uncanny results.
  3. 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.
  1. 指定全课程统一的视觉风格(例如:"扁平化插图、柔和淡色调、无文字、无人物")。风格不一致的插图会显得不专业。
  2. 避免人脸和品牌标志——AI图像生成器在这两方面表现不佳,会产生怪异的结果。
  3. 将提示词与图片一起存储
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 +
drop-shadow
filter to match the site's overall aesthetic, even if the SVG itself is line-art.
用于需要精确控制的图表:带命名的箭头框图、分步流程、带标签的UI原型、教室地图。
手动编写的SVG(或从Figma/Excalidraw导出)相比AI更适合:
  • 文本标签(AI无法可靠地在图像中渲染中文文本)
  • 箭头+框流程(AI容易布局错误)
  • 任何需要随课程细节更新的内容
风格提示:将SVG包裹在半透明容器中,并添加
drop-shadow
滤镜,以匹配网站的整体视觉风格,即使SVG本身是线稿。

Source 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
course-data.js
, every unit carries an
illustrations[]
array (1–3 entries) populated from the
圖片需求
blocks written in Stage 2:
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
  ],
  // ...
}
renderUnit
iterates
unit.illustrations
and calls
renderIllustration(entry)
, which handles PNG-first / SVG-fallback per entry. Render hero first (above the fold), then diagram, then screenshot — in that order.
Legacy single-
illustration
field
: older
course-data.js
files use
illustration: 'foo.png'
(single string). Treat it as
illustrations: [{ name: 'foo.png', kind: 'hero' }]
and migrate to the array form when convenient. Don't rely on the legacy shape for new sites.
course-data.js
中,每个单元都包含一个
illustrations[]
数组(1-3个条目),数据来自阶段2编写的「圖片需求」块:
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: '...' }  // 可选第三个条目
  ],
  // ...
}
renderUnit
会遍历
unit.illustrations
并调用
renderIllustration(entry)
,该函数会为每个条目处理PNG优先/SVG降级逻辑。先渲染主视觉图(首屏上方),然后是图表,最后是截图——按此顺序。
遗留的单个
illustration
字段
:旧版
course-data.js
文件使用
illustration: '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
    course-data.js
    has
    illustrations.length >= 1
    (and
    <= 3
    ).
  • At least one entry per unit has a real file under
    assets/
    (PNG or SVG). Pure-stub units (
    { kind: 'placeholder' }
    everywhere) fail.
  • Genuine no-image units carry an explicit waiver:
    illustrations: [{ kind: 'waived', reason: '...' }]
    . Silence — i.e. an empty array — fails the floor.
  • Cover image (
    assets/cover.png
    or equivalent) exists in addition to per-unit images. Cover does NOT count toward any unit's 1–3 quota.
When the floor is unmet, the site cannot be declared "feature-complete" and Stages 5b / 6 must NOT be dispatched yet — see
teaching-site/SKILL.md
Stage 5 Image Coverage Floor for the orchestrator-level enforcement.
仅带有封面图的教学网站看起来像未完成的草稿——阶段5必须达到最低覆盖标准才能宣布网站功能完整:
  • course-data.js
    中的每个单元都满足
    illustrations.length >= 1
    (且
    <= 3
    )。
  • 每个单元至少有一个条目在
    assets/
    下有真实文件(PNG或SVG)。全占位符单元(所有条目均为
    { kind: 'placeholder' }
    )不达标。
  • 确实不需要图片的单元需明确声明豁免:
    illustrations: [{ kind: 'waived', reason: '...' }]
    。空数组(即未声明)不达标。
  • 除了每个单元的图片外,封面图(
    assets/cover.png
    或等效文件)必须存在。封面图不计入任何单元的1-3张配额。
若未达到覆盖标准,网站不能被宣布为“功能完整”,且不得启动阶段5b/6——详见
teaching-site/SKILL.md
阶段5图片覆盖标准的编排器级强制要求。

Batch-generation pattern (recommended)

批量生成模式(推荐)

Don't generate images one unit at a time. Build a manifest from
course-data.js
and run a batch:
js
// 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–6
Why batch: style consistency. Single-unit ad-hoc generation drifts. A batch with the same style prefix produces a coherent visual system.
不要逐个单元生成图片。从
course-data.js
构建清单并批量运行:
js
// 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
    web-content-audit
    . Its Audit 1 (Cross-Artifact Reference Resolution) and the asset coverage audit cover this exactly.
  • "Do assets actually render in the browser without 404s?" (runtime check) → invoke
    web-visual-verification
    . A multi-viewport verify script that collects
    console.error
    will surface broken
    <img>
    requests automatically.
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.
有两个验证关注点,各对应一个技能:
  • “引用的资产是否存在于磁盘上?”(跨文件引用检查)→ 调用
    web-content-audit
    。其审计1(跨工件引用解析)和资产覆盖审计恰好涵盖此内容。
  • “资产在浏览器中是否能正常渲染,无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
node scripts/verify-assets.mjs
to confirm nothing's missing. The site is now feature-complete. Next stage (
course-ebook-publishing
) can turn the same content into a PDF/DOCX deliverable — invoke that only when the web version is stable."
告知用户:“视觉资产已部署完成。运行
node scripts/verify-assets.mjs
确认无缺失内容。网站现已功能完整。下一阶段(
course-ebook-publishing
)可将相同内容转换为PDF/DOCX交付物——仅当网页版本稳定后再调用该阶段。”