Consul Content Automation Audit (Architecture-Only)
Audit date: 2026-02-28
Scope: Full content stack (admin CMS, Supabase content layer, rendering surfaces, updates MDX subsystem, SEO/AI discovery routes, guide gating/PDF subsystem)
Method: Repository code and checked-in documentation only (no Supabase dashboard inspection)
Executive Summary
Consul currently runs a split content automation architecture with two primary pipelines:
-
Supabase-backed pipeline for blog, workflows, comparisons, and role pages.
Content is authored and edited through the in-repo admin UI, persisted toconsul_posts(plus keyword/auth tables), and rendered by Next.js App Router pages that fetch fromlib/supabase/content.ts. Public routes use ISR with route-specific revalidate windows and on-demand revalidation plumbing.
Evidence:app/blog/page.tsx:11,app/blog/[slug]/page.tsx:12,app/workflows/page.tsx:11,app/comparisons/page.tsx:14,app/for/[role]/page.tsx:12,lib/supabase/content.ts:45. -
Filesystem MDX pipeline for
/updates.
Update entries are read directly fromapp/updates/content/*.mdxvia synchronous filesystem reads inlib/updates.ts, then rendered in updates routes and included in sitemap generation.
Evidence:lib/updates.ts:11,lib/updates.ts:39,lib/updates.ts:45,app/updates/page.tsx:8,app/updates/[slug]/page.tsx:12,app/sitemap.ts:2,app/sitemap.ts:63.
Core automation behavior today:
- Admin auth and access control combine Supabase session checks (
middleware.ts) with server-sideadmin_usersverification in admin layout/login action.
Evidence:middleware.ts:5,middleware.ts:55,app/admin/(dashboard)/layout.tsx:15,app/admin/(dashboard)/layout.tsx:25,lib/supabase/admin-login-action.ts:27,lib/supabase/admin-login-action.ts:42. - Authoring and autosave are client-driven from the editor (
2sdebounce), writing through server actions to Supabase and syncing keywords.
Evidence:components/admin/editor.tsx:71,components/admin/editor.tsx:77,lib/supabase/admin-content.ts:102,lib/supabase/admin-content.ts:195. - Publish toggling updates
is_publishedinconsul_posts, but the editor's revalidation request shape differs from the revalidation API's expected webhook contract.
Evidence:components/admin/editor.tsx:155,components/admin/editor.tsx:168,app/api/revalidate/route.ts:56,app/api/revalidate/route.ts:77,app/api/revalidate/route.ts:87. - SEO/AI discovery surfaces (
sitemap.xml,robots.txt,llms.txt,llms-full.txt) are generated from mixed sources (Supabase + updates MDX + guides registry).
Evidence:app/sitemap.ts:2,app/sitemap.ts:72,app/sitemap.ts:118,app/robots.ts:7,app/llms.txt/route.ts:23,app/llms-full.txt/route.ts:19. - Guide gating uses cookie-based access (
guide_access_<slug>) with lead capture inguide_leadsand optional ZeroBounce verification; PDF export enforces the same cookie gate.
Evidence:lib/guide-leads.ts:41,lib/guide-leads.ts:109,lib/guide-leads.ts:124,app/guides/[slug]/page.tsx:66,app/api/guides/[slug]/pdf/route.ts:36.
Architecture Visual Breakdown
This section is a system-diagram-first walkthrough of how the content automation stack is assembled and how data moves.
Diagram A: System Context (External + Internal Boundaries)
Diagram B: Runtime Container View (Inside Next.js)
Diagram C: Source-of-Truth Map by Surface
Diagram D: Publish + Invalidation Pipeline (Current Behavior)
Diagram E: Public Read Path (Supabase vs Updates FS)
Step-by-Step Mental Model (Fast Understanding)
- Admins authenticate via Supabase auth and are authorized via
admin_users. - Editor writes content directly to Supabase tables (
consul_posts+ keyword tables). - Public SEO/content routes (blog/workflows/comparisons/for) read from Supabase only published rows.
- Updates routes are separate and read MDX files from repository filesystem.
- Sitemap is hybrid: combines Supabase content, updates MDX, authors, and guides registry.
- Revalidation route is webhook-shaped; editor-triggered revalidate payload currently uses a different contract.
- Guides use cookie-gated access with optional lead persistence and optional email verification.
- PDF generation for guides is gated by the same cookie and rendered through Playwright/Chromium.
System Boundary and Components
1) Admin CMS Subsystem
Scope files
app/admin/*components/admin/*lib/supabase/admin-auth.tslib/supabase/admin-login-action.tslib/supabase/admin-content.ts
Responsibilities
- Authenticate editors through Supabase auth.
- Gate admin dashboard to
admin_users. - List/filter content records.
- Create/update/delete posts.
- Toggle publish state.
- Manage metadata/keywords.
- Surface local validation metrics in editor UI.
Key implementation anchors
- Admin route/session guard:
middleware.ts:5,middleware.ts:64. - Dashboard-level
admin_userscheck:app/admin/(dashboard)/layout.tsx:22-32. - Login action with optional admin list verification:
lib/supabase/admin-login-action.ts:33-51. - CRUD server actions on
consul_posts:lib/supabase/admin-content.ts:58-157. - Keyword synchronization workflow:
lib/supabase/admin-content.ts:195-214. - Editor autosave + publish toggle UI behavior:
components/admin/editor.tsx:71-179.
2) Content Read Layer (Supabase)
Scope file
lib/supabase/content.ts
Responsibilities
- Read published content by type from
consul_posts. - Fetch post+author+keywords in joined queries.
- Convert DB records to route/component-facing typed objects.
- Provide slug lists for static params.
Key implementation anchors
- Published-only filtering:
lib/supabase/content.ts:48,lib/supabase/content.ts:78,lib/supabase/content.ts:115,lib/supabase/content.ts:220. - Joined read pattern (
consul_posts+consul_authors+ keyword junction):lib/supabase/content.ts:66-79,lib/supabase/content.ts:70-74. - High-level route-facing getters:
lib/supabase/content.ts:440-622. - Optional relationship RPC helper:
lib/supabase/content.ts:198-207.
3) Rendering Surfaces
Scope files
app/blog/*app/workflows/*app/comparisons/*app/for/*
Responsibilities
- Render list/detail pages using Supabase content.
- Generate metadata and static params from Supabase slugs.
- Apply ISR windows per surface.
- Render MDX content with shared component maps.
Key implementation anchors
- Blog:
app/blog/page.tsx:11,app/blog/page.tsx:16,app/blog/[slug]/page.tsx:12,app/blog/[slug]/page.tsx:31. - Workflows:
app/workflows/page.tsx:11,app/workflows/page.tsx:16,app/workflows/[slug]/page.tsx:13,app/workflows/[slug]/page.tsx:29. - Comparisons:
app/comparisons/page.tsx:14,app/comparisons/page.tsx:17,app/comparisons/[slug]/page.tsx:14,app/comparisons/[slug]/page.tsx:23. - ICP:
app/for/[role]/page.tsx:12,app/for/[role]/page.tsx:19,app/for/[role]/page.tsx:25-31. - Reserved static route override for fractional executives:
app/for/fractional-executives/page.tsx:41.
4) Filesystem Updates Subsystem
Scope files
lib/updates.tsapp/updates/*app/updates/content/*.mdx
Responsibilities
- Parse MDX update entries from disk.
- Generate list, detail pages, metadata, and static params.
Key implementation anchors
- Filesystem source root:
lib/updates.ts:11. - Sync file reads/parsing:
lib/updates.ts:39-47. - Updates list route usage:
app/updates/page.tsx:8,app/updates/page.tsx:28. - Updates detail route usage:
app/updates/[slug]/page.tsx:12,app/updates/[slug]/page.tsx:21-23,app/updates/[slug]/page.tsx:136-137.
5) SEO / AI Discovery Surfaces
Scope files
app/sitemap.tsapp/robots.tsapp/llms.txt/route.tsapp/llms-full.txt/route.tsapp/api/indexnow/route.ts
Responsibilities
- Publish crawl/index surface metadata across search and AI channels.
- Build sitemap from mixed dynamic/static sources.
- Emit robots rules and sitemap pointer.
- Emit AI-readable text indexes.
- Provide an IndexNow submission endpoint.
Key implementation anchors
- Mixed-source sitemap generation:
app/sitemap.ts:2-11,app/sitemap.ts:63,app/sitemap.ts:72,app/sitemap.ts:118. - Robots policy:
app/robots.ts:7-40. - LLM index routes:
app/llms.txt/route.ts:23-30,app/llms-full.txt/route.ts:19-25. - IndexNow endpoint contract:
app/api/indexnow/route.ts:21,app/api/indexnow/route.ts:43,app/api/indexnow/route.ts:83.
6) Guide Gating Subsystem
Scope files
app/guides/*components/guides/*lib/guides.tslib/guide-leads.tsapp/api/guides/[slug]/pdf/route.ts
Responsibilities
- Define guide registry metadata and slugs.
- Gate content with cookie access.
- Capture lead records and optional email verification.
- Generate gated PDF output.
Key implementation anchors
- Guide registry and slug list:
lib/guides.ts:17,lib/guides.ts:57,lib/guides.ts:61. - Gate cookie set + lead upsert:
lib/guide-leads.ts:109,lib/guide-leads.ts:124. - Route-level gate enforcement:
app/guides/[slug]/page.tsx:65-70. - PDF endpoint gate enforcement:
app/api/guides/[slug]/pdf/route.ts:35-39.
Data Model and Environment Contract
Content Entities and Joins (from code usage)
| Entity | Role in flow | Read/Write paths | Join/relationship behavior |
|---|---|---|---|
consul_posts | Canonical storage for blog/workflow/comparison/icp/update records | Read: lib/supabase/content.ts:45, lib/supabase/content.ts:66, lib/supabase/content.ts:107; Write: lib/supabase/admin-content.ts:65, lib/supabase/admin-content.ts:135, lib/supabase/admin-content.ts:151 | Linked to authors and keywords; filtered to published for public reads (eq("is_published", true)) |
consul_authors | Author attribution | Read in joins: lib/supabase/content.ts:70, lib/supabase/content.ts:111; Admin author list: lib/supabase/admin-content.ts:173 | Alias join author:consul_authors(*) |
consul_keywords | Keyword dimension table | Upsert/write: lib/supabase/admin-content.ts:203 | Linked through consul_post_keywords |
consul_post_keywords | Post-keyword junction | Read: lib/supabase/content.ts:71, lib/supabase/admin-content.ts:184; Write sync: lib/supabase/admin-content.ts:199, lib/supabase/admin-content.ts:210 | Ordered keyword extraction (position) in read layer |
consul_post_relationships | Cross-content relationship graph | Indirect read via RPC helper: lib/supabase/content.ts:198-201 | Not directly consumed in rendering routes in current code path |
admin_users | Admin authorization list | Read in login/dashboard checks: lib/supabase/admin-login-action.ts:42, app/admin/(dashboard)/layout.tsx:25 | Secondary check after auth session |
guide_leads | Guide lead capture sink | Upsert in server action: lib/guide-leads.ts:124 | Access cookie is granted regardless of upsert success (lib/guide-leads.ts:109, lib/guide-leads.ts:141-151) |
Environment Variable Contract (as implemented)
| Variable | Subsystem | Consumed by | Behavior notes |
|---|---|---|---|
GENERATIVE_SUPABASE_URL | Public content read + guides lead writes | lib/supabase/client.ts:12, lib/guide-leads.ts:8 | Used with anon key for public marketing content access path |
GENERATIVE_SUPABASE_ANON_KEY | Public content read + guides | lib/supabase/client.ts:13, lib/guide-leads.ts:9 | Required for Supabase read client and guide lead upsert client |
NEXT_PUBLIC_SUPABASE_URL | Admin auth + middleware + admin service client | middleware.ts:14, lib/supabase/admin-auth.ts:13, lib/supabase/admin-login-action.ts:11 | Separate naming from GENERATIVE_* content client |
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY | Admin auth/session fallback key | middleware.ts:15, lib/supabase/admin-auth.ts:14, lib/supabase/admin-auth.ts:44 | Also fallback key for createAdminServiceClient if service role missing |
SUPABASE_SERVICE_ROLE_KEY | Admin elevated writes / admin list validation | lib/supabase/admin-auth.ts:38-45, lib/supabase/admin-login-action.ts:34-39 | Optional in code; fallback path remains available |
REVALIDATION_SECRET | Revalidation API auth | app/api/revalidate/route.ts:78, app/api/revalidate/route.ts:153 | Used by webhook header and GET query secret |
NEXT_PUBLIC_REVALIDATE_SECRET | Admin editor attempted revalidation call | components/admin/editor.tsx:172 | Sent as Authorization bearer, not as x-webhook-secret |
INDEXNOW_KEY | IndexNow endpoint | app/api/indexnow/route.ts:21, app/api/indexnow/route.ts:77 | Defaults to consul-indexnow-key when absent |
ZEROBOUNCE_API | Guide email verification | lib/guide-leads.ts:41 | Optional; verification bypasses if unset |
Naming/Contract Mismatches Present in Repo
- Revalidation naming mismatch across code/docs:
REVALIDATION_SECRET(API route) vsNEXT_PUBLIC_REVALIDATE_SECRET(editor) vsREVALIDATE_SECRET(README).
Evidence:app/api/revalidate/route.ts:78,components/admin/editor.tsx:172,README.md:247. - README lists
GENERATIVE_SUPABASE_SERVICE_KEY, but code paths shown in this audit consumeSUPABASE_SERVICE_ROLE_KEYfor admin writes andGENERATIVE_SUPABASE_ANON_KEYfor content read.
Evidence:README.md:246,lib/supabase/admin-auth.ts:44,lib/supabase/client.ts:13.
End-to-End Flow Maps
Flow 1: Admin Login and Authorization Gate
Trigger: User opens /admin/login and submits credentials (app/admin/login/page.tsx:14-29).
Function boundaries: middleware.ts + loginAction + admin dashboard layout checks.
Data stores: Supabase auth session + admin_users table.
Cache behavior: No ISR/cache mutation; session and DB read checks only.
Output routes: /admin/login or /admin.
Step breakdown
- Middleware checks session on all
/admin/*paths and redirects unauthenticated users to/admin/login(middleware.ts:5-10,middleware.ts:57-59). - Login action performs
signInWithPassword(lib/supabase/admin-login-action.ts:27). - If
SUPABASE_SERVICE_ROLE_KEYexists, login action checksadmin_usersby normalized email (lib/supabase/admin-login-action.ts:34-45). - Dashboard layout repeats
admin_usersmembership check before rendering protected pages (app/admin/(dashboard)/layout.tsx:22-32).
Flow 2: Draft/Edit/Autosave Content in Admin Editor
Trigger: Editor state changes in title/content/metadata.
Function boundaries: components/admin/editor.tsx -> lib/supabase/admin-content.ts.
Data stores: consul_posts, consul_keywords, consul_post_keywords.
Cache behavior: No direct cache invalidation on autosave.
Output routes: Admin editor route remains in-place (/admin/editor/[id]).
Step breakdown
- Editor updates local state and schedules autosave with 2-second debounce (
components/admin/editor.tsx:71-79). - Existing posts call
updatePost; new posts callcreatePostthen route-replace to/admin/editor/{id}(components/admin/editor.tsx:107-150). - Server actions write post metadata/content and computed word/reading-time fields (
lib/supabase/admin-content.ts:61-85,lib/supabase/admin-content.ts:110-135). - Keyword sync deletes existing junction rows, upserts keywords, then upserts post-keyword junctions (
lib/supabase/admin-content.ts:199-211).
Flow 3: Publish/Unpublish and Cache Invalidation
Trigger: Publish button in editor (components/admin/editor.tsx:203-212).
Function boundaries: handlePublish -> handleSave -> updatePost + app/api/revalidate/route.ts.
Data stores: consul_posts.
Cache behavior: Path-based invalidation via revalidatePath when route receives valid webhook-authenticated payload.
Output routes: Affected public paths (/blog/*, /workflows/*, /comparisons/*, /for/*) and list pages.
Step breakdown
handlePublishflips localis_publishedand invokeshandleSave(components/admin/editor.tsx:155-160).- Save persists publish fields through
updatePost(lib/supabase/admin-content.ts:117-121). - Editor then sends
POST /api/revalidatewithAuthorizationheader and{ tag }body (components/admin/editor.tsx:168-175). - Revalidate route expects
x-webhook-secretand a webhook payload containingtype/table/record/old_record; it filters totable === "consul_posts"(app/api/revalidate/route.ts:56-87). - On valid webhook requests, route computes paths and calls
revalidatePathfor detail and list pages (app/api/revalidate/route.ts:99-121). - Manual GET fallback exists with
?secret=...&path=...(app/api/revalidate/route.ts:147-166).
Flow 4: Public Read/Render + ISR by Route
Trigger: Public HTTP request to content routes.
Function boundaries: Route files in app/* call lib/supabase/content.ts getters.
Data stores: consul_posts + joined author/keyword tables.
Cache behavior: ISR on each route according to local revalidate constant.
Output routes: /blog, /blog/[slug], /workflows, /workflows/[slug], /comparisons, /comparisons/[slug], /for/[role].
Route ISR windows and slug generation
| Route | Source getter(s) | ISR | Static params source |
|---|---|---|---|
/blog | getAllBlogPosts, getPillarPosts | 1800 sec | n/a |
/blog/[slug] | getBlogPostBySlug | 3600 sec | getAllBlogSlugs |
/workflows | getAllWorkflows | 1800 sec | n/a |
/workflows/[slug] | getWorkflowBySlug | 3600 sec | getAllWorkflowSlugs |
/comparisons | getAllComparisons | 86400 sec | n/a |
/comparisons/[slug] | getComparisonBySlug | 86400 sec | getAllComparisonSlugs |
/for/[role] | getICPPageBySlug, getAllICPPages | 86400 sec | getAllICPSlugs (excluding fractional-executives) |
Evidence: app/blog/page.tsx:16, app/blog/[slug]/page.tsx:22, app/workflows/page.tsx:16, app/workflows/[slug]/page.tsx:23, app/comparisons/page.tsx:17, app/comparisons/[slug]/page.tsx:23, app/for/[role]/page.tsx:19, app/for/[role]/page.tsx:25-30.
Flow 5: Filesystem Updates (MDX Changelog)
Trigger: Request to /updates or /updates/[slug].
Function boundaries: app/updates/* routes -> lib/updates.ts.
Data store: Filesystem MDX, not Supabase for updates route rendering.
Cache behavior: Static params for detail pages from file slugs; no route-level ISR constant declared in updates routes.
Output routes: /updates, /updates/[slug].
Step breakdown
- Updates utility sets source directory to
app/updates/content(lib/updates.ts:11). - Utility reads and parses each
.mdxfile synchronously (lib/updates.ts:39-47). /updatespage loads full list withgetAllUpdates(app/updates/page.tsx:28)./updates/[slug]route builds static params fromgetAllUpdateSlugsand renders withgetUpdateBySlug(app/updates/[slug]/page.tsx:21-23,app/updates/[slug]/page.tsx:37,app/updates/[slug]/page.tsx:136).
Flow 6: Guide Lead Capture, Cookie Gate, and PDF
Trigger: Access to /guides/[slug] and optional PDF download action.
Function boundaries: Route gate check, client form, server action, PDF API route.
Data stores: Cookie store + optional guide_leads upsert.
Cache behavior: Cookie-gated, user-specific access behavior; PDF response has private cache header.
Output routes: /guides/[slug], /api/guides/[slug]/pdf.
Step breakdown
- Guide page checks for
guide_access_<slug>cookie and gates content if absent (app/guides/[slug]/page.tsx:65-70). - Form submits to
submitGuideLeadserver action (components/guides/GuideGateForm.tsx:34). - Server action optionally verifies email with ZeroBounce (
lib/guide-leads.ts:41-74), sets gate cookie (lib/guide-leads.ts:109-115), and upserts intoguide_leads(lib/guide-leads.ts:124-139). - Content access can be granted even if DB write fails because cookie is set first (
lib/guide-leads.ts:141-151). - PDF route enforces same cookie and uses Playwright/Chromium rendering pipeline (
app/api/guides/[slug]/pdf/route.ts:35-39,app/api/guides/[slug]/pdf/route.ts:42-53,app/api/guides/[slug]/pdf/route.ts:113-140).
Automation Coverage Matrix
| Subsystem | Automated Trigger | Manual Step | Cache Behavior | Source of Truth | Primary Files |
|---|---|---|---|---|---|
| Admin route protection | Any /admin/* request | User login credentials | None | Supabase auth session + admin_users | middleware.ts, app/admin/(dashboard)/layout.tsx, lib/supabase/admin-login-action.ts |
| Admin autosave | Metadata/content state change | Editor modifies content | No public cache invalidation on autosave | consul_posts | components/admin/editor.tsx, lib/supabase/admin-content.ts |
| Keyword sync | createPost/updatePost with keywords | Keyword entry in metadata sidebar | N/A | consul_keywords, consul_post_keywords | lib/supabase/admin-content.ts |
| Publish toggle | Publish button click | Author decides publish/unpublish | Depends on revalidation path being valid | consul_posts.is_published | components/admin/editor.tsx, lib/supabase/admin-content.ts |
| Supabase webhook revalidation | DB webhook POST event | Supabase webhook configuration/secret setup | revalidatePath on detail and list paths | Next route cache + ISR pages | app/api/revalidate/route.ts |
| Manual revalidation | GET /api/revalidate?secret=&path= | Operator provides secret/path | revalidatePath(path) | Next route cache + ISR pages | app/api/revalidate/route.ts |
| Public content rendering | Visitor requests page | None | ISR windows by route | consul_posts (+ joins) | app/blog/*, app/workflows/*, app/comparisons/*, app/for/[role]/page.tsx, lib/supabase/content.ts |
| Updates rendering | Request to updates routes | Author commits MDX file | Build/runtime file read path; no explicit ISR constant | app/updates/content/*.mdx | lib/updates.ts, app/updates/* |
| Sitemap generation | Request /sitemap.xml | None | Generated per request in route function | Mixed: Supabase + MDX + guide registry + authors | app/sitemap.ts |
| Robots generation | Request /robots.txt | None | Generated per request | Static config in code | app/robots.ts |
| LLM index generation | Request /llms.txt or /llms-full.txt | None | Cache-Control max-age/s-maxage 86400 | Supabase content | app/llms.txt/route.ts, app/llms-full.txt/route.ts |
| Guide gate + lead capture | Gate form submission | Visitor fills form fields | Cookie-based access state; not ISR-driven | Cookie + optional guide_leads row | components/guides/GuideGateForm.tsx, lib/guide-leads.ts, app/guides/[slug]/page.tsx |
| Guide PDF generation | Download PDF click | User must first pass gate | Private cache header on PDF response | Rendered print route output | app/api/guides/[slug]/pdf/route.ts |
| IndexNow endpoint | Direct POST to /api/indexnow | Caller must supply URL list | None | Endpoint payload; external IndexNow APIs | app/api/indexnow/route.ts |
Observed Architectural Drift (Non-Scored)
D1) README content-system description diverges from current runtime implementation
Observed drift
- README describes filesystem MDX as source of truth for blog/workflows/comparisons/ICP and a sync script workflow.
- Current route code for those surfaces reads from Supabase directly via
lib/supabase/content.ts.
Evidence anchors
- Docs expectation:
README.md:112,README.md:118-123,README.md:183,README.md:191. - Runtime implementation:
app/blog/page.tsx:11,app/workflows/page.tsx:11,app/comparisons/page.tsx:14,app/for/[role]/page.tsx:12,lib/supabase/content.ts:45. - Supporting repo signal: package scripts do not expose a sync command (
package.json:5-10), while the only checked-in script inscripts/is image generation (scripts/generate-blog-images.py:1-7).
D2) Revalidation contract mismatch between editor trigger, API route, and README examples
Observed drift
- Editor sends
Authorization: Bearer ...and{ tag }inPOST /api/revalidate. - Revalidate route expects
x-webhook-secretand a Supabase webhook payload shape (type/table/record/old_record), and only processestable === "consul_posts". - README's curl example also uses
Authorization+tag, matching editor behavior but not route behavior.
Evidence anchors
- Editor call shape:
components/admin/editor.tsx:168-175. - Route auth/payload contract:
app/api/revalidate/route.ts:56-87,app/api/revalidate/route.ts:77-81. - README example:
README.md:227-233,README.md:247.
D3) Mixed content sources are intentional in code but unevenly documented
Observed drift
- Blog/workflow/comparison/icp surfaces are Supabase-driven.
- Updates are filesystem-driven.
- Sitemap aggregates both, plus guides and authors.
/for/fractional-executivesis a static component route excluded from dynamic ICP slug generation.
Evidence anchors
- Supabase routes:
app/blog/page.tsx:11,app/workflows/page.tsx:11,app/comparisons/page.tsx:14,app/for/[role]/page.tsx:12. - Filesystem updates:
lib/updates.ts:11,app/updates/page.tsx:8. - Aggregated sitemap:
app/sitemap.ts:2-11,app/sitemap.ts:63,app/sitemap.ts:72,app/sitemap.ts:118. - Static ICP exception:
app/for/[role]/page.tsx:25-31,app/for/fractional-executives/page.tsx:41-63.
D4) Relationship-based related-content automation is partially wired in runtime
Observed drift
lib/supabase/content.tsexposes RPC-basedgetRelatedPostsand marksrelatedWorkflowsas relationship-populated.- Blog and workflow pages currently rely on local fallback/sibling logic rather than the relationship RPC path.
Evidence anchors
- Relationship helper and placeholder:
lib/supabase/content.ts:198-207,lib/supabase/content.ts:368. - Blog local related-posts logic:
app/blog/[slug]/page.tsx:90-115. - Workflow fallback to "other workflows" when
relatedWorkflowsis absent:app/workflows/[slug]/page.tsx:71-73.
D5) Validation scoring persistence path described in docs is not visible in current admin write path
Observed drift
- README states validation scores are computed and stored (including
consul_post_validations). - Admin write actions calculate word_count/reading_time and sync keywords, but do not write
seo_score,geo_score, or a validation table. - Dashboard table reads
seo_score/geo_scorefields and editor computes client-side validation.
Evidence anchors
- Docs claims:
README.md:171-191. - Admin write path fields:
lib/supabase/admin-content.ts:61-85,lib/supabase/admin-content.ts:110-135. - Types/UI reading score fields:
lib/supabase/admin-types.ts:27-29,components/admin/content-table.tsx:152-153. - Client-side scoring UI:
components/admin/validation-panel.tsx:29-33,components/admin/validation-panel.tsx:124-128.
D6) IndexNow endpoint exists as automation capability but is not wired into publish workflow in this repo
Observed drift
- An IndexNow API route is implemented.
- Publish action path in editor only invokes save +
/api/revalidate; no IndexNow invocation in that flow.
Evidence anchors
- IndexNow endpoint implementation:
app/api/indexnow/route.ts:43-131. - Publish flow implementation:
components/admin/editor.tsx:155-175.
Important Public APIs/Interfaces/Types
This audit phase introduces no code changes and no interface changes.
- No public API route contracts were modified.
- No TypeScript types/interfaces were modified.
- No database schema migrations were applied.
- Output is documentation-only:
/docs/audits/2026-02-28-content-automation-audit.md.
Validation Scenarios Applied to This Audit
- Every flow map includes all required fields:
- Trigger
- Function boundary
- Data store
- Cache behavior
- Output route
- Every content surface is mapped to one source model:
- Supabase-driven surfaces (
/blog*,/workflows*,/comparisons*,/for/[role]) - Filesystem-driven updates (
/updates*) - Hybrid/cookie-gated guide flow (
/guides/[slug]+guide_leads+ cookie gate)
-
Every environment variable listed in this report is traceable to a specific code reference in this repo (see Environment Contract and Appendix).
-
Every drift statement includes at least two evidence anchors (implementation and/or docs/config references).
-
No secret values are reproduced in this report; only variable names and code locations are documented.
Assumptions and Defaults
- Scope includes all content-adjacent systems in this repository (full stack).
- This report is architecture-only and does not rank findings by severity.
- Source of truth for this audit is repository code and checked-in docs only.
- System state captured as of 2026-02-28.
Appendix: Evidence Index
Claim-to-Evidence Mapping
| Claim ID | Claim | Evidence |
|---|---|---|
| C01 | Admin surfaces enforce both auth-session and admin_users membership checks | middleware.ts:5-10, middleware.ts:55-59, app/admin/(dashboard)/layout.tsx:22-32, lib/supabase/admin-login-action.ts:33-45 |
| C02 | Public content read layer fetches only published rows | lib/supabase/content.ts:48, lib/supabase/content.ts:78, lib/supabase/content.ts:115, lib/supabase/content.ts:220 |
| C03 | Supabase joined reads include authors and keyword junctions | lib/supabase/content.ts:70-74, lib/supabase/content.ts:241-245, lib/supabase/content.ts:504-508 |
| C04 | Admin editor autosave and manual save both write through server actions | components/admin/editor.tsx:71-79, components/admin/editor.tsx:104-150, lib/supabase/admin-content.ts:58-147 |
| C05 | Keyword synchronization is explicit delete-and-upsert logic | lib/supabase/admin-content.ts:195-214 |
| C06 | Publish flow updates is_published and attempts revalidation call | components/admin/editor.tsx:155-175, lib/supabase/admin-content.ts:117-121 |
| C07 | Revalidation route expects webhook contract (x-webhook-secret, table-filtered payload) | app/api/revalidate/route.ts:56-87, app/api/revalidate/route.ts:77-81 |
| C08 | Revalidation route performs path-level invalidation on content and index paths | app/api/revalidate/route.ts:99-121, app/api/revalidate/route.ts:165 |
| C09 | Blog/workflow/comparison/icp surfaces are Supabase-backed with ISR | app/blog/page.tsx:11, app/blog/page.tsx:16, app/workflows/page.tsx:11, app/workflows/page.tsx:16, app/comparisons/page.tsx:14, app/comparisons/page.tsx:17, app/for/[role]/page.tsx:12, app/for/[role]/page.tsx:19 |
| C10 | Updates surface is filesystem-backed MDX | lib/updates.ts:11, lib/updates.ts:39-47, app/updates/page.tsx:8, app/updates/[slug]/page.tsx:12 |
| C11 | Sitemap aggregates mixed sources (Supabase + updates + guides + authors) | app/sitemap.ts:2-11, app/sitemap.ts:63, app/sitemap.ts:72, app/sitemap.ts:118 |
| C12 | LLM index routes dynamically materialize Supabase content lists | app/llms.txt/route.ts:23-30, app/llms-full.txt/route.ts:19-25 |
| C13 | Guide gating uses cookie grant plus best-effort lead persistence | lib/guide-leads.ts:109-121, lib/guide-leads.ts:124-151, app/guides/[slug]/page.tsx:65-70 |
| C14 | Guide PDF generation enforces gate cookie and uses Playwright/Chromium | app/api/guides/[slug]/pdf/route.ts:35-39, app/api/guides/[slug]/pdf/route.ts:42-53, app/api/guides/[slug]/pdf/route.ts:113-140 |
| C15 | Revalidation env/contract naming differs across code/docs | app/api/revalidate/route.ts:78, components/admin/editor.tsx:172, README.md:247 |
| C16 | README describes sync-based filesystem source model not reflected in route imports | README.md:112, README.md:118-123, README.md:183, app/blog/page.tsx:11, app/workflows/page.tsx:11 |
| C17 | Relationship automation helper exists but route logic currently uses fallback-related selection | lib/supabase/content.ts:198-207, lib/supabase/content.ts:368, app/blog/[slug]/page.tsx:90-115, app/workflows/[slug]/page.tsx:71-73 |
| C18 | Validation scoring is documented as persisted but not set in current admin writes | README.md:171-191, lib/supabase/admin-content.ts:61-85, lib/supabase/admin-content.ts:110-135, lib/supabase/admin-types.ts:27-29, components/admin/content-table.tsx:152-153 |
| C19 | IndexNow endpoint capability exists independently of publish path | app/api/indexnow/route.ts:43-131, components/admin/editor.tsx:155-175 |
| C20 | Static fractional executives page is carved out from dynamic ICP generation | app/for/[role]/page.tsx:25-31, app/for/fractional-executives/page.tsx:41-63 |
File-by-File Reference List
app/admin/(dashboard)/layout.tsx:15-32app/admin/(dashboard)/page.tsx:15-21app/admin/login/page.tsx:14-29app/api/guides/[slug]/pdf/route.ts:23-39app/api/guides/[slug]/pdf/route.ts:42-53app/api/guides/[slug]/pdf/route.ts:113-140app/api/indexnow/route.ts:21-29app/api/indexnow/route.ts:43-131app/api/revalidate/route.ts:56-87app/api/revalidate/route.ts:99-121app/api/revalidate/route.ts:147-166app/blog/[slug]/page.tsx:12-17app/blog/[slug]/page.tsx:22app/blog/[slug]/page.tsx:90-115app/blog/page.tsx:11app/blog/page.tsx:16app/comparisons/[slug]/page.tsx:14-17app/comparisons/[slug]/page.tsx:23app/comparisons/page.tsx:14app/comparisons/page.tsx:17app/for/[role]/page.tsx:12app/for/[role]/page.tsx:19app/for/[role]/page.tsx:25-31app/for/fractional-executives/page.tsx:41-63app/guides/[slug]/page.tsx:21-23app/guides/[slug]/page.tsx:65-70app/llms-full.txt/route.ts:19-25app/llms.txt/route.ts:23-30app/robots.ts:7-40app/sitemap.ts:2-11app/sitemap.ts:63-69app/sitemap.ts:72-78app/sitemap.ts:118-124app/updates/[slug]/page.tsx:12app/updates/[slug]/page.tsx:21-23app/updates/[slug]/page.tsx:136-137app/updates/page.tsx:8app/workflows/[slug]/page.tsx:13-16app/workflows/[slug]/page.tsx:23app/workflows/[slug]/page.tsx:71-73app/workflows/page.tsx:11app/workflows/page.tsx:16components/admin/content-table.tsx:152-153components/admin/editor.tsx:71-79components/admin/editor.tsx:155-175components/guides/GuideGateForm.tsx:34lib/content-validation.ts:123-131lib/content-validation.ts:188-200lib/guide-leads.ts:41-74lib/guide-leads.ts:109-115lib/guide-leads.ts:124-151lib/supabase/admin-auth.ts:38-45lib/supabase/admin-content.ts:58-85lib/supabase/admin-content.ts:102-147lib/supabase/admin-content.ts:195-214lib/supabase/admin-login-action.ts:33-45lib/supabase/admin-types.ts:27-29lib/supabase/client.ts:12-15lib/supabase/content.ts:45-49lib/supabase/content.ts:66-79lib/supabase/content.ts:70-74lib/supabase/content.ts:198-207lib/supabase/content.ts:368lib/supabase/content.ts:440-442lib/supabase/content.ts:548-550lib/supabase/content.ts:575-577lib/supabase/content.ts:602-604lib/updates.ts:11lib/updates.ts:39-47middleware.ts:5-10middleware.ts:55-59package.json:5-10README.md:112README.md:118-123README.md:171-191README.md:227-233README.md:245-247CLAUDE.md:27CLAUDE.md:55scripts/generate-blog-images.py:1-7