This project decouples UI copy and structured marketing data from React components using a lightweight, typed content layer. The goals: (1) enable future i18n & CMS integration, (2) keep components purely presentational, (3) allow automated linting to prevent hard‑coded strings creeping back in.
content/
site.ts # Global site metadata (name, description, nav, footer, CTAs)
home.hero.ts # Hero section copy & animated terminal lines
home.tech-stack.ts # Tech stack section groups
home.capabilities.ts # Capabilities section cards
lib/content/types.ts # Shared TypeScript interfaces for content objects
Add new section/content files under content/ with clear, scoped filenames: <page>.<section>.ts (e.g. about.timeline.ts). Keep cross-page constants (brand slogans, global CTAs) inside site.ts unless they are page-specific.
- All content objects must satisfy interfaces defined in
lib/content/types.ts(extend those interfaces instead of redefining shapes inline). - Prefer explicit IDs for list items (e.g. capabilities) to support stable keys, analytics mapping, and future localization fallbacks.
- Optional fields (
intro,description, etc.) should be omitted (not empty strings) when unused.
Components import the minimal content they need:
import { HOME_TECH_STACK } from "@/content/home.tech-stack";
export function TechStackSection(props: Partial<TechStackContent>) {
const { groups = HOME_TECH_STACK.groups } = props;
// ... render
}Avoid deep passing of large content objects—destructure early and pass only primitive props to leaf components to keep memoization efficient.
A custom ESLint rule (local/no-ui-literals) warns when raw UI text literals appear in TS/TSX files outside the content/ layer. This pushes new copy into centralized modules automatically during development.
Rule scope (current heuristics):
- Flags: JSX text nodes, string attribute values, top-level string assignments.
- Ignores: files under
/content/, attributes likeclassName,data-*,aria-*, pure utility/tailwind strings. - False positives: very short labels (e.g.
Logo)—either centralize or disable inline (// eslint-disable-next-line local/no-ui-literals).
You can tune allowed paths or ignored attributes in eslint.config.mjs.
- Define or extend types in
lib/content/types.tsif needed. - Create
<page>.<section>.tsexporting a named constant (UPPERSNAKE or HOME* prefix for page-scoped root objects). - Export via a barrel (
content/index.ts) if you want a grouped import. - Import into the component and use as default props (allow overrides for testing / future CMS injection).
- Run
pnpm lint– ensure no stray literals remain in components.
Future locale structure (not yet active):
content/
en/
site.ts
home.hero.ts
es/
site.ts
home.hero.ts
With a resolver: getContent(locale).home.hero. The current flat layout was chosen to keep Phase 1 simple; migration path is additive.
We'll introduce Zod schemas (e.g. lib/content/schema.ts) to validate content shapes at build time, ensuring CMS or manual edits can't break rendering.
- Clear ownership boundary: design/copy vs. component logic.
- Easier testability (components accept injected content overrides).
- Paves the way for static extraction, i18n, or build-time content transforms (markdown/MDX, CMS adapters).
- Prevents regression via lint rule.
Q: Can I keep a tiny label inline (e.g. button text)?
A: Prefer centralizing; consistent extraction improves future localization. For ephemeral dev placeholders, silence once—but replace before merging.
Q: How do I add computed content (e.g. dynamic year)?
A: Keep the dynamic expression in the component but store surrounding static text in content (e.g. template parts) or generate the value in a small helper imported by the content file.
Q: What about rich text / markdown?
A: Introduce MDX or serialized AST later; current layer is intentionally primitive TS objects.
This project uses brand fonts loaded with next/font and exposed as CSS variables:
- Inter (base/UI): applied globally to
bodyasfont-sans. - Poppins (headings): applied globally to
h1–h3and available via the.heading-brandutility. - Roboto Mono (code): use with
font-mono.
See the detailed guide in STYLEGUIDE.md.
The design system exposes brand colors via CSS variables and Tailwind tokens, with light/dark variants:
- Background/Text:
bg-background,text-foreground - Primary:
bg-primary,text-primary,ring-primaryand paired*-foreground - Secondary:
bg-secondary,text-secondaryand*-foreground - Accent:
bg-accent,text-accentand*-foreground
Always prefer design tokens over raw hex. See details and best practices in the "Brand Colors" section of STYLEGUIDE.md.
- Source code: Released under the MIT License.
- Assets (logos, images, icons, and other brand-related files located in
/assets):
All Rights Reserved. These files are not covered by the MIT license and may not be used, copied, or modified without explicit permission. - Other content such as external data is not part of this repository and is not licensed here.