EmpiezaTuSaaS
Funcionalidades

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:

config/site.ts
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:

app/layout.tsx
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:

app/blog/[slug]/page.tsx
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:

app/sitemap.ts
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:

app/robots.ts
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:

ComponenteDonde se usaProposito
OrganizationJsonLdLanding pageIdentifica tu marca
SoftwareAppJsonLdLanding pageProducto SaaS con precio y rating
ArticleJsonLdPost de blogRich snippet de articulo
FAQJsonLdCualquier páginaPreguntas frecuentes
BreadcrumbJsonLdCualquier páginaMigas de pan

Componente base

Todos los schemas usan un componente interno que inyecta el JSON-LD:

components/seo/json-ld.tsx
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:

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." },
  ]}
/>

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

  1. config/site.ts -- Cambia nombre, descripción, URL y redes sociales
  2. app/layout.tsx -- Agrega keywords, authors o verification
  3. components/seo/json-ld.tsx -- Ajusta precio, rating y categoria de tu SaaS
  4. app/sitemap.ts -- Añade nuevas páginas estáticas
  5. app/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.

On this page