Language: English | 简体中文
This repo hosts a static-first personal blog built with Astro and powered by a Notion database as the CMS.
- Site project:
extra-ellipse/ - CMS: Notion
- Main Notion DB:
Posts(seePLAN.mdfor schema + Database ID)
Design goal: a modern editorial look (Claude Blog–inspired), with a free/low-cost deployment path (Cloudflare Pages or Vercel).
my-blog/
PLAN.md Project plan + Notion schema
README.md (this file)
extra-ellipse/ Astro app
src/ Pages + components
public/ Static assets (favicons, logo)
.env.example Local env template
.env Local env values (not committed)
package.jsonThe actual website lives under:
cd extra-ellipse- Node.js + npm
- A Notion integration secret available as env var:
NOTION_API_KEY
- Notion database ID available as env var or via
.env:NOTION_DATABASE_ID
The Notion integration must be connected to the parent page (e.g. Blog_claw) that contains the Posts database:
Notion → open the page → Connections → Add integration.
If the integration is not connected, API calls will fail even if the key is correct.
From repo root:
cd extra-ellipse
npm install
cp .env.example .env
# edit .env and set NOTION_API_KEY
npm run devThen open the printed local URL (usually http://localhost:4321).
Astro build requires Notion env vars to be present in the build process environment.
If your key is stored via macOS launchctl setenv, you can inject it explicitly:
cd extra-ellipse
NOTION_API_KEY="$(launchctl getenv NOTION_API_KEY)" \
NOTION_DATABASE_ID="301b5a28fc988125a53fc0781262e71c" \
npm run buildcd extra-ellipse
npm run previewThe Posts database is the source of truth.
Common fields (see PLAN.md for full schema):
- Name (title)
- Slug (rich_text)
- Status:
Draft|Published - PublishedAt (date)
- Excerpt (rich_text)
- Category (select)
- Tags (multi_select)
- Cover (files)
- SEO Title / SEO Description
- Featured (checkbox)
- Open the Notion
Postsdatabase. - Add a new row.
- Fill in:
- Name (title)
- Slug (URL-safe)
- Status =
Draftwhile writing - PublishedAt (date)
- Excerpt (optional)
- Category/Tags/Cover (optional)
- Write the article content in the Notion page body.
- When ready: set Status =
Published. - Rebuild/redeploy the site (or wait for the scheduled auto-redeploy; see Auto redeploy (every 15 min) below).
This project can also create posts programmatically via Notion API.
Example (Node, from extra-ellipse/):
cd extra-ellipse
NOTION_API_KEY="$(launchctl getenv NOTION_API_KEY)" node - <<'NODE'
import { Client } from '@notionhq/client';
const notion = new Client({ auth: process.env.NOTION_API_KEY });
const databaseId = '301b5a28fc988125a53fc0781262e71c'.replace(/-/g,'');
const now = new Date();
const slug = `hello-${now.getTime()}`;
const page = await notion.pages.create({
parent: { database_id: databaseId },
properties: {
Name: { title: [{ text: { content: 'Hello from API' } }] },
Slug: { rich_text: [{ text: { content: slug } }] },
Status: { select: { name: 'Draft' } },
PublishedAt: { date: { start: now.toISOString() } },
Excerpt: { rich_text: [{ text: { content: '' } }] },
'SEO Title': { rich_text: [{ text: { content: '' } }] },
'SEO Description': { rich_text: [{ text: { content: '' } }] },
Featured: { checkbox: false },
},
children: [
{ object: 'block', type: 'paragraph', paragraph: { rich_text: [{ type: 'text', text: { content: 'Write here…' } }] } }
]
});
console.log(page.url);
NODEThis repo includes a GitHub Actions workflow that triggers a Vercel Deploy Hook every 15 minutes, so Notion edits get picked up without manual deploys.
- Workflow:
.github/workflows/vercel-redeploy-every-15m.yml - Required GitHub secret:
VERCEL_DEPLOY_HOOK_URL
- In Vercel project → Settings → Git → Deploy Hooks → create a hook (branch:
main). - Copy the hook URL.
- In GitHub repo → Settings → Secrets and variables → Actions → add repository secret:
- Name:
VERCEL_DEPLOY_HOOK_URL - Value: (the hook URL)
- Name:
- (Optional) GitHub repo → Actions → run the workflow once via Run workflow to verify.
Notes:
- GitHub schedule can drift by a few minutes; that’s normal.
- This does not update instantly; the expected freshness is ≤ 15 minutes.
Current setup lives in:
extra-ellipse/public/favicon.icoextra-ellipse/public/favicon.svgextra-ellipse/src/components/Shell.astro(header logo)
- Header logo:
extra-ellipse/public/Blog-LOGO-removebg-preview.png
- Browser tab icon:
extra-ellipse/public/Blog-tab-icon.png
- Source/backup image used for icon crop:
extra-ellipse/public/Blog.backup-before-crop.png
Icon and logo links are configured in:
extra-ellipse/src/components/Shell.astro
Cause: the build process does not see the env var.
Fix: inject explicitly when building:
cd extra-ellipse
NOTION_API_KEY="$(launchctl getenv NOTION_API_KEY)" npm run build(Or set it in your shell profile and restart the terminal.)
Likely causes:
- Integration secret is wrong
- Integration is not connected to the Notion page / database
Fix: In Notion, open the parent page containing the database → Connections → add the integration.
Check in Notion:
Statusmust bePublishedSlugmust be set, unique, and URL-safe
Then rebuild.
Notion API calls happen at build time. If rate-limited, retry after a moment.
Cause: newer Notion SDK versions moved query APIs toward dataSources.query.
Current code already handles both query styles in:
extra-ellipse/src/lib/notion.ts
Cause: deeply nested/large Notion pages can trigger request timeout when fetching blocks.
Current mitigation in code:
- Increased client timeout (configurable via
NOTION_TIMEOUT_MS) - Retry wrapper for Notion API calls (
NOTION_RETRY_TIMES) - Limited-concurrency recursive child block fetching (
NOTION_CHILD_CONCURRENCY) - Max depth guard for recursive blocks (
NOTION_MAX_BLOCK_DEPTH)
Optional local tuning in .env:
NOTION_TIMEOUT_MS=60000
NOTION_RETRY_TIMES=3
NOTION_CHILD_CONCURRENCY=8- Improved Notion data fetch resilience in
extra-ellipse/src/lib/notion.ts:- retries for query/block APIs
- configurable timeout and recursion constraints
- safer child-block recursion under heavy pages
- Updated table styles to a minimal line-grid style in:
extra-ellipse/src/styles/global.css(imported byShell.astro)
- Removed zebra/card-heavy visuals; kept thin borders and compact spacing.
- Added right-side heading navigation on post pages:
- file:
extra-ellipse/src/pages/blog/[slug].astro
- file:
- TOC now:
- extracts only
h1/h2/h3headings - supports click jump + active-section sync
- uses a Notion-style slim rail by default
- shows expanded TOC panel on hover
- hides rail while panel is shown; restores rail on mouse leave
- includes hover bridge to prevent premature panel dismissal
- extracts only
PLAN.mdis the canonical human-readable plan + schema reference.extra-ellipse/.env.exampledocuments required and optional env vars.extra-ellipse/.envcontains local values and is intentionally not committed.- Deployment (Cloudflare Pages/Vercel): set env vars there as well:
NOTION_API_KEYNOTION_DATABASE_ID
See PLAN.md for the full milestone list. Short version:
- Featured strip (use
Featuredproperty) ✅ - Post page typography + previous/next
- RSS + sitemap
- Deploy (Cloudflare Pages or Vercel) ✅
- Optional: scheduled rebuild via GitHub Actions ✅
- Grid/List View Optimized: Aligned with Claude Blog reference (cover images, consistent chips, compact list rows).
- Multi-select Filters: Replaced native select with custom multi-select dropdowns for Category/Tag (OR logic).
- Unified UX: Applied consistent styles and behavior across Home (
/) and Blog (/blog) pages.
- Added a real
Aboutpage and wired nav route:- route:
/about - page file:
extra-ellipse/src/pages/about.astro - nav link update:
extra-ellipse/src/components/Shell.astro
- route:
- Updated homepage hero copy:
- title:
Becoming in Public - subtitle:
Write to become. Build to be free.
- title:
- Updated homepage topic entry links to:
BecomingThinking ToolsLived ExperienceQuiet Essays
- Updated branding assets:
- header logo:
extra-ellipse/public/Blog-LOGO-removebg-preview.png - browser tab icon:
extra-ellipse/public/Blog-tab-icon.png
- header logo:
- Improved Notion rendering and data compatibility:
- recursive block fetching (nested children)
- renderer support for
toggle,callout,table,to_do, nested list item children - compatibility for both
databases.query(old) anddataSources.query(SDK v5+)
- Tag/Category filters remain dynamic from Notion published posts (frontend does not hardcode values).