This guide covers common development workflows, best practices, and patterns used in this project.
Daily Development Workflow
Starting Your Work
Pull latest changes
git checkout main
git pull origin main
Create a feature branch
Use descriptive branch names:# For features
git checkout -b feature/add-search-functionality
# For bug fixes
git checkout -b fix/navigation-mobile-issue
Start development server
This syncs Notion content and starts the dev server at http://localhost:4321 Make your changes
Edit files, test locally, and iterate on your implementation.
Test and verify
# Run tests
pnpm test
# Build production version
pnpm build
# Preview production build
pnpm preview
Commit your work
Write clear, imperative commit messages:git add .
git commit -m "Add search functionality to blog page"
git push origin feature/add-search-functionality
Content Management Workflows
Adding Blog Posts
The recommended approach is via Notion:
Create entry in Notion
Add a new page to the blog database (NOTION_BLOG_DB_ID)
Fill required properties
- title: Post title
- description: SEO description
- path: URL slug (e.g.,
my-post-slug)
- published: Publication date
- public: ✓ (must be checked)
- tags: Comma-separated tags (optional)
Write content
Use Notion’s block editor to write your post with full markdown support
Sync to local
Run the sync script or dev server:This generates an MDX file at src/content/blog/[notion-id].mdx
The sync process is incremental - it only updates files that have changed based on Notion’s lastEditedTime property.
Manual MDX Creation
For content that doesn’t come from Notion:
---
title: "My Post Title"
description: "SEO-friendly description"
path: "my-post-slug"
published: "2026-03-03"
public: true
tags: "typescript, astro"
---
# Content here
Your markdown/MDX content...
Save to src/content/blog/my-post-slug.mdx.
Component Development
Creating New Components
File naming conventions:
- Astro components: PascalCase (e.g.,
Navigation.astro)
- React/Solid components: PascalCase (e.g.,
Map.tsx, Icon.tsx)
- Utilities: kebab-case (e.g.,
notion-client.ts)
Framework Selection Guide
Choose the right framework for your component:
| Use Case | Framework | Example |
|---|
| Static content | Astro | Layout, article display |
| Image optimization | React | Astro’s <Image /> component |
| Interactive UI | Solid.js | Map, navigation, modals |
| SEO-critical | Astro | Headers, metadata |
Component Example
Astro component (src/components/ArticleCard.astro):
---
interface Props {
title: string;
description: string;
path: string;
published: string;
class?: string;
}
const { title, description, path, published, class: className } = Astro.props;
---
<article class:list={['prose prose-stone', className]}>
<h2 class="text-2xl font-bold">
<a href={path}>{title}</a>
</h2>
<time class="text-sm text-stone-600">{published}</time>
<p>{description}</p>
</article>
Solid.js interactive component:
import { createSignal } from 'solid-js';
export function Counter() {
const [count, setCount] = createSignal(0);
return (
<button
onClick={() => setCount(count() + 1)}
class="px-4 py-2 bg-stone-200 hover:bg-stone-300"
>
Count: {count()}
</button>
);
}
Use in Astro with client directive:
---
import { Counter } from '@components/Counter';
---
<Counter client:only="solid-js" />
Page Development
Creating New Pages
Create page file
Add a new file in src/pages/:// src/pages/about.astro
---
import Layout from '@layouts/Layout.astro';
import { getPage } from '../lib/notion-cms-page';
import { getBlock } from '../lib/notion-cms';
const pageId = import.meta.env.NOTION_PAGE_ID_ABOUT;
const page = await getPage(pageId);
const blocks = await getBlock(pageId);
---
<Layout
title="About"
description="About this website"
path="/about"
>
<Fragment slot="content">
<div class="prose prose-stone">
{/* Page content */}
</div>
</Fragment>
</Layout>
Add to navigation
Update the Notion pages database (NOTION_DB_ID_PAGES) with:
- Name: “About”
- Path: “/about”
- Order: Numeric position
Navigation updates automatically on next build. Test the page
Visit http://localhost:4321/about to verify
Build-Time Data Fetching
Pages fetch data at build time (SSG), not at request time:
---
import { getCollection } from 'astro:content';
// This runs ONCE during build
const posts = await getCollection('blog');
const publishedPosts = posts.filter(post => post.data.public);
---
<div>
{publishedPosts.map(post => (
<article>
<h2>{post.data.title}</h2>
<p>{post.data.description}</p>
</article>
))}
</div>
Testing Workflow
Running Tests
# Watch mode (runs on file changes)
pnpm test
# Single run with coverage
pnpm coverage
# Run specific test file
pnpm test src/lib/tests/notion-parse.test.ts
Writing Tests
Create test files in src/lib/tests/:
import { describe, it, expect } from 'vitest';
import { parseRichTextBlock } from '../notion-parse';
describe('parseRichTextBlock', () => {
it('should parse bold text', () => {
const input = {
rich_text: [{
type: 'text',
text: { content: 'hello', link: null },
annotations: { bold: true },
plain_text: 'hello'
}]
};
const output = parseRichTextBlock(input);
expect(output).toBe('<b>hello</b>');
});
});
See src/lib/tests/notion-parse.test.ts:1-128 for real examples.
Git Workflow
Commit Message Guidelines
Use imperative mood and be descriptive:
Good:
git commit -m "Add search functionality to blog page"
git commit -m "Fix mobile navigation z-index issue"
git commit -m "Update prose styling for better readability"
Avoid:
git commit -m "Updated stuff"
git commit -m "Fixed bugs"
git commit -m "WIP"
Pre-commit Checklist
Before committing:
Pull Request Process
Create PR
Push your branch and open a PR on GitHub
Fill PR template
## Summary
Brief description of changes
## Changes
- Added X feature
- Fixed Y bug
- Updated Z component
## Test Plan
- [ ] Tested locally
- [ ] Verified on preview deployment
- [ ] Checked mobile responsiveness
- [ ] Validated SEO metadata
Review preview deployment
Vercel automatically creates a preview deployment for PRs
Merge to main
Once approved, merge triggers production deployment
Deployment Workflow
The site deploys automatically via Vercel:
- Push to main → Production deployment
- Open PR → Preview deployment
- Environment variables → Configured in Vercel dashboard
Manual Build Check
Before merging to main:
# Build production version
pnpm build
# Preview locally
pnpm preview
# Visit http://localhost:4321
Code Organization
File Structure Pattern
Organize code consistently:
// External imports
import { marked } from 'marked';
import type { Block } from '@notionhq/client/build/src/api-endpoints';
// Internal imports
import { parseRichText } from './notion-parse';
// Types
interface ParseOptions {
format: 'markdown' | 'html';
}
// Constants
const DEFAULT_OPTIONS: ParseOptions = { format: 'markdown' };
// Main function
export function parseBlocks(blocks: Block[], options = DEFAULT_OPTIONS) {
// Implementation
}
// Helpers (private)
function isHeading(block: Block): boolean {
return block.type.startsWith('heading');
}
- Leverage static generation: Fetch data at build time, not runtime
- Optimize images: Use Astro’s
<Image /> component for automatic optimization
- Minimize bundle size: Prefer Solid.js over React for interactive components
- Incremental sync: Notion sync only updates changed content
- Preload fonts: Variable fonts are preloaded with
crossorigin attribute
Common Pitfalls
Don’t fetch data at runtime - This breaks static generation. Always use build-time data fetching in Astro components.
Remember client directives - Interactive components need client:only="solid-js" or similar directives to hydrate.
Check Notion permissions - If content doesn’t sync, verify the Notion integration has access to all databases and pages.
Next Steps