PageSpeed Insights SEO 优化

SKILL内容创作
内容创作 未标注权限 许可证不明确
安装这个技能

Optimize React/Vite SPAs for SEO and PageSpeed performance. Covers HTML shell, JSON-LD structured data, font optimization, Cloudflare Pages headers, image sitemaps, Core Web Vitals, Cumulative Layout Shift (CLS), and Lighthouse accessibility fixes.

一句话选型结论

按资料字段整理

PageSpeed Insights SEO 优化是内容创作类的 Agent Skill,安装方式未标注,权限级别未标注,许可证不明确,有可直接复制的安装命令。技能本质是给模型读的说明书,效果取决于模型与客户端;装完先用一个小任务验证,再放进正式流程。

分类内容创作 安装方式未标注 权限级别未标注权限 许可证许可证不明确
累计安装 未标注 本周 未标注
GitHub 星标 未标注 仓库 未标注
下载量 未标注 技能市场下载数
权限级别 未标注权限 安装前请确认风险范围
许可证 许可证不明确 未说明

安装

未标注
在支持 Skills 的客户端里执行,装完问一句「你有哪些技能」验证
skillhub install user_4e735797/spa-seo-optimization
  1. 1
    确认客户端支持

    确认你的客户端支持 Skills(Claude、Codex、Cursor 等),并升级到较新的版本。

  2. 2
    拿到技能文件

    执行上面的命令安装;也可以把仓库里的 SKILL.md 手动放进客户端的 skills 目录。

  3. 3
    刷新技能列表

    重新打开客户端,或刷新技能列表,让新放进来的技能被加载。

  4. 4
    验证是否生效

    问一句「你有哪些技能」,确认列表里出现它,再拿一个小任务试跑。

技能档案

内容创作
字段来自技能仓库与公开索引,中文说明由本站标注
作者 / 维护方user_4e735797仓库 未标注 · 版本 未标注
宿主客户端未标注技能可被这些客户端加载
安装方式未标注安装包 community
权限级别未标注权限安装前请确认风险范围
许可证未说明许可证不明确,商用前请自行确认条款
仓库可用性available技能仓库当前是否可访问
最近核验未标注上游最后核验时间 · 数据更新 1791703901
正文体量29.3 KBSKILL.md 原文大小,越大说明说明书写得越细

典型使用场景

3 类
按分类整理的常见用法,具体以 SKILL.md 为准
写文章与脚本

按主题产出初稿与分镜脚本。

改稿与润色

把草稿改顺、统一语气。

多平台文案改写

同一内容按平台改成长短不同的版本。

选择建议

装之前先看这几条
按条目字段给出的判断,不替你决定用不用

值得选的地方

  • 有可直接复制的安装命令,不用自己翻文档找入口。

先想清楚的地方

  • 没有标注权限级别,实际能做什么要装好后确认。
  • 许可证不明确,商用或二次分发前先到仓库确认条款。
  • 没有做站内核验,内容以作者仓库为准。
  • 技能只是说明书,实际效果取决于模型与客户端,装完先用小任务试一遍。

更适合

  • 需要在内容创作场景里给模型补一套做法的人。
  • 愿意先读一遍 SKILL.md、确认它会让模型做什么的人。

不太适合

  • 对许可证要求很严、必须明确可商用的人。
  • 只想装完就自动变强、不打算看说明书的人。

同类对比

6 个同类
同分类里挑最接近的几条,字段以各自详情页为准
技能权限级别安装方式累计安装宿主客户端安装命令
PageSpeed Insights SEO 优化当前 未标注权限 未标注 未标注 未标注 有
AI 视频生成 调用外部服务 GitHub 仓库 431.3 万 Codex、Claude、Cursor 有
视频帧 联网访问 ClawHub 市场 262.1 万 OpenClaw 有
Markdown 转换器 写入文件 ClawHub 市场 135.3 万 OpenClaw 有
哔哩哔哩全合一 接触敏感数据 ClawHub 市场 64.1 万 OpenClaw 有
Remotion video toolkit 命令行 浏览器控制 ClawHub 市场 63.9 万 OpenClaw 有

SKILL.md 全文

29.3 KB
模型实际读到的说明书原文,来自技能仓库

SPA SEO & Performance Optimization

Trigger

User asks to SEO/PageSpeed optimize a React/Vite SPA, or a PageSpeed report reveals issues.

Class-level approach

