SEO
Metadata, sitemap y datos estructurados optimizados
SEO
EmpiezaTuSaaS viene con SEO configurado desde el primer deploy: metadata global, OpenGraph, sitemap automatico, robots.txt y datos estructurados JSON-LD. No tienes que instalar nada ni configurar plugins.
Configuración global (config/site.ts)
Todos los valores de SEO se leen de config/site.ts. Es la unica fuente de verdad para nombre, descripción, URL y redes sociales:
export const siteConfig = {
name: "EmpiezaTuSaaS",
description:
"El boilerplate Next.js en español para lanzar tu SaaS en días, no meses.",
domain: "empiezatusaas.com",
url: process.env.NEXT_PUBLIC_APP_URL || "https://empiezatusaas.com",
supportEmail: "soporte@empiezatusaas.com",
twitter: "https://twitter.com/empiezatusaas",
github: "https://github.com/empiezatusaas",
// ...navLinks, footerLinks, auth
} as const;Para adaptar a tu proyecto, edita name, description, url y supportEmail. Todo lo demas (metadata, sitemap, JSON-LD) se actualiza automáticamente.
Metadata global (layout.tsx)
La metadata global se define en app/layout.tsx y aplica a todas las páginas:
import type { Metadata } from "next";
import { siteConfig } from "@/config/site";
export const metadata: Metadata = {
title: {
default: siteConfig.name,
template: `%s | ${siteConfig.name}`,
},
description: siteConfig.description,
metadataBase: new URL(siteConfig.url),
openGraph: {
type: "website",
locale: "es_ES",
url: siteConfig.url,
title: siteConfig.name,
description: siteConfig.description,
siteName: siteConfig.name,
},
twitter: {
card: "summary_large_image",
title: siteConfig.name,
description: siteConfig.description,
},
};- title.template: Las páginas con titulo propio se muestran como
Titulo | EmpiezaTuSaaS - metadataBase: URL base para resolver OG images y rutas relativas
- locale:
es_ES+lang="es"en el<html>
Metadata por página
Cada página puede sobrescribir la metadata global con generateMetadata. Ejemplo del blog:
export async function generateMetadata({
params,
}: BlogPostPageProps): Promise<Metadata> {
const { slug } = await params;
const post = await getPostBySlug(slug);
if (!post) return { title: "Post no encontrado" };
return {
title: post.title,
description: post.description,
openGraph: {
title: post.title,
description: post.description,
type: "article",
publishedTime: post.date,
authors: [post.author],
...(post.image && { images: [post.image] }),
},
twitter: {
card: "summary_large_image",
title: post.title,
description: post.description,
},
};
}Para páginas estáticas, exporta metadata directamente:
export const metadata: Metadata = {
title: "Blog",
description: "Articulos sobre SaaS y Next.js",
};Puedes agregar keywords, authors, creator o verification (Google, Bing) directamente en el objeto metadata de app/layout.tsx.
Open Graph y Twitter Cards
Ya configurados en la metadata global. Cada página hereda la configuración y puede sobrescribirla.
Lo que se genera automáticamente en el HTML:
<!-- Open Graph -->
<meta property="og:type" content="website" />
<meta property="og:locale" content="es_ES" />
<meta property="og:title" content="EmpiezaTuSaaS" />
<meta property="og:description" content="..." />
<meta property="og:site_name" content="EmpiezaTuSaaS" />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="EmpiezaTuSaaS" />Para posts del blog, el og:type cambia a article con publishedTime y authors.
Sitemap (auto-generado)
El sitemap se genera automáticamente desde app/sitemap.ts. Combina rutas estáticas con los posts del blog:
import { MetadataRoute } from "next";
import { siteConfig } from "@/config/site";
import { getAllPosts } from "@/lib/blog";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = siteConfig.url;
const staticRoutes: MetadataRoute.Sitemap = [
{ url: baseUrl, lastModified: new Date(), changeFrequency: "weekly", priority: 1 },
{ url: `${baseUrl}/blog`, lastModified: new Date(), changeFrequency: "daily", priority: 0.8 },
{ url: `${baseUrl}/privacy-policy`, lastModified: new Date(), changeFrequency: "yearly", priority: 0.3 },
{ url: `${baseUrl}/tos`, lastModified: new Date(), changeFrequency: "yearly", priority: 0.3 },
];
let blogRoutes: MetadataRoute.Sitemap = [];
try {
const posts = await getAllPosts();
blogRoutes = posts
.filter((post) => post.published)
.map((post) => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: new Date(post.date),
changeFrequency: "monthly" as const,
priority: 0.6,
}));
} catch {
// Blog posts not available
}
return [...staticRoutes, ...blogRoutes];
}Para agregar nuevas páginas, añade un objeto al array staticRoutes. Los posts del blog se incluyen automáticamente al publicar archivos .mdx.
Robots.txt
app/robots.ts bloquea las rutas protegidas de la indexacion:
import { MetadataRoute } from "next";
import { siteConfig } from "@/config/site";
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: "*",
allow: "/",
disallow: ["/dashboard", "/settings", "/billing", "/admin", "/api/"],
},
sitemap: `${siteConfig.url}/sitemap.xml`,
};
}Datos estructurados (JSON-LD)
El boilerplate incluye 5 componentes JSON-LD en components/seo/json-ld.tsx, listos para usar:
| Componente | Donde se usa | Proposito |
|---|---|---|
OrganizationJsonLd | Landing page | Identifica tu marca |
SoftwareAppJsonLd | Landing page | Producto SaaS con precio y rating |
ArticleJsonLd | Post de blog | Rich snippet de articulo |
FAQJsonLd | Cualquier página | Preguntas frecuentes |
BreadcrumbJsonLd | Cualquier página | Migas de pan |
Componente base
Todos los schemas usan un componente interno que inyecta el JSON-LD:
function JsonLd({ data }: { data: Record<string, unknown> }) {
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
/>
);
}Landing page
OrganizationJsonLd y SoftwareAppJsonLd se renderizan en app/page.tsx:
import { OrganizationJsonLd, SoftwareAppJsonLd } from "@/components/seo/json-ld";
export default function HomePage() {
return (
<div className="min-h-screen">
<OrganizationJsonLd />
<SoftwareAppJsonLd />
{/* ... secciónes de la landing */}
</div>
);
}Edita el precio (49 EUR) y el rating (5/5, 150 resenas) en SoftwareAppJsonLd para reflejar los valores reales de tu producto.
Blog posts
ArticleJsonLd se usa en app/blog/[slug]/page.tsx:
<ArticleJsonLd
title={post.title}
description={post.description}
url={postUrl}
publishedAt={post.date}
author={post.author}
image={post.image}
/>FAQ
FAQJsonLd recibe un array de preguntas y respuestas:
<FAQJsonLd
questions={[
{ question: "Que incluye?", answer: "Auth, pagos, emails y mas." },
{ question: "Que stack usa?", answer: "Next.js 16, React 19, Tailwind, Prisma." },
]}
/>Breadcrumbs
BreadcrumbJsonLd recibe un array de items con nombre y ruta:
<BreadcrumbJsonLd
items={[
{ name: "Inicio", href: "/" },
{ name: "Blog", href: "/blog" },
{ name: post.title, href: `/blog/${post.slug}` },
]}
/>Personalizar
config/site.ts-- Cambia nombre, descripción, URL y redes socialesapp/layout.tsx-- Agregakeywords,authorsoverificationcomponents/seo/json-ld.tsx-- Ajusta precio, rating y categoria de tu SaaSapp/sitemap.ts-- Añade nuevas páginas estáticasapp/robots.ts-- Bloquea rutas adicionales si es necesario
// Ejemplo: agregar verificación de Google
export const metadata: Metadata = {
// ...metadata existente
verification: {
google: "tu-código-de-google",
},
};Verifica tus datos estructurados en Google Rich Results Test y tu metadata con Twitter Card Validator.