Skip to content

OG Image System — Azure Foundry Generator + Brand Direction

What this is. The full playbook for aguidetocloud.com's OG/cover image system — the Azure Foundry infrastructure, the Python wrapper, the scene library, and the open creative direction question that the next session is meant to brainstorm fresh.

Sibling doc. blog-cover-prompt.md — the "paste-into-Designer/ChatGPT" recipe from yesterday's session. That doc is the manual-workflow companion to this one. Both will be re-aligned once the next session decides the creative direction.

Status (13 May 2026 PM — NZST). 🟢 V3-blog SHIPPED. Both Tier 1 surfaces are now locked AND live: - Tier 1a — Cert / Practice / Tool / Section landings = V3 dark-BG + family-band (§ 6, prototype lives in session-state; Phase 1 cert wire-up still pending). - Tier 1b — Blog covers = V3-blog "B2 Editorial-Light" SHIPPED in production at aguidetocloud-revamp/scripts/og-generator-blog/. 18 covers replaced 13 May 2026 PM. § 6.11 has the full spec + format rules + automation contract.

🔴 HARD RULE — JPG output format for ALL OG covers (set 13 May 2026). Every OG generator (existing Python + new Node) MUST output JPG @ q=85 with mozjpeg encoder + 4:4:4 chroma subsampling + stripped metadata. Yields ~25-35 KB per cover (vs ~70 KB legacy), preserves sharp text edges on coloured accents (4:4:4 prevents indigo-line bleed that default 4:2:0 causes), strips EXIF noise. See § 6.11 § Format-lock for the sharp().jpeg({...}) call. Apply when any legacy Python generator next gets touched.

🔒 Direction C (programmatic SVG, Node satori + resvg + sharp) is the lock for every Tier 1 OG surface. The Azure Foundry gpt-image-2 path documented in § 1-5 + § 7-9 is now ARCHIVED — kept for reproducibility / future illustrative needs but no longer the recommended pipeline for OG covers.


0. Quick-start

Tier 1 (cert pages · tool pages · section landings) — V3 programmatic SVG

Status: Prototype lives in ~/.copilot/session-state/e665659f.../files/og-c-prototype/. Production wire-up Phase 1 deferred to a fresh focused session per Sush's 12 May 2026 PM decision.

# Pre-req (one-time): the prototype has node_modules + fonts + lotus PNGs already
cd C:\Users\ssutheesh\.copilot\session-state\e665659f-9696-464e-a0ad-021a21f18c03\files\og-c-prototype
node make-og.mjs
# Generates 18 prototype covers in ~3s into ./out/ (6 fixtures × 3 layout variants V1/V2/V3)
# Locked variant: V3 (dark BG + pastel band + ink lotus). See § 6 for full spec.

Once production wire-up Phase 1 lands (separate session — see § 6.7):

cd C:\ssClawy\aguidetocloud-revamp
npm run build:og:v3        # renders all certs from manifest.json

Tier 1b (blog covers) — V3-blog "B2 Editorial-Light" — SHIPPED 13 May 2026

Status: 🟢 LIVE. 18 covers in production at aguidetocloud-revamp/static/images/og/blog/. Generator at aguidetocloud-revamp/scripts/og-generator-blog/. Full spec in § 6.11.

cd C:\ssClawy\aguidetocloud-revamp
npm run build:og:blog            # regenerates only changed posts (hash-cached)
npm run build:og:blog:dry        # preview what would generate, no writes
npm run build:og:blog:force      # bypass cache, regenerate all 18

