From 94f7a1f72a9e39c89bb7303363ded859f8553063 Mon Sep 17 00:00:00 2001 From: Andre Cobham Date: Tue, 9 Jun 2026 18:31:59 +0200 Subject: [PATCH] recent updates --- CONTENT.md | 121 ++ OPTIMIZATION.md | 616 ++++++ STACK.md | 1770 +++++++++++++++++ build/__pycache__/seed_sops.cpython-313.pyc | Bin 0 -> 9039 bytes build/seed_sops.py | 239 +++ image-gen-workflow/00-workflow-overview.md | 202 ++ image-gen-workflow/01-model-selection.md | 89 + .../archive/02-generation-log.md | 71 + .../archive/cobhamtech-image-requests.json | 22 + .../archive/select_hero_images.py | 317 +++ image-gen-workflow/imagen-api-reference.json | 128 ++ local-image-generation/01-comfyui-setup.md | 100 + local-image-generation/02-flux-images.md | 99 + local-image-generation/03-wan-video.md | 159 ++ local-image-generation/04-prompt-guide.md | 105 + local-image-generation/05-quality-levers.md | 87 + local-image-generation/README.md | 46 + sops.db | Bin 0 -> 491520 bytes stack-selector.json | 129 ++ stack.json | 239 +++ tools/verify-protection.sh | 122 ++ wp-divi-pipeline-to-am-stack/00-overview.md | 94 + .../01-wpress-extraction.md | 120 ++ .../02-database-analysis.md | 151 ++ .../03-divi-content-extraction.md | 157 ++ .../04-design-system-extraction.md | 172 ++ .../05-content-migration.md | 246 +++ .../06-media-assets.md | 177 ++ .../07-seo-preservation.md | 182 ++ wp-divi-pipeline-to-am-stack/08-run-order.md | 230 +++ .../09-stack-a-output.md | 370 ++++ .../10-agent-breadcrumbs.md | 249 +++ wp-divi-pipeline-to-am-stack/README.md | 81 + .../__pycache__/stage_seed.cpython-313.pyc | Bin 0 -> 19590 bytes .../scripts/analyze_db.py | 368 ++++ .../scripts/extract_divi5.py | 271 +++ .../scripts/extract_nav.py | 99 + .../scripts/extract_wpress.py | 110 + .../scripts/migrate.py | 149 ++ .../scripts/run_pipeline.sh | 175 ++ .../scripts/stage_seed.py | 574 ++++++ wp-migration.json | 50 + 42 files changed, 8686 insertions(+) create mode 100644 CONTENT.md create mode 100644 OPTIMIZATION.md create mode 100644 STACK.md create mode 100644 build/__pycache__/seed_sops.cpython-313.pyc create mode 100644 build/seed_sops.py create mode 100644 image-gen-workflow/00-workflow-overview.md create mode 100644 image-gen-workflow/01-model-selection.md create mode 100644 image-gen-workflow/archive/02-generation-log.md create mode 100644 image-gen-workflow/archive/cobhamtech-image-requests.json create mode 100644 image-gen-workflow/archive/select_hero_images.py create mode 100644 image-gen-workflow/imagen-api-reference.json create mode 100644 local-image-generation/01-comfyui-setup.md create mode 100644 local-image-generation/02-flux-images.md create mode 100644 local-image-generation/03-wan-video.md create mode 100644 local-image-generation/04-prompt-guide.md create mode 100644 local-image-generation/05-quality-levers.md create mode 100644 local-image-generation/README.md create mode 100644 sops.db create mode 100644 stack-selector.json create mode 100644 stack.json create mode 100644 tools/verify-protection.sh create mode 100644 wp-divi-pipeline-to-am-stack/00-overview.md create mode 100644 wp-divi-pipeline-to-am-stack/01-wpress-extraction.md create mode 100644 wp-divi-pipeline-to-am-stack/02-database-analysis.md create mode 100644 wp-divi-pipeline-to-am-stack/03-divi-content-extraction.md create mode 100644 wp-divi-pipeline-to-am-stack/04-design-system-extraction.md create mode 100644 wp-divi-pipeline-to-am-stack/05-content-migration.md create mode 100644 wp-divi-pipeline-to-am-stack/06-media-assets.md create mode 100644 wp-divi-pipeline-to-am-stack/07-seo-preservation.md create mode 100644 wp-divi-pipeline-to-am-stack/08-run-order.md create mode 100644 wp-divi-pipeline-to-am-stack/09-stack-a-output.md create mode 100644 wp-divi-pipeline-to-am-stack/10-agent-breadcrumbs.md create mode 100644 wp-divi-pipeline-to-am-stack/README.md create mode 100644 wp-divi-pipeline-to-am-stack/scripts/__pycache__/stage_seed.cpython-313.pyc create mode 100644 wp-divi-pipeline-to-am-stack/scripts/analyze_db.py create mode 100644 wp-divi-pipeline-to-am-stack/scripts/extract_divi5.py create mode 100644 wp-divi-pipeline-to-am-stack/scripts/extract_nav.py create mode 100644 wp-divi-pipeline-to-am-stack/scripts/extract_wpress.py create mode 100644 wp-divi-pipeline-to-am-stack/scripts/migrate.py create mode 100644 wp-divi-pipeline-to-am-stack/scripts/run_pipeline.sh create mode 100644 wp-divi-pipeline-to-am-stack/scripts/stage_seed.py create mode 100644 wp-migration.json diff --git a/CONTENT.md b/CONTENT.md new file mode 100644 index 0000000..f469630 --- /dev/null +++ b/CONTENT.md @@ -0,0 +1,121 @@ +# CONTENT - Copy Standards, Image Rules, and Asset Guidelines +Author: Andre Cobham / Arising Media +Updated: 2026-06-09 + +## Writing Standard + +Reading level: 7th to 8th grade for service businesses, 10th to 12th grade for professional/B2B. + +Active voice. Short paragraphs (2 to 4 sentences max). + +Lead with the customer's problem, not the business's credentials. + +One clear CTA per section. + +No marketing jargon (synergize, leverage, best-in-class, cutting-edge, state-of-the-art). + +No filler phrases (In today's fast-paced world, Look no further, We pride ourselves on, Don't hesitate, Whether you are X or Y). + +Specificity beats superlatives. A timeframe, a material, a measurable result beats any adjective. + +## What We Never Write + +Em dashes or en dashes. Replace with a period, comma, colon, or the word "and" or "to". + +Invented numbers (satisfaction rates, years of experience, award claims) without client-verified proof. + +"Licensed" for any provisionally licensed or permit-holding clinician. Use "Provisionally Licensed" instead. + +Exclamation points (one per page maximum, in a CTA only). + +Passive voice as the default sentence structure. + +Verification check: `grep -rn '.\|–\|—\|–' . --include="*.html" --include="*.json"`. Result must be zero. + +## Tone by Sector + +Service businesses (trades, cleaning, home services): plain, direct, neighborhood-familiar. + +Healthcare / counseling: warm, clinical accuracy required, never overpromise outcomes. + +Professional / B2B / tech: peer-level, systems-oriented, results-focused. + +## Healthcare Credential Rules + +MHC-LP is Provisionally Licensed, not Licensed. LP stands for Limited Permit. + +Any permit holder must display: "Practices under the supervision of [Name], [Credential]" in the footer, about page, and auto-response email. + +Use "Provisionally Licensed" everywhere: credential row, why-cards, location page intros, section headers, auto-response emails, meta descriptions. + +Run grep check before launch: `grep -r "Licensed Professional" --include="*.html"` . result must be zero. + +Supervisor's credential must be accurate to the letter. LMHC not equivalent to LMHC-D. Confirm the current designation before publishing. + +When a credential changes (upgrade or advancement), update the source templates first (render.py, copy_library.py), rebuild the Docker container, then re-run verification checks. + +## Copy Structure + +One h1 per page. h2 for major sections. h3 for cards or items within a section. Never skip levels. + +Footer on every page: phone, hours, address, in identical format. + +Phone format: (###) ###-####. Hours: Monday to Friday: 8:00 AM to 5:00 PM (no dashes). + +Links use relative paths with .html extension for internal pages. External links include target="_blank" rel="noopener". Phone links use tel:+1 format. Email links use mailto:. Map links open in new tab. + +## Image Standards + +Format: WebP only in production. No JPGs or PNGs in the webroot. + +Every image has descriptive alt text or alt="" if decorative. + +loading="lazy" on every image except the above-the-fold hero. + +width and height attributes on every img tag to prevent layout shift. + +No people, no faces in any generated or stock imagery. + +Show the result (clean room, finished floor, complete installation), not the process or equipment. + +Hero images: unique per page, named hero-{page-slug}.webp. + +## Image Generation + +Preferred source: local ComfyUI (FLUX.1 Schnell) or Google Imagen API. + +Every generated image passes a vision validation check for people/faces before being saved. + +Prompt structure: camera angle + lens + subject + foreground detail + background + lighting + no people. + +Specify lens focal length and depth of field. Vague room names produce incoherent scenes. + +Never generate: people, faces, cleaning equipment, text overlaid on source, before/after states. + +No cleaning machines, vacuums, steam equipment, or hoses in any image. + +Show upright equipment only, not flat industrial models. + +Machines must look functional and modern, not dated or commercial-scale. + +## Image Size Targets + +Service card / thumbnail: max 900px wide, 78% quality, 30-80KB target. + +Hero image (page header): max 1400px wide, 80% quality, 50-180KB target. + +OG / social share image: max 1200px wide, 85% quality, under 150KB. + +Images over these targets are rejected at deploy time. + +## Prompt Engineering + +All prompts follow this structure: {camera angle} {lens} {subject description}, {foreground detail} sharp in foreground, {background} receding into bokeh, {lighting description}, no people, ultra-realistic {type} photography. + +This pattern produces correct depth, perspective, and scene geometry because it names every surface explicitly. + +Fix incoherent objects by naming every part of the frame: background walls, floor material, ceiling (if visible), and what recedes. Avoid vague room names ("office" without detail). Specify plain surfaces (cream painted wall, white drop ceiling) not implied ones. + +Inline negative elements in the prompt itself (no people, no machines, no text), not in a separate negative prompt. + +Do not use "wide shot" without a camera angle qualifier. \ No newline at end of file diff --git a/OPTIMIZATION.md b/OPTIMIZATION.md new file mode 100644 index 0000000..75ad181 --- /dev/null +++ b/OPTIMIZATION.md @@ -0,0 +1,616 @@ +# OPTIMIZATION - Mobile Responsive, SEO, Testing, and Performance +Author: Andre Cobham / Arising Media +Updated: 2026-06-09 + +## Mobile + +Mobile-first CSS. Default styles target 320px and up. + +Breakpoints: 360px, 480px, 600px, 768px, 900px, 1023px, 1024px. + +Switch to mobile nav at max-width: 1023px, not 768px. A typical header does not fit cleanly below 1024px. + +Inline grid styles (style="display:grid;grid-template-columns:1fr 1fr") require !important overrides in media queries to collapse on mobile. Include override block at end of main.css. + +Always set: html, body { overflow-x: clip; max-width: 100%; } + +Form fields (input, select, textarea) require min-width: 0 and box-sizing: border-box on mobile or they push layout wider than viewport. + +Touch targets: 44x44px minimum (WCAG, Apple HIG). + +### Verification . Before Declaring Done + +Always run a Playwright check at multiple viewport widths. Save the script in +`.planning/mobile_check.py`: + +```python +from playwright.sync_api import sync_playwright + +PAGES = ['/', '/about/', '/services/', '/locations/buffalo.html', '/contact/'] + +with sync_playwright() as p: + b = p.firefox.launch(headless=True) + for w in [320, 360, 390, 768, 900, 1023, 1024, 1200]: + ctx = b.new_context(viewport={'width': w, 'height': 800}) + page = ctx.new_page() + for path in PAGES: + page.goto(f'http://localhost:8096{path}', wait_until='networkidle') + r = page.evaluate('() => ({sw: document.documentElement.scrollWidth, cw: document.documentElement.clientWidth})') + diff = r['sw'] - r['cw'] + status = 'OK' if diff <= 0 else f'OVERFLOW +{diff}px' + print(f' w={w} {path:<35} {status}') + ctx.close() + b.close() +``` + +Result must be zero overflow on every page at every width. + +### Animation on Mobile + +Scroll-triggered animations (`data-animate="up"` etc.) work the same on mobile. +But if you take a full-page screenshot for review, force-trigger them first: + +```python +page.evaluate("() => document.querySelectorAll('[data-animate]').forEach(e => e.classList.add('in-view'))") +page.wait_for_timeout(500) +``` + +Otherwise the screenshot shows blank sections that are actually hiding behind +the IntersectionObserver. + +### Touch Targets + +- Tap targets must be at least 44x44px (Apple HIG, WCAG) +- Header menu button: 44px square minimum +- Form submit buttons: padding 0.875rem vertical minimum +- Phone-link CTAs: same + +### Test Devices + +When the site is "done", verify on: +- Real iPhone (Safari) . test the form actually submits +- Real Android phone (Chrome) . same +- Tablet (iPad) . header switches to mobile menu cleanly at 1023px and below +- Desktop (any browser) . full nav, hover states work + +--- + +## SEO, Meta, and Schema + +Every page on every site must include the full set of head tags below. +No exceptions. + +### Required `` Tags (Every Page) + +```html + + + + + {Page-specific title under 60 chars} | {Brand} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +### OG Image + +One default OG image at `/assets/images/og-default.jpg`: +- 1200x630px +- Brand colors +- Logo + company name + city + one descriptor (e.g., "Hardwood Floor Refinishing . Buffalo, NY") +- Under 200KB optimized + +For pages with their own hero image (location, service detail), use that +image's webp/jpg version as the OG image instead of the default. + +### Schema.org JSON-LD + +#### Home Page . LocalBusiness + +```json +{ + "@context": "https://schema.org", + "@type": "LocalBusiness", + "@id": "https://{domain}/#business", + "name": "{Legal business name}", + "url": "https://{domain}", + "telephone": "+1{10digits}", + "email": "{contact email}", + "address": { + "@type": "PostalAddress", + "streetAddress": "{street}", + "addressLocality": "{city}", + "addressRegion": "{state}", + "postalCode": "{zip}", + "addressCountry": "US" + }, + "geo": { + "@type": "GeoCoordinates", + "latitude": {lat}, + "longitude": {lng} + }, + "areaServed": [ + { "@type": "City", "name": "Buffalo" }, + { "@type": "City", "name": "Amherst" } + ], + "openingHoursSpecification": [{ + "@type": "OpeningHoursSpecification", + "dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"], + "opens": "08:00", + "closes": "17:00" + }], + "aggregateRating": { + "@type": "AggregateRating", + "ratingValue": "4.9", + "reviewCount": "47" + } +} +``` + +#### Service Detail Pages . Service + +```json +{ + "@context": "https://schema.org", + "@type": "Service", + "serviceType": "{Service name}", + "provider": { "@id": "https://{domain}/#business" }, + "areaServed": { "@type": "AdministrativeArea", "name": "Erie County, NY" }, + "url": "https://{domain}{path}" +} +``` + +#### Location Pages . LocalBusiness with areaServed Override + +Same as the home `LocalBusiness` schema but override `areaServed` to the +specific city, and include a different `@id` per page. + +#### Every Page . BreadcrumbList + +```json +{ + "@context": "https://schema.org", + "@type": "BreadcrumbList", + "itemListElement": [ + { "@type": "ListItem", "position": 1, "name": "Home", "item": "https://{domain}/" }, + { "@type": "ListItem", "position": 2, "name": "Services", "item": "https://{domain}/services/" }, + { "@type": "ListItem", "position": 3, "name": "Floor Refinishing" } + ] +} +``` + +#### FAQ Pages or Sections . FAQPage + +```json +{ + "@context": "https://schema.org", + "@type": "FAQPage", + "mainEntity": [{ + "@type": "Question", + "name": "How long does refinishing take?", + "acceptedAnswer": { "@type": "Answer", "text": "Most rooms take 2 to 3 days..." } + }] +} +``` + +### robots.txt + +Every site needs one at `/robots.txt`: + +``` +User-agent: * +Allow: / +Disallow: /api/ + +Sitemap: https://{domain}/sitemap.xml +``` + +### sitemap.xml + +Generate at the end of every build. Save at `/sitemap.xml`. One `` entry +per page. Use `` from the file's mtime. + +```xml + + + + https://{domain}/ + 2026-05-08 + 1.0 + + + https://{domain}/services/floor-refinishing.html + 2026-05-08 + 0.8 + + +``` + +After deploy, submit the sitemap to Google Search Console. + +### Title and Description Rules + +- **Title** under 60 characters. Format: `{Service} | {Brand} . {City}, {State}` +- **Description** 150-160 characters, action-oriented, include city + service, phone when possible +- Never use the same title/description on multiple pages +- City + service name in title for location and service pages (huge SEO impact) +- No em-dashes in meta tags. Use `|` pipe as brand separator +- Always include `` . 5-10 comma-separated terms, city + service variants + +### Title/Meta Examples (Lahrcarpetcleaning.com Reference) + +Homepage: +```html +Lahr Carpet Cleaning | Residential & Commercial Carpet Cleaning . Finger Lakes, NY + + + +``` + +Service page: +```html +Carpet Cleaning | Lahr Carpet Cleaning . Waterloo, NY + + +``` + +Location page: +```html +Carpet Cleaning in Seneca Falls, NY | Lahr Carpet Cleaning + + +``` + +### Audit Before Launch + +```bash +# All pages have title +grep -L '' site/**/*.html + +# All pages have canonical +grep -L '<link rel="canonical"' site/**/*.html + +# All pages have og: tags +for f in $(find site -name "*.html"); do + grep -q 'property="og:' "$f" || echo "MISSING og: $f" +done + +# All pages have JSON-LD +for f in $(find site -name "*.html"); do + grep -q 'application/ld+json' "$f" || echo "MISSING schema: $f" +done +``` + +Each command should return zero results before declaring done. + +### llms.txt . AI Crawler Documentation + +Every project must include `/llms.txt` at the site root. This is the emerging standard (llmstxt.org) for telling AI crawlers (Perplexity, Claude, GPT) what your site does and what they should/should not index. + +**Location:** `src/llms.txt` (Docker) or `public_html/llms.txt` (cPanel) + +**Minimum content:** + +``` +# {Brand Name} +> {One-line description of what the site does} + +{2-3 sentence description of the business, services, and target audience.} + +## Services +- {Service 1} +- {Service 2} + +## Pages +- /: Homepage +- /about/: About +- /contact/: Contact + +## Not for external use +- /api/: Internal API endpoints +- /account/: User account pages +``` + +**robots.txt must also disallow /api/ and private paths** . llms.txt is the invitation, robots.txt is the boundary. + +--- + +## Testing and Verification + +Before declaring a site done, run every check in this document and show the +output. No exceptions. + +### The Law + +Before stating ANYTHING works, is configured, is ready, is installed, or is +complete . run a live test and show the raw output. No output = it was not +tested. Not for plugins, not for logins, not for deploys. Test it. Show the proof. + +### Build Verification + +After running build scripts: + +```bash +# Zero unreplaced placeholders +grep -rn "{{" site/locations/*.html site/services/*.html +# Result: empty +``` + +### Container Health + +```bash +docker compose ps +# All services "Up" and (healthy) where applicable + +docker logs {project}-api-1 2>&1 | tail -20 +# No errors, no stack traces + +curl -s -o /dev/null -w "%{http_code}\n" http://localhost:{port}/ +# 200 +``` + +### URL Surface Check + +Every public URL returns 200. Every sensitive URL returns 404. + +```bash +# Public . should all be 200 +for p in "/" "/about/" "/services/" "/locations/" "/locations/buffalo.html" \ + "/services/floor-refinishing.html" "/contact/" "/reviews/" \ + "/assets/css/main.css" "/components/header.html" "/api/health"; do + curl -s -o /dev/null -w "%{http_code} ${p}\n" http://localhost:{port}${p} +done + +# Sensitive . should all be 404 +for p in "/Dockerfile" "/nginx.conf" "/.env" "/api/.env" "/.git/HEAD" \ + "/build_locations.py" "/docker-compose.yml" "/README.md"; do + curl -s -o /dev/null -w "%{http_code} ${p}\n" http://localhost:{port}${p} +done +``` + +### Container Content Check + +Confirm sensitive files are not inside the nginx container: + +```bash +docker exec {project}-web-1 ls /usr/share/nginx/html/ +# Only public folders + index.html. No api/, no Dockerfile, no .env + +docker exec {project}-web-1 ls /usr/share/nginx/html/api/ 2>&1 +# Result: "No such file or directory" (api was correctly excluded from web image) +``` + +### Mobile Responsive Check (Playwright) + +```python +from playwright.sync_api import sync_playwright + +PAGES = ['/', '/about/', '/services/', '/services/floor-refinishing.html', + '/locations/', '/locations/buffalo.html', '/contact/', '/reviews/'] + +with sync_playwright() as p: + b = p.firefox.launch(headless=True) + fails = 0 + for w in [320, 360, 390, 768, 900, 1023, 1024, 1200]: + ctx = b.new_context(viewport={'width': w, 'height': 800}) + page = ctx.new_page() + for path in PAGES: + page.goto(f'http://localhost:{port}{path}', wait_until='networkidle') + r = page.evaluate('() => ({sw: document.documentElement.scrollWidth, cw: document.documentElement.clientWidth})') + diff = r['sw'] - r['cw'] + status = 'OK' if diff <= 0 else f'OVERFLOW +{diff}px' + print(f' w={w} {path:<40} {status}') + if diff > 0: fails += 1 + ctx.close() + print(f'\nfails: {fails}') + b.close() +``` + +Result must be `fails: 0` across every page at every width. + +### Form Submission End-to-End + +After Resend domain is verified: + +```bash +# Validation rejection test +curl -s -X POST http://localhost:{port}/api/estimate \ + -H "Content-Type: application/json" \ + -d '{"name":"","email":"bad"}' +# Expected: {"error":"Validation failed.","fields":[...]} + +# Real submission test +curl -s -X POST http://localhost:{port}/api/estimate \ + -H "Content-Type: application/json" \ + -d '{"name":"Test","email":"acobham@arisingmedia.us","phone":"(716) 555-1234","address":"100 Test St","city":"Buffalo","zip":"14201","service":"refinishing","message":"E2E test","token":""}' +# Expected: {"ok":true} + +# Verify the email actually arrived in the destination inbox +``` + +### Idempotency Test + +Send the same payload twice. Both should return `{"ok":true}`. Inbox should +receive only ONE email (Resend deduplicates via Idempotency-Key). + +### Rate Limit Test + +Send 6 requests rapidly from the same IP: +```bash +for i in 1 2 3 4 5 6; do + curl -s -X POST http://localhost:{port}/api/estimate \ + -H "Content-Type: application/json" \ + -d "{\"name\":\"RL$i\",\"email\":\"a@b.co\",\"phone\":\"(716) 555-1234\",\"address\":\"x\",\"service\":\"x\",\"token\":\"\"}" + echo "" +done +# Requests 1-5: 200 or 422 +# Request 6: 429 Too Many Requests +``` + +### SEO Surface + +```bash +# Every page has <title> +grep -L "<title>" site/**/*.html +# Result: empty + +# Every page has canonical +grep -L 'rel="canonical"' site/**/*.html + +# Every page has og:title +for f in $(find site -name "*.html"); do + grep -q 'property="og:title"' "$f" || echo "MISSING og: $f" +done + +# robots.txt and sitemap.xml exist and are served +curl -s http://localhost:{port}/robots.txt | head -5 +curl -s http://localhost:{port}/sitemap.xml | head -10 +``` + +### Dash Check (Content Rule) + +```bash +grep -rn '.\|–\|—\|–' site/ --include="*.html" --include="*.json" +# Result: empty +``` + +### Lighthouse / PageSpeed (Recommended) + +Run https://pagespeed.web.dev/ on the live URL after launch. Targets: +- Performance: 90+ +- Accessibility: 95+ +- Best Practices: 95+ +- SEO: 100 + +If accessibility is below 95, common fixes: +- Color contrast on body text (WCAG AA = 4.5:1 for normal text) +- Form labels associated with inputs (`<label for="...">`) +- Alt text on every meaningful image +- Skip-to-main-content link +- Focus indicators not removed in CSS + +### Browser Test Matrix + +Test in: +- Firefox (current) . desktop + mobile emulation +- Chrome (current) . desktop + mobile emulation +- Safari (current) . desktop + iOS emulator if possible +- Real iPhone . actual phone, actual Safari +- Real Android . actual phone, actual Chrome + +The form must actually submit and produce a real email on the real phones. +That's the launch gate. + +### Pre-Launch Sign-Off + +Don't ship until ALL of these are green: +- [ ] All public URLs return 200 +- [ ] All sensitive URLs return 404 +- [ ] No sensitive files inside nginx container +- [ ] Zero mobile horizontal overflow at 320-1440px +- [ ] Form submission produces a real email +- [ ] Idempotency dedupe works (same payload twice = one email) +- [ ] Rate limit triggers at 6th request +- [ ] All pages have title, description, canonical, og:, schema JSON-LD +- [ ] robots.txt and sitemap.xml exist and are accessible +- [ ] Zero em-dashes anywhere in HTML or JSON +- [ ] Resend domain shows fully green (SPF + DKIM + DMARC) +- [ ] First test email lands in primary inbox, not spam +- [ ] Tested on real iPhone and real Android device +- [ ] Lighthouse score 90+ across all four categories + +--- + +## Performance Standards + +### Images + +- Must be WebP, converted via `convert-to-webp.py` +- Service cards / thumbnails: max 900px wide, 78% quality, 30–80KB target +- Hero images: max 1400px wide, 80% quality, 50–180KB target +- OG images: max 1200px wide, 85% quality, under 150KB + +### Videos + +- Hero video: mp4 + webm pair +- Max ~5MB per clip +- Stitched reels: concatenated via ffmpeg + +### Cache Headers + +- Static assets (jpg, png, webp, css, js, svg, woff, woff2, mp4, webm): `Cache-Control: public, immutable; expires 30d` +- HTML pages: `Cache-Control: no-cache, must-revalidate` (or vary by deployment) +- API responses: `Cache-Control: no-store` + +### JavaScript + +- No render-blocking JS (use `defer` or `type="module"`) +- Vanilla JS only, no frameworks +- `fetch`, `IntersectionObserver`, `querySelector` + +### CSS + +- No unused CSS +- Plain CSS only, no Sass/Tailwind +- `tokens.css` (design tokens) + `main.css` (components) +- Inline `<style>` blocks only for critical above-the-fold styles + +### HTML + +- One `<h1>` per page +- Semantic tags: `<main>`, `<section>`, `<article>`, `<aside>`, `<header>`, `<footer>`, `<nav>` +- Every `<img>` has `alt`, `width`, `height`, and `loading="lazy"` (except hero) +- No inline event handlers (onclick, onload, etc.) +- defer or async on all non-critical `<script>` tags + +### Lighthouse Targets + +- Performance: 90+ +- Accessibility: 95+ +- Best Practices: 95+ +- SEO: 100 + +Minimum viable scores for launch: +- Performance: 80+ (mobile), 90+ (desktop) +- Accessibility: 90+ +- Best Practices: 90+ +- SEO: 95+ diff --git a/STACK.md b/STACK.md new file mode 100644 index 0000000..0633c31 --- /dev/null +++ b/STACK.md @@ -0,0 +1,1770 @@ +# STACK — Architecture, Deployment, and Build Pipeline +Author: Andre Cobham / Arising Media +Updated: 2026-06-09 + +## Stack Philosophy + +Two primary stacks. Pick based on page count and update frequency. + +### Stack A — PHP Router + SQLite (50+ pages, standard as of 2026-05-21) + +- **PHP Router** — `router.php` dispatches every content URL to the correct PHP template. Edit one template = entire page class updates on next request. No find-and-replace. No file edits. +- **SQLite** — single-file content DB. `pages.sqlite` holds all page content (title, meta, sections JSON, schema). 10,000 rows = 5MB. Sub-millisecond reads. No server process. +- **Vanilla JavaScript** — no frameworks. `fetch`, `IntersectionObserver`, `querySelector` +- **Plain CSS** — `tokens.css` (design tokens) + `main.css` (components). No Sass, no Tailwind +- **Docker + nginx** — nginx routes `/assets/*` directly; all content URLs → PHP-FPM → router.php +- **Resend** — transactional email via `/api/contact.php` +- **Reference:** `arisingmedia.us` — 10,000+ pages + +### Stack B — Static HTML (fewer than 50 pages) + +- **Static HTML** — every page is a `.html` file on disk +- Same JS, CSS, Docker, nginx, Resend as Stack A +- Python 3 stdlib for build scripts (no pip) +- **Reference:** `lahrcarpetcleaning.com` + +### Never Use (Both Stacks) + +- Node.js / npm packages on the website. Front-end JS uses ZERO packages +- WordPress for new builds (we migrate clients OUT of WordPress) +- CSS frameworks (Bootstrap, Tailwind, Bulma) +- JS frameworks (React, Vue, Angular, Svelte) +- jQuery, Lodash, Moment, axios, or any utility library +- CSS-in-JS, styled-components +- Build tools that require `node_modules` (webpack, vite, parcel, esbuild) +- Tracking pixels other than what the client explicitly requests + +### Why This Stack + +1. **Performance** — a static HTML page with vanilla JS loads in <100ms with no parse cost from frameworks +2. **Longevity** — no dependency rot. A site we build today still works in 10 years with no maintenance +3. **Security** — no `npm audit` warnings, no supply-chain attack vectors, no transitive deps to patch +4. **Auditability** — every line on the site is something we wrote and can read in plain text +5. **Hosting** — a static folder + tiny Python container fits in the smallest VM tier any provider sells + +### When to Add a Server-Side Service + +Static-only is the default. Add a small Python service ONLY when needed for: +- Form submission (handled via Resend in the stdlib HTTP server pattern) +- A specific dynamic feature the client paid for (e.g., booking widget, AI chat) + +Each service is its own Docker container. Keep them small (single file when possible). +Use Python `http.server` + `urllib` from stdlib. Do not introduce Flask, FastAPI, Django, or any third-party HTTP framework. + +--- + +## Project Structure + +Two folders per project: source and deployment. + +### Source Folder + +Lives in the dev tree under `concept-agent/projects/{domain}/site/`. +Contains everything needed to maintain and rebuild the site. + +``` +{domain}/site/ +├── index.html # home page +├── about/index.html # /about/ +├── contact/index.html # /contact/ +├── reviews/index.html # /reviews/ +├── blog/index.html # /blog/ +├── locations/ # location pages +│ ├── index.html # /locations/ +│ ├── _template.html # template stamped with JSON +│ ├── buffalo.html # generated, flat URL +│ ├── amherst.html +│ └── ... +├── services/ +│ ├── index.html +│ ├── _template.html +│ ├── floor-refinishing.html +│ └── ... +├── components/ +│ ├── header.html # loaded via fetch() by components.js +│ └── footer.html +├── data/ +│ ├── locations.json # source data for build_locations.py +│ └── services.json # source data for build_services.py +├── assets/ +│ ├── css/ +│ │ ├── main.css # variables, reset, layout +│ │ └── components.css # cards, hero, header, footer, nav, responsive +│ ├── js/ +│ │ ├── main.js # scroll animations, count-up, etc. +│ │ ├── components.js # fetch + inject header/footer +│ │ └── form.js # form validation + submit +│ ├── images/ +│ ├── videos/ # hero video files (.mp4 + .webm) +│ └── fonts/ # only if not using Google Fonts CDN +├── build_locations.py # JSON → flat .html stamping +├── build_services.py +└── README.md # project notes, content sources, status +``` + +### Deployment Folder + +Lives at `/home/sirdrez/arisingmedia-websites/{domain}/`. +Contains ONLY what's needed to run `docker compose up`. + +``` +{domain}/ +├── index.html # all public website folders +├── about/ # ↑ +├── assets/ # ↑ +├── blog/ # ↑ +├── components/ # ↑ +├── contact/ # ↑ +├── locations/ # ↑ +├── reviews/ # ↑ +├── services/ # ↑ +├── api/ # form-submit Python service (if used) +│ ├── server.py +│ ├── Dockerfile +│ ├── .env # gitignored — Resend key, etc. +│ └── .env.example +├── Dockerfile # nginx web container +├── nginx.conf +├── docker-compose.yml +├── .dockerignore +├── .gitignore +└── .planning/ # everything not needed at runtime + ├── build_locations.py # build scripts moved here + ├── data/ # JSON sources moved here + ├── README.md + ├── DNS_*.txt # DNS notes + └── review_*.png # design review screenshots +``` + +### What Goes Where + +**Source folder gets** every working file (build scripts, data JSON, screenshots, +notes, raw assets). This is the dev/maintenance copy. NOT what gets deployed. + +**Deployment folder gets** ONLY the rendered website + the small API service. +Build scripts, JSON data, and notes go into `.planning/` to keep root clean and +prevent accidental web exposure. + +### URL Structure — Two Valid Patterns + +#### Pattern A: Flat HTML (default for Docker/nginx projects) + +nginx `try_files $uri $uri/ $uri.html =404` serves `/locations/buffalo` and +`/locations/buffalo.html`. Canonical form: `/locations/buffalo.html`. + +Why flat: +- One file = one page, no `/index.html` confusion +- Easier sitemap generation +- `<a href>` links are unambiguous +- Crawl budget benefit — Google indexes one URL per page, not two + +#### Pattern B: Directory-style (default for cPanel/Apache projects) + +Each page lives at `{slug}/index.html`. Apache auto-serves `index.html` when +visiting `/{slug}/`. Use this when deploying to cPanel shared hosting. + +``` +services/ +├── carpet-cleaning/index.html → /services/carpet-cleaning/ +├── stairs/index.html → /services/stairs/ +commercial/ +├── offices/index.html → /commercial/offices/ +└── vacation-rentals/index.html → /commercial/vacation-rentals/ +``` + +### Lahrcarpetcleaning.com Reference (Directory-Style, cPanel) + +``` +lahrcarpetcleaning.com/ +├── index.html +├── about/index.html +├── contact/index.html +├── reviews/index.html +├── service-area/index.html +├── locations/ +│ ├── index.html +│ ├── waterloo-ny/index.html +│ ├── geneva-ny/index.html +│ └── ... (20 location pages) +├── services/ +│ ├── carpet-cleaning/index.html +│ ├── stairs/index.html +│ ├── upholstery/index.html +│ ├── floors/index.html +│ ├── area-rugs/index.html +│ ├── add-ons/index.html +│ └── commercial/index.html +├── commercial/ +│ ├── offices/index.html +│ ├── vacation-rentals/index.html +│ ├── hotels-inns/index.html +│ ├── retail-showrooms/index.html +│ └── property-management/index.html +├── assets/ +│ ├── css/styles.css?v=N ← always cache-bust on change +│ ├── js/ +│ │ ├── main.js +│ │ └── components.js ← injects nav+footer via innerHTML +│ ├── images/ +│ │ ├── hero/ ← hero-{slug}.webp, one per page +│ │ └── services/ ← {service}.webp card images +│ └── videos/hero/hero-reel.mp4 +├── tools/ ← NOT deployed to webroot +│ ├── convert-to-webp.py +│ ├── gen-images-flux.py +│ └── gen-hero-images.py +├── .cpanel.yml +├── robots.txt +├── sitemap.xml +├── 404.html +└── 500.html +``` + +All images are `.webp`. cPanel deployment via `.cpanel.yml`. + +--- + +## Build Pipeline + +When a site has many similar pages (location pages, service pages, blog posts, +team-member pages), use a JSON + template + Python build script. + +### When to Use a Build Script + +Use it when there are 4+ pages with identical structure differing only in +content. For example: 6 location pages where only the city name and +city-specific copy differs. + +For one-off pages (home, about, contact, services index), hand-write the HTML +directly. Build scripts are for repetition, not for everything. + +### Pattern + +Three files per template family: + +1. **`data/{thing}.json`** — array of objects, one per page +2. **`{thing}/_template.html`** — HTML with `{{placeholder}}` markers +3. **`build_{thing}.py`** — stdlib Python, stamps template with data + +#### Example: locations.json + +```json +[ + { + "slug": "buffalo", + "city": "Buffalo", + "state": "NY", + "title": "Hardwood Floor Refinishing in Buffalo, NY | Floor It", + "meta_description": "Professional hardwood floor refinishing...", + "canonical": "https://floorithardwoodfloors.com/locations/buffalo.html", + "hero_h1": "Hardwood Floor Refinishing in Buffalo, NY", + "hero_lead": "Western New York's most experienced...", + "overview_h2": "Buffalo's Trusted Floor Refinishing Specialists", + "overview_body_1": "...", + "overview_body_2": "...", + "faqs": [ + { "q": "...", "a": "..." } + ] + } +] +``` + +#### Example: _template.html + +```html +<!DOCTYPE html> +<html lang="en"> +<head> + <title>{{title}} + + + ... + + +

{{hero_h1}}

+

{{hero_lead}}

