Skip to main content

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:
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:
wordpress.ts
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:
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:
wordpress.ts
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"

Date Formatting

Converts WordPress date format to localized Spanish format:
wordpress.ts
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:
wordpress.ts
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:
wordpress.ts
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:
wordpress.ts
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:
Home.tsx
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:
wordpress.ts
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:
PageDetail.tsx
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

ParameterDescriptionExample
per_pageNumber of items to returnper_page=10
pagePage number for paginationpage=2
slugFilter by slugslug=hello-world
_embedInclude embedded data_embed
orderbyOrder results by fieldorderby=date
orderSort orderorder=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:
Home.tsx
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:
PageDetail.tsx
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:
PageDetail.tsx
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:
PageDetail.tsx
<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-none
  prose-headings:text-white prose-headings:font-bold
  prose-h2:text-2xl 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 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:
index.html
.aligncenter { margin: 0 auto; display: block; }
.alignleft { float: left; margin-right: 1.5em; }
.alignright { float: right; margin-left: 1.5em; }
.wp-block-image img { max-width: 100%; height: auto; }

SEO Integration

WordPress content is used for SEO metadata:
PageDetail.tsx
<SEO
  title={page.title}
  description={page.excerpt}
  image={page.image}
  url={`${siteContent.meta.siteUrl}/${page.slug}`}
/>

Best Practices

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 || '',
  };
}

Pagination Support

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

Build docs developers (and LLMs) love