Overview
The VENCOL Front Template integrates with WordPress as a headless CMS, fetching content via the WordPress REST API. This approach combines the power of WordPress content management with the performance of a React frontend.
WordPress API Configuration
API Endpoint
The WordPress REST API is configured in lib/wordpress.ts:
const WP_API_URL = 'https://cms.gobigagency.co/vencol/wp-json/wp/v2' ;
Update this URL to point to your WordPress installation. Ensure the WordPress REST API is publicly accessible.
Data Types
WordPress Post Interface
Internal interface for raw WordPress API responses:
interface WPPost {
id : number ;
date : string ;
slug : string ;
title : { rendered : string };
excerpt : { rendered : string };
content : { rendered : string };
featured_media : number ;
_embedded ?: {
'wp:featuredmedia' ?: Array <{ source_url : string }>;
'wp:term' ?: Array < Array <{ name : string }>>;
};
}
Application Types
Application-friendly interfaces defined in types.ts:
export interface BlogPost {
id : number ;
title : string ;
slug : string ;
excerpt : string ;
content : string ;
date : string ;
image : string ;
category : string ;
}
export interface WPPage {
id : number ;
slug : string ;
title : string ;
content : string ;
excerpt : string ;
date : string ;
image : string ;
}
Utility Functions
HTML Stripping
WordPress returns titles and excerpts with HTML tags. This function cleans them:
function stripHtml ( html : string ) : string {
const doc = new DOMParser (). parseFromString ( html , 'text/html' );
return doc . body . textContent ?. trim () || '' ;
}
Usage:
const cleanTitle = stripHtml ( post . title . rendered );
// Input: "Hello <em>World</em>"
// Output: "Hello World"
Converts WordPress date format to localized Spanish format:
function formatDate ( dateString : string ) : string {
const date = new Date ( dateString );
return date . toLocaleDateString ( 'es-ES' , {
year: 'numeric' ,
month: 'short' ,
day: 'numeric' ,
});
}
Example output: "3 mar 2026"
Data Mapping
Blog Post Mapping
Transforms WordPress post format to application format:
function mapWPPostToBlogPost ( post : WPPost ) : BlogPost {
const featuredImage =
post . _embedded ?.[ 'wp:featuredmedia' ]?.[ 0 ]?. source_url ||
'https://picsum.photos/400/250' ;
const category =
post . _embedded ?.[ 'wp:term' ]?.[ 0 ]?.[ 0 ]?. name || 'General' ;
return {
id: post . id ,
slug: post . slug ,
title: stripHtml ( post . title . rendered ),
excerpt: stripHtml ( post . excerpt . rendered ),
content: post . content . rendered ,
date: formatDate ( post . date ),
image: featuredImage ,
category ,
};
}
Featured images and categories are extracted from the _embedded object when using the _embed query parameter.
Page Mapping
Transforms WordPress page format:
function mapWPPageToWPPage ( page : WPPost ) : WPPage {
const featuredImage =
page . _embedded ?.[ 'wp:featuredmedia' ]?.[ 0 ]?. source_url ||
'' ;
return {
id: page . id ,
slug: page . slug ,
title: stripHtml ( page . title . rendered ),
content: page . content . rendered ,
excerpt: stripHtml ( page . excerpt . rendered ),
date: formatDate ( page . date ),
image: featuredImage ,
};
}
API Functions
Fetching Blog Posts
Retrieves multiple blog posts:
export async function fetchBlogPosts ( perPage = 10 ) : Promise < BlogPost []> {
const res = await fetch (
` ${ WP_API_URL } /posts?per_page= ${ perPage } &_embed`
);
if ( ! res . ok ) {
throw new Error ( `WordPress API error: ${ res . status } ` );
}
const posts : WPPost [] = await res . json ();
return posts . map ( mapWPPostToBlogPost );
}
Usage in components:
import { fetchBlogPosts } from '../lib/wordpress' ;
import { BlogPost } from '../types' ;
const [ blogPosts , setBlogPosts ] = useState < BlogPost []>([]);
useEffect (() => {
fetchBlogPosts ()
. then (( wpPosts ) => {
if ( wpPosts . length > 0 ) setBlogPosts ( wpPosts );
})
. catch (( err ) => console . warn ( 'WP blog fetch failed, using fallback:' , err ));
}, []);
Fetching Pages by Slug
Retrieves a single page by its slug:
export async function fetchWPPageBySlug ( slug : string ) : Promise < WPPage | null > {
const res = await fetch (
` ${ WP_API_URL } /pages?slug= ${ encodeURIComponent ( slug ) } &_embed`
);
if ( ! res . ok ) {
throw new Error ( `WordPress API error: ${ res . status } ` );
}
const pages : WPPost [] = await res . json ();
if ( pages . length === 0 ) return null ;
return mapWPPageToWPPage ( pages [ 0 ]);
}
Usage in PageDetail component:
const { slug } = useParams <{ slug : string }>();
const [ page , setPage ] = useState < WPPage | null >( null );
const [ loading , setLoading ] = useState ( true );
const [ notFound , setNotFound ] = useState ( false );
useEffect (() => {
if ( ! slug ) return ;
setLoading ( true );
setNotFound ( false );
fetchWPPageBySlug ( slug )
. then (( wpPage ) => {
if ( wpPage ) {
setPage ( wpPage );
} else {
setNotFound ( true );
}
})
. catch (( err ) => {
console . warn ( 'Error fetching WP page:' , err );
setNotFound ( true );
})
. finally (() => setLoading ( false ));
}, [ slug ]);
WordPress REST API Parameters
The _embed parameter includes related data in the response:
/wp-json/wp/v2/posts?_embed
This embeds:
Featured images (wp:featuredmedia)
Author information (author)
Categories and tags (wp:term)
Common Query Parameters
Parameter Description Example per_pageNumber of items to return per_page=10pagePage number for pagination page=2slugFilter by slug slug=hello-world_embedInclude embedded data _embedorderbyOrder results by field orderby=dateorderSort order order=desc
Example:
// Get 5 most recent posts with embedded data
fetch ( ` ${ WP_API_URL } /posts?per_page=5&_embed&orderby=date&order=desc` )
Error Handling
Graceful Degradation
The application uses fallback data when WordPress is unavailable:
const [ blogPosts , setBlogPosts ] = useState < BlogPost []>( siteContent . blog . posts );
useEffect (() => {
fetchBlogPosts ()
. then (( wpPosts ) => {
if ( wpPosts . length > 0 ) setBlogPosts ( wpPosts );
})
. catch (( err ) => console . warn ( 'WP blog fetch failed, using fallback:' , err ));
}, []);
Always initialize state with fallback data from data.tsx to ensure the site works even if WordPress is down.
404 Handling
When a page is not found:
if ( notFound || ! page ) {
return (
< div className = "min-h-screen flex items-center justify-center" >
< SEO title = "Página no encontrada" />
< div className = "text-center" >
< h2 > Página no encontrada </ h2 >
< Link to = "/" > Volver al Inicio </ Link >
</ div >
</ div >
);
}
Loading States
Display loading skeleton while fetching:
if ( loading ) {
return (
< div className = "pt-48 pb-20 relative z-10" >
< div className = "max-w-4xl mx-auto px-4" >
< div className = "animate-pulse space-y-8" >
< div className = "h-8 bg-white/10 rounded w-1/3" />
< div className = "h-[40vh] bg-white/5 rounded-2xl" />
< div className = "space-y-4" >
< div className = "h-6 bg-white/10 rounded w-3/4" />
< div className = "h-4 bg-white/5 rounded w-full" />
< div className = "h-4 bg-white/5 rounded w-5/6" />
</ div >
</ div >
</ div >
</ div >
);
}
Rendering WordPress Content
Dangerously Set HTML
WordPress content includes HTML that needs to be rendered:
< div
className = "prose prose-invert prose-lg max-w-none
prose-headings:text-white
prose-p:text-gray-300
prose-a:text-brand-green
prose-img:rounded-xl
"
dangerouslySetInnerHTML = { { __html: page . content } }
/>
Only use dangerouslySetInnerHTML with trusted content from your WordPress CMS. Never use it with user-generated content without sanitization.
Prose Styling
Tailwind Typography plugin provides prose classes for rich content:
proseClasses = "
prose prose-invert prose-lg max-w-non e
prose - headings : text - white prose - headings : font - bold
prose - h2 : text - 2 xl prose - h2 : border - b prose - h2 : border - white / 10
prose - h3 : text - xl prose - h3 : text - brand - green
prose - p : text - gray - 300 prose - p : leading - 8
prose - strong : text - white
prose - a : text - brand - green hover : prose - a : underline
prose - blockquote : border - l - brand - green
prose - img : rounded - xl prose - img : border prose - img : border - white / 10
"
WordPress Image Handling
Featured Images
Featured images are extracted from embedded data:
const featuredImage =
post . _embedded ?.[ 'wp:featuredmedia' ]?.[ 0 ]?. source_url ||
'https://picsum.photos/400/250' ; // Fallback placeholder
Image Alignment Classes
Custom CSS supports WordPress image alignment:
.aligncenter { margin : 0 auto ; display : block ; }
.alignleft { float : left ; margin-right : 1.5 em ; }
.alignright { float : right ; margin-left : 1.5 em ; }
.wp-block-image img { max-width : 100 % ; height : auto ; }
SEO Integration
WordPress content is used for SEO metadata:
< SEO
title = { page . title }
description = { page . excerpt }
image = { page . image }
url = { ` ${ siteContent . meta . siteUrl } / ${ page . slug } ` }
/>
Best Practices
Always Use _embed Parameter
Include _embed in API requests to get featured images and categories in a single request: // Good - single request
fetch ( ` ${ WP_API_URL } /posts?_embed` )
// Bad - requires additional requests for media
fetch ( ` ${ WP_API_URL } /posts` )
Always provide fallback data for when WordPress is unavailable: const [ posts , setPosts ] = useState < BlogPost []>( fallbackPosts );
useEffect (() => {
fetchBlogPosts ()
. then ( setPosts )
. catch (() => console . warn ( 'Using fallback' ));
}, []);
Implement proper loading UI to improve perceived performance: if ( loading ) return < LoadingSkeleton /> ;
if ( notFound ) return < NotFoundPage /> ;
return < Content data = { page } /> ;
WordPress content is trusted in this application, but if you allow user-generated content, implement proper sanitization before rendering HTML.
Define proper TypeScript interfaces for all WordPress data: interface WPPost { /* ... */ }
interface BlogPost { /* ... */ }
function mapPost ( wp : WPPost ) : BlogPost { /* ... */ }
Extending the Integration
Adding Custom Post Types
To fetch custom post types:
export async function fetchCustomPosts () : Promise < CustomPost []> {
const res = await fetch (
` ${ WP_API_URL } /custom-post-type?per_page=10&_embed`
);
if ( ! res . ok ) throw new Error ( `API error: ${ res . status } ` );
const posts = await res . json ();
return posts . map ( mapCustomPost );
}
Adding Custom Fields
If using ACF or custom fields:
interface WPPostWithACF extends WPPost {
acf ?: {
custom_field : string ;
another_field : number ;
};
}
function mapPostWithACF ( post : WPPostWithACF ) : BlogPost {
return {
... mapWPPostToBlogPost ( post ),
customField: post . acf ?. custom_field || '' ,
};
}
Implement pagination for blog listing:
export async function fetchBlogPosts (
perPage = 10 ,
page = 1
) : Promise <{ posts : BlogPost []; totalPages : number }> {
const res = await fetch (
` ${ WP_API_URL } /posts?per_page= ${ perPage } &page= ${ page } &_embed`
);
if ( ! res . ok ) throw new Error ( `API error: ${ res . status } ` );
const posts = await res . json ();
const totalPages = parseInt ( res . headers . get ( 'X-WP-TotalPages' ) || '1' );
return {
posts: posts . map ( mapWPPostToBlogPost ),
totalPages ,
};
}
Architecture Understand the overall application structure
Routing Learn about dynamic page routing
Styling Explore WordPress content styling