Skip to main content
Chapinismos implements comprehensive SEO with structured data (JSON-LD), meta tags, Open Graph, and hreflang for multilingual support.

Base Layout Meta Tags

The src/layouts/Base.astro component handles all SEO meta tags:

Primary Meta Tags

---
const {
  title = "Chapinismos",
  description = "Glosario de chapinismos guatemaltecos con definiciones y ejemplos",
  keywords = "chapinismos, diccionario guatemalteco, palabras chapinas, guatemaltequismos, jerga guatemalteca, modismos Guatemala",
  ogImage = "/og-image.svg",
  canonicalUrl = Astro.url.pathname,
  type = "website",
  lang = "es",
  alternateUrls = {},
} = Astro.props;

const siteUrl = Astro.site || "https://chapinismos.org";
const fullCanonicalUrl = new URL(canonicalUrl, siteUrl).toString();
const fullOgImage = new URL(ogImage, siteUrl).toString();
---

<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  
  <title>{title}</title>
  <meta name="title" content={title} />
  <meta name="description" content={description} />
  <meta name="keywords" content={keywords} />
  <meta name="author" content="Diccionario Chapín" />
  <meta name="robots" content="index, follow" />
  <meta name="language" content="Spanish" />
  <link rel="canonical" href={fullCanonicalUrl} />
</head>
Key features:
  • Canonical URLs prevent duplicate content issues
  • Robots directive allows indexing
  • Author metadata for content attribution

Hreflang Tags for Multilingual SEO

<!-- Hreflang tags for multilingual SEO -->
{
  alternateUrls.es && (
    <link rel="alternate" hreflang="es" href={new URL(alternateUrls.es, siteUrl).toString()} />
  )
}
{
  alternateUrls.en && (
    <link rel="alternate" hreflang="en" href={new URL(alternateUrls.en, siteUrl).toString()} />
  )
}
{
  alternateUrls.xDefault && (
    <link
      rel="alternate"
      hreflang="x-default"
      href={new URL(alternateUrls.xDefault, siteUrl).toString()}
    />
  )
}
Usage in pages:
<Base
  lang={lang}
  alternateUrls={{
    es: "/es/buscar/",
    en: "/en/buscar/",
    xDefault: "/es/buscar/",
  }}
/>
This tells search engines:
  • Spanish version is at /es/buscar/
  • English version is at /en/buscar/
  • Default language for unlisted regions is Spanish

Open Graph Tags

const ogLocale = lang === "en" ? "en_US" : "es_GT";
const alternateOgLocale = lang === "en" ? "es_GT" : "en_US";
<!-- Open Graph / Facebook -->
<meta property="og:type" content={type} />
<meta property="og:url" content={fullCanonicalUrl} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={fullOgImage} />
<meta property="og:locale" content={ogLocale} />
{
  alternateUrls.es && alternateUrls.en && (
    <meta property="og:locale:alternate" content={alternateOgLocale} />
  )
}
<meta property="og:site_name" content="Diccionario Chapín" />
Benefits:
  • Rich previews on Facebook, LinkedIn, WhatsApp
  • Proper language tagging for international content
  • Custom Open Graph images for better engagement

Twitter Card Tags

<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={fullCanonicalUrl} />
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={fullOgImage} />
Card type:
  • summary_large_image displays a large image preview
  • Optimized for sharing on X/Twitter

Geographic Meta Tags

<meta name="geo.region" content="GT" />
<meta name="geo.placename" content="Guatemala" />
Indicates content is targeted to Guatemala, helping with local search rankings.

Structured Data Components

Chapinismos uses multiple schema components for rich search results:

OrganizationSchema

Location: src/components/schemas/OrganizationSchema.astro Defines the organization behind the site (automatically included in Base layout):
<OrganizationSchema lang={lang} siteUrl={siteUrl} />

WebsiteSchema

Location: src/components/schemas/WebsiteSchema.astro
---
import { useTranslations } from "../../utils/i18n";

interface Props {
  lang: string;
  siteUrl: string | URL;
}

const { lang, siteUrl } = Astro.props;
const t = useTranslations(lang);

const websiteSchema = {
  "@context": "https://schema.org",
  "@type": "WebSite",
  name: t("schema.site.name"),
  alternateName: t("schema.site.alternateName"),
  url: siteUrl.toString(),
  description: t("schema.site.fullDescription"),
  inLanguage: ["es", "en"],
  potentialAction: {
    "@type": "SearchAction",
    target: {
      "@type": "EntryPoint",
      urlTemplate: new URL(`/${lang}/buscar/?q={search_term_string}`, siteUrl).toString(),
    },
    "query-input": "required name=search_term_string",
  },
  publisher: {
    "@type": "Organization",
    name: t("schema.site.name"),
  },
};
---

