Overview
The wedding website is built with Astro components, organized into reusable sections that make up the single-page application. All components are located in src/components/ and follow a consistent pattern for internationalization and styling.
Component Structure
Components are organized into three main categories:
Layout Components Base layout and navigation (Header, Footer, Layout)
Section Components Content sections for the main page
Interactive Components Forms, galleries, and dynamic elements
Layout Components
The base layout system provides structure and navigation:
Layout.astro (src/layouts/Layout.astro)
The main layout wrapper that:
Loads custom fonts (Amoresa, WeddingSerif)
Sets up global styles with TailwindCSS
Creates a centered background pattern
Provides the document structure
---
import '../styles/global.css' ;
import fondoWedding from '../assets/images/fondo-wedding.png' ;
interface Props {
title : string ;
}
const { title } = Astro . props ;
---
< ! doctype html >
< html lang = "es" >
< head >
< meta charset = "UTF-8" />
< title > { title } </ title >
<!-- Font loading, meta tags, etc -->
</ head >
< body class = "bg-[#F4EEE7] relative" >
<!-- Background pattern -->
< div class = "absolute inset-0 z-0 flex justify-center pointer-events-none" >
< div class = "w-full max-w-[1000px] bg-repeat-y bg-top"
style = { `background-image: url(' ${ fondoWedding . src } ');` } >
</ div >
</ div >
<!-- Content -->
< div class = "relative z-10" >
< slot />
</ div >
</ body >
</ html >
Header.astro (src/components/Header.astro)
Provides navigation and language switching:
Fixed header with language selector
Slide-out menu panel with anchor navigation
Responsive design with backdrop blur on scroll
src/components/Header.astro
---
import { useTranslations , type Lang } from '../i18n/index.ts' ;
interface Props {
lang ?: Lang ;
}
const { lang = 'es' } = Astro . props ;
const t = useTranslations ( lang );
---
< header id = "site-header" class = "fixed top-0 left-0 w-full z-50 pt-6" >
< div class = "w-full max-w-[1000px] mx-auto px-6 flex justify-between items-center" >
<!-- Language selector -->
< div class = "flex items-center space-x-2" >
< a href = "/" class:list = { [{ 'font-bold' : lang === 'es' }] } > { t . header . es } </ a >
< span class = "text-[#888]" > · </ span >
< a href = "/en" class:list = { [{ 'font-bold' : lang === 'en' }] } > { t . header . en } </ a >
< span class = "text-[#888]" > · </ span >
< a href = "/de" class:list = { [{ 'font-bold' : lang === 'de' }] } > { t . header . de } </ a >
</ div >
<!-- Menu toggle button -->
< button id = "menu-toggle" > ... </ button >
</ div >
</ header >
Section Components
Each section follows a consistent pattern:
Import translations and assets
Accept a lang prop (defaults to ‘es’)
Use useTranslations(lang) to get localized strings
Apply responsive TailwindCSS classes
Include client-side scripts for interactivity
Example: SectionPrincipal
The hero section demonstrates the standard component pattern:
src/components/SectionPrincipal.astro
---
import { Image } from "astro:assets" ;
import Wedding from '../assets/images/wedding.png' ;
import { useTranslations , type Lang } from '../i18n/index.ts' ;
interface Props {
lang ?: Lang ;
}
const { lang = 'es' } = Astro . props ;
const t = useTranslations ( lang );
---
< div class = "w-full max-w-[1000px] mx-auto relative z-10 px-6" >
<!-- Background image -->
< div class = "absolute inset-0 z-0 pointer-events-none"
style = { `background-image: url(' ${ Wedding . src } ');` } >
</ div >
<!-- Names -->
< div class = "pt-30 md:pt-42 mb-8 text-center" >
< h1 class = "font-['Amoresa'] text-[#1a1a1a]"
style = "font-size: clamp(3rem, 14vw, 6rem);" >
Alejandra
</ h1 >
< span class = "font-['Amoresa'] block text-[#1a1a1a]" >
&
</ span >
< h1 class = "font-['Amoresa'] text-[#1a1a1a]" >
Alexander
</ h1 >
</ div >
<!-- Date and location using translations -->
< div class = "flex flex-col items-center text-center" >
< span > { t . principal . fecha } </ span >
< hr class = "md:w-[450px] w-[350px] border-t-1 border-[#373737] my-4" >
< span > { t . principal . lugar } </ span >
</ div >
<!-- Story section -->
< div class = "text-center px-2 md:px-8" >
< h2 class = "reveal-heading font-['Amoresa'] text-[#1a1a1a]" >
{ t . historia . titulo }
</ h2 >
< div class = "font-['WeddingSerif'] space-y-6" >
< p > { t . historia . p1 } </ p >
< p > { t . historia . p2 } </ p >
< p > { t . historia . p3 } </ p >
</ div >
</ div >
</ div >
< script >
// Intersection Observer for reveal animations
const headings = document . querySelectorAll ( '.reveal-heading' );
headings . forEach ( el => {
( el as HTMLElement ). style . opacity = '0' ;
( el as HTMLElement ). style . filter = 'blur(10px)' ;
});
const observer = new IntersectionObserver (( entries ) => {
entries . forEach ( entry => {
if ( entry . isIntersecting ) {
( entry . target as HTMLElement ). style . opacity = '1' ;
( entry . target as HTMLElement ). style . filter = 'blur(0px)' ;
}
});
}, { threshold: 0.3 });
headings . forEach ( el => observer . observe ( el ));
</ script >
Available Section Components
All sections are located in src/components/ and follow similar patterns:
Content Sections
Interactive Sections
Information Sections
SectionPrincipal - Hero with couple names and story
SectionPreWeding - Pre-wedding event details
SectionCelebration - Wedding day timeline
SectionDressCode - Attire guidelines and reserved colors
SectionGift - Gift registry information
SectionContact - Wedding planner and contact info
SectionGaleria - Horizontal scrolling photo gallery
SectionPlayList - Music playlist integration
SectionForm - RSVP form with validation
SectionHospedaje - Hotel and accommodation recommendations
SectionSugerencia - Vendor recommendations (makeup, tuxedo rental)
Interactive Components
Creates a draggable photo gallery with mouse/touch support:
src/components/SectionGaleria.astro
---
import gallery1 from "../assets/images/gallery-1.png" ;
import gallery2 from "../assets/images/gallery-2.png" ;
---
< section class = "w-full max-w-[1000px] mx-auto overflow-hidden" >
< div id = "gallery-container"
class = "flex gap-3 px-6 overflow-x-auto scroll-smooth no-scrollbar" >
< div class = "flex-shrink-0 w-[42vw] md:w-[220px] aspect-[3/4]" >
< img src = { gallery1 . src } class = "w-full h-full object-cover rounded-[20px]" />
</ div >
<!-- More images... -->
</ div >
</ section >
< style >
.no-scrollbar::-webkit-scrollbar { display : none ; }
.no-scrollbar { -ms-overflow-style : none ; scrollbar-width : none ; }
#gallery-container {
cursor : grab ;
overscroll-behavior-x : contain ;
}
</ style >
< script >
const slider = document . getElementById ( 'gallery-container' );
let isDown = false ;
let startX : number ;
let scrollLeft : number ;
slider ?. addEventListener ( 'mousedown' , ( e ) => {
isDown = true ;
startX = ( e as MouseEvent ). pageX - slider . offsetLeft ;
scrollLeft = slider . scrollLeft ;
});
slider ?. addEventListener ( 'mousemove' , ( e ) => {
if ( ! isDown ) return ;
e . preventDefault ();
const x = ( e as MouseEvent ). pageX - slider . offsetLeft ;
slider . scrollLeft = scrollLeft - ( x - startX ) * 2 ;
});
</ script >
Handles guest confirmations with validation and API integration:
src/components/SectionForm.astro
---
import { useTranslations , type Lang } from '../i18n/index.ts' ;
interface Props { lang ?: Lang ; }
const { lang = 'es' } = Astro . props ;
const t = useTranslations ( lang );
---
< div class = "w-full max-w-[1000px] mx-auto" id = "asistencia" >
< h2 class = "font-['Amoresa'] text-center" > { t . form . titulo } </ h2 >
< p class = "text-center" > { t . form . subtitulo } </ p >
< input id = "nombre" type = "text" placeholder = { t . form . nombre } />
< input id = "alergia" type = "text" placeholder = { t . form . alergias_placeholder } />
< div class = "grid grid-cols-2 gap-2" >
< label >
< input type = "radio" name = "asistencia" value = "pre-wedding" />
{ t . form . pre_wedding }
</ label >
< label >
< input type = "radio" name = "asistencia" value = "boda" />
{ t . form . boda }
</ label >
< label >
< input type = "radio" name = "asistencia" value = "ambos" />
{ t . form . ambos }
</ label >
< label >
< input type = "radio" name = "asistencia" value = "ninguno" />
{ t . form . ninguno }
</ label >
</ div >
< button id = "btn-enviar"
data-msg-success = { t . form . msg_success }
data-msg-error = { t . form . msg_error }
data-msg-required = { t . form . msg_required } >
{ t . form . enviar }
</ button >
< p id = "form-msg" class = "hidden" ></ p >
</ div >
< script >
const btn = document . getElementById ( 'btn-enviar' ) as HTMLButtonElement ;
const msg = document . getElementById ( 'form-msg' );
btn ?. addEventListener ( 'click' , async () => {
const nombre = ( document . getElementById ( 'nombre' ) as HTMLInputElement )?. value ?. trim ();
const alergia = ( document . getElementById ( 'alergia' ) as HTMLInputElement )?. value ?. trim ();
const asistencia = ( document . querySelector ( 'input[name="asistencia"]:checked' ) as HTMLInputElement )?. value ;
if ( ! nombre || ! asistencia ) {
msg ! . textContent = btn . dataset . msgRequired ;
msg ! . classList . add ( 'text-red-500' );
return ;
}
btn . setAttribute ( 'disabled' , 'true' );
try {
const res = await fetch ( '/api/confirmar' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({ nombre , alergia , asistencia })
});
const json = await res . json ();
if ( json . ok ) {
msg ! . textContent = btn . dataset . msgSuccess ;
msg ! . classList . add ( 'text-green-600' );
}
} catch ( e ) {
msg ! . textContent = btn . dataset . msgError ;
msg ! . classList . add ( 'text-red-500' );
btn . removeAttribute ( 'disabled' );
}
});
</ script >
Usage in Pages
Components are composed in page files to create the full experience:
---
import Layout from '../layouts/Layout.astro' ;
import Header from '../components/Header.astro' ;
import SectionPrincipal from '../components/SectionPrincipal.astro' ;
import SectionGaleria from '../components/SectionGaleria.astro' ;
import SectionForm from '../components/SectionForm.astro' ;
import Footer from '../components/Footer.astro' ;
---
< Layout title = "Nuestra Historia - Alejandra & Alexander" >
< Header />
< main class = "relative min-h-screen" >
< SectionPrincipal />
< SectionGaleria />
<!-- More sections... -->
< SectionForm />
</ main >
< Footer />
</ Layout >
Best Practices
Always accept a lang prop
Every component should accept an optional lang?: Lang prop and default to 'es'.
Use translations consistently
Import and use useTranslations(lang) at the top of every component that displays text.
Apply responsive design
Use clamp() for font sizes and md: breakpoints for layout changes.
Keep scripts inline
Include component-specific JavaScript in <script> tags within the component file.
Maintain z-index hierarchy
Background elements use z-0, content uses z-10, header uses z-50, menu uses z-100.
All components use TypeScript for type safety. The Props interface defines component properties, and Astro provides built-in type checking.