A modern, production-ready React learning project that demonstrates how to integrate a headless CMS (Contentful) with a typed React frontend using React Query caching, Vite tooling, and Tailwind CSS styling.
Live Demo: https://headless-contentful-cms.vercel.app/
- Project Overview
- What You Will Learn
- Features
- Technology Stack
- How This Project Works
- Project Structure
- Detailed File Walkthrough
- API and Backend Overview
- Environment Variables Guide (.env)
- Contentful CMS Setup
- Installation and Setup
- How to Run the Project
- Code Examples
- Reusing Components and Logic
- Troubleshooting
- Keywords
- Conclusion
- License
This project fetches project entries from Contentful CMS and displays them as interactive cards in a responsive layout. It is built for instruction and learning purposes, with practical architecture and production-friendly patterns.
The app uses:
- Type-safe data structures with TypeScript
- Smart server-state handling with React Query
- Clean component composition in React
- Utility-first styling with Tailwind CSS
- A headless CMS backend (Contentful) via API
By following this project, you can learn:
- How to connect a React app to a headless CMS
- How to fetch, transform, and render remote content
- How React Query handles loading, caching, and deduplication
- How to keep UI responsive with skeleton loading states
- How to configure and safely use
.envvariables in Vite - How to structure a modern frontend project for readability and reuse
- Dynamic project content from Contentful CMS
- React Query-powered data caching and query management
- localStorage cache persistence logic in
src/main.tsx - Skeleton UI while data is loading
- Responsive grid for mobile, tablet, and desktop
- Card hover effects and smooth transitions
- Strong TypeScript typing for API and UI layers
- Linting support with ESLint v9 flat config
- React 19.0.0
- TypeScript 5.6.3
- Vite 6.0.0
- Tailwind CSS 3.4.14
- TanStack React Query 5.90.11
- Contentful JavaScript SDK 10.12.0
- ESLint 9.x
src/main.tsxbootstraps the app and creates aQueryClient.- React Query options define stale time, retries, and cache behavior.
- localStorage cache restore/save is handled before and after query updates.
src/App.tsxcomposes the page sections (HeroandProjects).src/Projects.tsxusesuseFetchProjects()fromsrc/fetchProjects.tsx.- Contentful data is fetched and mapped into the local
Projecttype. - The UI shows either
ProjectSkeleton(loading) or project cards (loaded).
08-contentful-cms/
βββ public/
β βββ vite.svg
βββ src/
β βββ assets/
β β βββ birthday.png
β β βββ hero.svg
β β βββ questions.png
β β βββ reviews.png
β β βββ tours.png
β βββ App.tsx
β βββ Hero.tsx
β βββ Projects.tsx
β βββ ProjectSkeleton.tsx
β βββ fetchProjects.tsx
β βββ data.ts
β βββ types.ts
β βββ global.css
β βββ main.tsx
β βββ vite-env.d.ts
βββ index.html
βββ package.json
βββ eslint.config.js
βββ tailwind.config.js
βββ postcss.config.js
βββ tsconfig.json
βββ tsconfig.node.json
βββ vite.config.ts- Creates and provides the React Query
QueryClient - Configures cache behavior
- Restores and persists
projectsquery data in localStorage
- Root layout component
- Renders
HeroandProjects
- Intro section with headline and supporting text
- Responsive image rendering for large screens
- Consumes data from custom hook
- Handles loading state with
ProjectSkeleton - Renders clickable project cards with animation and hover interactions
- Placeholder UI shown during pending API calls
- Maintains layout and improves perceived performance
- Creates Contentful client with API token
- Fetches entries of content type
cmsReactProject - Maps CMS payload to app-ready structure (
id,title,url,img)
- Defines interfaces for Contentful data and app data
- Provides strict typing for safer refactoring and development
- Static fallback/demo project data pattern
- Useful for understanding expected shape of project entries
- Tailwind directives
- Base layer styles
- Utility animation classes used by project cards
- Root HTML entry
- SEO metadata, social metadata, and structured data setup
This is a frontend project with a managed backend content source:
- Backend provider: Contentful (Headless CMS)
- API type: Content Delivery API (read-only)
- SDK used:
contentful
GET https://cdn.contentful.com/spaces/{SPACE_ID}/environments/{ENV}/entries?content_type=cmsReactProject
const response = await client.getEntries({
content_type: "cmsReactProject",
});The app maps nested Contentful data into a flat Project model:
item.sys.id->iditem.fields.title->titleitem.fields.url->urlitem.fields.image?.fields?.file?.url->img
Current project route behavior is single-page:
/
This project currently requires one environment variable:
VITE_API_KEY
touch .envVITE_API_KEY=your_contentful_content_delivery_access_tokenAfter adding or changing env values, restart:
npm run dev- In
src/fetchProjects.tsx - Accessed as
import.meta.env.VITE_API_KEY
- Log in to Contentful
- Open your target space
- Go to
Settings->API keys - Create/select an API key
- Copy
Content Delivery API - access token - Paste into
.env
- Do not commit
.env - Keep tokens private
- Rotate token if leaked
The current code hardcodes the space ID in src/fetchProjects.tsx. You can also externalize it:
VITE_CONTENTFUL_SPACE_ID=your_space_id
VITE_CONTENTFUL_ENV=master- Create a Contentful account and space
- Create content type:
cmsReactProject - Add fields:
title(Short text, required)url(Short text, required)image(Media, optional)
- Publish entries for your projects
- Verify API key access from CDA settings
# Clone repository
git clone <your-repo-url>
# Enter project directory
cd 08-contentful-cms
# Install dependencies
npm install
# Create .env and set VITE_API_KEY
cp .env.example .envnpm run devnpm run buildnpm run previewnpm run lintimport { useFetchProjects } from "./fetchProjects";
const ProjectsSection = () => {
const { loading, projects } = useFetchProjects();
if (loading) return <div>Loading...</div>;
return (
<ul>
{projects.map((project) => (
<li key={project.id}>{project.title}</li>
))}
</ul>
);
};const { data = [], isPending } = useQuery({
queryKey: ["projects"],
queryFn: fetchProjects,
});const mappedProjects = response.items.map((item) => {
const fields = item.fields as {
title: string;
url: string;
image?: { fields?: { file?: { url?: string } } };
};
return {
id: item.sys.id,
title: fields.title,
url: fields.url,
img: fields.image?.fields?.file?.url,
};
});You can reuse this codebase in other projects as:
- CMS-driven portfolio showcase
- Product/catalog card grid
- Blog/article card listing
- Internal dashboard cards from API content
Reuse approach:
- Keep
useFetchProjectshook pattern - Change content type and field mapping
- Reuse
ProjectSkeletonfor loading UX - Reuse card structure from
Projects.tsx - Keep React Query setup for efficient data flow
- Check
VITE_API_KEYvalue - Confirm
cmsReactProjectcontent type exists - Ensure entries are published in Contentful
- Confirm space ID in
src/fetchProjects.tsx
- Restart dev server after editing
.env
- Run
npm install - Re-run
npm run lintandnpm run build
react typescript vite tailwindcss react-query tanstack-query contentful headless-cms api-integration frontend-project learning-resource component-architecture skeleton-loading caching modern-web-development
This project is a practical learning reference for building CMS-powered React apps with strong typing, clean architecture, and production-ready data fetching patterns. It is suitable for beginners learning modern frontend development and also useful as a starter template for real projects.
This project is licensed under the MIT License. Feel free to use, modify, and distribute the code as per the terms of the license.
This is an open-source project - feel free to use, enhance, and extend this project further!
If you have any questions or want to share your work, reach out via GitHub or my portfolio at https://www.arnobmahmud.com.
Enjoy building and learning! π
Thank you! π