<script type="application/ld+json" set:html={JSON.stringify(websiteSchema)} />
Benefits:
  • Enables Google’s sitewide search box in results
  • Defines site structure and purpose
  • Multi-language support declaration

WordSchema (DefinedTerm)

Location: src/components/schemas/WordSchema.astro
---
import { useTranslations } from "../../utils/i18n";

interface Props {
  lang: string;
  siteUrl: string | URL;
  word: {
    word: string;
    meaning: string;
    category?: string;
    synonyms?: string[];
  };
  slug: string;
}

const { lang, siteUrl, word, slug } = Astro.props;
const t = useTranslations(lang);
const wordUrl = new URL(`/${lang}/palabras/${slug}/`, siteUrl).toString();

// JSON-LD DefinedTerm Schema
const definedTermSchema = {
  "@context": "https://schema.org",
  "@type": "DefinedTerm",
  "@id": wordUrl,
  name: word.word,
  description: word.meaning,
  inDefinedTermSet: {
    "@type": "DefinedTermSet",
    name: t("schema.dictionary.name"),
    url: siteUrl.toString(),
  },
  termCode: slug,
  ...(word.category && { additionalType: word.category }),
  ...(word.synonyms?.length && { sameAs: word.synonyms }),
  inLanguage: lang,
  isPartOf: {
    "@type": "WebSite",
    name: t("schema.site.name"),
    url: siteUrl.toString(),
  },
};

// JSON-LD Article Schema
const articleSchema = {
  "@context": "https://schema.org",
  "@type": "Article",
  headline: t("schema.word.headline", { word: word.word }),
  description: word.meaning,
  url: wordUrl,
  inLanguage: lang,
  about: {
    "@type": "DefinedTerm",
    name: word.word,
  },
  publisher: {
    "@type": "Organization",
    name: t("schema.site.name"),
    url: siteUrl.toString(),
  },
  mainEntityOfPage: {
    "@type": "WebPage",
    "@id": wordUrl,
  },
};
---

<script type="application/ld+json" set:html={JSON.stringify(definedTermSchema)} />
<script type="application/ld+json" set:html={JSON.stringify(articleSchema)} />
Schema features:
  • DefinedTerm: Marks content as a dictionary definition
  • Article: Provides article context for search engines
  • Links to parent DefinedTermSet (the dictionary)
  • Conditional fields (category, synonyms) only if present

FAQSchema

Location: src/components/schemas/FAQSchema.astro
---
import { useTranslations } from "../../utils/i18n";

interface Props {
  lang: string;
  word: {
    word: string;
    meaning: string;
    examples?: string[];
    category?: string;
    synonyms?: string[];
  };
}

const { lang, word } = Astro.props;
const t = useTranslations(lang);

// Build FAQ items dynamically based on available content
const faqItems: { question: string; answer: string }[] = [];

// Q1: What does {word} mean?
faqItems.push({
  question: t("schema.faq.meaning.question", { word: word.word }),
  answer: word.meaning,
});

// Q2: How to use {word}? (if examples exist)
if (word.examples?.length) {
  const examplesFormatted = word.examples.map((ex) => `"${ex}"`).join("; ");
  faqItems.push({
    question: t("schema.faq.usage.question", { word: word.word }),
    answer: t("schema.faq.usage.answer", { examples: examplesFormatted }),
  });
}

// Q3: What type of word is {word}? (if category exists)
if (word.category) {
  faqItems.push({
    question: t("schema.faq.category.question", { word: word.word }),
    answer: t("schema.faq.category.answer", { word: word.word, category: word.category }),
  });
}

// Q4: Synonyms (if they exist)
if (word.synonyms?.length) {
  faqItems.push({
    question: t("schema.faq.synonyms.question", { word: word.word }),
    answer: t("schema.faq.synonyms.answer", { synonyms: word.synonyms.join(", ") }),
  });
}

const faqSchema = {
  "@context": "https://schema.org",
  "@type": "FAQPage",
  mainEntity: faqItems.map((item) => ({
    "@type": "Question",
    name: item.question,
    acceptedAnswer: {
      "@type": "Answer",
      text: item.answer,
    },
  })),
};
---