+ ... + + +``` + +#### Example: build_locations.py (skeleton) + +```python +"""Build flat .html location pages from data/locations.json + locations/_template.html.""" +import json, sys +from pathlib import Path + +SITE_ROOT = Path(__file__).parent +DATA_FILE = SITE_ROOT / "data" / "locations.json" +TEMPLATE_FILE = SITE_ROOT / "locations" / "_template.html" +OUT_DIR = SITE_ROOT / "locations" + +def render(template: str, item: dict) -> str: + out = template + for key, value in item.items(): + if isinstance(value, (str, int, float)): + out = out.replace("{{" + key + "}}", str(value)) + # Custom rendering for nested arrays (e.g. faqs) + # ... handle item['faqs'] etc. + return out + +def main(): + data = json.loads(DATA_FILE.read_text(encoding="utf-8")) + template = TEMPLATE_FILE.read_text(encoding="utf-8") + print(f"Building {len(data)} location pages...") + for item in data: + rendered = render(template, item) + outfile = OUT_DIR / f"{item['slug']}.html" + outfile.write_text(rendered, encoding="utf-8") + print(f" Built: {outfile.relative_to(SITE_ROOT)}") + print(f"Done. {len(data)} pages written.") + +if __name__ == "__main__": + main() +``` + +### Rules + +1. **Source of truth is JSON, not HTML.** When content needs to change, edit the + JSON and re-run the build script. Never hand-edit a generated `.html` file — + the next build will overwrite your changes. + +2. **Generated files land in the same folder as their template.** Do not nest + into a subfolder. The template file is always named `_template.html` (leading + underscore so it sorts above the generated pages). + +3. **Build script lives in the SOURCE root**, not in deployment. After running + the build, sync the rendered `.html` files (not the script, not the JSON) to + deployment. + +4. **Verify zero unreplaced placeholders** after every build: + ```bash + grep -rn "{{" {thing}/*.html # should return nothing + ``` + +5. **Build is idempotent.** Running it twice produces identical files. + +### Stamping Rules — Escaping + +When a JSON value gets stamped into an HTML attribute or ``, special +characters can break the page. Use these rules: + +- Plain text in `<p>` or `<h1>`: ampersand-encode (`&` → `&`) +- `<title>` content: ampersand-encode + strip line breaks +- `<meta>` content attribute: encode `&`, `"`, and remove line breaks +- `href` URL attribute: never put user input here, but if needed, urlencode + +For our typical use case (controlled content authored by us), the simple +`str.replace("{{key}}", value)` is sufficient because we don't have hostile +input. Just don't put angle brackets or quotes in the JSON values. + +### Re-Running the Build + +```bash +cd {project}/site +python3 build_locations.py +python3 build_services.py +``` + +After build, sync the rendered files to deployment. + +--- + +## WordPress to Static HTML Migration + +The playbook for migrating a WordPress (Divi, Elementor, classic, whatever) site +to vanilla static HTML. + +### Phase 1 — Capture Source + +Before touching anything, capture the current site so nothing is lost. + +1. **Database dump** — `wp db export ${domain}.sql --add-drop-table` +2. **Wp-content snapshot** — tar the entire `wp-content/` (themes, plugins, uploads) +3. **Crawl the live site** — use `wget --mirror --convert-links --adjust-extension --page-requisites --no-parent https://{domain}` to capture rendered HTML + all assets +4. **Inventory pages** — list every URL returning 200 (use the sitemap if it has one) +5. **Inventory forms** — note every Gravity Form / Contact Form 7 / etc. field-by-field +6. **Inventory dynamic features** — search, comments, members, anything truly dynamic + +Save all of this in the project's `.planning/` folder. + +### Phase 2 — Decide What to Keep + +Re-design pass. Most WP sites have: +- Bloated copy → cut by 30-50% +- Outdated/inflated metrics → remove or replace with real, verifiable data +- Stock photos → replace with real client photos when available +- Cluttered layouts → strip back to one clear CTA per section +- Plugin features the client never uses → drop entirely + +Show the client a wireframe of the simplified structure before building anything. + +### Phase 3 — Information Architecture + +Standard structure for a small business: + +``` +/ home +/about/ about / story / team +/services/ services index +/services/{slug}.html one detail page per service +/locations/ locations index +/locations/{city}.html one detail page per service area (SEO gold) +/reviews/ customer reviews +/contact/ contact + form +/blog/ optional blog index +``` + +For each location and each service: one flat `.html` page generated from JSON + +template. + +### Phase 4 — Build + +1. Set up source folder per `01-project-structure.md` +2. Write `assets/css/main.css` (variables, reset, typography, layout) +3. Write `assets/css/components.css` (header, footer, hero, cards, forms) +4. Write `components/header.html` and `components/footer.html` +5. Write `assets/js/components.js` (fetch + inject header/footer) +6. Write `assets/js/main.js` (scroll animations, anything page-wide) +7. Build `index.html` first — this is the design system in working form +8. Generate location and service detail pages from JSON +9. Build remaining pages: about, contact, reviews, blog index + +### Phase 5 — Forms + +If the WP site had Gravity Forms or similar, build a vanilla replacement: +- HTML form in `contact/index.html` (and inline on service/location pages if needed) +- Client-side validation in `assets/js/form.js` +- POST to `/api/estimate` (or similar) handled by Python stdlib service +- Server-side validation, reCAPTCHA verification, send via Resend + +### Phase 6 — SEO Parity + +Before launch, every old URL must either: +- Have a matching new URL with the same or better content, OR +- 301-redirect to a relevant new URL + +Build a redirect map from the old WP sitemap. Add to `nginx.conf`: + +```nginx +location = /old-page-slug { return 301 /new-slug.html; } +location = /?p=123 { return 301 /about/; } +``` + +Per-page parity checklist: +- `<title>` matches or improves on the WP title +- `<meta name="description">` matches or improves +- `<link rel="canonical">` is set to the new URL +- Headings (h1, h2, h3) preserve the topical structure +- Internal links updated to new URLs +- Image alt text preserved or improved +- Schema.org JSON-LD added (`LocalBusiness`, `Service`, `BreadcrumbList`) + +### Phase 7 — Switch DNS / Cutover + +1. Deploy the static site to a separate URL first (`new.{domain}`) for client review +2. Once approved, point production DNS to the new container +3. Keep the WP container running for 14 days as fallback +4. Submit new sitemap to Google Search Console +5. Use Search Console URL inspection on 5-10 key pages to confirm indexing + +### Phase 8 — Post-Launch + +- Monitor Search Console for crawl errors / 404s, fix in nginx as redirects +- Monitor form submissions — first real lead through the new form is the + ultimate "it works" check +- Decommission WP only after 30 days of clean operation + +### What NOT to Do + +- Do not run a "headless WordPress" or "WordPress as API" — that defeats the + whole point. Static means static. +- Do not use a static-site-generator tool (Hugo, 11ty, Jekyll, Astro, Next.js + static export). We hand-write HTML and use small Python build scripts only + where data is repeated. +- Do not migrate the database. Content gets re-written cleaner during migration. + +--- + +## WP + Divi to AM HTML Pipeline Overview + +End-to-end playbook for converting a WordPress / Divi site backup (.wpress) +into an Arising Media vanilla HTML + vanilla JS deployment. + +### What This Pipeline Does + +Takes a single `.wpress` archive (All-in-One WP Migration backup) and produces: +- A fully structured `src/` directory matching AM project layout +- A CSS design system derived from the original Divi theme settings +- All page content extracted, cleaned, and re-authored into AM HTML templates +- All media migrated to WebP and remapped to `/assets/images/` +- SEO metadata (titles, descriptions, canonicals, schema.org) preserved or improved +- Docker-ready deployment with nginx + PHP contact form + +### Philosophy + +The goal is NOT a 1:1 copy. The goal is: +1. Preserve all content, SEO equity, and brand identity +2. ENHANCE the design — cleaner, faster, more modern +3. Remove all WordPress / Divi bloat (plugin CSS, shortcode residue, 300KB JS bundles) +4. Produce a site that loads in <2s on mobile and scores 95+ on Lighthouse + +Every migration is a design upgrade. The Divi site is the reference, not the target. + +### Divi Version Matters + +Two distinct extraction paths: + +| Version | Content Storage | How to detect | +|---------|----------------|---------------| +| Divi 4 | `[et_pb_section]` shortcodes in `wp_posts.post_content` | `post_content` contains `[et_pb_` | +| Divi 5 | Gutenberg blocks (`<!-- wp:divi/section -->`) + JSON in `wp_postmeta` | `post_content` contains `<!-- wp:divi/` | + +Run Phase 2 (database analysis) first to determine which version before choosing the extraction path. + +### Pipeline Phases + +``` +Phase 0 Setup Verify .wpress location, create extraction directory +Phase 1 Extract Unpack .wpress binary archive to wpress-extract/ +Phase 2 DB Analysis Inspect WordPress database dump, detect Divi version, inventory pages +Phase 3 Content Extract page content via Divi 4 or Divi 5 path +Phase 4 Design System Pull colors, fonts, spacing from wp_options → CSS custom properties +Phase 5 Media Catalog uploads/, convert to WebP, generate image manifest +Phase 6 Build HTML Map extracted content to AM templates, generate JSON data files +Phase 7 SEO Port titles, metas, canonicals, schema.org; build redirect map +Phase 8 Forms Replace Gravity Forms / CF7 with AM vanilla form + Python API +Phase 9 QA Lighthouse audit, grep for unreplaced placeholders, protection check +``` + +### Script Reference + +All scripts live in `.am-webdesign-sops/wp-divi-pipeline/scripts/`. + +| Script | Phase | Purpose | +|--------|-------|---------| +| `extract_wpress.py` | 1 | Unpack .wpress binary archive | +| `analyze_db.py` | 2 | Parse SQL dump, inventory pages + detect Divi version | +| `extract_divi4.py` | 3 | Parse et_pb_ shortcodes → structured content JSON | +| `extract_divi5.py` | 3 | Parse Gutenberg/Divi5 blocks → structured content JSON | +| `extract_design.py` | 4 | Pull Divi theme options → design-system.json | +| `extract_media.py` | 5 | Catalog uploads/, emit media-manifest.json | +| `convert_images.py` | 5 | Batch convert images → WebP | +| `run_pipeline.sh` | 0-7 | Master script — runs all phases in order | + +### Per-Project Working Directory + +``` +{domain}/ +└── .planning/ + ├── vibrantyou-yoga-YYYYMMDD-*.wpress ← source archive (never modify) + ├── wpress-extract/ ← Phase 1 output (gitignored) + │ ├── package.json ← archive metadata + │ ├── database.sql ← MySQL dump + │ └── uploads/ ← all media (NOT in wp-content/) + ├── data/ + │ ├── pages.json ← Phase 2 output + │ ├── design-system.json ← Phase 3 output + │ └── media-manifest.json ← Phase 4 output + └── scripts/ ← project-specific overrides if needed +``` + +### .wpress Extraction Details + +The `.wpress` binary format is NOT a standard zip or tar. Custom sequential binary format: + +``` +[HEADER 4377 bytes] [FILE DATA n bytes] [HEADER] [FILE DATA] ... +``` + +Header breakdown: +``` +Offset Length Field +0 255 Filename (null-padded) +255 14 File size in bytes (ASCII decimal, null-padded) +269 12 mtime unix timestamp (ASCII decimal, null-padded) +281 4096 Relative path (null-padded) +4377 n Raw file bytes (size from header) +``` + +The archive ends when a header of all null bytes is encountered, or EOF. + +Extraction script: + +```bash +python3 /home/sirdrez/arisingmedia-websites/.am-webdesign-sops/wp-divi-pipeline/scripts/extract_wpress.py \ + /home/sirdrez/arisingmedia-websites/{domain}/.planning/{file}.wpress \ + /home/sirdrez/arisingmedia-websites/{domain}/.planning/wpress-extract/ +``` + +### Database Analysis + +Parse the WordPress MySQL dump to inventory pages, detect Divi version, +extract design settings, and build the data JSON files. + +```bash +python3 /home/sirdrez/arisingmedia-websites/.am-webdesign-sops/wp-divi-pipeline/scripts/analyze_db.py \ + {domain}/.planning/wpress-extract/ \ + {domain}/.planning/data/ +``` + +Outputs three files into `.planning/data/`: +- `pages.json` — all published pages/posts with content and SEO meta +- `design-system.json` — colors, fonts, Divi settings +- `site-info.json` — domain, plugin list, WP version, Divi version + +### Divi 5 Content Extraction + +Parse raw Divi page content from `pages.json` into clean, structured HTML +sections ready to map into AM templates. + +```bash +python3 /home/sirdrez/arisingmedia-websites/.am-webdesign-sops/wp-divi-pipeline/scripts/extract_divi5.py \ + {domain}/.planning/data/pages.json \ + {domain}/.planning/data/content/ +``` + +Produces one JSON file per page: `content/{slug}.json` + +Key fields in page JSON: +- `slug`: page URL slug +- `title`: page title +- `seo_title`: SEO title (from Rank Math if available) +- `seo_description`: SEO description (from Rank Math if available) +- `sections`: array of content sections with type, background_color, and modules + +Map each Divi module type to AM component: + +| Divi module | Extract | Map to AM element | +|-------------|---------|-------------------| +| `divi/text` | inner HTML | `<section>`, `<p>`, headings as-is | +| `divi/button` | `text`, `url` | `<a class="btn-primary">` | +| `divi/image` | `src`, `alt`, `title` | `<img>` → rewrite to WebP path | +| `divi/blurb` | icon, title, body | `.am-card` component | +| `divi/testimonial` | quote, author, company | `.am-testimonial` component | +| `divi/video` | `src`, poster | `<video>` or YouTube embed | +| `divi/contact_form` | field list | → replace with AM form | +| `divi/accordion` | Q+A pairs | `<details><summary>` | +| `divi/fullwidth_header` | title, subhead, CTA | hero section | + +Strip Divi class/attribute noise using `clean_divi_html()` from `divi_to_html.py`: + +```python +from divi_to_html import clean_divi_html, rewrite_internal_links + +cleaned = clean_divi_html(raw_html) +cleaned = rewrite_internal_links(cleaned, staging_hosts=("vibrantyou.yoga",)) +``` + +### Design System Extraction + +Convert Divi theme settings into AM CSS custom properties. + +Input: `design-system.json` produced by `analyze_db.py` with fields: +- `primary_color`: main brand color +- `body_font`: font family name +- `header_font`: heading font name +- `body_font_size`: base font size in px +- `body_line_height`: line height ratio +- `divi_version`: "4" or "5" +- `wp_version`: WordPress version +- `site_url`: domain +- `site_name`: brand name + +Never lift the Divi palette 1:1. Use extracted colors as the base and build a +full 5-step scale around the primary hue: + +```css +:root { + --color-primary: {extracted-color}; + --color-primary-dark: {darken-by-15%}; + --color-primary-light: {lighten-by-40%}; + --color-surface: #fafafa; + --color-surface-alt: #f0f7f6; + --color-text: #1a1a1a; + --color-text-muted: #5a6e6b; + --color-border: #c8dedd; + --color-white: #ffffff; + + /* Fonts */ + --font-body: '{body-font}', system-ui, sans-serif; + --font-heading: '{header-font}', Georgia, serif; + + /* Modular scale (1.25 ratio) */ + --text-xs: 0.75rem; --text-sm: 0.875rem; + --text-base: 1rem; --text-lg: 1.125rem; + --text-xl: 1.25rem; --text-2xl: 1.5rem; + --text-3xl: 1.875rem; --text-4xl: 2.25rem; + --text-5xl: 3rem; --text-6xl: 3.75rem; + + /* Spacing scale */ + --space-1: 0.25rem; --space-2: 0.5rem; --space-3: 0.75rem; + --space-4: 1rem; --space-5: 1.25rem; --space-6: 1.5rem; + --space-8: 2rem; --space-10: 2.5rem; --space-12: 3rem; + --space-16: 4rem; --space-20: 5rem; --space-24: 6rem; + --space-32: 8rem; +} +``` + +### Content Migration + +Map extracted Divi content into AM HTML templates. + +Build order: +1. `src/assets/css/main.css` — design tokens, reset, typography, layout grid +2. `src/assets/css/components.css` — header, footer, hero, cards, forms, nav +3. `src/components/header.html` — navigation +4. `src/components/footer.html` — footer links, contact info +5. `src/assets/js/components.js` — fetch + inject header/footer +6. `src/assets/js/main.js` — scroll animations, intersection observer +7. `src/index.html` — home page (this IS the design system in working form) +8. Remaining pages: about, classes, contact, blog +9. `src/robots.txt`, `src/sitemap.xml`, `src/404.html`, `src/500.html` + +For 4+ similar pages (class types, locations), use JSON template build: + +``` +src/classes/ +├── _template.html ← class detail page template +├── hatha.html ← generated from classes.json +├── vinyasa.html +└── yin.html + +.planning/data/ +└── classes.json ← array of class objects +``` + +### Media Assets + +Migrate WordPress uploads to AM `/assets/images/`, convert to WebP, and +generate a media manifest for URL remapping. + +Steps: +1. Catalog all original media (skip WordPress-generated size variants like `-150x150`) +2. Copy originals to `src/assets/images/` +3. Convert to WebP using `cwebp` or Python Pillow +4. Generate media manifest with old → new URL mapping +5. Apply manifest during HTML build to rewrite all image paths + +```bash +# Catalog originals (skip WP size variants) +find .planning/wpress-extract/uploads -type f \( -name "*.jpg" -o -name "*.png" \) | \ + grep -v -E "\-[0-9]+x[0-9]+\.(jpg|png)$" > .planning/data/media-originals.txt + +# Copy and convert +while IFS= read -r src; do + cp "$src" "src/assets/images/$(basename $src)" +done < .planning/data/media-originals.txt + +cd src/assets/images/ +for img in *.jpg *.png; do + [ -f "$img" ] || continue + cwebp -q 82 "$img" -o "${img%.*}.webp" && rm "$img" +done +``` + +Remap URLs during HTML build: + +```python +import json, re + +manifest = json.loads(open('.planning/data/media-manifest.json').read()) +url_map = {m['wp_url']: m['am_url'] for m in manifest} + +def rewrite_media_urls(html: str) -> str: + for wp_url, am_url in url_map.items(): + html = html.replace(wp_url, am_url) + return html +``` + +### SEO Preservation + +Before building HTML, map every WordPress page URL to its new AM URL and +ensure title, description, canonical, and schema.org are preserved or improved. + +Rank Math SEO extraction (already in `pages.json` as `seo_title` and `seo_description`). + +Priority order for SEO fields: +1. `seo_title` from Rank Math (if not empty and not a template) +2. `post_title` with AM format appended: `{Title} | {Brand Name}` +3. Never leave title as the raw WP default + +Rank Math title templates use `%` tokens — strip them and rebuild: + +```python +import re + +def clean_rm_title(rm_title: str, post_title: str, site_name: str) -> str: + if not rm_title or "%" in rm_title: + return f"{post_title} | {site_name}" + return rm_title + +def clean_rm_desc(rm_desc: str) -> str: + return re.sub(r"%[a-z_]+%", "", rm_desc).strip(" -|") +``` + +Schema.org by page type: + +| Page | Schema type | Required fields | +|------|------------|----------------| +| Home | `LocalBusiness` | name, url, telephone, address, areaServed, openingHours | +| About | `AboutPage` + `Organization` | name, description, founders | +| Contact | `ContactPage` | name, url, telephone, email, address | +| Blog post | `Article` | headline, datePublished, author, image | + +Pre-launch SEO audit (all must return empty): + +```bash +SITE=src + +# Every page has title/description/canonical/JSON-LD +find $SITE -name "*.html" | xargs grep -L '<title>' +find $SITE -name "*.html" | xargs grep -L 'name="description"' +find $SITE -name "*.html" | xargs grep -L 'rel="canonical"' +find $SITE -name "*.html" | xargs grep -L 'application/ld+json' + +# No WP URLs leaked +grep -r "wp-content\|wp-admin\|?p=\|?page_id=" $SITE --include="*.html" + +# No unreplaced placeholders +grep -r "{{" $SITE --include="*.html" + +# No Divi residue +grep -r "et_pb_\|wp:divi" $SITE --include="*.html" +``` + +### Run Order (Complete Execution Sequence) + +```bash +export DOMAIN="vibrantyou.yoga" +export PROJECT="/home/sirdrez/arisingmedia-websites/$DOMAIN" +export SOPS="/home/sirdrez/arisingmedia-websites/.am-webdesign-sops" +export WPRESS=$(ls $PROJECT/.planning/*.wpress | head -1) + +# Phase 0: Setup +mkdir -p $PROJECT/{src/{about,services,contact,blog,classes,components,assets/{css,js,images,svg,fonts}},build,infra,api,.planning/{data/{content},scripts,wpress-extract}} + +# Phase 1: Extract archive +python3 $SOPS/wp-divi-pipeline/scripts/extract_wpress.py "$WPRESS" "$PROJECT/.planning/wpress-extract/" + +# Phase 2: Database analysis +python3 $SOPS/wp-divi-pipeline/scripts/analyze_db.py "$PROJECT/.planning/wpress-extract/" "$PROJECT/.planning/data/" + +# Phase 3: Content extraction (Divi 5 example) +python3 $SOPS/wp-divi-pipeline/scripts/extract_divi5.py "$PROJECT/.planning/data/pages.json" "$PROJECT/.planning/data/content/" + +# Phase 4: Design system (manual — read design-system.json, write main.css) + +# Phase 5: Media migration +find $PROJECT/.planning/wpress-extract/uploads -type f \( -name "*.jpg" -o -name "*.png" \) | \ + grep -v -E "\-[0-9]+x[0-9]+\.(jpg|png)$" > $PROJECT/.planning/data/media-originals.txt + +while IFS= read -r src; do + cp "$src" "$PROJECT/src/assets/images/$(basename $src)" +done < $PROJECT/.planning/data/media-originals.txt + +cd $PROJECT/src/assets/images/ +for img in *.jpg *.png; do + [ -f "$img" ] || continue + cwebp -q 82 "$img" -o "${img%.*}.webp" && rm "$img" +done + +# Phase 6: Build HTML (manual — per 05-content-migration.md) + +# Phase 7: SEO audit +cd $PROJECT/src +find . -name "*.html" | grep -v "_template" | xargs grep -L '<title>' +find . -name "*.html" | grep -v "_template" | xargs grep -L 'rel="canonical"' + +# Phase 8: Docker setup +docker compose -f $PROJECT/docker-compose.yml build +docker compose -f $PROJECT/docker-compose.yml up -d +curl -I http://localhost:PORT/ + +# Phase 9: Protection check +bash $SOPS/tools/verify-protection.sh https://$DOMAIN +``` + +--- + +## Docker + Nginx Deployment + +Every project ships with ALL deployment configs so it can go to either a +Docker VPS or a cPanel shared host without refactoring. + +### docker-compose.yml + +```yaml +services: + web: + image: {domain}-static + build: + context: . + dockerfile: Dockerfile + ports: + - "{port}:80" + depends_on: + api: + condition: service_healthy + restart: unless-stopped + + api: + image: {domain}-api + build: + context: ./api + dockerfile: Dockerfile + env_file: ./api/.env + expose: + - "3001" + healthcheck: + test: ["CMD", "python3", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:3001/health',timeout=3).status==200 else 1)"] + interval: 10s + timeout: 5s + retries: 3 + restart: unless-stopped +``` + +Port assignments are unique per project. Track in +`/home/sirdrez/arisingmedia-websites/PORTS.md` so no two projects collide. + +### Dockerfile (nginx web container) + +CRITICAL — the Dockerfile must explicitly list which folders to copy. Never use +`COPY . /usr/share/nginx/html/` because that copies `.env`, `Dockerfile`, +build scripts, etc. into the web root where they become URL-accessible. + +```dockerfile +FROM nginx:alpine + +# nginx config — server-only, never served as a static file +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Public website only — explicit list, no wildcards +COPY index.html /usr/share/nginx/html/ +COPY assets /usr/share/nginx/html/assets/ +COPY components /usr/share/nginx/html/components/ +COPY about /usr/share/nginx/html/about/ +COPY blog /usr/share/nginx/html/blog/ +COPY contact /usr/share/nginx/html/contact/ +COPY locations /usr/share/nginx/html/locations/ +COPY reviews /usr/share/nginx/html/reviews/ +COPY services /usr/share/nginx/html/services/ + +EXPOSE 80 +``` + +### Dockerfile (api Python container) + +```dockerfile +FROM python:3.13-alpine +WORKDIR /app +COPY server.py . +EXPOSE 3001 +CMD ["python3", "-u", "server.py"] +``` + +No pip, no requirements.txt, no node_modules. Python stdlib only. + +### nginx.conf + +```nginx +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Defense in depth — deny dotfiles, configs, scripts, source files + location ~ /\. { + deny all; + return 404; + } + location ~* \.(env|env\.example|conf|yml|yaml|py|pyc|md|txt|sh|sql|log|bak|old|swp|dockerfile)$ { + deny all; + return 404; + } + location = /Dockerfile { + deny all; + return 404; + } + + # API proxy — strip /api/ prefix, forward to Python service + location /api/ { + proxy_pass http://api:3001/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_read_timeout 10s; + proxy_connect_timeout 5s; + } + + # Flat HTML routing — /locations/buffalo serves /locations/buffalo.html + location / { + try_files $uri $uri/ $uri.html =404; + } + + # Cache static assets aggressively + location ~* \.(jpg|jpeg|png|webp|svg|ico|css|js|woff2?|mp4|webm)$ { + expires 30d; + add_header Cache-Control "public, immutable"; + access_log off; + } + + # Security headers + add_header X-Frame-Options "SAMEORIGIN"; + add_header X-Content-Type-Options "nosniff"; + add_header X-XSS-Protection "1; mode=block"; + add_header Referrer-Policy "strict-origin-when-cross-origin"; + add_header Permissions-Policy "geolocation=(), microphone=(), camera=()"; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.google.com https://www.gstatic.com https://www.recaptcha.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; img-src 'self' data: https:; object-src 'none'; frame-ancestors 'self'; form-action 'self'; base-uri 'self';"; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"; + + # Disable server tokens + server_tokens off; + client_max_body_size 16k; + + gzip on; + gzip_types text/html text/css application/javascript image/svg+xml; + gzip_min_length 1024; + + error_page 404 /404.html; + error_page 500 /500.html; +} +``` + +### .dockerignore + +Keeps sensitive files out of the build context: + +``` +.git +.gitignore +.dockerignore +api +build_*.py +__pycache__ +*.pyc +*.md +*.txt +review_*.png +docker-compose.yml +.DS_Store +.planning +``` + +### .gitignore + +``` +api/.env +api/__pycache__/ +__pycache__/ +*.pyc +*.log +.DS_Store +``` + +The `api/.env` file is NEVER committed. + +### Sync from Source to Deployment + +After every change to source HTML/CSS/JS/assets: + +```bash +SITE="/path/to/concept-agent/projects/{domain}/site" +DEPLOY="/home/sirdrez/arisingmedia-websites/{domain}" + +rsync -a \ + --exclude=.git --exclude=.planning --exclude=api \ + --exclude=Dockerfile --exclude=nginx.conf --exclude=docker-compose.yml \ + --exclude=.dockerignore --exclude=.gitignore \ + --exclude='build_*.py' --exclude=__pycache__ --exclude=data \ + --exclude='*.md' --exclude='*.txt' --exclude='review_*.png' \ + "$SITE/" "$DEPLOY/" + +cd "$DEPLOY" +docker compose up -d --build web +``` + +### Verify After Deploy + +Every deploy MUST be audited with `tools/verify-protection.sh` before being +considered live. The script probes a fixed list of sensitive paths +(`Dockerfile`, `.env`, `nginx.conf`, `.planning/`, `__pycache__/`, build +scripts, `.git/`, etc.) and fails if any returns 200. + +```bash +~/arisingmedia-websites/.am-webdesign-sops/tools/verify-protection.sh \ + http://localhost:{port} +``` + +Exit codes: +- `0` PASS — every sensitive path 404, every required path reachable. +- `0` PASS (with warnings) — protection clean but `/robots.txt` or + `/sitemap.xml` missing (content gap, not a leak). +- `1` FAIL — at least one sensitive path returned 200, or `/` is unreachable. + +Run it manually after every `docker compose up -d --build`. Wire it into CI +once the site has a remote pipeline. Treat a FAIL as a deploy rollback. + +For ad-hoc spot checks: + +```bash +curl -s -o /dev/null -w "site: %{http_code}\n" http://localhost:{port}/ +curl -s -o /dev/null -w "css: %{http_code}\n" http://localhost:{port}/assets/css/main.css +curl -s -o /dev/null -w "api: %{http_code}\n" http://localhost:{port}/api/health +``` + +All public paths return 200. All sensitive paths return 404. + +### Project Folder Rename Procedure + +WHY: Docker Compose derives its project name from the folder the +`docker-compose.yml` lives in. Renaming the folder changes the compose project +name, which orphans any running containers under the old name. + +The fix is to explicitly remove the old container before bringing up the new +compose project: + +```bash +# Stop and remove the old container by its known name +docker stop {container-name} +docker rm {container-name} + +# Now bring up from the renamed folder — clean start +docker compose -f /path/to/renamed-folder/docker-compose.yml up -d +``` + +Always confirm the env vars loaded correctly after restart: + +```bash +docker exec {container-name} env | grep RESEND +``` + +--- + +## cPanel + Apache Deployment + +Use this deployment method when the client's host is cPanel-based (shared hosting, +WHM, Bluehost, HostGator, SiteGround, etc.) instead of a VPS running Docker. + +### Key Rule: Repo Path ≠ Webroot + +cPanel Git requires an EMPTY directory as the repository path. The webroot +(`public_html/{domain}/`) is never the repo path — cPanel rejects it if it +already contains files. + +``` +Repo path (empty dir): /home/{username}/repositories/{domain}/ +Deploy target (webroot): /home/{username}/public_html/{domain}/ +``` + +### Setting Up the Repo in cPanel + +1. cPanel → Git Version Control → Create Repository +2. Repository Path: `/home/{username}/repositories/{domain}/` (must be empty) +3. Clone URL: your Git remote (GitHub, Bitbucket, etc.) +4. cPanel clones into the repo path — never into the webroot + +### .cpanel.yml + +This file lives in the repo root and tells cPanel what to copy to the webroot +on every push/deploy. All paths are relative to the repo root. + +```yaml +--- +deployment: + tasks: + - export DEPLOYPATH=/home/{username}/public_html/{domain}/ + - /bin/cp -r assets $DEPLOYPATH + - /bin/cp -r about $DEPLOYPATH + - /bin/cp -r commercial $DEPLOYPATH + - /bin/cp -r contact $DEPLOYPATH + - /bin/cp -r locations $DEPLOYPATH + - /bin/cp -r reviews $DEPLOYPATH + - /bin/cp -r service-area $DEPLOYPATH + - /bin/cp -r services $DEPLOYPATH + - /bin/cp index.html $DEPLOYPATH + - /bin/cp 404.html $DEPLOYPATH + - /bin/cp robots.txt $DEPLOYPATH + - /bin/cp sitemap.xml $DEPLOYPATH +``` + +Add or remove folder cp lines to match the project's actual directory structure. +Do NOT copy: `tools/`, `*.py`, `*.md`, `.git/`, `docker-compose.yml`, `Dockerfile`. + +### Lahrcarpetcleaning.com Reference + +```yaml +--- +deployment: + tasks: + - export DEPLOYPATH=/home/dev1communitypro/public_html/lahrcarpetcleaning.dev1.communityproud.com/ + - /bin/cp -r assets $DEPLOYPATH + - /bin/cp -r about $DEPLOYPATH + - /bin/cp -r commercial $DEPLOYPATH + - /bin/cp -r contact $DEPLOYPATH + - /bin/cp -r locations $DEPLOYPATH + - /bin/cp -r reviews $DEPLOYPATH + - /bin/cp -r service-area $DEPLOYPATH + - /bin/cp -r services $DEPLOYPATH + - /bin/cp index.html $DEPLOYPATH + - /bin/cp 404.html $DEPLOYPATH + - /bin/cp robots.txt $DEPLOYPATH + - /bin/cp sitemap.xml $DEPLOYPATH +``` + +### Deploying After a Push + +1. Push to the connected remote (GitHub) +2. cPanel → Git Version Control → Manage → Pull or Deploy +3. cPanel runs the `.cpanel.yml` tasks, copying files to webroot +4. Apache serves from webroot automatically — no nginx, no Docker + +### Apache vs nginx + +cPanel hosts use Apache (not nginx). There is no nginx.conf to manage. +URL routing is handled by `.htaccess`: + +```apache +Options -Indexes +RewriteEngine On + +# Directory-style URLs: /services/carpet-cleaning/ → index.html inside that folder +# Apache handles this automatically with DirectoryIndex — no extra rules needed + +# Deny sensitive files +<FilesMatch "\.(py|yml|yaml|md|log|sh|env|conf|dockerfile)$"> + Order allow,deny + Deny from all +</FilesMatch> + +# Security headers +<IfModule mod_headers.c> + Header set X-Frame-Options "SAMEORIGIN" + Header set X-Content-Type-Options "nosniff" + Header set X-XSS-Protection "1; mode=block" + Header set Referrer-Policy "strict-origin-when-cross-origin" + Header set Permissions-Policy "geolocation=(), microphone=(), camera=()" + Header set Strict-Transport-Security "max-age=31536000; includeSubDomains" +</IfModule> + +ErrorDocument 404 /404.html +ErrorDocument 500 /500.html +``` + +### Cache Busting on cPanel + +Apache does not auto-invalidate cached assets. Bump `?v=N` on CSS/JS in +all HTML files after every asset change: + +```html +<link rel="stylesheet" href="/assets/css/styles.css?v=6"> +<script src="/assets/js/main.js?v=3"></script> +``` + +Increment by 1 on every change. Apply across ALL HTML pages. + +### Verify After cPanel Deploy + +```bash +curl -s -o /dev/null -w "home: %{http_code}\n" https://{domain}/ +curl -s -o /dev/null -w "css: %{http_code}\n" https://{domain}/assets/css/styles.css +curl -s -o /dev/null -w "404: %{http_code}\n" https://{domain}/page-that-does-not-exist +``` + +All public paths return 200. All non-existent paths return 404. + +### Universal Project Checklist (Both Paths) + +Every project must include ALL of these before first deploy: + +``` +Dockerfile ✓ Docker/VPS +docker-compose.yml ✓ Docker/VPS +nginx.conf ✓ Docker/VPS +.htaccess ✓ cPanel/Apache +.cpanel.yml ✓ cPanel Git +.dockerignore ✓ Docker build security +.gitignore ✓ keeps .env and secrets out of git +robots.txt ✓ both paths +sitemap.xml ✓ both paths +404.html ✓ both paths +500.html ✓ both paths +``` + +Lahrcarpetcleaning.com is the reference implementation for both paths. + +--- + +## Domain, Email, DNS, and Resend + +### Resend Account Setup + +1. Sign up at https://resend.com +2. Generate an API key (one per project): https://resend.com/api-keys +3. Save the key in the project's `api/.env` as `RESEND_API_KEY=re_xxxx` +4. NEVER commit `.env`. NEVER paste the key in Slack, GitHub, or chat logs. + +### Add and Verify the Sending Domain + +1. https://resend.com/domains → **Add Domain** +2. Enter the domain (the one you'll send FROM, not necessarily the website domain) +3. Resend gives 3-4 DNS records. Add them all in Cloudflare (or whatever DNS host) +4. Wait 5-15 minutes, click **Verify** in Resend — all records must show green + +### Records Resend Provides + +| Type | Name | Value | Proxy | TTL | +|------|------|-------|-------|-----| +| TXT | `resend._domainkey` | `p=...long-rsa-key...` | DNS only | 1 hr | +| TXT | `send` | `v=spf1 include:amazonses.com ~all` | DNS only | 1 hr | +| MX | `send` | `feedback-smtp.{region}.amazonses.com` priority 10 | DNS only | 1 hr | + +(Resend uses Amazon SES under the hood, hence `amazonses.com` in the SPF.) + +### DMARC — REQUIRED for Inbox Placement + +Without DMARC, Gmail flags otherwise-correctly-configured email as suspicious +and routes it to spam. Resend doesn't auto-create this record. You must add it. + +| Type | Name | Value | Proxy | TTL | +|------|------|-------|-------|-----| +| TXT | `_dmarc` | `v=DMARC1; p=none; rua=mailto:dev@{domain}` | DNS only | Auto | + +Components: +- `v=DMARC1` — declares a DMARC policy exists +- `p=none` — monitor mode, doesn't reject anything yet (safe to start) +- `rua=mailto:...` — DMARC failure reports go to this inbox (review weekly) + +After 30 days of clean DMARC reports with no false positives, optionally +upgrade to `p=quarantine` then `p=reject`. + +### Verify DNS is Live + +```bash +dig +short TXT resend._domainkey.{domain} @8.8.8.8 +dig +short TXT send.{domain} @8.8.8.8 +dig +short TXT _dmarc.{domain} @8.8.8.8 +dig +short MX send.{domain} @8.8.8.8 +``` + +All four should return their expected values. + +### From-Name Format + +Always use a friendly From name, not bare email. Bare email looks robotic +and triggers spam filters. + +``` +FROM_EMAIL=Brand Name <webleads@{domain}> +``` + +### TO-Email Setup + +The `TO_EMAIL` is wherever the lead actually goes. Often a Gmail group address +or the owner's personal inbox. + +- During Resend domain verification (BEFORE green): you can ONLY send TO the + email tied to the Resend account +- After verification: send to anyone + +For local testing without verification, use: +``` +FROM_EMAIL=onboarding@resend.dev +TO_EMAIL={your-resend-account-email} +``` + +### When Emails Go to Spam + +Run this checklist: + +1. **All 4 DNS records green at Resend**? If not, deliverability suffers. +2. **DMARC TXT record exists**? Most common cause of spam folder. +3. **Friendly From name**? `Brand Name <webleads@...>` not bare `webleads@...` +4. **Both `html` and `text` parts in the payload**? HTML-only is suspicious. +5. **Subject line clean**? No em-dashes, no "Estimate Request URGENT", no all-caps. +6. **Recipient marked first emails as Not Spam**? Train Gmail. + +### Cloudflare-Specific Notes + +The user-agent quirk — Cloudflare in front of Resend's API blocks Python's default +`User-Agent: Python-urllib/3.x`. Always set a custom `User-Agent` in the API request headers. + +If the DNS provider is Cloudflare, ensure all Resend records have **proxy status: DNS only** +(the gray cloud icon, not orange). Proxying these breaks authentication. + +### Annual Key Rotation + +Rotate Resend API keys annually: +1. Generate new key in Resend dashboard +2. Update `api/.env` on the server +3. `docker compose down && docker compose up -d` to reload env +4. Confirm a test submission still works +5. Revoke the old key in Resend dashboard + +### Resend HTTP 403 — Domain Not Verified + +A 403 from the Resend API does NOT mean the API key is wrong. The specific +error is: + +```json +{"statusCode":403,"message":"The {domain} domain is not verified. Please, add and verify your domain on https://resend.com/domains","name":"validation_error"} +``` + +This means the key is valid and authenticated, but the FROM domain has not +been added or verified at resend.com/domains yet. + +Rule: **verify the domain BEFORE testing the form endpoint.** If you test +before verification, `{"ok":false}` will be returned to the visitor even +though the API key is correct and the code is correct. + +Sequence: +1. Set `RESEND_API_KEY` in `.env` +2. Add domain at resend.com/domains +3. Add DNS records in Cloudflare +4. Wait for green verification +5. Then test the form endpoint + +### DKIM Key Rotation + +Resend periodically rotates DKIM keys. They send email when this happens. Add +the new `resend2._domainkey` (or whichever selector they specify) TXT record +in Cloudflare, then click verify. Old key remains active until they remove it. + +--- + +## Form Handling — Resend + +Static sites can't send email by themselves. Every project that needs a +contact form gets a small Python service running in its own Docker container, +proxied by nginx. + +### Architecture + +``` +Browser → POST /api/estimate (vanilla JS fetch in form.js) + ↓ +nginx → proxies /api/ to api:3001 (strips /api/ prefix) + ↓ +Python service (server.py, stdlib only) + - Validates fields server-side + - Verifies reCAPTCHA v3 with Google + - Sends via Resend HTTPS API + - Returns {ok: true} or {error: ...} +``` + +### Front-End (Vanilla JS) + +`assets/js/form.js`: + +- Real-time validation (blur events) +- Phone formatting `(###) ###-####` +- Email regex check +- Required-field check +- Async submit to `/api/estimate` with JSON body +- Disable submit button + show "Sending..." during request +- Show success/error message in `.form-status` span +- Reset form on success +- reCAPTCHA v3 token fetched before submit and included in body + +### Back-End (Python stdlib) + +`api/server.py` (skeleton): + +```python +#!/usr/bin/env python3 +import hashlib, http.server, json, os, re, socketserver, time +import urllib.parse, urllib.request + +PORT = int(os.environ.get("PORT", "3001")) +RESEND_API_KEY = os.environ.get("RESEND_API_KEY", "") +RECAPTCHA_SECRET = os.environ.get("RECAPTCHA_SECRET", "") +TO_EMAIL = os.environ.get("TO_EMAIL", "") +FROM_EMAIL = os.environ.get("FROM_EMAIL", "") +RECAPTCHA_MIN = float(os.environ.get("RECAPTCHA_MIN", "0.5")) + +PHONE_RE = re.compile(r"^\(?\d{3}\)?[\s.\-]?\d{3}[\s.\-]?\d{4}$") +EMAIL_RE = re.compile(r"^[^\s@]+@[^\s@]+\.[^\s@]+$") + +# Rate limit: 5 requests / IP / 15 minutes +RATE_MAP = {} +RATE_WINDOW = 15 * 60 +RATE_MAX = 5 + +def sanitize(s): + if not isinstance(s, str): return "" + return s.replace("&","&").replace("<","<").replace(">",">").replace('"',""").strip()[:2000] + +def validate_fields(body): + errors = [] + if not body.get("name") or len((body["name"]).strip()) < 2: errors.append("name") + if not EMAIL_RE.match((body.get("email") or "").strip()): errors.append("email") + if not PHONE_RE.match((body.get("phone") or "").replace(" ", "")): errors.append("phone") + return errors + +def verify_recaptcha(token): + if not RECAPTCHA_SECRET or not token: return 0.0 + data = urllib.parse.urlencode({"secret": RECAPTCHA_SECRET, "response": token}).encode() + req = urllib.request.Request("https://www.google.com/recaptcha/api/siteverify", data=data) + try: + with urllib.request.urlopen(req, timeout=8) as resp: + return float(json.loads(resp.read()).get("score", 0)) + except Exception: + return 0.0 + +def send_via_resend(fields): + safe = {k: sanitize(fields.get(k,"")) for k in ["name","email","phone","address","city","zip","service","condition","message"]} + html = f"""<!DOCTYPE html>...{safe['name']}...""" + text = f"New estimate request\n\nName: {safe['name']}\n..." + payload = json.dumps({ + "from": FROM_EMAIL, + "to": [TO_EMAIL], + "reply_to": fields.get("email","").strip(), + "subject": f"New estimate request: {safe['name']} ({safe['city']})", + "html": html, "text": text, + }).encode("utf-8") + idem = hashlib.sha256(payload).hexdigest()[:64] + req = urllib.request.Request("https://api.resend.com/emails", data=payload, headers={ + "Authorization": f"Bearer {RESEND_API_KEY}", + "Content-Type": "application/json", + "Idempotency-Key": idem, + "User-Agent": "{Brand}-Estimate-Form/1.0", + }) + try: + with urllib.request.urlopen(req, timeout=10) as resp: + if resp.status >= 300: raise RuntimeError(f"Resend {resp.status}: {resp.read().decode('utf-8','ignore')}") + except urllib.error.HTTPError as e: + raise RuntimeError(f"Resend {e.code}: {e.read().decode('utf-8','ignore')}") from None +``` + +Reference implementation: `floorithardwoodfloors.com/api/server.py`. + +### Critical: User-Agent Header + +When calling the Resend API from Python, you MUST set a non-default User-Agent. +Cloudflare (which fronts Resend) blocks Python's default `Python-urllib/3.x` +with HTTP 403 / Cloudflare error code 1010. + +```python +"User-Agent": "{ProjectName}-Form/1.0" +``` + +### Idempotency + +Every Resend request includes an `Idempotency-Key` header set to the SHA-256 +of the payload (truncated to 64 chars). Identical payloads within 24 hours +are deduplicated by Resend automatically. This prevents: +- Double-clicks creating two leads +- Browser retries after a network blip +- Honest user submitting twice + +### Security Checklist + +- API key in `.env` file, NOT in source control. `.gitignore` it. +- API key NEVER reaches the browser bundle (only the server has it) +- `.env` file lives in `api/`, NOT in the nginx web root +- Server-side validation on EVERY field — never trust client +- HTML-escape every field rendered into the email body to prevent injection +- Rate limit per IP (5 / 15 min default) +- 16 KB body cap — reject anything larger +- 10-second upstream timeout — don't hold connections open +- CORS locked to the production domain only (`Access-Control-Allow-Origin: https://{domain}`) +- reCAPTCHA v3 with score threshold (default 0.5) once secret is configured + +### Environment Variables + +`api/.env`: +``` +RESEND_API_KEY=re_xxxxxxxxxxxx +RECAPTCHA_SECRET=6Ldq... +TO_EMAIL=leads@{domain} +FROM_EMAIL=Brand Name <webleads@{domain}> +RECAPTCHA_MIN=0.5 +PORT=3001 +``` + +`api/.env.example` (committed) is the same file with placeholder values. + +### reCAPTCHA Setup + +1. Create site at https://www.google.com/recaptcha/admin +2. Type: **reCAPTCHA v3** (not v2) +3. Add your domain +4. Copy the **site key** into `assets/js/form.js`: + ```js + const RECAPTCHA_SITE_KEY = '6Ldq...'; + ``` +5. Add the script tag to pages with the form: + ```html + <script src="https://www.google.com/recaptcha/api.js?render=6Ldq..."></script> + ``` +6. Copy the **secret key** into `api/.env` as `RECAPTCHA_SECRET` + +### Deliverability Checklist + +When emails are landing in spam: +1. Verify Resend domain is fully green (SPF + DKIM + DMARC) +2. From name set, not bare email: `Brand Name <webleads@{domain}>` +3. Both `html` and `text` parts in every Resend payload (no HTML-only) +4. Subject line is descriptive, no em-dash, no spam-trigger words +5. Recipient marks first 2-3 emails as "Not Spam" in Gmail to train the filter + +### Testing + +```bash +# Validation rejection (expect 422) +curl -X POST http://localhost:8096/api/estimate \ + -H "Content-Type: application/json" \ + -d '{"name":"","email":"bad"}' + +# Full valid submission (expect 200, real email sent) +curl -X POST http://localhost:8096/api/estimate \ + -H "Content-Type: application/json" \ + -d '{"name":"Test","email":"test@example.com","phone":"(716) 555-1234","address":"100 Test St","city":"Buffalo","zip":"14201","service":"refinishing","message":"Test","token":""}' +``` + +The first real test email confirms end-to-end works. + +--- + +## PHP App Stack (Server-Side Processing) + +Use this pattern when a project requires server-side processing that static HTML cannot handle: file conversion, at-rest encryption, payment processing, user authentication, or API-gated features. + +**Reference implementation:** `quickconvert.us` + +### When to Use This Pattern + +- File uploads and processing (image conversion, PDF generation, etc.) +- At-rest encryption of user data +- Payment processing with Stripe subscriptions +- User authentication with magic link or password-based login +- Rate-limited APIs that must be server-enforced + +**Do not** introduce this pattern just to add a contact form. Use the Python stdlib form service instead. + +### Stack + +- **PHP 8.3** (php:8.3-fpm-alpine base image) +- **Nginx** (Alpine package, same container via supervisord) +- **SQLite** (pdo_sqlite extension, no separate DB container needed) +- **libsodium** (built into PHP 8.x — use for all encryption) +- **ImageMagick** (pecl imagick for image processing) +- **msmtp** (SMTP relay for outbound email) +- **supervisord** (manages nginx + php-fpm + crond in one container) + +### Project Structure + +``` +project/ +├── src/ ← nginx document root +│ ├── index.php +│ ├── api/ +│ │ ├── convert.php ← POST endpoint (CSRF + reCAPTCHA protected) +│ │ └── download.php ← GET endpoint (signed token) +│ ├── assets/css/ +│ ├── assets/js/ +│ └── assets/images/ +├── includes/ ← PHP classes (above doc root, not web-accessible) +│ ├── bootstrap.php ← constants, session, autoload +│ ├── auth.php ← login, register, magic token +│ ├── csrf.php +│ ├── db.php ← SQLite PDO wrapper +│ ├── encryption.php ← libsodium wrappers +│ └── mailer.php +├── components/ +│ ├── header.php +│ └── footer.php +├── storage/ ← volume-mounted, NOT in docker image +│ ├── uploads/ ← encrypted .enc files only +│ ├── converted/ +│ ├── temp/ +│ ├── .htaccess ← deny all direct access +│ └── {app}.db +├── infra/ +│ ├── nginx.conf +│ ├── php.ini +│ ├── supervisord.conf +│ └── docker-entrypoint.sh +├── tools/ +│ └── cleanup.php ← cron: delete expired tokens + files +├── Dockerfile +├── docker-compose.yml +└── .env ← gitignored, never committed +``` + +### Security Requirements (Non-Negotiable) + +**CSRF** — every POST form and API endpoint must verify a CSRF token tied to the session. + +**Rate limiting** — two layers: +1. nginx: `limit_req_zone` on /api/ (10 req/s, burst 20) +2. PHP: per-IP daily counter in SQLite rate_limits table + +**reCAPTCHA v3** — on conversion/upload endpoints. Verify server-side via Google API. Cache result in session (verify once per session, not per request). + +**At-rest encryption** — any user-uploaded file must be encrypted before writing to disk. Use `sodium_crypto_secretstream_xchacha20poly1305_*` for files, `sodium_crypto_secretbox` for strings. Key stored in `.env` as `QC_ENCRYPTION_KEY` (32 bytes hex). + +**Signed download tokens** — never expose file paths. Issue a 64-char hex token stored in SQLite with expiry and single-use enforcement. + +**Magic link auth** — prefer magic link over password. On register: create account unverified, send verify email, block login until verified. Token: 64-char hex, 1-hour expiry, stored in `magic_tokens` table, consumed on use. + +### Nginx Security Headers + +```nginx +add_header X-Frame-Options "SAMEORIGIN" always; +add_header X-Content-Type-Options "nosniff" always; +add_header Referrer-Policy "strict-origin-when-cross-origin" always; +add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; +add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.google.com https://www.gstatic.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; object-src 'none'; base-uri 'self'; form-action 'self' https://checkout.stripe.com;" always; + +# Stripe webhook — POST only +location = /api/stripe-webhook.php { + limit_except POST { deny all; } +} + +# Block dotfiles +location ~ /\. { deny all; return 403; } +``` + +### Database Schema Pattern (SQLite, Idempotent) + +Use `CREATE TABLE IF NOT EXISTS` for all tables. Use `ALTER TABLE ... ADD COLUMN` wrapped in try/catch for schema migrations. + +```php +try { $pdo->exec("ALTER TABLE users ADD COLUMN verified_at INTEGER DEFAULT NULL"); } +catch (Throwable $e) { /* column already exists */ } +``` + +### Stripe Integration + +- Checkout: create session server-side, redirect to Stripe-hosted page +- Webhook: verify `Stripe-Signature` header using HMAC-SHA256 (implement without Stripe SDK — use curl) +- Webhook tolerance: 300 seconds (5 min) on timestamp +- Register webhook endpoint at: `https://{domain}/api/stripe-webhook.php` +- Events to subscribe: `checkout.session.completed`, `customer.subscription.created`, `customer.subscription.updated`, `customer.subscription.deleted`, `invoice.payment_succeeded`, `invoice.payment_failed` + +### .env Required Vars + +``` +APP_ENV=production +BASE_URL=https://{domain} +QC_ENCRYPTION_KEY={32-bytes-hex} +STRIPE_MODE=live +STRIPE_LIVE_SECRET_KEY=sk_live_... +STRIPE_LIVE_PUBLISHABLE_KEY=pk_live_... +STRIPE_WEBHOOK_SECRET=whsec_... +STRIPE_PRICE_ID=price_... +RECAPTCHA_SITE_KEY=... +RECAPTCHA_SECRET_KEY=... +SMTP_HOST=... +SMTP_PORT=587 +SMTP_USER=... +SMTP_PASS=... +MAIL_FROM=noreply@{domain} +MAIL_FROM_NAME={Brand} +``` + +Generate encryption key: `php -r "echo bin2hex(random_bytes(32));"` diff --git a/build/__pycache__/seed_sops.cpython-313.pyc b/build/__pycache__/seed_sops.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c099e932617f04c7ec7d1f7b251ade719e9e3f79 GIT binary patch literal 9039 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 N^{yBZyH#P}e*vQGb@c!M literal 0 HcmV?d00001 diff --git a/build/seed_sops.py b/build/seed_sops.py new file mode 100644 index 0000000..03d2139 --- /dev/null +++ b/build/seed_sops.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 + +import sqlite3 +import glob +import os +import re +from datetime import datetime + +DB_PATH = "/home/sirdrez/arisingmedia-websites/.am-webdesign-sops/sops.db" +SOP_DIR = "/home/sirdrez/arisingmedia-websites/.am-webdesign-sops" + +def init_db(): + """Initialize database with fresh schema.""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + # Drop tables in reverse dependency order + cursor.execute("DROP TABLE IF EXISTS sop_fts") + cursor.execute("DROP TABLE IF EXISTS rules") + cursor.execute("DROP TABLE IF EXISTS sop_sections") + cursor.execute("DROP TABLE IF EXISTS sops") + + # Create tables + cursor.execute(""" + CREATE TABLE sops ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + number TEXT, + filename TEXT, + title TEXT, + full_content TEXT, + updated_at TEXT + ) + """) + + cursor.execute(""" + CREATE TABLE sop_sections ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + sop_id INTEGER REFERENCES sops(id), + heading_level INTEGER, + title TEXT, + content TEXT + ) + """) + + cursor.execute(""" + CREATE TABLE rules ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + category TEXT, + rule TEXT, + source_sop TEXT, + source_section TEXT + ) + """) + + cursor.execute(""" + CREATE VIRTUAL TABLE sop_fts USING fts5( + sop_number, + sop_title, + section_title, + content + ) + """) + + conn.commit() + return conn + +def extract_number_from_filename(filename): + """Extract number prefix from filename (e.g., '00' from '00-stack-philosophy.md').""" + match = re.match(r'^(\d+)', filename) + if match: + return match.group(1) + return "" + +def extract_first_heading(content): + """Extract first line starting with # as title.""" + for line in content.split('\n'): + if line.startswith('#'): + return line.lstrip('#').strip() + return "" + +def split_into_sections(content): + """Split content into sections by ## or ### headings.""" + sections = [] + current_section = None + current_content = [] + + lines = content.split('\n') + + for line in lines: + if line.startswith('##'): + # Save previous section if exists + if current_section: + current_section['content'] = '\n'.join(current_content).strip() + sections.append(current_section) + + # Determine heading level + heading_level = 2 + if line.startswith('###'): + heading_level = 3 + + current_section = { + 'heading_level': heading_level, + 'title': line.lstrip('#').strip() + } + current_content = [] + elif current_section: + current_content.append(line) + + # Save last section + if current_section: + current_section['content'] = '\n'.join(current_content).strip() + sections.append(current_section) + + return sections + +def extract_rules_from_section(section_title, section_content, category_map): + """Extract rules from section if title matches keyword patterns.""" + title_lower = section_title.lower() + rules = [] + + # Determine category + category = None + if any(keyword in title_lower for keyword in ['never use', 'mandatory', 'rules', 'what we never']): + if 'never' in title_lower: + category = 'never_use' + elif 'mandatory' in title_lower or 'pattern' in title_lower: + category = 'mandatory' + + if not category: + return rules + + # Extract bullet points + for line in section_content.split('\n'): + stripped = line.strip() + if stripped.startswith('-') or stripped.startswith('*'): + rule_text = stripped.lstrip('-*').strip() + if rule_text: + rules.append({ + 'category': category, + 'rule': rule_text + }) + + return rules + +def process_sop_files(conn): + """Process all .md files and populate database.""" + cursor = conn.cursor() + + # Get all .md files in top level only + md_files = glob.glob(os.path.join(SOP_DIR, "*.md")) + md_files.sort() + + sop_count = 0 + section_count = 0 + rule_count = 0 + + for filepath in md_files: + filename = os.path.basename(filepath) + + # Skip certain files + if filename in ['README.md', 'STACK.md', 'CONTENT.md', 'OPTIMIZATION.md']: + continue + + with open(filepath, 'r', encoding='utf-8') as f: + full_content = f.read() + + # Extract metadata + number = extract_number_from_filename(filename) + title = extract_first_heading(full_content) + updated_at = datetime.now().isoformat() + + # Insert SOP record + cursor.execute(""" + INSERT INTO sops (number, filename, title, full_content, updated_at) + VALUES (?, ?, ?, ?, ?) + """, (number, filename, title, full_content, updated_at)) + + sop_id = cursor.lastrowid + sop_count += 1 + + # Split into sections and insert + sections = split_into_sections(full_content) + + for section in sections: + cursor.execute(""" + INSERT INTO sop_sections (sop_id, heading_level, title, content) + VALUES (?, ?, ?, ?) + """, (sop_id, section['heading_level'], section['title'], section['content'])) + + section_count += 1 + + # Extract rules from section + rules = extract_rules_from_section(section['title'], section['content'], {}) + + for rule in rules: + cursor.execute(""" + INSERT INTO rules (category, rule, source_sop, source_section) + VALUES (?, ?, ?, ?) + """, (rule['category'], rule['rule'], filename, section['title'])) + + rule_count += 1 + + conn.commit() + return sop_count, section_count, rule_count + +def rebuild_fts(conn): + """Rebuild FTS index.""" + cursor = conn.cursor() + + cursor.execute(""" + INSERT INTO sop_fts(sop_number, sop_title, section_title, content) + SELECT s.number, s.title, ss.title, ss.content + FROM sop_sections ss JOIN sops s ON ss.sop_id = s.id + """) + + conn.commit() + +def main(): + """Main entry point.""" + try: + conn = init_db() + sop_count, section_count, rule_count = process_sop_files(conn) + rebuild_fts(conn) + conn.close() + + print(f"SOP Database built successfully:") + print(f" SOPs loaded: {sop_count}") + print(f" Sections indexed: {section_count}") + print(f" Rules extracted: {rule_count}") + print(f" Database: {DB_PATH}") + + except Exception as e: + print(f"Error: {e}") + import traceback + traceback.print_exc() + exit(1) + +if __name__ == "__main__": + main() diff --git a/image-gen-workflow/00-workflow-overview.md b/image-gen-workflow/00-workflow-overview.md new file mode 100644 index 0000000..7fdbe6b --- /dev/null +++ b/image-gen-workflow/00-workflow-overview.md @@ -0,0 +1,202 @@ +# Image Generation Workflow — Arising Media + +Last updated: 2026-05-10 +Project reference: cobhamtech.com (first full run) + +--- + +## Purpose + +Standardized process for generating, validating, and deploying AI images across all Arising Media client static sites. Every decision made in this workflow is documented so any agent or session can continue without context loss. + +--- + +## Stack + +API: Google Gemini (generativelanguage.googleapis.com) +SDK: google-genai (NOT the deprecated google-generativeai package) +Draft model: gemini-2.5-flash-image (Nano Banana — Speed Mode) +Final model: imagen-4.0-generate-001 (Imagen 4 — Quality Mode) +Format: JPEG, 85% quality, max 1600px wide + +--- + +## Phase 1 — Site Analysis (before any generation) + +Before generating images, read: +- index.html (home page structure) +- All CSS files (understand existing color tokens, dark/light sections) +- About, services, contact pages (identify where images add value) + +Map each candidate image slot: +- What HTML section will it go in? +- Is it a CSS background-image or an inline img tag? +- What overlay/treatment is needed for text readability? +- What dimensions/aspect ratio does the slot require? + +Document this in: 01-model-selection.md (image plan table) + +--- + +## Phase 2 — Prompt Engineering + +### Rules +- Always reference the site color palette in the prompt (dark navy, slate blue, gold accents) +- Specify "no text" and "no logos" for background images +- Specify "photorealistic" for all marketing images +- NO PEOPLE. NO FACES. Hardware, infrastructure, and environment only across all client sites +- This applies to all slots: hero, about, services, contact, location — no exceptions +- Reason: faces introduce identity/representation risk and age poorly. Hardware stays neutral and professional. + +### Prompt structure +[Subject] + [Environment] + [Lighting] + [Mood/Tone] + [Technical quality terms] + [Exclusions] + +### Example (hero background) +"Professional enterprise server room, long corridor of dark rack servers with blue LED ambient lighting, deep perspective, dark navy background, cinematic shallow depth of field, no people, photorealistic, ultra detailed" + +### cobhamtech.com brand prompt additions +Always append to prompts for this client: +"dark navy and blue ambient lighting, professional, enterprise, no text" + +--- + +## Phase 3 — Generation Script Pattern + +```python +from google import genai +from google.genai import types + +client = genai.Client(api_key='KEY') + +response = client.models.generate_images( + model='imagen-4.0-generate-001', + prompt='PROMPT', + config=types.GenerateImagesConfig( + number_of_images=1, + aspect_ratio='16:9', # 16:9 | 4:3 | 3:2 | 1:1 | 9:16 + output_mime_type='image/jpeg', + ) +) + +with open('output.jpg', 'wb') as f: + f.write(response.generated_images[0].image.image_bytes) +``` + +Validate: file must be > 10,000 bytes. Anything smaller is an API error or empty response. + +CRITICAL — Vision validation is mandatory before saving any image: +The toolbox script (ai-imagen-generate.sh) automatically sends each generated image to +gemini-2.0-flash for visual inspection. It asks: "Does this image contain people, faces, +hands, silhouettes, or body parts?" If YES — the image is rejected, prompt is tightened, +and generation retries up to 3 times. Only images that pass inspection are saved. +Claude cannot visually inspect images — the vision validation step is the enforcement gate. + +--- + +## Phase 4 — Placement Patterns + +### Pattern A: CSS background-image with dark overlay (hero sections) + +Used when: image sits behind text on a dark section +Implementation: CSS only, no HTML change + +```css +.ct-hero { + background: var(--ct-black); /* fallback */ + background-image: linear-gradient(rgba(12,15,24,0.82), rgba(12,15,24,0.92)), url('/assets/images/hero-bg.jpg'); + background-size: cover; + background-position: center; +} +``` + +Overlay opacity guide: +- 0.82/0.92 = subtle image visible, text fully readable +- 0.90/0.95 = very subtle texture only +- 0.70/0.80 = image prominent (use only if no text overlay) + +### Pattern B: Inline img tag (editorial sections) + +Used when: image is a standalone visual element between content sections +Implementation: add img tag + container div + +```html +<div class="container" style="padding-bottom: var(--space-lg);"> + <img src="assets/images/intro-visual.jpg" + alt="Descriptive alt text" + style="width:100%;display:block;max-height:400px;object-fit:cover;"> +</div> +``` + +### Pattern C: Grid column image (about/story sections) + +Used when: image shares a row with text content +Implementation: add img to existing grid + expand grid columns + +```html +<!-- Expand grid to: grid-template-columns: 1fr 1fr 420px --> +<div> + <img src="assets/images/about-visual.jpg" + alt="Alt text" + style="width:100%;display:block;border-radius:4px;"> +</div> +``` + +--- + +## Phase 5 — CSP and nginx Updates + +Any new image source domain requires a CSP update in nginx.conf. +For Google Maps tiles: add `https://*.googleapis.com https://*.gstatic.com` to `img-src` +For self-hosted images: `img-src 'self' data:` is sufficient — no change needed + +--- + +## Phase 6 — Docker Rebuild and Verify + +After every image + HTML change: + +```bash +cd /home/sirdrez/arisingmedia-websites/[client] +docker stop [container-name] +docker rm [container-name] +docker build -t [image-name] . +docker run -d --name [container-name] -p [port]:80 [image-name] +sleep 2 +curl -s -o /dev/null -w "%{http_code}" http://localhost:[port]/ +``` + +Verify image loads: `curl -s -o /dev/null -w "%{http_code}" http://localhost:[port]/assets/images/hero-bg.jpg` +Expected: 200 with Content-Type: image/jpeg + +--- + +## File Naming Convention + +Pattern: `{page}-{slot}.jpg` + +| Slot | File name | Aspect | +|------|-----------|--------| +| Home hero background | hero-bg.jpg | 16:9 | +| Home intro visual | intro-visual.jpg | 3:2 | +| About story | about-visual.jpg | 4:3 | +| Services hub header | services-bg.jpg | 16:9 | +| Contact page | contact-bg.jpg | 16:9 | +| Location page | location-bg.jpg | 16:9 | + +--- + +## Logging Requirement + +Every generation run must produce a log entry in: +`am-webdesign-sops/image-gen-workflow/02-generation-log.md` + +Log must include: date, client, model, each image file name, prompt used, file size in bytes, placement pattern used, Docker rebuild result. + +--- + +## Cobhamtech.com Run Reference + +Container: cobhamtech-site +Port: 8010 +Assets path: /home/sirdrez/arisingmedia-websites/cobhamtech.com/assets/images/ +Color tokens: --ct-black #0c0f18 / --ct-slate #1c2d42 / --ct-blue #2d5a9e / --ct-gold #c79330 diff --git a/image-gen-workflow/01-model-selection.md b/image-gen-workflow/01-model-selection.md new file mode 100644 index 0000000..7286659 --- /dev/null +++ b/image-gen-workflow/01-model-selection.md @@ -0,0 +1,89 @@ +# Image Generation Model Selection + +Source: cutout.pro/model-comparison/imagen-vs-nanobanana + Gemini API model audit (2026-05-10) + +--- + +## Available Models (via Google Gemini API) + +### Imagen 4 — Quality Mode +Model ID: `imagen-4.0-generate-001` +Also available: `imagen-4.0-ultra-generate-001` + +Strengths: +- Photorealistic, high-fidelity output +- Handles complex prompts with multi-element consistency +- Superior text rendering inside images +- Best for brand-critical, final-delivery assets + +Use for: +- Hero background images +- Service page headers +- Marketing and case study visuals +- Any image that ships to production + +--- + +### Nano Banana (Gemini 2.5 Flash Image) — Speed Mode +Model ID: `gemini-2.5-flash-image` + +Strengths: +- Low latency, high volume +- Cost-effective for rapid iteration +- Good for concept previews and brainstorming + +Use for: +- Draft previews before committing to Imagen 4 +- AI chatbot or interactive UI image generation +- Avatar or thumbnail generation at scale +- Rapid iteration when exploring compositions + +--- + +### Imagen 4 Fast — Budget Mode +Model ID: `imagen-4.0-fast-generate-001` + +Use for: +- Quick internal previews +- Non-public-facing visuals +- High-volume batch jobs where quality is secondary + +--- + +## Recommended Workflow + +Step 1 — Draft with Speed Mode (`gemini-2.5-flash-image`) +Generate 2-4 variations quickly. Confirm composition, subject, and tone. Low cost. + +Step 2 — Refine with Quality Mode (`imagen-4.0-generate-001`) +Take the winning prompt from step 1. Generate final version at full quality. +This is the image that goes into the site. + +Step 3 — Review against brand palette +Check that image tones align with site color tokens: +- cobhamtech.com: dark navy (#0c0f18), slate (#1c2d42), blue accent (#2d5a9e), gold (#c79330) +- All hero images need to work behind dark overlays + +Step 4 — Save to project assets +Path convention: `assets/images/{page}-{slot}.jpg` +Examples: `hero-bg.jpg`, `about-visual.jpg`, `services-bg.jpg` + +--- + +## Cobhamtech.com Image Plan + +| Slot | File | Page | Prompt Theme | +|------|------|------|--------------| +| Hero background | `hero-bg.jpg` | index.html | Dark server room, blue ambient lighting, depth of field | +| About story | `about-visual.jpg` | about.html | IT professional at clean desk, dual monitors, neutral dark background | +| Services hub | `services-bg.jpg` | services/index.html | Enterprise network infrastructure, abstract, dark | +| Intro visual | `intro-visual.jpg` | index.html | Business and technology handshake, professional setting | + +--- + +## Notes + +- Never use Nano Banana for final production images on client sites +- Imagen 4 Ultra adds marginal quality gain over standard — not worth the cost for web assets +- All images should be exported as JPEG at 85% quality, max 1600px wide, for web performance +- Run generated images through the site CSP — ensure `img-src` allows `self` and `data:` only (no external CDN hotlinking) diff --git a/image-gen-workflow/archive/02-generation-log.md b/image-gen-workflow/archive/02-generation-log.md new file mode 100644 index 0000000..e9e38ca --- /dev/null +++ b/image-gen-workflow/archive/02-generation-log.md @@ -0,0 +1,71 @@ +# Image Generation Log — CobhamTech.com + +**Date:** 2026-05-10 +**Model:** imagen-4.0-generate-001 (Gemini Imagen 4) +**SDK:** google-genai (Python) +**API Key:** AIzaSyD-njx1-hyqnazckGTJ6SnMJ8o_B2C0UsI +**Script:** generate_images.py (deleted after run) + +--- + +## Images Generated + +### hero-bg.jpg +- **Prompt:** Professional enterprise server room, long corridor of dark rack servers with blue LED ambient lighting, deep perspective, dark navy background, cinematic shallow depth of field, no people, photorealistic, ultra detailed +- **Aspect ratio:** 16:9 +- **File size:** 395,927 bytes +- **Placement:** .ct-hero background-image in assets/css/page-home.css — overlay gradient rgba(12,15,24,0.82) to rgba(12,15,24,0.92), background-size cover +- **Status:** OK + +### about-visual.jpg +- **Prompt:** Professional IT consultant at a clean modern workstation, dual monitors displaying network diagrams and dashboards, dark office with subtle blue ambient lighting, business attire, confident expression, photorealistic +- **Aspect ratio:** 4:3 +- **File size:** 426,565 bytes +- **Placement:** about.html ct-about-story section — third column, grid-template-columns updated to 1fr 1fr 420px, img tag with border-radius 4px +- **Status:** OK + +### services-bg.jpg +- **Prompt:** Abstract enterprise technology network, dark background, glowing blue interconnected nodes and data pathways, minimal high-tech aesthetic, no text, no people, cinematic, photorealistic render +- **Aspect ratio:** 16:9 +- **File size:** 403,142 bytes +- **Placement:** .ct-svc-idx-hero background-image in assets/css/page-services-index.css — same overlay pattern as hero-bg +- **Status:** OK + +### intro-visual.jpg +- **Prompt:** Business professional and IT consultant collaborating at a modern conference table with laptops and tablets, professional corporate office, clean neutral dark background, photorealistic, teamwork and trust +- **Aspect ratio:** 4:3 (retried — original 3:2 not supported) +- **File size:** 373,852 bytes +- **Placement:** index.html — div.container block between ct-intro section and ct-home-sec-services, max-height 400px object-fit cover +- **Status:** OK + +--- + +## API Errors / Retries + +- intro-visual.jpg failed on first attempt with aspect ratio 3:2: `aspectRatio 3:2 is not supported. Supported values are 1:1, 9:16, 16:9, 4:3, 3:4.` +- Retried with 4:3. Succeeded. + +## Supported Aspect Ratios (Imagen 4) + +1:1, 9:16, 16:9, 4:3, 3:4 + +3:2 is NOT supported. Use 4:3 as the closest substitute for landscape-medium compositions. + +--- + +## Docker + +- Rebuilt cobhamtech-static image from scratch after HTML/CSS changes +- Container running on port 8010 +- All 4 images confirmed HTTP 200 at runtime +- Homepage HTTP 200 + +--- + +## Lessons Learned + +1. Imagen 4 does not support 3:2 aspect ratio. The supported set is: 1:1, 9:16, 16:9, 4:3, 3:4. Always validate aspect ratios before scripting a batch. +2. Generation of 4 images (3 x 16:9, 1 x 4:3) completed in under 90 seconds total. +3. Dark overlay gradients (rgba at 0.82-0.92 opacity) are necessary on these photorealistic images to maintain text legibility against white hero text. +4. File sizes ranged 374KB-427KB for JPEG output at these aspect ratios — appropriate for web use without additional compression. +5. The google-genai SDK uses `client.models.generate_images()` with a `GenerateImagesConfig` object — not the `generate_content()` path. diff --git a/image-gen-workflow/archive/cobhamtech-image-requests.json b/image-gen-workflow/archive/cobhamtech-image-requests.json new file mode 100644 index 0000000..70c4c2f --- /dev/null +++ b/image-gen-workflow/archive/cobhamtech-image-requests.json @@ -0,0 +1,22 @@ +[ + { + "file": "hero-bg.jpg", + "aspect": "16:9", + "prompt": "Long corridor of enterprise server racks in a dark data center, blue and white LED indicator lights blinking on rack units, cable management arms, deep perspective vanishing point, dark navy ambient lighting, no people, no humans, hardware only, photorealistic, 8K detail" + }, + { + "file": "about-visual.jpg", + "aspect": "4:3", + "prompt": "Dense fiber optic patch panel with multicolored LC connectors and cables, server rack mounted in data center, LED status lights green and blue, dark background, close-up macro shot, no people, no hands, hardware only, photorealistic, sharp focus" + }, + { + "file": "services-bg.jpg", + "aspect": "16:9", + "prompt": "Overhead view of enterprise network switches and routers mounted in open server rack, Ethernet cables organized in bundles, blue port indicator lights, dark equipment, clean cable management, no people, hardware only, photorealistic, professional data center" + }, + { + "file": "intro-visual.jpg", + "aspect": "4:3", + "prompt": "Cisco network switches and firewall appliances in a wall-mounted server cabinet, blinking LED activity lights, dark navy background, organized cable bundles, cooling vents visible, no people, no human presence, hardware only, photorealistic, enterprise IT infrastructure" + } +] diff --git a/image-gen-workflow/archive/select_hero_images.py b/image-gen-workflow/archive/select_hero_images.py new file mode 100644 index 0000000..c38383e --- /dev/null +++ b/image-gen-workflow/archive/select_hero_images.py @@ -0,0 +1,317 @@ +#!/usr/bin/env python3 +"""Select and regenerate hero images — Carbon Fiber Support CMS. + +- Click a thumbnail to select it (gold border = selected) +- Click Regen under any thumbnail to regenerate just that variant +- Save → writes selections.json for Webflow upload + +Usage: + python3 select_hero_images.py +""" +import json +import os +import subprocess +import threading +import tkinter as tk +from pathlib import Path +from PIL import Image, ImageTk + +SOURCED_DIR = Path("/home/sirdrez/Downloads/Carbon Fiber Support_PDF/image-rendering/Application_Problems/grounded/sourced") +GENERATOR = Path("/home/sirdrez/Downloads/Carbon Fiber Support_PDF/generate_sourced_photoreal.py") +ENV_FILE = Path("/home/sirdrez/Downloads/Carbon Fiber Support_PDF/.env") +SELECTIONS_OUT = Path(__file__).parent / "selections.json" + +APPLICATIONS = [ + ("bowing-basement-wall-repair", "Bowing Basement Wall Repair"), + ("horizontal-basement-wall-cracks", "Horizontal Basement Wall Cracks"), + ("parking-garage-column-wrapping", "Parking Garage Column Wrapping"), + ("bridge-girder-strengthening", "Bridge Girder Strengthening"), + ("stair-step-foundation-cracks", "Stair-Step Foundation Cracks"), + ("parking-garage-deck-repair", "Parking Garage Deck Repair"), + ("vertical-foundation-cracks", "Vertical Foundation Cracks"), + ("poured-concrete-wall-repair", "Poured Concrete Wall Repair"), + ("interior-block-wall-bulging", "Interior Block Wall Bulging"), + ("foundation-wall-repair", "Foundation Wall Repair"), + ("crawlspace-wall-reinforcement", "Crawlspace Wall Reinforcement"), + ("cracked-concrete-slab-repair", "Cracked Concrete Slab Repair"), + ("corner-crack-repair", "Corner Crack Repair"), + ("concrete-block-wall-repair", "Concrete Block Wall Repair"), + ("residential-retaining-wall-repair", "Residential Retaining Wall Repair"), + ("commercial-retaining-wall-repair", "Commercial Retaining Wall Repair"), + ("commercial-building-column-reinforcement", "Commercial Building Column Reinforcement"), + ("warehouse-roof-truss-repair", "Warehouse Roof Truss Repair"), + ("warehouse-beam-strengthening", "Warehouse Beam Strengthening"), + ("parking-garage-beam-strengthening", "Parking Garage Beam Strengthening"), + ("fire-and-impact-damage-beam-repair", "Fire and Impact Damage Beam Repair"), + ("concrete-foundation-beam-repair", "Concrete Foundation Beam Repair"), + ("bridge-column-and-pier-repair", "Bridge Column and Pier Repair"), +] + +VARIANTS = ["v1", "v2", "v3", "v4"] +V_LABELS = ["v1 24mm", "v2 macro", "v3 3/4", "v4 alt"] +THUMB_W, THUMB_H = 190, 143 + +BG = "#0e0e0e" +ROW_EVEN = "#141414" +ROW_ODD = "#111111" +SEL_CLR = "#ffc107" +DIM_CLR = "#555555" +REGEN_BG = "#2a1a00" +REGEN_FG = "#ff9800" +BUSY_CLR = "#ff5722" +OK_CLR = "#4caf50" +TEXT_CLR = "#ffffff" + +PLACEHOLDER = None # lazy-loaded gray image + + +def _load_env() -> dict: + env = os.environ.copy() + if ENV_FILE.exists(): + for line in ENV_FILE.read_text().splitlines(): + line = line.strip() + if line and not line.startswith("#") and "=" in line: + k, v = line.split("=", 1) + env[k.strip()] = v.strip() + return env + + +def _gray_placeholder(w: int, h: int) -> Image.Image: + img = Image.new("RGB", (w, h), "#1a1a1a") + return img + + +class App: + def __init__(self, root: tk.Tk): + self.root = root + self.root.title("CFS Image Selector") + self.root.configure(bg=BG) + self.root.geometry("1030x840") + + self.selections: dict[str, int] = {s: 0 for s, _ in APPLICATIONS} + self._tk_imgs: dict = {} + self._img_lbls: dict = {} # (slug, v_idx) -> Label (image widget) + self._bdr_lbls: dict = {} # (slug, v_idx) -> same Label for border + self._txt_lbls: dict = {} # (slug, v_idx) -> variant text Label + self._regen_btns: dict = {} # (slug, v_idx) -> regen Button + self._busy: set = set() # (slug, v_idx) currently regenerating + + self._build() + self._load_selections() + + # ------------------------------------------------------------------ build + + def _build(self): + bar = tk.Frame(self.root, bg=BG, pady=10) + bar.pack(fill="x", padx=20) + tk.Label(bar, text="Carbon Fiber Support — Select Hero Image", + bg=BG, fg=TEXT_CLR, font=("Helvetica", 12, "bold")).pack(side="left") + tk.Button(bar, text="Save Selections", command=self._save, + bg="#1a3a1a", fg=OK_CLR, relief="flat", padx=14, pady=6, + font=("Helvetica", 11, "bold"), cursor="hand2", + activebackground="#1f4a1f").pack(side="right", padx=(6, 0)) + tk.Button(bar, text="All v1", command=lambda: self._select_all(0), + bg="#1a2a3a", fg="#64b5f6", relief="flat", padx=10, pady=6, + font=("Helvetica", 10), cursor="hand2").pack(side="right") + + self._status = tk.StringVar(value="v1 pre-selected for all — click to change — Regen to regenerate") + tk.Label(self.root, textvariable=self._status, bg=BG, fg=DIM_CLR, + font=("Helvetica", 10), anchor="w").pack(fill="x", padx=20, pady=(0, 4)) + + outer = tk.Frame(self.root, bg=BG) + outer.pack(fill="both", expand=True, padx=8, pady=(0, 8)) + + canvas = tk.Canvas(outer, bg=BG, highlightthickness=0) + vsb = tk.Scrollbar(outer, orient="vertical", command=canvas.yview) + canvas.configure(yscrollcommand=vsb.set) + vsb.pack(side="right", fill="y") + canvas.pack(side="left", fill="both", expand=True) + + self._inner = tk.Frame(canvas, bg=BG) + win_id = canvas.create_window((0, 0), window=self._inner, anchor="nw") + canvas.bind("<Configure>", lambda e: canvas.itemconfig(win_id, width=e.width)) + self._inner.bind("<Configure>", + lambda _: canvas.configure(scrollregion=canvas.bbox("all"))) + canvas.bind_all("<Button-4>", lambda _: canvas.yview_scroll(-1, "units")) + canvas.bind_all("<Button-5>", lambda _: canvas.yview_scroll(1, "units")) + canvas.bind_all("<MouseWheel>", + lambda e: canvas.yview_scroll(int(-1 * e.delta / 120), "units")) + + for idx, (slug, name) in enumerate(APPLICATIONS): + self._build_row(idx, slug, name) + + def _build_row(self, idx: int, slug: str, name: str): + bg = ROW_EVEN if idx % 2 == 0 else ROW_ODD + row = tk.Frame(self._inner, bg=bg, pady=8, padx=12) + row.pack(fill="x", pady=1) + + tk.Label(row, text=f"{idx+1:02d} {name}", bg=bg, fg=TEXT_CLR, + font=("Helvetica", 10, "bold"), anchor="w", width=30).pack(side="left", padx=(0, 10)) + + for v_idx, (vk, vl) in enumerate(zip(VARIANTS, V_LABELS)): + path = SOURCED_DIR / f"{slug}_{vk}.jpg" + cell = tk.Frame(row, bg=bg) + cell.pack(side="left", padx=3) + + # Image label + tk_img = self._make_tk_img(path) + self._tk_imgs[(slug, v_idx)] = tk_img + is_sel = (v_idx == 0) + border = SEL_CLR if is_sel else "#2a2a2a" + lbl = tk.Label(cell, image=tk_img, + highlightthickness=4, highlightbackground=border, + cursor="hand2", bg="#000") + lbl.pack() + self._img_lbls[(slug, v_idx)] = lbl + self._bdr_lbls[(slug, v_idx)] = lbl + lbl.bind("<Button-1>", lambda e, s=slug, vi=v_idx: self._select(s, vi)) + lbl.bind("<Enter>", lambda e, s=slug, vi=v_idx: self._hover(s, vi, True)) + lbl.bind("<Leave>", lambda e, s=slug, vi=v_idx: self._hover(s, vi, False)) + + # Variant label row: text + regen button side by side + foot = tk.Frame(cell, bg=bg) + foot.pack(fill="x") + txt = tk.Label(foot, text=vl, bg=bg, + fg=SEL_CLR if is_sel else DIM_CLR, + font=("Helvetica", 9), anchor="w") + txt.pack(side="left") + self._txt_lbls[(slug, v_idx)] = txt + + rbtn = tk.Button(foot, text="Regen", + command=lambda s=slug, vi=v_idx: self._regen(s, vi), + bg=REGEN_BG, fg=REGEN_FG, relief="flat", + font=("Helvetica", 8), padx=5, pady=1, + cursor="hand2", activebackground="#3a2500") + rbtn.pack(side="right") + self._regen_btns[(slug, v_idx)] = rbtn + + # ------------------------------------------------------------------ logic + + def _make_tk_img(self, path: Path) -> ImageTk.PhotoImage: + if path.exists(): + try: + img = Image.open(path) + img.thumbnail((THUMB_W, THUMB_H), Image.LANCZOS) + return ImageTk.PhotoImage(img) + except Exception: + pass + return ImageTk.PhotoImage(_gray_placeholder(THUMB_W, THUMB_H)) + + def _select(self, slug: str, v_idx: int): + old = self.selections[slug] + if old == v_idx: + return + lbl_old = self._bdr_lbls.get((slug, old)) + if lbl_old: + lbl_old.configure(highlightbackground="#2a2a2a") + txt_old = self._txt_lbls.get((slug, old)) + if txt_old: + txt_old.configure(fg=DIM_CLR) + + lbl_new = self._bdr_lbls.get((slug, v_idx)) + if lbl_new: + lbl_new.configure(highlightbackground=SEL_CLR) + txt_new = self._txt_lbls.get((slug, v_idx)) + if txt_new: + txt_new.configure(fg=SEL_CLR) + + self.selections[slug] = v_idx + self._status.set(f"Selected {slug} → {VARIANTS[v_idx]}") + + def _hover(self, slug: str, v_idx: int, entering: bool): + if self.selections[slug] == v_idx or (slug, v_idx) in self._busy: + return + lbl = self._bdr_lbls.get((slug, v_idx)) + if lbl: + lbl.configure(highlightbackground="#555" if entering else "#2a2a2a") + + def _select_all(self, v_idx: int): + for slug, _ in APPLICATIONS: + self._select(slug, v_idx) + self._status.set(f"All applications set to {VARIANTS[v_idx]}") + + # ------------------------------------------------------------------ regen + + def _regen(self, slug: str, v_idx: int): + key = (slug, v_idx) + if key in self._busy: + return + self._busy.add(key) + + # Delete the file so the generator recreates it + path = SOURCED_DIR / f"{slug}_{VARIANTS[v_idx]}.jpg" + if path.exists(): + path.unlink() + + # Visual: busy state + lbl = self._bdr_lbls.get(key) + if lbl: + lbl.configure(highlightbackground=BUSY_CLR) + btn = self._regen_btns.get(key) + if btn: + btn.configure(text="...", state="disabled") + txt = self._txt_lbls.get(key) + if txt: + txt.configure(fg=BUSY_CLR) + + self._status.set(f"Regenerating {slug} {VARIANTS[v_idx]} ...") + + def run(): + env = _load_env() + env["SLUG_FILTER_VARIANT"] = VARIANTS[v_idx] + subprocess.run( + ["python3", str(GENERATOR), slug], + env=env, capture_output=True + ) + self.root.after(0, lambda: self._regen_done(slug, v_idx)) + + threading.Thread(target=run, daemon=True).start() + + def _regen_done(self, slug: str, v_idx: int): + key = (slug, v_idx) + self._busy.discard(key) + + path = SOURCED_DIR / f"{slug}_{VARIANTS[v_idx]}.jpg" + tk_img = self._make_tk_img(path) + self._tk_imgs[key] = tk_img + lbl = self._img_lbls.get(key) + if lbl: + lbl.configure(image=tk_img) + + is_sel = (self.selections[slug] == v_idx) + border = SEL_CLR if is_sel else "#2a2a2a" + bdr = self._bdr_lbls.get(key) + if bdr: + bdr.configure(highlightbackground=border) + txt = self._txt_lbls.get(key) + if txt: + txt.configure(fg=SEL_CLR if is_sel else DIM_CLR) + btn = self._regen_btns.get(key) + if btn: + btn.configure(text="Regen", state="normal") + + self._status.set(f"Done {slug} {VARIANTS[v_idx]}") + + # ------------------------------------------------------------------ save + + def _save(self): + out = {slug: VARIANTS[vi] for slug, vi in self.selections.items()} + SELECTIONS_OUT.write_text(json.dumps(out, indent=2)) + self._status.set(f"Saved {len(out)} selections → {SELECTIONS_OUT.name}") + + def _load_selections(self): + if SELECTIONS_OUT.exists(): + try: + data = json.loads(SELECTIONS_OUT.read_text()) + for slug, vk in data.items(): + if vk in VARIANTS: + self._select(slug, VARIANTS.index(vk)) + self._status.set(f"Loaded {SELECTIONS_OUT.name}") + except Exception: + pass + + +if __name__ == "__main__": + root = tk.Tk() + App(root) + root.mainloop() diff --git a/image-gen-workflow/imagen-api-reference.json b/image-gen-workflow/imagen-api-reference.json new file mode 100644 index 0000000..89d4be5 --- /dev/null +++ b/image-gen-workflow/imagen-api-reference.json @@ -0,0 +1,128 @@ +{ + "source": "https://ai.google.dev/gemini-api/docs/imagen", + "retrieved": "2026-05-13", + "sdk_package": "google-genai", + "sdk_import": "from google import genai\nfrom google.genai import types", + + "models": [ + { + "id": "imagen-4.0-generate-001", + "label": "Imagen 4 Standard", + "use_case": "Production — best balance of quality and speed", + "supports_image_size": true + }, + { + "id": "imagen-4.0-ultra-generate-001", + "label": "Imagen 4 Ultra", + "use_case": "Highest quality output, slower — hero images and print", + "supports_image_size": true + }, + { + "id": "imagen-4.0-fast-generate-001", + "label": "Imagen 4 Fast", + "use_case": "Drafts and rapid iteration — low latency", + "supports_image_size": false + } + ], + + "deprecated_models": [ + { "id": "imagen-3.0-generate-001", "status": "discontinued" } + ], + + "method": "client.models.generate_images", + "rest_endpoint": "https://generativelanguage.googleapis.com/v1beta/models/{model}:predict", + "auth_header": "x-goog-api-key", + + "parameters": { + "model": { + "type": "string", + "required": true, + "values": ["imagen-4.0-generate-001", "imagen-4.0-ultra-generate-001", "imagen-4.0-fast-generate-001"] + }, + "prompt": { + "type": "string", + "required": true, + "language": "English only", + "max_tokens": 480, + "notes": "Text overlays in images: keep under 25 characters for best results. Exact font replication not guaranteed." + }, + "config": { + "class": "types.GenerateImagesConfig", + "fields": { + "number_of_images": { + "type": "integer", + "min": 1, + "max": 4, + "default": 4 + }, + "aspect_ratio": { + "type": "string", + "default": "1:1", + "values": ["1:1", "3:4", "4:3", "9:16", "16:9"], + "notes": "Do NOT use '3:2' — not supported and will error" + }, + "image_size": { + "type": "string", + "default": "1K", + "values": ["1K", "2K"], + "applies_to": ["imagen-4.0-generate-001", "imagen-4.0-ultra-generate-001"], + "not_available_for": ["imagen-4.0-fast-generate-001"] + }, + "person_generation": { + "type": "string", + "default": "allow_adult", + "values": [ + { "value": "dont_allow", "description": "No people or faces in output — use for hardware, product, landscape" }, + { "value": "allow_adult", "description": "Adults only" }, + { "value": "allow_all", "description": "Adults and children — restricted in EU, UK, CH, MENA regions" } + ] + } + } + } + }, + + "output": { + "watermark": "SynthID — embedded in all generated images, not visible", + "format": "PIL Image object (SDK) / base64 bytes (REST)", + "access_sdk": "response.generated_images[i].image" + }, + + "python_minimal_example": "from google import genai\nfrom google.genai import types\n\nclient = genai.Client()\nresponse = client.models.generate_images(\n model='imagen-4.0-generate-001',\n prompt='Your prompt here',\n config=types.GenerateImagesConfig(\n number_of_images=4,\n aspect_ratio='16:9',\n person_generation='dont_allow'\n )\n)\nfor img in response.generated_images:\n img.image.show()", + + "rest_minimal_example": "curl -X POST 'https://generativelanguage.googleapis.com/v1beta/models/imagen-4.0-generate-001:predict' -H 'x-goog-api-key: $GEMINI_API_KEY' -H 'Content-Type: application/json' -d '{\"instances\":[{\"prompt\":\"Your prompt\"}],\"parameters\":{\"sampleCount\":4}}'", + + "arising_media_defaults": { + "draft_model": "imagen-4.0-fast-generate-001", + "production_model": "imagen-4.0-generate-001", + "hero_model": "imagen-4.0-ultra-generate-001", + "person_generation": "dont_allow", + "number_of_images": 4, + "aspect_ratio_web_hero": "16:9", + "aspect_ratio_square": "1:1", + "aspect_ratio_portrait": "3:4", + "file_naming": "{page}-{slot}.jpg", + "workflow": "draft with fast → select variant → regenerate with standard or ultra" + }, + + "prompt_engineering_notes": [ + "Describe subject, environment, lighting, and mood in one sentence", + "Photorealistic hardware/landscape: add 'photorealistic, 4K, professional photography'", + "Avoid people/faces: include 'no people, no humans' explicitly when using dont_allow is not enough", + "Camera style modifiers: 'shot on Canon 5D', 'wide angle lens', 'golden hour lighting'", + "Art style: 'architectural render', 'flat illustration', 'watercolor wash'", + "Keep text overlays short: 25 chars max, specify position ('top left', 'centered')" + ], + + "known_errors": [ + { + "error": "aspect ratio X:X not supported", + "cause": "3:2 or other non-standard ratio passed", + "fix": "Use only: 1:1, 3:4, 4:3, 9:16, 16:9" + }, + { + "error": "imageSize not applicable", + "cause": "imageSize passed to imagen-4.0-fast-generate-001", + "fix": "Remove imageSize parameter when using fast model" + } + ] +} diff --git a/local-image-generation/01-comfyui-setup.md b/local-image-generation/01-comfyui-setup.md new file mode 100644 index 0000000..16b3a73 --- /dev/null +++ b/local-image-generation/01-comfyui-setup.md @@ -0,0 +1,100 @@ +# 01 — ComfyUI Setup + +ComfyUI is installed at `~/ComfyUI/` on the Arising Media workstation. +Python venv is at `~/ComfyUI/venv/`. + +## Starting ComfyUI + +```bash +tmux new-session -d -s comfyui \ + "cd ~/ComfyUI && HSA_OVERRIDE_GFX_VERSION=10.3.0 venv/bin/python main.py --listen 0.0.0.0 --port 8188 2>&1 | tee ~/comfyui.log" +``` + +**Do NOT use `--cpu`.** The GPU is an AMD Ryzen 9 9950X integrated graphics +(gfx1036, RDNA 2 iGPU) with 30,942 MB unified VRAM (shares system RAM). +All models fit: FLUX (12GB), Wan 2.2 (3.2GB), T5-XXL (4.6GB). + +`HSA_OVERRIDE_GFX_VERSION=10.3.0` is required — gfx1036 (iGPU) is not in +the PyTorch ROCm kernel list, but gfx1030 (RDNA 2 dGPU) is compatible. +Without the override: `HIP error: invalid device function` on first compute op. + +Previous SOP said 2GB VRAM — that was wrong. It was reading the dedicated +VRAM pool, not the full unified memory PyTorch allocates via ROCm. + +Verify it's up: +```bash +curl -s -o /dev/null -w "%{http_code}" http://localhost:8188/system_stats +# should return 200 within 30 seconds +``` + +Check the log for node load errors: +```bash +tmux attach -t comfyui +``` + +## Required custom nodes + +Both installed at `~/ComfyUI/custom_nodes/`: + +- `ComfyUI-GGUF` — loads GGUF quantized models (FLUX, Wan 2.2) +- `ComfyUI-Detail-Daemon` — optional, detail enhancement + +If `ComfyUI-GGUF` fails to load, check for missing Python packages: +```bash +~/ComfyUI/venv/bin/pip install gguf sqlalchemy +``` + +## Known dependency gaps (fix if ComfyUI fails to start) + +```bash +~/ComfyUI/venv/bin/pip install sqlalchemy gguf +``` + +Audio nodes (`nodes_audio.py`, `nodes_lt_audio.py`) will fail to import +because `torchaudio` is not installed. This is safe to ignore — audio +nodes are not used in this pipeline. + +## GPU note + +GPU: AMD Ryzen 9 9950X integrated graphics (gfx1036, RDNA 2 iGPU) +Unified memory: 30,942 MB available to PyTorch via ROCm (shares system RAM) + +```bash +# Verify ROCm sees the GPU +~/ComfyUI/venv/bin/python -c "import torch; print(torch.cuda.is_available()); print(torch.cuda.get_device_name(0))" +# returns True / AMD Ryzen 9 9950X 16-Core Processor + +# Verify arch override works +HSA_OVERRIDE_GFX_VERSION=10.3.0 ~/ComfyUI/venv/bin/python -c " +import torch; x=torch.tensor([1.0]).cuda(); print('GPU OK:', x.device) +" +``` + +gfx1036 requires `HSA_OVERRIDE_GFX_VERSION=10.3.0` — always set this env var +before starting ComfyUI or running any Python that loads GPU tensors. +Without it: `HIP error: invalid device function` immediately on first op. + +## Model folder structure + +``` +~/ComfyUI/models/ +├── unet/ +│ └── flux1-schnell-Q8_0.gguf (12GB, FLUX image) +├── clip/ +│ ├── clip_l.safetensors (235MB, FLUX CLIP-L) +│ ├── t5xxl_fp8_e4m3fn.safetensors (4.6GB, FLUX T5-XXL) +│ └── umt5_xxl_fp8_e4m3fn_scaled.safetensors (6.3GB, Wan text encoder) +├── vae/ +│ ├── ae.safetensors (108MB, FLUX VAE) +│ └── wan_2.1_vae.safetensors (243MB, Wan VAE) +└── diffusion_models/ + └── Wan2.2-TI2V-5B-Q4_K_M.gguf (3.2GB, Wan 2.2 video) +``` + +## Stopping ComfyUI + +```bash +tmux send-keys -t comfyui C-c +# or kill the session: +tmux kill-session -t comfyui +``` diff --git a/local-image-generation/02-flux-images.md b/local-image-generation/02-flux-images.md new file mode 100644 index 0000000..5c1284f --- /dev/null +++ b/local-image-generation/02-flux-images.md @@ -0,0 +1,99 @@ +# 02 — FLUX.1 Schnell Image Pipeline + +## Why FLUX over SDXL + +FLUX is a 12B-parameter transformer model. SDXL (RealVisXL) is 3.5B. +FLUX has significantly better: +- Spatial depth and perspective (lens simulation) +- Scene geometry (vanishing points, depth-of-field) +- Prompt following (T5-XXL understands long, detailed prompts) + +SDXL was tested on lahrcarpetcleaning.com and rejected: flat angles, no depth, +poor spatial coherence. FLUX replaced it entirely. + +## Model stack + +| File | Size | Notes | +|---|---|---| +| flux1-schnell-Q8_0.gguf | 12GB | GGUF Q8, needs ComfyUI-GGUF node | +| t5xxl_fp8_e4m3fn.safetensors | 4.6GB | T5-XXL text encoder, fp8 quantized | +| clip_l.safetensors | 235MB | CLIP-L, short prompt encoder | +| ae.safetensors | 108MB | Official FLUX VAE from Black Forest Labs | + +## Download (one-time) + +FLUX GGUF (public, no auth): +```bash +wget "https://huggingface.co/city96/FLUX.1-schnell-gguf/resolve/main/flux1-schnell-Q8_0.gguf" \ + -O ~/ComfyUI/models/unet/flux1-schnell-Q8_0.gguf + +wget "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/t5xxl_fp8_e4m3fn.safetensors" \ + -O ~/ComfyUI/models/clip/t5xxl_fp8_e4m3fn.safetensors + +wget "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/clip_l.safetensors" \ + -O ~/ComfyUI/models/clip/clip_l.safetensors +``` + +FLUX VAE (gated — requires HF login and license acceptance): +```bash +hf auth login # paste read token +HF_TOKEN=$(cat ~/.cache/huggingface/token) +wget --header="Authorization: Bearer $HF_TOKEN" \ + "https://huggingface.co/black-forest-labs/FLUX.1-schnell/resolve/main/ae.safetensors" \ + -O ~/ComfyUI/models/vae/ae.safetensors +``` + +## ComfyUI workflow (what gen-images-flux.py sends) + +``` +UnetLoaderGGUF → flux1-schnell-Q8_0.gguf +DualCLIPLoader → t5xxl_fp8_e4m3fn + clip_l (type=flux) +VAELoader → ae.safetensors +CLIPTextEncode → prompt +EmptyLatentImage → 1024×576, batch=1 +KSampler → steps=4, cfg=1.0, euler, simple +VAEDecode +SaveImage +``` + +## Settings + +| Setting | Value | Why | +|---|---|---| +| Steps | 4 | Schnell is distilled — 4 steps is optimal | +| CFG | 1.0 | Distilled model, higher CFG degrades quality | +| Sampler | euler | Best for FLUX | +| Scheduler | simple | Matches FLUX training | +| Negative prompt | none | Distilled model ignores it | +| Resolution | 1024×576 | 16:9 hero format | + +## Running generation + +```bash +# ComfyUI must be running first (see 01-comfyui-setup.md) +cd /home/sirdrez/arisingmedia-websites/{domain} +python3 tools/gen-images-flux.py 2>&1 | tee tools/flux-gen.log +``` + +Monitor: +```bash +tmux attach -t comfyui # step progress bars +tail -f tools/flux-gen.log # per-image OK/FAIL +``` + +Speed: ~4 min/image on CPU (2GB VRAM insufficient for GPU). 28 images = ~1h50m. + +## After generation + +```bash +python3 tools/convert-to-webp.py # resize + convert to WebP +rm assets/images/**/*.jpg # delete source JPGs +docker compose build --no-cache web # bake WebP into image +docker compose up -d +``` + +Verify: +```bash +curl -s -o /dev/null -w "%{http_code}" http://localhost:{port}/assets/images/hero/hero-carpet-cleaning.webp +# must return 200 +``` diff --git a/local-image-generation/03-wan-video.md b/local-image-generation/03-wan-video.md new file mode 100644 index 0000000..acbea0e --- /dev/null +++ b/local-image-generation/03-wan-video.md @@ -0,0 +1,159 @@ +# 03 — Wan 2.2 Video Pipeline (Image-to-Video) + +## Default policy: local generation + +Video generation is done locally with Wan 2.2 by default. Google Veo (via +Vertex AI / Gemini API) is NOT used unless the client has explicit budget +allocated for it. Reasons: + +- Google Veo costs money per second of video generated (billed per request) +- Local Wan 2.2 is free after one-time model download (~10GB total) +- Quality from Wan 2.2 at 832x480 is sufficient for hero reels +- No API key, no quota limits, no vendor dependency + +Use Google Veo only when: client approves a paid media budget, OR the local +workstation is unavailable and a deadline cannot wait for CPU generation time. + +## Purpose + +Takes FLUX-generated hero stills and animates each into a 3-5 second clip. +Clips are stitched with ffmpeg into a marketing reel for the hero section. + +## Model stack + +| File | Size | Notes | +|---|---|---| +| Wan2.2-TI2V-5B-Q4_K_M.gguf | 3.2GB | Text+Image to Video, 5B Q4 GGUF | +| umt5_xxl_fp8_e4m3fn_scaled.safetensors | 6.3GB | UMT5-XXL text encoder, fp8 | +| wan_2.1_vae.safetensors | 243MB | Wan VAE (compatible with 2.2) | + +## Download (one-time, all public) + +```bash +# Wan 2.2 model +wget "https://huggingface.co/QuantStack/Wan2.2-TI2V-5B-GGUF/resolve/main/Wan2.2-TI2V-5B-Q4_K_M.gguf" \ + -O ~/ComfyUI/models/diffusion_models/Wan2.2-TI2V-5B-Q4_K_M.gguf + +# Text encoder +wget "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/text_encoders/umt5_xxl_fp8_e4m3fn_scaled.safetensors" \ + -O ~/ComfyUI/models/clip/umt5_xxl_fp8_e4m3fn_scaled.safetensors + +# VAE +wget "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/vae/wan_2.1_vae.safetensors" \ + -O ~/ComfyUI/models/vae/wan_2.1_vae.safetensors +``` + +## Critical: WanImageToVideo is a conditioning node, NOT a sampler + +This is the most important thing to understand about the Wan pipeline. The node +name is misleading. `WanImageToVideo` does NOT run diffusion — it sets up the +conditioning and empty latent. A separate `KSampler` runs the actual diffusion. + +Wrong mental model (what most tutorials imply): +``` +LoadImage → WanImageToVideo → SaveAnimatedWEBP +``` + +Correct node graph: +``` +UnetLoaderGGUF ─────────────────────────────────────→ KSampler.model +CLIPLoader ──→ CLIPTextEncode (positive) ─→ WanImageToVideo.positive ──→ KSampler.positive + └→ CLIPTextEncode (negative) ─→ WanImageToVideo.negative ──→ KSampler.negative +VAELoader ──→ WanImageToVideo.vae WanImageToVideo.latent ──→ KSampler.latent_image +LoadImage ──→ WanImageToVideo.start_image (optional) + KSampler.samples ──→ VAEDecode ──→ SaveAnimatedWEBP +``` + +WanImageToVideo outputs three things (in order): +- output[0] = positive CONDITIONING (enhanced with image) +- output[1] = negative CONDITIONING +- output[2] = latent LATENT (sized for video: width × height × frames) + +The `start_image` input (optional IMAGE) anchors the first frame. Without it, +video starts from noise. Always pass it for image-to-video. + +## Workflow + +Correct ComfyUI API node graph (as sent by `gen-video-wan.py`): + +``` +node 1: UnetLoaderGGUF → Wan2.2-TI2V-5B-Q4_K_M.gguf +node 2: CLIPLoader → umt5_xxl_fp8_e4m3fn_scaled.safetensors (type=wan) +node 3: VAELoader → wan_2.1_vae.safetensors +node 4: LoadImage → FLUX hero still (.webp) +node 5: CLIPTextEncode → motion prompt text (positive) +node 6: CLIPTextEncode → negative prompt text +node 7: WanImageToVideo → positive=[5,0], negative=[6,0], vae=[3,0], + start_image=[4,0], width=832, height=480, + length=25 (or 49), batch_size=1 +node 8: KSampler → model=[1,0], positive=[7,0], negative=[7,1], + latent_image=[7,2], steps=20, cfg=6.0, + sampler_name=uni_pc, scheduler=simple, denoise=1.0 +node 9: VAEDecode → samples=[8,0], vae=[3,0] +node 10: SaveAnimatedWEBP → images=[9,0], fps=12 +``` + +## Settings + +| Setting | Value | +|---|---| +| Resolution | 832×480 (16:9 ~480p) | +| Frames | 49 (~4 seconds at 12fps) | +| Steps | 20 | +| CFG | 6.0 | +| Sampler | uni_pc | + +**Frame count constraint:** `length` must follow the pattern 1, 5, 9, 13, 17, 21, 25, 29 ... (step of 4). +ComfyUI enforces this. 49 is valid (1 + 4×12). 50 is not. + +**CPU speed on Arising Media workstation (2GB VRAM, CPU inference):** +- ~415 seconds per diffusion step +- 20 steps × 415s = ~2.3 hours per clip +- 6 clips = ~14 hours total for a full reel +- Use 25 frames (not 49) for test runs to halve generation time +- Full reel generation: start before leaving for the day, check next morning + +**CLIPVision note:** No CLIPVision models are installed at `~/ComfyUI/models/clip_vision/`. +The `clip_vision_output` input on WanImageToVideo is optional and currently unused. +Image conditioning comes from `start_image` only (VAE-encoded first frame). +This is sufficient for smooth motion — CLIPVision would add semantic image +understanding but is not required. + +## Running video generation + +```bash +# ComfyUI must be running, FLUX images must be converted to WebP first +cd /home/sirdrez/arisingmedia-websites/{domain} +python3 tools/gen-video-wan.py 2>&1 | tee tools/wan-gen.log +``` + +Output goes to `assets/videos/clips/` as `.webp` animation files. + +## Stitching the reel + +```bash +# Create file list +ls assets/videos/clips/*.webp | sort | while read f; do echo "file '$PWD/$f'"; done > tools/clip-list.txt + +# Convert webp animations to mp4 first (if needed) +for f in assets/videos/clips/*.webp; do + ffmpeg -i "$f" "${f%.webp}.mp4" -y +done + +# Stitch +ls assets/videos/clips/*.mp4 | sort | while read f; do echo "file '$PWD/$f'"; done > tools/clip-list.txt +ffmpeg -f concat -safe 0 -i tools/clip-list.txt -c copy assets/videos/hero/hero-reel-flux.mp4 +``` + +## Reel shot list (lahrcarpetcleaning.com) + +| Clip | Source still | Motion prompt | +|---|---|---| +| clip-01 | hero-carpet-cleaning | slow dolly forward across carpet | +| clip-02 | hero-stairs | slow pan upward along staircase | +| clip-03 | hero-upholstery | gentle push in toward sofa | +| clip-04 | hero-commercial | tracking shot down lobby | +| clip-05 | hero-floors | floor-level drift forward | +| clip-06 | hero-clean-result | rack focus across carpet fibers | + +6 clips × ~4s = ~24 seconds total reel. diff --git a/local-image-generation/04-prompt-guide.md b/local-image-generation/04-prompt-guide.md new file mode 100644 index 0000000..bf9a3c8 --- /dev/null +++ b/local-image-generation/04-prompt-guide.md @@ -0,0 +1,105 @@ +# 04 — Prompt Guide (Interior / Carpet Photography) + +## The core pattern + +All image prompts follow this structure: + +``` +{camera angle} {lens} {subject description}, +{foreground detail} sharp in foreground, {background} receding into bokeh, +{lighting description}, {style tag}, no people, ultra-realistic {type} photography +``` + +## Why this works + +FLUX.1 Schnell uses T5-XXL as the primary text encoder (6GB model) which +understands natural language photography concepts deeply. Specifying lens +focal length, depth of field, and spatial relationships produces images with +correct depth, perspective, and scene geometry. + +SDXL models lack this — their text encoders (CLIP-L/CLIP-G) top out at +77 tokens and don't understand spatial concepts reliably. + +## Lens vocabulary + +| Lens | Effect | Use for | +|---|---|---| +| 24mm wide-angle | Strong perspective distortion, exaggerated depth | Corridors, lobbies, open spaces | +| 35mm | Natural perspective, slight depth emphasis | Most interior shots | +| 50mm prime | Near-natural perspective, shallow DoF | Close-ups, furniture, details | +| macro | Extreme close-up, very shallow DoF | Carpet fiber detail, texture | + +## Camera position vocabulary + +- `low-angle` / `low 35mm angle` — camera near floor level, looking across surface +- `floor level` — pressed to the floor, extreme low angle +- `corner angle` — shot from room corner, wide coverage +- `looking up` — camera below subject, looking upward +- `looking down` — camera above, bird's-eye (avoid for carpet — looks flat) + +## Depth of field vocabulary + +- `shallow depth of field` — subject sharp, background blurred +- `razor sharp in foreground ... receding into bokeh` — specific foreground/background split +- `raking light` — light hitting surface at low angle, reveals texture +- `vanishing point perspective` — strong linear convergence (corridors, offices) + +## Lighting vocabulary + +- `warm afternoon window light` — residential, golden hour feel +- `raking natural light` — reveals carpet texture and pile height +- `recessed ceiling lights creating depth` — commercial/corporate +- `warm wall sconces` — hotel corridors +- `crisp morning light` — bedrooms, bright and clean + +## Full prompt examples + +Carpet hero (residential): +``` +low-angle 35mm lens perspective looking across thick plush cream carpet +in an upstate New York living room, carpet fibers razor sharp in foreground, +couch and coffee table receding into shallow bokeh background, +warm afternoon window light raking across carpet texture, +Finger Lakes farmhouse interior, no people, +ultra-realistic architectural photography, 16:9 +``` + +Hotel corridor: +``` +low 24mm lens looking down a long hotel corridor from floor level, +patterned burgundy carpet runner sharp in extreme foreground receding to vanishing point, +warm wall sconces lining white walls, numbered doors converging in perspective, +no people, ultra-realistic hospitality photography, 16:9 +``` + +Hardwood floor: +``` +low 24mm angle pressed to gleaming light oak hardwood floor, +floor grain razor sharp in extreme foreground receding to hallway vanishing point, +white walls, natural light streaming in, shallow depth of field, +no people, ultra-realistic interior photography, 16:9 +``` + +## What NOT to include + +- No people, no faces, no hands, no feet, no shoes/boots +- No cleaning machines, vacuums, steam equipment, hoses +- No text, logos, watermarks in the scene +- No "before state" (dirty carpet, stains) — only clean result +- No "wide shot" without camera angle qualifier — produces flat frontal views + +## Video motion prompts (Wan 2.2) + +For animating stills, describe the camera motion, not the scene content: + +``` +slow dolly forward across {subject}, gentle camera push toward the far wall, +{lighting}, cinematic, smooth motion +``` + +Motion types: +- `slow dolly forward` — push toward subject +- `slow pan {direction}` — lateral camera rotation +- `tracking shot moving forward` — camera travels through space +- `rack focus` — lens focus shifts from foreground to background +- `gentle push in` — subtle zoom/move toward subject diff --git a/local-image-generation/05-quality-levers.md b/local-image-generation/05-quality-levers.md new file mode 100644 index 0000000..c599da8 --- /dev/null +++ b/local-image-generation/05-quality-levers.md @@ -0,0 +1,87 @@ +# 05 — Quality Improvement Levers + +Three levers control FLUX output quality, in order of impact: + +## 1. Prompt (highest impact, zero cost) + +Incoherent objects in the frame are almost always prompt bleed — the model fills +empty or ambiguous space with training-data defaults. Fix by naming every part of +the frame explicitly. + +**Background** — name it, don't imply it: +- Bad: "living room" (model invents furniture, decor, wall art) +- Good: "plain cream painted wall with a single frosted sliding glass door" + +**Floor material** — always explicit: +- "plush cream berber carpet" or "light oak hardwood floor" +- Ambiguous floor → random floor type generated + +**Ceiling** — if visible, name it; if not wanted, push it out of frame with a +lower camera angle: +- "white drop ceiling with recessed can lights" +- Or: lower the angle until ceiling exits the frame entirely + +**Negative scene elements** — add inline, not as a separate negative prompt +(FLUX Schnell ignores negative prompts): +- "no furniture clutter, no decorative objects, no picture frames, no signage" +- "no cleaning equipment, no machines, no people" + +**What not to use:** +- "wide shot" without a camera angle qualifier — produces flat frontal views +- Vague room names ("office", "lobby") without specifying what fills the space + +## 2. Steps (marginal gain, 2x slower) + +FLUX Schnell is distilled to 4 steps. The distillation process compresses +a full diffusion model's quality into very few steps. + +| Steps | Quality change | Time impact | +|---|---|---| +| 4 (default) | Baseline | ~4 min/image | +| 6 | Slightly sharper edges, cleaner fine detail | ~6 min/image | +| 8 | Diminishing returns past 6 | ~8 min/image | + +Not recommended as a first fix. The distillation ceiling is the constraint, +not step count. Step increases help texture detail but will not fix scene +incoherence — that requires prompt changes. + +KSampler in `gen-images-flux.py`: +```python +"steps": 4, # increase to 6 for detail passes +``` + +## 3. Model size (real quality jump, 6x slower on CPU) + +| Model | Steps | Quality | CPU time/image | +|---|---|---|---| +| FLUX.1 Schnell (current) | 4 | Good depth, some coherence gaps | ~4 min | +| FLUX.1 Dev (full, non-distilled) | 20-30 | Better coherence, sharper geometry | ~20-30 min | + +FLUX Dev would fix most coherence issues. At current CPU-only speed (2GB VRAM +insufficient), a full 28-image batch would take 9+ hours. + +**Practical path to FLUX Dev:** +- Cloud GPU: RunPod or Vast.ai A100 runs FLUX Dev in ~90 seconds/image +- Same prompts, same ComfyUI workflow — only model file and step count change +- Switch `flux1-schnell-Q8_0.gguf` → FLUX Dev GGUF, set steps to 20, cfg to 3.5 + +## Decision matrix + +| Issue | Fix | +|---|---| +| Objects that shouldn't be in frame | Prompt: name every surface explicitly | +| Wrong floor/wall material | Prompt: be specific about material | +| Flat angle despite prompt | Prompt: add "low-angle", lens mm, "foreground sharp" | +| Soft edges on carpet fibers | Steps: increase 4 → 6 | +| Incoherent room geometry | Model: switch to FLUX Dev on cloud GPU | +| Overall composition wrong | Prompt: camera position + lens + foreground/bokeh split | + +## Re-running specific images + +To re-run only the problem frames without regenerating all 28: + +1. Edit `tools/gen-images-flux.py` +2. Change the `IMAGES` list to include only the failed image keys +3. Run: `python3 tools/gen-images-flux.py 2>&1 | tee tools/flux-gen.log` +4. Run: `python3 tools/convert-to-webp.py` (converts only new JPGs) +5. Rebuild: `docker compose build --no-cache web && docker compose up -d` diff --git a/local-image-generation/README.md b/local-image-generation/README.md new file mode 100644 index 0000000..fbe9bbb --- /dev/null +++ b/local-image-generation/README.md @@ -0,0 +1,46 @@ +# Local Image Generation — SOPs + +Complete reference for generating site images locally using ComfyUI. +No cloud API required. No per-image cost. Runs on the Arising Media workstation. + +## Index + +1. [01-comfyui-setup.md](01-comfyui-setup.md) — Installing ComfyUI, venv, GGUF node +2. [02-flux-images.md](02-flux-images.md) — FLUX.1 Schnell image generation pipeline +3. [03-wan-video.md](03-wan-video.md) — Wan 2.2 image-to-video pipeline +4. [04-prompt-guide.md](04-prompt-guide.md) — Prompt patterns for interior/carpet photography +5. [05-quality-levers.md](05-quality-levers.md) — Prompt, steps, model size: what to adjust and when + +## Quick start (images already set up) + +```bash +# 1. Start ComfyUI +tmux new-session -d -s comfyui \ + "cd ~/ComfyUI && venv/bin/python main.py --listen 0.0.0.0 --port 8188 --cpu 2>&1 | tee ~/comfyui.log" + +# 2. Wait ~30s, then generate images +cd /home/sirdrez/arisingmedia-websites/{domain} +python3 tools/gen-images-flux.py 2>&1 | tee tools/flux-gen.log + +# 3. Convert to WebP and deploy +python3 tools/convert-to-webp.py +rm assets/images/**/*.jpg +docker compose build --no-cache web && docker compose up -d +``` + +## Model files (installed at ~/ComfyUI/models/) + +| Purpose | File | Size | Location | +|---|---|---|---| +| FLUX image UNet | flux1-schnell-Q8_0.gguf | 12GB | models/unet/ | +| FLUX T5 encoder | t5xxl_fp8_e4m3fn.safetensors | 4.6GB | models/clip/ | +| FLUX CLIP-L | clip_l.safetensors | 235MB | models/clip/ | +| FLUX VAE | ae.safetensors | 108MB | models/vae/ | +| Wan 2.2 video | Wan2.2-TI2V-5B-Q4_K_M.gguf | 3.2GB | models/diffusion_models/ | +| Wan UMT5 encoder | umt5_xxl_fp8_e4m3fn_scaled.safetensors | 6.3GB | models/clip/ | +| Wan VAE | wan_2.1_vae.safetensors | 243MB | models/vae/ | + +## Reference project + +`lahrcarpetcleaning.com` — first project using this full pipeline. +Scripts: `tools/gen-images-flux.py`, `tools/gen-video-wan.py`, `tools/convert-to-webp.py` diff --git a/sops.db b/sops.db new file mode 100644 index 0000000000000000000000000000000000000000..6b4b2c9e46d40b606e0a48fb7d9eec42c1143139 GIT binary patch literal 491520 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�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%0A0YWf`NtcTVJ=vk}NCXi!m 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 literal 0 HcmV?d00001 diff --git a/stack-selector.json b/stack-selector.json new file mode 100644 index 0000000..ea6ea06 --- /dev/null +++ b/stack-selector.json @@ -0,0 +1,129 @@ +{ + "_doc": "Arising Media stack selector — agent reference. Choose the correct stack before starting any project.", + "_updated": "2026-05-21", + + "stacks": { + "static-html": { + "name": "Static HTML", + "reference_project": "lahrcarpetcleaning.com", + "use_when": [ + "Site has fewer than 50 pages", + "Content changes infrequently (monthly or less)", + "Client hosts on cPanel shared hosting with no server-side scripting", + "No database required" + ], + "do_not_use_when": [ + "Site has more than 50 pages", + "Content must be updated across many pages simultaneously", + "Lead capture forms require server-side validation", + "Site has location pages, service pages, or any programmatic content" + ], + "files": ["Dockerfile", "nginx.conf", "docker-compose.yml", ".htaccess", ".cpanel.yml"], + "sops": ["01-project-structure.md", "03-build-pipeline.md", "08-deployment-docker.md"] + }, + + "php-router-sqlite": { + "name": "PHP Router + SQLite", + "reference_project": "arisingmedia.us", + "use_when": [ + "Site has 50+ pages of any type", + "Multiple page classes share a common template (services, locations, blog)", + "Header/footer/nav updates must propagate instantly across all pages", + "Content is authored in a database or Airtable and pulled at render time", + "Site will grow — new pages added without new HTML files" + ], + "do_not_use_when": [ + "Client requires cPanel shared hosting with no PHP-FPM (rare)", + "Site is a pure landing page (1-3 pages)" + ], + "architecture": { + "router": "src/api/router.php", + "templates": "src/api/templates/ — one .php file per page class", + "components": "src/api/components/_sections.php, _header.php, _footer.php", + "database": "src/api/data/pages.sqlite (all page content)", + "tokens": "src/assets/css/tokens.css", + "styles": "src/assets/css/main.css", + "js": "src/assets/js/main.js" + }, + "page_classes": { + "service": "service.php — detailed service pages with value_prop, use_case_carousel, roi_band, lead_magnet, tiers", + "location": "location.php — city + service combination pages, map embed, local content", + "challenge": "challenge.php — problem definition + our approach + CTA", + "static": "static.php — about, contact, hub pages, case studies", + "blog": "blog.php — blog posts with author, date, related posts", + "category": "category.php — service hub pages" + }, + "sops": ["15-php-router-sqlite-standard.md"], + "design_reference": "arisingmedia.us/.planning/WEBSITE_BUILD_STANDARD.md", + "architecture_diagram": "arisingmedia.us/.planning/RENDER_ARCHITECTURE.html", + "approved_mockup": "arisingmedia.us/.planning/template-gallery-2026-05-20/mockup.html" + }, + + "php-app": { + "name": "PHP App Stack", + "reference_project": "quickconvert.us", + "use_when": [ + "File uploads and server-side processing required", + "At-rest encryption of user data", + "Payment processing (Stripe subscriptions)", + "User authentication" + ], + "sops": ["14-php-app-stack.md"] + } + }, + + "design_system": { + "fonts": ["Plus Jakarta Sans (display)", "Inter (body)"], + "approved_colors": { + "cobalt_deep": "#021a6a", + "cobalt": "#042fac", + "cobalt_light": "#5d78c9", + "navy": "#172034", + "footer": "#0f1626", + "slate": "#1c2c44", + "graphite": "#222f42", + "blue": "#1e6bd6", + "facet": "#6b82b2", + "stone": "#eef1f6", + "white": "#ffffff" + }, + "rejected_colors": { + "teal": "never use — not brand", + "mist": "#f8f9fb — eliminated from band rhythm 2026-05-21", + "angled_edges": "clip-path diagonals rejected — tech brands use flat horizontal lines" + }, + "band_rhythm": ["graphite", "light", "slate", "stone"], + "statement_type": "clamp(40px, 6.5vw, 96px)", + "section_padding_desktop": "120px", + "credibility_pattern": "proof numbers in dark grid band (IBM/Nvidia) — NOT generic icon logos" + }, + + "section_types": { + "available": ["text", "split", "pain", "process", "spotlight", "benefits", "faqs", "grid", "comparison", "testimonials", "stats", "cta", "pin_story", "tiers", "value_prop", "use_case_carousel", "roi_band", "lead_magnet"], + "v4_schema_columns": ["hero_value_proposition", "lead_magnet_json", "use_case_carousel_json", "roi_proof_json", "service_variant_strategy_json"], + "deprecated": ["grid — replaced by use_case_carousel when cases are populated", "mist band — eliminated from rhythm"] + }, + + "databases": { + "sqlite": { + "role": "Primary rendering database — exact slug lookups, structured content", + "suitable_up_to": "Millions of rows — 10,000 pages is tiny (5MB)", + "query_time": "< 1ms for slug lookup", + "file": "src/api/data/pages.sqlite" + }, + "chromadb": { + "role": "Future semantic layer — related content, site search, content generation grounding", + "not_suitable_for": "Primary rendering — no exact-match primary key lookup", + "status": "Planned — future phase after all content is in SQLite" + } + }, + + "deployment": { + "container": "am-web (Docker)", + "local_port": 8001, + "db_path_in_container": "/var/www/data/pages.sqlite", + "assets_path": "/var/www/html/assets/", + "hot_copy_db": "docker cp src/api/data/pages.sqlite am-web:/var/www/data/pages.sqlite && docker exec am-web sh -c 'rm -f /var/www/data/pages.sqlite-shm /var/www/data/pages.sqlite-wal && chown www-data:www-data /var/www/data/pages.sqlite'", + "hot_copy_assets": "docker cp src/assets/css/main.css am-web:/var/www/html/assets/css/main.css" + } +} diff --git a/stack.json b/stack.json new file mode 100644 index 0000000..d5f0d85 --- /dev/null +++ b/stack.json @@ -0,0 +1,239 @@ +{ + "meta": { + "author": "Andre Cobham / Arising Media", + "updated": "2026-06-09", + "version": "3.0" + }, + "stack": { + "base_image": "php:8.3-fpm-alpine", + "process_manager": "supervisord", + "web_server": "nginx", + "database": "SQLite (pdo_sqlite)", + "email": "SMTP via msmtp or Resend API", + "js": "vanilla (no frameworks)", + "css": "tokens.css + main.css", + "hosting": "Coolify (Docker) or cPanel (shared hosting)" + }, + "databases": { + "header.db": ["nav_items"], + "footer.db": ["footer_columns", "footer_links", "footer_legal"], + "pages.db": ["pages", "page_sections"], + "blog.db": ["posts", "post_images", "post_schema", "post_stats", "related_posts", "linkedin_drafts", "subscribers"], + "one_db_per_domain": true, + "never_monolithic": "Do not combine unrelated content domains in one database" + }, + "deployment": { + "docker": { + "build_command": "docker compose build", + "run_command": "docker compose up -d", + "process_manager": "supervisord", + "platforms": ["VPS", "DigitalOcean", "Linode", "Coolify", "custom servers"] + }, + "cpanel": { + "requires": [".htaccess", ".cpanel.yml"], + "repo_path": "/home/{username}/repositories/{domain}/ (empty)", + "webroot": "/home/{username}/public_html/{domain}/", + "platforms": ["cPanel", "Bluehost", "HostGator", "SiteGround"] + } + }, + "content_update": { + "new_page": [ + "Insert row in pages.db: slug, title, hero fields, sections_json", + "Re-seed: python3 build/seed_databases.py", + "Rebuild Docker: docker build -t {domain}:local .", + "For new URL patterns: add location block to infra/nginx.conf" + ], + "edit_content": [ + "All body copy lives in sections_json column of pages.db", + "Never put body copy in PHP template files", + "Edit CSV source or build/seed_databases.py and re-run" + ], + "build_scripts": "Use JSON + template + Python for 4+ similar pages (location pages, service pages)" + }, + "security": { + "required_headers": [ + "X-Frame-Options: SAMEORIGIN", + "X-Content-Type-Options: nosniff", + "Referrer-Policy: strict-origin-when-cross-origin", + "Permissions-Policy: camera=(), microphone=(), geolocation=()", + "Strict-Transport-Security: max-age=31536000; includeSubDomains", + "Cross-Origin-Opener-Policy: same-origin", + "Cross-Origin-Resource-Policy: same-origin", + "Content-Security-Policy (tight, project-specific)" + ], + "php_hardening": [ + "expose_php = Off", + "display_errors = Off", + "open_basedir = /var/www/html:/var/www/data", + "disable_functions = exec,passthru,shell_exec,system,proc_open,popen,pcntl_exec" + ], + "php_fpm": "clear_env = no (CRITICAL: required for getenv())", + "blocking_sensitive_paths": [ + "location ~ /\\. { deny all; return 404; }", + "location ~* \\.(env|conf|yml|md|sh|py|sql|bak|log|dockerfile)$ { deny all; return 404; }" + ] + }, + "forms": { + "spam_protection": "Altcha (self-hosted, proof-of-work SHA-256, no third-party)", + "altcha_key_generation": "openssl rand -hex 32", + "altcha_csp_requirement": "worker-src 'self' blob: (CRITICAL)", + "rate_limiting": { + "nginx": "5 requests/min per IP, burst 3", + "php": "5/10min per IP, file-backed (/tmp/form-rate-limit/)" + }, + "security_layers": [ + "nginx rate limit: 5r/min per IP", + "PHP rate limit: 5/10min per IP", + "honeypot field: hidden 'website' input", + "time-on-page check: <3s = [REVIEW] in subject", + "Altcha proof-of-work: SHA-256 + HMAC verification", + "server-side validation: all fields checked, HTML-escaped", + "32KB body cap: reject oversized payloads" + ], + "email_providers": { + "resend": "REST API (transactional, PHP curl)", + "msmtp": "SMTP relay (for cPanel + existing SMTP)" + } + }, + "seo": { + "required_meta_tags": [ + "charset UTF-8", + "viewport width=device-width, initial-scale=1.0", + "title (under 60 chars)", + "description (150-160 chars)", + "canonical link", + "Open Graph: og:type, og:url, og:title, og:description, og:image, og:site_name", + "Twitter: card, url, title, description, image", + "robots index, follow", + "theme-color (mobile)", + "favicon.svg, favicon-32.png, apple-touch-icon" + ], + "required_files": [ + "/robots.txt (Disallow /api/, include Sitemap)", + "/sitemap.xml (one <url> per page with <lastmod>)", + "/llms.txt (llmstxt.org standard)" + ], + "schema_required": [ + "LocalBusiness (home page, location pages)", + "Service (service detail pages)", + "BreadcrumbList (every page)", + "FAQPage (FAQ pages only)" + ], + "og_image": "1200x630px, brand colors, under 200KB", + "title_rules": "{Service} | {Brand} | {City}, {State}", + "description_rules": "150-160 chars, action-oriented, include city + service" + }, + "images": { + "format": "AVIF + JPG fallback", + "picture_element_required": true, + "conversion_command": "convert input.jpg -resize 1920x -quality 80 -define avif:speed=6 output.avif", + "size_targets": { + "portrait": "original width, 80 quality, 50-120KB", + "hero": "1920px max, 80 quality, 150-350KB", + "og_social": "1200px, 85 quality, under 150KB" + }, + "hero_naming": "hero-{page-slug}.avif + .jpg fallback", + "attributes_required": ["alt text or alt=\"\"", "loading=\"lazy\" (except hero)", "width and height"] + }, + "mobile": { + "breakpoints": { + "320": "mobile-first base", + "360": "iPhone SE portrait", + "480": "small phones", + "600": "phones", + "768": "tablets (main mobile breakpoint)", + "900": "small laptops, tablet landscape", + "1023": "IMPORTANT: switch to mobile menu here", + "1024": "sub-desktop" + }, + "header_nav_switch_at": "max-width: 1023px (not 768px)", + "touch_targets_minimum": "44x44px (Apple HIG, WCAG)", + "grid_mobile_override": "grid-template-columns: 1fr !important at 900px", + "overflow_protection": "overflow-x: clip; max-width: 100%" + }, + "cookie_consent": { + "option1": { + "name": "orestbida/cookieconsent", + "stars": "4600+", + "size": "23KB UMD + 32KB CSS", + "license": "MIT", + "recommendation": "default choice" + }, + "option2": { + "name": "Osano Cookie Consent", + "stars": "3500+", + "size": "30KB bundle", + "license": "MIT" + }, + "when_not_needed": "GDPR Article 4(11): strictly necessary cookies (session, CSRF, form state) exempt from consent" + }, + "testing": { + "build_verification": "grep -rn '{{' site/ -- result: empty", + "container_health": "docker compose ps, docker logs, curl / returns 200", + "url_surface": "public paths 200, sensitive paths 404", + "mobile_responsive": "zero horizontal overflow at 320, 360, 390, 768, 900, 1023, 1024, 1200", + "form_e2e": "submit real form, verify email arrives", + "rate_limit": "request 6 returns 429 Too Many Requests", + "seo_surface": "all pages have title, canonical, og:, schema JSON-LD", + "cache_busting": "main.css?v=<unix-timestamp> changes on deploy", + "pre_launch_gates": [ + "All public URLs 200", + "All sensitive URLs 404", + "No sensitive files in container", + "Zero mobile overflow", + "Form submits, email arrives", + "Rate limit triggers", + "All pages have required meta tags", + "robots.txt and sitemap.xml exist", + "Zero em-dashes in HTML/JSON", + "Resend domain fully verified", + "Test email lands in primary inbox", + "Tested on real iPhone and Android", + "Lighthouse scores 90+ on all categories" + ] + }, + "never_use": [ + "Node.js / npm on frontend", + "WordPress for new builds", + "CSS frameworks (Bootstrap, Tailwind, Bulma)", + "JS frameworks (React, Vue, Angular, Svelte)", + "jQuery, Lodash, Moment, axios, utility libraries", + "CSS-in-JS, styled-components", + "Build tools requiring node_modules", + "Tracking pixels (except client-explicitly-requested)", + "Single monolithic database", + "Hardcoded copy in PHP templates", + "Docker Compose for arisingmedia.us stack", + "Google Drive / Sheets for content (evaluated and reverted 2026-06)" + ], + "secure_app_features": { + "when_to_use": "File conversion, encryption, payment processing, user authentication, rate-limited APIs", + "php_hardening": [ + "CSRF token on every POST endpoint", + "Rate limiting (nginx + PHP file-backed)", + "Altcha proof-of-work verification", + "At-rest encryption (sodium_crypto_secretbox + secretstream)", + "Signed download tokens (never expose file paths)", + "Server-side validation + HTML-escaped output", + "Session httponly + secure flags", + "storage/ directory access denied via .htaccess" + ], + "database": "SQLite with pdo_sqlite, schema migrations in try/catch, BEGIN IMMEDIATE transactions", + "environment": { + "clear_env": "no (php-fpm-pool.conf)", + "secrets": ".env (never hardcoded, never committed)", + "trust_proxy": "1 when behind reverse proxy", + "encryption_key": "32-byte hex QC_ENCRYPTION_KEY", + "altcha_key": "32-byte hex ALTCHA_HMAC_KEY" + }, + "reference": "quickconvert.us" + }, + "directory_structure": { + "src/api/": "PHP router, contact handler, templates, components, data (SQLite)", + "src/assets/": "CSS (tokens.css + main.css), JS (vanilla only), images (AVIF+JPG), altcha, cookieconsent", + "build/": "seed_databases.py", + "infra/": "nginx.conf, supervisord.conf, php-fpm-pool.conf, entrypoint.sh", + ".planning/": "not served, not in Docker image", + "root_files": "Dockerfile, docker-compose.yml, .dockerignore, .htaccess, .cpanel.yml, .gitignore, .env (ignored)" + } +} diff --git a/tools/verify-protection.sh b/tools/verify-protection.sh new file mode 100644 index 0000000..52983aa --- /dev/null +++ b/tools/verify-protection.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +# verify-protection.sh — confirm a deployed Arising Media site is not leaking +# build artifacts, server config, or dotfiles to the public web. +# +# Usage: +# ./verify-protection.sh <base-url> +# ./verify-protection.sh http://localhost:8010 +# ./verify-protection.sh https://cobhamtech.com +# +# Exit 0 if every check passes, 1 otherwise. Designed to be run after every +# deploy and in CI. + +set -euo pipefail + +BASE="${1:-}" +if [[ -z "$BASE" ]]; then + echo "usage: $0 <base-url>" >&2 + exit 2 +fi +BASE="${BASE%/}" + +# Required paths — site must serve these; missing = audit FAIL. +REQUIRED=( + "/" +) + +# Public paths — should be reachable; missing = WARN (content gap, not a leak). +PUBLIC=( + "/robots.txt" + "/sitemap.xml" +) + +# Sensitive paths — must NOT return 200. 404 or 403 is acceptable. +# This list mirrors the deny patterns in SOP 08 nginx.conf. +SENSITIVE=( + "/Dockerfile" + "/dockerfile" + "/docker-compose.yml" + "/nginx.conf" + "/.dockerignore" + "/.gitignore" + "/.git/config" + "/.git/HEAD" + "/.env" + "/.env.example" + "/api/.env" + "/.planning/" + "/.planning/build_locations.py" + "/.planning/build_services.py" + "/.planning/regen_images.py" + "/.planning/playwright_audit.py" + "/__pycache__/" + "/build_locations.py" + "/build_services.py" + "/regen_images.py" + "/playwright_audit.py" + "/server.py" + "/main.py" + "/README.md" + "/package.json" + "/composer.json" +) + +fail=0 + +warn=0 + +probe() { + local path="$1" + local expect="$2" # public | required | sensitive + local code + code=$(curl -k -s -o /dev/null -w '%{http_code}' --max-time 5 "${BASE}${path}" || echo "000") + case "$expect" in + required) + if [[ "$code" =~ ^(200|301|302|304)$ ]]; then + printf ' OK %-3s %s\n' "$code" "$path" + else + printf ' FAIL %-3s %s (required public path unreachable)\n' "$code" "$path" + fail=1 + fi + ;; + public) + if [[ "$code" =~ ^(200|301|302|304)$ ]]; then + printf ' OK %-3s %s\n' "$code" "$path" + else + printf ' WARN %-3s %s (public path unreachable — content gap, not a leak)\n' "$code" "$path" + warn=1 + fi + ;; + sensitive) + if [[ "$code" == "200" ]]; then + printf ' LEAK %-3s %s (must not return 200)\n' "$code" "$path" + fail=1 + else + printf ' OK %-3s %s\n' "$code" "$path" + fi + ;; + esac +} + +echo "Verifying ${BASE}" +echo +echo "Required paths (site must serve these):" +for p in "${REQUIRED[@]}"; do probe "$p" required; done +echo +echo "Public paths (should be reachable):" +for p in "${PUBLIC[@]}"; do probe "$p" public; done +echo +echo "Sensitive paths (must not be reachable):" +for p in "${SENSITIVE[@]}"; do probe "$p" sensitive; done +echo + +if [[ $fail -ne 0 ]]; then + echo "FAIL — exposure or required-path failure at ${BASE}" >&2 + exit 1 +elif [[ $warn -ne 0 ]]; then + echo "PASS (with warnings) — no exposure at ${BASE}, but missing public content" + exit 0 +else + echo "PASS — no exposure detected at ${BASE}" + exit 0 +fi diff --git a/wp-divi-pipeline-to-am-stack/00-overview.md b/wp-divi-pipeline-to-am-stack/00-overview.md new file mode 100644 index 0000000..a6bd9cd --- /dev/null +++ b/wp-divi-pipeline-to-am-stack/00-overview.md @@ -0,0 +1,94 @@ +# 00 — WP + Divi to AM Stack A Pipeline — Overview + +Converts a .wpress archive (All-in-One WP Migration) into a Stack A deployment: +PHP router + SQLite databases + vanilla JS/CSS. Never a 1:1 Divi copy. +Every migration is a content extraction and redesign, not a port. + +## Stack A output (what this pipeline produces) + +``` +src/api/router.php URL dispatcher +src/api/contact.php form handler (Resend via curl) +src/api/templates/*.php home | static | classes | schedule | glossary | blog +src/api/components/_header.php nav from nav.sqlite +src/api/components/_footer.php +src/api/data/*.sqlite one DB per content domain (see 09-stack-a-output.md) +build/seed_databases.py creates + seeds all SQLite DBs — THE source of truth +assets/ vanilla CSS/JS/images +infra/nginx.conf, supervisord.conf, php-fpm-pool.conf +Dockerfile (php:8.3-fpm-alpine) +docker-compose.yml +``` + +## Why NOT static HTML + +Any site with a glossary, blog, schedule, or recurring content model gets Stack A. +Editing content = edit seed_databases.py → reseed → rebuild. No PHP file edits. + +## Divi is the data source, not the design target + +Extract from Divi: +- Page content (headings, body copy, CTAs) +- Navigation menus (wp_terms + wp_termmeta) +- Header logo + tagline (wp_options: blogname, blogdescription, et_divi) +- Media (uploads/ → WebP → assets/images/) +- Design tokens (colors, fonts → tokens.css) +- SEO (Yoast wp_postmeta → pages.sqlite meta_description) +- Blog posts (wp_posts where post_type=post) +- Custom post types (testimonials, FAQs, glossary terms if present) + +Do NOT replicate: +- Divi section/row/column grid structure +- Divi module types (blurbs, toggles, CTAs, pricing tables) +- WordPress page slugs (map to clean slugs per nginx.conf pattern) +- WordPress menu item IDs + +## Pipeline phases + +``` +Phase 0 Setup Point pipeline at .wpress file; create working dirs +Phase 1 Extract Unpack .wpress → wpress-extract/ +Phase 2 DB Analysis Parse SQL dump; detect Divi version; inventory pages, posts, menus +Phase 3 Content Extract page sections + nav menus + blog posts from Divi +Phase 4 Design Pull colors + fonts → tokens.css draft +Phase 5 Media Catalog uploads/; convert to WebP; build media-manifest.json +Phase 6 Staging Map extracted JSON → seed_databases.py skeleton (content on standby) +Phase 7 Fill Agent fills each SQLite table row by row from staged JSON +Phase 8 Templates Scaffold PHP templates + components from AM reference +Phase 9 SEO Port titles, metas, canonicals, schema.org, redirect map +Phase 10 Build docker compose build && docker compose up -d +Phase 11 QA Lighthouse, protection check, grep for Divi residue +``` + +## CLI launcher + +``` +python3 scripts/migrate.py --wpress /path/to/backup.wpress --domain example.com +``` + +Runs phases 0-6 automatically, then prints agent breadcrumbs for phases 7-11. + +## Key missed items from prior migrations (REQUIRED fixes) + +1. **NAV MENUS**: Must extract wp_terms (taxonomy=nav_menu) + wp_termmeta for label/URL/order. + Output: nav.json → seeded into nav.sqlite (label, href, display_order, is_cta). + +2. **DIVI HEADER**: Must extract et_divi options from wp_options for logo, header layout, colors. + The _header.php must be written from scratch using AM design tokens, not copied from Divi. + +3. **MEDIA**: All uploads/ files must be: cataloged → copied to assets/images/ → converted to WebP. + Every image reference in content JSON must be updated to /assets/images/{filename}.webp. + +4. **SECTION REMAPPING**: Divi modules must be remapped to AM section types. + - blurb_module → feature_cards item + - toggle_module → accordion item + - cta_module → cta_band section + - pricing_module → booking_options section + - testimonial_mod → testimonials.sqlite row + - text_module → text_block section + +## Related SOPs + +- **09-stack-a-output.md** — SQLite schema + sections_json spec +- **10-agent-breadcrumbs.md** — Step-by-step ordered checklist for agent execution +- **00-stack-philosophy.md** — Stack A vs Stack B decision rationale diff --git a/wp-divi-pipeline-to-am-stack/01-wpress-extraction.md b/wp-divi-pipeline-to-am-stack/01-wpress-extraction.md new file mode 100644 index 0000000..b26905e --- /dev/null +++ b/wp-divi-pipeline-to-am-stack/01-wpress-extraction.md @@ -0,0 +1,120 @@ +# 01 — .wpress Extraction + +Unpack the All-in-One WP Migration `.wpress` archive into the project's +`.planning/wpress-extract/` directory. + +## .wpress binary format + +NOT a standard zip or tar. Custom sequential binary format: + +``` +[HEADER 4377 bytes] [FILE DATA n bytes] [HEADER] [FILE DATA] ... +``` + +Header breakdown: +``` +Offset Length Field +0 255 Filename (null-padded) +255 14 File size in bytes (ASCII decimal, null-padded) +269 12 mtime unix timestamp (ASCII decimal, null-padded) +281 4096 Relative path (null-padded) +4377 n Raw file bytes (size from header) +``` + +The archive ends when a header of all null bytes is encountered, or EOF. + +## Extraction script + +Script: `.am-webdesign-sops/wp-divi-pipeline/scripts/extract_wpress.py` + +```bash +python3 ~/.am-webdesign-sops-path/scripts/extract_wpress.py \ + .planning/vibrantyou-yoga-YYYYMMDD-*.wpress \ + .planning/wpress-extract/ +``` + +Or from the SOP scripts directory directly: + +```bash +python3 /home/sirdrez/arisingmedia-websites/.am-webdesign-sops/wp-divi-pipeline/scripts/extract_wpress.py \ + /home/sirdrez/arisingmedia-websites/{domain}/.planning/{file}.wpress \ + /home/sirdrez/arisingmedia-websites/{domain}/.planning/wpress-extract/ +``` + +Progress prints every 200 files. A 300-400MB archive typically extracts in +2-5 minutes and produces 1,000-5,000 files. + +## Expected archive contents + +After extraction, `wpress-extract/` contains: + +``` +wpress-extract/ +├── package.json ← archive metadata (domain, WP version, plugin list) +├── database.sql ← full MySQL dump (the most important file) +└── wp-content/ + ├── uploads/ ← all media (images, PDFs, videos) + │ └── YYYY/MM/ ← WordPress date-organized subdirs + ├── themes/ + │ ├── Divi/ ← Divi 4 theme files (if Divi 4) + │ └── divi-5/ ← Divi 5 theme files (if Divi 5) + └── plugins/ ← installed plugins (useful for form schema) + ├── gravityforms/ + └── contact-form-7/ +``` + +## Verify extraction + +After the script completes, confirm the key files exist: + +```bash +# Database dump present? +ls -lh .planning/wpress-extract/database.sql + +# Uploads present? +find .planning/wpress-extract/wp-content/uploads -name "*.jpg" | wc -l +find .planning/wpress-extract/wp-content/uploads -name "*.png" | wc -l + +# Archive metadata +cat .planning/wpress-extract/package.json +``` + +`package.json` contains the site URL, WordPress version, Divi version, and +plugin list — read it before proceeding to Phase 2. + +## Common issues + +**"Not a zip file" error** — Expected. The .wpress format is not zip. +The `extract_wpress.py` script handles it correctly. + +**Missing database.sql** — The archive may name it differently. Check: +```bash +find .planning/wpress-extract -name "*.sql" 2>/dev/null +``` + +**Partial extraction** — If the script stops early, check disk space: +```bash +df -h .planning/wpress-extract/ +``` +A 378MB .wpress typically expands to 1-3GB uncompressed. + +**Path traversal in filenames** — The script strips leading `/` and `.` from +paths. If files land in unexpected locations, check the raw path field with: +```bash +python3 -c " +import sys +HEADER_SIZE=4377; NAME_LEN=255; SIZE_LEN=14; MTIME_LEN=12; PATH_LEN=4096 +with open(sys.argv[1],'rb') as f: + for i in range(5): + h = f.read(HEADER_SIZE) + name = h[:NAME_LEN].split(b'\x00',1)[0].decode(errors='replace') + size = int(h[NAME_LEN:NAME_LEN+SIZE_LEN].split(b'\x00',1)[0] or 0) + path = h[NAME_LEN+SIZE_LEN+MTIME_LEN:].split(b'\x00',1)[0].decode(errors='replace') + print(f' [{i}] path={repr(path)} name={repr(name)} size={size}') + f.seek(size, 1) +" .planning/file.wpress +``` + +## Next step + +Proceed to `02-database-analysis.md` to inventory pages and detect Divi version. diff --git a/wp-divi-pipeline-to-am-stack/02-database-analysis.md b/wp-divi-pipeline-to-am-stack/02-database-analysis.md new file mode 100644 index 0000000..482ac92 --- /dev/null +++ b/wp-divi-pipeline-to-am-stack/02-database-analysis.md @@ -0,0 +1,151 @@ +# 02 — Database Analysis + +Parse the WordPress MySQL dump to inventory pages, detect Divi version, +extract design settings, and build the data JSON files that drive the AM build. + +## Script + +```bash +python3 /home/sirdrez/arisingmedia-websites/.am-webdesign-sops/wp-divi-pipeline/scripts/analyze_db.py \ + {domain}/.planning/wpress-extract/ \ + {domain}/.planning/data/ +``` + +Outputs three files into `.planning/data/`: +- `pages.json` — all published pages/posts with content and SEO meta +- `design-system.json` — colors, fonts, Divi settings +- `site-info.json` — domain, plugin list, WP version, Divi version + +## Divi version detection + +The script auto-detects Divi version by scanning `database.sql`: + +| Signal in SQL | Divi version | +|---------------|-------------| +| `wp:divi/` in post_content | Divi 5 | +| `[et_pb_section` in post_content | Divi 4 | + +**This determines the content extraction path.** Divi 4 → use `extract_divi4.py`. +Divi 5 → use `extract_divi5.py`. See `03-divi-content-extraction.md`. + +## Key WordPress tables + +| Table | Contents | Used for | +|-------|----------|---------| +| `wp_posts` | All pages, posts, attachments, layouts | Page inventory, content | +| `wp_postmeta` | Per-post metadata | ACF fields, Rank Math SEO, Divi layout JSON | +| `wp_options` | Site-wide settings | Divi theme settings, colors, fonts | +| `wp_gf_forms` | Gravity Forms definitions | Form field schema | +| `wp_gf_entries` | Gravity Form submissions | Not needed for migration | +| `wp_rank_math_seo_meta` | Rank Math SEO per page | SEO titles, descriptions | + +## Reading pages.json + +Each entry in `pages.json`: + +```json +{ + "id": "42", + "post_type": "page", + "slug": "about", + "title": "About VibrantYou Yoga", + "status": "publish", + "date": "2026-03-15", + "modified": "2026-04-10", + "content_raw": "<!-- wp:divi/section ... -->...", + "excerpt": "", + "parent_id": "0", + "menu_order": "3", + "seo_title": "About VibrantYou Yoga | Mindful Movement in [City]", + "seo_description": "...", + "seo_keywords": "yoga studio, mindful movement", + "acf": { + "vyy_hero_headline": "Move With Intention", + "vyy_hero_subhead": "..." + } +} +``` + +`content_raw` holds the raw Divi block markup. Pass it to the extractor scripts. +`acf` holds Advanced Custom Fields values — often cleaner than block content. + +## Reading design-system.json + +Contains extracted Divi theme settings. Key fields: + +```json +{ + "primary_color": "#1a8a7a", + "body_font": "DM Sans", + "header_font": "DM Serif Display", + "body_font_size": "16", + "body_line_height": "1.7", + "divi_version": "5", + "wp_version": "6.9.4", + "site_url": "https://vibrantyou.yoga", + "site_name": "VibrantYou Yoga" +} +``` + +Use these values to seed the AM `main.css` CSS custom properties block. + +## Manual inspection (when script output is sparse) + +Sometimes the Divi theme options are stored as PHP-serialized data. +Use grep to find and eyeball the raw values: + +```bash +DB=.planning/wpress-extract/database.sql + +# Divi global colors +grep -o "'et_divi[^']*','[^']*'" $DB | head -30 + +# Site name + URL +grep -E "'(siteurl|blogname|admin_email)','[^']*'" $DB + +# Rank Math SEO meta for a specific post +grep "rank_math_title\|rank_math_description" $DB | head -20 + +# All published page slugs +grep -o "post_name','[^']*'" $DB | grep -v "revision\|auto-draft" | sort | uniq +``` + +## Gravity Forms schema (for form replacement) + +Find form field definitions: + +```bash +grep "INSERT INTO \`wp_gf_forms\`" .planning/wpress-extract/database.sql | \ + python3 -c " +import sys, json, re +for line in sys.stdin: + m = re.search(r\"'([^']+)'\s*\)\s*;\", line) + if m: + try: print(json.dumps(json.loads(m.group(1).replace('\\\\\"','\"')), indent=2)[:2000]) + except: pass +" 2>/dev/null | head -100 +``` + +Field types seen in Gravity Forms: text, email, phone, textarea, select, checkbox, radio, name, address, fileupload. Map each to a plain HTML input equivalent. + +## Archive directory layout note + +The AIOIM .wpress format extracts flat — no `wp-content/` wrapper: + +``` +wpress-extract/ +├── database.sql ← NOT in wp-content/ +├── package.json +├── uploads/ ← NOT wp-content/uploads/ +├── themes/ ← NOT wp-content/themes/ +├── plugins/ ← NOT wp-content/plugins/ +└── et-cache/ +``` + +Scripts must reference `uploads/`, `themes/`, `plugins/` directly under +`wpress-extract/`, not `wpress-extract/wp-content/`. + +## Next step + +Once `pages.json` is written, proceed to `03-divi-content-extraction.md` +to parse `content_raw` for each page into structured AM-ready HTML. diff --git a/wp-divi-pipeline-to-am-stack/03-divi-content-extraction.md b/wp-divi-pipeline-to-am-stack/03-divi-content-extraction.md new file mode 100644 index 0000000..c8d3991 --- /dev/null +++ b/wp-divi-pipeline-to-am-stack/03-divi-content-extraction.md @@ -0,0 +1,157 @@ +# 03 — Divi Content Extraction + +Parse raw Divi page content from `pages.json` into clean, structured HTML +sections ready to map into AM templates. + +## Divi 4 vs Divi 5 — critical difference + +### Divi 4 (shortcode-based) + +Content is stored as shortcodes in `wp_posts.post_content`: + +``` +[et_pb_section fb_built="1" admin_label="Hero" _builder_version="4.27.4" + background_color="#0f5f53" ...] + [et_pb_row ...] + [et_pb_column type="4_4" ...] + [et_pb_text ...]<h1>Move With Intention</h1>[/et_pb_text] + [et_pb_button button_url="/contact" button_text="Book a Class" /] + [/et_pb_column] + [/et_pb_row] +[/et_pb_section] +``` + +Use `extract_divi4.py` → parses shortcode tree into section/row/module JSON. + +### Divi 5 (block-based) + +Content is stored as Gutenberg-style block comments: + +```html +<!-- wp:divi/section {"id":"section-abc123","attrs":{"backgroundColor":{"value":"#0f5f53"}}} --> +<div class="et_pb_section ..."> + <!-- wp:divi/row ... --> + <!-- wp:divi/column ... --> + <!-- wp:divi/text ... --> + <div class="et_pb_text_inner"><h1>Move With Intention</h1></div> + <!-- /wp:divi/text --> + <!-- /wp:divi/column --> + <!-- /wp:divi/row --> +</div> +<!-- /wp:divi/section --> +``` + +Use `extract_divi5.py` → strips block wrapper, extracts inner HTML per module. + +## Divi 5 extraction script + +```bash +python3 /home/sirdrez/arisingmedia-websites/.am-webdesign-sops/wp-divi-pipeline/scripts/extract_divi5.py \ + {domain}/.planning/data/pages.json \ + {domain}/.planning/data/content/ +``` + +Produces one JSON file per page: `content/{slug}.json` + +```json +{ + "slug": "about", + "title": "About VibrantYou Yoga", + "seo_title": "About VibrantYou Yoga | ...", + "seo_description": "...", + "sections": [ + { + "type": "hero", + "background_color": "#0f5f53", + "modules": [ + { "module": "text", "html": "<h1>Move With Intention</h1>" }, + { "module": "button", "text": "Book a Class", "url": "/contact/" } + ] + }, + { + "type": "standard", + "modules": [ + { "module": "text", "html": "<h2>Our Story</h2><p>...</p>" }, + { "module": "image", "src": "/assets/images/studio.webp", "alt": "..." } + ] + } + ] +} +``` + +## ACF fields take priority + +If a page has ACF fields (in `pages.json[].acf`), use those over block content. +ACF fields are typically cleaner, pre-authored copy without Divi wrapper noise. + +Convention for VYY-specific ACF keys: +- `vyy_hero_headline` → `<h1>` in hero section +- `vyy_hero_subhead` → `<p class="hero-lead">` in hero +- `vyy_hero_cta_text` → primary CTA button label +- `vyy_hero_cta_url` → primary CTA button href + +Always check `acf` keys before parsing `content_raw`. + +## Stripping Divi class/attribute noise + +After extraction, run every HTML snippet through the `clean_divi_html()` +function from `divi_to_html.py`: + +```python +from divi_to_html import clean_divi_html, rewrite_internal_links + +cleaned = clean_divi_html(raw_html) +cleaned = rewrite_internal_links(cleaned, staging_hosts=("vibrantyou.yoga",)) +``` + +This removes: +- `<!-- wp:divi/... -->` block comments +- `data-et-*`, `data-builder-*` attributes +- `et_pb_*`, `divi-builder-*`, `d5_*` class tokens +- Empty `class=""` attributes + +## What to extract per section type + +| Divi module | Extract | Map to AM element | +|-------------|---------|-------------------| +| `divi/text` | inner HTML | `<section>`, `<p>`, headings as-is | +| `divi/button` | `text`, `url` | `<a class="btn-primary">` | +| `divi/image` | `src`, `alt`, `title` | `<img>` → rewrite to WebP path | +| `divi/blurb` | icon, title, body | `.am-card` component | +| `divi/testimonial` | quote, author, company | `.am-testimonial` component | +| `divi/video` | `src`, poster | `<video>` or YouTube embed | +| `divi/contact_form` | field list | → replace with AM form, see `08` | +| `divi/accordion` | Q+A pairs | `<details><summary>` | +| `divi/fullwidth_header` | title, subhead, CTA | hero section | + +## Section background colors → AM section modifiers + +Divi 5 stores `backgroundColor` in the block `attrs` JSON. +Map to AM CSS modifier classes: + +| Divi background | AM class modifier | +|----------------|------------------| +| `#0f5f53` (dark teal) | `.section--dark` | +| `#1a8a7a` (mid teal) | `.section--brand` | +| `#f5f5f5` / `#fafafa` | `.section--light` | +| `#ffffff` / none | `.section--white` | + +## Content quality pass (required before HTML build) + +After extraction, review every page's content for: + +1. **Cut bloated copy** — WordPress sites often have 3x more text than needed. + Target 30-50% reduction. One clear idea per paragraph. +2. **Remove stale metrics** — "Over 500 students" only stays if it's verifiable. + Otherwise remove or mark `DRAFT NEEDED`. +3. **Remove plugin artifacts** — Gravity Forms shortcodes `[gravityforms id="1"]`, + Events Manager tags, Divi shortcode residue that survived extraction. +4. **Improve CTAs** — Replace generic "Learn More" with action-specific text: + "Book a Free Class", "View the Schedule", "Start Your Practice". +5. **Flag images** — Note every `<img>` that needs a real photo vs stock. + +## Next step + +Proceed to `04-design-system-extraction.md` to convert Divi theme settings +into AM CSS custom properties, then `05-content-migration.md` to build the +HTML templates. diff --git a/wp-divi-pipeline-to-am-stack/04-design-system-extraction.md b/wp-divi-pipeline-to-am-stack/04-design-system-extraction.md new file mode 100644 index 0000000..36ff95f --- /dev/null +++ b/wp-divi-pipeline-to-am-stack/04-design-system-extraction.md @@ -0,0 +1,172 @@ +# 04 — Design System Extraction + +Convert Divi theme settings into AM CSS custom properties. +The goal is to ENHANCE the design — cleaner, more modern — not replicate it. + +## Input + +`design-system.json` produced by `analyze_db.py`. Key fields: + +```json +{ + "primary_color": "#1a8a7a", + "body_font": "DM Sans", + "header_font": "DM Serif Display", + "body_font_size": "16", + "body_line_height": "1.7", + "site_name": "VibrantYou Yoga" +} +``` + +## Color palette strategy + +Never lift the Divi palette 1:1. Use extracted colors as the base and build a +full 5-step scale around the primary hue: + +| Token | Derived from | Role | +|-------|-------------|------| +| `--color-primary` | Divi accent_color | Buttons, links, active states | +| `--color-primary-dark` | Darken primary 15% | Hover states, section backgrounds | +| `--color-primary-light` | Lighten primary 40% | Subtle tints, borders | +| `--color-surface` | Always `#fafafa` | Page background | +| `--color-surface-alt` | `#f3f3f3` | Alternating sections | +| `--color-text` | Always `#1a1a1a` | Body copy | +| `--color-text-muted` | `#666` | Subheadings, captions | +| `--color-border` | 10% primary or `#e0e0e0` | Dividers, inputs | +| `--color-white` | `#ffffff` | Card backgrounds, hero text | + +For VibrantYou Yoga (primary `#1a8a7a`, dark `#0f5f53`): + +```css +:root { + --color-primary: #1a8a7a; + --color-primary-dark: #0f5f53; + --color-primary-light: #d4f0eb; + --color-surface: #fafafa; + --color-surface-alt: #f0f7f6; + --color-text: #1a1a1a; + --color-text-muted: #5a6e6b; + --color-border: #c8dedd; + --color-white: #ffffff; +} +``` + +## Typography strategy + +Use the extracted fonts but upgrade the type scale. +Divi's default type scale is too small and too flat. Aim for 1.25–1.333 modular ratio. + +```css +:root { + /* Fonts from design-system.json */ + --font-body: 'DM Sans', system-ui, sans-serif; + --font-heading: 'DM Serif Display', Georgia, serif; + + /* Modular scale (1.25 ratio from 16px base) */ + --text-xs: 0.75rem; /* 12px */ + --text-sm: 0.875rem; /* 14px */ + --text-base: 1rem; /* 16px */ + --text-lg: 1.125rem; /* 18px */ + --text-xl: 1.25rem; /* 20px */ + --text-2xl: 1.5rem; /* 24px */ + --text-3xl: 1.875rem; /* 30px */ + --text-4xl: 2.25rem; /* 36px */ + --text-5xl: 3rem; /* 48px */ + --text-6xl: 3.75rem; /* 60px */ + + /* Line heights */ + --leading-tight: 1.2; + --leading-normal: 1.6; + --leading-loose: 1.8; + + /* Font weights */ + --weight-normal: 400; + --weight-medium: 500; + --weight-semibold: 600; + --weight-bold: 700; +} +``` + +## Spacing and layout + +Divi uses pixel-based margins/paddings that must be converted to a consistent +rem-based spacing scale: + +```css +:root { + --space-1: 0.25rem; /* 4px */ + --space-2: 0.5rem; /* 8px */ + --space-3: 0.75rem; /* 12px */ + --space-4: 1rem; /* 16px */ + --space-5: 1.25rem; /* 20px */ + --space-6: 1.5rem; /* 24px */ + --space-8: 2rem; /* 32px */ + --space-10: 2.5rem; /* 40px */ + --space-12: 3rem; /* 48px */ + --space-16: 4rem; /* 64px */ + --space-20: 5rem; /* 80px */ + --space-24: 6rem; /* 96px */ + --space-32: 8rem; /* 128px */ + + /* Section vertical padding */ + --section-py: var(--space-20); /* 80px default */ + --section-py-sm: var(--space-12); /* 48px mobile */ + + /* Container */ + --container-max: 1200px; + --container-px: var(--space-6); + + /* Border radius */ + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 20px; + --radius-full: 9999px; + + /* Shadows */ + --shadow-sm: 0 1px 3px rgba(0,0,0,.08); + --shadow-md: 0 4px 16px rgba(0,0,0,.1); + --shadow-lg: 0 12px 40px rgba(0,0,0,.12); +} +``` + +## Google Fonts import + +For DM Sans + DM Serif Display: + +```html +<link rel="preconnect" href="https://fonts.googleapis.com"> +<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> +<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&family=DM+Serif+Display:ital@0;1&display=swap" rel="stylesheet"> +``` + +## Enhancement rules (required) + +These upgrades apply to every AM migration regardless of source: + +1. **Increase contrast** — body text must be #1a1a1a on white (WCAG AA minimum). + Never use the grey-on-grey color schemes that Divi themes commonly use. + +2. **Whitespace is content** — section padding must be at minimum 80px vertical + on desktop. Divi often uses 40-60px which feels cramped. + +3. **One weight per heading level** — h1 at 700, h2 at 600, h3 at 500. + Divi often leaves all headings at the same weight. + +4. **Max-width prose** — body copy containers max 680px wide. Divi stretches + copy to full column width on 1200px screens, which is unreadable. + +5. **Brand color is a highlight, not a wallpaper** — primary color should + appear on buttons, links, and 1-2 hero sections only. Divi sites often + paint every other section in the primary color. + +## Output: main.css variables block + +Write the complete `:root {}` block into `src/assets/css/main.css` as the +first section. All other CSS rules reference only `var(--token-name)`. +Never hard-code a color, font, or spacing value outside of `:root`. + +## Next step + +Proceed to `05-content-migration.md` to map extracted content into AM HTML +templates using this design system. diff --git a/wp-divi-pipeline-to-am-stack/05-content-migration.md b/wp-divi-pipeline-to-am-stack/05-content-migration.md new file mode 100644 index 0000000..5fab6b4 --- /dev/null +++ b/wp-divi-pipeline-to-am-stack/05-content-migration.md @@ -0,0 +1,246 @@ +# 05 — Content Migration + +Map extracted Divi content into AM HTML templates. This is the build phase. +Follow `01-project-structure.md` for directory layout and `03-build-pipeline.md` +for JSON + template stamping. + +## Source files + +After running Phase 2-4 scripts, `.planning/data/` contains: + +``` +.planning/data/ +├── pages.json ← all published pages (from analyze_db.py) +├── site-info.json ← domain, plugin list, Divi version +├── design-system.json ← colors, fonts, spacing tokens +└── content/ + ├── home.json ← parsed sections for home page + ├── about.json ← parsed sections for about page + ├── services.json + └── ... ← one file per published page +``` + +## Information architecture for yoga sites + +Standard AM structure for a yoga studio / wellness site: + +``` +/ home (hero, classes preview, testimonials, CTA) +/about/ about / story / instructors +/classes/ class schedule index +/classes/{slug}.html one page per class type (hatha, vinyasa, yin, etc.) +/private-sessions/ 1:1 session offerings +/workshops/ workshops + retreats index +/contact/ contact + booking form +/blog/ optional blog index +/blog/{slug}.html individual blog posts +/404.html +/500.html +/robots.txt +/sitemap.xml +``` + +Map every WP page slug to this structure first. Some WP slugs may need to be +consolidated, renamed, or dropped. Document the redirect map in +`.planning/redirect-map.txt` (old slug → new path). + +## Build order + +Build in this sequence. Each page uses the previous as a reference: + +1. `src/assets/css/main.css` — design tokens, reset, typography, layout grid +2. `src/assets/css/components.css` — header, footer, hero, cards, forms, nav +3. `src/components/header.html` — navigation +4. `src/components/footer.html` — footer links, contact info +5. `src/assets/js/components.js` — fetch + inject header/footer +6. `src/assets/js/main.js` — scroll animations, intersection observer +7. `src/index.html` — home page (this IS the design system in working form) +8. `src/about/index.html` +9. `src/classes/index.html` + individual class pages (from JSON template if 4+) +10. `src/contact/index.html` + AM form +11. `src/blog/index.html` + individual posts +12. `src/robots.txt`, `src/sitemap.xml`, `src/404.html`, `src/500.html` + +## HTML page skeleton + +Every page uses the same skeleton. Copy from 06-seo-meta.md for the full +`<head>` requirements. Shell: + +```html +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta name="site-root" content="/"> + <title>{{seo_title}} + + + + + + + +
+ +
+ +
+ + + + + + +``` + +## Section HTML patterns + +Map each `content/{slug}.json` section to one of these AM patterns: + +### Hero (role: "hero") + +```html +
+
+
+

