Single-page React + TypeScript + Vite site powered by Contentful.
The landing page is composed from modular CMS sections, and article pages render deep dives with Rich Text.
- Overview
- Features
- Stack
- Project Structure
- Getting Started
- Environment Variables
- How It Works
- Local Preview Checklist
- Scripts
- Troubleshooting
- Content Modeling Notes
- Safety & Publishing
- Docs
This repo powers a personal landing experience and long-form writing pages:
- Landing (
/) renders a singlepagePersonalLandingentry composed of modular section entries. - Articles (
/articles/:slug) renderarticleentries (Rich Text + optional hero image + attachments). - A lean router keeps this as a lightweight SPA without adding a routing dependency.
- Content-driven landing page from Contentful modular sections
- Article pages with SEO metadata fallbacks
- Minimal, reusable UI primitives (
src/components/ui) - Small, explicit “content layer” (
src/content/) that isolates CMS concerns from UI concerns - Custom SPA router with internal link interception
- React 19, TypeScript, Vite (path alias
@→src) - Contentful delivery SDK + light mappers (
src/content/contentful) - Minimal design primitives and section renderers (
src/components/ui,src/components/sections) - Custom SPA router for:
//articles/:slug- 404
High-level map:
src/pages—LandingPage,ArticlePage,NotFoundPagesrc/router— path parsing and SPA navigation helperssrc/content— source contract + Contentful client/API/adapters/types;static/holds fixtures for UI-first prototypingsrc/components/sections— per-section renderers driven by CMS content type idsrc/components/ui&src/components/layout— lightweight UI primitives and SEO wrappersrc/components/rich-text— minimal rich text renderersrc/styles— tokens + base resetsdocs/— IA, design system notes, CMS guidance
Architecture stance: Single-Page App (Vite + React) with a clean CMS boundary, a lightweight router, and a design-system-driven UI. Content models live outside UI logic and flow through adapters before rendering.
gilbertoaharo/
├─ .env.example # Sample env vars (Contentful, site config, preview flags)
├─ LICENSE # Project license
├─ README.md # Project overview, setup, and architecture notes
├─ eslint.config.js # ESLint rules (React + TS)
├─ index.html # Vite HTML entry template
├─ package.json # Scripts, dependencies, metadata
├─ package-lock.json # npm dependency lockfile
├─ tsconfig.json # Base TS config with project references
├─ tsconfig.app.json # TS config for the app bundle
├─ tsconfig.node.json # TS config for Node/Vite tooling
├─ tsconfig.tsbuildinfo # TypeScript incremental build cache (generated)
├─ vite.config.ts # Vite dev/build config (aliases, plugins, tests)
│
├─ public/ # Static assets copied as-is to build output
│ ├─ _redirects # SPA redirect rules (Netlify-style hosting)
│ └─ vite.svg # Example static asset
│
└─ src/ # Application source
├─ main.tsx # React entry point (mounts <App />)
├─ App.tsx # Root app component (router + global wiring)
├─ App.css # Legacy Vite starter styles (currently unused)
├─ index.css # Legacy Vite global styles (currently unused)
├─ env.ts # Centralized env parsing + defaults (fail-fast)
├─ vite-env.d.ts # Vite/TS env type declarations
│
├─ assets/ # Bundled app assets (imported by JS/TS)
│ └─ react.svg
│
├─ styles/ # Global styling layer
│ ├─ tokens.css # Design tokens (colors, spacing, typography)
│ └─ base.css # Base resets/global styles (imports tokens)
│
├─ router/ # Lightweight SPA routing (no react-router)
│ ├─ routes.ts # Route parsing & route definitions
│ ├─ Router.tsx # Route state + view selection
│ └─ link.ts # Internal navigation helpers (history-based)
│
├─ pages/ # Route-level views (thin, data-driven)
│ ├─ LandingPage.tsx # `/` — Personal landing page
│ ├─ ArticlePage.tsx # `/articles/:slug` — Long-form article view
│ ├─ NotFoundPage.tsx # 404 fallback
│ └─ DebugPage.tsx # Debug/diagnostics view (CMS visibility)
│
├─ components/ # UI components (design system + composition)
│ ├─ layout/ # Page-level layout & chrome
│ │ ├─ PageShell.tsx # Page wrapper (SEO, spacing, structure)
│ │ └─ SeoHead.tsx # Document head + meta tags
│ │
│ ├─ rich-text/ # Controlled rich-text rendering
│ │ └─ RichTextRenderer.tsx
│ │ # Maps allowed Contentful nodes → UI primitives
│ │
│ ├─ sections/ # Content-driven page sections
│ │ ├─ SectionRenderer.tsx # Switch on section content-type ID
│ │ ├─ SectionShell.tsx # Shared section framing (anchors, spacing)
│ │ ├─ HeroSection.tsx
│ │ ├─ ProjectsSection.tsx
│ │ ├─ SkillsSection.tsx
│ │ ├─ TimelineSection.tsx
│ │ ├─ LearningSection.tsx
│ │ └─ ContactSection.tsx
│ │
│ └─ ui/ # Design-system primitives (reusable atoms)
│ ├─ Badge.tsx # Badge / label primitive
│ ├─ Button.tsx # Button primitive (CTA)
│ ├─ Card.tsx # Card surface primitive
│ ├─ Container.tsx # Layout container primitive
│ ├─ Heading.tsx # Heading typography primitive
│ ├─ Link.tsx # Styled anchor primitive
│ ├─ Stack.tsx # Stack/spacing layout primitive
│ └─ Text.tsx # Text typography primitive
│
├─ content/ # Content layer (CMS abstraction boundary)
│ ├─ source.ts # ContentSource interface (UI-first contract)
│ │
│ ├─ static/ # UI-first prototyping (no CMS dependency)
│ │ ├─ fixtures.ts # Local fixture content data
│ │ └─ staticSource.ts # Static ContentSource implementation
│ │
│ └─ contentful/ # Contentful implementation of ContentSource
│ ├─ client.ts # Contentful SDK client (delivery/preview)
│ ├─ api.ts # Raw Contentful query helpers
│ ├─ includes.ts # Include/reference depth helpers
│ ├─ types.ts # Contentful model/type definitions
│ ├─ adapters.ts # CMS → UI data mapping (contracts live here)
│ └─ contentfulSource.ts # Contentful ContentSource implementation
│
└─ preview/ # Preview-mode support (draft content)
├─ previewMode.ts # Preview state helpers (env + toggles)
└─ PreviewBanner.tsx # Preview mode UI indicator
- Node: 20.19+ or 22.12+
- npm
npm installnpm run devOpen: http://localhost:5173
Copy .env.example to .env.local (recommended) and fill in your values:
cp .env.example .env.localRequired:
VITE_CONTENTFUL_SPACE_IDVITE_CONTENTFUL_DELIVERY_TOKENVITE_CONTENTFUL_ENVIRONMENT(defaults tomaster)
Recommended:
VITE_ARTICLE_ROUTE_PREFIX(defaults to/articles)VITE_SITE_URL(absolute URL for canonical fallbacks)
Optional (if you support draft preview later):
VITE_CONTENTFUL_USE_PREVIEWVITE_CONTENTFUL_PREVIEW_TOKEN
Never commit
.env.localor tokens. Commit.env.exampleonly.
LandingPagefetches the singlepagePersonalLandingentry.- The page’s
sections[]are rendered bySectionRenderer. SectionRendererdispatches to a section component based on Contentful content type id.
-
ArticlePagefetches anarticleentry byslug. -
Renders:
- title / excerpt
- optional hero image
RichTextRendererfor body- optional attachments list
-
SEO meta is applied via
SeoHead:<title>- meta description
- canonical link fallback
Routerlistens topopstateand routes between landing, article, and not-found states.- A lightweight
Link/ navigation helper intercepts internal links for SPA navigation.
src/content/source.tsdefines theContentSourcecontract.contentfulSourceis the default implementation.- A
staticSourceexists for UI-first prototyping via fixtures (optional workflow).
Use this when “it loads but content is missing”:
-
Contentful tokens are present and correct (
.env.local) -
You’re using the right environment (
VITE_CONTENTFUL_ENVIRONMENT) -
Entries are Published (Delivery API only)
-
pagePersonalLandingexists and hassections[] -
At least one
articleexists with a known slug (e.g.article-test) -
Visit:
/(landing)/articles/<slug>(article route)
npm run dev # start local dev server
npm run build # type-check + build
npm run preview # preview built app
npm run lint # eslint- Missing/invalid Contentful token
- Fix: verify
VITE_CONTENTFUL_DELIVERY_TOKEN, restart dev server
pagePersonalLandingmissing, unpublished, orsections[]empty- Fix: publish the entry and ensure
sections[]contains the section entries
- Wrong route prefix or slug mismatch
- Fix: confirm route prefix (
/articles) and the article’sslugfield
- Include depth too low or referenced entries unpublished
- Fix: increase
includedepth in fetch logic and publish referenced entries
-
Internal navigation should prefer references over hard-coded URLs:
projectLink.article(internal) wins overprojectLink.url(external)
-
Rich Text is intentionally guarded; the renderer supports a minimal set of nodes.
-
Slugs are treated as stable identifiers once published.
-
Secrets:
.env,.env.*are gitignored; commit.env.exampleonly. -
Audit: search for credentials before pushing:
rg -i "secret|token|password|apikey" -
Dependencies: lockfile is
package-lock.json. -
Docs are tracked and safe to publish (no env values in docs).
docs/ia.md— Information architecture & content mappingdocs/editorial-guidelines.md— writing/SEO/link rulesdocs/design-system.md— UI primitives and component patterns