<script type="application/ld+json" set:html={JSON.stringify(faqSchema)} />
Dynamic FAQ generation:
  • Always includes: Meaning question
  • Conditionally adds: Usage examples, category, synonyms
  • Creates rich FAQ snippets in Google search results
Location: src/components/schemas/BreadcrumbSchema.astro
---
import { useTranslations } from "../../utils/i18n";

interface BreadcrumbItem {
  name: string;
  url: string;
}

interface Props {
  lang: string;
  siteUrl: string | URL;
  items: BreadcrumbItem[];
}

const { lang, siteUrl, items } = Astro.props;
const t = useTranslations(lang);

const breadcrumbSchema = {
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  itemListElement: [
    {
      "@type": "ListItem",
      position: 1,
      name: t("nav.home"),
      item: new URL(`/${lang}/`, siteUrl).toString(),
    },
    ...items.map((item, index) => ({
      "@type": "ListItem",
      position: index + 2,
      name: item.name,
      item: new URL(item.url, siteUrl).toString(),
    })),
  ],
};
---

<script type="application/ld+json" set:html={JSON.stringify(breadcrumbSchema)} />
Usage:
<BreadcrumbSchema
  slot="schema"
  lang={lang}
  siteUrl={siteUrl}
  items={[
    { name: t("nav.search"), url: `/${lang}/buscar/` },
  ]}
/>
Benefits:
  • Shows breadcrumb trail in search results
  • Improves site structure understanding
  • Better navigation for users from search

SearchPageSchema

Location: src/components/schemas/SearchPageSchema.astro Marks the search page with appropriate schema (used in buscar.astro).

Using Schemas in Pages

Schemas are inserted via named slots in the Base layout:
<Base
  title={t("search.title") + " — " + t("schema.site.name")}
  description={t("search.description")}
  keywords={t("search.keywords")}
  lang={lang}
  alternateUrls={alternateUrls}
>
  <BreadcrumbSchema
    slot="schema"
    lang={lang}
    siteUrl={siteUrl}
    items={[{ name: t("nav.search"), url: `/${lang}/buscar/` }]}
  />
  <SearchPageSchema
    slot="schema"
    lang={lang}
    siteUrl={siteUrl}
    title={t("search.title")}
    description={t("search.description")}
    url={`/${lang}/buscar/`}
  />
  
  <!-- Page content -->
</Base>
Base layout schema slot:
<head>
  <!-- ... other head content ... -->
  
  <!-- Global schemas -->
  <OrganizationSchema lang={lang} siteUrl={siteUrl} />
  <WebsiteSchema lang={lang} siteUrl={siteUrl} />
  
  <!-- Page-specific schemas -->
  <slot name="schema" />
</head>

Performance Optimizations

Preconnect to External Resources

<link rel="preconnect" href="https://api-gateway.umami.dev" crossorigin />
<link rel="dns-prefetch" href="https://api-gateway.umami.dev" />
<link rel="preconnect" href="https://cloud.umami.is" crossorigin />
<link rel="dns-prefetch" href="https://cloud.umami.is" />
Establishes early connections to analytics servers.

Preload Background Image

<link rel="preload" href="/images/bg.webp" as="image" fetchpriority="low" />
Preloads non-critical images with low priority to avoid blocking rendering.

Async Analytics

<script
  async
  src="https://cloud.umami.is/script.js"
  data-website-id="7e8f3679-b1bd-4ee9-97e8-cc136e9416f2"
  data-endpoint="/api/umami-proxy"
></script>
Loads analytics asynchronously to prevent render blocking.

Sitemap Integration

<link rel="sitemap" href="/sitemap-index.xml" />
Points search engines to the XML sitemap for complete site indexing.

Best Practices

Prevent duplicate content penalties:
const fullCanonicalUrl = new URL(canonicalUrl, siteUrl).toString();
<link rel="canonical" href={fullCanonicalUrl} />
Helps Google show the right language version:
alternateUrls={{
  es: "/es/palabras/chucho/",
  en: "/en/palabras/chucho/",
  xDefault: "/es/palabras/chucho/",
}}
Match schema to content type:
  • DefinedTerm for dictionary entries
  • FAQPage for Q&A content
  • BreadcrumbList for navigation
  • SearchAction for search functionality
Use Google’s Rich Results Test:
Ensure images:
  • Are at least 1200x630px
  • Use absolute URLs
  • Have relevant alt text
  • Are properly sized (< 300KB)

Build docs developers (and LLMs) love