Skip to main content

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
src/layouts/Layout.astro
---
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:
  1. Import translations and assets
  2. Accept a lang prop (defaults to ‘es’)
  3. Use useTranslations(lang) to get localized strings
  4. Apply responsive TailwindCSS classes
  5. 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]">
      &amp;
    </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:
  • 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

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>

SectionForm - RSVP Form

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:
src/pages/index.astro
---
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

1

Always accept a lang prop

Every component should accept an optional lang?: Lang prop and default to 'es'.
2

Use translations consistently

Import and use useTranslations(lang) at the top of every component that displays text.
3

Apply responsive design

Use clamp() for font sizes and md: breakpoints for layout changes.
4

Keep scripts inline

Include component-specific JavaScript in <script> tags within the component file.
5

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.

Build docs developers (and LLMs) love