skills/briefing-critique/SKILL.md
Compare generated draft pages against the source briefing to catch copy errors, missing content, wrong block types, and structural mismatches. Use when "critique drafts", "check briefing compliance", "verify content fidelity", "QA against briefing", or "validate draft pages".
npx skillsauth add paolomoz/skills briefing-critiqueInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
| Category | Trigger | Complexity | Source | |----------|---------|------------|--------| | audit | "critique drafts", "check briefing compliance", "verify content fidelity", "QA against briefing" | High | az-sitebuilder |
Systematically compare generated draft .plain.html pages against a source briefing document to detect copy errors, missing content, wrong block types, image mismatches, and structural problems. Also verifies published AEM content against the briefing to catch post-upload drift. Produces a severity-rated findings report with fix suggestions. Auto-detects fidelity level from the briefing's own instructions to avoid false positives.
upload-to-da.sh as a final quality gateLocate the briefing and draft files for the target site:
sites/{sitename}/briefing.md or sites/{sitename}/briefing.docx
.docx files, extract text content (use a tool or read what's available).md files, read directlydrafts/{sitename}/*.plain.html filesRead the briefing thoroughly and detect three independent fidelity dimensions by examining the briefing's own instructions and structure:
| Dimension | Strict | Flexible |
|-----------|--------|----------|
| Copy | Briefing says "must be used exactly as written", "verbatim", "MLR-approved", or provides precise copy per section | No such instruction; briefing provides descriptions, outlines, or topic summaries |
| Block types | Briefing specifies block types explicitly (e.g., **Block:** hero-teaser) per section | Briefing describes content intent ("three cards", "image left text right") without naming blocks |
| Image mapping | Briefing specifies exact image filenames per section (e.g., Image: hero-home.jpeg) | Briefing gives general image descriptions without specific filenames |
The detected fidelity level determines what subsequent steps check and how strictly they check it:
State the detected fidelity level at the top of the report with evidence (quote the briefing text that determined each dimension).
Compare the set of expected pages (from the briefing's page/sitemap list) against the actual files found in drafts/{sitename}/.
| Check | Severity |
|-------|----------|
| Page listed in briefing but no corresponding .plain.html file | CRITICAL |
| .plain.html file exists with no corresponding briefing entry | LOW |
| nav.plain.html missing | CRITICAL |
| footer.plain.html missing | CRITICAL |
List all expected pages and their found/missing status. Include nav.plain.html and footer.plain.html in the inventory.
/{sitename}/ prefix<img src> uses CDN URL (https://{sitename}-images.pages.dev/)href in nav/footer that equals /{sitename}/ and is NOT the logo home link or Login CTA is a likely unfilled placeholderFlag any mismatch with severity HIGH.
For each page, compare the structural layout between briefing and draft:
highlight, light, or other style variants match what the briefing specifies?Some briefing sections describe the content intent rather than naming a block explicitly (e.g., "Hero (full-width image with text overlay)" instead of "Block: hero-teaser"). In strict mode, infer the block type from the description using this mapping before flagging a mismatch:
| Briefing description pattern | Inferred block type |
|------------------------------|---------------------|
| "Hero", "full-width image with text overlay" | hero-teaser |
| "Page header (text only, no image)" | title |
| "centred text", "centered text" | introduction |
| "two-column: image left, text right" or "image right, text left" | columns-teaser |
| "equal columns", "card grid", "three cards" | cards-teaser |
| "data table", "comparison table" | table-data |
| "tabs", "tabbed content" | tabs-large |
| "expandable sections", "accordion" | accordion |
Only flag a block type mismatch if neither the explicit name nor the inferred name matches the draft.
| Finding | Severity | |---------|----------| | Missing content section | HIGH | | Wrong block type (strict mode) | MEDIUM | | Block type could be better (flexible mode) | LOW | | Wrong section-metadata style | MEDIUM | | Sections in wrong order | MEDIUM | | Missing metadata block | MEDIUM |
This is the critical step. The approach depends on the detected fidelity level.
NEVER compare copy by reading — always run the diff script below. Visual comparison misses single-word deletions that change clinical meaning (e.g., "adults and adolescents" → "adults" narrows the approved indication by one deleted word). The script catches every difference mechanically.
Run this script for each content page (excluding nav/footer). Replace {sitename} with the actual site name:
python3 << 'PYEOF'
import html, re, glob, difflib, json
SITENAME = "{sitename}"
def normalize(text):
"""Strip HTML tags, decode entities, collapse whitespace."""
text = re.sub(r'<[^>]+>', ' ', text)
text = html.unescape(text)
text = re.sub(r'\s+', ' ', text).strip()
return text
# --- Extract briefing paragraphs keyed by page ---
with open(f'sites/{SITENAME}/briefing.md') as f:
briefing = f.read()
# Split into page sections
page_sections = re.split(r'## PAGE \d+:\s*', briefing)[1:]
page_labels = re.findall(r'## PAGE \d+:\s*(.+)', briefing)
findings = []
for label, section in zip(page_labels, page_sections):
# Extract text fields from briefing section
fields = re.findall(
r'\*\*(?:Heading|Body text|Body text \(continued\)|Card heading|Card body|Card link):\*\*\s*(.+?)(?=\n\n|\n\*\*[A-Z]|\Z)',
section, re.DOTALL
)
briefing_texts = []
for f in fields:
t = f.strip().split(' → ')[0] # strip CTA arrow targets
t = t.replace('\\\n', ' ').replace('\\', '')
t = normalize(t)
if len(t) > 15:
briefing_texts.append(t)
# Determine draft filename from page label
slug_map = {}
url_match = re.search(r'\*\*URL path:\*\*\s*/\w+/(\S*)', section)
slug = url_match.group(1) if url_match and url_match.group(1) else 'index'
draft_path = f'drafts/{SITENAME}/{slug}.plain.html' if slug else f'drafts/{SITENAME}/index.plain.html'
try:
with open(draft_path) as df:
draft_html = df.read()
except FileNotFoundError:
findings.append({"page": slug, "severity": "CRITICAL", "msg": f"Draft file not found: {draft_path}"})
continue
# Extract text from draft paragraphs and headings
draft_paras = re.findall(r'<(?:p|h[1-6])(?:\s[^>]*)?>(.+?)</(?:p|h[1-6])>', draft_html, re.DOTALL)
# Also extract table cell text
draft_cells = re.findall(r'<div>([^<]+)</div>', draft_html)
all_draft = [normalize(p) for p in draft_paras + draft_cells if len(normalize(p)) > 10]
# For each briefing paragraph, find the best match in the draft
for bt in briefing_texts:
best_ratio = 0
best_draft = ""
for dt in all_draft:
r = difflib.SequenceMatcher(None, bt, dt).ratio()
if r > best_ratio:
best_ratio = r
best_draft = dt
if best_ratio >= 0.85 and best_ratio < 1.0:
# Near-match: show what changed
findings.append({
"page": slug or "index", "severity": "CRITICAL",
"briefing": bt[:300], "draft": best_draft[:300],
"ratio": f"{best_ratio:.3f}"
})
elif best_ratio < 0.85:
# No close match found — paragraph may be missing
findings.append({
"page": slug or "index", "severity": "CRITICAL",
"msg": f"Briefing text not found in draft (best match ratio: {best_ratio:.2f})",
"briefing": bt[:300], "draft": best_draft[:300] if best_draft else "(none)"
})
if findings:
print(f"COPY DIFF FINDINGS ({len(findings)}):\n")
for f in findings:
print(f"[{f['severity']}] {f['page']}")
if 'msg' in f:
print(f" {f['msg']}")
if 'briefing' in f:
print(f" BRIEFING: {f['briefing']}")
if 'draft' in f:
print(f" DRAFT: {f['draft']}")
print()
else:
print("ALL COPY MATCHES — no differences found between briefing and drafts.")
PYEOF
Review the script output. Each finding shows the exact briefing text and draft text side by side. Classify findings using these rules:
Important exclusions — do NOT flag these as copy differences:
<strong> wrapping on CTAs (this is structural markup per block conventions)<em> wrapping for emphasis in block markup<a href> wrapping around text<br> tags within paragraphs<ul><li> vs paragraph text)' vs ') — these are rendering-equivalent| Finding | Severity | |---------|----------| | Clinical data differs (numbers, percentages, p-values) | CRITICAL | | Drug name or dosing information differs | CRITICAL | | Body copy text differs from briefing | CRITICAL | | Heading text differs from briefing | HIGH | | CTA text differs from briefing | HIGH | | Table cell content differs | CRITICAL |
The briefing may contain contradictions between its own brand rules and the actual section copy. For example, a brand rule may say "use ™ on first mention per page" but the briefing's section copy for a specific page omits the ™ from the heading and places it in the body text instead.
In strict mode, the explicit section copy always takes precedence over general brand rules. The draft is correct to reproduce the section copy verbatim, even if that copy violates a brand rule stated elsewhere in the briefing.
When such contradictions are detected:
| Finding | Severity | |---------|----------| | Draft copy matches briefing section but contradicts briefing brand rule | LOW (briefing inconsistency) |
Check that all key messages, topics, and data points from the briefing appear somewhere in the draft:
| Finding | Severity | |---------|----------| | Clinical data missing or differs | CRITICAL | | Key topic completely absent from draft | HIGH | | Data point present but rephrased — acceptable | Not flagged |
Compare images by filename (the last path segment of the URL), NOT by full URL. DA reprocesses images and assigns new URLs on the published site, so full URL comparison produces false positives.
For each section where the briefing specifies an image:
hero-home.jpeg)<img src> and <source srcset> attributes in the corresponding draft sectionCDN reachability check (CRITICAL):
After filename matching, verify that the CDN actually serves the images. Sample 3–5 unique image URLs from the drafts and curl -sI each one. If any return non-200, the CDN deployment is missing or incomplete — flag as CRITICAL.
# Sample image URLs and verify CDN returns HTTP 200
for url in $(grep -oh 'https://[^"]*\.jpeg' drafts/{sitename}/*.plain.html | sort -u | head -5); do
code=$(curl -sI -o /dev/null -w '%{http_code}' "$url")
[ "$code" != "200" ] && echo "CRITICAL: $url returns $code"
done
This catches the case where image filenames are correct in the HTML but the CDN was never deployed — the most common cause of broken images in DA.
Additional image checks across all pages:
| Check | Severity |
|-------|----------|
| CDN image URL returns non-200 (CDN not deployed) | CRITICAL |
| Briefing image filename not found in draft section | HIGH |
| No image in a section that expects one | HIGH |
| Local image path (src="/..." not using CDN) | CRITICAL |
| about:error in any image src | CRITICAL |
| Alt text significantly differs from briefing | LOW |
| Image present but for wrong section | HIGH |
Check each page's metadata block (the last section containing Title and Description):
| Finding | Severity | |---------|----------| | Missing metadata block | MEDIUM | | Title doesn't match briefing | LOW | | Description doesn't match briefing | LOW |
These checks span all pages in the site:
Accordion consistency: The prescribing information / important safety information accordion should be identical (or near-identical) across all pages that include it. Diff the accordion content between pages and flag differences.
Nav link list matches briefing nav section: Compare the links in nav.plain.html against the briefing's navigation section (not the full page inventory). If the briefing explicitly lists which pages appear in main navigation, that is authoritative. Extra pages in the nav that aren't in the briefing's nav spec should be flagged as MEDIUM. Missing pages that the briefing's nav spec lists should be flagged as HIGH.
No local image paths: Run grep -rn 'src="/' drafts/{sitename}/ --include='*.html' — should return nothing. Any matches are CRITICAL.
No about:error: Run grep -rn 'about:error' drafts/{sitename}/ --include='*.html' — should return nothing. Any matches are CRITICAL.
Consistent CDN base URL: All image URLs should use https://{sitename}-images.pages.dev/ as the base. Flag any image using a different CDN or domain.
CDN reachability: Sample 3–5 unique image URLs from the drafts and curl -sI each one. If any return non-200, the CDN deployment is missing or incomplete. Flag as CRITICAL — this means every image on the site will be broken in DA/production.
Internal link validity: All <a href="/{sitename}/..."> links should point to pages that exist in the drafts folder.
After validating local drafts, check whether the published AEM content still matches the briefing. Content can drift after upload if someone edits directly in DA. This step is not optional — it catches changes that are invisible in the local drafts.
remote_url=$(git remote get-url origin 2>/dev/null)
owner=$(echo "$remote_url" | sed -E 's#.*/([^/]+)/[^/]+(\.git)?$#\1#')
repo=$(echo "$remote_url" | sed -E 's#.*/([^/]+)(\.git)?$#\1#')
branch=$(git branch --show-current)
echo "AEM preview: https://${branch}--${repo}--${owner}.aem.page"
IMPORTANT — URL pattern: The home page endpoint is /{sitename}/index.plain.html, NOT /{sitename}/.plain.html. The latter returns 404 even when the page exists.
Check each page independently. Do NOT skip all pages because one returns 404.
# Check each page individually — use index.plain.html for the home page
AEM_BASE="https://{branch}--{repo}--{owner}.aem.page/{sitename}"
for page in index luminos-data safety dosing how-treluxia-works resources nav footer; do
code=$(curl -sI -o /dev/null -w '%{http_code}' "${AEM_BASE}/${page}.plain.html")
echo "$code $page"
done
If a page returns 200, include it in the drift check. If it returns non-200, note it as LOW but continue checking the others. Only skip this entire step if every single page returns non-200.
Run this script to compare published AEM content against both the briefing AND the local drafts. It reuses the same normalization and matching logic as Step 5 but fetches from AEM instead of reading local files. Replace {sitename} and the AEM base URL:
python3 << 'PYEOF'
import html, re, difflib, subprocess
SITENAME = "{sitename}"
AEM_BASE = "https://{branch}--{repo}--{owner}.aem.page/{sitename}"
# Pages to check: (slug, draft_filename)
PAGES = [
("index", "index"),
("luminos-data", "luminos-data"),
("safety", "safety"),
("dosing", "dosing"),
("how-treluxia-works", "how-treluxia-works"),
("resources", "resources"),
]
def normalize(text):
text = re.sub(r'<[^>]+>', ' ', text)
text = html.unescape(text)
text = re.sub(r'\s+', ' ', text).strip()
return text
def fetch_aem(slug):
"""Fetch AEM page, return None if not available."""
r = subprocess.run(
['curl', '-sL', '-w', '\\n%{http_code}', f'{AEM_BASE}/{slug}.plain.html'],
capture_output=True, text=True, timeout=15
)
lines = r.stdout.rsplit('\\n', 1)
body = lines[0] if len(lines) > 1 else r.stdout
code = lines[-1].strip() if len(lines) > 1 else '000'
if not code.startswith('2') or 'Page not found' in body[:200]:
return None
return body
def extract_text_paragraphs(html_content):
"""Extract text from <p>, <h1-6>, and <div> cells, normalized."""
paras = re.findall(r'<(?:p|h[1-6])(?:\s[^>]*)?>(.+?)</(?:p|h[1-6])>', html_content, re.DOTALL)
cells = re.findall(r'<div>([^<]+)</div>', html_content)
return [normalize(p) for p in paras + cells if len(normalize(p)) > 10]
# --- Read briefing ---
with open(f'sites/{SITENAME}/briefing.md') as f:
briefing = f.read()
page_sections = re.split(r'## PAGE \d+:\s*', briefing)[1:]
page_labels = re.findall(r'## PAGE \d+:\s*(.+)', briefing)
# Build briefing text per page slug
briefing_by_slug = {}
for label, section in zip(page_labels, page_sections):
url_match = re.search(r'\*\*URL path:\*\*\s*/\w+/(\S*)', section)
slug = url_match.group(1) if url_match and url_match.group(1) else 'index'
fields = re.findall(
r'\*\*(?:Heading|Body text|Body text \(continued\)|Card heading|Card body):\*\*\s*(.+?)(?=\n\n|\n\*\*[A-Z]|\Z)',
section, re.DOTALL
)
texts = []
for f in fields:
t = f.strip().replace('\\\n', ' ').replace('\\', '')
t = normalize(t)
if len(t) > 15:
texts.append(t)
briefing_by_slug[slug] = texts
findings = []
unreachable = 0
for slug, _ in PAGES:
aem_html = fetch_aem(slug)
if aem_html is None:
unreachable += 1
findings.append({"page": slug, "severity": "LOW", "type": "unreachable",
"msg": f"AEM page not reachable (not yet uploaded/previewed)"})
continue
aem_texts = extract_text_paragraphs(aem_html)
# --- 9c: Compare AEM vs briefing ---
for bt in briefing_by_slug.get(slug, []):
best_ratio = 0
best_aem = ""
for at in aem_texts:
r = difflib.SequenceMatcher(None, bt, at).ratio()
if r > best_ratio:
best_ratio = r
best_aem = at
if best_ratio >= 0.85 and best_ratio < 1.0:
findings.append({
"page": slug, "severity": "CRITICAL", "type": "aem-vs-briefing",
"briefing": bt[:300], "aem": best_aem[:300], "ratio": f"{best_ratio:.3f}"
})
# --- 9d: Compare AEM vs local draft ---
try:
with open(f'drafts/{SITENAME}/{slug}.plain.html') as df:
local_html = df.read()
local_texts = extract_text_paragraphs(local_html)
# Compare each local paragraph against AEM
for lt in local_texts:
best_ratio = 0
best_aem = ""
for at in aem_texts:
r = difflib.SequenceMatcher(None, lt, at).ratio()
if r > best_ratio:
best_ratio = r
best_aem = at
if best_ratio >= 0.85 and best_ratio < 1.0:
findings.append({
"page": slug, "severity": "HIGH", "type": "aem-vs-draft",
"local": lt[:300], "aem": best_aem[:300], "ratio": f"{best_ratio:.3f}"
})
except FileNotFoundError:
pass
# --- Report ---
if unreachable == len(PAGES):
print("AEM DRIFT CHECK SKIPPED — no pages reachable on AEM preview.")
else:
real_findings = [f for f in findings if f["type"] != "unreachable"]
if real_findings:
print(f"AEM DRIFT FINDINGS ({len(real_findings)}):\n")
for f in real_findings:
print(f"[{f['severity']}] [AEM] {f['page']} ({f['type']})")
if 'briefing' in f:
print(f" BRIEFING: {f['briefing']}")
if 'local' in f:
print(f" LOCAL: {f['local']}")
if 'aem' in f:
print(f" AEM: {f['aem']}")
if 'msg' in f:
print(f" {f['msg']}")
print()
else:
reachable = len(PAGES) - unreachable
print(f"NO AEM DRIFT — all {reachable} reachable pages match the briefing and local drafts.")
if unreachable > 0 and unreachable < len(PAGES):
unreachable_pages = [f['page'] for f in findings if f['type'] == 'unreachable']
print(f"\nNote: {unreachable} page(s) not reachable on AEM: {', '.join(unreachable_pages)}")
PYEOF
Review the script output. Each finding is pre-classified:
| Finding | Severity | |---------|----------| | Published text differs from briefing (strict mode) | CRITICAL | | Published clinical data differs from briefing | CRITICAL | | Published content differs from local draft (text drift detected) | HIGH | | Published page not reachable (not yet uploaded) | LOW |
Report labelling: Prefix all findings from this step with "[AEM]" in the location field to distinguish them from local draft findings. For example: [AEM] index → Section 1 (hero-teaser) → Heading.
The interactive report requires a small overlay script in the project's scripts/delayed.js. Check if it already exists — if not, append the contents of references/critique-overlay.js to scripts/delayed.js.
The overlay activates only when ?critique-section or ?critique-target query params are present in the URL. It has zero impact on production pages.
It supports three targeting modes:
?critique-section=N — highlights the Nth <main> .section (1-indexed)?critique-target=header — highlights the <header> element?critique-target=footer — highlights the <footer> element?critique-label=TEXT — optional label shown as a tag on the highlighted elementWrite the report to sites/{sitename}/critique.html. Read the template from references/report-template.html and populate all {{PLACEHOLDER}} values with the actual findings data.
| Placeholder | Value |
|-------------|-------|
| {{SITENAME}} | Site folder name (e.g., zenturis) |
| {{DRUG_BRAND}} | Brand name from briefing (e.g., Zenturis) |
| {{DRUG_GENERIC}} | Generic name from briefing (e.g., zenturigliptin) |
| {{BRIEFING_PATH}} | Relative path to briefing (e.g., sites/zenturis/briefing.docx) |
| {{DATE}} | Current date |
| {{COPY_FIDELITY}}, {{BLOCKS_FIDELITY}}, {{IMAGES_FIDELITY}} | Strict or Flexible |
| {{COPY_EVIDENCE}}, {{BLOCKS_EVIDENCE}}, {{IMAGES_EVIDENCE}} | Quote from briefing that determined the fidelity level |
| {{CRITICAL_COUNT}}, {{HIGH_COUNT}}, {{MEDIUM_COUNT}}, {{LOW_COUNT}} | Integer counts |
| {{VERDICT_CLASS}} | pass, needs-fixes, or blocked |
| {{VERDICT_TEXT}} | Verdict text with summary |
| {{AEM_PREVIEW_URL}} | AEM preview base URL (e.g., https://main--az-sitebuilder--paolomoz.aem.page) or "N/A" if not available |
Set the .value div class based on the detected fidelity level:
badge-strict (red) for strict dimensionsbadge-flexible (green) for flexible dimensionsEach finding is a clickable <div class="finding"> with data attributes for preview targeting:
<div class="finding" data-page="PAGE" data-section="N" data-label="SHORT LABEL">
<div class="location">PAGE → Section N (BLOCK)</div>
<div class="description">DESCRIPTION</div>
<div class="diff">
<div>
<div class="diff-label">Expected</div>
<div class="diff-expected">BRIEFING TEXT</div>
</div>
<div>
<div class="diff-label">Actual</div>
<div class="diff-actual">DRAFT TEXT</div>
</div>
</div>
<div class="fix">FIX SUGGESTION</div>
<div class="view-hint">Click to view in preview →</div>
</div>
Preview targeting rules:
data-page="pageslug" + data-section="N" (1-indexed)data-page="index" + data-target="header" (shows nav in context of home page)data-page="index" + data-target="footer" (shows footer in context of home page)Each <tr> has data-page="pageslug" to make the row clickable. Use these classes:
class="match" (green) for passing countsclass="mismatch" (orange) for failing countsclass="status-pass" / class="status-warn" / class="status-fail"Each <li> uses one of: class="check-pass", class="check-warn", class="check-fail"
The report starts full-width (centered, comfortable reading). When the user clicks any finding card or page table row, the report panel animates to 440px on the left and the preview iframe slides in from the right, loading the relevant page from http://localhost:3000 with the critique overlay highlighting the affected section. A close button collapses the preview back to full-width.
| Verdict | Class | Condition |
|---------|-------|-----------|
| PASS | pass | Zero CRITICAL and zero HIGH |
| NEEDS FIXES | needs-fixes | Zero CRITICAL, but HIGH or MEDIUM exist |
| BLOCKED | blocked | One or more CRITICAL |
| Severity | Criteria |
|----------|----------|
| CRITICAL | Copy text differs from briefing (strict mode); clinical data wrong; missing page; local image path; about:error; published AEM content differs from briefing (strict mode) |
| HIGH | Missing image; wrong image filename; missing content section; wrong link target; key topic missing (flexible mode); published content drifted from local drafts |
| MEDIUM | Wrong block type (strict mode); wrong section-metadata style; wrong link text; missing metadata |
| LOW | Extra file not in briefing; metadata mismatch; alt text differs; block type could be better (flexible mode); published page not reachable |
After writing the file, open it in the browser and tell the user the path. Remind them the preview iframe requires the dev server running at localhost:3000.
| Problem | Cause | Fix |
|---------|-------|-----|
| Too many CRITICAL copy findings | Briefing has HTML entities, drafts have decoded characters | Ensure normalization in Step 5 decodes all HTML entities before comparison |
| False positives on <strong> text | CTA text wrapped in <strong> per block conventions | Step 5 explicitly excludes structural markup wrapping from copy diffs |
| Image comparison flags every image | Comparing full URLs instead of filenames | Step 6 specifies filename-only comparison (last path segment) |
| Briefing is .docx and unreadable | Binary Word document | Extract text using available tools, or ask user to provide a text/markdown version |
| Accordion diffs across pages | Pages were generated independently | Flag in Step 8 cross-page checks; provide the specific diff |
| Entity encoding differences (≥ vs ≥) | Briefing uses HTML entities, draft uses Unicode | Normalization handles this — decode entities in both sources before comparing |
| Table cell ordering differs | Table rows or columns reordered in draft | Compare cell content regardless of position within the same table; flag if data is missing entirely |
| Published content check flags every image URL | DA rewrites image URLs to ./media_xxx paths during upload | Step 9c excludes image src/srcset from text comparison — only compare text content |
| Published content returns 404 for home page only | Used /.plain.html instead of /index.plain.html | The home page endpoint is /{sitename}/index.plain.html — see Step 9b URL pattern note |
| Published content returns 404 for all pages | Site not yet uploaded/previewed on AEM | Step 9 skips the published check and notes it in the report |
| Published content differs from local draft but matches briefing | Someone fixed an error directly in DA | Flag as HIGH (draft-to-published drift) so local drafts can be re-synced |
| False positives from DA whitespace changes | DA normalizes whitespace differently | Apply aggressive whitespace normalization (collapse all runs, trim) before comparing |
development
Generate artistic infographics from any topic. Runs the Sumi pipeline (analyze → structure → craft prompt → generate image) entirely within Claude Code. Use when "generate infographic", "create infographic", "sumi", "make an infographic about", or "visualize topic".
tools
Implement Server-Sent Events streaming from Cloudflare Workers to browser clients with reconnection, state persistence, and progress tracking. Use when building "SSE streaming", "real-time updates", "server push", or "event streaming".
development
Audit websites by cross-referencing query indexes, sitemaps, and navigation to identify content gaps, stale pages, missing metadata, and quality issues. Use when "auditing a website", "finding content gaps", "site quality audit", or "content inventory analysis".
data-ai
Track user session context across multi-turn interactions using browser sessionStorage and server-side KV caching with TTL. Use when implementing "session tracking", "conversation context", "multi-turn sessions", or "user journey tracking".