Establishes the deploy baseline on main so the admin agent's publish/rollback has a clean starting point. Everything built to date: sumi-e brand system, homepage, projects (DB-driven case studies), resume, about, services + website-design detail, contact form + DB, changelog, favicon + share card. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DoFYZY9gkGPNDqZ7NuEa9a
67 lines
3.2 KiB
SQL
67 lines
3.2 KiB
SQL
-- Projects — the source of truth for /projects and /projects/{slug}.
|
|
--
|
|
-- Two shapes live in one row: the short form the index needs (title, period,
|
|
-- summary, categories) and the long form a case-study page needs (lede, body,
|
|
-- metrics, links). Keeping them together avoids a join on every page render
|
|
-- and keeps editing in one place.
|
|
--
|
|
-- JSON is used for the small repeating lists — categories, stack, metrics,
|
|
-- links. At this scale that is the right trade: a normalised category table
|
|
-- would add two tables and a join to support a filter over a handful of rows.
|
|
-- MariaDB can still query inside them via JSON_CONTAINS if that is ever needed.
|
|
-- If projects grow past a few dozen, or categories need their own descriptions
|
|
-- and pages, promote `categories` to cja_project_categories + a map table.
|
|
|
|
CREATE TABLE IF NOT EXISTS `cja_projects` (
|
|
`project_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
|
|
|
-- identity
|
|
`slug` varchar(120) NOT NULL,
|
|
`title` varchar(160) NOT NULL,
|
|
`kind` varchar(60) NOT NULL DEFAULT '', -- "SaaS", "SaaS / Media"
|
|
|
|
-- timeline. period_label is what renders; the dates are what sorts, so an
|
|
-- ongoing project ("2020 — present") still orders correctly against a dated one.
|
|
`period_label` varchar(60) NOT NULL DEFAULT '',
|
|
`started_on` date DEFAULT NULL,
|
|
`ended_on` date DEFAULT NULL, -- NULL = ongoing
|
|
`status` enum('live','building','archived','concept')
|
|
NOT NULL DEFAULT 'live',
|
|
|
|
-- copy
|
|
`summary` varchar(500) NOT NULL DEFAULT '', -- index row
|
|
`lede` text DEFAULT NULL, -- detail page opener
|
|
`body` mediumtext DEFAULT NULL, -- markdown
|
|
`role` varchar(200) NOT NULL DEFAULT '', -- "Solo — design, build, ops"
|
|
|
|
-- structured lists
|
|
`categories` longtext DEFAULT NULL CHECK (json_valid(`categories`)),
|
|
`stack` longtext DEFAULT NULL CHECK (json_valid(`stack`)),
|
|
`metrics` longtext DEFAULT NULL CHECK (json_valid(`metrics`)),
|
|
`links` longtext DEFAULT NULL CHECK (json_valid(`links`)),
|
|
|
|
-- media
|
|
`cover_image` varchar(255) NOT NULL DEFAULT '',
|
|
`cover_alt` varchar(255) NOT NULL DEFAULT '',
|
|
|
|
-- seo. Nullable so the page can fall back to title/summary rather than
|
|
-- forcing an editor to duplicate them.
|
|
`seo_title` varchar(180) DEFAULT NULL,
|
|
`seo_description` varchar(320) DEFAULT NULL,
|
|
|
|
-- publishing
|
|
`featured` tinyint(1) NOT NULL DEFAULT 0,
|
|
`sort_order` int(11) NOT NULL DEFAULT 0,
|
|
`published` tinyint(1) NOT NULL DEFAULT 0,
|
|
`published_at` datetime DEFAULT NULL,
|
|
|
|
`created_at` datetime NOT NULL DEFAULT current_timestamp(),
|
|
`updated_at` datetime NOT NULL DEFAULT current_timestamp()
|
|
ON UPDATE current_timestamp(),
|
|
|
|
PRIMARY KEY (`project_id`),
|
|
UNIQUE KEY `uq_cja_projects_slug` (`slug`),
|
|
KEY `idx_cja_projects_listing` (`published`, `sort_order`, `started_on`),
|
|
KEY `idx_cja_projects_status` (`status`),
|
|
KEY `idx_cja_projects_featured` (`featured`, `sort_order`)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|