Component Architecture
The FiNAN website uses a component-driven architecture built with Astro. This page explains the component types, prop patterns, and reusable design patterns that make the codebase maintainable and scalable.
Component Types
Section titled “Component Types”The FiNAN website uses three distinct types of components, each serving a specific purpose in the architecture.
1. Layout Components
Section titled “1. Layout Components”Purpose: Provide structural wrappers and consistent page layouts.
Examples: BaseLayout.astro, Navbar.astro, Footer.astro
Characteristics:
- Wrap entire pages or major sections
- Handle SEO metadata and document structure
- Provide consistent navigation and branding
- Usually accept minimal props (title, description, etc.)
---interface Props { title: string; description: string; ogImage?: string;}
const { title, description, ogImage } = Astro.props;---
<!doctype html><html lang="en"> <head> <title>{title}</title> <meta name="description" content={description} /> <!-- SEO metadata, stylesheets, etc. --> </head> <body> <Navbar /> <slot /> <Footer /> </body></html>2. Content Components
Section titled “2. Content Components”Purpose: Display dynamic data from configuration files with consistent styling.
Examples: Committee.astro, FAQAccordion.astro, Partners.astro
Characteristics:
- Accept structured data as props (arrays of objects)
- Use TypeScript interfaces for type safety
- Render lists, grids, or structured content
- Often used on multiple pages with different data
---import type { CommitteeMember } from '@/data/representation/committee/types';
interface Props { members: CommitteeMember[]; country: string;}
const { members, country } = Astro.props;---
<section class="committee-section"> <h2>{country} Committee</h2> <div class="grid"> {members.map((member) => ( <div class="member-card"> <img src={member.image.src} alt={member.name} /> <h3>{member.name}</h3> <p>{member.role}</p> </div> ))} </div></section>3. Utility Components
Section titled “3. Utility Components”Purpose: Provide reusable functionality or metadata without visual output.
Examples: StructuredData.astro (JSON-LD schema markup)
Characteristics:
- May not render visible content
- Provide SEO, accessibility, or technical functionality
- Often render
<script>tags or metadata - Enhance page capabilities without affecting layout
---interface Props { type: 'Organization' | 'FAQPage'; data: Record<string, any>;}
const { type, data } = Astro.props;---
<script type="application/ld+json" set:html={JSON.stringify(data)} />Props Patterns
Section titled “Props Patterns”The FiNAN website follows consistent patterns for component props to maintain type safety and developer experience.
TypeScript Interface Pattern
Section titled “TypeScript Interface Pattern”All components define props using TypeScript interfaces, ensuring type safety throughout the application.
// ✅ Good - Explicit interface with typesinterface Props { title: string; members: CommitteeMember[]; isVisible?: boolean; // Optional props use ?}
const { title, members, isVisible = true } = Astro.props; // Default values// ❌ Bad - No types, prone to errorsconst { title, members } = Astro.props;Image Props Pattern
Section titled “Image Props Pattern”Components that display images use Astro’s Image type for optimization:
import type { ImageMetadata } from 'astro';
interface Props { image: ImageMetadata; // Type-safe image imports alt: string;}This ensures images are properly optimized at build time and prevents broken image references.
Data Array Props Pattern
Section titled “Data Array Props Pattern”Components that display lists use typed arrays from data configuration files:
import type { Partner } from '@/data/partnersData';
interface Props { partners: Partner[]; // Array of typed objects limit?: number; // Optional filtering}Optional Props with Defaults
Section titled “Optional Props with Defaults”Use optional props with default values for flexible components:
---interface Props { variant?: 'primary' | 'secondary'; isFullWidth?: boolean;}
const { variant = 'primary', isFullWidth = false} = Astro.props;---
<div class={`component ${variant} ${isFullWidth ? 'full-width' : ''}`}> <slot /></div>Reusable Patterns
Section titled “Reusable Patterns”Data-Driven Rendering
Section titled “Data-Driven Rendering”Components accept configuration data as props, allowing reuse across pages:
Pattern:
- Define data in
src/data/directory - Import data in page files
- Pass data to component as props
- Component renders based on data
Example:
---import Committee from '@/components/Committee.astro';import { finlandCommittee } from '@/data/representation/committee/finlandCommittee';---
<Committee members={finlandCommittee} country="Finland" />import type { CommitteeMember } from './types';import exampleImage from '@/assets/images/committee/finland/john-doe.webp';
export const finlandCommittee: CommitteeMember[] = [ { name: 'John Doe', role: 'President', image: exampleImage, }, // ... more members];---import type { CommitteeMember } from '@/data/representation/committee/types';
interface Props { members: CommitteeMember[]; country: string;}
const { members, country } = Astro.props;---
<section> <h2>{country} Committee</h2> {members.map((member) => ( <div> <img src={member.image.src} alt={member.name} /> <h3>{member.name}</h3> <p>{member.role}</p> </div> ))}</section>Slot-Based Composition
Section titled “Slot-Based Composition”Astro’s <slot /> enables flexible content composition:
---interface Props { title: string;}
const { title } = Astro.props;---
<section class="wrapper"> <h2>{title}</h2> <slot /> <!-- Content injected here --></section>Usage:
<Wrapper title="Our Services"> <p>Custom content goes here</p> <button>Learn More</button></Wrapper>Conditional Rendering
Section titled “Conditional Rendering”Components use TypeScript for conditional logic:
---interface Props { items: Item[]; emptyMessage?: string;}
const { items, emptyMessage = 'No items found' } = Astro.props;---
{items.length > 0 ? ( <ul> {items.map((item) => <li>{item.name}</li>)} </ul>) : ( <p class="empty-state">{emptyMessage}</p>)}Component Styling Pattern
Section titled “Component Styling Pattern”Styles are scoped to components using Astro’s built-in scoping:
---// Component logic---
<div class="component"> <!-- Markup --></div>
<style> /* Scoped styles - only apply to this component */ .component { padding: 2rem; }
/* Global styles use :global() */ :global(.override-class) { color: red; }</style>Benefits:
- No CSS class name collisions
- Easier to maintain and refactor
- Automatic dead code elimination
Component Communication
Section titled “Component Communication”Parent to Child (Props)
Section titled “Parent to Child (Props)”Standard pattern - pass data down via props:
<ChildComponent title="Example" data={arrayData} />Child to Parent (Events)
Section titled “Child to Parent (Events)”Astro components are static by default, so traditional event handling requires client-side JavaScript:
<button id="action-btn">Click Me</button>
<script> document.getElementById('action-btn')?.addEventListener('click', () => { console.log('Button clicked'); });</script>For complex interactivity, consider Astro Islands with framework components (React, Vue, Svelte).
Shared State (Data Files)
Section titled “Shared State (Data Files)”For data shared across multiple components, use centralized data files:
export const siteConfig = { organizationName: 'FiNAN', supportEmail: 'info@finan.org',};Import in multiple components:
---import { siteConfig } from '@/data/shared/appState';---
<p>Contact: {siteConfig.supportEmail}</p>Best Practices
Section titled “Best Practices”1. Keep Components Focused
Section titled “1. Keep Components Focused”Each component should have a single, clear responsibility:
- ✅
Committee.astro- Displays committee members - ✅
FAQAccordion.astro- Renders FAQ accordion - ❌
PageContent.astro- Too generic, unclear purpose
2. Use Type-Safe Props
Section titled “2. Use Type-Safe Props”Always define TypeScript interfaces for props:
// ✅ Goodinterface Props { title: string; count: number;}
// ❌ Bad - No type safetyconst { title, count } = Astro.props;3. Extract Reusable Logic
Section titled “3. Extract Reusable Logic”If multiple components share logic, extract to utility functions:
export function formatDate(date: Date): string { return new Intl.DateTimeFormat('en-US').format(date);}4. Prefer Data Configuration
Section titled “4. Prefer Data Configuration”Instead of hardcoding content in components, use data files:
// ✅ Good - Data in config fileimport { partners } from '@/data/partnersData';<Partners partners={partners} />
// ❌ Bad - Hardcoded in component<Partners partners={[{ name: 'Partner 1' }, { name: 'Partner 2' }]} />5. Document Complex Components
Section titled “5. Document Complex Components”Add comments for non-obvious logic:
---// Filter active members and sort by join dateconst activeMembers = members .filter((m) => m.isActive) .sort((a, b) => a.joinDate.getTime() - b.joinDate.getTime());---Next Steps
Section titled “Next Steps”Now that you understand the component architecture, learn how to create your own components:
- Creating Components - Step-by-step guide to building components
- Component API Reference - Documentation for all 25 components
For practical examples, explore the component implementations in the main repository:
- Main repository: https://github.com/poncardasm/finan-website/tree/main/src/components