recent updates
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user