A modern, production-ready React application demonstrating seamless integration with Contentful headless CMS. This project showcases best practices in TypeScript, React Query for data fetching and caching, Tailwind CSS for styling, and modern React patterns.
Live Demo: https://contentful-cms-demo.netlify.app/
- Project Overview
- Features
- Technology Stack
- Project Structure
- Installation & Setup
- Environment Variables
- Contentful CMS Setup
- Running the Project
- Project Components
- API Integration
- Code Examples
- Key Concepts & Learning Points
- Reusing Components
- Keywords
- Conclusion
This project is a comprehensive learning resource that demonstrates how to build a modern React application with Contentful CMS integration. It features:
- Headless CMS Integration: Dynamic content management using Contentful
- TypeScript: Full type safety throughout the application
- React Query: Advanced data fetching with caching and persistence
- Modern UI: Beautiful, responsive design with Tailwind CSS
- Performance Optimized: Caching, request deduplication, and skeleton loaders
- Educational: Well-commented code for learning purposes
The application fetches project data from Contentful CMS and displays it in an elegant, interactive card layout with smooth animations and hover effects.
- Dynamic Content Fetching: Projects are fetched from Contentful CMS in real-time
- Intelligent Caching: React Query caches data with localStorage persistence
- Skeleton Loading: Beautiful loading states while data is being fetched
- Responsive Design: Fully responsive layout (mobile, tablet, desktop)
- Modern Animations: Staggered fade-in animations and smooth hover effects
- Type Safety: Complete TypeScript implementation with proper type definitions
- Performance Optimized:
- Request deduplication (multiple components = one API call)
- Cache persistence across page refreshes
- No redundant API calls
- Instant content display from cache
- Gradient Effects: Modern gradient text and backgrounds
- Interactive Cards: Hover effects with image zoom and overlay
- External Link Indicators: Visual feedback for external links
- Smooth Transitions: 500ms transitions for all interactive elements
- Accessibility: Proper alt texts, semantic HTML, and keyboard navigation
- React 19.0.0: Latest stable version of React
- TypeScript 5.6.3: Type-safe JavaScript
- Vite 6.0.0: Next-generation frontend build tool
- Tailwind CSS 3.4.14: Utility-first CSS framework
- @tanstack/react-query 5.90.11: Data fetching, caching, and synchronization
- contentful 10.12.0: Contentful JavaScript SDK for API integration
- ESLint 9.15.0: Code linting and quality
- TypeScript ESLint: TypeScript-specific linting rules
- PostCSS & Autoprefixer: CSS processing and vendor prefixing
contentful-cms/
βββ public/ # Static assets
β βββ vite.svg
βββ src/
β βββ assets/ # Image assets
β β βββ birthday.png
β β βββ hero.svg
β β βββ questions.png
β β βββ reviews.png
β β βββ tours.png
β βββ App.tsx # Root component
β βββ Hero.tsx # Hero section component
β βββ Projects.tsx # Projects display component
β βββ ProjectSkeleton.tsx # Loading skeleton component
β βββ fetchProjects.tsx # Custom hook for data fetching
β βββ types.ts # TypeScript type definitions
β βββ data.ts # Static fallback data
β βββ main.tsx # Application entry point
β βββ global.css # Global styles and Tailwind directives
β βββ vite-env.d.ts # Vite type declarations
βββ index.html # HTML template
βββ package.json # Dependencies and scripts
βββ tsconfig.json # TypeScript configuration
βββ vite.config.ts # Vite configuration
βββ tailwind.config.js # Tailwind CSS configuration
βββ postcss.config.js # PostCSS configurationmain.tsx: Application bootstrap, React Query setup, cache persistenceApp.tsx: Root component that composes Hero and ProjectsHero.tsx: Landing section with project introductionProjects.tsx: Main component displaying project cardsfetchProjects.tsx: Custom hook using React Query for data fetchingProjectSkeleton.tsx: Loading state UI componenttypes.ts: All TypeScript interfaces and type definitionsdata.ts: Static fallback data (for development/testing)global.css: Tailwind directives and global styles
- Node.js: Version 18.x or higher
- npm: Version 9.x or higher (comes with Node.js)
- Contentful Account: Free account at contentful.com
git clone https://github.com/your-username/contentful-cms-react-project.git
cd contentful-cms-react-projectnpm installThis will install all required dependencies including:
- React and React DOM
- TypeScript and type definitions
- TanStack React Query
- Contentful SDK
- Tailwind CSS and related tools
- Vite and build tools
Create a .env file in the root directory:
touch .envAdd your Contentful API credentials:
VITE_API_KEY=your_contentful_access_token_hereImportant:
- Never commit your
.envfile to version control - The
.envfile is already in.gitignore - Replace
your_contentful_access_token_herewith your actual Contentful access token
See the Contentful CMS Setup section for detailed instructions.
Description: Contentful Content Delivery API access token
How to Get It:
- Log in to your Contentful account
- Navigate to Settings β API keys
- Click Add API key or use an existing one
- Copy the Content Delivery API - access token
- Paste it in your
.envfile
Example:
VITE_API_KEY=abc123def456ghi789jkl012mno345pqr678stu901vwx234yzSecurity Notes:
- This is a read-only token (Content Delivery API)
- Safe to use in client-side applications
- Never share your token publicly
- Regenerate if accidentally exposed
- Prefix Required: All environment variables must be prefixed with
VITE_ - Access in Code: Use
import.meta.env.VITE_API_KEY - Type Safety: Defined in
src/vite-env.d.ts
// In fetchProjects.tsx
const client = createClient({
space: "your_space_id",
environment: "master",
accessToken: import.meta.env.VITE_API_KEY as string,
});- Visit contentful.com
- Sign up for a free account
- Create a new space (or use an existing one)
-
Navigate to Content model in the sidebar
-
Click Add content type
-
Name it:
cmsReactProject -
Add the following fields:
Field 1: Title
- Field ID:
title - Type: Short text
- Required: Yes
Field 2: URL
- Field ID:
url - Type: Short text
- Required: Yes
- Validation: URL format
Field 3: Image
- Field ID:
image - Type: Media
- Required: No
- Allowed media types: Images only
- Field ID:
-
Click Save to create the content type
- Navigate to Content in the sidebar
- Click Add entry
- Select
cmsReactProjectcontent type - Fill in the fields:
- Title: Your project name
- URL: Project demo URL
- Image: Upload or select an image
- Click Publish
Repeat for each project you want to display.
- Navigate to Settings β API keys
- Find your Space ID (you'll need this for the code)
- Find or create a Content Delivery API key
- Copy the Access token
In src/fetchProjects.tsx, update the space ID:
const client = createClient({
space: "your_space_id_here", // Replace with your Space ID
environment: "master",
accessToken: import.meta.env.VITE_API_KEY as string,
});Start the development server:
npm run devThe application will be available at:
- Local:
http://localhost:5173 - Network: Check terminal for network URL
Features in Development:
- Hot Module Replacement (HMR) - instant updates
- Fast refresh - preserves component state
- Source maps for debugging
- Detailed error messages
Build for production:
npm run buildThis will:
- Type-check the code with TypeScript
- Optimize and bundle assets
- Generate production-ready files in
dist/folder
Preview the production build locally:
npm run previewCheck code quality:
npm run lintPurpose: Root component that composes the entire application
Structure:
<main>
<Hero /> {/* Landing section */}
<Projects /> {/* Dynamic projects section */}
</main>Usage: Imported in main.tsx and rendered as the root component.
Reusability: Can be extended to include navigation, footer, or additional sections.
Purpose: Displays the hero/banner section with project introduction
Features:
- Responsive layout (single column mobile, two columns desktop)
- Hero image (hidden on mobile for performance)
- Project description text
Props: None (static content)
Responsive Breakpoints:
- Mobile: Single column, text only
- Desktop (lg+): Two-column grid with image
Code Example:
import Hero from "./Hero";
// Usage
<Hero />;Reusability:
- Can be modified to accept props for dynamic content
- Image can be made configurable
- Text content can be passed as props
Purpose: Displays dynamic project cards fetched from Contentful
Features:
- Uses
useFetchProjectscustom hook - Conditional rendering (skeleton vs. content)
- Responsive grid layout
- Interactive card animations
- Hover effects with image zoom
State Management:
const { loading, projects } = useFetchProjects();Responsive Grid:
- Mobile: 1 column
- Tablet (md): 2 columns
- Desktop (lg): 3 columns
Code Example:
import Projects from "./Projects";
// Usage
<Projects />;Reusability:
- Can accept custom query key for different content types
- Grid columns can be made configurable via props
- Card design can be customized
Purpose: Loading state UI that matches the project card layout
Features:
- Pulse animation for visual feedback
- Matches exact layout of project cards
- Responsive grid (same as Projects component)
Usage: Automatically displayed by Projects component when loading === true
Code Example:
import ProjectSkeleton from "./ProjectSkeleton";
// Usage
{
loading && <ProjectSkeleton />;
}Reusability:
- Can be adapted for any card-based loading state
- Animation speed and colors can be customized
- Number of skeleton items can be made configurable
Purpose: Custom React hook for fetching projects from Contentful
Returns:
{
loading: boolean; // True while fetching, false when complete
projects: Project[]; // Array of project objects
}Features:
- React Query integration for caching
- Automatic error handling
- Request deduplication
- Cache persistence
Usage Example:
import { useFetchProjects } from "./fetchProjects";
const MyComponent = () => {
const { loading, projects } = useFetchProjects();
if (loading) return <div>Loading...</div>;
return (
<div>
{projects.map((project) => (
<div key={project.id}>{project.title}</div>
))}
</div>
);
};Reusability:
- Can be modified to fetch different content types
- Query key can be made dynamic
- Can accept additional query parameters
This project uses Contentful's Content Delivery API (CDA), which is:
- Read-only: Safe for client-side use
- Public: No authentication required (uses access token)
- Fast: CDN-backed for global performance
- RESTful: Standard HTTP requests
Base URL: https://cdn.contentful.com
Endpoint Used:
GET /spaces/{space_id}/environments/{environment}/entries?content_type={content_type}In Code:
const response = await client.getEntries({
content_type: "cmsReactProject",
});- Request:
useFetchProjectshook callsfetchProjects() - API Call: Contentful SDK makes HTTP request to Contentful API
- Response: Contentful returns entries in nested structure
- Transformation: Data is mapped to simplified
Project[]format - Caching: React Query caches the result
- Persistence: Cache is saved to localStorage
- Display: Projects component renders the data
Contentful Response:
{
"items": [
{
"sys": {
"id": "entry_id",
"type": "Entry"
},
"fields": {
"title": "Project Name",
"url": "https://example.com",
"image": {
"fields": {
"file": {
"url": "https://images.ctfassets.net/..."
}
}
}
}
}
]
}Transformed to:
[
{
id: "entry_id",
title: "Project Name",
url: "https://example.com",
img: "https://images.ctfassets.net/...",
},
];React Query automatically handles:
- Network errors
- API errors
- Timeout errors
- Retry logic (configured to retry once)
Errors are logged to console and loading state is set to false.
import React from "react";
interface ComponentProps {
// Define props here
}
const Component: React.FC<ComponentProps> = () => {
return <div>{/* Component JSX */}</div>;
};
export default Component;import { useFetchProjects } from "./fetchProjects";
const MyProjects = () => {
const { loading, projects } = useFetchProjects();
if (loading) {
return <div>Loading projects...</div>;
}
return (
<div>
{projects.map((project) => (
<div key={project.id}>
<h3>{project.title}</h3>
<a href={project.url}>Visit Project</a>
</div>
))}
</div>
);
};import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
refetchOnWindowFocus: false,
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
{/* Your app components */}
</QueryClientProvider>
);
}// Define Contentful structure
interface ContentfulProjectFields {
title: string;
url: string;
image?: {
fields: {
file: {
url: string;
};
};
};
}
// Define application structure
interface Project {
id: string;
title: string;
url: string;
img?: string;
}// Responsive classes
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{/* Content */}
</div>
// Hover effects
<div className="hover:scale-105 transition-transform duration-300">
{/* Content */}
</div>
// Gradient text
<h1 className="bg-gradient-to-r from-blue-500 to-purple-500 bg-clip-text text-transparent">
Gradient Text
</h1>What is a Headless CMS?
- Content is stored separately from presentation
- API-first approach for content delivery
- Frontend and backend are decoupled
- Multiple frontends can use the same content
Benefits:
- Flexibility: Use any frontend framework
- Scalability: Content and presentation scale independently
- Reusability: Same content for web, mobile, IoT
- Performance: CDN-backed delivery
What is React Query?
- Powerful data synchronization library for React
- Handles server state management
- Provides caching, background updates, and more
Key Features Used:
- Caching: Stores fetched data in memory
- Deduplication: Multiple requests = one API call
- Background Refetching: Keeps data fresh automatically
- Persistence: localStorage integration for cache survival
Why Use React Query?
- Reduces API calls significantly
- Better user experience (instant loading from cache)
- Automatic error handling and retries
- Built-in loading states
Benefits:
- Type Safety: Catch errors at compile time
- IntelliSense: Better IDE autocomplete
- Documentation: Types serve as inline documentation
- Refactoring: Safer code changes
Key Patterns:
- Interface definitions for props and data
- Type assertions for external APIs
- Generic types for reusable components
- Strict mode for maximum safety
What are Custom Hooks?
- Reusable functions that use React hooks
- Encapsulate component logic
- Can be shared across components
Benefits:
- Reusability: Use same logic in multiple components
- Separation of Concerns: Logic separate from UI
- Testability: Easier to test in isolation
- Readability: Cleaner component code
Mobile-First Approach:
- Design for mobile first, enhance for larger screens
- Use breakpoint prefixes:
md:,lg:,xl: - Responsive utilities:
grid-cols-1 md:grid-cols-2
Tailwind Benefits:
- Utility-first: Compose styles from utilities
- No CSS file switching: Styles in JSX
- PurgeCSS: Removes unused styles in production
- Consistent design system
Techniques Used:
- Code Splitting: Vite automatically splits code
- Image Optimization: Lazy loading and responsive images
- Caching: React Query + localStorage
- Skeleton Loading: Better perceived performance
- Request Deduplication: One API call for multiple components
// Basic usage
import Hero from "./components/Hero";
function App() {
return <Hero />;
}
// With custom props (modify Hero.tsx to accept props)
interface HeroProps {
title: string;
description: string;
image?: string;
}
const Hero: React.FC<HeroProps> = ({ title, description, image }) => {
return (
<section>
<h1>{title}</h1>
<p>{description}</p>
{image && <img src={image} alt={title} />}
</section>
);
};// Reuse with different content type
// Modify useFetchProjects to accept content type parameter
export const useFetchProjects = (contentType: string = "cmsReactProject") => {
return useQuery({
queryKey: [contentType],
queryFn: () => fetchProjects(contentType),
});
};
// Usage
const { loading, projects } = useFetchProjects("blogPosts");// Adapt for any card-based loading state
const CardSkeleton = ({ count = 3 }) => {
return (
<div className="grid grid-cols-3 gap-4">
{Array.from({ length: count }).map((_, i) => (
<div key={i} className="animate-pulse bg-gray-200 h-64 rounded" />
))}
</div>
);
};// Create a generic fetch hook
const useFetchContent = <T,>(queryKey: string[], fetchFn: () => Promise<T>) => {
return useQuery({
queryKey,
queryFn: fetchFn,
});
};
// Usage
const { data, isLoading } = useFetchContent(["blogPosts"], () =>
fetchBlogPosts()
);react typescript contentful headless cms react query tanstack query vite tailwind css custom hooks api integration environment variables caching localStorage skeleton loading responsive design modern ui frontend development web development javascript jsx tsx component architecture data fetching state management performance optimization code splitting type safety educational project learning resource portfolio project
This project demonstrates modern React development practices with a focus on:
- Type Safety: Complete TypeScript implementation
- Performance: React Query caching and optimization
- User Experience: Skeleton loaders and smooth animations
- Code Quality: Well-structured, commented, and maintainable
- Best Practices: Industry-standard patterns and conventions
- How to integrate Contentful CMS with React
- React Query for efficient data fetching
- TypeScript for type-safe React applications
- Tailwind CSS for modern, responsive styling
- Custom hooks for reusable logic
- Environment variable management
- Performance optimization techniques
- Extend the project with additional content types
- Add filtering and search functionality
- Implement pagination for large datasets
- Add error boundaries for better error handling
- Create a blog or portfolio using the same patterns
- Deploy to production (Vercel, Netlify, etc.)
β
Production Ready: Type-safe, optimized, and well-structured
β
Educational: Comprehensive comments and documentation
β
Modern Stack: Latest versions of React, TypeScript, and tools
β
Performance: Caching, deduplication, and optimization
β
Responsive: Works perfectly on all device sizes
β
Maintainable: Clean code with clear separation of concerns
Feel free to use this project repository 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://arnob-mahmud.vercel.app/.
Enjoy building and learning! π
Thank you! π

