Custom Post Types y Taxonomías en Astro
Por ximo
¿Qué son los Custom Post Types?
Los Custom Post Types (CPT) de WordPress te permiten crear tipos de contenido más allá de posts y páginas: portfolios, testimonios, productos, eventos, etc. Combinados con Astro, puedes crear sitios web altamente especializados manteniendo la facilidad de gestión de WordPress.
Configurar Custom Post Types en WordPress
Primero, registra tus tipos de contenido personalizados:
<?php
// wp-content/themes/tu-tema/functions.php
function registrar_portfolio() {
$args = array(
'label' => 'Portfolio',
'public' => true,
'show_in_graphql' => true, // Importante para GraphQL
'graphql_single_name' => 'portfolioItem',
'graphql_plural_name' => 'portfolioItems',
'supports' => array('title', 'editor', 'thumbnail', 'custom-fields'),
'has_archive' => true,
'rewrite' => array('slug' => 'portfolio'),
'menu_icon' => 'dashicons-portfolio'
);
register_post_type('portfolio', $args);
}
function registrar_testimonios() {
$args = array(
'label' => 'Testimonios',
'public' => true,
'show_in_graphql' => true,
'graphql_single_name' => 'testimonial',
'graphql_plural_name' => 'testimonials',
'supports' => array('title', 'editor', 'thumbnail'),
'menu_icon' => 'dashicons-testimonial'
);
register_post_type('testimonio', $args);
}
add_action('init', 'registrar_portfolio');
add_action('init', 'registrar_testimonios');
Registrar Taxonomías Personalizadas
Las taxonomías organizan tu contenido (como categorías y etiquetas):
<?php
function registrar_taxonomias_portfolio() {
// Taxonomía: Tipo de proyecto
register_taxonomy('tipo_proyecto', 'portfolio', array(
'label' => 'Tipos de Proyecto',
'hierarchical' => true, // Como categorías
'show_in_graphql' => true,
'graphql_single_name' => 'projectType',
'graphql_plural_name' => 'projectTypes',
'rewrite' => array('slug' => 'tipo-proyecto')
));
// Taxonomía: Tecnologías
register_taxonomy('tecnologia', 'portfolio', array(
'label' => 'Tecnologías',
'hierarchical' => false, // Como etiquetas
'show_in_graphql' => true,
'graphql_single_name' => 'technology',
'graphql_plural_name' => 'technologies',
'rewrite' => array('slug' => 'tecnologia')
));
}
add_action('init', 'registrar_taxonomias_portfolio');
Queries GraphQL para Custom Post Types
Crea queries específicas para cada tipo de contenido:
// src/queries/portfolio.js
import { gql } from '@apollo/client';
export const GET_PORTFOLIO_ITEMS = gql`
query GetPortfolioItems {
portfolioItems(first: 100) {
nodes {
id
title
slug
content
date
featuredImage {
node {
sourceUrl
altText
mediaDetails {
width
height
}
}
}
projectTypes {
nodes {
name
slug
}
}
technologies {
nodes {
name
slug
}
}
portfolioFields {
clientName
projectUrl
completionDate
}
}
}
}
`;
export const GET_PORTFOLIO_BY_SLUG = gql`
query GetPortfolioBySlug($slug: ID!) {
portfolioItem(id: $slug, idType: SLUG) {
id
title
content
date
featuredImage {
node {
sourceUrl
altText
}
}
projectTypes {
nodes {
name
slug
}
}
technologies {
nodes {
name
slug
}
}
portfolioFields {
clientName
projectUrl
completionDate
challenge
solution
results
}
}
}
`;
Queries para Testimonios
// src/queries/testimonials.js
import { gql } from '@apollo/client';
export const GET_TESTIMONIALS = gql`
query GetTestimonials {
testimonials(first: 50) {
nodes {
id
title
content
featuredImage {
node {
sourceUrl
altText
}
}
testimonialFields {
authorName
authorPosition
company
rating
}
}
}
}
`;
Página de Portfolio
Muestra todos los items del portfolio con filtros:
---
// src/pages/portfolio.astro
import { Image } from 'astro:assets';
import client from '../lib/wordpress';
import { GET_PORTFOLIO_ITEMS } from '../queries/portfolio';
const { data } = await client.query({
query: GET_PORTFOLIO_ITEMS,
});
const portfolioItems = data.portfolioItems.nodes;
// Obtener todas las tecnologías únicas para filtros
const allTechnologies = [...new Set(
portfolioItems.flatMap(item =>
item.technologies.nodes.map(tech => tech.name)
)
)];
---
<html lang="es">
<head>
<title>Portfolio</title>
</head>
<body>
<div class="container">
<h1>Nuestro Portfolio</h1>
<div class="filters">
<button class="filter-btn active" data-filter="all">
Todos
</button>
{allTechnologies.map(tech => (
<button class="filter-btn" data-filter={tech}>
{tech}
</button>
))}
</div>
<div class="portfolio-grid">
{portfolioItems.map(item => (
<article
class="portfolio-item"
data-technologies={item.technologies.nodes.map(t => t.name).join(',')}
>
{item.featuredImage && (
<div class="portfolio-image">
<Image
src={item.featuredImage.node.sourceUrl}
alt={item.featuredImage.node.altText || item.title}
width={item.featuredImage.node.mediaDetails.width}
height={item.featuredImage.node.mediaDetails.height}
format="webp"
quality={80}
/>
</div>
)}
<div class="portfolio-content">
<h2>{item.title}</h2>
<div class="meta">
{item.projectTypes.nodes.map(type => (
<span class="badge">{type.name}</span>
))}
</div>
<div class="technologies">
{item.technologies.nodes.map(tech => (
<span class="tech-tag">{tech.name}</span>
))}
</div>
{item.portfolioFields?.clientName && (
<p class="client">Cliente: {item.portfolioFields.clientName}</p>
)}
<a href={`/portfolio/${item.slug}`} class="view-project">
Ver Proyecto →
</a>
</div>
</article>
))}
</div>
</div>
</body>
</html>
<script>
const filterButtons = document.querySelectorAll('.filter-btn');
const portfolioItems = document.querySelectorAll('.portfolio-item');
filterButtons.forEach(button => {
button.addEventListener('click', () => {
const filter = button.dataset.filter;
// Actualizar botones activos
filterButtons.forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
// Filtrar items
portfolioItems.forEach(item => {
const technologies = item.dataset.technologies.split(',');
if (filter === 'all' || technologies.includes(filter)) {
item.style.display = 'block';
item.style.animation = 'fadeIn 0.3s';
} else {
item.style.display = 'none';
}
});
});
});
</script>
<style>
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.filters {
display: flex;
gap: 1rem;
flex-wrap: wrap;
margin: 2rem 0;
}
.filter-btn {
padding: 0.5rem 1rem;
border: 2px solid #ddd;
background: white;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
}
.filter-btn:hover,
.filter-btn.active {
background: #2563eb;
color: white;
border-color: #2563eb;
}
.portfolio-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 2rem;
}
.portfolio-item {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
transition: transform 0.3s;
}
.portfolio-item:hover {
transform: translateY(-5px);
box-shadow: 0 8px 12px rgba(0,0,0,0.15);
}
.portfolio-image {
width: 100%;
height: 250px;
overflow: hidden;
}
.portfolio-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.portfolio-content {
padding: 1.5rem;
}
.meta {
display: flex;
gap: 0.5rem;
margin: 1rem 0;
}
.badge {
background: #e0e7ff;
color: #3730a3;
padding: 0.25rem 0.75rem;
border-radius: 6px;
font-size: 0.875rem;
font-weight: 600;
}
.technologies {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin: 1rem 0;
}
.tech-tag {
background: #f3f4f6;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
}
.view-project {
display: inline-block;
margin-top: 1rem;
color: #2563eb;
font-weight: 600;
text-decoration: none;
}
@keyframes fadeIn {
from { opacity: 0; transform: scale(0.95); }
to { opacity: 1; transform: scale(1); }
}
</style>
Página Individual de Portfolio
---
// src/pages/portfolio/[slug].astro
import { Image } from 'astro:assets';
import client from '../../lib/wordpress';
import { GET_PORTFOLIO_BY_SLUG, GET_PORTFOLIO_ITEMS } from '../../queries/portfolio';
export async function getStaticPaths() {
const { data } = await client.query({
query: GET_PORTFOLIO_ITEMS,
});
return data.portfolioItems.nodes.map(item => ({
params: { slug: item.slug },
}));
}
const { slug } = Astro.params;
const { data } = await client.query({
query: GET_PORTFOLIO_BY_SLUG,
variables: { slug },
});
const project = data.portfolioItem;
---
<html lang="es">
<head>
<title>{project.title} - Portfolio</title>
</head>
<body>
<article class="project">
<header class="project-header">
{project.featuredImage && (
<div class="hero-image">
<Image
src={project.featuredImage.node.sourceUrl}
alt={project.featuredImage.node.altText || project.title}
width={1200}
height={600}
format="webp"
quality={90}
/>
</div>
)}
<div class="container">
<h1>{project.title}</h1>
<div class="project-meta">
{project.portfolioFields?.clientName && (
<div class="meta-item">
<strong>Cliente:</strong>
<span>{project.portfolioFields.clientName}</span>
</div>
)}
{project.portfolioFields?.completionDate && (
<div class="meta-item">
<strong>Fecha:</strong>
<span>{project.portfolioFields.completionDate}</span>
</div>
)}
{project.portfolioFields?.projectUrl && (
<a
href={project.portfolioFields.projectUrl}
target="_blank"
class="project-link"
>
Ver Proyecto en Vivo →
</a>
)}
</div>
<div class="taxonomies">
<div class="taxonomy-group">
<h3>Tipo de Proyecto</h3>
<div class="tags">
{project.projectTypes.nodes.map(type => (
<span class="tag">{type.name}</span>
))}
</div>
</div>
<div class="taxonomy-group">
<h3>Tecnologías</h3>
<div class="tags">
{project.technologies.nodes.map(tech => (
<span class="tag tech">{tech.name}</span>
))}
</div>
</div>
</div>
</div>
</header>
<div class="container">
<div class="project-content">
{project.portfolioFields?.challenge && (
<section class="content-section">
<h2>El Desafío</h2>
<div set:html={project.portfolioFields.challenge} />
</section>
)}
{project.portfolioFields?.solution && (
<section class="content-section">
<h2>La Solución</h2>
<div set:html={project.portfolioFields.solution} />
</section>
)}
{project.portfolioFields?.results && (
<section class="content-section">
<h2>Resultados</h2>
<div set:html={project.portfolioFields.results} />
</section>
)}
<section class="content-section">
<h2>Sobre el Proyecto</h2>
<div set:html={project.content} />
</section>
</div>
</div>
</article>
<a href="/portfolio" class="back-link">← Volver al Portfolio</a>
</body>
</html>
<style>
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.hero-image {
width: 100%;
height: 500px;
overflow: hidden;
}
.hero-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.project-header {
margin-bottom: 3rem;
}
.project-header h1 {
font-size: 3rem;
margin: 2rem 0;
}
.project-meta {
display: flex;
gap: 2rem;
align-items: center;
margin: 2rem 0;
padding: 1.5rem;
background: #f9fafb;
border-radius: 12px;
}
.meta-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.meta-item strong {
font-size: 0.875rem;
color: #6b7280;
text-transform: uppercase;
}
.project-link {
margin-left: auto;
padding: 0.75rem 1.5rem;
background: #2563eb;
color: white;
text-decoration: none;
border-radius: 8px;
font-weight: 600;
}
.taxonomies {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem;
margin: 2rem 0;
}
.taxonomy-group h3 {
font-size: 1rem;
color: #6b7280;
margin-bottom: 0.75rem;
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.tag {
padding: 0.5rem 1rem;
background: #e0e7ff;
color: #3730a3;
border-radius: 6px;
font-size: 0.875rem;
font-weight: 600;
}
.tag.tech {
background: #fef3c7;
color: #92400e;
}
.content-section {
margin: 3rem 0;
}
.content-section h2 {
font-size: 2rem;
margin-bottom: 1.5rem;
color: #111827;
}
.back-link {
display: inline-block;
margin: 2rem;
padding: 0.75rem 1.5rem;
background: #f3f4f6;
color: #374151;
text-decoration: none;
border-radius: 8px;
font-weight: 600;
}
</style>
Componente de Testimonios
---
// src/components/Testimonials.astro
import { Image } from 'astro:assets';
import client from '../lib/wordpress';
import { GET_TESTIMONIALS } from '../queries/testimonials';
const { data } = await client.query({
query: GET_TESTIMONIALS,
});
const testimonials = data.testimonials.nodes;
---
<section class="testimonials">
<div class="container">
<h2>Lo que Dicen Nuestros Clientes</h2>
<div class="testimonials-grid">
{testimonials.map(testimonial => (
<div class="testimonial-card">
{testimonial.featuredImage && (
<div class="author-image">
<Image
src={testimonial.featuredImage.node.sourceUrl}
alt={testimonial.featuredImage.node.altText}
width={80}
height={80}
format="webp"
/>
</div>
)}
<div class="rating">
{[...Array(testimonial.testimonialFields?.rating || 5)].map(() => (
<span class="star">★</span>
))}
</div>
<div class="content" set:html={testimonial.content} />
<div class="author-info">
<strong>{testimonial.testimonialFields?.authorName || testimonial.title}</strong>
{testimonial.testimonialFields?.authorPosition && (
<span class="position">
{testimonial.testimonialFields.authorPosition}
</span>
)}
{testimonial.testimonialFields?.company && (
<span class="company">
{testimonial.testimonialFields.company}
</span>
)}
</div>
</div>
))}
</div>
</div>
</section>
<style>
.testimonials {
padding: 4rem 0;
background: #f9fafb;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 2rem;
}
.container h2 {
text-align: center;
font-size: 2.5rem;
margin-bottom: 3rem;
}
.testimonials-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 2rem;
}
.testimonial-card {
background: white;
padding: 2rem;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.author-image {
width: 80px;
height: 80px;
border-radius: 50%;
overflow: hidden;
margin-bottom: 1rem;
}
.author-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.rating {
color: #fbbf24;
font-size: 1.5rem;
margin-bottom: 1rem;
}
.content {
margin: 1.5rem 0;
font-style: italic;
color: #4b5563;
line-height: 1.6;
}
.author-info {
display: flex;
flex-direction: column;
gap: 0.25rem;
margin-top: 1.5rem;
padding-top: 1.5rem;
border-top: 1px solid #e5e7eb;
}
.author-info strong {
font-size: 1.125rem;
color: #111827;
}
.position {
color: #6b7280;
font-size: 0.875rem;
}
.company {
color: #2563eb;
font-size: 0.875rem;
font-weight: 600;
}
</style>
Páginas de Archivo por Taxonomía
---
// src/pages/tecnologia/[slug].astro
import client from '../../lib/wordpress';
import { gql } from '@apollo/client';
export async function getStaticPaths() {
const { data } = await client.query({
query: gql`
query GetTechnologies {
technologies {
nodes {
slug
name
}
}
}
`,
});
return data.technologies.nodes.map(tech => ({
params: { slug: tech.slug },
props: { techName: tech.name },
}));
}
const { slug } = Astro.params;
const { techName } = Astro.props;
const { data } = await client.query({
query: gql`
query GetPortfolioByTechnology($slug: String!) {
portfolioItems(where: { taxQuery: {
taxArray: {
taxonomy: TECHNOLOGY,
field: SLUG,
terms: [$slug]
}
}}) {
nodes {
id
title
slug
excerpt
featuredImage {
node {
sourceUrl
altText
}
}
}
}
}
`,
variables: { slug },
});
const projects = data.portfolioItems.nodes;
---
<html lang="es">
<head>
<title>Proyectos con {techName}</title>
</head>
<body>
<div class="container">
<h1>Proyectos usando {techName}</h1>
<p class="count">{projects.length} proyectos encontrados</p>
<div class="projects-grid">
{projects.map(project => (
<article class="project-card">
{project.featuredImage && (
<img
src={project.featuredImage.node.sourceUrl}
alt={project.featuredImage.node.altText}
/>
)}
<h2>{project.title}</h2>
<div set:html={project.excerpt} />
<a href={`/portfolio/${project.slug}`}>Ver Proyecto</a>
</article>
))}
</div>
</div>
</body>
</html>
Ventajas de Custom Post Types con Astro
- Organización clara: Cada tipo de contenido tiene su propia estructura
- Consultas optimizadas: GraphQL obtiene exactamente lo que necesitas
- Flexibilidad total: Combina múltiples CPTs en una sola página
- SEO mejorado: URLs semánticas y contenido bien estructurado
- Escalabilidad: Añade nuevos tipos sin afectar los existentes
Este enfoque te permite crear sitios web complejos y especializados manteniendo WordPress como un CMS potente y familiar para los editores de contenido.
Categorías
Astro Desarrollo Web WordPress