Overview
Chapinismos uses Astro components (.astro files) for building the UI. Components are highly reusable, support props, and can include scoped styles and client-side scripts.
Component Architecture
Component Categories
components/
├── Layout Components # Header, Footer, Navigation
├── Feature Components # SearchBox, WordCard, Ticker
├── Home Components # Homepage sections
├── Icon Components # SVG icons
└── Schema Components # JSON-LD structured data
Core Components
Main site header with logo, navigation, language switcher, and theme toggle.
src/components/Header.astro
---
import { getLangFromUrl } from "../utils/i18n" ;
import Logo from "./icons/Logo.astro" ;
import LanguageSwitcher from "./LanguageSwitcher.astro" ;
import Navigation from "./Navigation.astro" ;
import MobileMenu from "./MobileMenu.astro" ;
import ThemeToggle from "./ThemeToggle.astro" ;
const lang = getLangFromUrl ( Astro . url );
const prefix = lang === "en" ? "/en" : "/es" ;
---
< header class = "relative mx-auto max-w-[1100px] px-4 py-4" transition:persist >
< div class = "flex items-center justify-between gap-4" >
< a href = { ` ${ prefix } /` } class = "flex items-center gap-2" >
< Logo height = { 75 } />
</ a >
<!-- Desktop navigation -->
< nav class = "hidden items-center gap-2 md:flex" >
< Navigation />
< LanguageSwitcher />
< ThemeToggle />
</ nav >
<!-- Mobile navigation -->
< div class = "flex items-center gap-2 md:hidden" >
< LanguageSwitcher />
< ThemeToggle isMobile = { true } />
< MobileMenu />
</ div >
</ div >
</ header >
None - Uses Astro.url to detect language
Auto-detects language from URL
Responsive (desktop/mobile layouts)
Persistent across page transitions
import Header from "../components/Header.astro";
< Header />
WordCard Component
Displays a word preview card with category badge.
src/components/WordCard.astro
---
import type { CollectionEntry } from "astro:content" ;
import { getCategoryColor } from "../utils/categoryColors" ;
import { useTranslations } from "../utils/i18n" ;
import { ArrowRight } from "@lucide/astro" ;
interface Props {
entry : CollectionEntry < "words-es" | "words-en" >;
lang : string ;
titleLevel ?: "h3" | "h4" ;
}
const { entry , lang , titleLevel = "h4" } = Astro . props ;
const t = useTranslations ( lang );
---
< a
href = { `/ ${ lang } /palabras/ ${ entry . slug } /` }
class = "word-related-card rounded-lg border p-4 transition-all hover:-translate-y-1"
>
{ entry . data . category && (
< span
class = "absolute top-3 right-3 rounded-full px-2 py-0.5 text-xs"
style = { `background-color: ${ getCategoryColor ( entry . data . category ) } ;` }
>
{ entry . data . category }
</ span >
) }
{ titleLevel === "h3" ? (
< h3 class = "m-0 mb-2 font-semibold" > { entry . data . word } </ h3 >
) : (
< h4 class = "m-0 mb-2 font-semibold" > { entry . data . word } </ h4 >
) }
< p class = "m-0 line-clamp-2 text-sm" > { entry . data . meaning } </ p >
< span class = "mt-2 inline-flex items-center gap-1 text-xs" >
{ t ( "word.learn_more" ) }
< ArrowRight size = { 12 } />
</ span >
</ a >
Prop Type Required Default Description entryCollectionEntry Yes - Word data from content collection langstring Yes - Current language (es/en) titleLevel”h3” | “h4” No ”h4” Heading level for SEO
TypeScript props with validation
Dynamic category colors
Hover animations
Line clamping for long text
import WordCard from "../components/WordCard.astro";
{ words . map (( entry ) => (
< WordCard entry = { entry } lang = { lang } titleLevel = "h3" />
)) }
SearchBox Component
Search input with keyboard shortcut support.
src/components/SearchBox.astro
---
interface Props {
placeholder ?: string ;
}
const { placeholder = "Search..." } = Astro . props ;
---
< form role = "search" class = "search-form" >
< input
type = "search"
name = "q"
placeholder = { placeholder }
class = "search-input"
autocomplete = "off"
aria-label = "Search words"
/>
</ form >
< style >
.search-input {
width : 100 % ;
padding : 0.75 rem 1 rem ;
border-radius : 0.5 rem ;
border : 1 px solid var ( --border );
background : var ( --card );
color : var ( --text );
font-size : 1 rem ;
}
.search-input:focus {
outline : 2 px solid var ( --primary );
border-color : var ( --primary );
}
</ style >
Homepage Components
HeroSection Component
Homepage hero with title, subtitle, and search.
src/components/home/HeroSection.astro
---
import { Info } from "@lucide/astro" ;
import SearchBox from "../SearchBox.astro" ;
import { useTranslations } from "../../utils/i18n" ;
interface Props {
lang : string ;
}
const { lang } = Astro . props ;
const t = useTranslations ( lang );
---
< section class = "mx-auto max-w-[1100px]" >
< div class = "card-with-gradient m-6 grid gap-3.5" >
< h1 class = "gradient-text mx-4 mt-4 text-[clamp(1.8rem,2.4vw,2.6rem)]" >
{ t ( "home.title" ) }
</ h1 >
< p class = "text-muted mx-4 text-base" >
{ t ( "home.subtitle" ) }
</ p >
< div class = "mx-4 mb-4" >
< SearchBox placeholder = { t ( "home.search.placeholder" ) } />
</ div >
< div class = "text-muted mx-4 mb-4 hidden md:block" >
< span class = "inline-flex items-center gap-2" >
< Info size = { 16 } />
{ t ( "home.search.tip" ) }
< kbd class = "rounded border px-2 py-0.5" > / </ kbd >
{ t ( "home.search.tip2" ) }
</ span >
</ div >
</ div >
</ section >
FeaturedWords Component
Grid of featured word cards.
src/components/home/FeaturedWords.astro
---
import WordCard from "../WordCard.astro" ;
import { useTranslations } from "../../utils/i18n" ;
interface Props {
words : any [];
lang : string ;
}
const { words , lang } = Astro . props ;
const t = useTranslations ( lang );
---
< section class = "mx-auto max-w-[1100px] px-6" >
< h2 class = "mb-6 text-2xl font-bold" > { t ( "home.featured.title" ) } </ h2 >
< div class = "grid grid-cols-[repeat(auto-fill,minmax(260px,1fr))] gap-3.5" >
{ words . map (( entry ) => (
< WordCard entry = { entry } lang = { lang } titleLevel = "h3" />
)) }
</ div >
</ section >
Schema Components
JSON-LD structured data components for SEO.
WordSchema Component
src/components/schemas/WordSchema.astro
---
interface Props {
lang : string ;
siteUrl : string ;
word : any ;
slug : string ;
}
const { lang , siteUrl , word , slug } = Astro . props ;
const schema = {
"@context" : "https://schema.org" ,
"@type" : "DefinedTerm" ,
"name" : word . word ,
"description" : word . meaning ,
"inDefinedTermSet" : ` ${ siteUrl } / ${ lang } /indice/` ,
"url" : ` ${ siteUrl } / ${ lang } /palabras/ ${ slug } /` ,
};
---
< script type = "application/ld+json" set:html = { JSON . stringify ( schema ) } />
Creating New Components
Basic Component Template
Create the file
Create a new .astro file in src/components/: touch src/components/MyComponent.astro
Define the component
---
// TypeScript props interface
interface Props {
title : string ;
description ?: string ;
}
const { title , description } = Astro . props ;
---
< div class = "my-component" >
< h2 > { title } </ h2 >
{ description && < p > { description } </ p > }
</ div >
< style >
.my-component {
padding : 1 rem ;
border-radius : 0.5 rem ;
}
</ style >
Use the component
---
import MyComponent from "../components/MyComponent.astro" ;
---
< MyComponent title = "Hello" description = "World" />
Component with Client Script
For interactive components, add a <script> tag:
---
interface Props {
buttonText : string ;
}
const { buttonText } = Astro . props ;
---
< button id = "my-button" class = "btn" > { buttonText } </ button >
< script >
document . addEventListener ( "astro:page-load" , () => {
const button = document . getElementById ( "my-button" );
button ?. addEventListener ( "click" , () => {
alert ( "Clicked!" );
});
});
</ script >
Use astro:page-load event for scripts that should work with View Transitions.
Component with Slots
Use slots for flexible content:
---
interface Props {
title : string ;
}
const { title } = Astro . props ;
---
< section class = "card" >
< h2 > { title } </ h2 >
< div class = "content" >
< slot /> <!-- Default slot -->
</ div >
< footer >
< slot name = "footer" /> <!-- Named slot -->
</ footer >
</ section >
Usage:
< MyCard title = "Title" >
< p > This goes in the default slot </ p >
< div slot = "footer" > Footer content </ div >
</ MyCard >
Best Practices
Use TypeScript interfaces for props
interface Props {
title : string ;
count ?: number ; // Optional prop
}
const { title , count = 0 } = Astro.props; // Default value
Each component should have a single, clear purpose. Break large components into smaller, reusable pieces.
Styles in <style> tags are automatically scoped to the component: < style >
.my-class {
color : red ; /* Only affects this component */
}
</ style >
Use CSS custom properties for theming
< style >
.card {
background : var ( --card );
color : var ( --text );
border : 1 px solid var ( --border );
}
</ style >
For components that need translations: ---
import { useTranslations } from "../utils/i18n" ;
interface Props {
lang : string ;
}
const { lang } = Astro . props ;
const t = useTranslations ( lang );
---
Component Utilities
Category Colors
import { getCategoryColor } from "../utils/categoryColors" ;
const color = getCategoryColor ( "sustantivo" ); // Returns hex color
Translations
import { useTranslations } from "../utils/i18n" ;
const t = useTranslations ( "es" );
const title = t ( "home.title" );
Next Steps
Layouts Learn about layout components
Styling Deep dive into styling
Internationalization Work with translations