CLAUDE.md

CLAUDE.md
# Frontier Metals: Steel Manufacturer Template (Gallop Canon)
A Next.js template for a regional steel manufacturer / metal building supplier, built on the Gallop Canon architecture system. Canon provides ESLint rules (`@gallop.software/canon/eslint`) and an audit CLI (`gallop audit`) that enforce the patterns below. The `CLAUDE.md` file is the canonical AI reference, and there is no auto-generation pipeline.
"Frontier Metals" is fictional demo content. See **Rebranding** below.
## Rebranding: where company content lives
This is a reusable template. Company identity is centralized; do NOT scatter business details through components.
| What | Where |
|---|---|
| Company name, tagline, contact, address, hours, service area, stats, values, timeline, social, CTA labels, SEO defaults | `src/config.ts` |
| Panels, products, colors, building systems, industries, testimonials, resources, process steps | `src/catalog.ts` |
| Brand colors (semantic tokens) | `src/styles/tailwind.css` `@theme` |
| Fonts | `src/fonts/*.ts` |
| Logo (inline SVG) | `src/components/logo.tsx` |
| Navigation + utility bar | `src/components/navbar/config.ts` |
| Footer columns | `src/components/footer/config.tsx` |
| Site-wide JSON-LD | `src/app/metadata.tsx` (reads `src/config.ts`) |
| AI assistant system prompt | `src/app/api/assistant/route.ts` (reads `src/config.ts`) |
| AI crawler summary | `public/llms.txt` (hand-maintained, not generated) |
Rebranding should mean editing `src/config.ts`, `src/catalog.ts`, and the `@theme` tokens, not dozens of files.
**Never** state certifications, warranties, engineering approvals, code compliance, or test results as fact. Those fields are configurable placeholders and must stay labelled as such.
## Auto-Generated Files: Never Edit Manually
| File | Regenerate With | Triggered By |
|---|---|---|
| `_data/_blog.json` | `npm run blog` | Adding/editing projects in src/app/(hero)/projects/<slug>/ |
| `src/app/(demo)/block/[[...slug]]/_block-index.ts` | `npm run blocks` | Block additions/removals |
| `public/search-index.json` | `npm run search` | Content changes |
## Build Commands
| Command | Purpose |
|---|---|
| `npm run dev` | Development server |
| `npm run build` | Production build (auto-runs npm run blog) |
| `npm run blog` | Regenerate blog metadata |
| `npm run blocks` | Regenerate blocks catalog |
| `npm run search` | Regenerate search index |
| `npm run lint` | ESLint check (run after edits) |
| `npm run ts` | TypeScript check |
| `npm run check` | lint + ts combined |
| `npm run prettier` | Format code |
## File & Folder Authority
These rules govern what AI is allowed and forbidden to do when creating, moving, or modifying files and folders.
### Defined `/src` Structure
```
src/
├── app/ # Routes, layouts, metadata (Next.js App Router)
│ └── {route}/
│ ├── page.tsx
│ └── _blocks/ # Co-located page-specific blocks
├── components/ # Reusable UI primitives
├── hooks/ # Custom React hooks
├── styles/ # CSS, Tailwind, fonts
├── tools/ # Utility tools
├── utils/ # Utility functions
└── state.ts # Global state
```
### App Router Structure
Routes must use Next.js route groups. At minimum, `(default)` must exist:
```
src/app/
├── (default)/ # Required - default layout group
│ ├── layout.tsx
│ └── {routes}/
├── (hero)/ # Optional - hero layout variant
├── api/ # API routes (exception - no grouping)
├── layout.tsx # Root layout
└── metadata.tsx # Shared metadata
```
- All page routes must be inside a route group (parentheses folder)
- Never create routes directly under `src/app/` (except `api/`, root files)
- New route groups are allowed freely when a new layout variant is needed
### File Structure Rules
**Blocks:**
- Co-located in `_blocks/` subdirectories alongside each page route
- Singleton naming: `hero.tsx` (one per page), numbered when multiple: `content-2.tsx`, `content-3.tsx`
- Import with relative paths: `import Hero from './_blocks/hero'`
- Example: `src/app/(default)/furniture/_blocks/hero.tsx`
**Projects (posts):**
- Each project is a folder under `src/app/(hero)/projects/<slug>/` with two files:
- `page.tsx`: route + `const metadata: PageMetadata = {...}` (including `date`, `categories`, `featuredImage`) + `PageWrapper` wrapping `<Content />` imported from `./content`, with JSON-LD from `buildProjectStructuredData()`
- `content.tsx`: owns the project body (`TITLE`, `Details`, exported `Content` wrapped in `<ProjectCaseStudy>`, exported `BlogContent` for the sidebar/index card)
- Routes: archive `/projects`, one static route per project at `/projects/<slug>`, category archive `/projects/category/[slug]` (all under `src/app/(hero)/projects/`)
- **Do not** reintroduce a `/projects/[slug]` dynamic route that resolves content with a template-literal `import()` — see **Cloudflare Workers Constraints** below.
- The blog grid (`src/components/blog/blog-client.tsx`) dynamic-imports `../../app/(hero)/projects/<slug>/content.tsx` for the in-sidebar preview
- `npm run blog` extracts each project's metadata block into `_data/_blog.json`; runtime code imports it via `@/data/_blog.json`
**Components:**
- Simple components: Single file in `src/components/`
- Complex components: Folder with `index.tsx`
- Use folders when component has multiple sub-files
### DO: What AI IS Allowed To Do
- Create files only inside existing Canon-defined zones
- Place new files in the zone that matches their architectural role
- Follow existing folder conventions within a zone
- Reuse existing folders when possible
- Create new route groups in `src/app/` when new layouts are needed
- Create new archive content folders (like `portfolio/`) in `/src`
- Create dotfiles/directories at project root (`.github/`, `.cursor/`, etc.)
- Import generated `_data/*.json` in runtime code via the `@/data/*` alias (bundled at build time)
- Ask for confirmation if the correct zone is ambiguous
### DO NOT: What AI Is Forbidden To Do
- Create new top-level directories (except dotfiles)
- Create new folders in `/src` (except archive content or route groups)
- Place files outside Canon-defined zones
- Mix responsibilities across zones (components importing blocks, etc.)
- Reorganize or move folders without explicit instruction
- Invent new organizational conventions
- Create placeholder or speculative files
- Import from `_scripts/` in runtime code (Node-only deps: `fs`, `jsdom`, etc.)
- Read `_data/*.json` from disk at runtime (`fs.readFileSync` + `process.cwd()`) — there is no filesystem on edge/Workers runtimes, so it silently returns empty; import via `@/data/*` instead
- Manually edit files in `_data/` (generated only)
## Cloudflare Workers Constraints
The site deploys to both Vercel and Cloudflare Workers (via OpenNext, `npm run cf:deploy`). Workers has no filesystem and no incremental-cache backing store unless one is configured in `open-next.config.ts`. Code that works on Vercel can 404 or 500 on Workers. These three rules are not optional:
**1. No `generateStaticParams` + `dynamicParams = false` on dynamic routes.** That combination prerenders params into the OpenNext incremental cache, which has no backing store on Workers, so every such URL 404s. Let dynamic routes render on demand and reject unknown slugs with `notFound()` in the page body instead. This broke `projects/category/[slug]` and `panels/[profile]`.
**2. No template-literal `await import()` to resolve page content.** On Workers that resolves to a lazily-loaded chunk which intermittently fails to load; a surrounding `catch` then turns the module error into a spurious 404, so the same URL returns 200 and 404 on alternating requests. Use static route folders with a direct `import { Content } from './content'`. This broke every project case study.
**3. No `fs` at runtime.** `readdirSync`/`statSync` against `process.cwd()` throw on Workers. Enumerate content from generated `_data/*.json` via the `@/data/*` alias, from `src/catalog.ts`, or from an explicit list. This broke `/sitemap.xml` (500 on Workers, 200 on Vercel): `src/tools/get-page-slugs.tsx` scanned `src/app` with `readdirSync`, and `revalidate` on `src/app/sitemap.ts` made the route ISR, so it re-rendered at runtime instead of staying build-time static. Note that `dynamic = 'force-static'` alone did **not** prevent the runtime re-render — removing the `fs` dependency was required. `get-page-slugs.tsx` now holds a hand-maintained `STATIC_PAGES` list: **add new top-level pages to it or they will be missing from the sitemap.**
After any routing or content-loading change, verify on the Workers deploy — not just `npm run dev` or Vercel. Request each affected URL more than once; this class of bug is often intermittent.
## Enforced Patterns (ESLint)
These patterns are enforced by `@gallop.software/canon/eslint`. Violations will be flagged.
- **001: Server-First Blocks** (`gallop/no-client-blocks`): Blocks must be server components
- **002: Layout Hierarchy** (`gallop/no-container-in-section`): No Container inside Section
- **003: Typography Components** (`gallop/prefer-typography-components`): Use Paragraph/Span, not raw tags
- **004: Component Props** (`gallop/prefer-component-props`): Use props over className for supported styles
- **006: Block Naming** (`gallop/block-naming-convention`): Descriptive kebab-case naming, PascalCase exports
- **007: Import Paths** (`gallop/prefer-alias-imports`): @/ aliases, direct file imports
- **008: Tailwind Only** (`gallop/no-inline-styles`): No inline styles in blocks, components allowed for dynamic values
- **009: Color Tokens** (`gallop/no-raw-colors`): Use semantic color tokens
- **012: Icon System** (`gallop/no-inline-svg`): Iconify with Icon component
- **014: clsx Not classnames** (`gallop/no-classnames-package`): Use clsx, never classnames package
- **018: Layout Components** (`gallop/prefer-layout-components`): Use Grid/Columns, not raw div with grid
- **019: Background Image Rounded** (`gallop/background-image-rounded`): Background images must have rounded="rounded-none"
- **020: No Arbitrary Colors** (`gallop/no-arbitrary-colors`): Use defined color tokens, not arbitrary color values
- **021: Cross-Zone Import Boundaries** (`gallop/no-cross-zone-imports`): Enforce import boundaries between Canon zones
- **024: React Intersection Observer** (`gallop/no-native-intersection-observer`): Use react-intersection-observer package, not native API
- **025: No Components in Blocks** (`gallop/no-component-in-blocks`): Exported component functions must be in components folder; non-exported content components are allowed in blocks
- **026: List Components** (`gallop/prefer-list-components`): Use List/Li, not raw ul/li tags
- **027: Luxon for Dates** (`gallop/no-native-date`): Use Luxon DateTime, not native JavaScript Date
## Documentation Patterns
These patterns are not enforced by ESLint but should be followed.
- **005: Page Structure**: PageWrapper, generatePageMetadata pattern
- **010: Spacing System**: Standard padding/margin values
- **011: Responsive Mobile-First**: sm/md/lg/xl breakpoint usage
- **013: New Component Pattern**: Props for margin/color/fontSize
- **015: No Inline Hover Styles**: Tailwind for hover states
- **016: Client Extraction**: Extract hooks to components, not blocks (see Pattern 001 for enforcement)
- **017: SEO Metadata**: PageMetadata structure, structured data
## Color Token System
All colors use semantic tokens defined in `src/styles/tailwind.css` `@theme`:
- **Surface**: `body`, `body-light`, `body-dark`, `body2`
- **Text**: `contrast`, `contrast-light`, `contrast-dark`
- **Overlay** (fixed, don't flip in dark mode): `overlay`, `overlay-text`
- **Accents**: `accent`, `accent-light`, `accent-dark`, `accent-contrast`, `accent2`, `accent2-light`, `accent2-dark`, `accent2-contrast`, `accent3`, `accent3-light`, `accent3-dark`, `accent3-contrast`, `accent4`, `accent4-light`, `accent4-dark`, `accent4-contrast`, `accent5`, `accent5-light`, `accent5-dark`, `accent5-contrast`, `accent6`, `accent6-light`, `accent6-dark`, `accent6-contrast`
**Never use**: `gray-*`, `white`, `black`, `slate-*`. Always map to a semantic token.
## Component Quick Reference
- `Heading` - props: `as`, `styleAs`, `color`, `margin`, `fontSize`, `fontWeight`, `textAlign` (h1 defaults to title-case text-contrast display; h2 defaults to a small uppercase text-accent eyebrow style, so use `SectionHeading` for section headers rather than raw h2)
- `SectionHeading` - props: `eyebrow`, `heading`, `intro`, `as`, `align`, `dark` (the standard section header: eyebrow + display heading + intro)
- `Paragraph` - props: `color`, `margin`, `fontSize`, `lineHeight`, `textAlign`
- `Span` - props: `color`, `margin`, `fontSize` (Inline text, mb-0 default)
- `Label` - props: `color`, `margin`, `fontSize`, `fontWeight`, `textAlign`
- `Accent` - props: `color`, `margin`, `size`, `textAlign` (Has conditional text-shadow when color='text-body')
- `Quote` - props: `variant`, `color`, `margin`, `fontSize`, `fontWeight`, `textAlign`
- `Section` - props: `overlayColor` (Wraps content with Container. Default overlay bg-overlay/30)
- `Cover` - props: `overlayColor` (Full-height hero-style with image backgrounds)
- `Columns` - props: `cols`, `gap`, `align`
- `Column`
- `Button` - props: `href`, `variant`, `icon`, `iconPlacement`, `margin`, `dark` (dark prop flips light/dark variant class sets (render-time boolean, not CSS dark mode))
- `Icon` (Iconify icon wrapper. Always include sizing class (w-5 h-5))
- `Image` - props: `size` (Always use size prop ('small'|'medium'|'large'|'full'). Uses getStudioImage() from src/utils/studio-helpers.ts)
## State Management
valtio store at `src/state.ts`. Read with `useSnapshot(state)`, write with `state.propName = value`.
Available state: `playVideo`, `offsetTop`, `windowHeight`, `lastOffsetTop`, `isScrolling`, `dialogOpen`, `scrollingDirection`, `lastScrollingDirection`, `lockScrollDirection`.
## Template-Specific Rules
- Use the color prop on typography components (Heading, Paragraph, Label, Span, Accent)
- Use props (fontSize, margin, fontWeight, textAlign) instead of className when a component prop exists
- Import icons from @iconify/icons-heroicons/ or @iconify/icons-lucide/
- Run npm run blog after adding/editing projects in src/app/(hero)/projects/<slug>/
## Do NOT
- Use `'use client'` in blocks - extract to components
- Use raw `<p>`, `<span>`, or `<h1>``<h6>` - use Paragraph/Span/Heading components
- Use className for margin/color/fontSize when component has props
- Use Container inside Section - Section already provides containment
- Use `classnames` package - use `clsx` instead
- Use inline styles for hover states - use Tailwind classes
- Use native `IntersectionObserver` - use `react-intersection-observer` package
- Use inline `<svg>` in blocks - use the Icon component with Iconify icons
- Use deep relative imports (`../../`) - use `@/` alias imports
- Use gray-*, white, black, slate-*. Always map to a semantic token
- Put text color in className when component has a color prop
- Manually construct image URLs when a _data/_studio.json metadata entry exists
- Read _data/*.json with fs at runtime - import via @/data/* (no filesystem on Workers)
- Manually edit files in _data/ (generated only)
## Post-Edit Verification
After editing files:
1. Run `npm run lint`
2. Run `npm run ts`
3. Fix any violations before committing
Note: Only lint files you edited, not the entire codebase.

Support

Talk to the developers of this project to learn more

We have been building professional websites for big clients for over 15 years. Gallop templates and blocks is our best foundation for SEO websites and web apps.

© 2026 Web Plant Media, LLC