371 lines
8.0 KiB
Markdown
371 lines
8.0 KiB
Markdown
# 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)
|