Move With Intention

+

Discover yoga classes for all levels in [City].

+ +
+
+
+``` + +### Feature grid (4-col blurb modules) + +```html +
+
+

Why VibrantYou Yoga

+
+
+
+

All Levels Welcome

+

From first-timers to advanced practitioners.

+
+ +
+
+
+``` + +### Testimonials (3-col) + +```html +
+
+

What Students Say

+
+
+

"..."

+
+ Jane D. + Student since 2024 +
+
+
+
+
+``` + +### CTA section + +```html +
+
+

Ready to Begin?

+

Your first class is on us.

+ Book a Free Class +
+
+``` + +## Class pages — JSON template build + +If there are 4+ class types (Hatha, Vinyasa, Yin, Meditation, etc.), use the +build pipeline: + +``` +src/classes/ +├── _template.html ← class detail page template +├── hatha.html ← generated from classes.json +├── vinyasa.html +├── yin.html +└── meditation.html + +.planning/data/ +└── classes.json ← array of class objects +``` + +`classes.json` schema: +```json +[ + { + "slug": "hatha", + "name": "Hatha Yoga", + "title": "Hatha Yoga Classes | VibrantYou Yoga", + "meta_description": "...", + "canonical": "https://vibrantyou.yoga/classes/hatha.html", + "hero_h1": "Hatha Yoga", + "hero_lead": "A grounding practice for all experience levels.", + "description": "

...

", + "duration": "60 min", + "level": "All levels", + "schedule": "Mon, Wed, Fri — 9:00 AM", + "instructor": "Sarah M.", + "faqs": [ + { "q": "Do I need prior experience?", "a": "No." } + ] + } +] +``` + +## Events Manager → static schedule + +The site uses Events Manager plugin. For static migration: +- Extract recurring class schedule from the database (`wp_em_events` table) +- Convert to a static schedule table / cards in `src/classes/index.html` +- Do NOT recreate a dynamic booking system unless explicitly requested +- Link the "Book" button to the contact form or an external booking URL + +## Image remapping + +Every `` extracted from Divi content will have a WordPress +upload URL like `/wp-content/uploads/2026/03/image.jpg`. + +Remap to AM path: +- Source: `wpress-extract/uploads/2026/03/image.jpg` +- AM dest: `src/assets/images/image.webp` (after WebP conversion) +- HTML: `...` + +Always include `width`, `height`, `loading="lazy"`, and `alt` on every ``. + +## After build — verify + +```bash +# Zero unreplaced template placeholders +grep -rn "{{" src/**/*.html + +# All pages have canonical +grep -rL 'rel="canonical"' src/**/*.html + +# All images have alt text +grep -rn ' .planning/data/media-raw-list.txt + +wc -l .planning/data/media-raw-list.txt +``` + +## Step 2 — Skip WordPress-generated size variants + +WordPress auto-generates resized variants: `-150x150`, `-300x200`, `-768x512`, etc. +Skip these — they are redundant once we have the originals. + +```bash +grep -v -E "\-[0-9]+x[0-9]+\.(jpg|jpeg|png|webp)$" \ + .planning/data/media-raw-list.txt > .planning/data/media-originals.txt + +echo "Originals: $(wc -l < .planning/data/media-originals.txt)" +``` + +## Step 3 — Copy originals to src/assets/images/ + +Flatten the date-organized subdirs into a single flat directory. +Preserve filenames exactly (except extension will change to .webp). + +```bash +mkdir -p src/assets/images/ + +while IFS= read -r src_path; do + filename=$(basename "$src_path") + cp "$src_path" "src/assets/images/$filename" +done < .planning/data/media-originals.txt + +echo "Copied: $(ls src/assets/images/ | wc -l) files" +``` + +## Step 4 — Convert to WebP + +Use the project's standard WebP conversion script (see `12-image-assets.md`). +If cwebp is available: + +```bash +cd src/assets/images/ +for img in *.jpg *.jpeg *.png; do + [ -f "$img" ] || continue + base="${img%.*}" + cwebp -q 82 "$img" -o "${base}.webp" 2>/dev/null && rm "$img" +done +echo "WebP conversion done. Count: $(ls *.webp | wc -l)" +``` + +Or use the Python Pillow batch script if cwebp is not installed: + +```bash +python3 /home/sirdrez/arisingmedia-websites/.am-webdesign-sops/wp-divi-pipeline/scripts/convert_images.py \ + src/assets/images/ +``` + +## Step 5 — Generate media manifest + +After conversion, build the URL remap table used during HTML build: + +```bash +python3 -c " +import os, json +from pathlib import Path + +uploads_dir = Path('.planning/wpress-extract/uploads') +site_url = 'https://vibrantyou.yoga' +am_path = '/assets/images' + +manifest = [] +for root, dirs, files in os.walk(uploads_dir): + for f in files: + full = Path(root) / f + rel = full.relative_to(uploads_dir) + # WordPress URL for this file + wp_url = f'{site_url}/wp-content/uploads/{rel}' + # Strip size variants from slug + stem = Path(f).stem + import re + stem_clean = re.sub(r'-\d+x\d+$', '', stem) + am_url = f'{am_path}/{stem_clean}.webp' + manifest.append({'wp_url': wp_url, 'am_url': am_url, 'original': f}) + +Path('.planning/data/media-manifest.json').write_text( + json.dumps(manifest, indent=2)) +print(f'Manifest: {len(manifest)} entries') +" +``` + +## Step 6 — Apply manifest during HTML build + +When writing HTML from extracted content, use the manifest to rewrite +every WordPress upload URL: + +```python +import json, re + +manifest = json.loads(open('.planning/data/media-manifest.json').read()) +url_map = {m['wp_url']: m['am_url'] for m in manifest} + +def rewrite_media_urls(html: str) -> str: + for wp_url, am_url in url_map.items(): + html = html.replace(wp_url, am_url) + # Also rewrite relative /wp-content/uploads/ paths + html = re.sub( + r'/wp-content/uploads/\d{4}/\d{2}/([^"\'>\s]+)', + lambda m: f"/assets/images/{m.group(1).split('/')[-1].rsplit('.',1)[0]}.webp", + html + ) + return html +``` + +## Files to skip + +Do not migrate these WordPress system images to `src/assets/images/`: +- `woocommerce-placeholder.png` and variants +- `wp-includes/` images (WordPress core UI) +- Plugin admin icons (anything from `plugins/` in uploads) +- Files in `wc-logs/`, `ithemes-security/`, `amcu-chunks/` subdirs + +## Logo handling + +The logo is typically at: +``` +uploads/YYYY/MM/VibrantYouYogaLogo.png +``` + +Place the logo at: +- `src/assets/images/logo.webp` — standard WebP version +- `src/assets/svg/logo.svg` — if an SVG version exists (preferred) +- `src/assets/images/logo.png` — keep PNG fallback for email/OG use + +Reference in header.html: +```html + +``` + +## OG image + +Generate one 1200×630px OG image per `06-seo-meta.md` requirements. +Place at: `src/assets/images/og-default.jpg` + +## Next step + +Proceed to `07-seo-preservation.md` to build the redirect map and audit +every page's title, description, and canonical before the HTML build. diff --git a/wp-divi-pipeline-to-am-stack/07-seo-preservation.md b/wp-divi-pipeline-to-am-stack/07-seo-preservation.md new file mode 100644 index 0000000..ac0f757 --- /dev/null +++ b/wp-divi-pipeline-to-am-stack/07-seo-preservation.md @@ -0,0 +1,182 @@ +# 07 — SEO Preservation + +Before building HTML, map every WordPress page URL to its new AM URL and +ensure title, description, canonical, and schema.org are preserved or improved. + +## Step 1 — Inventory all WP URLs + +Extract every published page slug from `pages.json`: + +```bash +python3 -c " +import json +pages = json.load(open('.planning/data/pages.json')) +for p in pages: + slug = p['slug'] + ptype = p['post_type'] + print(f'/{slug}/ ({ptype}) title={p[\"title\"]!r}') +" | tee .planning/data/wp-url-inventory.txt +``` + +## Step 2 — Build redirect map + +Map each WP URL to the new AM URL. Write to `.planning/data/redirect-map.txt`: + +Format: `OLD_PATH -> NEW_PATH` + +Common mapping patterns for yoga sites: + +| Old WP URL | New AM URL | Action | +|-----------|-----------|--------| +| `/` | `/` | Same | +| `/about/` | `/about/` | Same | +| `/classes/` | `/classes/` | Same | +| `/yoga-class-name/` | `/classes/yoga-class-name.html` | Restructure | +| `/private-yoga-sessions/` | `/private-sessions/` | Rename | +| `/contact-us/` | `/contact/` | Simplify | +| `/?page_id=42` | `/about/` | WP ID → slug | +| `/blog/post-title/` | `/blog/post-title.html` | Flatten | +| `/events/event-name/` | `/classes/` | Consolidate into schedule | + +Redirects go into `infra/nginx.conf`: + +```nginx +# Exact-match redirects +location = /contact-us/ { return 301 /contact/; } +location = /private-yoga-sessions/ { return 301 /private-sessions/; } + +# WP page ID redirects +location = / { + if ($arg_page_id = "42") { return 301 /about/; } + if ($arg_p) { return 301 /blog/; } +} + +# WP upload URLs → AM asset paths (catch-all) +location ^~ /wp-content/uploads/ { + return 301 /assets/images/$uri; +} + +# Block all WP URLs +location ~ ^/wp-(admin|login|json|cron|includes|content/plugins|content/themes) { + return 410; +} +``` + +## Step 3 — Rank Math SEO extraction + +Rank Math stores titles and descriptions in `wp_postmeta`. +`analyze_db.py` already extracts these into `pages.json` as `seo_title` and `seo_description`. + +For each page, the priority order for SEO fields: +1. `seo_title` from Rank Math (if not empty and not a template like `%title% - %sitename%`) +2. `post_title` with AM format appended: `{Title} | VibrantYou Yoga` +3. Never leave title as the raw WP default + +Rank Math title templates use `%` tokens — strip them and rebuild: +```python +import re + +def clean_rm_title(rm_title: str, post_title: str, site_name: str) -> str: + if not rm_title or "%" in rm_title: + return f"{post_title} | {site_name}" + return rm_title + +def clean_rm_desc(rm_desc: str) -> str: + # Strip %token% placeholders + return re.sub(r"%[a-z_]+%", "", rm_desc).strip(" -|") +``` + +## Step 4 — Per-page SEO checklist + +For every page in `pages.json`, fill in this record before writing HTML: + +```json +{ + "slug": "about", + "new_path": "/about/", + "canonical": "https://vibrantyou.yoga/about/", + "title": "About VibrantYou Yoga | Mindful Movement in [City], [State]", + "description": "Meet the instructors and story behind VibrantYou Yoga. [150-160 chars, include city]", + "keywords": "yoga studio [city], yoga instructor, mindful movement", + "og_image": "/assets/images/about-studio.webp", + "schema_type": "AboutPage", + "h1": "Our Story" +} +``` + +Write to `.planning/data/seo-map.json`. The HTML build reads this file to +stamp `` tags. + +## Step 5 — Schema.org per page type + +| Page | Schema type | Required fields | +|------|------------|----------------| +| Home | `LocalBusiness` | name, url, telephone, address, areaServed, openingHours | +| About | `AboutPage` + `Organization` | name, description, founders | +| Classes index | `ItemList` of `Course` | name, url, description per class | +| Class detail | `Course` | name, description, provider, educationalLevel | +| Contact | `ContactPage` | name, url, telephone, email, address | +| Blog post | `Article` | headline, datePublished, author, image | +| 404 | none | — | + +LocalBusiness schema for vibrantyou.yoga (seed from `site-info.json`): +```json +{ + "@context": "https://schema.org", + "@type": ["LocalBusiness", "HealthAndBeautyBusiness"], + "@id": "https://vibrantyou.yoga/#business", + "name": "VibrantYou Yoga", + "url": "https://vibrantyou.yoga", + "telephone": "", + "priceRange": "$$", + "servesCuisine": null, + "currenciesAccepted": "USD", + "paymentAccepted": "Cash, Credit Card", + "address": { + "@type": "PostalAddress", + "streetAddress": "", + "addressLocality": "", + "addressRegion": "", + "postalCode": "", + "addressCountry": "US" + } +} +``` +Mark address fields `DRAFT NEEDED` — do not fabricate. Pull from `wp_options` +(`admin_email`, Events Manager location settings) or ask client. + +## Step 6 — Pre-launch SEO audit commands + +Run these before declaring the build complete: + +```bash +SITE=src + +# Every page has a +find $SITE -name "*.html" | xargs grep -L '<title>' | grep -v "_template" + +# Every page has meta description +find $SITE -name "*.html" | xargs grep -L 'name="description"' | grep -v "_template" + +# Every page has canonical +find $SITE -name "*.html" | xargs grep -L 'rel="canonical"' | grep -v "_template" + +# Every page has JSON-LD +find $SITE -name "*.html" | xargs grep -L 'application/ld+json' | grep -v "_template" + +# No WP URLs leaked into HTML +grep -r "wp-content\|wp-admin\|wordpress\|?p=\|?page_id=" $SITE --include="*.html" + +# No unreplaced template placeholders +grep -r "{{" $SITE --include="*.html" + +# No Divi class residue +grep -r "et_pb_\|divi-builder" $SITE --include="*.html" +``` + +All six commands must return zero results before launch. + +## Next step + +Proceed to `08-run-order.md` for the complete execution sequence, +then `02-wordpress-to-html-migration.md` Phase 7 for DNS cutover. diff --git a/wp-divi-pipeline-to-am-stack/08-run-order.md b/wp-divi-pipeline-to-am-stack/08-run-order.md new file mode 100644 index 0000000..20a8594 --- /dev/null +++ b/wp-divi-pipeline-to-am-stack/08-run-order.md @@ -0,0 +1,230 @@ +# 08 — Run Order (DEPRECATED) + +> **Superseded by `10-agent-breadcrumbs.md`.** +> This file described the WP → static HTML (Stack B) run order. +> The pipeline now targets Stack A (PHP router + SQLite). +> Use `10-agent-breadcrumbs.md` for the current ordered execution checklist. + +--- + +Step-by-step execution sequence for a complete .wpress → AM HTML migration. +Run each command, verify the output, then proceed to the next. + +## Prerequisites + +```bash +# Python 3.8+ required +python3 --version + +# cwebp for image conversion (optional — Python fallback available) +which cwebp || echo "cwebp not installed — will use Python Pillow fallback" + +# Set project domain variable (use throughout) +export DOMAIN="vibrantyou.yoga" +export PROJECT="/home/sirdrez/arisingmedia-websites/$DOMAIN" +export SOPS="/home/sirdrez/arisingmedia-websites/.am-webdesign-sops" +export WPRESS=$(ls $PROJECT/.planning/*.wpress | head -1) + +echo "Domain: $DOMAIN" +echo "Project: $PROJECT" +echo "Archive: $WPRESS" +``` + +--- + +## Phase 0 — Setup + +```bash +# Create directory structure +mkdir -p $PROJECT/{src/{about,services,contact,blog,classes,components,assets/{css,js,images,svg,fonts}},build,infra,api,.planning/{data/{content},scripts,wpress-extract}} + +# Verify archive +ls -lh $WPRESS +file $WPRESS +``` + +--- + +## Phase 1 — Extract archive + +```bash +python3 $SOPS/wp-divi-pipeline/scripts/extract_wpress.py \ + "$WPRESS" \ + "$PROJECT/.planning/wpress-extract/" + +# Verify +ls $PROJECT/.planning/wpress-extract/ +cat $PROJECT/.planning/wpress-extract/package.json | python3 -m json.tool | head -20 +ls -lh $PROJECT/.planning/wpress-extract/database.sql +``` + +Expected output: `DONE: N files | X MB` + +--- + +## Phase 2 — Database analysis + +```bash +python3 $SOPS/wp-divi-pipeline/scripts/analyze_db.py \ + "$PROJECT/.planning/wpress-extract/" \ + "$PROJECT/.planning/data/" + +# Verify +cat $PROJECT/.planning/data/site-info.json +echo "Pages: $(python3 -c "import json; print(len(json.load(open('$PROJECT/.planning/data/pages.json'))))")" +cat $PROJECT/.planning/data/design-system.json +``` + +Expected output: `pages.json (N pages/posts)` +If pages = 0, check the SQL prefix detection in the script output. + +--- + +## Phase 3 — Content extraction + +### Divi 5 (most common — check design-system.json divi_version first) + +```bash +python3 $SOPS/wp-divi-pipeline/scripts/extract_divi5.py \ + "$PROJECT/.planning/data/pages.json" \ + "$PROJECT/.planning/data/content/" + +# Verify +ls $PROJECT/.planning/data/content/ +cat $PROJECT/.planning/data/content/home.json | python3 -m json.tool | head -40 +``` + +--- + +## Phase 4 — Design system + +Read `$PROJECT/.planning/data/design-system.json` and seed `main.css`: + +```bash +cat $PROJECT/.planning/data/design-system.json +``` + +Manually translate to CSS custom properties per `04-design-system-extraction.md`. +Write to: `$PROJECT/src/assets/css/main.css` + +Key values for vibrantyou.yoga: +- Primary: #1a8a7a Dark: #0f5f53 +- Body font: DM Sans Heading font: DM Serif Display + +--- + +## Phase 5 — Media migration + +```bash +# Catalog originals (skip WP-generated size variants) +find $PROJECT/.planning/wpress-extract/uploads -type f \ + \( -name "*.jpg" -o -name "*.jpeg" -o -name "*.png" -o -name "*.webp" \) | \ + grep -v -E "\-[0-9]+x[0-9]+\.(jpg|jpeg|png|webp)$" | \ + sort > $PROJECT/.planning/data/media-originals.txt + +echo "Original images: $(wc -l < $PROJECT/.planning/data/media-originals.txt)" + +# Copy to src/assets/images/ +while IFS= read -r src; do + cp "$src" "$PROJECT/src/assets/images/$(basename $src)" +done < $PROJECT/.planning/data/media-originals.txt + +# Convert to WebP (cwebp path) +cd $PROJECT/src/assets/images/ +for img in *.jpg *.jpeg *.png; do + [ -f "$img" ] || continue + base="${img%.*}" + cwebp -q 82 "$img" -o "${base}.webp" 2>/dev/null && rm "$img" +done +echo "WebP count: $(ls *.webp 2>/dev/null | wc -l)" +cd $PROJECT +``` + +--- + +## Phase 6 — Build HTML + +Per `05-content-migration.md`, build pages in this order: + +```bash +# 1. Write src/assets/css/main.css (design tokens — manual) +# 2. Write src/assets/css/components.css (manual) +# 3. Write src/components/header.html (manual) +# 4. Write src/components/footer.html (manual) +# 5. Write src/assets/js/components.js (fetch + inject) +# 6. Write src/assets/js/main.js (scroll, animations) +# 7. Write src/index.html (home page — first, establishes design) +# 8. Write remaining pages + +# After build, verify zero unreplaced placeholders +grep -r "{{" $PROJECT/src --include="*.html" && echo "FAIL: placeholders found" || echo "OK" + +# Verify no Divi residue +grep -rn "et_pb_\|wp:divi\|\[et_pb" $PROJECT/src --include="*.html" && echo "FAIL: Divi residue" || echo "OK" +``` + +--- + +## Phase 7 — SEO audit + +```bash +cd $PROJECT/src + +# All pages have title +find . -name "*.html" | grep -v "_template" | xargs grep -L '<title>' | head + +# All pages have canonical +find . -name "*.html" | grep -v "_template" | xargs grep -L 'rel="canonical"' | head + +# All pages have JSON-LD +find . -name "*.html" | grep -v "_template" | xargs grep -L 'ld+json' | head + +cd $PROJECT +``` + +All commands must return empty output. + +--- + +## Phase 8 — Infra (Docker) + +```bash +# Copy infra from reference project +cp /home/sirdrez/arisingmedia-websites/vibrantyoucoaching.com/Dockerfile $PROJECT/ +cp /home/sirdrez/arisingmedia-websites/vibrantyoucoaching.com/docker-compose.yml $PROJECT/ +cp -r /home/sirdrez/arisingmedia-websites/vibrantyoucoaching.com/infra/ $PROJECT/infra/ + +# Update nginx.conf: set server_name to $DOMAIN, add redirects from 07-seo-preservation.md +# Update docker-compose.yml: set container_name and port + +# Test build +docker compose -f $PROJECT/docker-compose.yml build 2>&1 | tail -5 +docker compose -f $PROJECT/docker-compose.yml up -d +curl -I http://localhost:PORT/ 2>&1 | head -5 +``` + +--- + +## Phase 9 — Protection check + +```bash +# Run after deploy +bash $SOPS/tools/verify-protection.sh https://$DOMAIN + +# Must return exit 0 with no FAIL lines +``` + +--- + +## Checklist summary + +- [ ] Phase 0: Directories created +- [ ] Phase 1: .wpress extracted, database.sql present +- [ ] Phase 2: pages.json > 0 entries, design-system.json has colors + fonts +- [ ] Phase 3: content/ dir has one JSON per page +- [ ] Phase 4: main.css written with full :root{} token block +- [ ] Phase 5: WebP images in src/assets/images/ +- [ ] Phase 6: All HTML pages built, zero {{ placeholders, zero Divi residue +- [ ] Phase 7: All SEO audit commands return empty +- [ ] Phase 8: Docker container up, curl returns 200 +- [ ] Phase 9: verify-protection.sh exits 0 diff --git a/wp-divi-pipeline-to-am-stack/09-stack-a-output.md b/wp-divi-pipeline-to-am-stack/09-stack-a-output.md new file mode 100644 index 0000000..f8d3631 --- /dev/null +++ b/wp-divi-pipeline-to-am-stack/09-stack-a-output.md @@ -0,0 +1,370 @@ +# 09 — Stack A Output Spec (SQLite Schema + sections_json) + +## SQLite databases produced by seed_databases.py + +### pages.sqlite + +```sql +CREATE TABLE pages ( + id INTEGER PRIMARY KEY, + slug TEXT UNIQUE NOT NULL, + template TEXT NOT NULL, -- home | static | classes | schedule | glossary | blog + title TEXT NOT NULL, + meta_description TEXT, + canonical_url TEXT, + og_image TEXT, + schema_json TEXT, + hero_eyebrow TEXT, + hero_h1 TEXT, + hero_lead TEXT, + sections_json TEXT, -- JSON array of section objects + updated_at TEXT +); +``` + +### nav.sqlite + +```sql +CREATE TABLE nav_items ( + id INTEGER PRIMARY KEY, + label TEXT NOT NULL, + href TEXT NOT NULL, + display_order INTEGER DEFAULT 0, + is_cta INTEGER DEFAULT 0 -- 1 = render as button +); +``` + +### blog.sqlite + +```sql +CREATE TABLE posts ( + id INTEGER PRIMARY KEY, + slug TEXT UNIQUE NOT NULL, + title TEXT NOT NULL, + excerpt TEXT, + body_html TEXT, + author TEXT DEFAULT 'Admin', + published_at TEXT, + og_image TEXT, + tags TEXT +); +``` + +### testimonials.sqlite + +```sql +CREATE TABLE testimonials ( + id INTEGER PRIMARY KEY, + quote TEXT NOT NULL, + author_name TEXT NOT NULL, + author_role TEXT, + is_featured INTEGER DEFAULT 0, + display_order INTEGER DEFAULT 0 +); +``` + +### glossary.sqlite (if site has a glossary) + +```sql +CREATE TABLE terms ( + id INTEGER PRIMARY KEY, + slug TEXT UNIQUE NOT NULL, + term TEXT NOT NULL, + pronunciation TEXT, + definition TEXT NOT NULL, + category TEXT NOT NULL, + level TEXT NOT NULL, + display_order INTEGER DEFAULT 0 +); +``` + +### faq.sqlite (if site has FAQs) + +```sql +CREATE TABLE faqs ( + id INTEGER PRIMARY KEY, + question TEXT NOT NULL, + answer TEXT NOT NULL, + category TEXT NOT NULL, + display_order INTEGER DEFAULT 0 +); +``` + +## sections_json section types + +Each page row's sections_json is a JSON array. Each element is a typed object: + +### text_split + +Two-column: text on one side, image on the other. CTAs optional. + +```json +{ + "type": "text_split", + "eyebrow": "", + "h2": "", + "body": "", + "img": "/assets/images/x.webp", + "img_alt": "", + "cta_label": "", + "cta_href": "", + "reverse": false +} +``` + +### feature_cards + +Grid of 3-4 cards, each with icon + title + body. + +```json +{ + "type": "feature_cards", + "eyebrow": "", + "h2": "", + "lead": "", + "cards": [ + {"icon": "", "title": "", "body": ""} + ] +} +``` + +### accordion + +Collapsible question/answer pairs. + +```json +{ + "type": "accordion", + "eyebrow": "", + "h2": "", + "items": [ + {"q": "", "a": ""} + ] +} +``` + +### cta_band + +Full-width call-to-action with headline + button. + +```json +{ + "type": "cta_band", + "eyebrow": "", + "h2": "", + "lead": "", + "btn_label": "", + "btn_href": "", + "variant": "forest" +} +``` + +### text_block + +Simple text heading + body. + +```json +{ + "type": "text_block", + "eyebrow": "", + "h2": "", + "body": "" +} +``` + +### stats_strip + +Grid of stat + label pairs. + +```json +{ + "type": "stats_strip", + "stats": [ + {"value": "", "label": ""} + ] +} +``` + +### topic_pills + +Row of clickable topic/tag items. + +```json +{ + "type": "topic_pills", + "eyebrow": "", + "h2": "", + "items": [ + {"label": "", "href": ""} + ] +} +``` + +### form_contact + +Embedded contact form. + +```json +{ + "type": "form_contact", + "h2": "", + "lead": "" +} +``` + +### booking_options + +Pricing table or service options grid. + +```json +{ + "type": "booking_options", + "eyebrow": "", + "h2": "", + "options": [ + {"name": "", "price": "", "features": [], "cta_label": "", "cta_href": ""} + ] +} +``` + +## Divi module → section type mapping + +| Divi Module | AM Section Type | Notes | +|---|---|---| +| et_pb_blurb | feature_cards item | Extract icon, title, body | +| et_pb_toggle | accordion item | Extract q/a pairs | +| et_pb_cta | cta_band | Extract headline, button text, href | +| et_pb_pricing_table | booking_options | Extract plan names, prices, features | +| et_pb_testimonial | testimonials.sqlite row | Extract quote, author, role | +| et_pb_text | text_block | Extract body copy | +| et_pb_code | text_block (sanitized) | Extract HTML, remove script tags | +| et_pb_number_counter | stats_strip item | Extract number, label | +| et_pb_button | cta_band (minimal) | Extract button text, href | +| et_pb_menu / header | nav.sqlite rows | Extract label, URL, menu order | + +## seed_databases.py structure + +Every migration generates a seed_databases.py at `build/seed_databases.py`. + +Template structure: + +```python +import sqlite3 +import json +from pathlib import Path + +pages_path = Path('src/api/data/pages.sqlite') +nav_path = Path('src/api/data/nav.sqlite') +blog_path = Path('src/api/data/blog.sqlite') +testimonials_path = Path('src/api/data/testimonials.sqlite') + +def seed_pages(conn): + """INSERT all pages with sections_json and hero data.""" + cursor = conn.cursor() + cursor.execute(''' + CREATE TABLE IF NOT EXISTS pages ( + id INTEGER PRIMARY KEY, + slug TEXT UNIQUE NOT NULL, + template TEXT NOT NULL, + title TEXT NOT NULL, + meta_description TEXT, + canonical_url TEXT, + og_image TEXT, + schema_json TEXT, + hero_eyebrow TEXT, + hero_h1 TEXT, + hero_lead TEXT, + sections_json TEXT, + updated_at TEXT + ) + ''') + + pages = [ + ('home', 'home', 'Home', 'Home meta', '/home', '', '{}', + '', 'Welcome', 'Lead text', json.dumps([...])), + # ... more rows + ] + for page in pages: + cursor.execute( + 'INSERT INTO pages (slug, template, title, meta_description, canonical_url, og_image, schema_json, hero_eyebrow, hero_h1, hero_lead, sections_json, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime("now"))', + page + ) + +def seed_nav(conn): + """INSERT navigation items from nav.json.""" + cursor = conn.cursor() + cursor.execute(''' + CREATE TABLE IF NOT EXISTS nav_items ( + id INTEGER PRIMARY KEY, + label TEXT NOT NULL, + href TEXT NOT NULL, + display_order INTEGER DEFAULT 0, + is_cta INTEGER DEFAULT 0 + ) + ''') + + items = [ + ('Home', '/', 0, 0), + ('About', '/about', 1, 0), + ('Contact', '/contact', 2, 1), + # ... more rows + ] + for item in items: + cursor.execute( + 'INSERT INTO nav_items (label, href, display_order, is_cta) VALUES (?, ?, ?, ?)', + item + ) + +def seed_blog(conn): + """INSERT blog posts if site has a blog.""" + cursor = conn.cursor() + cursor.execute(''' + CREATE TABLE IF NOT EXISTS posts ( + id INTEGER PRIMARY KEY, + slug TEXT UNIQUE NOT NULL, + title TEXT NOT NULL, + excerpt TEXT, + body_html TEXT, + author TEXT DEFAULT 'Admin', + published_at TEXT, + og_image TEXT, + tags TEXT + ) + ''') + # ... INSERT rows + +def seed_testimonials(conn): + """INSERT testimonials if present.""" + # ... CREATE TABLE + INSERT rows + +if __name__ == '__main__': + for db_path, seeder_fn in [ + (pages_path, seed_pages), + (nav_path, seed_nav), + (blog_path, seed_blog), + (testimonials_path, seed_testimonials), + ]: + if db_path.exists(): + db_path.unlink() # clear if re-running + conn = sqlite3.connect(db_path) + seeder_fn(conn) + conn.commit() + conn.close() + print(f"seeded: {db_path.name}") + + print("All databases seeded successfully.") +``` + +## Content validation checklist + +After staging seed_databases.py and before running it: + +- [ ] No raw Divi shortcode residue: `[et_pb_`, `[vc_`, etc. +- [ ] No em-dashes (—): replace with commas, periods, or spaces +- [ ] No "Netherlands" or other location-specific copy (unless intentional) +- [ ] hero_h1 is 5-10 words (brand voice, not generic) +- [ ] Each section type matches the spec above (no custom types) +- [ ] All images are `/assets/images/{name}.webp` (not absolute URLs) +- [ ] All CTAs point to correct slugs (`/about`, `/contact`, etc.) +- [ ] Nav items include at least 3 menu links +- [ ] At least one nav item has `is_cta=1` (usually Contact or Book) diff --git a/wp-divi-pipeline-to-am-stack/10-agent-breadcrumbs.md b/wp-divi-pipeline-to-am-stack/10-agent-breadcrumbs.md new file mode 100644 index 0000000..1e577d3 --- /dev/null +++ b/wp-divi-pipeline-to-am-stack/10-agent-breadcrumbs.md @@ -0,0 +1,249 @@ +# 10 — Agent Execution Breadcrumbs + +Step-by-step ordered checklist for an agent executing a .wpress migration to Stack A. +Each step has: input, command, expected output, verification. Complete each before next. + +## Pre-flight + +- [ ] .wpress file confirmed at `$PROJECT/.planning/*.wpress` +- [ ] python3 --version >= 3.8 +- [ ] docker compose version confirmed +- [ ] DOMAIN and PROJECT env vars set + +## Step 1 — Extract archive + +**INPUT:** `$WPRESS` (path to .wpress file) + +**CMD:** +```bash +python3 $SOPS/wp-divi-pipeline-to-am-stack/scripts/extract_wpress.py "$WPRESS" "$PROJECT/.planning/wpress-extract/" +``` + +**VERIFY:** +```bash +ls $PROJECT/.planning/wpress-extract/ +``` + +Expected: `database.sql` and `wp-content/` present + +**BLOCK:** If database.sql missing, .wpress format differs — check extract_wpress.py logs. + +--- + +## Step 2 — Analyze database + +**INPUT:** `$PROJECT/.planning/wpress-extract/database.sql` + +**CMD:** +```bash +python3 $SOPS/wp-divi-pipeline-to-am-stack/scripts/analyze_db.py "$PROJECT/.planning/wpress-extract/" "$PROJECT/.planning/data/" +``` + +**VERIFY:** +```bash +cat $PROJECT/.planning/data/pages.json | python3 -m json.tool | head -20 +cat $PROJECT/.planning/data/site-info.json +``` + +Expected: page objects with slug + title visible; divi_version: 4 or 5 + +**BLOCK:** If pages.json empty, check table prefix detection in analyze_db.py output. + +--- + +## Step 3 — Extract nav menus + +**INPUT:** `$PROJECT/.planning/wpress-extract/database.sql` + +**CMD:** +```bash +python3 $SOPS/wp-divi-pipeline-to-am-stack/scripts/extract_nav.py "$PROJECT/.planning/wpress-extract/" "$PROJECT/.planning/data/" +``` + +**VERIFY:** +```bash +cat $PROJECT/.planning/data/nav.json | python3 -m json.tool +``` + +Expected: array of `{label, href, display_order, is_cta}` objects. At least 3 items. + +**NOTE:** `is_cta=1` for "Book", "Get Started", "Contact", "Sign Up" type items. + +--- + +## Step 4 — Extract page content + +**INPUT:** `$PROJECT/.planning/data/pages.json` + `wpress-extract/` + +**CMD:** (choose based on Divi version from Step 2) + +Divi 5: +```bash +python3 $SOPS/wp-divi-pipeline-to-am-stack/scripts/extract_divi5.py "$PROJECT/.planning/data/pages.json" "$PROJECT/.planning/data/content/" +``` + +Divi 4: +```bash +python3 $SOPS/wp-divi-pipeline-to-am-stack/scripts/extract_divi4.py "$PROJECT/.planning/data/pages.json" "$PROJECT/.planning/data/content/" +``` + +**VERIFY:** +```bash +ls $PROJECT/.planning/data/content/ +cat $PROJECT/.planning/data/content/home.json | python3 -m json.tool | head -40 +``` + +Expected: one .json file per page (home.json, about.json, etc.); sections array with type fields visible. + +--- + +## Step 5 — Extract media + +**INPUT:** `$PROJECT/.planning/wpress-extract/wp-content/uploads/` + +**CMD:** +```bash +python3 $SOPS/wp-divi-pipeline-to-am-stack/scripts/extract_media.py "$PROJECT/.planning/wpress-extract/" "$PROJECT/.planning/data/" "$PROJECT/assets/images/" +``` + +**VERIFY:** +```bash +ls $PROJECT/assets/images/ | head -10 +cat $PROJECT/.planning/data/media-manifest.json | python3 -m json.tool | head -20 +``` + +Expected: .webp files present; media-manifest.json shows `original_url → /assets/images/x.webp` mapping. + +--- + +## Step 6 — Stage seed_databases.py skeleton + +**INPUT:** All .json files in `$PROJECT/.planning/data/content/` + `nav.json` + `media-manifest.json` + +**CMD:** +```bash +python3 $SOPS/wp-divi-pipeline-to-am-stack/scripts/stage_seed.py "$PROJECT/.planning/data/" "$PROJECT/build/seed_databases.py" --domain "$DOMAIN" +``` + +**VERIFY:** +```bash +python3 -c "import ast; ast.parse(open('$PROJECT/build/seed_databases.py').read()); print('syntax OK')" +grep "def seed_pages" $PROJECT/build/seed_databases.py +``` + +Expected: seed_databases.py is valid Python; contains seed_pages, seed_nav functions. + +**NOTE:** Content stubs are in place. Human/agent reviews + fills in prose before running. + +--- + +## Step 7 — Review and fill content + +**MANUAL:** Open `$PROJECT/build/seed_databases.py` + +For each page's `sections_json`: +- [ ] Confirm `hero_h1` and `hero_lead` match the brand (not raw Divi copy-paste) +- [ ] Confirm each section has correct type (see 09-stack-a-output.md mapping) +- [ ] Replace any em-dashes (—) with commas or periods +- [ ] Replace any Divi shortcode residue (`[et_pb_`, `vc_`, etc.) +- [ ] Ensure no "Netherlands" or location-specific copy if site is global +- [ ] Confirm nav items in `seed_nav()` match final site IA +- [ ] Verify all image paths are `/assets/images/{name}.webp` +- [ ] Verify all CTAs point to correct slugs (`/about`, `/contact`, etc.) + +--- + +## Step 8 — Run seed_databases.py + +**CMD:** +```bash +cd $PROJECT && python3 build/seed_databases.py +``` + +**VERIFY:** +```bash +ls -lh src/api/data/ +``` + +Expected: Output line shows counts > 0: `seeded: pages=N nav=N blog=N ...`. Database files exist. + +**BLOCK:** Any count=0 means that seeder function has an error — fix before continuing. + +--- + +## Step 9 — Scaffold PHP templates + +**CMD:** Copy reference templates from vibrantyou.yoga as starting point: + +```bash +VYOGA="/home/sirdrez/arisingmedia-websites/vibrantyou.yoga" +cp $VYOGA/src/api/router.php $PROJECT/src/api/router.php +cp $VYOGA/src/api/contact.php $PROJECT/src/api/contact.php +cp $VYOGA/src/api/templates/static.php $PROJECT/src/api/templates/static.php +cp $VYOGA/src/api/templates/home.php $PROJECT/src/api/templates/home.php +cp $VYOGA/src/api/components/_header.php $PROJECT/src/api/components/_header.php +cp $VYOGA/src/api/components/_footer.php $PROJECT/src/api/components/_footer.php +cp -r $VYOGA/assets/css $PROJECT/assets/ +cp -r $VYOGA/assets/js $PROJECT/assets/ +cp $VYOGA/Dockerfile $PROJECT/ +cp $VYOGA/docker-compose.yml $PROJECT/ +cp -r $VYOGA/infra $PROJECT/ +``` + +**VERIFY:** +```bash +php -l $PROJECT/src/api/router.php +``` + +Expected: `No syntax errors detected` + +**NOTE:** Update brand name, colors, and any site-specific logic in templates. + +**NOTE:** `_header.php` reads from nav.sqlite — no hardcoded nav needed. + +--- + +## Step 10 — Build and test + +**CMD:** +```bash +cd $PROJECT && docker compose build --no-cache && docker compose up -d +``` + +**VERIFY:** +```bash +sleep 5 +curl -I http://localhost:8000/ +curl -s http://localhost:8000/ | grep -i "title\|h1" | head -3 +``` + +Expected: HTTP 200; site name visible in page. + +--- + +## Step 11 — Protection + SEO check + +**CMD:** +```bash +bash /home/sirdrez/arisingmedia-websites/.am-webdesign-sops/tools/verify-protection.sh http://localhost:8000 +``` + +**VERIFY:** Exit 0, no FAIL lines + +--- + +## Step 12 — Lighthouse + cleanup + +**MANUAL:** +- Open Firefox: `firefox http://localhost:8000/` +- Run Lighthouse (DevTools > Lighthouse) + +**TARGET:** +- Performance >= 90 +- SEO >= 95 +- Accessibility >= 90 + +**CLEANUP:** +```bash +cd $PROJECT && docker compose down +``` diff --git a/wp-divi-pipeline-to-am-stack/README.md b/wp-divi-pipeline-to-am-stack/README.md new file mode 100644 index 0000000..876781c --- /dev/null +++ b/wp-divi-pipeline-to-am-stack/README.md @@ -0,0 +1,81 @@ +# WP + Divi to AM Stack A Pipeline — SOP Index + +End-to-end playbook for converting any WordPress / Divi site backup (.wpress) +into an Arising Media Stack A deployment: PHP router + SQLite + vanilla JS/CSS. + +## Quick start (CLI launcher) + +```bash +python3 scripts/migrate.py --wpress /path/to/backup.wpress --domain example.com +``` + +Runs phases 0-6 automatically (extract, analyze, nav, content, media, stage seed). +Prints agent breadcrumbs for phases 7-11. See `10-agent-breadcrumbs.md` for the +complete ordered execution checklist. + +## SOPs in this folder + +| File | Phase | Description | +|------|-------|-------------| +| `00-overview.md` | — | Pipeline overview, philosophy, what to extract vs not replicate | +| `01-wpress-extraction.md` | 1 | .wpress binary format, extraction script, verification | +| `02-database-analysis.md` | 2 | MySQL dump parsing, page inventory, Divi version detection | +| `03-divi-content-extraction.md` | 3 | Divi 4 shortcodes vs Divi 5 blocks, extraction scripts | +| `04-design-system-extraction.md` | 4 | Colors, fonts, spacing → tokens.css | +| `05-content-migration.md` | 5-6 | Section remapping, content staging, seed_databases.py | +| `06-media-assets.md` | 5 | Upload migration, WebP conversion, media manifest | +| `07-seo-preservation.md` | 7 | Redirect map, Rank Math extraction, schema.org | +| `08-run-order.md` | — | DEPRECATED — superseded by `10-agent-breadcrumbs.md` | +| `09-stack-a-output.md` | — | SQLite schemas, sections_json spec, Divi→AM module mapping | +| `10-agent-breadcrumbs.md` | 0-11 | Ordered agent execution checklist (.wpress → live Docker) | + +## Scripts in scripts/ + +| Script | Purpose | +|--------|---------| +| `migrate.py` | CLI launcher — runs phases 0-6, prints breadcrumbs for 7-11 | +| `run_pipeline.sh` | Legacy shell wrapper (pre-migrate.py) | +| `extract_wpress.py` | Unpack .wpress binary archive | +| `analyze_db.py` | Parse SQL dump → pages.json + design-system.json | +| `extract_divi5.py` | Parse Divi 5 blocks → per-page content JSON | +| `extract_nav.py` | Extract WordPress nav menus → nav.json | +| `stage_seed.py` | Map extracted JSON → seed_databases.py skeleton (Phase 6) | + +## Key facts about .wpress archives + +- Format: Custom sequential binary (NOT zip/tar) — 4377-byte headers +- Table prefix in SQL dump: `SERVMASK_PREFIX_` (placeholder, NOT `wp_`) +- Directory layout: flat — `uploads/`, `themes/`, `plugins/` at archive root (no `wp-content/` wrapper) +- Divi 5 stores theme settings in `et_divi` option as PHP-serialized array + +## vibrantyou.yoga — extracted data reference + +Site: Vibrant You Yoga (instructor: Meghan) +Domain: https://vibrantyou.yoga +Divi version: 5.0.3 +WP version: 6.9.4 + +Design system: +- Primary: #1a8a7a Dark: #0f5f53 Secondary: #2ea3f2 +- Body: #5a6b68 Headings: #2d2d2d +- Body font: DM Sans 17px / 1.6 lh +- Heading font: DM Serif Display 600 / 36px / 1.2 lh + +Pages to migrate (22 published): +- home, about, classes, schedule, instructors, contact, blog, faq +- book (private sessions), online-yoga, donate +- Drop: video-category, video-tag, search-videos, user-videos, player-embed, + categories, tags, my-bookings (all plugin-generated archive pages) + +Plugins requiring AM replacements: +- Gravity Forms + Stripe → AM HTML form + Python API + Resend +- Events Manager → static schedule table in /schedule/ +- All-in-One Video Gallery → embed YouTube/Vimeo directly or drop + +## Related SOPs + +- `../01-project-structure.md` — AM deployment directory layout +- `../02-wordpress-to-html-migration.md` — Original 8-phase WP migration playbook +- `../03-build-pipeline.md` — JSON + template stamping for repeated pages +- `../06-seo-meta.md` — Full `<head>` requirements, schema.org per page type +- `../tools/verify-protection.sh` — Post-deploy security audit diff --git a/wp-divi-pipeline-to-am-stack/scripts/__pycache__/stage_seed.cpython-313.pyc b/wp-divi-pipeline-to-am-stack/scripts/__pycache__/stage_seed.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5ae9b2f2d95689057b19512b6cb170f9d8d295ba GIT binary patch literal 19590 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 literal 0 HcmV?d00001 diff --git a/wp-divi-pipeline-to-am-stack/scripts/analyze_db.py b/wp-divi-pipeline-to-am-stack/scripts/analyze_db.py new file mode 100644 index 0000000..3ba303c --- /dev/null +++ b/wp-divi-pipeline-to-am-stack/scripts/analyze_db.py @@ -0,0 +1,368 @@ +#!/usr/bin/env python3 +"""Analyze WordPress MySQL dump from a .wpress extract. + +Parses database.sql and outputs: + - pages.json : all published pages with title, slug, content, SEO meta + - design-system.json : colors, fonts from wp_options (Divi theme settings) + - site-info.json : domain, WP version, detected Divi version, plugin list + +Usage: + python3 analyze_db.py <extract_dir> <output_data_dir> + + extract_dir : path to wpress-extract/ (contains database.sql) + output_data_dir : where to write JSON output files (e.g. .planning/data/) +""" +from __future__ import annotations + +import json +import os +import re +import sys +from pathlib import Path +from typing import Any + + +# --------------------------------------------------------------------------- +# SQL parsing helpers +# --------------------------------------------------------------------------- + +def _unescape_sql(s: str) -> str: + """Undo MySQL string escaping.""" + return (s + .replace("\\'", "'") + .replace('\\"', '"') + .replace("\\\\", "\\") + .replace("\\n", "\n") + .replace("\\r", "\r") + .replace("\\t", "\t") + .replace("\\0", "\0")) + + +def _parse_values_block(sql_block: str) -> list[list[str]]: + """Extract rows from a multi-row INSERT VALUES block. + + Handles commas inside quoted strings via a simple state machine. + Returns list of rows; each row is a list of raw string values. + """ + rows: list[list[str]] = [] + # Find VALUES section + m = re.search(r"VALUES\s*", sql_block, re.IGNORECASE) + if not m: + return rows + rest = sql_block[m.end():] + + i = 0 + n = len(rest) + while i < n: + # Skip to '(' + while i < n and rest[i] != '(': + i += 1 + if i >= n: + break + i += 1 # skip '(' + + row: list[str] = [] + field = [] + in_quote = False + quote_char = '' + + while i < n: + c = rest[i] + if not in_quote: + if c in ("'", '"'): + in_quote = True + quote_char = c + i += 1 + continue + elif c == ',' : + row.append("".join(field)) + field = [] + i += 1 + continue + elif c == ')': + row.append("".join(field)) + field = [] + rows.append(row) + i += 1 + break + elif c == 'N' and rest[i:i+4] == 'NULL': + field.append('\x00NULL\x00') + i += 4 + continue + else: + field.append(c) + i += 1 + else: + if c == '\\' and i + 1 < n: + field.append(c) + field.append(rest[i + 1]) + i += 2 + continue + elif c == quote_char: + in_quote = False + i += 1 + continue + else: + field.append(c) + i += 1 + + return rows + + +def load_table(sql_text: str, table_name: str) -> list[dict]: + """Return all rows for table_name as list of dicts.""" + # Find column definition + col_re = re.compile( + rf"CREATE TABLE `{re.escape(table_name)}`\s*\((.*?)\)\s*ENGINE", + re.DOTALL | re.IGNORECASE, + ) + m = col_re.search(sql_text) + if not m: + return [] + col_block = m.group(1) + cols = re.findall(r"`([^`]+)`\s+(?:bigint|int|mediumint|smallint|tinyint|varchar|text|mediumtext|longtext|char|datetime|date|float|double|decimal|enum|set|blob|mediumblob|longblob)", col_block, re.IGNORECASE) + + # Find INSERT blocks for this table + insert_re = re.compile( + rf"INSERT INTO `{re.escape(table_name)}`\s+VALUES\s*\(.+?\);", + re.DOTALL | re.IGNORECASE, + ) + rows_out: list[dict] = [] + for block in insert_re.finditer(sql_text): + parsed = _parse_values_block(block.group(0)) + for row in parsed: + d: dict[str, Any] = {} + for idx, col in enumerate(cols): + val = row[idx] if idx < len(row) else "" + if val == "\x00NULL\x00": + d[col] = None + else: + d[col] = _unescape_sql(val) + rows_out.append(d) + return rows_out + + +# --------------------------------------------------------------------------- +# Divi version detection +# --------------------------------------------------------------------------- + +def detect_divi_version(sql_text: str) -> str: + if "wp:divi/" in sql_text: + return "5" + if "[et_pb_section" in sql_text: + return "4" + # Check et_theme_builder version in options + m = re.search(r"'et_theme_builder_api_version','([^']+)'", sql_text) + if m: + return "5" + return "unknown" + + +# --------------------------------------------------------------------------- +# Options extraction +# --------------------------------------------------------------------------- + +def load_options(sql_text: str, prefix: str = "wp_") -> dict[str, str]: + table = f"{prefix}options" + rows = load_table(sql_text, table) + return {r["option_name"]: r["option_value"] for r in rows if r.get("option_name")} + + +def _parse_php_serialized_pairs(raw: str) -> dict[str, str]: + """Extract key/value string pairs from a PHP-serialized array. + + Handles both escaped (SQL-dump) and unescaped forms. + Only returns s->s pairs (string key, string value). + """ + result: dict[str, str] = {} + # SQL dumps escape double-quotes as \\", giving patterns like: + # s:9:\\"body_font\\";s:7:\\"DM Sans\\"; + # Also handle unescaped form: s:9:"body_font";s:7:"DM Sans"; + pat = re.compile( + r's:\d+:\\"([^"\\]+)\\";s:\d+:\\"([^"\\]*)\\"' # SQL-escaped + r'|s:\d+:"([^"]+)";s:\d+:"([^"]*)"', # plain + ) + for m in pat.finditer(raw): + if m.group(1) is not None: + k, v = m.group(1), m.group(2) + else: + k, v = m.group(3), m.group(4) + result[k] = v + return result + + +def extract_design_system(options: dict[str, str]) -> dict: + """Pull Divi theme colors, fonts, and spacing from wp_options.""" + raw = options.get("et_divi", "") or options.get("et_divi_options", "") + + design: dict[str, Any] = {} + + # Parse PHP-serialized et_divi option (Divi 4 + 5 store settings here) + if raw: + pairs = _parse_php_serialized_pairs(raw) + # Map Divi option keys to design-system keys + key_map = { + "accent_color": "primary_color_dark", + "link_color": "primary_color", + "body_font": "body_font", + "heading_font": "heading_font", + "header_font": "heading_font", # Divi 4 alias + "body_font_size": "body_font_size", + "body_line_height": "body_line_height", + "heading_font_weight": "heading_font_weight", + "header_text_size": "heading_font_size", + "header_line_height": "heading_line_height", + "header_color": "heading_color", + "font_color": "body_color", + "secondary_accent_color": "secondary_color", + } + for divi_key, design_key in key_map.items(): + if divi_key in pairs: + design.setdefault(design_key, pairs[divi_key]) + + # Site info + design["site_url"] = options.get("siteurl", "") + design["site_name"] = options.get("blogname", "") + + return design + + +# --------------------------------------------------------------------------- +# Page extraction +# --------------------------------------------------------------------------- + +def extract_pages(sql_text: str, prefix: str = "wp_") -> list[dict]: + """Return all published pages and posts with SEO meta.""" + posts = load_table(sql_text, f"{prefix}posts") + postmeta = load_table(sql_text, f"{prefix}postmeta") + + # Build postmeta lookup: post_id -> {meta_key: meta_value} + meta_map: dict[str, dict[str, str]] = {} + for row in postmeta: + pid = str(row.get("post_id", "")) + meta_map.setdefault(pid, {})[row.get("meta_key", "")] = row.get("meta_value", "") + + pages = [] + for p in posts: + if p.get("post_status") not in ("publish",): + continue + post_type = p.get("post_type", "") + if post_type not in ("page", "post", "event"): + continue + + pid = str(p.get("ID", "")) + meta = meta_map.get(pid, {}) + + # Rank Math SEO fields + rm_title = meta.get("rank_math_title", "") + rm_desc = meta.get("rank_math_description", "") + rm_focus = meta.get("rank_math_focus_keyword", "") + + entry = { + "id": pid, + "post_type": post_type, + "slug": p.get("post_name", ""), + "title": p.get("post_title", ""), + "status": p.get("post_status", ""), + "date": p.get("post_date", "")[:10], + "modified": p.get("post_modified", "")[:10], + "content_raw": p.get("post_content", ""), + "excerpt": p.get("post_excerpt", ""), + "parent_id": p.get("post_parent", "0"), + "menu_order": p.get("menu_order", "0"), + "seo_title": rm_title, + "seo_description": rm_desc, + "seo_keywords": rm_focus, + "acf": {k: v for k, v in meta.items() if not k.startswith("_") and not k.startswith("rank_math") and not k.startswith("et_")}, + } + pages.append(entry) + + pages.sort(key=lambda x: int(x["menu_order"] or 0)) + return pages + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +def main(): + if len(sys.argv) < 3: + print(f"Usage: {sys.argv[0]} <extract_dir> <output_data_dir>") + sys.exit(1) + + extract_dir = Path(sys.argv[1]) + out_dir = Path(sys.argv[2]) + out_dir.mkdir(parents=True, exist_ok=True) + + sql_file = extract_dir / "database.sql" + if not sql_file.exists(): + # Search for it + found = list(extract_dir.rglob("*.sql")) + if not found: + print(f"ERROR: No .sql file found under {extract_dir}") + sys.exit(1) + sql_file = found[0] + print(f"Found SQL at: {sql_file}") + + print(f"Loading {sql_file} ({sql_file.stat().st_size / 1024 / 1024:.1f} MB)...") + sql_text = sql_file.read_text(encoding="utf-8", errors="replace") + + # Detect Divi version + divi_version = detect_divi_version(sql_text) + print(f"Divi version detected: {divi_version}") + + # Load wp_options + pkg = {} + pkg_file = extract_dir / "package.json" + if pkg_file.exists(): + pkg = json.loads(pkg_file.read_text()) + + # AIOIM dumps use SERVMASK_PREFIX_ as a placeholder in the SQL file. + # Detect which prefix the dump actually uses. + if "SERVMASK_PREFIX_" in sql_text: + sql_prefix = "SERVMASK_PREFIX_" + else: + sql_prefix = pkg.get("Database", {}).get("Prefix", "wp_") + runtime_prefix = pkg.get("Database", {}).get("Prefix", "wp_") + print(f"SQL prefix: {sql_prefix!r} (runtime prefix: {runtime_prefix!r})") + + options = load_options(sql_text, sql_prefix) + print(f"Loaded {len(options)} options") + + # Design system + design = extract_design_system(options) + design["divi_version"] = divi_version + design["wp_version"] = pkg.get("WordPress", {}).get("Version", "") + design["plugins"] = pkg.get("Plugins", []) + (out_dir / "design-system.json").write_text(json.dumps(design, indent=2, ensure_ascii=False)) + print(f"Wrote design-system.json ({len(design)} keys)") + + # Pages + pages = extract_pages(sql_text, sql_prefix) + (out_dir / "pages.json").write_text(json.dumps(pages, indent=2, ensure_ascii=False)) + print(f"Wrote pages.json ({len(pages)} pages/posts)") + + # Site info summary + site_info = { + "domain": pkg.get("SiteURL", options.get("siteurl", "")), + "name": options.get("blogname", ""), + "tagline": options.get("blogdescription", ""), + "admin_email": options.get("admin_email", ""), + "wp_version": pkg.get("WordPress", {}).get("Version", ""), + "divi_version": divi_version, + "plugins": pkg.get("Plugins", []), + "prefix": runtime_prefix, + "total_pages": len([p for p in pages if p["post_type"] == "page"]), + "total_posts": len([p for p in pages if p["post_type"] == "post"]), + } + (out_dir / "site-info.json").write_text(json.dumps(site_info, indent=2, ensure_ascii=False)) + print(f"Wrote site-info.json") + + print(f"\nDone. Output in: {out_dir}") + print(f" pages.json : {len(pages)} entries") + print(f" design-system.json: {len(design)} keys") + print(f" site-info.json : done") + + +if __name__ == "__main__": + main() diff --git a/wp-divi-pipeline-to-am-stack/scripts/extract_divi5.py b/wp-divi-pipeline-to-am-stack/scripts/extract_divi5.py new file mode 100644 index 0000000..5369f3f --- /dev/null +++ b/wp-divi-pipeline-to-am-stack/scripts/extract_divi5.py @@ -0,0 +1,271 @@ +#!/usr/bin/env python3 +"""Extract content from Divi 5 block markup in pages.json. + +Reads .planning/data/pages.json (produced by analyze_db.py) and for each page +parses the `content_raw` Divi 5 block structure into a clean per-page JSON +under .planning/data/content/{slug}.json. + +Usage: + python3 extract_divi5.py <pages_json> <output_dir> + + pages_json : path to .planning/data/pages.json + output_dir : directory to write {slug}.json files (created if missing) +""" +from __future__ import annotations + +import json +import re +import sys +from pathlib import Path +from html.parser import HTMLParser + + +# --------------------------------------------------------------------------- +# HTML inner-text extractor +# --------------------------------------------------------------------------- + +class _TextExtractor(HTMLParser): + def __init__(self): + super().__init__() + self.parts: list[str] = [] + + def handle_data(self, data: str): + self.parts.append(data) + + def get_text(self) -> str: + return " ".join(self.parts).strip() + + +def _text(html: str) -> str: + p = _TextExtractor() + p.feed(html) + return p.get_text() + + +# --------------------------------------------------------------------------- +# Divi block parsing +# --------------------------------------------------------------------------- + +# Matches opening block comment: <!-- wp:divi/MODULE {JSON} --> +_BLOCK_OPEN = re.compile(r"<!--\s*wp:(divi/[a-z0-9_-]+)\s*(.*?)--?>", re.DOTALL) +# Matches closing block comment: <!-- /wp:divi/MODULE --> +_BLOCK_CLOSE = re.compile(r"<!--\s*/wp:(divi/[a-z0-9_-]+)\s*-->") + +# Strip et_pb_* class tokens and data-et-* attributes +_ET_CLASS = re.compile(r"\b(et_pb_[a-z0-9_-]+|divi-[a-z0-9_-]+-[a-z0-9_-]+|d5_[a-z0-9_-]+)\b", re.IGNORECASE) +_ET_ATTR = re.compile(r'\s+data-(?:et|builder|module-id|module-class|d5)-[a-z0-9_-]+\s*=\s*"[^"]*"', re.IGNORECASE) +_EMPTY_CL = re.compile(r'\s+class="\s*"') + + +def _clean(html: str) -> str: + """Strip Divi noise from an HTML fragment.""" + out = _BLOCK_OPEN.sub("", html) + out = _BLOCK_CLOSE.sub("", out) + out = _ET_ATTR.sub("", out) + out = _ET_CLASS.sub("", out) + out = _EMPTY_CL.sub("", out) + out = re.sub(r"\n{3,}", "\n\n", out) + return out.strip() + + +def _parse_attrs(raw_json: str) -> dict: + """Parse the JSON attrs blob from a block comment (may be empty).""" + raw_json = raw_json.strip() + if not raw_json: + return {} + try: + return json.loads(raw_json) + except Exception: + return {} + + +def _extract_inner(content: str, block_type: str) -> str: + """Return the raw inner HTML of the first matching block.""" + open_pat = re.compile(rf"<!--\s*wp:{re.escape(block_type)}[^>]*-->", re.DOTALL) + close_pat = re.compile(rf"<!--\s*/wp:{re.escape(block_type)}\s*-->") + m = open_pat.search(content) + if not m: + return "" + start = m.end() + m2 = close_pat.search(content, start) + end = m2.start() if m2 else len(content) + return content[start:end] + + +def _bg_color(attrs: dict) -> str: + """Extract background colour from Divi 5 attrs dict.""" + bg = attrs.get("backgroundColor", {}) + if isinstance(bg, dict): + return bg.get("value", bg.get("color", "")) + return str(bg) if bg else "" + + +def _section_type(bg: str) -> str: + """Classify section by background colour.""" + dark_colors = {"#0f5f53", "#1a3a34", "#0d4d42"} + brand_colors = {"#1a8a7a", "#20a090"} + light_colors = {"#f5f5f5", "#fafafa", "#f0f0f0", "#efefef"} + bg_lower = bg.lower().strip() + if bg_lower in dark_colors: + return "dark" + if bg_lower in brand_colors: + return "brand" + if bg_lower in light_colors: + return "light" + if bg_lower in ("#ffffff", "#fff", ""): + return "white" + return "custom" + + +# --------------------------------------------------------------------------- +# Section/module extraction +# --------------------------------------------------------------------------- + +def _extract_modules(section_html: str) -> list[dict]: + """Walk block comments inside a section and extract module data.""" + modules: list[dict] = [] + pos = 0 + content = section_html + + for m in _BLOCK_OPEN.finditer(content): + block_type = m.group(1) # e.g. "divi/text" + attrs = _parse_attrs(m.group(2)) + inner_start = m.end() + + # Find matching close tag + close_pat = re.compile(rf"<!--\s*/wp:{re.escape(block_type)}\s*-->") + close_m = close_pat.search(content, inner_start) + inner_html = content[inner_start : close_m.start() if close_m else len(content)] + clean_inner = _clean(inner_html) + + module_type = block_type.split("/")[-1] # "text", "button", "image", etc. + + mod: dict = {"module": module_type} + + if module_type == "text": + mod["html"] = clean_inner + mod["text"] = _text(clean_inner) + + elif module_type in ("button", "cta"): + mod["text"] = attrs.get("buttonText", _text(clean_inner)) + mod["url"] = attrs.get("buttonUrl", attrs.get("url", "#")) + + elif module_type == "image": + src = attrs.get("src", attrs.get("url", "")) + mod["src"] = src + mod["alt"] = attrs.get("altText", attrs.get("alt", "")) + mod["caption"] = attrs.get("caption", "") + + elif module_type == "blurb": + mod["title"] = attrs.get("title", "") + mod["icon"] = attrs.get("iconName", "") + mod["html"] = clean_inner + mod["text"] = _text(clean_inner) + + elif module_type == "testimonial": + mod["quote"] = attrs.get("content", _text(clean_inner)) + mod["author"] = attrs.get("authorName", "") + mod["company"] = attrs.get("authorJobTitle", "") + + elif module_type == "video": + mod["src"] = attrs.get("src", "") + mod["poster"] = attrs.get("poster", attrs.get("image", "")) + + elif module_type in ("accordion", "toggle"): + items = re.findall(r"<dt[^>]*>(.*?)</dt>\s*<dd[^>]*>(.*?)</dd>", clean_inner, re.DOTALL) + mod["items"] = [{"q": q.strip(), "a": a.strip()} for q, a in items] + + elif module_type == "contact_form": + mod["form_id"] = attrs.get("formId", "") + mod["note"] = "REPLACE with AM vanilla form — see 08-forms.md" + + else: + mod["html"] = clean_inner + mod["attrs"] = attrs + + modules.append(mod) + + return modules + + +def parse_page_content(content_raw: str) -> list[dict]: + """Parse Divi 5 block content into a list of section dicts.""" + sections: list[dict] = [] + + section_pat = re.compile(r"<!--\s*wp:divi/section(.*?)-->", re.DOTALL) + section_close = re.compile(r"<!--\s*/wp:divi/section\s*-->") + + for sm in section_pat.finditer(content_raw): + attrs = _parse_attrs(sm.group(1).strip()) + start = sm.end() + close_m = section_close.search(content_raw, start) + sec_html = content_raw[start : close_m.start() if close_m else len(content_raw)] + + bg = _bg_color(attrs) + sec_type = _section_type(bg) + modules = _extract_modules(sec_html) + + # Determine semantic role from first module + role = "content" + if modules and modules[0]["module"] in ("fullwidth_header", "text"): + first_html = modules[0].get("html", "") + if "<h1" in first_html: + role = "hero" + + sections.append({ + "role": role, + "section_type": sec_type, + "background_color": bg, + "attrs": attrs, + "modules": modules, + }) + + return sections + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +def main(): + if len(sys.argv) < 3: + print(f"Usage: {sys.argv[0]} <pages_json> <output_dir>") + sys.exit(1) + + pages_path = Path(sys.argv[1]) + out_dir = Path(sys.argv[2]) + out_dir.mkdir(parents=True, exist_ok=True) + + pages = json.loads(pages_path.read_text(encoding="utf-8")) + print(f"Processing {len(pages)} pages...") + + for page in pages: + slug = page.get("slug") or f"page-{page['id']}" + content = page.get("content_raw", "") + + sections = parse_page_content(content) if content.strip() else [] + + output = { + "id": page["id"], + "slug": slug, + "title": page["title"], + "post_type": page["post_type"], + "seo_title": page.get("seo_title", ""), + "seo_description": page.get("seo_description", ""), + "seo_keywords": page.get("seo_keywords", ""), + "acf": page.get("acf", {}), + "date": page.get("date", ""), + "modified": page.get("modified", ""), + "sections": sections, + "section_count": len(sections), + } + + out_file = out_dir / f"{slug}.json" + out_file.write_text(json.dumps(output, indent=2, ensure_ascii=False)) + print(f" {slug}.json ({len(sections)} sections)") + + print(f"\nDone. {len(pages)} content files in {out_dir}") + + +if __name__ == "__main__": + main() diff --git a/wp-divi-pipeline-to-am-stack/scripts/extract_nav.py b/wp-divi-pipeline-to-am-stack/scripts/extract_nav.py new file mode 100644 index 0000000..179ce7b --- /dev/null +++ b/wp-divi-pipeline-to-am-stack/scripts/extract_nav.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +""" +extract_nav.py — Extract WordPress navigation menus from database.sql dump. +Outputs nav.json: [{label, href, display_order, is_cta}] + +Usage: python3 extract_nav.py <wpress-extract-dir> <output-data-dir> +""" +import sys, re, json, os + +CTA_KEYWORDS = {'book', 'get started', 'contact', 'sign up', 'register', 'join', 'buy', 'shop'} + +def extract_nav(extract_dir: str, data_dir: str): + sql_path = os.path.join(extract_dir, 'database.sql') + if not os.path.exists(sql_path): + print(f"ERROR: {sql_path} not found", file=sys.stderr) + sys.exit(1) + + with open(sql_path, encoding='utf-8', errors='replace') as f: + sql = f.read() + + # Detect table prefix + prefix_match = re.search(r"INSERT INTO `(\w+)options`", sql) + prefix = prefix_match.group(1) if prefix_match else 'wp_' + + # Find nav menu items: post_type = 'nav_menu_item' + # Extract INSERT rows from wp_posts + posts_pattern = re.compile( + r"INSERT INTO `%sposts`[^;]+?;" % re.escape(prefix), + re.DOTALL | re.IGNORECASE + ) + postmeta_pattern = re.compile( + r"INSERT INTO `%spostmeta`[^;]+?;" % re.escape(prefix), + re.DOTALL | re.IGNORECASE + ) + + nav_posts = {} + for m in posts_pattern.finditer(sql): + rows = re.findall(r"\((\d+),[^,]*,'[^']*','[^']*','([^']*)'[^,]*,[^,]*,[^,]*,[^,]*,[^,]*,[^,]*,[^,]*,'([^']*)'[^,]*,[^,]*,\d+,'nav_menu_item'", m.group()) + for post_id, post_title, post_status in rows: + if post_status == 'publish': + nav_posts[post_id] = {'label': post_title, 'href': '/', 'menu_order': 0} + + if not nav_posts: + # Fallback: simpler pattern + for m in posts_pattern.finditer(sql): + block = m.group() + ids = re.findall(r"\((\d+),", block) + titles = re.findall(r"'([^']{1,60})'", block) + for i, post_id in enumerate(ids): + if i < len(titles) and titles[i]: + nav_posts[post_id] = {'label': titles[i], 'href': '/', 'menu_order': i} + + # Extract menu item URLs from postmeta (_menu_item_url or _menu_item_object_id) + for m in postmeta_pattern.finditer(sql): + block = m.group() + # _menu_item_url + url_matches = re.findall(r"\((\d+),\s*\d+,\s*'_menu_item_url',\s*'([^']*)'\)", block) + for post_id, url in url_matches: + if post_id in nav_posts and url: + nav_posts[post_id]['href'] = url + # _menu_item_menu_order + order_matches = re.findall(r"\((\d+),\s*\d+,\s*'_menu_item_menu_order',\s*'(\d+)'\)", block) + for post_id, order in order_matches: + if post_id in nav_posts: + nav_posts[post_id]['menu_order'] = int(order) + + # Clean up hrefs: make relative if same domain + items = [] + for idx, (post_id, item) in enumerate(sorted(nav_posts.items(), key=lambda x: x[1].get('menu_order', 0))): + label = item['label'].strip() + href = item['href'].strip() + if not label: + continue + # Make relative + href = re.sub(r'https?://[^/]+', '', href) or '/' + if not href.startswith('/'): + href = '/' + href + is_cta = 1 if any(kw in label.lower() for kw in CTA_KEYWORDS) else 0 + items.append({ + 'label': label, + 'href': href, + 'display_order': idx + 1, + 'is_cta': is_cta + }) + + os.makedirs(data_dir, exist_ok=True) + out_path = os.path.join(data_dir, 'nav.json') + with open(out_path, 'w', encoding='utf-8') as f: + json.dump(items, f, indent=2, ensure_ascii=False) + + print(f"nav.json: {len(items)} items → {out_path}") + for item in items: + print(f" {'[CTA]' if item['is_cta'] else ' '} {item['label']} → {item['href']}") + +if __name__ == '__main__': + if len(sys.argv) != 3: + print("Usage: python3 extract_nav.py <wpress-extract-dir> <output-data-dir>") + sys.exit(1) + extract_nav(sys.argv[1], sys.argv[2]) diff --git a/wp-divi-pipeline-to-am-stack/scripts/extract_wpress.py b/wp-divi-pipeline-to-am-stack/scripts/extract_wpress.py new file mode 100644 index 0000000..59fad45 --- /dev/null +++ b/wp-divi-pipeline-to-am-stack/scripts/extract_wpress.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +"""Extract All-in-One WP Migration .wpress archive. + +Usage: + python3 extract_wpress.py <path/to/file.wpress> <output/directory> + +The .wpress format is a sequential binary archive with 4377-byte headers: + 255 bytes filename (null-padded) + 14 bytes file size in bytes (ASCII digits, null-padded) + 12 bytes mtime unix timestamp (ASCII digits, null-padded) + 4096 bytes relative path (null-padded) +Followed immediately by the raw file bytes, then the next header. +""" +import os +import sys +import argparse +from pathlib import Path + +HEADER_SIZE = 4377 +NAME_LEN = 255 +SIZE_LEN = 14 +MTIME_LEN = 12 +PATH_LEN = 4096 + + +def _parse_int(b: bytes) -> int: + s = b.split(b"\x00", 1)[0].decode(errors="replace").strip() + return int(s) if s else 0 + + +def _parse_str(b: bytes) -> str: + return b.split(b"\x00", 1)[0].decode(errors="replace") + + +def extract(wpress_path: str, out_dir: str, verbose: bool = True) -> dict: + out = Path(out_dir) + out.mkdir(parents=True, exist_ok=True) + count = 0 + total_bytes = 0 + skipped = 0 + + with open(wpress_path, "rb") as f: + while True: + header = f.read(HEADER_SIZE) + if not header or len(header) < HEADER_SIZE: + break + if header == b"\x00" * HEADER_SIZE: + break + + name = _parse_str(header[0:NAME_LEN]) + size = _parse_int(header[NAME_LEN : NAME_LEN + SIZE_LEN]) + mtime = _parse_int(header[NAME_LEN + SIZE_LEN : NAME_LEN + SIZE_LEN + MTIME_LEN]) + path = _parse_str(header[NAME_LEN + SIZE_LEN + MTIME_LEN : NAME_LEN + SIZE_LEN + MTIME_LEN + PATH_LEN]) + + # Sanitise path traversal + path = path.lstrip("/").lstrip("\\").lstrip(".") + path = path.lstrip("/") + + dest_dir = out / path if path else out + dest_dir.mkdir(parents=True, exist_ok=True) + dest_file = dest_dir / name + + if not name: + skipped += 1 + f.seek(size, 1) + continue + + with open(dest_file, "wb") as o: + remaining = size + while remaining > 0: + chunk = f.read(min(65536, remaining)) + if not chunk: + break + o.write(chunk) + remaining -= len(chunk) + + try: + if mtime > 0: + os.utime(dest_file, (mtime, mtime)) + except Exception: + pass + + count += 1 + total_bytes += size + + if verbose and count % 200 == 0: + print(f" [{count} files | {total_bytes / 1024 / 1024:.1f} MB extracted]", flush=True) + + result = { + "files": count, + "bytes": total_bytes, + "mb": round(total_bytes / 1024 / 1024, 1), + "skipped": skipped, + "out_dir": str(out), + } + print(f"DONE: {count} files | {result['mb']} MB -> {out_dir} (skipped {skipped})") + return result + + +def main(): + p = argparse.ArgumentParser(description="Extract .wpress archive") + p.add_argument("wpress", help="Path to .wpress file") + p.add_argument("outdir", help="Destination directory") + p.add_argument("-q", "--quiet", action="store_true", help="Suppress progress output") + args = p.parse_args() + extract(args.wpress, args.outdir, verbose=not args.quiet) + + +if __name__ == "__main__": + main() diff --git a/wp-divi-pipeline-to-am-stack/scripts/migrate.py b/wp-divi-pipeline-to-am-stack/scripts/migrate.py new file mode 100644 index 0000000..9a4504d --- /dev/null +++ b/wp-divi-pipeline-to-am-stack/scripts/migrate.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +""" +migrate.py — AM Stack A migration launcher. +Points at a .wpress file and runs all extraction phases automatically. +Phases 7+ require human/agent review of staged seed_databases.py. + +Usage: + python3 migrate.py --wpress /path/to/backup.wpress --domain example.com [--project /path/to/project] + +Output: + Runs phases 0-6, then prints agent breadcrumbs for phases 7-11. +""" +import argparse, os, sys, subprocess, json + +SOPS = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +SCRIPTS = os.path.join(SOPS, 'scripts') + +def run(cmd: list, label: str) -> bool: + print(f"\n[{label}] Running: {' '.join(cmd)}") + result = subprocess.run(cmd, capture_output=False) + if result.returncode != 0: + print(f"[{label}] FAILED (exit {result.returncode})") + return False + print(f"[{label}] OK") + return True + +def phase_header(n: int, title: str): + print(f"\n{'='*60}") + print(f" Phase {n} — {title}") + print(f"{'='*60}") + +def main(): + parser = argparse.ArgumentParser(description='AM Stack A migration launcher') + parser.add_argument('--wpress', required=True, help='Path to .wpress backup file') + parser.add_argument('--domain', required=True, help='Target domain (e.g. example.com)') + parser.add_argument('--project', help='Project directory (default: ~/arisingmedia-websites/{domain})') + args = parser.parse_args() + + wpress = os.path.abspath(args.wpress) + domain = args.domain + project = args.project or os.path.expanduser(f'~/arisingmedia-websites/{domain}') + extract_dir = os.path.join(project, '.planning', 'wpress-extract') + data_dir = os.path.join(project, '.planning', 'data') + content_dir = os.path.join(data_dir, 'content') + + if not os.path.exists(wpress): + print(f"ERROR: .wpress file not found: {wpress}") + sys.exit(1) + + print(f"\nAM Stack A Migration Pipeline") + print(f" Domain: {domain}") + print(f" Project: {project}") + print(f" Archive: {wpress}") + + # Phase 0 — Setup + phase_header(0, 'Setup') + for d in [extract_dir, data_dir, content_dir, + os.path.join(project, 'assets', 'images'), + os.path.join(project, 'build'), + os.path.join(project, 'src', 'api', 'data'), + os.path.join(project, 'src', 'api', 'templates'), + os.path.join(project, 'src', 'api', 'components')]: + os.makedirs(d, exist_ok=True) + print(f" mkdir {d}") + + # Phase 1 — Extract + phase_header(1, 'Extract .wpress archive') + if not run(['python3', os.path.join(SCRIPTS, 'extract_wpress.py'), wpress, extract_dir], 'Phase 1'): + sys.exit(1) + + # Phase 2 — DB Analysis + phase_header(2, 'Database analysis') + if not run(['python3', os.path.join(SCRIPTS, 'analyze_db.py'), extract_dir, data_dir], 'Phase 2'): + sys.exit(1) + + # Detect Divi version + site_info_path = os.path.join(data_dir, 'site-info.json') + divi_version = 5 + if os.path.exists(site_info_path): + with open(site_info_path) as f: + info = json.load(f) + divi_version = info.get('divi_version', 5) + print(f" Divi version detected: {divi_version}") + + # Phase 3 — Nav extraction + phase_header(3, 'Extract navigation menus') + run(['python3', os.path.join(SCRIPTS, 'extract_nav.py'), extract_dir, data_dir], 'Phase 3 (nav)') + + # Phase 3 — Content extraction + extract_script = f'extract_divi{divi_version}.py' + pages_json = os.path.join(data_dir, 'pages.json') + if not run(['python3', os.path.join(SCRIPTS, extract_script), pages_json, content_dir], f'Phase 3 (divi{divi_version})'): + print(f" WARNING: content extraction had errors — review {content_dir}") + + # Phase 5 — Media + phase_header(5, 'Extract and convert media') + run(['python3', os.path.join(SCRIPTS, 'extract_media.py'), extract_dir, data_dir, + os.path.join(project, 'assets', 'images')], 'Phase 5') + + # Phase 6 — Stage seed_databases.py + phase_header(6, 'Stage seed_databases.py skeleton') + seed_path = os.path.join(project, 'build', 'seed_databases.py') + # Check if stage_seed.py exists + stage_script = os.path.join(SCRIPTS, 'stage_seed.py') + if os.path.exists(stage_script): + run(['python3', stage_script, data_dir, seed_path, '--domain', domain], 'Phase 6') + else: + print(f" WARNING: stage_seed.py not found — seed_databases.py must be written manually") + print(f" Reference: /home/sirdrez/arisingmedia-websites/vibrantyou.yoga/build/seed_databases.py") + + # Print agent breadcrumbs for remaining phases + print(f"\n{'='*60}") + print(" EXTRACTION COMPLETE — Manual/Agent phases follow") + print(f"{'='*60}") + print(f""" +Phases 0-6 complete. Staged content is at: + {data_dir}/content/ ← extracted page sections (JSON) + {data_dir}/nav.json ← navigation items + {data_dir}/media-manifest.json ← image URL mappings + {seed_path} ← seed_databases.py skeleton + +Next steps (see 10-agent-breadcrumbs.md for full detail): + + Phase 7 — REVIEW seed_databases.py + Open: {seed_path} + For each page: verify sections_json has correct section types + Replace em-dashes. Remove Divi shortcode residue. Review nav items. + + Phase 8 — RUN seed_databases.py + cd {project} && python3 build/seed_databases.py + Verify: output shows all counts > 0 + + Phase 9 — SCAFFOLD PHP templates + Copy from reference: vibrantyou.yoga/src/api/ + Update brand name and colors in _header.php + _footer.php + + Phase 10 — BUILD + cd {project} && docker compose build --no-cache && docker compose up -d + Verify: curl -I http://localhost:PORT/ + + Phase 11 — QA + bash {SOPS}/../tools/verify-protection.sh http://localhost:PORT + Lighthouse in Firefox + +Reference: {SOPS}/wp-divi-pipeline-to-am-stack/10-agent-breadcrumbs.md +""") + +if __name__ == '__main__': + main() diff --git a/wp-divi-pipeline-to-am-stack/scripts/run_pipeline.sh b/wp-divi-pipeline-to-am-stack/scripts/run_pipeline.sh new file mode 100644 index 0000000..b958847 --- /dev/null +++ b/wp-divi-pipeline-to-am-stack/scripts/run_pipeline.sh @@ -0,0 +1,175 @@ +#!/usr/bin/env bash +# run_pipeline.sh — AM WP+Divi to HTML pipeline master script +# Usage: bash run_pipeline.sh <domain> +# Example: bash run_pipeline.sh vibrantyou.yoga +set -euo pipefail + +DOMAIN="${1:-}" +if [ -z "$DOMAIN" ]; then + echo "Usage: $0 <domain>" + echo " Example: $0 vibrantyou.yoga" + exit 1 +fi + +PROJECT="/home/sirdrez/arisingmedia-websites/$DOMAIN" +SOPS="/home/sirdrez/arisingmedia-websites/.am-webdesign-sops" +SCRIPTS="$SOPS/wp-divi-pipeline/scripts" +WPRESS=$(ls "$PROJECT/.planning/"*.wpress 2>/dev/null | head -1) + +if [ -z "$WPRESS" ]; then + echo "ERROR: No .wpress file found in $PROJECT/.planning/" + exit 1 +fi + +echo "================================================" +echo " AM WP+Divi Pipeline" +echo " Domain: $DOMAIN" +echo " Archive: $(basename $WPRESS)" +echo "================================================" +echo "" + +# --------------------------------------------------------------------------- +# Phase 0 — Directory structure +# --------------------------------------------------------------------------- +echo "[Phase 0] Creating directory structure..." +mkdir -p "$PROJECT"/{src/{about,services,contact,blog,classes,components,assets/{css,js,images,svg,fonts}},build,infra,api} +mkdir -p "$PROJECT/.planning"/{data/{content},scripts,wpress-extract} +echo " OK: directories created" +echo "" + +# --------------------------------------------------------------------------- +# Phase 1 — Extract .wpress archive +# --------------------------------------------------------------------------- +EXTRACT_DIR="$PROJECT/.planning/wpress-extract" + +if [ -f "$EXTRACT_DIR/database.sql" ]; then + echo "[Phase 1] Archive already extracted — skipping" + echo " Found: $EXTRACT_DIR/database.sql" +else + echo "[Phase 1] Extracting archive (this may take a few minutes)..." + python3 "$SCRIPTS/extract_wpress.py" "$WPRESS" "$EXTRACT_DIR" + echo " OK: extraction complete" +fi +echo "" + +# --------------------------------------------------------------------------- +# Phase 2 — Database analysis +# --------------------------------------------------------------------------- +DATA_DIR="$PROJECT/.planning/data" +echo "[Phase 2] Analyzing database..." +python3 "$SCRIPTS/analyze_db.py" "$EXTRACT_DIR" "$DATA_DIR" + +PAGE_COUNT=$(python3 -c "import json; print(len(json.load(open('$DATA_DIR/pages.json'))))" 2>/dev/null || echo 0) +echo " OK: $PAGE_COUNT pages extracted" +echo "" + +# --------------------------------------------------------------------------- +# Phase 3 — Content extraction (Divi 5) +# --------------------------------------------------------------------------- +echo "[Phase 3] Extracting Divi 5 content..." +python3 "$SCRIPTS/extract_divi5.py" \ + "$DATA_DIR/pages.json" \ + "$DATA_DIR/content/" +echo " OK: content JSON files written" +echo "" + +# --------------------------------------------------------------------------- +# Phase 4 — Design system (manual step) +# --------------------------------------------------------------------------- +echo "[Phase 4] Design system (MANUAL STEP REQUIRED)" +echo " Read: $DATA_DIR/design-system.json" +echo " Write: $PROJECT/src/assets/css/main.css" +echo " Ref: $SOPS/wp-divi-pipeline/04-design-system-extraction.md" +echo "" + +# --------------------------------------------------------------------------- +# Phase 5 — Media migration +# --------------------------------------------------------------------------- +UPLOADS_DIR="$EXTRACT_DIR/uploads" +IMAGES_DIR="$PROJECT/src/assets/images" + +if [ -d "$UPLOADS_DIR" ]; then + echo "[Phase 5] Migrating media..." + # Catalog originals (skip WP-generated size variants) + find "$UPLOADS_DIR" -type f \( -name "*.jpg" -o -name "*.jpeg" -o -name "*.png" -o -name "*.gif" -o -name "*.webp" \) \ + | grep -v -E "\-[0-9]+x[0-9]+\.(jpg|jpeg|png|webp|gif)$" \ + | sort > "$DATA_DIR/media-originals.txt" + + MEDIA_COUNT=$(wc -l < "$DATA_DIR/media-originals.txt") + echo " Found: $MEDIA_COUNT original images" + + # Copy to src/assets/images/ + while IFS= read -r src_img; do + fname=$(basename "$src_img") + cp "$src_img" "$IMAGES_DIR/$fname" + done < "$DATA_DIR/media-originals.txt" + + # Convert to WebP if cwebp available + if command -v cwebp &>/dev/null; then + echo " Converting to WebP..." + cd "$IMAGES_DIR" + for img in *.jpg *.jpeg *.png; do + [ -f "$img" ] || continue + base="${img%.*}" + cwebp -q 82 "$img" -o "${base}.webp" 2>/dev/null && rm "$img" + done + WEBP_COUNT=$(ls *.webp 2>/dev/null | wc -l) + echo " WebP files: $WEBP_COUNT" + cd "$PROJECT" + else + echo " WARN: cwebp not found — images copied as-is (convert manually)" + fi + echo " OK: media migrated to $IMAGES_DIR" +else + echo "[Phase 5] No uploads/ directory found — skipping media migration" +fi +echo "" + +# --------------------------------------------------------------------------- +# Phase 6 — HTML build (manual step) +# --------------------------------------------------------------------------- +echo "[Phase 6] HTML Build (MANUAL STEP REQUIRED)" +echo " Ref: $SOPS/wp-divi-pipeline/05-content-migration.md" +echo " Build order:" +echo " 1. src/assets/css/main.css" +echo " 2. src/assets/css/components.css" +echo " 3. src/components/header.html" +echo " 4. src/components/footer.html" +echo " 5. src/assets/js/components.js" +echo " 6. src/assets/js/main.js" +echo " 7. src/index.html (home — design system anchor)" +echo " 8. Remaining pages" +echo "" + +# --------------------------------------------------------------------------- +# Phase 7 — SEO audit +# --------------------------------------------------------------------------- +echo "[Phase 7] SEO audit (run after HTML build):" +echo " grep -rL '<title>' $PROJECT/src --include='*.html' | grep -v _template" +echo " grep -rL 'canonical' $PROJECT/src --include='*.html' | grep -v _template" +echo " grep -rL 'ld+json' $PROJECT/src --include='*.html' | grep -v _template" +echo " grep -r '{{' $PROJECT/src --include='*.html'" +echo "" + +# --------------------------------------------------------------------------- +# Phase 8 — Infra +# --------------------------------------------------------------------------- +echo "[Phase 8] Infra setup:" +echo " Copy Dockerfile + docker-compose.yml from vibrantyoucoaching.com" +echo " Update server_name in infra/nginx.conf to: $DOMAIN" +echo " Run: docker compose up -d --build" +echo "" + +# --------------------------------------------------------------------------- +# Phase 9 — Protection check +# --------------------------------------------------------------------------- +echo "[Phase 9] After deploy, run:" +echo " bash $SOPS/tools/verify-protection.sh https://$DOMAIN" +echo "" + +echo "================================================" +echo " Pipeline setup complete." +echo " Phases 0-3 + 5 executed automatically." +echo " Phases 4, 6, 7, 8, 9 require manual steps." +echo " See $SOPS/wp-divi-pipeline/ for all SOPs." +echo "================================================" diff --git a/wp-divi-pipeline-to-am-stack/scripts/stage_seed.py b/wp-divi-pipeline-to-am-stack/scripts/stage_seed.py new file mode 100644 index 0000000..74d1521 --- /dev/null +++ b/wp-divi-pipeline-to-am-stack/scripts/stage_seed.py @@ -0,0 +1,574 @@ +#!/usr/bin/env python3 +""" +stage_seed.py — Phase 6 of WP/Divi → Stack A migration pipeline. + +Reads extracted JSON from prior pipeline run and generates a seed_databases.py +skeleton for the target project. Human/agent reviews [FILL] markers and fills +gaps before running the seeder. + +Usage: + python3 stage_seed.py <data_dir> <seed_path> --domain <domain> [--force] + +Example: + python3 stage_seed.py /path/to/.planning/data build/seed_databases.py --domain example.com +""" + +import argparse +import json +import os +import re +from datetime import datetime + + +def slugify(text): + """Convert text to URL-safe slug.""" + return re.sub(r'[^a-z0-9]+', '-', text.lower()).strip('-') + + +def infer_template(slug): + """Infer template type from page slug.""" + slug_lower = slug.lower() + if slug_lower == 'home': + return 'home' + elif slug_lower in ('classes', 'class'): + return 'classes' + elif slug_lower == 'schedule': + return 'schedule' + elif slug_lower == 'glossary': + return 'glossary' + elif slug_lower in ('blog', 'posts', 'articles'): + return 'blog' + else: + return 'static' + + +def load_json_file(path): + """Load JSON file, return empty dict/list if not found.""" + if not os.path.exists(path): + return None + try: + with open(path, 'r') as f: + return json.load(f) + except Exception as e: + print(f"Warning: Failed to load {path}: {e}") + return None + + +def generate_seed_script(data_dir, domain, design_system, pages, glossary, nav): + """Generate the seed_databases.py script content.""" + now = datetime.now().isoformat() + + # Build pages_data list in outer scope + pages_list = [] + for page in pages: + if page.get('status') != 'publish' or page.get('post_type') != 'page': + continue + + slug = page.get('slug', '') + title = page.get('title', '[FILL] Title needed') + meta_desc = page.get('seo_description', '') + if not meta_desc: + meta_desc = f"[FILL] Meta description for {slug}" + + canonical = f"https://{domain}/{slug}/" if slug != 'home' else f"https://{domain}/" + date_str = page.get('date', datetime.now().isoformat()) + + # Infer template + template_map = { + 'home': 'home', + 'classes': 'classes', + 'schedule': 'schedule', + 'glossary': 'glossary', + 'blog': 'blog', + } + template = template_map.get(slug, 'static') + + pages_list.append({ + 'slug': slug, + 'template': template, + 'title': title, + 'meta_description': meta_desc, + 'canonical_url': canonical, + 'hero_h1': f"[FILL] {title}", + 'sections_json': '[]', + 'updated_at': date_str + }) + + # Build pages_data JSON string + pages_json_str = json.dumps(pages_list, indent=8) + + script = f'''#!/usr/bin/env python3 +""" +seed_databases.py — generated by stage_seed.py on {now} +Source: {data_dir} +Domain: {domain} + +EDIT THIS FILE then run: python3 build/seed_databases.py +Content marked [FILL] needs human/agent review before seeding. +""" +import sqlite3 +import json +import os +from datetime import datetime + +DB_DIR = os.path.join(os.path.dirname(__file__), '..', 'src', 'api', 'data') +os.makedirs(DB_DIR, exist_ok=True) + + +def slugify(text): + """Convert text to URL-safe slug.""" + import re + return re.sub(r'[^a-z0-9]+', '-', text.lower()).strip('-') + + +def seed_pages(): + """Create pages.sqlite and populate with published pages.""" + db_path = os.path.join(DB_DIR, 'pages.sqlite') + conn = sqlite3.connect(db_path) + c = conn.cursor() + + c.execute(""" + CREATE TABLE IF NOT EXISTS pages ( + id INTEGER PRIMARY KEY, + slug TEXT UNIQUE NOT NULL, + template TEXT NOT NULL, + title TEXT NOT NULL, + meta_description TEXT, + canonical_url TEXT, + og_image TEXT, + schema_json TEXT, + hero_eyebrow TEXT, + hero_h1 TEXT, + hero_lead TEXT, + sections_json TEXT, + updated_at TEXT + ) + """) + + pages_data = {pages_json_str} + + for page in pages_data: + c.execute(""" + INSERT OR REPLACE INTO pages + (slug, template, title, meta_description, canonical_url, hero_h1, sections_json, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, ( + page['slug'], + page['template'], + page['title'], + page['meta_description'], + page['canonical_url'], + page['hero_h1'], + page['sections_json'], + page['updated_at'] + )) + + conn.commit() + conn.close() + print(f"✓ pages.sqlite created with {{len(pages_data)}} pages") + + +def seed_nav(): + """Create nav.sqlite and populate navigation items.""" + db_path = os.path.join(DB_DIR, 'nav.sqlite') + conn = sqlite3.connect(db_path) + c = conn.cursor() + + c.execute(""" + CREATE TABLE IF NOT EXISTS nav_items ( + id INTEGER PRIMARY KEY, + label TEXT NOT NULL, + href TEXT NOT NULL, + display_order INTEGER DEFAULT 0, + is_cta INTEGER DEFAULT 0 + ) + """) +''' + + if nav: + script += f''' + nav_items = {json.dumps(nav, indent=8)} + + for item in nav_items: + c.execute(""" + INSERT INTO nav_items (label, href, display_order, is_cta) + VALUES (?, ?, ?, ?) + """, (item['label'], item['href'], item.get('display_order', 0), item.get('is_cta', 0))) + + conn.commit() + conn.close() + print(f"✓ nav.sqlite created with {{len(nav_items)}} nav items") +''' + else: + script += ''' + # [FILL] nav.json not found — add navigation items manually + # Example: + # nav_items = [ + # {"label": "Home", "href": "/", "display_order": 1, "is_cta": 0}, + # {"label": "Classes", "href": "/classes", "display_order": 2, "is_cta": 0}, + # {"label": "Schedule", "href": "/schedule", "display_order": 3, "is_cta": 0}, + # {"label": "Get Started", "href": "/contact", "display_order": 4, "is_cta": 1}, + # ] + # Then uncomment and insert rows + + conn.commit() + conn.close() + print("✓ nav.sqlite created (empty — [FILL] navigation items)") +''' + + # Seed glossary + if glossary: + script += f''' + + +def seed_glossary(): + """Create glossary.sqlite and populate terms.""" + db_path = os.path.join(DB_DIR, 'glossary.sqlite') + conn = sqlite3.connect(db_path) + c = conn.cursor() + + c.execute(""" + CREATE TABLE IF NOT EXISTS terms ( + id INTEGER PRIMARY KEY, + slug TEXT UNIQUE NOT NULL, + term TEXT NOT NULL, + pronunciation TEXT, + definition TEXT NOT NULL, + category TEXT NOT NULL, + level TEXT NOT NULL, + display_order INTEGER DEFAULT 0 + ) + """) + + glossary_items = {json.dumps(glossary, indent=8)} + + for idx, item in enumerate(glossary_items): + fields = item.get('fields', {{}}) + term = fields.get('sanskrit_name', '[FILL] Term needed') + slug = slugify(term) + pronunciation = fields.get('pronunciation', '') + definition = fields.get('definition', '[FILL] Definition needed') + category = fields.get('category', 'yoga') + level = fields.get('level', 'beginner') + + c.execute(""" + INSERT OR REPLACE INTO terms + (slug, term, pronunciation, definition, category, level, display_order) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, (slug, term, pronunciation, definition, category, level, idx)) + + conn.commit() + conn.close() + print(f"✓ glossary.sqlite created with {{len(glossary_items)}} terms") +''' + else: + script += ''' + + +def seed_glossary(): + """Create glossary.sqlite (empty — no glossary.json found).""" + db_path = os.path.join(DB_DIR, 'glossary.sqlite') + conn = sqlite3.connect(db_path) + c = conn.cursor() + + c.execute(""" + CREATE TABLE IF NOT EXISTS terms ( + id INTEGER PRIMARY KEY, + slug TEXT UNIQUE NOT NULL, + term TEXT NOT NULL, + pronunciation TEXT, + definition TEXT NOT NULL, + category TEXT NOT NULL, + level TEXT NOT NULL, + display_order INTEGER DEFAULT 0 + ) + """) + + conn.commit() + conn.close() + print("✓ glossary.sqlite created (empty)") +''' + + script += ''' + + +def seed_testimonials(): + """Create testimonials.sqlite (empty stub).""" + db_path = os.path.join(DB_DIR, 'testimonials.sqlite') + conn = sqlite3.connect(db_path) + c = conn.cursor() + + c.execute(""" + CREATE TABLE IF NOT EXISTS testimonials ( + id INTEGER PRIMARY KEY, + quote TEXT NOT NULL, + author_name TEXT NOT NULL, + author_role TEXT, + is_featured INTEGER DEFAULT 0 + ) + """) + + # [FILL] Add testimonials extracted from Divi testimonial modules or client-provided + # rows = [ + # {"quote": "...", "author_name": "...", "author_role": "...", "is_featured": 0}, + # ] + + conn.commit() + conn.close() + print("✓ testimonials.sqlite created (empty — [FILL] add testimonials)") + + +def seed_blog(): + """Create blog.sqlite (empty stub).""" + db_path = os.path.join(DB_DIR, 'blog.sqlite') + conn = sqlite3.connect(db_path) + c = conn.cursor() + + c.execute(""" + CREATE TABLE IF NOT EXISTS posts ( + id INTEGER PRIMARY KEY, + slug TEXT UNIQUE NOT NULL, + title TEXT NOT NULL, + excerpt TEXT, + content TEXT, + author TEXT, + published_at TEXT, + is_featured INTEGER DEFAULT 0 + ) + """) + + # [FILL] Add blog posts extracted from WP posts table + # rows = [ + # {"slug": "...", "title": "...", "excerpt": "...", "content": "...", "author": "...", "published_at": "..."}, + # ] + + conn.commit() + conn.close() + print("✓ blog.sqlite created (empty — [FILL] add blog posts)") + + +def seed_videos(): + """Create videos.sqlite (empty stub).""" + db_path = os.path.join(DB_DIR, 'videos.sqlite') + conn = sqlite3.connect(db_path) + c = conn.cursor() + + c.execute(""" + CREATE TABLE IF NOT EXISTS videos ( + id INTEGER PRIMARY KEY, + slug TEXT UNIQUE NOT NULL, + title TEXT NOT NULL, + duration TEXT, + embed_url TEXT, + thumbnail TEXT, + category TEXT, + level TEXT, + is_free INTEGER DEFAULT 1 + ) + """) + + # [FILL] Add on-demand video entries if site has video content + # rows = [ + # {"slug": "...", "title": "...", "duration": "12:34", "embed_url": "...", "category": "...", "level": "..."}, + # ] + + conn.commit() + conn.close() + print("✓ videos.sqlite created (empty — [FILL] add videos)") + + +def seed_events(): + """Create events.sqlite (empty stub).""" + db_path = os.path.join(DB_DIR, 'events.sqlite') + conn = sqlite3.connect(db_path) + c = conn.cursor() + + c.execute(""" + CREATE TABLE IF NOT EXISTS events ( + id INTEGER PRIMARY KEY, + slug TEXT UNIQUE NOT NULL, + title TEXT NOT NULL, + event_date TEXT, + time_cet TEXT, + format TEXT, + capacity INTEGER, + price_eur REAL, + status TEXT DEFAULT 'open' + ) + """) + + # [FILL] Add workshop/event entries + # rows = [ + # {"slug": "...", "title": "...", "event_date": "2026-06-15", "time_cet": "10:00", "format": "online", "capacity": 20, "price_eur": 29.99}, + # ] + + conn.commit() + conn.close() + print("✓ events.sqlite created (empty — [FILL] add events)") + + +def seed_schedule(): + """Create schedule.sqlite (empty stub).""" + db_path = os.path.join(DB_DIR, 'schedule.sqlite') + conn = sqlite3.connect(db_path) + c = conn.cursor() + + c.execute(""" + CREATE TABLE IF NOT EXISTS classes ( + id INTEGER PRIMARY KEY, + day_of_week TEXT NOT NULL, + day_order INTEGER NOT NULL, + time_cet TEXT NOT NULL, + class_name TEXT NOT NULL, + level TEXT NOT NULL, + format TEXT NOT NULL, + duration_min INTEGER NOT NULL, + badge_variant TEXT DEFAULT '' + ) + """) + + # [FILL] Add recurring class schedule rows + # rows = [ + # {"day_of_week": "Monday", "day_order": 1, "time_cet": "10:00", "class_name": "Hatha Yoga", "level": "beginner", "format": "online", "duration_min": 60, "badge_variant": "featured"}, + # ] + + conn.commit() + conn.close() + print("✓ schedule.sqlite created (empty — [FILL] add class schedule)") + + +def seed_instructors(): + """Create instructors.sqlite (empty stub).""" + db_path = os.path.join(DB_DIR, 'instructors.sqlite') + conn = sqlite3.connect(db_path) + c = conn.cursor() + + c.execute(""" + CREATE TABLE IF NOT EXISTS instructors ( + id INTEGER PRIMARY KEY, + slug TEXT UNIQUE NOT NULL, + name TEXT NOT NULL, + title TEXT, + bio TEXT, + certifications TEXT, + image TEXT, + is_primary INTEGER DEFAULT 0 + ) + """) + + # [FILL] Add instructor rows + # rows = [ + # {"slug": "alice-johnson", "name": "Alice Johnson", "title": "Lead Instructor", "bio": "...", "certifications": "...", "is_primary": 1}, + # ] + + conn.commit() + conn.close() + print("✓ instructors.sqlite created (empty — [FILL] add instructors)") + + +def seed_packages(): + """Create packages.sqlite (empty stub).""" + db_path = os.path.join(DB_DIR, 'packages.sqlite') + conn = sqlite3.connect(db_path) + c = conn.cursor() + + c.execute(""" + CREATE TABLE IF NOT EXISTS packages ( + id INTEGER PRIMARY KEY, + slug TEXT UNIQUE NOT NULL, + name TEXT NOT NULL, + price_eur REAL, + sessions_count INTEGER, + validity_days INTEGER, + is_featured INTEGER DEFAULT 0 + ) + """) + + # [FILL] Add class pack/package options + # rows = [ + # {"slug": "starter", "name": "Starter Pack", "price_eur": 49.99, "sessions_count": 5, "validity_days": 30, "is_featured": 0}, + # {"slug": "unlimited", "name": "Unlimited Monthly", "price_eur": 99.99, "sessions_count": None, "validity_days": 30, "is_featured": 1}, + # ] + + conn.commit() + conn.close() + print("✓ packages.sqlite created (empty — [FILL] add packages)") + + +if __name__ == '__main__': + seed_pages() + seed_nav() + seed_glossary() + seed_testimonials() + seed_blog() + seed_videos() + seed_events() + seed_schedule() + seed_instructors() + seed_packages() + print("\\nSeeding complete. Review [FILL] markers before running in production.") +''' + + return script + + +def main(): + parser = argparse.ArgumentParser( + description='Generate seed_databases.py from extracted WP/Divi JSON data' + ) + parser.add_argument('data_dir', help='Path to extracted data directory (.planning/data/)') + parser.add_argument('seed_path', help='Output path for seed_databases.py') + parser.add_argument('--domain', required=True, help='Domain name (e.g., example.com)') + parser.add_argument('--force', action='store_true', help='Overwrite existing seed_databases.py') + + args = parser.parse_args() + + # Validate inputs + if not os.path.isdir(args.data_dir): + print(f"Error: data_dir not found: {args.data_dir}") + return 1 + + if os.path.exists(args.seed_path) and not args.force: + print(f"Error: seed_databases.py already exists at {args.seed_path}") + print("Use --force to overwrite") + return 1 + + # Load required data files + pages = load_json_file(os.path.join(args.data_dir, 'pages.json')) + if not pages: + print("Error: pages.json not found or invalid") + return 1 + + design_system = load_json_file(os.path.join(args.data_dir, 'design-system.json')) + glossary = load_json_file(os.path.join(args.data_dir, 'glossary.json')) + nav = load_json_file(os.path.join(args.data_dir, 'nav.json')) + + # Generate script + script_content = generate_seed_script( + args.data_dir, + args.domain, + design_system, + pages, + glossary, + nav + ) + + # Write output + os.makedirs(os.path.dirname(args.seed_path), exist_ok=True) + with open(args.seed_path, 'w') as f: + f.write(script_content) + + # Make executable + os.chmod(args.seed_path, 0o755) + + print(f"✓ Generated: {args.seed_path}") + print(f" Pages: {len([p for p in pages if p.get('status') == 'publish' and p.get('post_type') == 'page'])}") + print(f" Glossary terms: {len(glossary) if glossary else 0}") + print(f" Nav items: {len(nav) if nav else 0}") + print("\nNext: Review [FILL] markers, then run: python3 " + args.seed_path) + + return 0 + + +if __name__ == '__main__': + exit(main()) diff --git a/wp-migration.json b/wp-migration.json new file mode 100644 index 0000000..6536d0c --- /dev/null +++ b/wp-migration.json @@ -0,0 +1,50 @@ +{ + "meta": { + "author": "Andre Cobham / Arising Media", + "updated": "2026-06-09", + "version": "1.0", + "description": "WordPress to AM PHP stack migration configuration and run order" + }, + "input": { + "format": ".wpress (All-in-One WP Migration backup)", + "supported_builders": ["Divi", "Elementor", "classic", "Gutenberg"], + "database_format": "MySQL dump extracted from .wpress" + }, + "output": { + "stack": "php:8.3-fpm-alpine + nginx + supervisord", + "data_layer": "SQLite (one db per content domain)", + "assets": "WebP only, baked into Docker image", + "routing": "PHP router + nginx location blocks" + }, + "pipeline_phases": [ + {"phase": 1, "name": "Extract", "description": "Unpack .wpress archive, extract MySQL dump and uploads folder"}, + {"phase": 2, "name": "Analyze", "description": "Parse WordPress DB dump, detect Divi version, inventory pages, extract content"}, + {"phase": 3, "name": "Design extraction", "description": "Extract color tokens, typography, layout patterns from Divi CSS"}, + {"phase": 4, "name": "Content migration", "description": "Rewrite content clean into SQLite pages and page_sections"}, + {"phase": 5, "name": "Media migration", "description": "Catalog uploads, skip WP-generated size variants, convert to WebP, remap paths"}, + {"phase": 6, "name": "SEO preservation", "description": "Map old WP URLs to new AM slugs, generate 301 redirect map"}, + {"phase": 7, "name": "Build", "description": "Scaffold AM project structure, seed SQLite DBs, write PHP templates"}, + {"phase": 8, "name": "Verify", "description": "Docker build, HTTP 200 all pages, mobile check, SEO audit, zero em-dashes"} + ], + "rules": [ + "Never a 1:1 Divi copy. Every migration is a content extraction and redesign", + "Never migrate the WordPress database. Content is rewritten cleaner", + "Never run headless WordPress or WordPress as API", + "Strip all Divi shortcodes, plugin CSS, and JS bundles", + "All media converted to WebP before baking into Docker image", + "URL slugs cleaned to flat lowercase-hyphen format", + "301 redirect map required for all changed URLs" + ], + "never_migrate": [ + "wp-admin paths", + "Divi shortcode markup", + "WordPress plugin CSS/JS", + "wp-content/cache", + "WordPress user accounts or session data" + ], + "tooling": { + "extraction": "scripts in wp-divi-pipeline-to-am-stack/scripts/", + "sop_folder": ".am-webdesign-sops/wp-divi-pipeline-to-am-stack/", + "reference_docs": ["00-overview.md", "08-run-order.md", "10-agent-breadcrumbs.md"] + } +}