Skip to main content
The Chapinismos search feature is a client-side implementation that filters through all dictionary words in real-time without requiring a backend API.

Search Page Implementation

The search page is located at src/pages/[lang]/buscar.astro and uses Astro’s static site generation with client-side JavaScript for interactivity.

Data Preparation

All words are loaded at build time and serialized to JSON for client-side filtering:
---
import { getCollection } from "astro:content";

const { lang } = Astro.params;
const collectionName = lang === "es" ? "words-es" : "words-en";
const allWords = await getCollection(collectionName);

const wordData = JSON.stringify(
  allWords.map((w) => ({
    slug: w.slug,
    word: w.data.word,
    meaning: w.data.meaning,
    category: w.data.category,
  }))
);
---
This creates a lightweight array with only the essential fields needed for searching.

Search Algorithm

Text Normalization

The search normalizes text to handle accents and case-insensitivity:
function normalizeText(text) {
  return text
    .toLowerCase()
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "");
}
How it works:
  1. Convert to lowercase: "Guacamaya""guacamaya"
  2. Decompose accented characters: "búho""búho" (separates accent marks)
  3. Remove diacritical marks: "búho""buho"
Example:
normalizeText("Búho"); // Returns: "buho"
normalizeText("guacamaya"); // Returns: "guacamaya"
This allows users to search for “buho” and find “búho”.

Search Function

The core search logic filters across multiple fields:
function search(query) {
  if (!query.trim()) {
    resultsContainer.innerHTML = "";
    return;
  }

  const normalized = normalizeText(query);
  const results = words.filter((w) => {
    const word = normalizeText(w.word);
    const meaning = normalizeText(w.meaning);
    const category = normalizeText(w.category);
    return (
      word.includes(normalized) || 
      meaning.includes(normalized) || 
      category.includes(normalized)
    );
  });

  renderResults(results);
}
Searchable fields:
  • word - The Guatemalan slang term
  • meaning - The definition/meaning
  • category - Word type (sustantivo, verbo, etc.)

URL Parameter Support

The search supports pre-filling via URL query parameters:
// Search on page load if there's a query in the URL
document.addEventListener("astro:page-load", () => {
  const params = new URLSearchParams(window.location.search);
  const query = params.get("q");
  if (query && input) {
    input.value = query;
    search(query);
  }
});
Usage examples:
  • /es/buscar/?q=chucho - Searches for “chucho”
  • /en/buscar/?q=verbo - Searches for “verbo”

Results Rendering

No Results State

When no matches are found:
if (results.length === 0) {
  resultsContainer.innerHTML = `
    <div class="text-center py-12 px-4 border border-theme rounded-lg bg-card">
      <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" 
           stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
           class="text-muted mx-auto mb-4">
        <circle cx="11" cy="11" r="8"></circle>
        <path d="m21 21-4.35-4.35"></path>
      </svg>
      <p class="text-theme text-lg font-semibold mb-2">
        ${t.noResults}
      </p>
      <p class="text-muted">
        ${t.tryAnother}
      </p>
    </div>
  `;
  return;
}

Results Grid

Results are displayed in a responsive grid:
const prefix = lang === "es" ? "/es" : "/en";

resultsContainer.innerHTML = `
  <p class="text-muted mb-4 text-sm">
    ${results.length} ${t.results}
  </p>
  <div class="grid grid-cols-[repeat(auto-fill,minmax(260px,1fr))] gap-3.5">
    ${results
      .map(
        (item) => `
      <a href="${prefix}/palabras/${item.slug}/" 
         class="block no-underline p-4 border border-theme rounded-lg bg-card shadow-theme-sm transition-all duration-200 hover:-translate-y-0.5 hover:shadow-theme-md"
         data-umami-event="Search Result Click"
         data-umami-event-word="${item.word}"
         data-umami-event-category="${item.category}">
        <div class="flex items-center justify-between mb-2">
          <h3 class="m-0 text-xl font-semibold text-theme">
            ${item.word}
          </h3>
          <span class="inline-block py-0.5 px-2 rounded text-xs font-medium text-white" style="background-color: ${getCategoryColor(item.category)};">
            ${getCategoryLabel(item.category)}
          </span>
        </div>
        <p class="m-0 text-secondary text-sm leading-relaxed">
          ${item.meaning}
        </p>
      </a>
    `
      )
      .join("")}
  </div>
`;

Category Colors

Categories are color-coded for visual distinction:
---
import { getCategoryColor } from "../../utils/categoryColors";

const categoryColorsData = JSON.stringify({
  sustantivo: getCategoryColor("sustantivo"),
  noun: getCategoryColor("noun"),
  verbo: getCategoryColor("verbo"),
  verb: getCategoryColor("verb"),
  adjetivo: getCategoryColor("adjetivo"),
  adjective: getCategoryColor("adjective"),
  adverbio: getCategoryColor("adverbio"),
  adverb: getCategoryColor("adverb"),
  expresion: getCategoryColor("expresion"),
  expression: getCategoryColor("expression"),
  interjección: getCategoryColor("interjección"),
  interjection: getCategoryColor("interjection"),
});
---
In the client script:
function getCategoryColor(category) {
  return categoryColors[category] || "#1e40af";
}

Event Handling

Input Event Listener

Search triggers on every keystroke:
input?.addEventListener("input", (e) => {
  search(e.target.value);
});

Form Submission Prevention

Prevent page reload on form submit:
const form = document.querySelector("form");
form?.addEventListener("submit", (e) => {
  e.preventDefault();
  if (input) {
    search(input.value);
  }
});

Analytics Integration

Search results are tracked with Umami analytics:
<a href="${prefix}/palabras/${item.slug}/" 
   data-umami-event="Search Result Click"
   data-umami-event-word="${item.word}"
   data-umami-event-category="${item.category}">
This tracks:
  • Which words users click from search results
  • Category distribution of clicked words
  • Search effectiveness

Internationalization

Search translations are passed to the client:
<script
  is:inline
  define:vars={{
    searchTranslations: JSON.stringify({
      noResults: t("search.noResults"),
      tryAnother: t("search.tryAnother"),
      results: t("search.results"),
      categories: {
        sustantivo: t("search.category.sustantivo"),
        verbo: t("search.category.verbo"),
        adjetivo: t("search.category.adjetivo"),
        adverbio: t("search.category.adverbio"),
        expresion: t("search.category.expresion"),
        interjección: t("search.category.interjección"),
        noun: t("search.category.noun"),
        verb: t("search.category.verb"),
        adjective: t("search.category.adjective"),
        adverb: t("search.category.adverb"),
        expression: t("search.category.expression"),
        interjection: t("search.category.interjection"),
      },
    }),
    lang,
  }}
>

Performance Considerations

All filtering happens in the browser, which:
  • Provides instant results with no network latency
  • Works offline after initial page load
  • Scales well for dictionaries with hundreds of words
  • May need optimization for thousands of entries
Only essential fields are serialized:
{
  slug: w.slug,
  word: w.data.word,
  meaning: w.data.meaning,
  category: w.data.category,
}
Examples, synonyms, and other heavy data are excluded to reduce bundle size.
Currently, search triggers on every keystroke. For larger datasets, consider debouncing:
let debounceTimer;
input?.addEventListener("input", (e) => {
  clearTimeout(debounceTimer);
  debounceTimer = setTimeout(() => {
    search(e.target.value);
  }, 300); // Wait 300ms after user stops typing
});

SEO Implementation

The search page includes structured data:
<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/`}
/>
This helps search engines understand the page’s purpose and structure.

Build docs developers (and LLMs) love