recent updates

This commit is contained in:
2026-06-09 18:31:59 +02:00
parent 398b94965c
commit 94f7a1f72a
42 changed files with 8686 additions and 0 deletions
@@ -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)