Creating Custom Layouts
This guide walks through creating a new custom layout from scratch. We'll build a pricing page layout as an example.
Step 1: Create the Layout Folder
mkdir -p src/layouts/custom/styles/pricing/
Step 2: Create the Layout Component
File: src/layouts/custom/styles/pricing/Layout.astro
---
/**
* Pricing Layout - Pricing page with plans
*/
import { loadFile } from '@loaders/data';
// Import styles
import './styles.css';
// Define data interface
interface Plan {
name: string;
price: number | string;
period?: string;
description?: string;
features: string[];
cta: {
label: string;
href: string;
};
popular?: boolean;
}
interface PricingData {
title?: string;
subtitle?: string;
plans: Plan[];
}
interface Props {
dataPath: string;
}
const { dataPath } = Astro.props;
// Load page data
let pageData: PricingData = { plans: [] };
try {
const content = await loadFile(dataPath);
pageData = content.data as PricingData;
} catch (error) {
console.error('Error loading pricing data:', error);
}
const { title = 'Pricing', subtitle, plans } = pageData;
---
<div class="pricing-page">
<header class="pricing-header">
<h1>{title}</h1>
{subtitle && <p>{subtitle}</p>}
</header>
<div class="pricing-grid">
{plans.map(plan => (
<div class={`pricing-card ${plan.popular ? 'pricing-card--popular' : ''}`}>
{plan.popular && <span class="popular-badge">Most Popular</span>}
<h2 class="pricing-card__name">{plan.name}</h2>
<div class="pricing-card__price">
{typeof plan.price === 'number' ? (
<>
<span class="currency">$</span>
<span class="amount">{plan.price}</span>
<span class="period">/{plan.period || 'mo'}</span>
</>
) : (
<span class="custom">{plan.price}</span>
)}
</div>
{plan.description && (
<p class="pricing-card__description">{plan.description}</p>
)}
<ul class="pricing-card__features">
{plan.features.map(feature => (
<li>
<span class="check">✓</span>
{feature}
</li>
))}
</ul>
<a href={plan.cta.href} class="pricing-card__cta">
{plan.cta.label}
</a>
</div>
))}
</div>
</div>
Step 3: Add Styles
File: src/layouts/custom/styles/pricing/styles.css
.pricing-page {
max-width: 1200px;
margin: 0 auto;
padding: 4rem 2rem;
}
.pricing-header {
text-align: center;
margin-bottom: 3rem;
}
.pricing-header h1 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
}
.pricing-header p {
font-size: 1.25rem;
color: var(--color-text-secondary);
}
.pricing-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 2rem;
align-items: start;
}
.pricing-card {
position: relative;
background: var(--color-bg-secondary);
border-radius: 12px;
padding: 2rem;
border: 1px solid var(--color-border);
}
.pricing-card--popular {
border-color: var(--color-primary);
transform: scale(1.05);
}
.popular-badge {
position: absolute;
top: -12px;
left: 50%;
transform: translateX(-50%);
background: var(--color-primary);
color: white;
padding: 0.25rem 1rem;
border-radius: 20px;
font-size: 0.875rem;
font-weight: 500;
}
.pricing-card__name {
font-size: 1.5rem;
margin-bottom: 1rem;
}
.pricing-card__price {
margin-bottom: 1rem;
}
.pricing-card__price .currency {
font-size: 1.5rem;
vertical-align: top;
}
.pricing-card__price .amount {
font-size: 3rem;
font-weight: 700;
}
.pricing-card__price .period {
color: var(--color-text-secondary);
}
.pricing-card__description {
color: var(--color-text-secondary);
margin-bottom: 1.5rem;
}
.pricing-card__features {
list-style: none;
padding: 0;
margin-bottom: 2rem;
}
.pricing-card__features li {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.pricing-card__features .check {
color: var(--color-success);
font-weight: bold;
}
.pricing-card__cta {
display: block;
text-align: center;
padding: 0.75rem 1.5rem;
background: var(--color-primary);
color: white;
border-radius: 8px;
text-decoration: none;
font-weight: 500;
}
.pricing-card__cta:hover {
opacity: 0.9;
}
Step 4: Create the Data File
File: dynamic_data/data/pages/pricing.yaml
title: "Simple, Transparent Pricing"
subtitle: "Choose the plan that's right for you"
plans:
- name: "Starter"
price: 0
period: "forever"
description: "Perfect for trying out"
features:
- "Up to 3 projects"
- "Basic analytics"
- "Community support"
- "1 GB storage"
cta:
label: "Get Started Free"
href: "/signup?plan=starter"
- name: "Pro"
price: 29
period: "mo"
description: "For growing teams"
popular: true
features:
- "Unlimited projects"
- "Advanced analytics"
- "Priority support"
- "10 GB storage"
- "Custom domains"
- "Team collaboration"
cta:
label: "Start Free Trial"
href: "/signup?plan=pro"
- name: "Enterprise"
price: "Custom"
description: "For large organizations"
features:
- "Everything in Pro"
- "Unlimited storage"
- "SSO & SAML"
- "Dedicated support"
- "SLA guarantee"
- "Custom integrations"
cta:
label: "Contact Sales"
href: "/contact?plan=enterprise"
Step 5: Configure in site.yaml
File: dynamic_data/config/site.yaml
pages:
# ... other pages ...
pricing:
base_url: "/pricing"
type: custom
layout: "@custom/pricing"
data: "@data/pages/pricing.yaml"
Step 6: Test
npm run build
Visit /pricing to see your new layout.
Extracting Components
As your layout grows, extract reusable parts into components:
Create Component
mkdir -p src/layouts/custom/components/pricing-card/default/
File: pricing-card/default/PricingCard.astro
---
interface Props {
name: string;
price: number | string;
period?: string;
description?: string;
features: string[];
cta: { label: string; href: string };
popular?: boolean;
}
const { name, price, period, description, features, cta, popular } = Astro.props;
---
<div class={`pricing-card ${popular ? 'pricing-card--popular' : ''}`}>
{popular && <span class="popular-badge">Most Popular</span>}
<h2>{name}</h2>
<!-- ... rest of card ... -->
</div>
Use in Layout
---
import PricingCard from '../../components/pricing-card/default/PricingCard.astro';
---
<div class="pricing-grid">
{plans.map(plan => (
<PricingCard {...plan} />
))}
</div>
Adding Sections
Compose multiple sections in your layout:
---
import Hero from '../../components/hero/default/Hero.astro';
import PricingGrid from './PricingGrid.astro';
import FAQ from '../../components/faq/default/FAQ.astro';
// ... load data ...
---
<div class="pricing-page">
{hero && <Hero hero={hero} />}
<PricingGrid plans={plans} />
{faq && <FAQ items={faq} />}
</div>
Checklist
Before shipping your custom layout:
- Layout receives
dataPathand loads data - TypeScript interfaces defined
- Error handling for missing data
- Empty states for missing sections
- Responsive design
- Dark mode support
- Accessible markup
- Styles scoped or prefixed
- Data file created
- site.yaml configured
- Build passes