Personal website built with Astro, using Notion as a headless CMS.
Live site: https://ntemposd.me
This project implements a Notion-powered content pipeline:
- Writing and structured content are managed in Notion
- Posts and projects are transformed into Markdown
- Experience and working data are synced into local JSON files
- Astro compiles everything into a static site during build time
A custom sync layer connects Notion to the site:
- Fetches structured content from Notion databases
- Transforms Notion blocks into Markdown
- Downloads and caches assets locally
- Generates Astro content or local JSON data depending on the content type
This allows Notion to act as a CMS while keeping the site fully static.
- 📝 Notion CMS - Posts, projects, experience, and working data sync from Notion databases via API
- 🖼️ Image handling - Downloads and caches images locally, preserves Notion captions as Markdown alt text
- 💼 About page data sync - Latest experience renders with date ranges, and craftsmanship cards are populated from a dedicated Notion database
- 🌓 Dark mode - System preference detection with manual toggle, persists to localStorage
- 📱 Responsive nav - Mobile hamburger menu, desktop horizontal nav
- 📧 ButtonDown embed - Newsletter subscription iframe on post pages
- 🎨 Tailwind CSS - Responsive styling with typography plugin for prose content
- 🔍 SEO basics - Sitemap generation and Open Graph meta tags
- Node.js 20.19+ (recommended)
- npm or pnpm
- A Notion account with API integration
git clone https://github.com/ntemposd/myastro.git
cd myastro
npm installCreate a .env file in the root directory:
# Notion API
NOTION_SECRET=secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
NOTION_POSTS_DB_ID=your-posts-database-id
NOTION_PROJECTS_DB_ID=your-projects-database-id
NOTION_EXPERIENCE_DB_ID=your-experience-database-id
NOTION_WORKING_DB_ID=your-working-database-id
# Optional: customize date property name
NOTION_POSTS_DATE_PROP=Date
NOTION_POSTS_STRICT_DATE=true
# Site configuration (local .env)
SITE_BASE_URL=http://localhost:4321
# Optional: Google Analytics (only if you use GA)
PUBLIC_GA_ID=G-XXXXXXXXXXCreate four Notion databases with the following structures:
Title(title)Published(checkbox) orStatus(select: "Published")Date(date)Excerpt(text)Tags(multi-select)Slug(text, optional - auto-generated from title if empty)Image(files or URL, optional)
Name(title)Type(select)Published(checkbox)Excerpt(text)Tags(multi-select)Link(URL)Image(files or URL)
Role(title)Company(text)Start(date)End(date, optional - leave empty for current)Description(text)Location(text, optional)
Skill(title or text)Tools(multi-select preferred, rich text also supported)Description(text)Order(number, optional)
- Go to Notion Integrations
- Create a new integration and copy the "Internal Integration Token"
- Share your databases with the integration
- Copy each database ID from the URL (the part after the workspace name and before the
?)
npm run devOpens at http://localhost:4321
myastro/
├── public/ # Static assets served directly
│ ├── posts/ # Copied images for dev (avoid 404s)
│ └── robots.txt
├── scripts/ # Notion sync scripts
│ ├── sync-posts.ts
│ ├── sync-projects.ts
│ ├── sync-experience.ts
│ └── sync-working.ts
├── src/
│ ├── assets/ # Bundled images (hashed in prod)
│ │ ├── posts/
│ │ └── projects/
│ ├── components/
│ │ ├── Share.astro # Social sharing buttons
│ │ └── MailingListSubscribe.astro # Newsletter embed
│ ├── content/ # Content collections
│ │ ├── config.ts
│ │ ├── posts/ # Synced from Notion
│ │ ├── projects/
│ │ └── experience/
│ ├── data/
│ │ └── working/ # Synced JSON used by the about page craftsmanship section
│ ├── layouts/
│ │ └── Layout.astro # Base layout with SEO
│ ├── pages/
│ │ ├── index.astro
│ │ ├── about.astro
│ │ ├── projects.astro
│ │ └── writing/
│ │ ├── index.astro
│ │ └── [slug].astro
│ └── styles/
│ └── global.css
└── package.json
| Command | Action |
|---|---|
npm install |
Install dependencies |
npm run dev |
Start dev server + sync content |
npm run build |
Build production site + sync content |
npm run preview |
Preview production build locally |
npm run sync:content |
Sync all content from Notion |
npm run sync:posts |
Sync posts only |
npm run sync:working |
Sync working/craftsmanship data only |
npm run format |
Format code with Prettier |
Content is automatically synced from Notion during dev and build. To manually sync:
npm run sync:contentHow it works:
- Fetches entries from the posts, projects, experience, and working Notion databases via API
- Downloads post and project images to
src/assets/...andpublic/posts/ - Converts post content blocks to Markdown using
notion-to-md - Writes Markdown content to
src/content/posts/andsrc/content/projects/ - Writes ordered JSON data to
src/content/experience/_ordered.jsonandsrc/data/working/_ordered.json
Note: Image captions from Notion sync as Markdown alt text (). The public/posts/ copy prevents 404s during development before client-side URL rewriting kicks in.
Edit src/layouts/Layout.astro to change:
- Site title and description
- Open Graph images
- Google Analytics ID
- Global styles:
src/styles/global.css - Tailwind config:
tailwind.config.ts - Typography (prose): Configured via
@tailwindcss/typography
- Update your ButtonDown username in
src/components/MailingListSubscribe.astro - The embed iframe will load from
https://buttondown.com/api/emails/embed-subscribe/[username]
Automatically deploys to GitHub Pages on push to main via .github/workflows/deploy.yml.
- Go to repository Settings → Pages
- Source: GitHub Actions
- Configure repository secrets and variables (see below)
NOTION_SECRETNOTION_POSTS_DB_IDNOTION_PROJECTS_DB_IDNOTION_EXPERIENCE_DB_IDNOTION_WORKING_DB_IDPUBLIC_GA_ID(optional)
SITE_BASE_URL(live URL, e.g.,https://<user>.github.io/<repo>or your custom domain)
Push to main triggers automatic build and deploy.
Feel free to open an issue or reach out via ntemposd.me
MIT