P
PaperHouseastro theme ~
Developer

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-2
  • wide → md:col-span-2
  • normal → 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

  1. Push to GitHub
  2. Import repo on Vercel
  3. Framework preset: Astro
  4. Deploy

Netlify

  1. Push to GitHub
  2. Import repo on Netlify
  3. Build command: npm run build
  4. Publish directory: dist

Cloudflare Pages

  1. Push to GitHub
  2. Import repo on Cloudflare Pages
  3. Build command: npm run build
  4. 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 .webp or .avif, compress with Squoosh
  • Fonts: preload only the weights you use
  • Meta: unique title and description per page
  • Sitemap: submit to Google Search Console
  • Analytics: add your provider in src/layouts/Base.astro

Getting help

If you get stuck:

  1. Check the browser console for errors
  2. Check the terminal where npm run dev runs
  3. Run npm run build — it catches schema errors
  4. 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.

Get PaperHouse

Buy PaperHouse — $45 on Gumroad