1. Audit the current state

  • Fetch the live site's response headers (curl -sI) — check x-robots-tag, CSP, caching
  • Check _headers, _redirects, robots.txt, sitemap.xml in the project
  • Identify font weight using @font-face analysis in CSS
  • Check for Google/Baidu verification tags

2. Fix critical issues first (SEO scoring)

Issue Fix
x-robots-tag: noindex Add X-Robots-Tag: index, follow to _headers for /*.html and /
Missing hreflang Add for all language variants + x-default
No Baidu verification Get code from ziyuan.baidu.com, add to HTML
Missing OG tags Add og:site_name, og:image:alt, og:locale

3. Font optimization (@fontsource projects)

Case A: Latin-only fonts (Inter, JetBrains Mono, Noto Sans/Serif Latin)

Before: Inter 400/500/600/700 + Noto Serif Latin 500/600 + Noto Serif CJK 500/600 + JetBrains Mono 400/500
After:  Inter 400/500/600/700 + Noto Serif CJK 500/600 ONLY
Savings: ~78KB + 2-4 fewer font requests

Steps:
1. Remove Noto Serif SC Latin subsets — CJK range already covers Latin chars
2. Remove JetBrains Mono @font-face — replace with "SF Mono", "Consolas", monospace in CSS classes
3. Replace .font-mono { font-family: "JetBrains Mono"... } with system monospace
4. Replace .label-text font-family similarly
5. Change font-display: swap to font-display: optional for Latin fonts — prevents font-swap CLS (body-wide ~0.586) on all text.

Case B: CJK/Chinese fonts (⚠️ #1 perf killer for Chinese sites)

CJK font packages are ~1.5MB per font weight. Two weights = 3MB.

Fix: Remove entirely. System CJK serif fonts (Songti SC on Mac, SimSun on Windows) ship on every OS. Update font-family to use system fonts only, rebuild, verify no woff2 in dist.

Alternative (subset): Google Fonts API with text= parameter limits to needed characters (5-15KB instead of 1.5MB).

3b. Vendor chunk splitting (Vite build optimization)

If PageSpeed reports "Reduce unused JavaScript" for the vendor bundle, split React chunks:

rollupOptions: {
  output: {
    manualChunks(id) {
      if (id.includes('node_modules/react-dom')) return 'vendor-dom';
      if (id.includes('node_modules/react')) return 'vendor-react';
      if (id.includes('node_modules/')) return 'vendor-misc';
    },
  },
},

Before: one vendor.js (59KB, 25KB flagged as unused)
After: vendor-react (8KB) + vendor-dom (180KB) + vendor-misc (7KB)

Pitfall: react-dom's size is not unused code — it's the hydration runtime. The PSI warning for vendor chunks is a false positive for SPAs.

3c. React → Preact swap (nuclear JS reduction)

When "Reduce unused JavaScript" persists even after chunk splitting (react-dom still 180KB, 22-25KB unused), swap React for Preact via aliases. Zero code changes needed — Preact is API-compatible.

// vite.config.ts — add to resolve.alias
resolve: {
  alias: {
    'react': 'preact/compat',
    'react-dom': 'preact/compat',
    'react-dom/client': 'preact/compat',
    'react/jsx-runtime': 'preact/jsx-runtime',
  },
},

After the alias, update manualChunks since there's no separate react-dom chunk anymore:

manualChunks(id) {
  if (id.includes('node_modules/@emailjs')) return 'vendor-email';
  if (id.includes('node_modules/')) return 'vendor-misc';
},

Results from real project (55KB gzip total JS → 12.8KB gzip main + 7.8KB misc):
| Chunk | Before (React) | After (Preact) |
|-------|---------------|----------------|
| vendor-dom | 180KB (56KB gzip) | vanishes entirely 🗑️ |
| vendor-react | 8KB (3KB gzip) | vanishes entirely 🗑️ |
| Index (main) | 34KB (13KB gzip) | 33KB (12.8KB gzip) — includes Preact runtime |

Total gzip JS: 75KB → 22KB. Lighthouse "unused JS" warning goes from 22.5KB to ~0.

Installation: npm install preact

Pitfall: Preact does NOT include SyntheticEvent (React's cross-browser event wrapper). If code accesses e.nativeEvent or relies on event pooling, it may break. For standard form submissions and click handlers, Preact works identically.

3d. Eliminate third-party SDKs in favor of raw fetch()

When Lighthouse or PageSpeed flags an external vendor chunk (e.g., @emailjs/browser at 3.5KB), check if the library's REST API is simple enough to call directly.

EmailJS example — replace the SDK with raw fetch():

- import emailjs from "@emailjs/browser";
- await emailjs.send(SERVICE_ID, TEMPLATE_ID, params, { publicKey: KEY });
+ const res = await fetch("https://api.emailjs.com/api/v1.0/email/send", {
+   method: "POST",
+   headers: { "Content-Type": "application/json" },
+   body: JSON.stringify({
+     service_id: SERVICE_ID,
+     template_id: TEMPLATE_ID,
+     user_id: PUBLIC_KEY,
+     template_params: { name, email, ... },
+   }),
+ });
+ if (!res.ok) throw new Error(`EmailJS responded with ${res.status}`);

Savings: ~3.5KB removed (SDK + dependency tree), 31 modules vs 51 modules transformed.

⚠️ CSP update required: Add the API endpoint to connect-src in _headers:

- connect-src 'self'
+ connect-src 'self' https://api.emailjs.com

Rule of thumb: Before adding any npm SDK, check if the service exposes a simple REST API. If yes, raw fetch() + key exposed in browser contexts anyway — no benefit from the SDK wrapper.

3e. Switch minifier: esbuild → terser

esbuild is the Vite default (faster builds) but terser does better dead-code elimination. Switch when:

  • "Reduce unused JavaScript" persists after chunk splitting and Preact swap
  • drop_console is desired (strips all console.log from production bundles)
  • Even marginal KB savings matter (vendor-dom shrank ~2KB gzip)

Config:

build: {
  minify: 'terser',          // replaces 'esbuild'
  terserOptions: {
    compress: {
      drop_console: true,    // strips console.log/debug/error
      drop_debugger: true,
      passes: 2,             // multiple optimization passes
    },
    format: {                // ⚠️ use 'format' NOT 'output' (deprecated in terser 6)
      comments: false,
    },
  },
},

Installation: npm install -D terser

Results: vendor-dom 180.29KB (56.27KB gzip) → 178.55KB (55.73KB gzip). Modest but meaningful, and the console stripping prevents accidental debug log leaks.

4. HTML shell optimization (source index.html)

<!-- Resource Hints: preconnect to third-party origins -->
<link rel="dns-prefetch" href="//static.cloudflareinsights.com" />
<link rel="preconnect" href="https://static.cloudflareinsights.com" />

<!-- Preload hero image (LCP element) -->
<link rel="preload" href="./images/hero-showcase.webp" as="image" fetchpriority="high" />

Preconnect rules: 3-4 max. Always pair dns-prefetch + preconnect. Check CSP first — preconnecting to a blocked origin wastes the connection.

Common origins: static.cloudflareinsights.com, Google Fonts/Analytics (if CSP allows), CDN font origins.

5. Post-build plugin regex trap (expanded)

Vite post-build plugins (like html-transform) that inject/replace content via regex silently fail when the source HTML format changes (e.g., comment style <!-- X --> vs <!-- ===== X ===== -->).

Fix pattern: Match the ACTUAL comment text in source index.html, not a stylized delimiter:

// BROKEN: plugin regex looks for delimiters the source doesn't use
html.replace(/<!-- ===== Critical CSS ===== -->[\s\S]*?<\/style>/, replacement)

// FIXED: match the real comment text in source index.html
html.replace(/<!-- Inline critical CSS to prevent CLS -->[\s\S]*?<\/style>/, replacement)

Verification: Always inspect dist/index.html after build — grep for the injected classes:

grep -c 'font-display' dist/index.html       # > 0 = critical CSS expanded
grep -c 'preconnect' dist/index.html          # > 0 = hints survived
grep -c 'media="print"' dist/index.html       # > 0 = async CSS loading
ls dist/assets/*.woff2                         # font files exist

5a. Preload main JS entry in post-build plugin

In the same html-transform plugin, parse the built HTML to find the main JS entry (index-*.js) and add it as an explicit <link rel="modulepreload"> before the script tag. This lets the browser discover and download the main JS entry even sooner:

// Inside html-transform plugin's closeBundle handler
const mainJsMatch = html.match(/<script type="module" crossorigin src="(\.\/assets\/index-[^"]+\.js)">/);
if (mainJsMatch) {
  const mainJsUrl = mainJsMatch[1];
  if (!html.includes(`href="${mainJsUrl}"`)) {
    html = html.replace(
      mainJsMatch[0],
      `<link rel="modulepreload" href="${mainJsUrl}" />\n    ${mainJsMatch[0]}`,
    );
  }
}

This adds critical-path discovery without changing the execution order. Verify:

grep -c 'modulepreload.*index-' dist/index.html  # > 0 = JS preload active

Workflow Preference for This User

Verify current state before applying optimizations. This user explicitly asks to re-read the latest report and compare with current source/dist state before making any changes. Do not jump to fixes — first audit what's actually different between report claims and local files. Use execute_code or parallel read_file + terminal calls to generate a side-by-side comparison and present findings before writing code.

Lighthouse Report → Source Code Optimization Pipeline

When PageSpeed/Lighthouse returns a list of suggestions, do NOT apply them blindly — the report may be stale or based on cached content.

Step 1: Verify report accuracy against actual files

Image dimensions: Lighthouse reports HTML attribute values, not actual file dimensions. Cross-check:

# macOS (sips)
sips -g pixelWidth -g pixelHeight public/images/*.webp | grep -E "(pixelWidth|pixelHeight|\.webp)"
# ImageMagick
identify public/images/*.webp

Common finding: Images were already resized to display size but HTML width/height attributes reference old larger values.

Fix: Update React component attributes to match actual file dimensions:

// Before: stale attributes (wrong aspect-ratio → CLS)
width="1344" height="768"
// After: matches actual file on disk
width="1200" height="686"

Step 2: Apply image-specific fixes

See references/image-resize-workflow.md. Key addition: After resizing images, ALWAYS sync the width/height attributes in React components (Hero.tsx, Industries.tsx, Cases.tsx are common sites).

Step 2a: Re-compress images for further savings (after resizing)

After verifying dimensions are correct (Step 1), re-encode ALL resized images at quality 75 for additional byte savings beyond dimension-only reduction:

from PIL import Image
import os, glob

images_dir = 'public/images'
for f in sorted(glob.glob(f'{images_dir}/*.webp')):
    name = os.path.basename(f)
    if 'qr' in name: continue  # skip tiny files
    before = os.path.getsize(f)
    img = Image.open(f)
    img.save(f, 'WEBP', quality=75, method=6)
    after = os.path.getsize(f)
    print(f'{name}: {before/1024:.1f} KiB -> {after/1024:.1f} KiB')

⚠️ Re-compression trap: Re-encoding an already-optimized WebP at the same or higher quality can INCREASE file size (double-compression artifacts). Always use quality ≤ original. Test 3-5 images first before batch-running. Skip images that grow (revert with git checkout).

Typical results from this session:
| Image type | Before resize | After resize+compress | Total savings |
|-----------|--------------|----------------------|--------------|
| hero-showcase (1344x768) | 28 KiB | 9.8 KiB (800×457, q75) | -65% |
| case images (1184x635) | 49-82 KiB | 5.5-16 KiB (480×350, q75) | -70-87% |
| industry images (1184×864) | 23-74 KiB | 7-21 KiB (600×438, originals) | -70-90% |

Step 3: Apply structural fixes

  • Add preconnect hints (section 4 above)
  • Split vendor chunks if "Reduce unused JavaScript" warning is significant (section 3b)
  • Verify critical CSS expansion survived post-build plugin (section 5)

Step 4: Rebuild and verify

# CLEAN rebuild — ensures no stale images/files in dist/
rm -rf dist && npm run build

# Verify everything survived the post-build plugin
grep -c 'font-display' dist/index.html
grep -c 'preconnect' dist/index.html
grep -c 'modulepreload.*index-' dist/index.html   # JS entry preload (if added)

# Verify fresh images were copied from public/
sips -g pixelWidth -g pixelHeight dist/images/hero-showcase.webp 2>/dev/null | grep pixel
ls -lh dist/images/*.webp | head -5

JSON-LD structured data

Always include these schema types when applicable:

  1. Organization — @id, logo, sameAs, knowsAbout, areaServed (array of Country objects, not array of strings)
  2. Service — @id, inLanguage, provider refs Organization @id
  3. WebSite — @id, publisher refs Organization @id
  4. BreadcrumbList — @id, first item should use real URL not #hero
  5. ItemList — for case studies/portfolio, include datePublished
  6. FAQPage — rich snippet potential, target 6-8 questions

Noscript fallback for crawlers

Place AFTER <body> tag (NOT in <head>):

<noscript>
  <h1>Site Name — Description</h1>
  <p>Summary paragraph...</p>
  <h2>Services</h2>
  <ul><li>Service 1</li></ul>
  <p>Contact: email@example.com</p>
</noscript>

⚠️ Vite's HTML parser rejects block elements inside <noscript> in <head>. Always place in <body>.

Sitemap with image extensions

Add xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" to <urlset>. Each image needs <image:image> block with <image:loc> and <image:title>.

Cloudflare Pages specifics

  • _headers must include X-Robots-Tag: index, follow — Cloudflare may add noindex by default on Pages
  • CSP script-src 'self' blocks Google Analytics/GTM — update if needed
  • SPA fallback in _redirects: /* /index.html 200

Browser Console Verification (when PSI API is unavailable)

// Get ALL resource transfer sizes
performance.getEntriesByType('resource').map(r => ({
  name: r.name.split('/').pop(),
  size: r.transferSize,
  type: r.initiatorType,
  duration: r.duration.toFixed(0) + 'ms'
}))
// Get total page transfer
performance.getEntriesByType('resource').reduce((s, r) => s + (r.transferSize || 0), 0)
// Filter for fonts specifically
performance.getEntriesByType('resource').filter(r => r.name.includes('woff2'))
  .map(r => ({ name: r.name.split('/').pop(), size: r.transferSize }))

PSI API Quota Pitfall

PSI free tier has a daily query limit (~25K/day). On HTTP 429, use curl-based offline analysis:

curl -sI https://example.com                                     # headers, caching
curl -o /dev/null -s -w "TTFB: %{time_starttransfer}\n" url      # TTFB
curl -s url | grep -E '(title|meta|script type="application/ld|link rel=")'  # SEO meta
curl -sI url | grep -i cf-cache-status                            # CDN status

Rule: One PSI API call per session — for final verification only.


SPA-Specific CLS Prevention

React SPAs suffer CLS from three distinct root causes. Fix all three to get CLS below 0.1.

Root Cause 1: Hydration Gap (the app-shell trap)

The traditional fix is a <div id="app-shell"> that mirrors the React header. This is wrong. The app-shell's content (SVG icon with multiple <rect>, <path> elements, brand text, inline styles) never exactly matches the React-rendered <header> — any structural or dimensional difference causes a measurable CLS during hydration.

Fix: Remove the app-shell entirely. No shell = nothing to swap.

- <div id="root">
-   <div id="app-shell" style="position:fixed;...">svg...</div>
- </div>
+ <div id="root" style="padding-top:72px;min-height:100vh;"></div>

How this eliminates CLS:
1. #root has padding-top: 72px — content area starts 72px from the top
2. No app-shell exists to be replaced — nothing swaps during hydration
3. React adds the fixed <header> on top of the padding area
4. Zero DOM swap = zero layout shift

Update critical CSS — remove the #app-shell rule:

 #root { min-height: 100vh; padding-top: 72px; }
-#app-shell { position: fixed; top: 0; left: 0; right: 0; z-index: 50; height: 72px; }

Result: CLS drops from >1.0 to <0.05 on desktop Lighthouse. Bonus: index.html shrinks by ~1.7KB (no more embedded SVG).

Root Cause 2: Font-Swap CLS (Latin fonts)

font-display: swap on Latin fonts (Inter, 24KB each) causes body-wide CLS (~0.586).

Fix: Use font-display: optional instead of swap for preloaded Latin fonts. Preloaded fonts still arrive in time on fast connections; on slow connections the fallback stays. Zero CLS either way.

LCP Hero Text Font Fallback

Even with font-display: optional, the hero title (often the LCP element) may use a serif display font (.font-display class) that the browser hasn't loaded yet. Without a system-serif fallback in critical CSS, the title text renders invisibly — no CLS, but delayed LCP measurement.

Fix: Add a system-serif fallback to BOTH the source index.html's inline &lt;style&gt; AND the post-build plugin's criticalCSS string:

.font-display { font-family: Georgia, &#x27;Times New Roman&#x27;, serif; }

Why Georgia: 0KB (pre-installed on every OS), renders within the LCP measurement window (2.5s). Without it, the hero title stays invisible until the display font loads. Double-check this rule survived the post-build plugin by grepping dist/index.html after build.

Root Cause 3: Image CLS (aspect-ratio override)

Images with CSS overrides (w-full h-auto) ignore HTML dimensions.

Fix: Add to inline critical CSS:

img[width][height] { aspect-ratio: attr(width) / attr(height); }

html-transform Plugin Trap

If a Vite post-build plugin replaces the inline &lt;style&gt; block via regex, it silently drops styles in the original index.html that aren't in the plugin's criticalCSS template. Both index.html AND the plugin must contain the same critical CSS.

Tailwind fontFamily Config Duplicate

Tailwind's fontFamily.display in tailwind.config.js generates its own .font-display utility. If you ALSO define .font-display in @layer components, both get emitted and Tailwind's takes precedence.

Fix: Update BOTH tailwind.config.js fontFamily array AND manual class definitions. Verify:

grep -o &quot;font-display{font-family[^}]*}&quot; dist/assets/*.css

Accessibility Contrast Fix for Badges

Light-tint badges often fail WCAG AA (4.5:1 for 12px text). Don't darken the tint — introduce a separate CSS variable:

:root { --accent-badge-text: #7A3B10; }      /* 5.7:1 on #EED5C0 */
.dark  { --accent-badge-text: #E6A070; }     /* light orange on 20% opaque bg */

Dark-Surface Text: rgba Opacity Fix (common PSI fail)

React sections on --surface-dark (#1A1A1A in light mode / #0A0A0A in dark) frequently use semi-transparent white text like rgba(245,240,235,0.5). These opacity values produce effective colors that FAIL WCAG AA (4.5:1 for normal text).

Rule of thumb for rgba(245,240,235,X) on #1A1A1A:
| Opacity | Effective color | WCAG AA (4.5:1) | Use for |
|---------|----------------|-----------------|---------|
| 0.4 | #716F6E (2.6:1) | ❌ FAIL | Never |
| 0.5 | #878582 (3.5:1) | ❌ FAIL | Never for text |
| 0.55 | #94918F (4.1:1) | ⚠️ Borderline | Strikethrough / decorative |
| 0.6 | #9D9A97 (4.6:1) | ✅ PASS | Secondary text (barely) |
| 0.65 | #A8A5A2 (5.1:1) | ✅ PASS | Body text / links |
| 0.7 | #B3AFAC (5.8:1) | ✅ PASS | Primary text |
| 0.75 | #C0BCB8 (6.5:1) | ✅ PASS | Label text (use this instead of accent-color on dark) |
| 0.85 | #DCD8D4 (8.5:1) | ✅ PASS | Section headings |
| 1.0 | #F5F0EB (12.2:1) | ✅ PASS | Pure white |

Fix pattern for text-white/XX on accent backgrounds (#B85C28):
- text-white/90 on #B85C28 → 4.57:1 (barely passes AA)
- text-white (full opacity) → 5.37:1 ✅
- Prefer pure white on accent-color backgrounds

Fix checklist when PSI reports contrast failures on dark-themed sections:
1. Identify all rgba(245,240,235,X) values in the section's TSX files
2. Bump each to the next row up in the table (e.g., 0.5 → 0.65, 0.6 → 0.7, 0.4 → 0.55)
3. For text-white/90 on accent bg, change to text-white
4. For label-text on dark surfaces, change from var(--accent-text) (#8A4A1F, 2.3:1) to var(--accent-color) (#B85C28, 6.5:1)
5. Rebuild and re-scan — PSI may report different failures on second pass

See references/dark-surface-contrast-calc.md for the per-component opacity values.


Deployment

wrangler CLI

# Deploy to production (use the configured production branch — typically &quot;main&quot;)
npx wrangler pages deploy dist --project-name NAME --branch main

⚠️ CRITICAL: --branch production creates a PREVIEW, not production. Cloudflare Pages's production branch is typically "main" (or "master"), NOT "production". Using --branch production when the production branch is "main" creates a Preview deployment only accessible at &lt;hash&gt;.project.pages.dev — the custom domain is NOT updated.

How to check:

npx wrangler pages deployment list --project-name NAME
# Look for &quot;Environment&quot; column — &quot;Production&quot; is the one serving the custom domain

Common pattern that silently fails:

# ❌ Creates a PREVIEW on branch &quot;production&quot; — custom domain NOT updated
npx wrangler pages deploy dist --project-name NAME --branch production
# ✅ Deploys to PRODUCTION (branch &quot;main&quot; — the configured production branch)
npx wrangler pages deploy dist --project-name NAME --branch main

Verification after deploy: Compare JS chunk hashes between local dist/ and the live site:

grep -o &#x27;index-[^.]*&#x27; dist/index.html
curl -s https://yoursite.com/ | grep -o &#x27;index-[^.]*&#x27;
# If they match, your deployment is live. If they don&#x27;t, you deployed to the wrong branch.

Post-deploy cache purge

Cloudflare CDN caches images with max-age=31536000 (1 year) + immutable — meaning once cached, browsers and edge nodes never re-request for a full year. After resizing, the new content is invisible to users.

Verification script available: scripts/verify-deployment.py under this skill. Call it with python3 scripts/verify-deployment.py https://yourdomain.com path/to/dist to auto-compare JS chunk hashes between local and deployed — catching stale CDN cache or incomplete deployment without manual curl/grep.

Method A: Cache purge (admin access required)
- Dashboard: Cloudflare → Caching → Purge Everything
- API: POST /zones/{zone_id}/purge_cache --data &#x27;{&quot;purge_everything&quot;:true}&#x27;

Method B: Image rename (no admin access) ⭐

When cache purge is unavailable (no API token, can't access dashboard), rename all image files to new filenames and update all source references. This creates entirely new URLs that bypass the CDN cache:

# In public/images/, create new versions with a suffix
cd public/images
for f in *.webp; do
  base=&quot;${f%.webp}&quot;
  cp &quot;$f&quot; &quot;${base}-opt.webp&quot;   # or use a version number
done

Then update ALL source references (TSX files + index.html preload):

- src=&quot;./images/hero-showcase.webp&quot;
+ src=&quot;./images/hero-showcase-opt.webp&quot;
- &lt;link rel=&quot;preload&quot; href=&quot;./images/hero-showcase.webp&quot; ... /&gt;
+ &lt;link rel=&quot;preload&quot; href=&quot;./images/hero-showcase-opt.webp&quot; ... /&gt;

Impact: Old files remain on the server (unreferenced until TTL expiry), new files serve immediately with correct cache headers. Verify with curl -sI https://domain.com/images/hero-showcase-opt.webp | grep content-length.

Pitfall: Every image URL change means a new hash in JS bundles → larger upload each deployment. Batch all image renames in one commit, not incrementally.

Method C: _headers race-deploy (moderate)

When cache purge is unavailable and image rename is too heavy, temporarily change the image cache policy to max-age=0, must-revalidate, deploy, then revert:

# Step 1: In public/_headers, change image cache to immediate revalidation
#   /images/*         → Cache-Control: public, max-age=0, must-revalidate
#   /og-image.webp    → Cache-Control: public, max-age=0, must-revalidate
# Step 2: Rebuild + deploy
rm -rf dist &amp;&amp; npm run build &amp;&amp; wrangler pages deploy dist --project-name NAME --branch production
# Step 3: Revert _headers back to immutable once cache is refreshed
#   /images/*         → Cache-Control: public, max-age=31536000, immutable
# Step 4: Deploy again
wrangler pages deploy dist --project-name NAME --branch production

Why this works: Cloudflare Pages respects the _headers file. Each deployment triggers a fresh origin fetch from the CDN edge. With max-age=0, the CDN revalidates on every request, immediately serving the new content. After cache refresh, revert to long TTL.

Pitfall: Requires two deployments. The window of no-cache lasts only between deploys (typically 30-120 seconds). Old cached copies on user browsers are NOT invalidated — only the CDN edge cache.

Which method to choose?
| Scenario | Method |
|----------|--------|
| Admin access (API key / Dashboard) | A: Purge cache |
| Can't access admin, OK with one large commit | B: Image rename |
| Can't access admin, want minimal changes | C: _headers race-deploy |
| Need to invalidate user browser caches | Only Method B works (new URL = new cache key) |

Deployment Verification Script

A helper script scripts/verify-deployment.py under this skill automates hash comparison between local dist/ and the deployed site. It:

  1. Fetches deployed HTML and extracts the main JS hash
  2. Compares it against local dist/index.html
  3. Fetches the deployed main JS to extract lazy chunk references
  4. Compares all lazy chunk hashes against local
  5. Fetches one chunk to confirm its content is accessible
python3 scripts/verify-deployment.py https://yourdomain.com /path/to/dist

# Example output:
# Deployed main JS: index-D-5wANjw.js
# ✅ Main JS hash MATCHES local — deployment is live
# ✅ All 7 lazy chunk imports match local
# ✅ Sample chunk WhyUs-CimLE5wr.js: deployed=2341B, local=2341B

These items push a Lighthouse Accessibility audit from 96→100. Each corresponds to a specific automated check.

Contrast (largest scoring category)

Check Rule Fix
Body text ≥ 4.5:1 vs background Use contrast table in main SKILL.md
Large text (≥18px / ≥14px bold) ≥ 3:1 Same table, lower threshold
Text on dark surfaces Opacity ≥ 0.6 body, ≥ 0.65 secondary See references/dark-surface-contrast-calc.md
Text on accent bg Use pure #fff, not text-white/90 text-white/90 on #B85C28 = 4.57:1 (barely passes); pure white = 5.37:1 ✅
label-text class on dark bg var(--accent-text) (#8A4A1F) = 2.3:1 ❌ rgba(245,240,235,0.75) = 6.5:1 ✅. Pitfall: var(--accent-color) (#B85C28) on #1A1A1A = only 3.81:1 — STILL fails WCAG AA for small text. Do NOT use accent color for label text on dark surfaces.

Form Labels

  • Every &lt;input&gt;, &lt;select&gt;, &lt;textarea&gt; needs a &lt;label&gt; (visible or .sr-only)
  • Fallback: aria-label matching the visible placeholder

Common miss: Newsletter email inputs with only placeholder:

+ &lt;label htmlFor=&quot;newsletter-email&quot; className=&quot;sr-only&quot;&gt;{t(&quot;footer.emailPlaceholder&quot;)}&lt;/label&gt;
- &lt;input type=&quot;email&quot; placeholder={t(&quot;footer.emailPlaceholder&quot;)} ... /&gt;
+ &lt;input id=&quot;newsletter-email&quot; type=&quot;email&quot; placeholder={t(&quot;footer.emailPlaceholder&quot;)}
+   aria-label={t(&quot;footer.emailPlaceholder&quot;)} ... /&gt;
  • Required fields: add aria-required=&quot;true&quot; alongside required
  • Error/success messages: add role=&quot;alert&quot;

Document and Landmarks

  • [ ] &lt;html&gt; has lang attribute
  • [ ] &lt;main&gt; exists with id=&quot;main-content&quot;
  • [ ] &lt;nav&gt; has aria-label=&quot;Main navigation&quot;
  • [ ] Page has skip-to-content link at top of &lt;body&gt;
  • [ ] &lt;header&gt;, &lt;main&gt;, &lt;footer&gt; landmark elements used

Skip link boilerplate:

&lt;!-- In index.html, first child of &lt;body&gt; --&gt;
&lt;a href=&quot;#main-content&quot; class=&quot;skip-link&quot;
   style=&quot;position:absolute;top:-9999px;left:-9999px;z-index:9999;
          padding:12px 24px;background:var(--accent-color,#B85C28);color:#fff;
          font-size:14px;font-weight:500;border-radius:0 0 8px 0;text-decoration:none;&quot;&gt;
  Skip to main content
&lt;/a&gt;
.skip-link:focus { top: 0 !important; left: 0 !important; position: fixed !important; }

Keyboard and Focus

  • [ ] Global :focus-visible with 2px accent-color ring + 2px offset
  • [ ] :focus:not(:focus-visible) removes outline for mouse users only
  • [ ] All interactive elements are keyboard-focusable (Tab order matches DOM order)
  • [ ] Touch targets ≥ 44×44px (WCAG 2.2)
*:focus-visible { outline: 2px solid var(--accent-color); outline-offset: 2px; border-radius: 4px; }
*:focus:not(:focus-visible) { outline: none; }

Images

  • [ ] Every &lt;img&gt; has meaningful alt (even alt=&quot;&quot; for decorative)
  • [ ] SVGs have role=&quot;img&quot; + &lt;title&gt; or aria-label
  • [ ] Lazy-loaded images still have alt text

Headings

  • [ ] Exactly one &lt;h1&gt; per page
  • [ ] Hierarchy is logical: h1 → h2 → h3, no gaps
  • [ ] Lists use &lt;ul&gt; / &lt;ol&gt;, not plain &lt;div&gt; or &lt;span&gt;

常见问题

5 问
答案由本站依据条目字段整理
PageSpeed Insights SEO 优化怎么安装?

在支持 Skills 的客户端里执行:skillhub install user_4e735797/spa-seo-optimization。安装方式为未标注。装完刷新技能列表或重开客户端。

PageSpeed Insights SEO 优化支持哪些客户端?

资料没有标注宿主客户端,但只要客户端支持 Skills(Claude、Codex、Cursor 等),一般都能加载。

PageSpeed Insights SEO 优化权限高吗,安全吗?

权限级别为未标注权限。安装前先看它会读取或写入哪些内容。

PageSpeed Insights SEO 优化收费吗?

本站不代收费用,也没有标注具体价格。是否收费取决于技能本身和所用的模型或客户端,正式使用前按作者说明确认。

PageSpeed Insights SEO 优化用什么许可证?

许可证标注为 未说明(许可证不明确)。商用或二次分发前请到仓库确认条款原文。

热门专题

按场景挑技能
全部专题 →

技能内容以作者仓库为准;分类、权限级别与许可证状态的中文说明为中文整理,最近核验 未标注。