Imported from nublson/nublson (
AGENTS.md). Install upstream withnpx skills add nublson/nublson. Copyright stays with the author.
Learned User Preferences
- Prefers concise explanations after code fixes with explicit verification.
- Often asks to commit immediately after confirming a change works.
- Expects feature/fix work on a short-lived branch from
developbefore implementation, then commit/PR when asked.
Learned Workspace Facts
- Posts metadata includes Notion properties
Path(url),Source(url), andCategory(select). - Blog article pages expect
MorePoststo exclude the current post and show the next three latest posts. - Repository workflow uses
developas the integration/default branch for feature PRs before promotion tomain; a manual GitHub ActionsReleaseworkflow mergesdevelopintomain. - Content block renderers run as server components; avoid client-only hooks like
useEffectunless explicitly making the component client-side. - Published date rendering should prefer Notion datetime values when present and fall back to formatter utilities.
getDatabasePages(databaseId, media?, limit?, sorts?, filterProperties?)defaults toPublish Datedescending and blog/work filter properties when overrides are omitted.- Markdown negotiation serves blog and work posts: proxy rewrites to
/api/markdown/blog/[slug]or/api/markdown/work/[slug]whenAccept: text/markdownis present. - Agent discovery includes proxy
Linkheaders, dynamic/llms.txt,/agents.txt,/agents.json, AI bot rules inrobots.ts, and staticpublic/.well-known/*pluspublic/auth.md. - Share/OG image fallbacks must be raster (e.g.
/apple-icon.png), not SVG — Safari and many clients ignore SVG for link previews. - Blog/work
[slug]pages should await Notion data in the async page so article HTML renders inside<main>for scrapers; avoid parentloading.tsxskeletons on slug routes. - Index/listing pages (
/,/about,/projects,/writings, and similar): await in the page all non-list content (hero metadata + Notion page body/blocks) so it is in the initial HTML without JS. Where a list's length varies (/projects,/writings, and Home's projects), await the list too, fetched in parallel with the page viaPromise.all— its length is unknown, so no skeleton can match it and the swap shifts the layout. Use Suspense with skeletons only for secondary sections of known size (Home's 4 posts, "More posts", the reactions row), and build those skeletons from the real rows' line boxes so the swap is shift-free. - Descriptions render as Notion rich text via
RichDescription(links with profile favicons, emphasis, blank-line paragraphs): index pages'descriptionproperty (Home, About, Writing, Projects) and the article lede from posts'Descriptionproperty. Project rows, feeds and metadata use the joined plain text with whitespace collapsed. Edit copy like Home's "Find me on…" line in Notion, not in code. - Public blog view counts are lifetime unique visitors via Supabase (session cookie shared with reactions); blog-only UI; private daily uniques stay in Vercel Analytics.
Cursor Cloud specific instructions
Standard commands live in package.json scripts and CLAUDE.md (pnpm dev, build, lint, type-check, test). Package manager is pnpm (Node 22 is the CI baseline). The update script runs pnpm install --frozen-lockfile, so dependencies are already installed when a session starts.
Env vars: the Notion + Supabase secrets (NOTION_ACCESS_TOKEN, NOTION_DATABASE_*, NOTION_PAGE_*, NEXT_PUBLIC_SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY) are provided as injected Cloud-agent secrets, so they are already in process.env. The one var that is not injected is BASE_URL — before running pnpm dev, create .env (gitignored) with BASE_URL=http://localhost:3000, otherwise every route 500s (see below). .env values and injected env vars are merged by Next.js.
Non-obvious runtime gotchas:
- The app does not degrade gracefully without env vars.
BASE_URLmust be a valid absolute URL: the root layout doesmetadataBase: new URL(process.env.BASE_URL!)at module load, which throwsERR_INVALID_URL(HTTP 500 on every route) if unset.- All content comes from a private Notion workspace. With
BASE_URLset but an invalid/absentNOTION_ACCESS_TOKEN, routes return 200 at the HTTP layer but content sections throw401 API token is invalidand the browser shows a hard "server error" page (no error boundary). - Reactions use Supabase.
src/lib/supabase.tscreates the client at module load and throws ifNEXT_PUBLIC_SUPABASE_URL/SUPABASE_SERVICE_ROLE_KEYare missing, so post pages (/blog/[slug],/work/[slug]) and/api/reactions/*error without those vars. Core index pages do not import it. Posting a like/dislike writes to the real Supabasepost_reactionstable (dedups by session cookie + hashed IP). pnpm buildcallsgenerateStaticParams(), which queries Notion at build time, so a production build also needs valid Notion credentials. Usepnpm devfor local work.pnpm test(Vitest + jsdom) runs fully offline — coverage excludes app routes and the Notion/Supabase service layers, so no secrets are needed for the test suite, lint, or type-check.