Every content/blog/*.md MUST have og_headline: (3-7 word keyword fragment) in its frontmatter. Optional: og_glyph: calendar|compare|layers|list (auto-detected from card_tag + headline if absent).

Tier 2 (legacy — Azure Foundry gpt-image-2) — ARCHIVED 13 May 2026

# Required env (encoding bug workaround — see § 7 gotchas)
$env:PYTHONIOENCODING="utf-8"; $env:PYTHONUTF8="1"

# Must be logged in to LAB tenant (see § 2). Verify with:
az account show --query "{tenant:tenantId, user:user.name}" -o table
# Expected: tenant 00b98149-2e3e-468c-b063-fb0cfa35fe44
#           user admin@M365CPI52224224.onmicrosoft.com

# Generate from a named scene (v4 watercolour-notebook for blog posts)
python "C:\Users\ssutheesh\.copilot\scripts\blog-cover-gen.py" `
  --scene tier1-v4-az900-study-guide `
  --output ".\some-image.png" `
  --quality medium `
  --size 1536x1024

Or pass a raw prompt as the --scene value to skip the named-scene library.


1. Why this exists

Yesterday's blog-cover-prompt.md session built a manual recipe: paste the prompt into Microsoft Designer / ChatGPT / Midjourney, hand-pick a winner, save. Workable but slow at scale.

This session asked: can we eliminate the manual step using Azure? Sush has Azure access and a Lab tenant. Foundry hosts the same families of image models that ChatGPT and Midjourney use — gpt-image-2, FLUX.1 [pro], Stable Diffusion. If one of them produces output we're happy with, we can:

  • Generate covers programmatically for every blog post on push
  • Generate covers for the rest of the site too — ~50 cert pages × 2 product types (study guide / practice exam), tool pages, section landings. Order of magnitude ~170+ covers at ~$0.07 each = ~$12 to brand the entire OG layer.

The cost calculus only works because we're not paying ChatGPT Plus or Midjourney. Lab credits cover the prototype phase.


2. Azure infrastructure (what's running, what's the auth model)

Tenant + sub

Field Value
Tenant M365CPI52224224.onmicrosoft.com (id 00b98149-2e3e-468c-b063-fb0cfa35fe44) — Sush's Lab tenant
Subscription ME-M365CPI52224224-ssutheesh-1 (id 96879ea6-389e-417f-a3a2-16c415a2b6b5)
Resource group ssClawy
Signed-in user admin@M365CPI52224224.onmicrosoft.com (objId 87bc8b84-b22e-4ca2-917c-1d5ed1dda6e7) — generic "MOD Administrator"

The resource

Field Value
Resource clawy-images-openai (Cognitive Services / Azure OpenAI account)
Region eastus2
SKU S0 (standard)
Endpoint https://clawy-images-openai.openai.azure.com/

The deployment

Field Value
Deployment name gpt-image-2
Model gpt-image-2, version 2026-04-21
SKU GlobalStandard, capacity 1
API version 2025-04-01-preview

Auth model — Entra ID ONLY

⚠️ Lab tenant has disableLocalAuth=true policy. API keys are disabled by org policy. You MUST use DefaultAzureCredential over Entra ID. The wrapper handles this via:

token_provider = get_bearer_token_provider(
    DefaultAzureCredential(),
    "https://cognitiveservices.azure.com/.default",
)

client = AzureOpenAI(
    api_version=API_VERSION,
    azure_endpoint=ENDPOINT,
    azure_ad_token_provider=token_provider,  # NOT api_key
)

The Lab user has been granted the Cognitive Services OpenAI User role on this resource. To re-grant if needed:

az role assignment create `
  --assignee 87bc8b84-b22e-4ca2-917c-1d5ed1dda6e7 `
  --role "Cognitive Services OpenAI User" `
  --scope /subscriptions/96879ea6-389e-417f-a3a2-16c415a2b6b5/resourceGroups/ssClawy/providers/Microsoft.CognitiveServices/accounts/clawy-images-openai

MCP tools cannot reach this

The MCP azure-foundry / azure-cognitiveservices tools auth as Sush's corp identity (ssutheesh@microsoft.com), which has zero access to the Lab tenant. All Azure work touching this resource must go through the powershell tool with az CLI (Sush's az login cache is auth'd to Lab).

Secret files

~/.copilot/secrets/azure-openai-images-endpoint    # https://clawy-images-openai.openai.azure.com/
~/.copilot/secrets/azure-openai-images-deployment  # gpt-image-2

No API key file — auth is Entra ID, no key to store.


3. The wrapper script

Path: C:\Users\ssutheesh\.copilot\scripts\blog-cover-gen.py

What it does. Takes a named scene OR raw prompt, joins it with the LOCKED_STYLE block, calls gpt-image-2, decodes the base64 response, writes a PNG.

Anatomy:

Lines Block Notes
28-30 Endpoint + deployment + API version constants Edit if region or model changes
32-50 LOCKED_STYLE modular prompt BRAND FRAME / LOCKED CONSTRAINTS / COMPOSITION blocks — the "brand container" that every image shares
52-72 NAMED_SCENES dict Library of scenes by tier and brand-version. Currently 3 legacy (v1 Darren-template), 3 brand-system v2 (dense Tier 2), 2 v3 Tier 1 (cert covers), 2 v4 Tier 1 (bold ink fix)
74-80 build_prompt() Concats LOCKED_STYLE + scene
83-117 generate() Hits the API, base64-decodes, writes file
120-148 CLI entry point Argparse — supports --scene, --output, --quality, --size

Invocation patterns:

# Named scene, named output
python "C:\Users\ssutheesh\.copilot\scripts\blog-cover-gen.py" `
  --scene tier1-v4-az900-study-guide `
  --output ".\my-image.png" `
  --quality medium

# Raw scene text, fast iteration
python "C:\Users\ssutheesh\.copilot\scripts\blog-cover-gen.py" `
  --scene "A glowing lightbulb in the top-left with arrows flowing to a laptop..." `
  --output ".\test.png" `
  --quality low

# Output directory + scene = auto-named file (og-<scene>.png inside dir)
python "C:\Users\ssutheesh\.copilot\scripts\blog-cover-gen.py" `
  --scene notebook-licensing-cafe `
  --output ".\out\"

Always set $env:PYTHONIOENCODING="utf-8"; $env:PYTHONUTF8="1" before invoking — see § 7 gotchas.


4. Cost & speed economics

Quality $/image Wall time When to use
low ~$0.02 ~30-60s Throwaway sanity checks / quick comp tests
medium ~$0.07 ~75-190s Default for all iteration. All v2/v3/v4 prototypes used this.
high ~$0.19 ~150-220s Final renders of locked winners only

Rule: never iterate at high. You'll spend twice the money to discover the same design problem at twice the wait. Iterate at medium, regenerate winners at high.

Scale arithmetic (whole-site OG layer):

  • ~170 covers × $0.07 medium = ~$12 to brand the entire site at iteration quality
  • ~170 covers × $0.19 high = ~$32 if every final is at top quality
  • Total runway on a single Visual Studio Enterprise personal credit ($150/mo): comfortable, with room for re-runs.

Output size. Default 1536×1024 PNG comes out at 2.7-3.0 MB. Way too heavy for shipping. Need a downscale+compress post-step (PIL: resize to 1200×675, save JPEG quality 85, expect ~150-300 KB) before any image lands in the aguidetocloud-revamp repo. Not built yet.


5. Iteration history (what was tested and why)

v1 — Darren-template baseline ("vibe-crafting") · 11 May 2026

  • Prompt copied directly from yesterday's blog-cover-prompt.md Example 1
  • Generated at high quality (the only run that did, before discovering medium is good enough for iteration)
  • 216s, 2.7 MB PNG
  • Sush reaction: "looks good! Much better." ✅ — beat the Microsoft Designer / Copilot Create attempts from yesterday
  • But: too close to Darren's style, not yet a Sush-owned visual identity

v2 — Brand-system "Notebook frame" Tier 2 (dense spreads) · 12 May 2026

  • Three different topics generated to stress-test the system (vibe-crafting / licensing-cafe / curriculum-path)
  • Brand frame: open notebook page filling 75-85% of frame, paper grain, ring binding, pencil at edge, "AGTC field note" stamp, indigo accent on 1-2 elements
  • All three landed coherent — the frame worked as a brand container
  • But: too dense for OG / thumbnail use. Dense imagery is the enemy of clicks at 246×138 px. Sush flagged this — the v2 set is good for inside-blog explainer art, not for covers.

v3 — Brand-system Tier 1 (cert covers — first attempt at thumbnail-scale) · 12 May 2026

  • Generated tier1-az900-study-guide (peach palette, open book, indigo bookmark) and tier1-az900-practice-exam (pink palette, exam paper with mock questions, indigo checkmarks)
  • The PATTERN was right: Hero ("AZ-900") + supporting visual + palette accent (peach vs pink) + locked brand frame
  • But: washed out. Hero text used pastel watercolour on pastel paper = no contrast. Supporting visual was too prominent. Subtitle was too small.

v4 — Bold ink typography fix · 12 May 2026

  • Same pattern, but:
  • Hero in deep charcoal/ink, occupies upper 40% of page, instantly readable at thumbnail size
  • Subtitle in deep charcoal, ~40% of hero size, all-caps, also bold
  • Supporting visual demoted to a small corner pencil-sketch (4-6 strokes, suggestion not illustration) — saves prompt budget for the typography to dominate
  • ~60% of page is negative breathing space
  • Sush paused here — wants a fresh-perspective session to question the watercolour direction itself rather than continue tweaking.

Files

All test outputs at: C:\Users\ssutheesh\.copilot\session-state\cd9c07ae-62b0-4fb1-a8ab-f149e5c99881\files\og-test\

File Tier Version Notes
og-vibe-crafting.png (OG) v1 Darren-template baseline — Sush approved as "much better"
v2-notebook-vibe-crafting.png T2 spread v2 Dense, multi-station, indigo accent — good for inside-blog
v2-notebook-licensing-cafe.png T2 spread v2 Café menu metaphor, indigo price-corrections
v2-notebook-curriculum-path.png T2 spread v2 Two-page transit map, five stations
v3-tier1-az900-study-guide.png T1 cover v3 Pastel hero — washed out
v3-tier1-az900-practice-exam.png T1 cover v3 Pastel hero — washed out, exam-paper too detailed
v4-tier1-az900-study-guide.png T1 cover v4 Bold ink hero — direction is right, but pattern paused for fresh perspective
v4-tier1-az900-practice-exam.png T1 cover v4 Bold ink hero — direction is right, but pattern paused for fresh perspective

6. ✅ LOCKED — V3 system (Direction C, programmatic SVG) — 12 May 2026 PM EVENING

Sush's previous-session question — "is watercolour too limiting, what are world-class brands doing?" — got answered with a peer-set audit (11 reference covers from 5 brands) + 18 prototype renders + rubber-duck-validated production plan. Result: watercolour deserved a category demotion. Programmatic SVG won the Tier 1 OG layer.

6.1 Peer-set audit findings (cached in session files/og-audit/)

Downloaded 11 real-world covers from peers — 6 Linear (dynamic via linear.app/api/og/generic?title=...), 1 Vercel landing, 1 Vercel per-post (OG_Card22.png from /blog/ai-gateway), 1 Stripe default, 1 Cloudflare default, 1 Microsoft Learn default cert cover.

The bombshell: Nobody in the entire peer set ships illustrated / painted / watercolour OG covers. Every world-class brand has converged on bold sans-serif typography + brand colour/gradient + tiny brand mark. Three production patterns:

Tier Pattern Who
⭐⭐⭐ Programmatic from one template (different title = different cover, generated on demand) Linear (/api/og/generic)
⭐⭐ Hand-designed per-post in CMS Vercel
Single static default for everything Stripe / Cloudflare / Microsoft Learn

Microsoft Learn specifically uses ONE generic OG image for every cert page (https://learn.microsoft.com/en-us/media/open-graph-image.png). They don't bother with per-cert covers. We leapfrog them just by having per-cert covers at all.

6.2 V3 visual system — LOCKED

+--------------------------------------------------------------+
|                                                              |
|                                                              |
|                     AZ-900    (deep ink BG #0F0F10           |
|                                white Inter ExtraBold 800     |
|                                adaptive 72→240 px            |
|                                centre-aligned)               |
|                                                              |
|                      ▬▬▬     (indigo accent line             |
|                              #6366F1, 80×6px rounded)        |
|                                                              |
|                   STUDY GUIDE (#A3A3A3 muted, Inter Bold 700 |
|                                36px, 4px letter-spacing,     |
|                                all-caps)                     |
|                                                              |
+--------------------------------------------------------------+
| aguidetocloud.com                                       🪷  | ← family-pastel band 96px
+--------------------------------------------------------------+   AZ=peach · MS=pink ·
                                                                   AI=lavender · SC=periwinkle ·
                                                                   MB=teal; dark-ink lotus 72×72
                                                                   right-aligned

Locked tokens:

Token Value Source / rationale
Hero BG #0F0F10 (warmer than pure black) Direction C lock
Hero text #FAFAFA, Inter 800, adaptive 72-240px Linear / Vercel pattern
Accent line #6366F1 (Zen --accent indigo), 80×6px rounded Zen system
Subtitle #A3A3A3 (Zen --text-muted), Inter 700, 36px, +4px tracking, all-caps V3 prototype lock
Band height 96 px Locked
Band BG Family palette (see 6.3) Brand continuity
Brand wordmark "aguidetocloud.com", Inter 600, 22px, #1A1A1A ink, band-left Linear pattern
Brand logo Dark-ink lotus, 72×72px, band-right Sush's call 12 May PM
Default fallback BG #EEF2FF Zen --accent-subtle for non-cert pages V3 prototype

6.3 Family palette mapping (locked, never change)

Family Hex Use
AZ — Azure #FFD9C7 peach All AZ-* certs (AZ-900, AZ-104, AZ-204, AZ-305, etc.)
MS — Microsoft 365 #F9C4D2 pink MS-* certs (MS-100, MS-700, MS-900, etc.)
AI — Azure AI #E1D7F0 lavender AI-* certs (AI-102, AI-200, AI-900, etc.)
SC — Security & Compliance #CFD4F0 periwinkle SC-* certs (SC-100/200/300/400/500, etc.)
MB — Dynamics 365 #C9E4DD pale teal MB-* certs
DEFAULT #EEF2FF neutral indigo-tint Non-cert pages

6.4 Architecture — Node + satori + @resvg/resvg-js

Industry standard. Vercel's library (satori), powering Linear / Vercel / Next.js / @vercel/og. Pure-JS, ~50 ms/image, deterministic, no headless browser required (lighter, faster CI than the existing Python+Playwright generator).

[manifest.json] → [Node `make-og.mjs`] → [satori → SVG] → [@resvg/resvg-js → PNG] → [static/images/og/v3/<route>.jpg]

Dependencies: satori@^0.26, @resvg/resvg-js@^2.6, sharp@^0.34 (one-time lotus extraction only). Fonts: Inter-Regular/SemiBold/Bold/ExtraBold.ttf from github.com/rsms/inter v4.0 release; bundled (~1.6 MB total).

Cost: $0 marginal, ~0.22 s/image. 170 covers in <60 seconds, $0 total. Versus gpt-image-2 medium: 75-190 s/image at $0.07 → ~3 hours + $11.90 for the same batch.

6.5 Adaptive font-sizing rules (defined PRE-implementation per rubber-duck guidance)

title length    hero font size      example
≤ 6 chars       240 px              "AZ-900", "MB-330", "SC-100"
≤ 10 chars      180 px              "MS-700", "AI-900"
≤ 16 chars      130 px              "SC-100 ARCHITECT"
≤ 28 chars       92 px              "Microsoft 365 Copilot"
> 28 chars       72 px              fallback for longer non-cert titles

Tested against worst-case fixtures ("SC-100" + "CYBERSECURITY ARCHITECT" subtitle and "Microsoft 365 Copilot" + "WAVE 2 ROUNDUP"). Both render cleanly. All 6 prototype fixtures × 3 variants = 18 covers verified at thumbnail scale.

6.6 Manifest-driven generation (per rubber-duck guidance)

The generator consumes a normalized JSON manifest, NOT raw Hugo nav. Decouples renderer from frontmatter / nav schema; prevents silent rot.

[
  { "route": "/cert-tracker/az-900/", "hero": "AZ-900", "subtitle": "STUDY GUIDE",   "family": "AZ", "output": "v3/az-900-study-guide.jpg" },
  { "route": "/practice/az-900/",     "hero": "AZ-900", "subtitle": "PRACTICE EXAM", "family": "AZ", "output": "v3/az-900-practice-exam.jpg" }
]

6.7 Production rollout — Phases 1-4

Phase 1 — AZ-900 proof + ALL certs.SHIPPED 13 May 2026 PM. Generator graduated from session-state prototype into aguidetocloud-revamp/scripts/og-generator-cert/ (NOT the originally-planned og-generator-v3/ name — og-generator-cert is clearer since it handles both study + practice). Wired into root npm run build:og:certs and npm run build:og:practice. Migrated ALL 149 cert-tracker pages (52 replaced + 97 new) + 125 live practice exam pages in two atomic commits. Replaced JPGs in-place at existing paths (images/og/certs/<slug>.jpg, images/og/practice/<slug>.jpg) — NO baseof.html template change needed, atomic switchover, zero template risk. Full spec in § 6.12.

Phase 2 — Roll out to all 50+ certs.SHIPPED 13 May 2026 PM (merged into Phase 1 above — single-shot rollout rather than gradual). 149 study guide + 125 practice OGs rendered in ~104 s combined.

Phase 3 — Tool covers.SHIPPED 13 May 2026 PM (commit a6e3447f). Adapted V3 to a tool-specific layout — T-A "Dark Tool Card" — chosen after a 3-variant prototype review (T-A dark sibling-cert / T-B light sibling-blog / T-C accent gradient). T-A wins because it keeps tools in the same charcoal family as cert/practice (so the whole "interactive/reference" half of the site shares one DNA strand, leaving the warm-white blog as the editorial half). Generator lives at aguidetocloud-revamp/scripts/og-generator-tool/. 59 tool OGs replaced in-place at static/images/og/<slug>.jpg (no Hugo template or frontmatter change needed). Full spec in § 6.13.

Phase 4 — Tier 1b (blog covers).SHIPPED 13 May 2026. B2 Editorial-Light system — warm-white BG + headline-left + line-art glyph corner-right + lotus+wordmark bottom-left. Full spec in § 6.11. Watercolour direction abandoned in favour of programmatic SVG (peer-evidence-led — Vercel/Linear/Stripe/Cloudflare all converge on light-BG editorial OGs for their blogs).

Out-of-scope (intentional): - Brain Bar (cmd) covers — keep terminal monospace aesthetic; it's product-coherent. - Cosmos planet covers — own visual language (dark space + planets).

6.8 🔴 HARD RULE — keep OG generation isolated from Hugo build

Per rubber-duck SLA review: OG generation must NOT be a hard dependency of hugo build. Any Node/WASM/font/dependency failure inside the OG generator would otherwise block production deploys — including practice exam pages, the #1 paid-product SLA surface.

Concrete: - V3 cert + practice lives at aguidetocloud-revamp/scripts/og-generator-cert/ - V3-blog lives at aguidetocloud-revamp/scripts/og-generator-blog/ - V3-tool lives at aguidetocloud-revamp/scripts/og-generator-tool/ - All three invoked via npm scripts only (build:og:certs, build:og:practice, build:og:blog; tool generator runs via cd scripts/og-generator-tool && node make.mjs — no npm script added to avoid collision with parallel sessions modifying package.json) - NOT in any package.json build pipeline that Cloudflare Pages or GitHub Actions runs - If any generator fails, existing committed JPGs in static/images/og/<surface>/ remain valid and serve from CDN

The existing Python scripts/og-generator/ already follows this rule. V3 + V3-blog do too. Verified 13 May 2026 — Hugo safe build (1964 pages, 193 s) ran clean after V3 cert rollout, generated HTML serves the right OG paths.

6.9 Current Python generator vs V3 — what carries over

The existing Python generate_cert_og.py already does many things well (Playwright + HTML template, hash-based cache busting, per-category palette, bold hero typography). The actual V3 wins:

Current Python cert OGs V3
✅ Bold dark hero typography ✅ Same
✅ Per-category accent (cyan/purple/red/green) ✅ Per-family pastel band (peach/pink/lavender/periwinkle/teal)
✅ Programmatic, hash-tracked ✅ Programmatic, manifest-driven
No brand wordmark visible aguidetocloud.com in band
No brand logo visible ✅ Ink-lotus in band
⚠️ Line-art ambient bg (disappears at 256 px) ✅ Clean, zero decoration
🐢 Playwright headless browser, ~2-3 s/image ⚡ satori pure-JS, ~0.22 s/image
Generator inside Hugo workflow Isolated npm script (SLA rule)

Migration approach (Phase 1→2): run BOTH generators side-by-side during transition. Migrate cert by cert via baseof.html conditional, then bulk-switch once stable. Never delete the Python generator until V3 has shipped and verified at scale.

6.10 Where this session's artefacts live

  • Prototype project: ~/.copilot/session-state/e665659f-9696-464e-a0ad-021a21f18c03/files/og-c-prototype/make-og.mjs + extract-lotus.mjs + fonts/ (Inter weights + lotus PNGs) + out/ (18 prototype renders: V1 full-pastel, V2 neutral+band, V3 dark+band — final lock = V3)
  • Peer-audit gallery: ~/.copilot/session-state/e665659f-9696-464e-a0ad-021a21f18c03/files/og-audit/ — 11 downloaded reference covers (Linear ×6, Vercel ×2, Stripe, Cloudflare, MS Learn)
  • V4 watercolour test images (still valid for Tier 2 blog covers): ~/.copilot/session-state/e665659f-9696-464e-a0ad-021a21f18c03/files/v*-*.png

Next session graduates the prototype from session-state into aguidetocloud-revamp/scripts/og-generator-v3/. The 18 prototype renders + peer audit stay in session-state as visual reference; they're not promoted to the repo.

13 May 2026 PM update: Prototype has graduated into production as aguidetocloud-revamp/scripts/og-generator-cert/ (clearer name than og-generator-v3/ since it handles both study + practice). Full spec in § 6.12 below.


6.12 ✅ SHIPPED — V3 cert + practice exam — 13 May 2026 PM

Mirror system to V3-blog (§ 6.11). Same JPG mozjpeg+4:4:4 format rule, same SLA isolation, same hash-cached idempotent render. Differs only in visual chassis (dark BG + family-band for product/study surfaces; warm-white + glyph for blog surfaces).

6.12.1 What shipped

Surface Source of truth Output path Coverage Subtitle
Study guide content/cert-tracker/*.md (Hugo) static/images/og/certs/<slug>.jpg 149 covers (52 replaced + 97 net new) STUDY GUIDE
Practice exam ../guided/src/content/certs/*.toml (READ-ONLY, filtered to status == "live") static/images/og/practice/<slug>.jpg 125 covers PRACTICE EXAM

Commits: 39ffd5c5 (study guide) + 52f30eb3 (practice exam) on aguidetocloud-revamp/main. Both verified live via curl after Cloudflare deploy (HTTP 200 · image/jpeg · ~25-31 KB · mozjpeg DQT magic bytes).

6.12.2 Generator location + scripts

aguidetocloud-revamp/
├── package.json                           # root npm scripts (build:og:certs / :practice)
└── scripts/og-generator-cert/
    ├── make.mjs                           # ~330 lines, handles BOTH modes via --mode flag
    ├── package.json                       # satori + resvg + sharp + gray-matter + @iarna/toml
    ├── README.md                          # one-pager for future contributors
    ├── fonts/                             # Inter Regular/SemiBold/Bold/ExtraBold + lotus-ink/white
    ├── .og-cert-cache.json                # study mode hash cache (gitignored)
    └── .og-practice-cache.json            # practice mode hash cache (gitignored)

Root npm scripts: - npm run build:og:certs--mode=study → study guide rebuild - npm run build:og:practice--mode=practice → practice exam rebuild - Both have :dry and :force variants

6.12.3 Why in-place replacement (not the originally-planned /v3/ path)

The earlier (12 May 2026 PM) playbook proposed routing V3 outputs to images/og/v3/<slug>-<surface>.jpg and updating baseof.html to point cert-tracker pages at the new path. That plan was designed for cert-by-cert gradual rollout — you'd flip pages one at a time via conditional Hugo template logic.

The 13 May 2026 PM rollout chose a single-shot atomic replacement instead: - Same output paths as legacy (images/og/certs/<slug>.jpg, images/og/practice/<slug>.jpg) - baseof.html ALREADY routes cert-tracker pages there (line 34) — zero template change required - 149 + 125 = 274 covers atomically swap from legacy to V3 - Hugo build untouched (no template risk, no SLA risk) - Rollback = git revert the commit + Cloudflare auto-deploys the previous JPGs

For surfaces with stable URL contracts and a single-shot rollout, in-place is strictly safer than /v3/ route swapping. The original plan stays valid for future cert-by-cert experiments.

6.12.4 Family palette (V3 locked, extended for non-MS vendors)

Same 5 Microsoft families as § 6.3, plus DEFAULT for everything else:

Prefix detection (cert code uppercased) Family Band hex Vendor coverage
AZ-... AZ #FFD9C7 peach Microsoft Azure
MS-... MD-... PL-... MS #F9C4D2 pink Microsoft 365 / Power Platform
AI-... DP-... AB-... AI #E1D7F0 lavender Microsoft AI / Data / Agentic Business
SC-... SC #CFD4F0 periwinkle Microsoft Security & Compliance
MB-... MB #C9E4DD pale teal Microsoft Dynamics 365
everything else DEFAULT #EEF2FF neutral indigo-tint AWS · Cisco · CompTIA · CNCF · GCP · ISACA · ISC2 · ECCouncil · Fortinet · HashiCorp · Juniper · Palo Alto

Vendor palette extension (e.g. AWS = orange, GCP = blue) deferred to a separate session per Sush's call. DEFAULT neutral is intentional brand-coherence for now.

6.12.5 Family breakdown by surface

Family Study (149) Practice (125) Net delta
AZ 12 7 5 study-only (status != live)
MS 11 6 5 study-only
AI 19 16 3 study-only
SC 8 5 3 study-only
MB 10 2 8 study-only
DEFAULT 89 89 (parity)

The ~24 study-only certs are pages without an active practice exam yet (status flagged differently than live in guided/src/content/certs/*.toml). When a cert flips to live, the next npm run build:og:practice run will pick it up automatically (hash cache skips unchanged).

6.12.6 Guided repo safety (read-only contract)

The practice mode reads ../guided/src/content/certs/*.toml as strictly read-only. Zero writes to the guided repo. Verified by: - Generator parses TOML with @iarna/toml, never writes back - Output target is aguidetocloud-revamp/static/, never guided/ - Parallel sessions doing practice exam enrichment in guided (question JSON, PracticeQuiz.tsx) operate on a totally different surface — zero conflict

If you ever need to add a cert to the practice OG set, just flip status = "live" in the guided TOML (when ready) and re-run.

6.12.7 Pre-existing SEO guardrail failure (NOT blocking)

The GitHub Actions "SEO + OG image guardrail" workflow has been failing on every commit since 33e7990c (11 May 2026) — pre-existing issue unrelated to OG image work. Specifically, one blog post (content/blog/microsoft-365-copilot-brand-kit-complete-guide.md) has title=84 chars (limit 60) + description=221 chars (limit 155). The OG image presence check ("Pages with missing OG images: 0") passes cleanly. Fix is editorial copy work for the offending post, not an OG image task. Logged here as a known follow-up.

6.12.8 SLA-isolation re-confirmed

Both V3 cert generator and V3-blog generator run locally only. They commit JPGs to aguidetocloud-revamp/static/. Cloudflare Pages serves the static files. No part of OG image generation runs in the production deploy pipeline. Practice exam SLA (PracticeQuiz.tsx, question JSON, payment flow) was never at risk and remains untouched.


6.11 ✅ SHIPPED — V3-blog "B2 Editorial-Light" — 13 May 2026 PM

Sister system to § 6 (V3 cert). Same chassis (satori + resvg + sharp + Inter + ink-lotus + indigo accent + Zen tokens) but flipped to a light-BG editorial aesthetic for /blog/ covers. Replaces the legacy Python generate_blog_og.py 3-tier-text output (eyebrow + sentence title + truncated description) with single-tier keyword headlines.

6.11.1 Visual lock

+--------------------------------------------------------------+
|                                                              |
|                                                              |
|  Headline (Inter ExtraBold,            ┌─┐ ┌─┐ ┌─┐           |
|  adaptive 72-132 px,                   └─┘ └─┘ └─┘           |
|  left-aligned, 1.05 leading,           ┌─┐ ┌─┐ ┌─┐  ← line-  |
|  -1.5px tracking,                      └─┘ └─┘ └─┘    art    |
|  max-width 720px)                      ┌─┐ ┌─┐ ┌─┐    glyph  |
|                                        └─┘ └─┘ └─┘    (one   |
|  ▬▬ indigo accent line                                of 4)  |
|                                                              |
|                                                              |
|  🪷 aguidetocloud.com                                        |
+--------------------------------------------------------------+
       warm-white BG #FAFAF8

Locked tokens:

Token Value Rationale
BG #FAFAF8 warm-white Less LCD-glare than pure white. Editorial signal vs cert V3's dark #0F0F10.
Headline color #1A1A1A Zen --text Max contrast on warm-white.
Headline font Inter ExtraBold (800), -1.5px tracking, 1.05 leading Locked since V3 prototype.
Adaptive font sizes ≤14ch → 132px · ≤24ch → 108px · ≤36ch → 84px · ≤48ch → 72px · else 64px Tested against 4 worst-case fixtures (calendar / vs / deep-dive / list).
Accent line #6366F1 indigo, 64×5px, rounded 3px, margin-top 32px Same Zen --accent as cert V3 — brand-coherence cue.
Glyph color #737373 Zen --text-tertiary Subtle, never compete with headline.
Lotus lotus-ink.png (dark ink on transparent), 32×32 Reused from cert V3 fonts dir. Half-size vs cert (which has 72×72 in the band).
Wordmark "aguidetocloud.com", Inter SemiBold 22px, #1A1A1A, 0.3px tracking Bottom-left, beside lotus.
Padding 88px top/bottom · 80px left · 64px right Maximises negative space.
Glyph column width 320px (right) Headline takes left ~720px, glyph + breathing space take right ~320px.

6.11.2 Glyph palette (4 line-art ornaments)

All glyphs built from satori-compatible <div> trees (no inline SVG required → simpler render path, no XML-namespace gotchas).

Glyph Shape Use case Auto-detection rule
calendar 3×3 grid of bordered squares Monthly recaps, dated changes card_tag contains What's New
compare Three uneven vertical bars (bar-chart) vs posts, comparisons og_headline contains vs
list 4 rows of bullet+line Listicles, checklists og_headline starts with a digit
layers 4 stair-stepped horizontal bars (narrowing) Deep dives, explainers (DEFAULT) else

Override via og_glyph: in frontmatter wins over auto-detection.

6.11.3 🔴 LOCKED format — JPG mozjpeg + 4:4:4 + stripped metadata

Universal rule for ALL OG covers across the site, regardless of generator (V3 cert, V3-blog, legacy Python). Apply when any generator next gets touched.

// In the post-satori pipeline, after resvg PNG buffer:
const jpgBuffer = await sharp(pngBuffer)
  .jpeg({
    quality: 85,           // visually lossless for our flat-BG + bold-type content
    mozjpeg: true,         // mozjpeg encoder — ~30% smaller than libjpeg at same q
    chromaSubsampling: '4:4:4',  // CRITICAL: 4:2:0 (default) blurs coloured edges
                                 // → indigo accent line + line-art glyphs go fuzzy
  })
  // sharp strips EXIF / XMP / ICC by default — no .withMetadata() call.
  .toBuffer();

Why each setting: - mozjpeg: ~30% better compression than libjpeg at the same visible quality. Mozilla-funded encoder, optimised for the web. Sharp ships it bundled — no extra install. - 4:4:4 chroma: default 4:2:0 quarters the colour resolution. For natural photos this is invisible; for our bold black text on warm-white with a thin indigo accent + thin line-art glyphs in #737373, 4:2:0 produces a faint coloured halo around the indigo line and softens the glyph edges. 4:4:4 preserves them sharp. Tested side-by-side: 4:2:0 = ~26 KB but fuzzy; 4:4:4 = ~29 KB and crisp. Negligible size cost for clear win on text-heavy OGs. - Stripped metadata: sharp's default behaviour. Saves ~2 KB per file (EXIF + XMP + ICC). For an OG image scrapers don't read metadata anyway. - q=85: sweet spot. q=90 adds ~30% bytes for no visible gain on our content (flat BG + bold typography compresses cleanly).

Measured outcomes (13 May 2026 rollout): - Legacy Python+Playwright covers: 70-76 KB · default chroma · text edges OK but accent colours bled - V3-blog mozjpeg+4:4:4 covers: 23-31 KB · text + indigo accent + glyphs all crisp - 62% size reduction · 18 covers · CWV impact: ~800 KB site-wide footprint cut

Apply to legacy generators next time they're touched: - scripts/og-generator/generate_cert_og.py — currently writes JPG via Playwright screenshot + manual quality. Refactor to pipe through sharp (or Python Pillow with mozjpeg via pillow-heif + savings preset). Until then, the legacy Python output stays valid but suboptimal. - scripts/og-generator/generate_section_og.py, generate_practice_og.py, generate_og.py — same.

6.11.4 Frontmatter contract

Every content/blog/<slug>.md requires:

images: ["images/og/blog/<slug>.jpg"]   # tells baseof.html which OG to advertise
og_headline: "..."                       # 3-7 word keyword fragment — the ONLY text on the cover
og_glyph: "calendar|compare|layers|list" # optional; auto-detected if absent

og_headline voice rules (locked): - 3-7 words. Lower is better. The cover is for thumbnails, not SEO. - Sentence case with proper-noun caps. e.g. April 2026 — 41 M365 Copilot updates (not Title Case). - Lowercase trailing descriptor word (guide, changes, simplified, checklist) — gives a casual notebook feel matching the /blog/ body. - Em-dash (not hyphen -) for date+count compounds. - Question mark ? only where the headline reads as a rhetorical hook (e.g. How M365 Copilot works?). Skip for declarative comparisons. - "M365" prefix where the product is in the M365 Copilot family AND no other product brand owns the post (skip the prefix for Agent 365, Agent Builder, E7, Cowork — they have their own brand).

6.11.5 Where it lives

aguidetocloud-revamp/
├── package.json                       # npm scripts: build:og:blog / :dry / :force
└── scripts/og-generator-blog/
    ├── make.mjs                       # main generator (~280 lines)
    ├── package.json                   # satori + @resvg/resvg-js + sharp + gray-matter
    ├── README.md                      # one-pager for future contributors
    ├── fonts/
    │   ├── Inter-Regular.ttf          # 400
    │   ├── Inter-SemiBold.ttf         # 600
    │   ├── Inter-Bold.ttf             # 700
    │   ├── Inter-ExtraBold.ttf        # 800 (headline)
    │   └── lotus-ink.png              # 32×32 brand mark (dark ink on transparent)
    └── .og-blog-cache.json            # hash cache (gitignored)

Run from repo root: npm run build:og:blog. Hash-cache skip on unchanged posts → typically 0-1 s for incremental.

6.11.6 SLA isolation (per § 6.8)

V3-blog is NOT part of any Hugo build or Cloudflare Pages build pipeline. It runs locally only, commits JPGs to static/images/og/blog/<slug>.jpg, Cloudflare serves them statically. A satori / resvg / sharp / gray-matter failure here CANNOT block a production deploy. Verified 13 May 2026 — Hugo build (1964 pages, 210 s) ran clean after V3-blog rollout, OG meta tags reference correct paths in generated HTML.

6.11.7 Adding new blog posts (future-session contract)

When future sessions create a new blog post:

  1. Write the post, set images: ["images/og/blog/<slug>.jpg"]
  2. Set og_headline: "..." per the voice rules in § 6.11.4
  3. Optionally set og_glyph: "..." to override auto-detection
  4. Run npm run build:og:blog from repo root
  5. git add static/images/og/blog/<slug>.jpg content/blog/<slug>.md + commit

Pre-commit gate (informal but enforced by 20-step checklist § 18-style discipline): a post without og_headline will print a warning and skip rendering. CI guard could be added later to fail builds if any blog post lacks og_headline.

6.11.8 The 18 covers shipped 13 May 2026

# Slug og_headline Glyph
1 20-copilot-features-... 22 M365 Copilot Features list
2 agent-365-security-governance-... Agent 365 simplified layers
3 agent-builder-vs-copilot-studio-vs-foundry Agent Builder vs Studio vs Foundry compare
4 copilot-pro-vs-microsoft-365-copilot Copilot Pro vs M365 Copilot? compare
5 how-microsoft-365-copilot-works-... How M365 Copilot works? layers
6 microsoft-365-copilot-april-2026-updates April 2026 — 41 M365 Copilot updates calendar
7 microsoft-365-copilot-brand-kit-... M365 Copilot Brand Kit guide list
8 microsoft-365-copilot-chat-april-2026-changes-... M365 Copilot Chat changes calendar
9 microsoft-365-copilot-chat-complete-guide-... M365 Copilot Chat - trainer guide layers
10 microsoft-365-copilot-content-safety-... M365 Copilot Content safety layers
11 microsoft-365-copilot-control-system-... Copilot Control System simplified layers
12 microsoft-365-copilot-deployment-... M365 Copilot Deployment checklist list
13 microsoft-365-copilot-february-2026-updates February 2026 — 45 M365 Copilot updates calendar
14 microsoft-365-copilot-january-2026-updates January 2026 — 30 M365 Copilot updates calendar
15 microsoft-365-copilot-licensed-... M365 Copilot Licensed - trainer guide layers
16 microsoft-365-copilot-march-2026-updates March 2026 — 36 M365 Copilot updates calendar
17 microsoft-365-e7-frontier-suite-... M365 E7 (Frontier Suite) simplified layers
18 microsoft-copilot-cowork-complete-guide M365 Copilot Cowork simplified layers

6.11.9 Peer evidence trail

Same audit as cert V3 (§ 6.1) but Tier 1b's pattern target = Vercel + Linear + Stripe + Cloudflare BLOG OGs, which converge on light-BG + headline-left + tiny line-art glyph corner-right + small wordmark bottom-left. AGTC's V3-blog adopts that exact template with our brand tokens (Inter / indigo / lotus). Reference renders cached in ~/.copilot/session-state/e665659f-.../files/og-audit/ (Vercel blog default, Vercel per-post AI Gateway, Linear blog M365 Copilot, Stripe default, Cloudflare default — all light-BG).


6.13 ✅ LOCKED — V3-tool "T-A Dark Tool Card" (13 May 2026 PM, commit a6e3447f)

6.13.1 Design lock

Asymmetric variant of V3 — shares the cert charcoal foundation but breaks the centred-cert layout to signal interactive product rather than credential.

+--------------------------------------------------------------+
|                                              ┌────────────┐  |
|  FREE TOOL  (indigo, 4px tracking)           │            │  |
|                                              │   icon     │  |
|  Tool Name (white, Inter 800, adaptive       │  (Lucide,  │  |
|             56-120 px, wraps to 2 lines)     │   168×168, │  |
|                                              │   white    │  |
|  ▬▬▬  indigo line                            │   stroke)  │  |
|                                              │            │  |
|  Tagline (Inter 400, 22-26 px, #A3A3A3)      │  300×300   │  |
|                                              │  rounded   │  |
|                                              │  tile @    │  |
|                                              │  accent×.16│  |
|                                              └────────────┘  |
+--------------------------------------------------------------+
|  aguidetocloud.com                                       🪷  |  ← 88 px category band
+--------------------------------------------------------------+    copilot-ai=lavender · admin-security=
                                                                    periwinkle · certs=peach · utilities=
                                                                    pale teal · fallback=neutral indigo

6.13.2 Tokens (LOCKED)

Token Value Notes
BG #0F0F10 Matches cert V3 (deliberate family signal)
Eyebrow FREE TOOL · #6366F1 indigo · Inter 700 · 20 px · 4 px tracking
Hero text #FAFAFA Inter 800 · adaptive 56/64/80/96/120 px wraps to 2 lines for ≥9 char names
Accent line #6366F1 · 72×5 px · rounded
Tagline #A3A3A3 Inter 400 · 22 px (>70 chars) / 26 px (else) · lineHeight 1.35 max 600 px
Icon tile 300×300 · borderRadius 40 px · BG = tool-accent at α 0.16 · border = tool-accent at α 0.3 tool's identity preserved here
Icon Lucide stroke 1.8 · white #FFFFFF · 168×168 inside tile resolved from toolkit_nav.toml icon field via alias map
Band height 88 px (cert V3 uses 96 px — tool intentionally tighter to feel chunkier)
Band wordmark Inter 600 · 22 px · #1A1A1A band-left
Band logo Ink lotus 64×64 band-right

Category → band-colour mapping (matches family palette where there's overlap):

Category Band hex Sibling cert family
copilot-ai #E1D7F0 lavender AI family
admin-security #CFD4F0 periwinkle SC family
certs #FFD9C7 peach AZ family
utilities #C9E4DD pale teal tool-only
fallback #EEF2FF neutral community/legacy

6.13.3 Why T-A won (3-variant prototype review)

The other two prototypes were: - T-B "Light Product Tile" — sibling-blog warm-white BG, accent slab right. Strong product-page feel (Linear/Vercel marketing) but visually breaks the dark/light system map. Tools would feel "different site" from cert/practice. - T-C "Accent Gradient Wash" — tool-accent → charcoal gradient, ghost icon top-right. Most dramatic but lean-marketing on bright accents (magenta tested as worst case). Wouldn't age well across 59 tools.

T-A keeps the system map clean: dark = reference/product (cert + tool); warm-white = editorial (blog). Cosmos and Brain Bar (cmd) keep their own visual languages — intentional out-of-scope.

6.13.4 Generator location + lifecycle

  • Path: aguidetocloud-revamp/scripts/og-generator-tool/
  • Run: cd scripts/og-generator-tool && node make.mjs [--force | --dry-run]
  • No npm script added to root package.json — deliberate parallel-safety decision. Sibling session was mid-flight on package.json; a future commit can wire build:og:tool once that settles.
  • Output: static/images/og/<slug>.jpg (overwrites legacy Python path — atomic switchover, no Hugo template change)
  • Pipeline: satori → @resvg/resvg-js → sharp (mozjpeg 85, 4:4:4, no metadata) — identical encoder settings to V3-blog
  • Data sources: data/toolkit_nav.toml + data/tool_colours.toml + scripts/og-generator/taglines.toml + scripts/og-generator/extra_tools.toml (same SSOT as legacy Python generator)
  • Lucide icons: lucide-static npm pkg (1920 icons). Non-standard toolkit_nav.toml names aliased: boxing→swords · robot→bot · fishing→anchor · tomato→apple · chart-bar→bar-chart-3 · bolt→zap · gamepad→gamepad-2 · dollar→dollar-sign · hospital→cross · flask→flask-conical · dice→dices · edit-3→pencil. Final fallback circle.
  • Hash cache: .og-tool-cache.json (committed) — keys = SHA256 of (name | tagline | accent | category | icon). Use --force to bypass.
  • External-tool slug fix: Brain Bar has url = https://cmd.aguidetocloud.com/. Naïve URL last-segment parsing produced cmd.aguidetocloud.com (wrong). Loader now uses tool_colours.toml slug field as canonical SSOT and matches nav by url-last-segment OR slugified-short OR slugified-name. Mirrors legacy Python generator behaviour.
  • Output size: ~30 KB per cover vs ~50–60 KB from legacy Python+Playwright. Same mozjpeg win as V3-blog.
  • Render time: 5.5 s for 18 prototype renders; ~20 s for full 59 production renders.

6.13.5 Parallel-safety pattern (worth memorising)

This session shipped while the sibling session was committing cert + blog OGs simultaneously to the same repo. Strategy that worked:

  1. Always-new paths onlyscripts/og-generator-tool/ (new dir, zero overlap) + static/images/og/<slug>.jpg (modifications to existing files only, no untracked-by-them paths)
  2. Skip package.json — they were modifying it for build:og:blog script; I deliberately didn't add build:og:tool to avoid collision
  3. git commit --only --pathspec-from-file=<list> — commits ONLY my listed paths, leaving their staged content alone. The --only semantics are critical: index entries from other sessions are preserved across the commit.
  4. Generator writes .last-run-paths.txt — every non-dry run emits the exact list of paths it touched. Use for git add/git commit so no glob accidentally sweeps in sibling-session paths.
  5. git pull --rebase may fail with "unstaged changes" when sibling session has uncommitted mods to OTHER subdirs (e.g. static/images/og/practice/* mid-session). If git log HEAD..origin/main is empty (= HEAD is already at origin), the rebase is a no-op and you can push directly without pulling first.

6.13.6 SLA isolation re-confirmed

Hugo safe build run before commit: clean (1964 pages, 48 s, exit 0). No template errors. Same warning-baseline as cert V3 shipped 1 hour earlier (deprecated module.mounts.excludeFiles + a handful of unknown-icon warnings, both pre-existing). Practice-exam SLA files (PracticeQuiz.tsx, practice.astro, question JSON, build-question-data.mjs) NOT touched — zero risk to paid-product surface.

6.13.7 The 59 covers shipped 13 May 2026

Category Band Count
copilot-ai lavender 26
admin-security periwinkle 13
utilities pale teal 16
certs peach 2
fallback neutral 2 (Learn/Community legacy)

Full slug list: prompts · prompt-polisher · prompt-guide · prompt-lab · prompt-tester · copilot-readiness · copilot-matrix · copilot-data-flow · copilot-model-map · copilot-frontier-map · roi-calculator · ai-news · m365-roadmap · service-health · deprecation-timeline · ai-mapper · ai-showdown · ai-cost-calculator · token-calculator · licence-picker · licensing · ca-builder · ps-builder · purview-starter · security-toolkit · cs-companion · agent-365-planner · agent-builder-guide · migration-planner · cert-tracker · cert-compass · mind-maps · world-clock · qr-generator · wifi-qr · password-generator · image-compressor · typing-test · countdown · color-palette · pomodoro · site-analytics · admin-bingo · acronym-battle · feature-roulette · rename-tracker · admin-comms · compliance-passport · phishing-test · sla-calculator · admin-badges · policy-tester · incident-comms · cli-quiz · it-day-sim · demo-scripts · instruct-builder · feedback · brain-bar

Live-verified slugs (5 spot-checks against https://www.aguidetocloud.com/images/og/<slug>.jpg): all serving new T-A design at 29-35 KB each; CF-Cache HIT within 4 minutes of push. Sister-system regression check (blog B2 + cert V3 random samples): no regression — both still serving with their respective designs.

6.13.8 Orphan + follow-up cleanup

  • agent-instructions.jpg (2026-04-18 timestamp, 49 KB) — orphan from a renamed tool. Not in current registry. Harmless (no page references it). Could be deleted in a follow-up commit.
  • scripts/og-generator/ Python generator + template.html + og_hashes.json — still in repo, no longer the live source-of-truth for tool OGs. Can be archived after a 2-week verification window.
  • Root npm run build:og:tool script — deliberately deferred to avoid package.json collision with sibling session. Add when both sessions settle.

6.14 ✅ LOCKED — V3-section "S-A Magazine Editorial" (13 May 2026 PM, commit d332bad3)

6.14.1 Design lock

Symmetric, warm-white, magazine-cover feel. Sibling-family with blog B2 (same #FAFAF8 BG + same inline lotus+wordmark bottom-left) but visually distinct: blog is asymmetric headline-LEFT with a small corner-RIGHT glyph; section is symmetric centred with a small bottom-RIGHT glyph.

+--------------------------------------------------------------+
|                                                              |
|                FREE · FOR IT PROS  (indigo eyebrow,           |
|                                     uppercase, 5px tracking)  |
|                                                              |
|             A Guide to Cloud & AI  (Inter ExtraBold,           |
|                                     adaptive 72-180 px,        |
|                                     centred, #1A1A1A)          |
|                                                              |
|                     ▬▬▬  indigo accent line                   |
|                                                              |
|       Free cloud cert study guides, practice exams,          |
|       IT tools, M365 licensing wiki and video tutorials      |
|       — for IT pros and students. Built by a Microsoft       |
|       engineer.  (Inter 400, 24 px, #525252, centred)        |
|                                                              |
|  🪷 aguidetocloud.com                                    🏠  |
+--------------------------------------------------------------+

6.14.2 Tokens (LOCKED)

Token Value Notes
BG #FAFAF8 Matches blog B2 (warm white)
Eyebrow #6366F1 indigo · Inter 700 · 18 px · 5 px tracking · uppercase per-section curated, e.g. Free · For IT Pros, Read · Learn, Prepare · Pass
Hero text #1A1A1A Inter 800 · adaptive 72/84/100/124/156/180 px · centred wraps to multi-line for ≥21 chars
Accent line #6366F1 · 72×5 px · rounded between hero and tagline
Tagline #525252 Inter 400 · 24 px · centred · lineHeight 1.35 max 880 px · sourced from frontmatter description:
Wordmark #1A1A1A Inter 600 · 20 px · bottom-left inline matches blog B2 exactly
Lotus Ink lotus 28×28 · inline left of wordmark matches blog B2
Section glyph Lucide stroke 1.4 · #737373 · 56×56 · bottom-right · opacity 0.55 gives each section its visual signature

6.14.3 Section registry (10 + 1 hidden)

Curated per-slug metadata lives in SECTIONS[] in make.mjs. The mind-maps INDEX cover is the hidden 11th file the rubber-duck caught — it lives at static/images/og/mind-maps.jpg (NOT in sections/) because that's what content/mind-maps/_index.md's frontmatter sets.

Slug Source Output OG Title Eyebrow Icon
homepage content/_index.md og/sections/homepage.jpg A Guide to Cloud & AI Free · For IT Pros home
about content/about.md og/sections/about.jpg About Meet · The Maker user
ai-hub content/ai-hub/_index.md og/sections/ai-hub.jpg AI Hub Hands-on · AI sparkles
blog content/blog/_index.md og/sections/blog.jpg Blog Read · Learn book-open
certifications content/certifications/_index.md og/sections/certifications.jpg Certifications Prepare · Pass graduation-cap
cloud-labs content/cloud-labs/_index.md og/sections/cloud-labs.jpg Cloud Labs Practice · Azure flask-conical
exam-qa content/exam-qa/_index.md og/sections/exam-qa.jpg Exam Q&A Practice · Pass help-circle
free-tools content/free-tools/_index.md og/sections/free-tools.jpg Cloud & AI Toolkit Free · No Sign-up wrench
interview-prep content/interview-prep/_index.md og/sections/interview-prep.jpg Interview Prep Land · The Role briefcase
music content/music/_index.md og/sections/music.jpg Study Music Focus · Flow music
mind-maps content/mind-maps/_index.md og/mind-maps.jpg (top level!) Mind Maps Visual · Branded git-branch

Leading emoji in title: frontmatter (e.g. "🤖 AI Hub", "📜 Certifications") is intentionally stripped — the Lucide icon expresses that more cleanly. Curated ogTitle field controls the hero text.

6.14.4 Why S-A won (2-variant prototype review)

The other candidate was: - S-B "Three Lines Hub" — asymmetric: eyebrow + name + tagline LEFT, large 240×240 ghost Lucide icon RIGHT. More dynamic, more product-page feel.

Sush's pick was S-A "Magazine Editorial" — quieter, centred, magazine-cover vibe. Aligns better with sections being navigation hubs (not product surfaces). The small bottom-right glyph provides per-section visual signature without competing with the hero text.

6.14.5 Generator location + lifecycle

  • Path: aguidetocloud-revamp/scripts/og-generator-sections/
  • Run: cd scripts/og-generator-sections && node make.mjs [--force | --dry-run]
  • No npm script added to root package.json — same parallel-safety pattern as V3-tool (deferred until package.json is settled).
  • Pipeline: satori → @resvg/resvg-js → sharp (mozjpeg 85, 4:4:4, no metadata) — identical encoder settings to V3-blog + V3-tool
  • Hash cache: .og-section-cache.json — keys = SHA256 of (ogTitle | eyebrow | icon | tagline). Use --force to bypass.
  • Output sizes: 26-36 KB per cover (vs 33-71 KB legacy Python+Playwright at the same path)
  • Render time: 3.6 s for all 11 covers

6.14.6 The 11 covers shipped 13 May 2026

Live-verified slugs (5 spot-checks): all serving new S-A design at 26-36 KB. Earth pages route via Hugo frontmatter images: field (no template change). Mind-maps INDEX correctly serves from /images/og/mind-maps.jpg (top-level) — verified.

6.14.7 What's STILL pending after this commit

Only 1 surface remains pending across Earth + Moon:

  1. og/mind-maps/<slug>.jpg (66 files) — biggest unresolved design call: title-only vs hybrid-with-data-preview. Deferred per rubber-duck guidance. See todo og-mindmaps-redesign in session DB.

Everything else on Earth + Moon is shipped as of commit bf1abf6a (og-default V6, 13 May 2026 PM — see § 6.15 below).

6.14.8 The Earth + Moon fallback chain (verified live)

Both worlds already share the same og-default.jpg fallback via independent code paths:

World Layout file Fallback line
Earth (Hugo) layouts/_default/baseof.html line 38 <meta property="og:image" content="{{ .Site.BaseURL }}images/og-default.jpg">
Moon (Astro) src/layouts/BaseLayout.astro const DEFAULT_OG_IMAGE = '/images/og-default.jpg'; resolved via Astro.site (https://www.aguidetocloud.com/)

Both URLs resolve to the same file: https://www.aguidetocloud.com/images/og-default.jpg. Moon's Astro.site config doesn't put guided on a separate origin — it's deployed under /guided/ path on the main domain. So Moon's per-page og:image resolves to the main domain's static files, which means Moon shares Earth's OG library completely:

  • Moon's per-cert practice OG → /images/og/practice/<cert>.jpg (V3 cert design, shipped) ✓
  • Moon's per-cert overview → /images/og/practice/<slug>.jpg (V3 cert design, shipped) ✓
  • Moon's home / explore / help / vendor hubs → /images/og-default.jpg (V6 cosmos-template, shipped 13 May PM) ✓

6.15 ✅ LOCKED — V6 og-default "Cosmos-template card" (13 May 2026 PM, commit bf1abf6a)

6.15.1 Design lock

The brand-parent OG cover — used as fallback for ALL pages without explicit OG (both Earth and Moon). Sits above the V3 family system map (cert + tool + blog + section) as the cover that represents the whole brand, not a sub-surface. Visually distinct: dark cosmos background with the brand inside a focal central card.

+------------------------------------------------------------------------+
|  ·  brainbar($)                            plainai([AI])  ·            |
|                                                                        |
|                  ┌─────────────────────────────────────┐               |
|                  │                                     │               |
|                  │           ┌───[lotus]───┐           │               |
|     agentic •    │           │  pink halo  │           │  • guided     |
|                  │           └─────────────┘           │               |
|                  │                                     │               |
|                  │           A G U I D E  T O          │               |
|                  │              ───────                │               |
|                  │           Cloud & AI                │               |
|                  │      (pink → indigo → white)        │               |
|                  │                                     │               |
|                  └─────────────────────────────────────┘               |
|                                                                        |
|       •curriculum                                                      |
|  shift(⏱)                                              claw([*])       |
|                       aguidetocloud.com                                |
+------------------------------------------------------------------------+

6.15.2 Tokens (LOCKED)

Token Value Notes
Canvas BG #02030E (cosmosBg) Deep cosmos from atlas.json
Card BG linear-gradient(180deg, rgba(11,12,24,0.94) 0%, rgba(7,8,18,0.97) 100%) Subtle vertical depth
Card geometry 920×480 · 28px radius · centred · 1px hairline border rgba(242,237,227,0.14) · drop-shadow 0 30px 100px rgba(0,0,0,0.65) Focal point
Earth lotus static/images/og-default.jpg route uses earth-lotus.webp from cosmos-atlas/public/planets/ — webp→png converted at load (resvg-js doesn't decode webp inside SVG <image>) Home-planet logo
Lotus halo 136×136 circle · linear-gradient(135deg, rgba(251,207,232,0.18), rgba(236,72,153,0.10)) · 1.5px pink border · pink box-shadow Subtle pink halo
Eyebrow "A GUIDE TO" · Inter 800 · 56px · solid white #FAFAFA · letter-spacing 14px · all-caps SAME weight as hero, only the hero gets gradient
Hierarchy line 64×2 indigo #6366F1 · rounded · 18px above, 20px below Internal divider between eyebrow and hero
Hero "Cloud & AI" · Inter 800 · 124px · letter-spacing -3px · linear-gradient(135deg, #EC4899 0%, #818CF8 55%, #FAFAFA 100%) with background-clip: text + color: transparent Full phrase gets the gradient
URL "aguidetocloud.com" · Inter 600 · 20px · cosmosHud #F2EDE3 · centred · 32px from bottom Below the card

6.15.3 Background "beads-on-orbit" composition

3 concentric orbit ellipses centred at canvas centre (600, 315), drawn as faint SVG strokes (rgba(242,237,227,0.10), stroke-width 1). Planets sit ON these rings — polar→cartesian positioning, NOT random scatter. All planet icons are rendered MONOCHROME cream #A39B8A at 75% opacity — the card's gradient is the only colour source on the canvas.

Orbit rx ry Bodies
Outer 580 320 Brain Bar (145°), Plain AI (35°), Shift (215°), Claw (325°)
Middle 470 290 Agentic (180°), Guided (0°)
Inner 380 280 Curriculum (260°)

The MCP relay is deliberately excluded — it's conceptually a satellite/relay, not a planet/moon. 7 bodies is cleaner than 8.

6.15.4 Why V6 won (6-variant prototype review)

Sush rejected D-A (Centred Brand Hero) and D-B (Aurora Hero) in the prior session — both stripped the rich line-art layering the 16 Apr file had. This session rendered 5 fresh satori variants (cosmos top-down · cosmos orbital · cosmos constellation · field-notebook postcard · three-card collage). Sush picked the 16 Apr file as the LAYOUT TEMPLATE he liked, asked for: bigger card · home-planet lotus circle above hero · A GUIDE TO at hero weight · real cosmos planet+moon icons (not random spheres) in the background.

V6 iterated through 4 rounds: 1. R1 — template + cosmos: card + lotus + eyebrow + Cloud & AI, random sphere planets at corners 2. R2 — refined: bigger card, Earth lotus from atlas, big A GUIDE TO, real cosmos icons 3. R3 — subdued: planet halos toned down 50%, glows reduced, sparser star-field 4. R4 — full gradient on "Cloud & AI" (was split: Cloud gradient + & AI solid white) 5. R5 senior-designer pass — monochrome cosmos + beads-on-orbit + hierarchy hairline + tracking opened to 14px

6.15.5 Generator location + lifecycle

  • Path: session-state/aef696ca-.../files/og-default-v2/ (prototype) — has full satori → resvg → sharp pipeline + all 6 variants + monochrome and colour planet SVGs vendored from cosmos-atlas
  • Run: cd files/og-default-v2 && node make.mjs
  • Output: out/v6-template-cosmos.jpg (47 KB) — copied to static/images/og-default.jpg + AVIF + WebP variants generated via sharp
  • Pipeline: satori → @resvg/resvg-js → sharp (mozjpeg 85, 4:4:4, no metadata) — identical encoder to V3 family
  • WebP+AVIF derivatives: sharp().avif({quality:60, effort:6}) and sharp().webp({quality:82}) from the same JPG buffer

6.15.6 Sizes shipped 13 May 2026 PM

Format Old (16 Apr) New (V6) Δ
og-default.jpg 55 KB 47 KB −15%
og-default.webp 34 KB 20 KB −41%
og-default.avif 25 KB 13 KB −48%

All 3 formats serve at https://www.aguidetocloud.com/images/og-default.* — Earth + Moon both fall through to this same trio.

6.15.7 Live-verify on push

Post-deploy curl (commit bf1abf6a): - ✅ og-default.webp → 20678 B image/webp (matches new) - ✅ og-default.avif → 13405 B image/avif (matches new) - ⚠️ og-default.jpg → 55006 B + cf=HIT (stale CDN cache at one PoP) — cache-bust ?v=20260513-v6 returned 48032 B with new etag d7332b63... (origin verified fresh) - ✅ Practice exam SLA endpoint /guided/data/questions/az-900.json → 200 + valid JSON (paranoid check passed)

6.15.8 ✅ Cloudflare cache-purge token (resolved 13 May 2026 PM)

A dedicated single-purpose token now lives at ~/.copilot/secrets/cloudflare-purge-token (token id 089c18c7757d554cac84bd4d9c7f9f30, scope: Zone.Cache Purge on aguidetocloud.com only — nothing else). The other 4 Cloudflare tokens (cloudflare-api-token, cloudflare-api-token-analytics, cloudflare-dns-token, sveltia-cms-auth) deliberately have NO purge scope — single-purpose hygiene.

Standard purge command for any future static-asset swap:

$token = (Get-Content "$env:USERPROFILE\.copilot\secrets\cloudflare-purge-token" -Raw).Trim()
$body  = @{ files = @(
  'https://www.aguidetocloud.com/images/og-default.jpg',
  'https://www.aguidetocloud.com/images/og-default.webp',
  'https://www.aguidetocloud.com/images/og-default.avif'
) } | ConvertTo-Json
Invoke-RestMethod -Uri "https://api.cloudflare.com/client/v4/zones/8bf12b5f305f7b482dafa8059a0fe384/purge_cache" -Method POST -Headers @{ Authorization = "Bearer $token"; "Content-Type" = "application/json" } -Body $body

Returns success=true + HTTP 200 → next request returns cf=MISS with fresh bytes. Verified end-to-end on this commit (og-default.jpg went from 55006 B stale → 48032 B fresh + new etag d7332b63 within ~5s of purge).


6.16 ✅ OPERATIONAL HARDENING — 19 May 2026 (Batch 3 commit dd87e402)

The system as shipped 13 May worked. Six days later three operational problems surfaced — all detected, all fixed, all now guarded against with code-level checks. This section documents them so future sessions don't trip the same wires.

6.16.1 The "two generators in the same output folder" trap

Problem. Both scripts/og-generator/generate_blog_og.py (legacy Python dark glassmorphism) and scripts/og-generator-blog/make.mjs (V3-blog "B2 Editorial-Light" light Zen) write to the same path: static/images/og/blog/<slug>.jpg. Whichever runs LAST overwrites the other.

What happened. The m365-agent-builder-explained blog shipped 17 May. That session used the legacy Python generator (likely because a generic python scripts/og-generator/generate_blog_og.py does it, or because the V3-blog Node generator wasn't on someone's mental map). Output: 70.5 KB dark glassmorphism OG. The V3-blog version from 13 May was clobbered.

For 2 days, the brand-new blog had an OG image that visually clashed with every other recent blog.

Why this isn't fixed by the V3-blog cache. The Node generator caches by sha256(og_headline + glyph + slug). Re-running it after a Python-clobber finds: - Cache key matches (frontmatter unchanged since 13 May) - File exists (Python-generated one) - → SKIPS regeneration

So once Python clobbers, the only way to recover is --force OR delete the JPG (the Node generator falls through to regen when existsSync(out) returns false).

Recovery recipe (proven 19 May):

# 1. Delete the clobbered file so the Node cache check fails
Remove-Item static\images\og\blog\<slug>.jpg

# 2. Re-run the V3-blog generator
cd scripts\og-generator-blog
npm run build

# 3. Commit the new file
cd ..\..
git add static/images/og/blog/<slug>.jpg
git commit -m "..."
git push

# 4. CDN purge (see § 6.16.4)

Long-term fix options (deferred): - Option A: Archive the Python generator entirely (README threatens this in "Phase 2"). Risk: no fallback if Node toolchain breaks. - Option B: Add a sentinel to the V3-blog output (e.g., EXIF comment) and have the Python generator check for it before clobbering. Skipped if present. - Option C: Run the V3-blog audit (pwsh scripts/check-seo-lengths.ps1 -Strict — see § 6.16.3) on every pre-push. The OG-image-size warning catches a fresh clobber the next time anyone pushes.

Option C is what shipped 19 May. Cheapest, no toolchain changes, structurally enforced.

6.16.2 The "silent glyph fallback" trap

Problem. make.mjs line 117-119 routes any unrecognized og_glyph value to glyphLayers (the default). No warning, no error. Frontmatter says og_glyph: "shield" → rendered glyph is layers.

What happened. Two blogs in the repo had invented glyph names: - m365-agent-builder-explained.mdog_glyph: "build" (no "build" glyph exists) - sharepoint-oversharing-controls-microsoft-365-copilot.mdog_glyph: "shield" (no "shield" glyph exists)

Both rendered with the layers fallback. The visuals were OK (layers is a fine default), but the frontmatter lied about what was on the cover. If a future session reads the frontmatter and trusts it, they'd be wrong.

Fix. § 6.16.3's guardrail catches this with a WARN-level check.

Caveat. "build" and "shield" might be glyph names someone intended to add. The current 4 (calendar, compare, layers, list) cover the existing post types. If a new theme genuinely needs a dedicated glyph (e.g., "shield" for security posts), add it to both the glyphFor switch in make.mjs AND the $ValidGlyphs array in check-seo-lengths.ps1. Otherwise rewrite to one of the 4 existing values.

6.16.3 ✅ Extended SEO guardrail (scripts/check-seo-lengths.ps1)

The pre-existing guardrail covered title length (≤60) + description length (≤155) + frontmatter images: paths exist. 19 May extension adds three V3-blog format checks:

Strict-fail checks (block deploy in -Strict mode): - title.Length > 60 - description.Length > 155 - images: reference doesn't exist on disk - NEW: og_headline is missing — V3-blog generator silently skips these blogs (their OG never gets generated, leaves whatever stale OG is on disk)

Warn-only checks (advisory, never block): - NEW: og_headline.Length > 40 (V3-blog adaptive font shrinks uncomfortably below this) - NEW: og_glyph is set but not one of [calendar, compare, layers, list] — catches the § 6.16.2 silent-fallback bug - NEW: OG image file > 50 KB (heuristic for legacy dark-glass — V3-blog covers are 20-35 KB; >50 KB usually means § 6.16.1 happened)

Behavioural fix: the guardrail now excludes _index.md (matches V3-blog generator's own behaviour at make.mjs line 276). Was a false-positive before — section index pages don't need og_headline because they don't get programmatic OGs.

Run pattern:

# Quick check — warn-only, fast
pwsh scripts/check-seo-lengths.ps1

# Pre-push — fail on strict issues
pwsh scripts/check-seo-lengths.ps1 -Strict

# Full audit including non-blog content
pwsh scripts/check-seo-lengths.ps1 -All -Strict

CI integration. The same script runs in .github/workflows/... (the existing SEO + OG image guardrail action). After 19 May extension, the action ALSO catches: - New blogs missing og_headline - Invalid og_glyph values - Stale legacy-format OG images committed by mistake

This is the structural fix Sush asked for: "make that as a guardrail for new blogs".

6.16.4 CDN cache purge — same recipe as § 6.15.8 also applies to blog OGs

Confirmed 19 May: regenerating an OG and pushing it through Cloudflare Pages does NOT invalidate the CDN cache automatically. The new file is on the origin, but the edge keeps serving the cached version (CF-Cache-Status: HIT even minutes after deploy).

Recipe (works for any OG path, not just og-default.jpg):

$token = (Get-Content "$env:USERPROFILE\.copilot\secrets\cloudflare-purge-token" -Raw).Trim()
$headers = @{ 'Authorization' = "Bearer $token"; 'Content-Type' = 'application/json' }
$zoneId = '8bf12b5f305f7b482dafa8059a0fe384'
$body = @{ files = @(
  "https://www.aguidetocloud.com/images/og/blog/<slug>.jpg",
  "https://aguidetocloud.com/images/og/blog/<slug>.jpg"
) } | ConvertTo-Json
Invoke-RestMethod -Uri "https://api.cloudflare.com/client/v4/zones/$zoneId/purge_cache" `
  -Method POST -Headers $headers -Body $body

Returns success: true → next fetch returns CF-Cache-Status: MISS with fresh bytes. Both the apex (aguidetocloud.com) AND www. URLs must be listed — they cache separately.

Automation opportunity (deferred — 10 LoC). Every blog OG regeneration could automatically purge the matching URLs as a post-step in npm run build (read the changed slugs from the cache delta, call the API). Would close the loop between local generation and live CDN. Suggested but not built 19 May — keep manual for now while deploy frequency is low; revisit when blogging cadence picks up.

6.16.5 Live verification commands

After regen + commit + push + CDN purge:

# 1. Verify file size on origin (cache-busted query param bypasses CDN)
$url = "https://www.aguidetocloud.com/images/og/blog/<slug>.jpg?v=$(Get-Date -f yyyyMMddHHmmss)"
(Invoke-WebRequest -Uri $url -Method Head -UseBasicParsing).Headers.'Content-Length'
# Expect 20000-35000 (V3-blog range). >50000 = legacy clobber, see § 6.16.1.

# 2. Verify direct fetch matches (post-purge)
$direct = Invoke-WebRequest -Uri "https://www.aguidetocloud.com/images/og/blog/<slug>.jpg" -Method Head -UseBasicParsing
"$($direct.Headers.'Content-Length') bytes · $($direct.Headers.'CF-Cache-Status')"
# Expect: bytes match cache-busted + CF-Cache-Status: MISS or HIT-with-correct-size

# 3. Run the guardrail (should pass strict)
cd C:\ssClawy\aguidetocloud-revamp
pwsh scripts/check-seo-lengths.ps1 -Strict
# Expect: "All pages compliant. ✓" + exit 0

6.16.6 Drafts are excluded — by design

The guardrail respects draft: true frontmatter (skips those files, matching Hugo's own behaviour — drafts don't render in production builds). This means a draft blog with og_glyph: "shield" won't trigger a warning today.

Trade-off. If a draft has problems, you find out only when draft: true is removed and the blog ships. That's the moment the guardrail will fail strict mode and block the publish push.

Mitigation if you want earlier feedback. Add a -IncludeDrafts flag (not built 19 May — easy to add: 5 LoC change, just removes the draft:\s*true skip clause). Or just run pwsh scripts/check-seo-lengths.ps1 -All periodically and grep for invalid glyphs across all .md.

In practice 19 May, both invalid-glyph blogs were drafts (may-2026-updates, sharepoint-oversharing). Both have been proactively rewritten to valid glyphs (calendar, list) before they ship, so guardrail will pass cleanly at publish time.


7. Gotchas (learnt the hard way this session)

PowerShell + Python emoji encoding bug

PowerShell on Windows defaults to cp1252. Python's print statements with emoji (📐 🎨 ✅) crash with 'charmap' codec can't encode character. Fix: always set both env vars before invoking:

$env:PYTHONIOENCODING="utf-8"
$env:PYTHONUTF8="1"

The wrapper script is fine — only the print output crashes. Could be hardened by writing to a log file instead of stdout, but the env-var fix is cleaner.

Start-Job swallows stdout

If you wrap the python call inside Start-Job for parallelism, the stdout including the success path is lost. Run sequentially or use chained calls (;), don't background-job.

Auth confusion — corp vs lab

The MCP azure-* tools authenticate via the chat agent's Entra context, which is Sush's corp identity (ssutheesh@microsoft.com). That identity has no access to the Lab tenant. All Azure work for this resource MUST go through the powershell tool → az CLI (auth'd to Lab via az login).

If MCP tools start working unexpectedly: stop. Check az account show. Don't ship images generated from corp identity — that crosses the do-no-harm line (commercial site on corp tenant).

disableLocalAuth=true is org policy

You cannot enable key-based auth on this Lab tenant's Cognitive Services resources. Don't try az cognitiveservices account update --disable-local-auth false — it'll succeed at the resource level but get reset by tenant policy. Use Entra ID auth, always.

Output PNG sizes are huge

2.7-3.0 MB per image at 1536×1024 PNG. Production needs a downscale+compress post-step. Not built yet — add when the visual direction locks.

MOD admin identity is generic

The Lab signed-in user is admin@M365CPI52224224.onmicrosoft.com — a generic "MOD Administrator", not a per-tenant role-based identity. Fine for prototyping. If we ever automate this from a pipeline (e.g., auto-generate covers on git push), we'll need a service principal + federated credential, not this admin account.


8. Production-use guardrails (when this graduates)

When (if) the next session locks a direction and we want to ship images:

  1. Provenance question. Lab tenant is for learning. aguidetocloud.com is commercial. Production-shipped images either:
  2. Option A: Stay generated on Lab — but document each cover's prompt verbatim in repo so it's reproducible on any sub
  3. Option B: Set up a personal Visual Studio Enterprise credit ($150/mo, separate from corp) and regenerate locked winners there before commit. Cleaner provenance for a paid product.
  4. Recommendation: Option B for production-shipped images, Option A for inside-blog illustrations that are explicitly "research/learning notes."

  5. Auto-pipeline integration. When a blog post (or cert page) is created, the build step calls the wrapper, generates a cover, downscales+compresses, and saves to aguidetocloud-revamp/static/images/og/<route>.jpg. Wire into Hugo build or a Cloudflare Worker / Action.

  6. Master-template fallback. gpt-image-2 reliably renders short codes like "AZ-900" but may stumble on rarer ones like "AI-3007". If we hit consistency issues across 50 different cert codes, fallback is: master notebook-paper template + PIL hand-lettering overlay using a real handwriting font (e.g., Caveat, Patrick Hand). Less expressive, but consistent at scale.

  7. YouTube thumbnail extension. Same system at 1280×720 instead of 1536×1024. Hero typography needs to be ~20% larger because YouTube grid view is smaller than LinkedIn OG. Worth a dedicated test once visual direction is locked.

  8. Print extensions. If covers feel signature enough, the same prompts → upscale → could ship as stickers, postcards, Ko-fi shop merch. Don't optimise for this until covers are locked, but the optionality is real.


9. Reproduction — spinning this back up cold

If everything in ~/.copilot/scripts/blog-cover-gen.py got deleted tomorrow, here's how to rebuild:

# 1. Auth to Lab
az login --tenant 00b98149-2e3e-468c-b063-fb0cfa35fe44
az account set --subscription 96879ea6-389e-417f-a3a2-16c415a2b6b5

# 2. Verify the resource is still there (it should be — created with regular SKU)
az cognitiveservices account show `
  --name clawy-images-openai `
  --resource-group ssClawy `
  --query "{state:properties.provisioningState, region:location, endpoint:properties.endpoint}" -o table

# 3. Verify the deployment is still there
az cognitiveservices account deployment list `
  --name clawy-images-openai `
  --resource-group ssClawy `
  --query "[].{name:name, model:properties.model.name, version:properties.model.version}" -o table

# 4. Verify Sush still has the role
az role assignment list `
  --assignee 87bc8b84-b22e-4ca2-917c-1d5ed1dda6e7 `
  --scope /subscriptions/96879ea6-389e-417f-a3a2-16c415a2b6b5/resourceGroups/ssClawy/providers/Microsoft.CognitiveServices/accounts/clawy-images-openai `
  --query "[].{role:roleDefinitionName}" -o table

# 5. Install Python deps if needed
pip install openai azure-identity

# 6. Test the script with a low-quality / cheap call
$env:PYTHONIOENCODING="utf-8"; $env:PYTHONUTF8="1"
python "C:\Users\ssutheesh\.copilot\scripts\blog-cover-gen.py" `
  --scene "A simple watercolour test sphere on warm-white paper" `
  --output ".\smoke-test.png" `
  --quality low

# 7. If image opens cleanly → infrastructure is healthy. Move on to direction work.

If the resource was deleted (e.g., RG cleanup), recreate with:

az cognitiveservices account create `
  --name clawy-images-openai `
  --resource-group ssClawy `
  --kind OpenAI `
  --sku S0 `
  --location eastus2 `
  --custom-domain clawy-images-openai

az cognitiveservices account deployment create `
  --name clawy-images-openai `
  --resource-group ssClawy `
  --deployment-name gpt-image-2 `
  --model-name gpt-image-2 `
  --model-version 2026-04-21 `
  --model-format OpenAI `
  --sku-name GlobalStandard `
  --sku-capacity 1

Built 12 May 2026 AM (v1→v4 watercolour iteration + Azure infra). Updated 12 May 2026 PM EVENING with peer audit + Direction C lock. Updated 13 May 2026 PM with V3-blog "B2 Editorial-Light" SHIPPED across all 18 production blog covers + the universal JPG mozjpeg+4:4:4 format rule (§ 6.11). Updated 13 May 2026 PM (later) with V3 cert + practice SHIPPED — 149 study guide + 125 practice exam covers via in-place atomic replacement (§ 6.12). Updated 19 May 2026 with operational hardening (§ 6.16) — two-generator clobber trap, silent glyph fallback, three new V3-blog guardrail checks shipping in commit dd87e402. Tier 1a (cert study guide + practice exam) = V3 programmatic SVG dark+family-band, LIVE (§ 6.12). Tier 1b (blog covers) = V3-blog B2 Editorial-Light, LIVE (§ 6.11). Tools (Phase 3) in progress in a parallel session. Tier 2 Azure Foundry path = ARCHIVED (§ 1-5 + § 7-9 kept for reproducibility / future illustrative needs only).