Render-validate the built Copilot Workshops site in a real browser using the Playwright MCP server. Use as an optional deeper QA pass — after the static checks in build-and-verify-docs — to confirm pages actually render. Trigger when asked to "validate the built site", "check the pages render", "do a browser/visual QA pass", or before opening a PR that changes rendered output.
Install
npx skillscat add boi6765/agents-in-sdlc/validate-site-playwright Install via the SkillsCat registry.
Validate the built site with Playwright
build-and-verify-docs checks the site statically — it type-checks, builds, inspects affected output against its source, and link-checks the HTML with lychee. Browser checks complement that evidence by finding runtime failures: console/hydration errors, images that fail to load, or visual rendering problems.
This skill is the optional, deeper, browser-based pass. It drives the Playwright MCP server against a local preview of the built site. It is interactive/local only — CI (pages.yml) has no browser step, so this is never a merge gate. Run it before a PR that changes how pages render (new site-shell components, image-heavy lessons, layout changes), or whenever you want to confirm the real rendered output.
Run every command from the repo root unless a step says otherwise.
Prerequisites
The Playwright MCP server must be available (the agent has
browser_*tools).A clean build must exist. If you haven't just built, run the build from `build-and-verify-docs` first:
cd website && rm -rf dist && npm run build && cd ..
1. Serve the built site
Serve the production build (not the dev server) so you validate exactly what ships. npm run preview serves website/dist/ at the real base path:
cd website && npm run previewThe site is served at http://localhost:4321/copilot-workshops/. Start it as a detached background process so it survives while you navigate, and capture its PID for teardown. Wait until the server logs that it's listening before navigating.
2. Derive the routes to check
Don't hard-code URLs. The built dist/ is the source of truth for what routes exist:
# every built route, as site-absolute paths under the base
find website/dist -name index.html | grep -v 404 | sed 's#website/dist#/copilot-workshops#; s#/index.html#/#'Validate a representative sample that covers every category and workshop: the landing page (/copilot-workshops/), a first-steps lesson, and at least one real-world development lesson from each of cli/, vscode/, cloud/, and app/. For a release pass or a change that touches shared layout/components, validate all routes.
3. Validate each route
For each chosen route, use the Playwright MCP tools:
Navigate —
browser_navigatetohttp://localhost:4321/copilot-workshops/<route>.Confirm it rendered —
browser_snapshotand check the page has its heading/title and real content (not an error page or raw, unrendered Markdown).Check the console —
browser_console_messageswithlevel: "error". There should be zero errors. Hydration warnings and 404s for assets surface here.Known benign exception: the legacy redirect route
/copilot-workshops/shared/0-prereqs/is a minimal full-HTML redirect page (it immediately forwards to the home page/copilot-workshops/via a meta refresh) and declares no favicon, so the browser auto-requests/favicon.icoand logs a single404 (Not Found). That one favicon 404 on the redirect page only is expected. Validate that route by confirming it lands on the home page, not by console cleanliness. A favicon 404 on any real page is a genuine finding (real pages linkfavicon.svg).Find broken images —
browser_evaluatewith an async function that force-loads lazy images first, then flags only the ones that truly fail. Starlight/Astro mark below-the-fold imagesloading="lazy", so a naivenaturalWidth === 0check reports false positives for images that simply haven't scrolled into view yet:async () => { const imgs = Array.from(document.images); const results = await Promise.all(imgs.map(img => new Promise(resolve => { img.loading = 'eager'; // defeat lazy-loading so the asset actually fetches if (img.complete) return resolve(img.naturalWidth === 0 ? (img.currentSrc || img.src) : null); const done = ok => resolve(ok ? null : (img.currentSrc || img.src)); img.addEventListener('load', () => done(true), { once: true }); img.addEventListener('error', () => done(false), { once: true }); setTimeout(() => done(img.naturalWidth > 0), 5000); // timeout guard }))); return results.filter(Boolean); }An empty array is a pass. Any entry is a genuinely broken/missing image — most often an
_images/path that didn't survive a rename (cross-check with the `build-and-verify-docs` consistency pass). If you ever get a hit, confirm it withcurl -o /dev/null -w '%{http_code}'against the asset URL before treating it as a real failure — a200means it was a lazy-load timing artifact, not a broken image.Confirm Markdown rendered cleanly — GitHub admonitions should render as styled Starlight callouts, not literal text. In the snapshot, verify there is no visible
[!NOTE],[!TIP],[!CAUTION],[!WARNING],[!IMPORTANT], leftover:::directive, or raw frontmatter in the body.Screenshot (optional) —
browser_take_screenshotfor a visual record of key pages. Save screenshots outside the repo as throwaway artifacts, not content.
4. Tear down
Close the browser and stop the preview server when finished:
browser_closeto release the browser.kill <PID>for the preview server you started in step 1.
What to report
Summarize per route: rendered (yes/no), console errors (count), broken images (list), Markdown rendered cleanly (yes/no). A clean pass is every route rendered, zero console errors, zero broken images, all Markdown rendered cleanly. Flag anything else with the specific route and the failing URL/selector so it can be fixed before the PR.
Scope notes
- This validates rendering, not link correctness — lychee (in
build-and-verify-docs) owns link checking. Run both. - It is not an accessibility audit. For accessibility authoring conventions, see `markdown-accessibility.instructions.md`.
- It is local-only and does not gate merges; treat failures as must-fix-before-PR, not as CI status.