Developer Notes
Custom layouts, adding new collections, extending components, and deploying PaperHouse as your own theme.
Developer Notes
This page is for developers who want to extend PaperHouse — new collections, custom layouts, new components, or a full rebrand.
Adding a new collection
A collection is a folder of markdown files with a shared schema. To add one:
1. Create the folder
src/content/projects/
project-one.md
project-two.md
2. Define the collection
Edit src/content.config.ts:
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const projects = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/content/projects' }),
schema: z.object({
title: z.string(),
description: z.string(),
year: z.string(),
cover: z.string(),
tags: z.array(z.string()).optional(),
}),
});
export const collections = { works, blog, pages, contact, docs, projects };
3. Create a route
src/pages/projects/index.astro — listing:
---
import Base from '../../layouts/Base.astro';
import { getCollection } from 'astro:content';
const projects = await getCollection('projects');
---
<Base page="projects">
<div class="grid md:grid-cols-2 gap-5">
{projects.map(p => (
<a href={`/projects/${p.id}`} class="bento p-6 block">
<h2 class="font-serif text-[24px]">{p.data.title}</h2>
<p class="mt-2 opacity-60">{p.data.description}</p>
</a>
))}
</div>
</Base>
src/pages/projects/[...slug].astro — detail:
---
import Base from '../../layouts/Base.astro';
import { getCollection, render } from 'astro:content';
export async function getStaticPaths() {
const projects = await getCollection('projects');
return projects.map(p => ({
params: { slug: p.id },
props: { project: p }
}));
}
const { project } = Astro.props;
const { Content } = await render(project);
---
<Base page="projects">
<article class="max-w-[720px] mx-auto">
<h1 class="font-serif text-[clamp(2.5rem,6vw,5rem)]">{project.data.title}</h1>
<div class="prose mt-10"><Content /></div>
</article>
</Base>
Custom layouts
Some pages need layouts that the generic [slug].astro cannot provide. To create a custom layout:
1. Create the content
src/content/pages/pricing.md:
---
title: "Pricing"
subtitle: "Simple one-time pricing."
---
## What you get
- Full source code
2. Create the layout
src/pages/pricing.astro:
---
import Base from '../layouts/Base.astro';
import { getEntry, render } from 'astro:content';
const entry = await getEntry('pages', 'pricing');
const { Content } = await render(entry);
---
<Base page="pricing">
<article>
<h1>{entry.data.title}</h1>
<div class="prose"><Content /></div>
</article>
</Base>
3. Exclude from [slug].astro
src/pages/[slug].astro:
export async function getStaticPaths() {
const pages = await getCollection('pages');
const excluded = ['pricing'];
return pages
.filter(p => !excluded.includes(p.id))
.map(p => ({ params: { slug: p.id }, props: { entry: p } }));
}
Without this exclusion, both routes would try to serve /pricing and Astro would warn about the conflict.
Adding a new page meta entry
Every page referenced by <Base page="key"> needs an entry in src/data/site.json → pages:
"pages": {
"pricing": {
"title": "Pricing",
"description": "Simple one-time pricing for PaperHouse.",
"image": "/astro.svg",
"type": "website"
}
}
If you forget, Base.astro falls back to pages.home — no error, but your page inherits the home page’s meta.
Overriding meta on a page
For dynamic pages (works, blog, tags, categories), pass overrides to <Base>:
<Base
page="workSlug"
overrides={{
titleValue: work.data.title,
descriptionValue: work.data.description,
image: work.data.cover,
type: 'article'
}}
>
titleValue and descriptionValue are injected into the page’s titleTemplate and descriptionTemplate from site.json.
Extending components
BentoCard.astro
Accepts these props:
| Prop | Type | Required |
|---|---|---|
title |
string | yes |
cat |
string | yes |
img |
string | yes |
href |
string | yes |
size |
normal / wide / large |
no |
Sizes map to grid classes:
large→md:col-span-2 md:row-span-2wide→md:col-span-2normal→md:col-span-1
Adding a new component
Create a .astro file anywhere under src/components/. Import it where you need it:
---
import MyComponent from '../components/MyComponent.astro';
---
<MyComponent title="Hello" />
Components receive props via Astro.props in their frontmatter.
Theming the design system
All design tokens live in src/styles/globals.css inside the @theme block:
@theme {
--color-paper: #FFFCF7;
--color-ink: #14120F;
--color-stone: #EDE8DF;
--color-sage: #D4DDD4;
--color-lime: #E8FF5A;
--font-serif: "Instrument Serif", serif;
--font-sans: "Instrument Sans", "Syne", sans-serif;
--font-hand: "Caveat", cursive;
--radius-bento: 28px;
--radius-bento-sm: 20px;
}
Change these to rebrand the entire site. Tailwind picks them up automatically.
Example — switch to a dark theme:
@theme {
--color-paper: #14120F;
--color-ink: #FFFCF7;
--color-stone: #2A2620;
--color-lime: #E8FF5A;
}
Save and the whole site inverts.
Adding global CSS
For custom utilities, add to src/styles/globals.css:
.my-shadow {
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.06);
}
Then use it in any .astro file:
<div class="my-shadow">...</div>
Deploying
Vercel
- Push to GitHub
- Import repo on Vercel
- Framework preset: Astro
- Deploy
Netlify
- Push to GitHub
- Import repo on Netlify
- Build command:
npm run build - Publish directory:
dist
Cloudflare Pages
- Push to GitHub
- Import repo on Cloudflare Pages
- Build command:
npm run build - Output directory:
dist
Any static host
Run npm run build and upload the dist/ folder to any web host.
Setting the production domain
Before deploying, set site in astro.config.mjs:
export default defineConfig({
site: 'https://your-domain.com',
vite: { plugins: [tailwindcss()] }
});
Astro uses this for canonical URLs and sitemaps.
Adding a sitemap
npx astro add sitemap
This auto-updates astro.config.mjs and generates a sitemap on build.
Performance checklist
Before shipping:
- Images: use
.webpor.avif, compress with Squoosh - Fonts: preload only the weights you use
- Meta: unique
titleanddescriptionper page - Sitemap: submit to Google Search Console
- Analytics: add your provider in
src/layouts/Base.astro
Getting help
If you get stuck:
- Check the browser console for errors
- Check the terminal where
npm run devruns - Run
npm run build— it catches schema errors - Open an issue or email hello@paperhouse.studio
What you can build
PaperHouse is designed to be a foundation. On top of it, you can add:
- Headless CMS (Decap, Tina, Sanity)
- Booking systems (Cal.com embed, custom)
- E-commerce (Snipcart, Shopify Buy Button)
- Multi-language routing
- Comments (Giscus, Utterances)
- Search (Pagefind)
Every one of these is a component or a route — none requires rewriting PaperHouse.
Credits
- Astro — astro.build
- Tailwind CSS — tailwindcss.com
- Content Collections — docs.astro.build/en/guides/content-collections
License reminder
One-time $45. Unlimited personal and client projects. Not for resale as a template.