Skip to content

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.

The FiNAN website uses three distinct types of components, each serving a specific purpose in the architecture.

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.)
src/layouts/BaseLayout.astro
---
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>

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
src/components/Committee.astro
---
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>

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
src/components/StructuredData.astro
---
interface Props {
type: 'Organization' | 'FAQPage';
data: Record<string, any>;
}
const { type, data } = Astro.props;
---
<script type="application/ld+json" set:html={JSON.stringify(data)} />

The FiNAN website follows consistent patterns for component props to maintain type safety and developer experience.

All components define props using TypeScript interfaces, ensuring type safety throughout the application.

// ✅ Good - Explicit interface with types
interface Props {
title: string;
members: CommitteeMember[];
isVisible?: boolean; // Optional props use ?
}
const { title, members, isVisible = true } = Astro.props; // Default values
// ❌ Bad - No types, prone to errors
const { title, members } = Astro.props;

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.

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
}

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>

Components accept configuration data as props, allowing reuse across pages:

Pattern:

  1. Define data in src/data/ directory
  2. Import data in page files
  3. Pass data to component as props
  4. Component renders based on data

Example:

src/pages/representation/finland.astro
---
import Committee from '@/components/Committee.astro';
import { finlandCommittee } from '@/data/representation/committee/finlandCommittee';
---
<Committee members={finlandCommittee} country="Finland" />

Astro’s <slot /> enables flexible content composition:

src/components/Wrapper.astro
---
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>

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>
)}

Styles are scoped to components using Astro’s built-in scoping:

src/components/ExampleComponent.astro
---
// 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

Standard pattern - pass data down via props:

<ChildComponent title="Example" data={arrayData} />

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).

For data shared across multiple components, use centralized data files:

src/data/shared/appState.ts
export const siteConfig = {
organizationName: 'FiNAN',
supportEmail: 'info@finan.org',
};

Import in multiple components:

---
import { siteConfig } from '@/data/shared/appState';
---
<p>Contact: {siteConfig.supportEmail}</p>

Each component should have a single, clear responsibility:

  • Committee.astro - Displays committee members
  • FAQAccordion.astro - Renders FAQ accordion
  • PageContent.astro - Too generic, unclear purpose

Always define TypeScript interfaces for props:

// ✅ Good
interface Props {
title: string;
count: number;
}
// ❌ Bad - No type safety
const { title, count } = Astro.props;

If multiple components share logic, extract to utility functions:

src/lib/utils.ts
export function formatDate(date: Date): string {
return new Intl.DateTimeFormat('en-US').format(date);
}

Instead of hardcoding content in components, use data files:

// ✅ Good - Data in config file
import { partners } from '@/data/partnersData';
<Partners partners={partners} />
// ❌ Bad - Hardcoded in component
<Partners partners={[{ name: 'Partner 1' }, { name: 'Partner 2' }]} />

Add comments for non-obvious logic:

---
// Filter active members and sort by join date
const activeMembers = members
.filter((m) => m.isActive)
.sort((a, b) => a.joinDate.getTime() - b.joinDate.getTime());
---

Now that you understand the component architecture, learn how to create your own components:

For practical examples, explore the component implementations in the main repository: