Skip to content
Back to skills

Pathway Builder

ASecurity

Build type-safe React pathway pages from pathway_sections database content — generates types, hooks, section components, and page composition. Triggers on: pathway page, pathway builder, build pathway, pathway components. NOT for: pathway content editing, seed scripts, schema changes, or Stitch conversions.

  • 2 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added October 26, 2026
databasestypescriptshellbashsqlreactexpressapidatabase

Works with

  • cli
  • api

Security analysis

A100/100

Scanned October 26, 2026

npx -y skills add JoshuaShepherd/my-skills --skill pathway-builder --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Pathway Builder?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Pathway Builder
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/joshuashepherd-pathway-builder/badge)](https://www.skillsdirectory.com/skills/joshuashepherd-pathway-builder)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: pathway-builder
description: "Build type-safe React pathway pages from pathway_sections database content — generates types, hooks, section components, and page composition. Triggers on: pathway page, pathway builder, build pathway, pathway components. NOT for: pathway content editing, seed scripts, schema changes, or Stitch conversions."
user-invocable: true
allowed-tools: Read, Write, Edit, Bash, Grep, Glob, Agent
---

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 content
- `structured` (JSONB) — typed metadata specific to the section_type

---

## Critical Rules

1. **Components consume hooks (Layer 5), never call APIs or services directly.**
2. **Use semantic CSS classes only** — `bg-primary`, `text-muted-foreground`, etc. Never hardcode hex/rgb.
3. **Use shadcn/ui components** — `Card`, `Button`, `Accordion`, `Badge`, `Tabs`, etc. Never raw HTML with inline styles.
4. **Use `tenantConfig`** for any text that could vary per tenant. Never hardcode tenant strings.
5. **Push "use client" to leaf components only.** Keep page.tsx as a Server Component where possible.
6. **Follow the design chain:** Tokens → Tailwind → Radix/shadcn → Domain components → Page.
7. **NEVER modify existing Layer 1–5 files** (schema, Zod, services, routes, hooks in `simplified/`). If something is missing in those layers, flag it.
8. **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:

1. **Read `src/types/pathway.ts`** to understand existing types (`StudioPathway`, `PathwaySections`, sub-types).
2. **Read `src/hooks/usePathways.ts`** to understand existing hooks.
3. **Read `src/app/globals.css`** and `src/lib/config/tenant.config.ts` for tokens and tenant config.
4. **Check if `src/types/pathway-section.ts` exists** — if not, it needs to be created (Phase 2).
5. **Check if `src/hooks/usePathwaySections.ts` exists** — if not, it needs to be created (Phase 2).
6. **Check if `server/routes/pathway-sections.routes.ts` exists** — if not, it needs to be created (Phase 2).
7. **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:
```typescript
// 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:

```typescript
// --- 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`

```typescript
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`

```typescript
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`:
```typescript
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.

```tsx
// 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:

```tsx
// 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:

1. **Props, not fetches** — section components receive their `PathwaySection` as a prop
2. **Null-safe** — always default structured fields (`section.structured.items ?? []`)
3. **Return null for empty** — if a section has no meaningful content, render nothing
4. **Semantic tokens** — `text-foreground`, `bg-card`, `border-border`, never hex values
5. **shadcn/ui** — Accordion for expandable lists, Card for contained content, Badge for labels, Tabs for multi-view
6. **Markdown rendering** — for `body` fields containing markdown, use the project's markdown renderer or `dangerouslySetInnerHTML` with sanitization. Check if `@/components/ui/markdown` or a similar utility exists first.
7. **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:

```tsx
// 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.ts` exists with all 13 section structured types
- [ ] Hook file `src/hooks/usePathwaySections.ts` exists with `usePathwaySections` and `usePathwaySection`
- [ ] Server route `server/routes/pathway-sections.routes.ts` exists 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 `tenantConfig` where 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 `pathwaySections` table (in `shared/alan-hirsch/database/schema.ts`)

Report the checklist results to the user.

---

## Phase 7 — Build Report

```markdown
## 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') → <PathwayHero section={...} />
        → findSection(sections, 'faq')  → <PathwayFaq section={...} />
        → ... (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 `PathwaySections` type** (from `src/types/pathway.ts`) for new pathway pages. That type maps to the legacy `attachments` JSONB column. Use `PathwaySection` from `src/types/pathway-section.ts`.
- **Never hardcode section order.** Use `order_index` from 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.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.