Skip to content
Zutra HyperSaaS — 6 languages, auth UI, dashboard, 69 tests
Get it on Gumroad

Zutra v1.3.0 — 6 languages, auth UI, dashboard, 69 tests. Saves 80+ hours.

See what's included

Project Architecture

Directory layout, conventions, and where to find things in the Zutra codebase.

Directory layout

zutra-saas/
├── astro.config.mjs       ← Astro integrations + i18n config
├── tailwind.config.*      ← Tailwind v4 (configured via @theme in global.css)
├── tsconfig.json          ← Strict TS, extends astro/tsconfigs/strict
├── package.json           ← pnpm + scripts (dev / build / test / lint)
├── public/                ← Static assets served as-is (favicons, OG images, logos)
├── scripts/               ← Build-time scripts (icon generation, OG generation)
├── src/
│   ├── pages/             ← File-based routes
│   ├── components/        ← Astro components
│   ├── layouts/           ← Layout.astro (marketing) + AppLayout.astro (dashboard)
│   ├── content/           ← Content collections (blog, docs, changelog, …)
│   ├── content.config.ts  ← Zod schemas for every collection
│   ├── config/            ← The files you edit (site, pricing, features, …)
│   ├── i18n/              ← en/ + es/ (5 files each) + helpers
│   ├── lib/               ← API helpers, rate limiting
│   ├── stories/           ← Storybook stories for UI primitives
│   └── styles/
│       └── global.css     ← Design tokens (@theme block) + base reset
└── docs/                  ← Deployment guides (Vercel, Cloudflare)

Pages

src/pages/ uses Astro’s file-based routing.

  • Files at the root of src/pages/ are English routes: pages/pricing.astro/pricing
  • Files under src/pages/es/ are Spanish routes: pages/es/pricing.astro/es/pricing
  • Dynamic routes use brackets: pages/blog/[slug].astro, pages/docs/[...slug].astro
  • API routes live under src/pages/api/: api/waitlist.ts, api/contact.ts, api/auth/login.ts

The i18n config (astro.config.mjs) uses prefixDefaultLocale: false, which is why English routes don’t have a /en/ prefix.

Components

Components live under src/components/, grouped by feature:

src/components/
├── ui/                ← Reusable primitives (Badge, Button, Card, Input, Logo, Select, Alert)
├── auth/              ← Auth forms (Login, Signup, ForgotPassword, SuccessReceipt)
├── blog/              ← Blog-specific (BlogGrid, BlogHeader)
├── changelog/         ← Changelog-specific (ChangelogHeader, ChangelogEntry)
├── contact/           ← Contact form + channels
├── customers/         ← Customer page (CustomersHero)
├── dashboard/         ← Dashboard sub-components (Sidebar, Overview, etc.)
├── home/              ← Homepage sections (Hero, FeaturesSection, PricingSection, …)
├── open/              ← /open page (TechStack, OpenHero)
├── pricing/           ← Pricing page (PricingHero, FeatureMatrix)
├── status/            ← Status page (ServicesList, IncidentsList)
├── roadmap/           ← Roadmap page (RoadmapList, RoadmapHero)
└── about/             ← About page (MissionSection, TeamSection, ValuesGrid, SocialLinks)

When adding a new feature, create a subdirectory rather than dumping into src/components/.

Layouts

Two layouts wrap every page:

  • Layout.astro — marketing pages. Includes SiteHead (SEO, fonts, OG), SiteNav, SiteFooter, CookieBanner, PurchaseBanner. Used by every page except the dashboard.
  • AppLayout.astro — dashboard pages. Minimal chrome — no nav, no footer. Just DashboardSidebar + main content area.

When you create a new page, decide: marketing page or app page? That tells you which layout to import.

Config (the files you edit)

Everything that’s a string, URL, color, or toggle lives in src/config/:

File What it controls
site.ts Name, tagline, URL, social handles, OG image, theme color
pricing.ts Plan definitions, features, CTAs
features.ts Feature list rendered on the homepage
nav.ts Top nav links + dropdown
hero.ts Hero copy, simulator window metrics/logs/deploy steps
integrations.ts Logos and labels for the integration section
index.ts Re-exports everything as a single CONFIG object

Rule: if you find a string hardcoded outside config/, that’s a bug — open an issue (or fix it).

Content collections

src/content/ is where Markdown/JSON content lives, organized by collection:

  • blog/*.md — blog posts
  • docs/*.md — this documentation
  • changelog/*.json — release notes
  • faq/*.json — FAQ items
  • features/*.json — feature list (alternative to src/config/features.ts)
  • testimonials/*.json — customer quotes
  • roadmap/*.json — public roadmap items
  • team/*.json — team members
  • values/*.json — company values
  • services/*.json — system status services
  • metrics/*.json — open-page metrics
  • case-studies/*.json — customer case studies
  • stack/*.json — tech stack entries
  • contact-channels/*.json — contact page channels

Every collection has a Zod schema in src/content.config.ts — frontmatter is validated at build time.

i18n

Two locales out of the box: en and es. Translations are split into 5 focused files per locale inside src/i18n/en/ and src/i18n/es/ (nav.json, hero.json, sections.json, auth.json, app.json). All files are merged at build time by index.ts — components use the same t() function unchanged. See the i18n Guide for adding a third language.

Styles

src/styles/global.css is the single source of design truth. It defines:

  • The @theme block (Tailwind v4 tokens): colors, fonts, radii
  • The base reset
  • Reusable utility classes: .bg-grid, .card-border, .card-border-highlighted, .btn-primary, .pulse-dot, .text-gradient-white, .text-display, .text-display-sm, .section-divider

When you add a new color or radius, put it in @theme so Tailwind picks it up as a utility class automatically.

Scripts

scripts/ runs at build time:

  • generate-icons.mjs — regenerates the curated Phosphor icon set
  • generate-og.mjs — generates per-page OG images

Run with pnpm tsx scripts/<filename>.mjs.

Scripts (npm)

Top-level npm scripts:

pnpm dev          # Astro dev server on :4321
pnpm build        # Production build → dist/
pnpm preview      # Preview the production build
pnpm test         # Vitest unit tests
pnpm lint         # ESLint
pnpm typecheck    # astro check + tsc
pnpm storybook    # Component explorer on :6006

Conventions

  1. Edit config/ before components — copy, branding, pricing all live there.
  2. Components are dumb — they read from config and render. They don’t fetch data themselves.
  3. Content collections for text-heavy content — blog posts, changelog, docs, FAQ. Not config.
  4. i18n via t() — never hardcode user-facing strings in components.
  5. Tailwind utilities over custom CSS — the <style> block in a .astro file is for animation/keyframes only.
  6. No inline styles — except for truly dynamic values (CSS custom properties, computed colors).

Next steps