-- 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;