Skip to content

sitecrate-presence (Tier 1 template)

sitecrate-presence is the starting point for every SiteCrate Tier 1 (Presence) client build. It’s a minimal forkable scaffold — no pre-built sections. Ahmed builds sections per client with Claude Code, driven by the Build Brief from the admin.

The stack mirrors sitecrate.ca: Vite + React 18 SPA, hash-based routing, no React Router, no component library, no Tailwind.

The golden rule: everything client-specific lives in client.config.js

Section titled “The golden rule: everything client-specific lives in client.config.js”

src/client.config.js exports a single CLIENT object. Every component imports from it. No other file should contain client strings.

export const CLIENT = {
// Identity
name: 'Business Name',
tagline: 'Your tagline goes here',
phone: '', // must contain digits — 'N/A' renders a broken tel: link
email: '',
address: '',
domain: '', // bare domain only — no https:// prefix
// Branding — injected as CSS vars at runtime by App.jsx
accentHue: '250', // oklch hue: 250=blue, 145=green, 25=red, 30=orange, 210=teal
accentChroma: '0.14', // 0.14 vibrant, 0.08 muted, 0.20 vivid
fontHead: 'Geist', // 'Geist' | 'Instrument Serif' | 'Space Grotesk'
// SEO — gaId must exactly match GA_MEASUREMENT_ID in index.html (both instances)
metaTitle: 'Business Name | City',
metaDesc: '140–160 char description for Google.',
gaId: 'G-XXXXXXXXXX',
// Google Business
gmbUrl: '', // Google Maps short URL
mapsEmbed: '', // Full Maps embed src
// Content — always an array, even when empty (null causes TypeError)
services: [
// { title: 'Service Name', icon: '🔧', desc: 'One-line description' }
],
}
  • phone must contain digits after stripping non-numeric chars. 'N/A' / 'TBD' are truthy but produce href="tel:" (a broken link). Nav and Footer both guard against this.
  • domain must be bare (e.g. hamiltonplumbing.ca) — no https://. The Footer strips the protocol if present.
  • gaId must exactly match both instances of GA_MEASUREMENT_ID in index.html. If they differ, every analytics event is silently lost.
  • services must always be an array — null/undefined causes a TypeError on the ?.length check in HomePage.jsx.

App.jsx runs once on mount and injects the brand colours as CSS variables:

root.style.setProperty('--accent', `oklch(0.62 ${c} ${h})`)
root.style.setProperty('--accent-2', `oklch(0.72 ${c} ${h})`)
root.style.setProperty('--accent-soft', `oklch(0.62 ${c} ${h} / 0.12)`)
// Also overrides --font-head if CLIENT.fontHead !== 'Geist'

All CSS uses var(--accent) — changing two numbers in client.config.js re-themes the entire site.

Hash-based. Add routes as you build pages:

src/App.jsx
const ROUTES = ['home', 'services', 'about', 'contact']
const pages = {
home: <HomePage go={go} />,
services: <ServicesPage go={go} />,
about: <AboutPage go={go} />,
contact: <ContactPage go={go} />,
}

Add nav links in src/components/Nav.jsx:

const NAV_LINKS = [
{ label: 'Services', route: 'services' },
{ label: 'Contact', route: 'contact' },
]
  • Analyticssrc/analytics.js reads CLIENT.gaId. Page views auto-fire on route change; window.gtag existence is checked before every call, so it’s safe if the GA script hasn’t loaded yet.
  • Netlify Forms — a hidden <form name="contact" netlify> in index.html registers the form at build time. Any React form with name="contact" and the netlify attribute submits to Netlify Forms. Always include netlify-honeypot="bot-field".

All base styles live in src/styles/global.css:

GroupTokens
Light surfaces--bg, --bg-elev, --bg-muted
Text--ink, --ink-2, --ink-3, --ink-4
Borders--line, --line-2
Dark sections--dark, --dark-2, --dark-3, --dark-ink, --dark-ink-2, --dark-ink-3
Brand (runtime)--accent, --accent-2, --accent-soft
Typography--font-head, --font-body, --font-mono
Radii--r-sm, --r-md, --r-lg, --r-xl

Base classes: .container, .btn, .btn-primary, .btn-ghost, .btn-on-dark, .eyebrow, .section-head, .cta-band. Add component styles directly in global.css — no CSS modules.

  • client.config.js — all fields populated (name, phone, email, address, domain, services, GA ID, accent colours)
  • index.html<title>, <meta name="description">, all OG/Twitter tags; both GA_MEASUREMENT_ID instances replaced with the real GA4 ID matching CLIENT.gaId; canonical URL updated
  • public/sitemap.xml — replace DOMAIN.ca with the client’s real domain
  • public/robots.txt — replace DOMAIN.ca with the client’s real domain
  • public/og-image.png — 1200×630 for the client brand
  • public/favicon.svg — replaced with the client favicon
  • npm run build passes zero errors
  • All pages score 90+ Lighthouse mobile
  • Contact form appears in the Netlify Forms dashboard
  • GA4 Realtime shows events firing

See Client delivery: intake → launch for the full per-client workflow.