SP (Souvenir Programme) microsites are specialized Astro-based applications designed for Singapore’s SWA annual events. Each edition (SP/48, SP/49, SP/50) is a self-contained microsite with consistent architecture, reusable components, and dynamic content management through YAML collections.
/CLAUDE.md for general project guidelinesTime to set up a new SP edition: ~30 minutes (for experienced developers)
# 1. Copy SP/49 folder to new edition (SP/50)
cp -r src/pages/sp/49 src/pages/sp/50
cp -r src/content/sp49 src/content/sp50
cp -r src/components/sp/49 src/components/sp/50
cp src/styles/sp49.css src/styles/sp50.css
# 2. Create new image folder
mkdir -p src/images/sp/50
# 3. Update content.config.ts with new collections
# (See "Content Collections" section)
# 4. Rename CSS framework class references in new files
# sp/50/ components: update sp49.css → sp50.css
# sp/50/ components: update sp49 imports → sp50
# 5. Update component props from sp49 → sp50 theme
Then proceed with detailed setup below.
src/pages/sp/
├── 48/ # SP/48 Souvenir Programme
│ ├── index.astro # Main landing page
│ ├── charity-presentation.astro
│ ├── gtw-totals.astro
│ ├── stage-display.astro
│ └── README.md
├── 49/ # SP/49 Souvenir Programme
│ ├── index.astro
│ ├── charity-presentation.astro
│ ├── gtw-totals.astro
│ └── README.md
└── 50/ # Future editions follow same pattern
├── index.astro
└── [other pages as needed]
src/components/sp/
├── 48/ # 42 SP/48-specific components
├── 49/ # 50 SP/49-specific components
├── 50/ # Copy from 49 and customize
└── shared/ # 8 Edition-agnostic components
├── SPLayout.astro # Base layout with theme support
├── SPHeader.astro # Navigation with scroll tracking
├── SPFooter.astro
├── SPSection.astro # Reusable section wrapper
├── SPSponsorCard.astro
├── SPBottomNav.astro
├── SPGa4Tracker.astro
└── SPStageLayout.astro
src/styles/
├── sp48.css # SP/48 CSS Framework
├── sp49.css # SP/49 CSS Framework (current best practice)
└── sp50.css # New editions copy sp49.css and customize
src/content/
├── sp48/
│ └── schedule.md # Schedule data
├── sp49/ # 8 content files
│ ├── schedule.md
│ ├── messages.md
│ ├── committee.md
│ ├── contestants.md
│ ├── judges.md
│ ├── acknowledgements.md
│ └── gtw-prizes.md
└── sp49Sponsors/
└── sponsors.yaml # Sponsor data by tier
src/images/sp/
├── peony-bg.webp # Shared background
├── swa-logo.png # Shared logo
├── 49/ # Edition-specific images
└── 50/ # Future editions
Every SP section follows a 3-layer pattern for consistent visual design:
<section id="section-id" class="sp-section-base">
<!-- Layer 1: Background (z-0) -->
<div class="sp-background-layer">
<Image src={BackgroundImage} alt="..." class="w-full h-full object-cover" />
</div>
<!-- Layer 2: Overlay (z-10) - Controls transparency/readability -->
<div class="sp-overlay-layer"></div>
<!-- Layer 3: Content (z-20) - Text, buttons, interactive elements -->
<article class="sp-content-layer">
<h2 class="sp-title">Section Title</h2>
<p class="sp-body-text">Your content here</p>
</article>
</section>
Z-index Strategy:
z-0: Background imagesz-10: Overlay for readability controlz-20: Interactive contentThe SP CSS Framework provides reusable component classes for consistent styling across all editions. Each edition has its own CSS file (sp48.css, sp49.css, etc.) that can be customized.
.sp-section-base /* Full-screen section container */
.sp-background-layer /* Background image container */
.sp-overlay-layer /* Dark overlay (transparency configurable) */
.sp-content-layer /* Content with proper z-index and padding */
.sp-title /* Main headings (responsive, progressive scaling) */
.sp-subtitle /* Secondary headings */
.sp-body-text /* Body text with optimal line height */
.sp-accent-text /* Brand accent color text */
.sp-container /* Content spacing and margins */
.sp-center-content /* Flex centered layout */
.sp-text-center /* Text alignment center */
.sp-full-width /* Full-width responsive container */
.sp-grid-responsive /* Responsive grid (1 col mobile, 2-3 cols desktop) */
.sp-timeline-item /* Schedule/timeline items */
.sp-sponsor-card /* Sponsor card layout */
.sp-btn-primary /* Primary action button */
.sp-image-container /* Image wrapper with styling */
.sp-flex-buttons /* Button layout container */
.sp-divider /* Content divider line */
Use this as the baseline for new editions:
Container Widths:
Typography Scaling:
Overlay Opacity:
rgba(0, 0, 0, 0.2) - Allows background color to show throughrgba(0, 0, 0, 0.5) or higherContent Padding (Progressive):
When creating SP/50, you’ll customize sp50.css:
Color Changes (primary customization):
.sp-background-layer {
@apply absolute inset-0 z-0 bg-swa-1; /* Change to bg-swa-2 or custom color */
}
.sp-overlay-layer {
background-color: rgba(0, 0, 0, 0.2); /* Adjust transparency (0.2 = light) */
}
.sp-subtitle {
@apply font-semibold text-swa-4; /* Change accent text color */
}
Layout Changes (advanced):
.sp-full-width {
max-width: 90rem; /* Make wider if needed */
}
.sp-title {
font-size: 2.5rem; /* Larger/smaller titles */
line-height: 1.1;
}
Use CSS Framework Classes (90% of cases):
<section class="sp-section-base">
<div class="sp-overlay-layer"></div>
<article class="sp-content-layer">
<h2 class="sp-title">Title</h2>
</article>
</section>
Inline Classes Only For:
Never Use Inline Styles (except for dynamic values):
<!-- ✗ Avoid -->
<div style="min-height: 100vh; background-color: purple;"></div>
<!-- ✓ Use -->
<div class="sp-section-base"></div>
---
import { Image } from "astro:assets";
import Layout from "../../../components/sp/shared/SPLayout.astro";
import PeonyBg from "../../../images/sp/peony-bg.webp";
interface Props {
theme?: "48" | "49" | "50";
variant?: "default" | "alt";
}
const { theme = "50", variant = "default" } = Astro.props;
---
<Layout title="Section Title" theme={theme}>
<section id="unique-id" class="sp-section-base">
<!-- Background Layer -->
<div class="sp-background-layer">
<Image
src={PeonyBg}
alt="Background"
class="w-full h-full object-cover"
format="webp"
quality={20}
/>
</div>
<!-- Overlay Layer -->
<div class="sp-overlay-layer"></div>
<!-- Content Layer -->
<article class="sp-content-layer">
<h2 class="sp-title">Your Section Title</h2>
<p class="sp-body-text">Content goes here</p>
</article>
</section>
</Layout>
Purpose: Wraps all pages with theme, header, footer, and GA4 tracking
Props:
interface Props {
title: string; // Page title
description?: string; // Meta description
url?: string; // Page URL for SEO
imageUrl?: string; // OG image URL
siteName?: string; // Site name for schema
theme: "48" | "49" | "50"; // Edition theme
edition?: string; // Edition name (e.g., "SP/50")
}
Usage:
---
import Layout from "../components/sp/shared/SPLayout.astro";
---
<Layout title="Event Name" theme="50" edition="SP/50">
<!-- Your pages go here -->
</Layout>
Purpose: Pre-styled section wrapper with consistent structure
Props:
interface Props {
id: string; // Section anchor ID
title: string; // Section heading
subtitle?: string; // Optional subheading
variant?: "default" | "alt" | "accent"; // Style variant
theme?: "48" | "49" | "50";
}
Usage:
<SPSection id="schedule" title="Event Schedule" theme="50">
<!-- Content inside uses sp-* classes -->
</SPSection>
Features:
Scroll Events Tracked:
sp50_menu_click - Menu navigationsp50_back_to_top - Back-to-top buttonPurpose: Event delegation tracking for GA4
Tracked Events:
"sp50_cta_click" // CTA button clicks
"sp50_sponsor_click" // Sponsor logo clicks
"sp50_donation_click" // Donation buttons
Implementation:
<button
data-sp-track="true"
data-sp-cta-type="donation"
class="sp-btn-primary"
>
Donate Now
</button>
When creating new components for your edition:
src/components/sp/50/YourComponent.astrosp50.cssExample Custom Component:
---
import { Image } from "astro:assets";
import SponsorImage from "../../../images/sp/50/sponsor-logo.webp";
interface Props {
sponsorName: string;
sponsorImage: ImageMetadata;
theme?: "50";
}
const { sponsorName, sponsorImage, theme = "50" } = Astro.props;
---
<article class="sp-sponsor-card">
<Image
src={sponsorImage}
alt={sponsorName}
quality={90}
/>
<h3 class="sp-subtitle">{sponsorName}</h3>
</article>
<style>
article {
@apply p-6 bg-white/10 rounded-lg;
}
</style>
SP editions use YAML content collections for dynamic content management. This allows non-technical team members to update content without modifying code.
sp50 in content.config.ts)Purpose: Event programme schedule with times and descriptions
File Location: src/content/sp50/schedule.md
Schema:
scheduleItems: Array<{
time: string; // Display time (e.g., "6.00pm")
timeOrder: number; // Sort order in 24-hour format (1800 = 6:00pm)
title: string; // Event title
subtitle?: string; // Optional subtitle
description?: string; // Optional description
}>
Example:
---
scheduleItems:
- time: "6.00pm"
timeOrder: 1800
title: "Arrival of Guests"
- time: "6.50pm"
timeOrder: 1850
title: "Arrival of Guest-of-Honour"
subtitle: "Mr Seah Kian Peng"
description: "Lead Advisor, Marine Parade-Braddell Heights"
---
sp50Sponsors)Purpose: Sponsor data organized by sponsorship tier
File Location: src/content/sp50Sponsors/sponsors.yaml
Schema:
platinum?: Array<{
name: string;
website?: string; // Optional link
role?: string; // Role/contribution
image?: string; // Logo filename
}>;
gold?: Array<{...}>;
silver?: Array<{...}>;
bronze?: Array<{...}>;
official?: Array<{...}>;
doorGifts?: Array<{...}>;
Example:
platinum:
- name: "Major Sponsor Ltd"
website: "https://example.com"
role: "Title Sponsor"
image: "sponsor-logo.webp"
gold:
- name: "Gold Partner Inc"
image: "gold-partner.webp"
sp50Messages)Purpose: Welcome/greeting messages from VIPs
File Location: src/content/sp50Messages/messages.yaml
Schema:
messages: Array<{
name: string; // Person's name
title: string; // Their title
content: string; // Message text (can include markdown)
}>
sp50Committee)Purpose: Organizing and working committee members
Schema:
organisingCommittee?: Array<{
name: string;
role: string;
title?: string;
}>;
workingCommittee?: Array<{
name: string;
role: string;
title?: string;
}>;
sp50Contestants)Purpose: MSPI contestant profiles
Schema:
contestants: Array<{
number: number; // Contestant number
name: string;
image: string; // Image filename
imageAlt: string; // Alt text
bio?: string; // Optional biography
}>
sp50Judges)Purpose: Chief judge and judging panel members
Schema:
judges: {
chief?: Array<{name: string; title?: string}>;
table1?: Array<{name: string; title?: string}>;
table2?: Array<{name: string; title?: string}>;
}
sp50Acknowledgements)Purpose: Special thanks and recognition
Schema:
guestOfHonour?: {
name: string;
title: string;
};
specialAcknowledgements?: Array<{
name: string;
contribution?: string;
}>;
friends?: string[]; // Simple list of names
sp50Prizes)Purpose: GTW contest prizes and rewards
Schema:
prizes: Array<{
rank: number; // 1st, 2nd, 3rd, etc.
name: string; // Prize name
sponsor?: string; // Sponsor name
value?: string; // Prize value/description
description?: string; // Detailed description
image?: string; // Prize image
imageAlt?: string;
winners?: number; // Number of winners (default: 1)
}>
Example: Loading and displaying schedule:
---
import { getEntry } from "astro:content";
const scheduleData = await getEntry("sp50", "schedule");
const items = scheduleData.data.scheduleItems.sort(
(a, b) => a.timeOrder - b.timeOrder
);
---
{items.map((item) => (
<div class="sp-timeline-item">
<time class="sp-subtitle">{item.time}</time>
<div>
<h3 class="sp-body-text font-semibold">{item.title}</h3>
{item.description && <p class="sp-body-text text-sm">{item.description}</p>}
</div>
</div>
))}
src/content/sp50/new-collection.yamlgetEntry("sp50", "new-collection")# Pages
cp -r src/pages/sp/49 src/pages/sp/50
# Components
cp -r src/components/sp/49 src/components/sp/50
# Styles
cp src/styles/sp49.css src/styles/sp50.css
# Content collections
cp -r src/content/sp49 src/content/sp50
cp -r src/content/sp49Sponsors src/content/sp50Sponsors
cp -r src/content/sp49Messages src/content/sp50Messages
cp -r src/content/sp49Committee src/content/sp50Committee
cp -r src/content/sp49Contestants src/content/sp50Contestants
cp -r src/content/sp49Judges src/content/sp50Judges
cp -r src/content/sp49Acknowledgements src/content/sp50Acknowledgements
cp -r src/content/sp49Prizes src/content/sp50Prizes
# Images
mkdir -p src/images/sp/50
# Copy edition-specific images to src/images/sp/50/
In /src/content.config.ts:
Find the SP49 sections and duplicate for SP50:
// Add new SP50 collection definitions
const sp50 = defineCollection({
loader: glob({ pattern: '**/[^_]*.{md,mdx}', base: "./src/content/sp50" }),
schema: z.object({
scheduleItems: z.array(z.object({
time: z.string(),
timeOrder: z.number(),
title: z.string(),
subtitle: z.string().optional(),
description: z.string().optional(),
})),
}),
});
const sp50Sponsors = defineCollection({
type: "data",
schema: z.object({
platinum: z.array(z.object({
name: z.string(),
website: z.string().optional().nullable(),
role: z.string().optional(),
image: z.string().optional(),
})).optional(),
// ... repeat for gold, silver, bronze, official, doorGifts
}),
});
// Repeat for: sp50Messages, sp50Committee, sp50Contestants,
// sp50Judges, sp50Acknowledgements, sp50Prizes
// Update exports
export const collections = {
// ... existing
sp50: sp50,
sp50Sponsors: sp50Sponsors,
sp50Messages: sp50Messages,
// ... etc for all collections
};
In src/pages/sp/50/index.astro and other pages:
Search and Replace:
sp/49 → sp/50 (in import paths)sp49 → sp50 (in component/collection references)"49" → "50" (in theme props)sp49.css → sp50.cssExample Update:
---
// OLD
import Layout from "../../../components/sp/shared/SPLayout.astro";
import SPHome from "../../../components/sp/49/SPHome.astro";
// NEW
import Layout from "../../../components/sp/shared/SPLayout.astro";
import SPHome from "../../../components/sp/50/SPHome.astro";
---
<Layout title="SP/50" theme="50" edition="SP/50">
<SPHome theme="50" />
</Layout>
In src/components/sp/50/ folder, update all component files:
Replace in all files:
// OLD
import { getEntry } from "astro:content";
const data = await getEntry("sp49", "schedule");
// NEW
import { getEntry } from "astro:content";
const data = await getEntry("sp50", "schedule");
In components, update CSS import:
---
// OLD
import "../../styles/sp49.css";
// NEW
import "../../styles/sp50.css";
---
Edit src/styles/sp50.css:
Update colors if desired:
.sp-background-layer {
@apply absolute inset-0 z-0 bg-swa-2; /* Changed from bg-swa-1 */
}
.sp-subtitle {
@apply font-semibold text-swa-3; /* Changed color */
}
Adjust typography if needed:
.sp-title {
font-size: 2.5rem; /* Make titles smaller/larger */
}
.sp-full-width {
max-width: 85rem; /* Adjust container width */
}
Edit src/content/sp50/schedule.md:
---
scheduleItems:
- time: "6.00pm"
timeOrder: 1800
title: "[Your event title]"
---
Edit src/content/sp50Sponsors/sponsors.yaml with your sponsor information.
Update all YAML files in src/content/sp50*/ directories with edition-specific content.
Place edition-specific images in src/images/sp/50/:
Convert all images to WebP:
npm run convert:webp src/images/sp/50/
Quality Settings:
In src/pages/sp/50/index.astro:
<Layout
title="SWA SP/50 Souvenir Programme"
description="Join us for the 50th Singapore Women's Achievement Dinner 2026"
imageUrl="/og-image-sp50.png"
siteName="SWA"
edition="SP/50"
theme="50"
>
Check SPLayout.astro for schema.org structured data generation. Update date, name, and description for your edition.
npm run dev
# Navigate to http://localhost:4321/sp/50/
npm run build
# Check for build errors and warnings
All images in SP microsites should be converted to WebP format for optimal performance.
Quality Settings Guide:
Background images: quality={20} # Highly compressed
Contestant photos: quality={80} # Balanced
Sponsor logos: quality={90} # High quality needed
Logo/icon images: quality={95} # Logo clarity
Convert Images:
# For a specific folder
npm run convert:webp src/images/sp/50/
# For entire site
npm run optimize:site
---
import { Image } from "astro:assets";
import SampleImage from "../../../images/sp/50/sample.png";
---
<Image
src={SampleImage}
alt="Descriptive alt text for accessibility"
format="webp"
quality={80}
class="w-full h-auto rounded-lg"
/>
Use Tailwind classes for responsive sizes:
<!-- Small hero images -->
<Image src={...} class="w-32 h-32 md:w-48 md:h-48 lg:w-64 lg:h-64" />
<!-- Full-width background images -->
<Image src={...} class="w-full h-full object-cover" />
<!-- Sponsor logos (max width to prevent stretching) -->
<Image src={...} class="w-full max-w-xs h-auto mx-auto" />
All content should follow British English conventions for consistency across SWA publications.
Spelling:
Date Format:
Time Format:
Numbers:
Terminology:
✓ Correct:
The SWA celebrates 50 years of honouring outstanding women across
Singapore. This year's souvenir programme features a full schedule
of events, recognising the contributions of our sponsors and organisers.
✗ Incorrect:
The SWA celebrates 50 years of honoring outstanding women across
Singapore. This year's program features a full schedule of events,
recognizing the contributions of our sponsors and organizers.
The SPLayout.astro component automatically generates:
<title> tag<meta name="description"><meta name="og:title"><meta name="og:description"><meta name="og:image"><meta name="og:type"> = “event”Provide these props to SPLayout:
<Layout
title="SWA SP/50: 50th Souvenir Programme"
description="Join us for the 50th Singapore Women's Achievement Dinner"
imageUrl="/images/og-sp50.png"
url="https://swa.sg/sp/50"
siteName="SWA"
/>
SPLayout generates Event schema markup:
{
"@context": "https://schema.org",
"@type": "Event",
"name": "SWA SP/50",
"description": "...",
"url": "https://swa.sg/sp/50",
"image": "...",
"startDate": "2026-07-19",
"endDate": "2026-07-19",
"organizer": {
"@type": "Organization",
"name": "SWA",
"url": "https://swa.sg"
},
"location": {
"@type": "VirtualLocation",
"url": "https://swa.sg/sp/50"
}
}
All SP pages include:
Events are tracked via SPGa4Tracker.astro:
<!-- Track CTA clicks -->
<button
data-sp-track="true"
data-sp-cta-type="donation"
class="sp-btn-primary"
>
Donate
</button>
<!-- Track sponsor clicks -->
<a
href="https://sponsor.com"
data-sp-track="true"
data-sp-sponsor="Sponsor Name"
data-sp-tier="platinum"
>
Sponsor Logo
</a>
Events Generated:
sp50_cta_click - CTA button interactionssp50_sponsor_click - Sponsor link clickssp50_donation_click - Donation button clickssp50_menu_click - Navigation menu (from SPHeader)sp50_back_to_top - Back-to-top button (from SPHeader)For edition-specific tracking, add data attributes:
<button
data-sp-track="true"
data-sp-cta-type="[custom-type]"
data-sp-custom-prop="value"
>
Click Me
</button>
To Process Custom Events: Update SPGa4Tracker.astro to handle new data attributes.
1. Theme Consistency
theme prop to shared components2. Component Reusability
sp/50/ folder, don’t modify sp/shared/3. Content Management
getEntry() to load content at build time1. Image Optimization
2. Build Optimization
# Before production deployment
npm run build
# Check for warnings
# File sizes should be < 2MB per page
3. CSS Framework Usage
sp50.css for reuse1. Color Contrast
2. Alt Text All images require descriptive alt text:
<Image
src={...}
alt="Contestant Jane Doe, number 5, wearing a red dress"
/>
3. Keyboard Navigation
<button>, <a>, not <div>)focus: classes)Updating Future Editions (SP/51, SP/52):
sp/50/ → sp/51/sp50.css → sp51.csscontent.config.ts with new collectionsKeeping CSS Framework Updated:
Issue: Images not displaying
<Image> component used with src= propIssue: Content collections not loading
content.config.ts match getEntry() calls, verify YAML syntax is valid, restart dev serverIssue: Styling inconsistencies between pages
sp-section-base three-layer pattern, verify CSS import exists (import "../../styles/sp50.css")Issue: GA4 events not firing
data-sp-track="true" attribute present, check browser console for errors, ensure GA4 tag is loaded (check in shared Layout)Issue: Mobile layout breaks at certain viewport sizes
sm:, md:, lg:, xl:), verify sp-content-layer padding adjustments in CSS/CLAUDE.md for general project guidelinesThis documentation maintains the SP microsite architecture. For questions or updates, refer to component comments and official Astro documentation.