Build pathway page components for: $ARGUMENTS
$ARGUMENTS should include:
- A pathway slug (e.g., "mdna", "discipleship", "metanoia") OR "all" for every pathway
- Optionally: "types-only" to generate types and hooks without building components
- Optionally: a specific section_type to build a single section (e.g., "hero", "faq")
- Empty — list available pathways from the database and ask what to build
Purpose
This skill builds production React components for pathway detail pages using content stored in the pathway_sections database table. It generates the full type-safe chain: database types → hooks → section components → page composition. Every component consumes data through hooks (Layer 5), never direct API calls.
Data Source: pathway_sections Table
Each pathway has 13 section rows in the pathway_sections table:
| order | section_type | What it stores |
|---|---|---|
| 0 | hero |
Title, subtitle, hero quote, hero image brief |
| 1 | core_question |
The pathway's central question |
| 2 | anchor_quote |
Primary Hirsch quote with citation |
| 3 | overview |
Prose overview of the pathway |
| 4 | model |
Framework/model with titled elements |
| 5 | scripture |
Biblical foundations with verse references |
| 6 | case_studies |
Real-world examples with narratives |
| 7 | faq |
Question/answer pairs |
| 8 | practices |
Formation exercises with steps |
| 9 | reflection |
Guided reflection prompts |
| 10 | courses |
Related course connections |
| 11 | resources |
Books, articles, further reading |
| 12 | glossary |
Key term definitions |
Each row has:
body(text) — main markdown/prose contentstructured(JSONB) — typed metadata specific to the section_type
Critical Rules
- Components consume hooks (Layer 5), never call APIs or services directly.
- Use semantic CSS classes only —
bg-primary,text-muted-foreground, etc. Never hardcode hex/rgb. - Use shadcn/ui components —
Card,Button,Accordion,Badge,Tabs, etc. Never raw HTML with inline styles. - Use
tenantConfigfor any text that could vary per tenant. Never hardcode tenant strings. - Push "use client" to leaf components only. Keep page.tsx as a Server Component where possible.
- Follow the design chain: Tokens → Tailwind → Radix/shadcn → Domain components → Page.
- NEVER modify existing Layer 1–5 files (schema, Zod, services, routes, hooks in
simplified/). If something is missing in those layers, flag it. - Archive before overwriting. If a pathway page or component already exists, archive it first (see Phase 1).
Phase 0 — Pre-Flight Checks
Before any build work:
- Read
src/types/pathway.tsto understand existing types (StudioPathway,PathwaySections, sub-types). - Read
src/hooks/usePathways.tsto understand existing hooks. - Read
src/app/globals.cssandsrc/lib/config/tenant.config.tsfor tokens and tenant config. - Check if
src/types/pathway-section.tsexists — if not, it needs to be created (Phase 2). - Check if
src/hooks/usePathwaySections.tsexists — if not, it needs to be created (Phase 2). - Check if
server/routes/pathway-sections.routes.tsexists — if not, it needs to be created (Phase 2). - Verify the target pathway exists by checking the database or the pathway list. If building for a specific slug, confirm it has section rows.
Phase 1 — Archive Existing Implementation
MANDATORY when a pathway page or component already exists at the target path.
Archive protocol:
src/app/(public)/pathways/[slug]/page.tsx → page-old.tsx
src/components/pathway/[component].tsx → [component]-old.tsx
Add archive comment:
// ARCHIVED: [date] — replaced by pathway-builder skill
// This file is preserved for reference. Delete when verified.
If -old already exists, use -old-2, -old-3, etc.
Phase 2 — Types, Hook, and Route Generation
Step 1 — Create src/types/pathway-section.ts
Define types for the pathway_sections table row and per-section structured data:
// --- Section Types (matching pathway_sections.section_type CHECK constraint) ---
export type PathwaySectionType =
| 'hero'
| 'core_question'
| 'anchor_quote'
| 'overview'
| 'model'
| 'scripture'
| 'case_studies'
| 'faq'
| 'practices'
| 'reflection'
| 'courses'
| 'resources'
| 'glossary';
// --- Per-section structured JSONB shapes ---
export interface HeroStructured {
subtitle?: string;
hero_quote?: string;
hero_quote_source?: string;
hero_image_brief?: string;
}
export interface CoreQuestionStructured {
question?: string;
}
export interface AnchorQuoteStructured {
quote?: string;
source?: string;
book?: string;
}
export interface OverviewStructured {
summary?: string;
}
export interface ModelElement {
title: string;
body: string;
}
export interface ModelStructured {
elements?: ModelElement[];
intro?: string;
}
export interface ScriptureRef {
reference: string;
text: string;
connection?: string;
}
export interface ScriptureStructured {
refs?: ScriptureRef[];
}
export interface CaseStudy {
title: string;
description: string;
context?: string;
lessons?: string[];
}
export interface CaseStudiesStructured {
studies?: CaseStudy[];
}
export interface FaqItem {
question: string;
answer: string;
}
export interface FaqStructured {
items?: FaqItem[];
}
export interface PracticeStep {
title: string;
description: string;
duration?: string;
}
export interface Practice {
title: string;
intro?: string;
steps?: PracticeStep[];
}
export interface PracticesStructured {
practices?: Practice[];
}
export interface ReflectionStructured {
prompts?: string[];
}
export interface CourseLink {
title: string;
slug: string;
description?: string;
}
export interface CoursesStructured {
courses?: CourseLink[];
}
export interface Resource {
title: string;
author?: string;
type?: 'book' | 'article' | 'video' | 'podcast';
description?: string;
url?: string;
}
export interface ResourcesStructured {
resources?: Resource[];
}
export interface GlossaryTerm {
term: string;
definition: string;
}
export interface GlossaryStructured {
terms?: GlossaryTerm[];
}
// --- Union of all structured types ---
export type SectionStructured =
| HeroStructured
| CoreQuestionStructured
| AnchorQuoteStructured
| OverviewStructured
| ModelStructured
| ScriptureStructured
| CaseStudiesStructured
| FaqStructured
| PracticesStructured
| ReflectionStructured
| CoursesStructured
| ResourcesStructured
| GlossaryStructured;
// --- Database row shape ---
export interface PathwaySection {
id: string;
pathway_id: string;
section_type: PathwaySectionType;
title: string | null;
slug: string;
body: string | null;
structured: SectionStructured;
order_index: number;
status: string;
organization_id: string | null;
created_at: string;
updated_at: string;
}
// --- Typed section helpers ---
export type TypedSection<T extends PathwaySectionType, S extends SectionStructured> = PathwaySection & {
section_type: T;
structured: S;
};
export type HeroSection = TypedSection<'hero', HeroStructured>;
export type CoreQuestionSection = TypedSection<'core_question', CoreQuestionStructured>;
export type AnchorQuoteSection = TypedSection<'anchor_quote', AnchorQuoteStructured>;
export type OverviewSection = TypedSection<'overview', OverviewStructured>;
export type ModelSection = TypedSection<'model', ModelStructured>;
export type ScriptureSection = TypedSection<'scripture', ScriptureStructured>;
export type CaseStudiesSection = TypedSection<'case_studies', CaseStudiesStructured>;
export type FaqSection = TypedSection<'faq', FaqStructured>;
export type PracticesSection = TypedSection<'practices', PracticesStructured>;
export type ReflectionSection = TypedSection<'reflection', ReflectionStructured>;
export type CoursesSection = TypedSection<'courses', CoursesStructured>;
export type ResourcesSection = TypedSection<'resources', ResourcesStructured>;
export type GlossarySection = TypedSection<'glossary', GlossaryStructured>;
Step 2 — Create src/hooks/usePathwaySections.ts
import { useQuery } from '@tanstack/react-query';
import { apiFetch } from '../api/client';
import type { PathwaySection, PathwaySectionType } from '../types/pathway-section';
/** Fetch all sections for a pathway, ordered by order_index */
export function usePathwaySections(pathwayId: string | null) {
return useQuery({
queryKey: ['pathway-sections', pathwayId],
queryFn: () => apiFetch<PathwaySection[]>(`/pathway-sections/${pathwayId}`),
enabled: !!pathwayId,
});
}
/** Fetch a single section by pathway + section_type */
export function usePathwaySection(pathwayId: string | null, sectionType: PathwaySectionType) {
return useQuery({
queryKey: ['pathway-sections', pathwayId, sectionType],
queryFn: () => apiFetch<PathwaySection>(`/pathway-sections/${pathwayId}/${sectionType}`),
enabled: !!pathwayId,
});
}
/** Helper to extract a typed section from the full sections array */
export function findSection<T extends PathwaySectionType>(
sections: PathwaySection[] | undefined,
type: T
): PathwaySection | undefined {
return sections?.find((s) => s.section_type === type);
}
Step 3 — Create server/routes/pathway-sections.routes.ts
import { Router } from 'express';
import { eq, and, asc } from 'drizzle-orm';
import { getDb, schema } from '../db';
import { getTenantOrgId } from '../tenant';
export const pathwaySectionsRouter = Router();
// Get all sections for a pathway (by pathway_id), ordered by order_index
pathwaySectionsRouter.get('/:pathwayId', async (req, res) => {
try {
const orgId = getTenantOrgId();
const { pathwayId } = req.params;
const rows = await getDb()
.select()
.from(schema.pathwaySections)
.where(
and(
eq(schema.pathwaySections.pathway_id, pathwayId),
eq(schema.pathwaySections.organization_id, orgId)
)
)
.orderBy(asc(schema.pathwaySections.order_index));
res.json(rows);
} catch (err) {
console.error('GET /api/pathway-sections/:pathwayId error:', err);
res.status(500).json({ error: 'Failed to fetch pathway sections' });
}
});
// Get single section by pathway_id + section_type
pathwaySectionsRouter.get('/:pathwayId/:sectionType', async (req, res) => {
try {
const orgId = getTenantOrgId();
const { pathwayId, sectionType } = req.params;
const [row] = await getDb()
.select()
.from(schema.pathwaySections)
.where(
and(
eq(schema.pathwaySections.pathway_id, pathwayId),
eq(schema.pathwaySections.section_type, sectionType),
eq(schema.pathwaySections.organization_id, orgId)
)
);
if (!row) {
res.status(404).json({ error: 'Section not found' });
return;
}
res.json(row);
} catch (err) {
console.error('GET /api/pathway-sections/:pathwayId/:sectionType error:', err);
res.status(500).json({ error: 'Failed to fetch pathway section' });
}
});
Register the route in the server's main router file alongside the existing pathwaysRouter:
import { pathwaySectionsRouter } from './routes/pathway-sections.routes';
app.use('/api/pathway-sections', pathwaySectionsRouter);
Types-only mode:
If the user specified "types-only", stop after this phase and report what was generated.
Phase 3 — Component Decomposition Plan
Section-to-component mapping:
| section_type | Component | shadcn/ui primitives | Client? | Key data fields |
|---|---|---|---|---|
hero |
PathwayHero |
— | Server | title, subtitle, hero_quote, hero_image_brief |
core_question |
PathwayCoreQuestion |
Card | Server | question from structured |
anchor_quote |
PathwayAnchorQuote |
Card | Server | quote, source, book |
overview |
PathwayOverview |
— | Server | body (markdown prose) |
model |
PathwayModel |
Card, Tabs | Client | elements[] with title/body |
scripture |
PathwayScripture |
Accordion | Client | refs[] with reference/text |
case_studies |
PathwayCaseStudies |
Card | Client | studies[] |
faq |
PathwayFaq |
Accordion | Client | items[] Q&A |
practices |
PathwayPractices |
Card, Badge | Client | practices[] with steps |
reflection |
PathwayReflection |
Card | Server | prompts[] |
courses |
PathwayCourses |
Card, Button | Client | courses[] links |
resources |
PathwayResources |
Card, Badge | Server | resources[] |
glossary |
PathwayGlossary |
Accordion | Client | terms[] |
Component directory structure:
src/components/pathway/
├── pathway-hero.tsx
├── pathway-core-question.tsx
├── pathway-anchor-quote.tsx
├── pathway-overview.tsx
├── pathway-model.tsx
├── pathway-scripture.tsx
├── pathway-case-studies.tsx
├── pathway-faq.tsx
├── pathway-practices.tsx
├── pathway-reflection.tsx
├── pathway-courses.tsx
├── pathway-resources.tsx
├── pathway-glossary.tsx
└── pathway-page-shell.tsx # Client wrapper that fetches all sections
Present the plan to the user and wait for approval before Phase 4.
Phase 4 — Build Components
Build bottom-up: individual section components first, then the page shell.
Component pattern:
Each section component receives its typed section data as a prop — it does NOT fetch data itself. The page shell fetches all sections via usePathwaySections and distributes them.
// Example: src/components/pathway/pathway-faq.tsx
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
import type { FaqSection } from "@/types/pathway-section";
interface PathwayFaqProps {
section: FaqSection;
}
export function PathwayFaq({ section }: PathwayFaqProps) {
const items = section.structured.items ?? [];
if (items.length === 0) return null;
return (
<section className="space-y-6">
<h2 className="text-2xl font-bold text-foreground">
{section.title ?? "Frequently Asked Questions"}
</h2>
<Accordion type="single" collapsible className="w-full">
{items.map((item, i) => (
<AccordionItem key={i} value={`faq-${i}`}>
<AccordionTrigger>{item.question}</AccordionTrigger>
<AccordionContent>{item.answer}</AccordionContent>
</AccordionItem>
))}
</Accordion>
</section>
);
}
Page shell pattern:
// src/components/pathway/pathway-page-shell.tsx
"use client";
import { usePathway } from "@/hooks/usePathways";
import { usePathwaySections, findSection } from "@/hooks/usePathwaySections";
import type { PathwaySection } from "@/types/pathway-section";
import { PathwayHero } from "./pathway-hero";
import { PathwayCoreQuestion } from "./pathway-core-question";
import { PathwayAnchorQuote } from "./pathway-anchor-quote";
import { PathwayOverview } from "./pathway-overview";
import { PathwayModel } from "./pathway-model";
import { PathwayScripture } from "./pathway-scripture";
import { PathwayCaseStudies } from "./pathway-case-studies";
import { PathwayFaq } from "./pathway-faq";
import { PathwayPractices } from "./pathway-practices";
import { PathwayReflection } from "./pathway-reflection";
import { PathwayCourses } from "./pathway-courses";
import { PathwayResources } from "./pathway-resources";
import { PathwayGlossary } from "./pathway-glossary";
interface PathwayPageShellProps {
pathwayId: string;
}
export function PathwayPageShell({ pathwayId }: PathwayPageShellProps) {
const { data: pathway, isLoading: pathwayLoading } = usePathway(pathwayId);
const { data: sections, isLoading: sectionsLoading } = usePathwaySections(pathwayId);
if (pathwayLoading || sectionsLoading) {
return <PathwayPageSkeleton />;
}
if (!pathway || !sections) {
return <div className="text-center py-12 text-muted-foreground">Pathway not found</div>;
}
const hero = findSection(sections, 'hero');
const coreQuestion = findSection(sections, 'core_question');
const anchorQuote = findSection(sections, 'anchor_quote');
const overview = findSection(sections, 'overview');
const model = findSection(sections, 'model');
const scripture = findSection(sections, 'scripture');
const caseStudies = findSection(sections, 'case_studies');
const faq = findSection(sections, 'faq');
const practices = findSection(sections, 'practices');
const reflection = findSection(sections, 'reflection');
const courses = findSection(sections, 'courses');
const resources = findSection(sections, 'resources');
const glossary = findSection(sections, 'glossary');
return (
<div className="space-y-16">
{hero && <PathwayHero section={hero} />}
{coreQuestion && <PathwayCoreQuestion section={coreQuestion} />}
{anchorQuote && <PathwayAnchorQuote section={anchorQuote} />}
{overview && <PathwayOverview section={overview} />}
{model && <PathwayModel section={model} />}
{scripture && <PathwayScripture section={scripture} />}
{caseStudies && <PathwayCaseStudies section={caseStudies} />}
{faq && <PathwayFaq section={faq} />}
{practices && <PathwayPractices section={practices} />}
{reflection && <PathwayReflection section={reflection} />}
{courses && <PathwayCourses section={courses} />}
{resources && <PathwayResources section={resources} />}
{glossary && <PathwayGlossary section={glossary} />}
</div>
);
}
function PathwayPageSkeleton() {
return (
<div className="space-y-16 animate-pulse">
<div className="h-64 bg-muted rounded-lg" />
<div className="h-24 bg-muted rounded-lg max-w-2xl mx-auto" />
<div className="space-y-4">
<div className="h-6 bg-muted rounded w-1/3" />
<div className="h-4 bg-muted rounded w-full" />
<div className="h-4 bg-muted rounded w-5/6" />
</div>
</div>
);
}
Rules for every component:
- Props, not fetches — section components receive their
PathwaySectionas a prop - Null-safe — always default structured fields (
section.structured.items ?? []) - Return null for empty — if a section has no meaningful content, render nothing
- Semantic tokens —
text-foreground,bg-card,border-border, never hex values - shadcn/ui — Accordion for expandable lists, Card for contained content, Badge for labels, Tabs for multi-view
- Markdown rendering — for
bodyfields containing markdown, use the project's markdown renderer ordangerouslySetInnerHTMLwith sanitization. Check if@/components/ui/markdownor a similar utility exists first. - Type assertions — when passing to typed section components, assert the type:
section={hero as HeroSection}
Phase 5 — Compose the Page
Create the page route that renders the shell:
// src/app/(public)/pathways/[slug]/page.tsx
import { PathwayPageShell } from "@/components/pathway/pathway-page-shell";
interface PathwayPageProps {
params: { slug: string };
}
export default function PathwayPage({ params }: PathwayPageProps) {
// The shell needs a pathway_id, but the route gives us a slug.
// Option A: Pass slug and let shell resolve it via usePathways
// Option B: Server-side lookup to get the ID
// Use the approach that matches the project's existing pattern.
return <PathwayPageShell pathwaySlug={params.slug} />;
}
If the page route already exists, the page shell must coexist or replace only the sections portion. Check the existing page before writing.
Phase 6 — Verification Checklist
After building, verify:
- Types file
src/types/pathway-section.tsexists with all 13 section structured types - Hook file
src/hooks/usePathwaySections.tsexists withusePathwaySectionsandusePathwaySection - Server route
server/routes/pathway-sections.routes.tsexists and is registered - All 13 section components exist in
src/components/pathway/ - Page shell fetches via hooks, never direct API calls
- No hardcoded colors — only semantic Tailwind classes
- No hardcoded tenant strings — uses
tenantConfigwhere appropriate - All components use shadcn/ui primitives
- Every section component handles empty/null data gracefully
- TypeScript compiles without errors (
pnpm tsc --noEmit) - No existing files were overwritten without archiving
- Drizzle schema includes
pathwaySectionstable (inshared/alan-hirsch/database/schema.ts)
Report the checklist results to the user.
Phase 7 — Build Report
## Pathway Builder — Complete
### Pathway
- **Slug:** [pathway slug]
- **Sections built:** [count] / 13
### New Files Created
1. `src/types/pathway-section.ts` — Section types and structured JSONB shapes
2. `src/hooks/usePathwaySections.ts` — TanStack Query hooks
3. `server/routes/pathway-sections.routes.ts` — Express route
4. `src/components/pathway/pathway-hero.tsx` — Hero section
5. `src/components/pathway/pathway-core-question.tsx` — Core question
6. ... [list all]
13. `src/components/pathway/pathway-glossary.tsx` — Glossary
14. `src/components/pathway/pathway-page-shell.tsx` — Client shell
15. `src/app/(public)/pathways/[slug]/page.tsx` — Page route
### Archived
- [List any archived files, or "None — fresh build"]
### Data Flow
pathway_sections table → GET /api/pathway-sections/:pathwayId → usePathwaySections(pathwayId) → PathwayPageShell → findSection(sections, 'hero') → → findSection(sections, 'faq') → → ... (13 sections)
### Flagged Issues
- [Any missing dependencies, schema gaps, or items needing manual attention]
### Prerequisites
- [ ] Run migration `001_create_pathway_sections.sql` in Supabase
- [ ] Run seed `002_seed_pathway_sections.sql` to populate content
- [ ] Register route in server entry: `app.use('/api/pathway-sections', pathwaySectionsRouter)`
### Next Steps
- Run `pnpm dev` and navigate to `/pathways/[slug]`
- Verify all 13 sections render with database content
- Run `/design-audit` to check visual quality
- Run `/responsive-audit` to check breakpoints
Anti-Patterns
- Never create API routes or hooks that duplicate existing Layer 1-5 files. Check first.
- Never fetch data inside section components. The shell fetches; sections receive props.
- Never copy markdown content into component files. All content comes from the database at runtime.
- Never use the old
PathwaySectionstype (fromsrc/types/pathway.ts) for new pathway pages. That type maps to the legacyattachmentsJSONB column. UsePathwaySectionfromsrc/types/pathway-section.ts. - Never hardcode section order. Use
order_indexfrom the database. - Never skip the decomposition plan. Present it to the user before building.
- Never put all 13 sections in one giant component file. One file per section type.