From b4c790802961dc5ce1046778af5c7889eab34a1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E9=95=87?= Date: Thu, 8 Oct 2026 11:09:37 +0800 Subject: [PATCH] --- .gitignore | 3 + AGENTS.md | 9 - CLAUDE.md | 1 - DESIGN.md | 852 +++++ README.md | 487 ++- app/[locale]/about/page.tsx | 234 ++ app/[locale]/error.tsx | 59 + app/[locale]/layout.tsx | 117 + app/[locale]/not-found.tsx | 86 + app/[locale]/page.tsx | 135 + app/[locale]/posts/[slug]/page.tsx | 250 ++ app/api/health/route.ts | 44 + app/api/posts/[slug]/route.ts | 101 + app/api/posts/route.ts | 77 + app/api/route.ts | 52 + app/api/tags/route.ts | 14 + app/globals.css | 462 ++- app/layout.tsx | 64 +- app/not-found.tsx | 39 + app/page.tsx | 69 - app/robots.ts | 22 + app/sitemap.ts | 62 + components/home/hero.tsx | 111 + components/layout/locale-switcher.tsx | 82 + components/layout/site-footer.tsx | 107 + components/layout/site-header.tsx | 154 + components/mdx/callout.tsx | 67 + components/mdx/code-block.tsx | 119 + components/mdx/mdx-components.tsx | 74 + components/motion/motion-item.tsx | 78 + components/motion/reading-progress.tsx | 31 + components/motion/reveal.tsx | 73 + components/post/post-card.tsx | 79 + components/post/post-skeleton.tsx | 68 + components/post/table-of-contents.tsx | 99 + components/ui/badge.tsx | 40 + components/ui/button-link.tsx | 30 + components/ui/button-variants.ts | 57 + components/ui/button.tsx | 25 + components/ui/card.tsx | 42 + components/ui/layout.tsx | 84 + components/ui/section-heading.tsx | 52 + content/posts/building-a-blog-with-nextjs.mdx | 119 + .../posts/design-tokens-with-tailwind-v4.mdx | 144 + content/posts/react-server-components.mdx | 143 + content/posts/web-performance-priority.mdx | 15 + drizzle.config.ts | 13 + drizzle/migrations/0000_silky_shocker.sql | 21 + drizzle/migrations/meta/0000_snapshot.json | 167 + drizzle/migrations/meta/_journal.json | 13 + eslint.config.mjs | 27 +- i18n/navigation.ts | 5 + i18n/request.ts | 15 + i18n/routing.ts | 8 + lib/api/response.ts | 199 + lib/db/index.ts | 125 + lib/db/schema.ts | 107 + lib/fonts.ts | 86 + lib/mdx.ts | 71 + lib/posts/index.ts | 48 + lib/posts/repository.ts | 386 ++ lib/posts/types.ts | 40 + lib/site-config.ts | 38 + lib/toc.ts | 103 + lib/utils.ts | 29 + lib/validation/post.ts | 139 + messages/en.json | 81 + messages/ja.json | 81 + messages/zh.json | 81 + next.config.ts | 7 +- package.json | 38 +- pnpm-lock.yaml | 3285 +++++++++++++++++ pnpm-workspace.yaml | 3 + proxy.ts | 32 + public/og.svg | 40 + scripts/check-404.mjs | 42 + scripts/check-code-blocks.mjs | 85 + scripts/db-check.mjs | 68 + scripts/db-diagnose.mjs | 120 + scripts/db-verify-schema.mjs | 164 + scripts/diagnose-404.mjs | 35 + scripts/measure-overflow.mjs | 145 + scripts/screenshot.mjs | 86 + scripts/seed-posts.mjs | 189 + scripts/test-api.mjs | 306 ++ scripts/test-validation.mjs | 127 + skill/fullstack-task/SKILL.md | 143 + skill/fullstack-task/references/api-design.md | 55 + skill/fullstack-task/references/database.md | 46 + skill/fullstack-task/references/frontend.md | 43 + .../references/stack-defaults.md | 39 + skill/fullstack-task/references/testing.md | 37 + skill/fullstack-task/scripts/init-check.sh | 23 + .../fullstack-task/templates/feature-spec.md | 50 + .../fullstack-task/templates/pr-checklist.md | 13 + v0.1功能文档.md | 89 + 文档地址.md | 232 ++ 97 files changed, 12403 insertions(+), 154 deletions(-) delete mode 100644 AGENTS.md delete mode 100644 CLAUDE.md create mode 100644 DESIGN.md create mode 100644 app/[locale]/about/page.tsx create mode 100644 app/[locale]/error.tsx create mode 100644 app/[locale]/layout.tsx create mode 100644 app/[locale]/not-found.tsx create mode 100644 app/[locale]/page.tsx create mode 100644 app/[locale]/posts/[slug]/page.tsx create mode 100644 app/api/health/route.ts create mode 100644 app/api/posts/[slug]/route.ts create mode 100644 app/api/posts/route.ts create mode 100644 app/api/route.ts create mode 100644 app/api/tags/route.ts create mode 100644 app/not-found.tsx delete mode 100644 app/page.tsx create mode 100644 app/robots.ts create mode 100644 app/sitemap.ts create mode 100644 components/home/hero.tsx create mode 100644 components/layout/locale-switcher.tsx create mode 100644 components/layout/site-footer.tsx create mode 100644 components/layout/site-header.tsx create mode 100644 components/mdx/callout.tsx create mode 100644 components/mdx/code-block.tsx create mode 100644 components/mdx/mdx-components.tsx create mode 100644 components/motion/motion-item.tsx create mode 100644 components/motion/reading-progress.tsx create mode 100644 components/motion/reveal.tsx create mode 100644 components/post/post-card.tsx create mode 100644 components/post/post-skeleton.tsx create mode 100644 components/post/table-of-contents.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/button-link.tsx create mode 100644 components/ui/button-variants.ts create mode 100644 components/ui/button.tsx create mode 100644 components/ui/card.tsx create mode 100644 components/ui/layout.tsx create mode 100644 components/ui/section-heading.tsx create mode 100644 content/posts/building-a-blog-with-nextjs.mdx create mode 100644 content/posts/design-tokens-with-tailwind-v4.mdx create mode 100644 content/posts/react-server-components.mdx create mode 100644 content/posts/web-performance-priority.mdx create mode 100644 drizzle.config.ts create mode 100644 drizzle/migrations/0000_silky_shocker.sql create mode 100644 drizzle/migrations/meta/0000_snapshot.json create mode 100644 drizzle/migrations/meta/_journal.json create mode 100644 i18n/navigation.ts create mode 100644 i18n/request.ts create mode 100644 i18n/routing.ts create mode 100644 lib/api/response.ts create mode 100644 lib/db/index.ts create mode 100644 lib/db/schema.ts create mode 100644 lib/fonts.ts create mode 100644 lib/mdx.ts create mode 100644 lib/posts/index.ts create mode 100644 lib/posts/repository.ts create mode 100644 lib/posts/types.ts create mode 100644 lib/site-config.ts create mode 100644 lib/toc.ts create mode 100644 lib/utils.ts create mode 100644 lib/validation/post.ts create mode 100644 messages/en.json create mode 100644 messages/ja.json create mode 100644 messages/zh.json create mode 100644 proxy.ts create mode 100644 public/og.svg create mode 100644 scripts/check-404.mjs create mode 100644 scripts/check-code-blocks.mjs create mode 100644 scripts/db-check.mjs create mode 100644 scripts/db-diagnose.mjs create mode 100644 scripts/db-verify-schema.mjs create mode 100644 scripts/diagnose-404.mjs create mode 100644 scripts/measure-overflow.mjs create mode 100644 scripts/screenshot.mjs create mode 100644 scripts/seed-posts.mjs create mode 100644 scripts/test-api.mjs create mode 100644 scripts/test-validation.mjs create mode 100644 skill/fullstack-task/SKILL.md create mode 100644 skill/fullstack-task/references/api-design.md create mode 100644 skill/fullstack-task/references/database.md create mode 100644 skill/fullstack-task/references/frontend.md create mode 100644 skill/fullstack-task/references/stack-defaults.md create mode 100644 skill/fullstack-task/references/testing.md create mode 100644 skill/fullstack-task/scripts/init-check.sh create mode 100644 skill/fullstack-task/templates/feature-spec.md create mode 100644 skill/fullstack-task/templates/pr-checklist.md create mode 100644 v0.1功能文档.md create mode 100644 文档地址.md diff --git a/.gitignore b/.gitignore index 5ef6a52..b969e0e 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# 本地校验脚本产出的截图 +/.screenshots/ diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 643577d..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,9 +0,0 @@ - - -# This is NOT the Next.js you know - -This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. - -This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. - - diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 43c994c..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1 +0,0 @@ -@AGENTS.md diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..dbe946b --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,852 @@ +--- +version: alpha +name: Mintlify-design-analysis +description: Mintlify presents documentation infrastructure with a dual-mode aesthetic — atmospheric sky-gradient marketing heroes (cloud illustration backdrops, soft cream-to-blue washes) paired with dense developer-grade documentation surfaces. The system uses Inter for UI prose, Geist Mono for code, and a signature Mintlify green ({colors.brand-green}) reserved for accent CTAs and active states. Black-pill primary buttons dominate marketing, white-on-dark inversions appear on dark hero bands, and a 3-column documentation layout (sidebar / prose / TOC) anchors the developer experience. Coverage spans homepage, startups program, pricing comparison, and the live tabs documentation page. + +colors: + primary: "#0a0a0a" + on-primary: "#ffffff" + brand-green: "#00d4a4" + brand-green-deep: "#00b48a" + brand-green-soft: "#7cebcb" + brand-tag: "#3772cf" + brand-warn: "#c37d0d" + brand-annotate: "#1ba673" + brand-error: "#d45656" + brand-cursor: "#888888" + hero-sky-from: "#87a8c8" + hero-sky-to: "#f5e9d8" + hero-dark-from: "#1a3d4a" + hero-dark-to: "#2d5a4f" + testimonial-orange: "#f55a3c" + testimonial-orange-deep: "#cc3a1f" + canvas: "#ffffff" + canvas-dark: "#0a0a0a" + surface: "#f7f7f7" + surface-soft: "#fafafa" + surface-code: "#1c1c1e" + hairline: "#e5e5e5" + hairline-soft: "#ededed" + hairline-dark: "#1f1f1f" + ink: "#0a0a0a" + charcoal: "#1c1c1e" + slate: "#3a3a3c" + steel: "#5a5a5c" + stone: "#888888" + muted: "#a8a8aa" + on-dark: "#ffffff" + on-dark-muted: "#b3b3b3" + +typography: + hero-display: + fontFamily: Inter + fontSize: 72px + fontWeight: 600 + lineHeight: 1.05 + letterSpacing: -2px + display-lg: + fontFamily: Inter + fontSize: 56px + fontWeight: 600 + lineHeight: 1.10 + letterSpacing: -1.5px + heading-1: + fontFamily: Inter + fontSize: 48px + fontWeight: 600 + lineHeight: 1.10 + letterSpacing: -1px + heading-2: + fontFamily: Inter + fontSize: 36px + fontWeight: 600 + lineHeight: 1.20 + letterSpacing: -0.5px + heading-3: + fontFamily: Inter + fontSize: 28px + fontWeight: 600 + lineHeight: 1.25 + heading-4: + fontFamily: Inter + fontSize: 22px + fontWeight: 600 + lineHeight: 1.30 + heading-5: + fontFamily: Inter + fontSize: 18px + fontWeight: 600 + lineHeight: 1.40 + subtitle: + fontFamily: Inter + fontSize: 18px + fontWeight: 400 + lineHeight: 1.50 + body-md: + fontFamily: Inter + fontSize: 16px + fontWeight: 400 + lineHeight: 1.50 + body-md-medium: + fontFamily: Inter + fontSize: 16px + fontWeight: 500 + lineHeight: 1.50 + body-sm: + fontFamily: Inter + fontSize: 14px + fontWeight: 400 + lineHeight: 1.50 + body-sm-medium: + fontFamily: Inter + fontSize: 14px + fontWeight: 500 + lineHeight: 1.50 + caption: + fontFamily: Inter + fontSize: 13px + fontWeight: 400 + lineHeight: 1.40 + caption-bold: + fontFamily: Inter + fontSize: 13px + fontWeight: 600 + lineHeight: 1.40 + micro: + fontFamily: Inter + fontSize: 12px + fontWeight: 500 + lineHeight: 1.40 + micro-uppercase: + fontFamily: Inter + fontSize: 11px + fontWeight: 600 + lineHeight: 1.40 + letterSpacing: 0.5px + button-md: + fontFamily: Inter + fontSize: 14px + fontWeight: 500 + lineHeight: 1.30 + code-md: + fontFamily: Geist Mono + fontSize: 14px + fontWeight: 400 + lineHeight: 1.50 + code-sm: + fontFamily: Geist Mono + fontSize: 13px + fontWeight: 400 + lineHeight: 1.40 + code-inline: + fontFamily: Geist Mono + fontSize: 13px + fontWeight: 500 + lineHeight: 1.30 + +rounded: + xs: 4px + sm: 6px + md: 8px + lg: 12px + xl: 16px + xxl: 24px + full: 9999px + +spacing: + xxs: 4px + xs: 8px + sm: 12px + md: 16px + lg: 20px + xl: 24px + xxl: 32px + xxxl: 40px + section-sm: 48px + section: 64px + section-lg: 96px + hero: 120px + +components: + button-primary: + backgroundColor: "{colors.primary}" + textColor: "{colors.on-primary}" + typography: "{typography.button-md}" + rounded: "{rounded.full}" + padding: "10px 20px" + button-primary-pressed: + backgroundColor: "{colors.charcoal}" + textColor: "{colors.on-primary}" + button-primary-disabled: + backgroundColor: "{colors.hairline}" + textColor: "{colors.muted}" + button-accent-green: + backgroundColor: "{colors.brand-green}" + textColor: "{colors.primary}" + typography: "{typography.button-md}" + rounded: "{rounded.full}" + padding: "10px 20px" + button-on-dark: + backgroundColor: "{colors.on-dark}" + textColor: "{colors.primary}" + typography: "{typography.button-md}" + rounded: "{rounded.full}" + padding: "10px 20px" + button-secondary: + backgroundColor: "transparent" + textColor: "{colors.ink}" + typography: "{typography.button-md}" + rounded: "{rounded.full}" + padding: "10px 20px" + border: "1px solid {colors.hairline}" + button-ghost: + backgroundColor: "transparent" + textColor: "{colors.ink}" + typography: "{typography.button-md}" + rounded: "{rounded.md}" + padding: "8px 12px" + button-link: + backgroundColor: "transparent" + textColor: "{colors.ink}" + typography: "{typography.body-sm-medium}" + padding: "0" + button-icon-circular: + backgroundColor: "{colors.canvas}" + textColor: "{colors.ink}" + rounded: "{rounded.full}" + size: 32px + border: "1px solid {colors.hairline}" + card-base: + backgroundColor: "{colors.canvas}" + rounded: "{rounded.lg}" + padding: "{spacing.xl}" + border: "1px solid {colors.hairline}" + card-feature: + backgroundColor: "{colors.surface}" + rounded: "{rounded.lg}" + padding: "{spacing.xxl}" + card-help: + backgroundColor: "{colors.canvas}" + rounded: "{rounded.lg}" + padding: "{spacing.xl}" + border: "1px solid {colors.hairline}" + card-startup-perk: + backgroundColor: "{colors.canvas}" + rounded: "{rounded.lg}" + padding: "{spacing.xl}" + border: "1px solid {colors.hairline}" + pricing-card: + backgroundColor: "{colors.canvas}" + rounded: "{rounded.lg}" + padding: "{spacing.xxl}" + border: "1px solid {colors.hairline}" + pricing-card-featured: + backgroundColor: "{colors.canvas}" + rounded: "{rounded.lg}" + padding: "{spacing.xxl}" + border: "2px solid {colors.brand-green}" + shadow: "rgba(0, 212, 164, 0.08) 0px 8px 24px" + testimonial-card-feature: + backgroundColor: "{colors.testimonial-orange}" + textColor: "{colors.on-dark}" + rounded: "{rounded.lg}" + padding: "{spacing.section}" + testimonial-card-quote: + backgroundColor: "{colors.canvas}" + textColor: "{colors.ink}" + rounded: "{rounded.lg}" + padding: "{spacing.xxl}" + border: "1px solid {colors.hairline}" + text-input: + backgroundColor: "{colors.canvas}" + textColor: "{colors.ink}" + typography: "{typography.body-md}" + rounded: "{rounded.md}" + padding: "{spacing.sm} {spacing.md}" + border: "1px solid {colors.hairline}" + height: 40px + text-input-focused: + backgroundColor: "{colors.canvas}" + textColor: "{colors.ink}" + border: "2px solid {colors.brand-green}" + search-pill: + backgroundColor: "{colors.surface}" + textColor: "{colors.steel}" + typography: "{typography.body-sm}" + rounded: "{rounded.md}" + padding: "{spacing.xs} {spacing.md}" + height: 36px + border: "1px solid {colors.hairline}" + segmented-tab: + backgroundColor: "transparent" + textColor: "{colors.steel}" + typography: "{typography.body-sm-medium}" + padding: "{spacing.sm} {spacing.md}" + border: "0 0 2px transparent solid" + segmented-tab-active: + backgroundColor: "transparent" + textColor: "{colors.ink}" + typography: "{typography.body-sm-medium}" + border: "0 0 2px {colors.ink} solid" + pill-tab: + backgroundColor: "{colors.canvas}" + textColor: "{colors.steel}" + typography: "{typography.body-sm-medium}" + rounded: "{rounded.full}" + padding: "8px 16px" + border: "1px solid {colors.hairline}" + pill-tab-active: + backgroundColor: "{colors.primary}" + textColor: "{colors.on-primary}" + rounded: "{rounded.full}" + border: "1px solid {colors.primary}" + toggle-monthly-yearly: + backgroundColor: "{colors.surface}" + textColor: "{colors.ink}" + rounded: "{rounded.full}" + padding: "4px" + badge-discount: + backgroundColor: "{colors.brand-green}" + textColor: "{colors.primary}" + typography: "{typography.caption-bold}" + rounded: "{rounded.full}" + padding: "2px 8px" + badge-required: + backgroundColor: "{colors.brand-error}" + textColor: "{colors.on-dark}" + typography: "{typography.micro-uppercase}" + rounded: "{rounded.sm}" + padding: "2px 6px" + badge-type: + backgroundColor: "{colors.surface}" + textColor: "{colors.steel}" + typography: "{typography.code-sm}" + rounded: "{rounded.sm}" + padding: "2px 6px" + badge-tag: + backgroundColor: "rgba(55, 114, 207, 0.15)" + textColor: "{colors.brand-tag}" + typography: "{typography.caption-bold}" + rounded: "{rounded.sm}" + padding: "2px 8px" + promo-banner: + backgroundColor: "{colors.canvas-dark}" + textColor: "{colors.on-dark}" + typography: "{typography.body-sm-medium}" + padding: "{spacing.sm} {spacing.md}" + code-block: + backgroundColor: "{colors.surface-code}" + textColor: "{colors.on-dark}" + typography: "{typography.code-md}" + rounded: "{rounded.md}" + padding: "{spacing.md}" + code-block-header: + backgroundColor: "{colors.surface-code}" + textColor: "{colors.on-dark-muted}" + typography: "{typography.caption}" + padding: "{spacing.xs} {spacing.md}" + border: "0 0 1px {colors.hairline-dark} solid" + code-inline: + backgroundColor: "{colors.surface}" + textColor: "{colors.charcoal}" + typography: "{typography.code-inline}" + rounded: "{rounded.xs}" + padding: "2px 6px" + border: "1px solid {colors.hairline}" + property-row: + backgroundColor: "transparent" + textColor: "{colors.ink}" + typography: "{typography.body-sm}" + padding: "{spacing.md} 0" + border: "0 0 1px {colors.hairline-soft} solid" + feature-comparison-table: + backgroundColor: "{colors.canvas}" + textColor: "{colors.ink}" + typography: "{typography.body-sm}" + rounded: "{rounded.md}" + border: "1px solid {colors.hairline}" + feature-comparison-row: + backgroundColor: "{colors.canvas}" + textColor: "{colors.ink}" + padding: "{spacing.md} {spacing.lg}" + border: "0 0 1px {colors.hairline-soft} solid" + sidebar-nav-item: + backgroundColor: "transparent" + textColor: "{colors.steel}" + typography: "{typography.body-sm}" + rounded: "{rounded.sm}" + padding: "{spacing.xs} {spacing.md}" + sidebar-nav-item-active: + backgroundColor: "{colors.surface}" + textColor: "{colors.ink}" + typography: "{typography.body-sm-medium}" + sidebar-section-header: + backgroundColor: "transparent" + textColor: "{colors.steel}" + typography: "{typography.micro-uppercase}" + padding: "{spacing.md} {spacing.md} {spacing.xs}" + doc-toc-item: + backgroundColor: "transparent" + textColor: "{colors.steel}" + typography: "{typography.body-sm}" + padding: "{spacing.xxs} 0" + doc-toc-item-active: + backgroundColor: "transparent" + textColor: "{colors.ink}" + typography: "{typography.body-sm-medium}" + copy-code-button: + backgroundColor: "transparent" + textColor: "{colors.on-dark-muted}" + typography: "{typography.caption}" + rounded: "{rounded.sm}" + padding: "{spacing.xxs} {spacing.xs}" + border: "1px solid {colors.hairline-dark}" + hero-band-sky: + backgroundColor: "{colors.hero-sky-from}" + textColor: "{colors.on-dark}" + rounded: "0" + padding: "{spacing.hero}" + hero-band-dark: + backgroundColor: "{colors.hero-dark-from}" + textColor: "{colors.on-dark}" + rounded: "0" + padding: "{spacing.hero}" + hero-product-mockup: + backgroundColor: "{colors.canvas}" + rounded: "{rounded.lg}" + padding: "0" + border: "1px solid {colors.hairline-soft}" + shadow: "rgba(0, 0, 0, 0.12) 0px 24px 48px -8px" + logo-wall-item: + backgroundColor: "transparent" + textColor: "{colors.steel}" + typography: "{typography.body-md-medium}" + padding: "{spacing.lg}" + faq-accordion-item: + backgroundColor: "{colors.canvas}" + rounded: "{rounded.md}" + padding: "{spacing.xl}" + border: "1px solid {colors.hairline-soft}" + footer-region: + backgroundColor: "{colors.canvas}" + textColor: "{colors.steel}" + typography: "{typography.body-sm}" + padding: "{spacing.section} {spacing.xxl}" + border: "1px solid {colors.hairline}" + footer-link: + backgroundColor: "transparent" + textColor: "{colors.steel}" + typography: "{typography.body-sm}" + padding: "{spacing.xxs} 0" + startup-program-card: + backgroundColor: "{colors.canvas}" + rounded: "{rounded.lg}" + padding: "{spacing.xxl}" + border: "1px solid {colors.hairline}" + founder-quote-card: + backgroundColor: "{colors.testimonial-orange}" + textColor: "{colors.on-dark}" + rounded: "{rounded.lg}" + padding: "{spacing.xxl}" +--- + +## Overview + +Mintlify positions itself at the intersection of polished marketing presentation and developer-grade documentation density. The home and startups pages open with cinematic atmospheric heroes — soft sky-gradient backdrops with cloud illustrations on the homepage, dark teal-to-mint gradients with a rocket launch on the startups page — that feel more like a SaaS landing aesthetic than a developer tool. Then the deeper surfaces (pricing comparison, live documentation pages) collapse into dense, high-information layouts where Inter body type carries 14–16px copy across long-form prose, syntax-highlighted code blocks, and 3-column documentation grids. + +The brand's signature mint green ({colors.brand-green}) appears sparingly but decisively — on the hero "Get started" pill button, the green checkmark icons inside feature lists, the "Featured" pricing tier border, and active state indicators inside docs UI. Black-pill primary buttons dominate the marketing flow; white-on-dark inversions appear on dark hero bands. The signature pairing of Inter (body, headings) with Geist Mono (code blocks, inline references, type signatures) reinforces the developer-tool DNA without requiring a third typeface. + +**Key Characteristics:** +- Atmospheric gradient hero bands (sky-blue to cream on homepage; teal-to-mint on startups) provide cinematic marketing presentation +- Signature Mintlify mint green ({colors.brand-green}) reserved for accent CTAs, active states, and feature confirmations +- Black-pill primary buttons ({colors.primary} + `{rounded.full}`) for marketing CTAs +- Inter for all UI prose; Geist Mono for code blocks, inline code, and type/property signatures +- 3-column documentation layout (sidebar / prose / TOC) with dense 14px body type for long-form developer reading +- Tightly-controlled radius scale: marketing uses `{rounded.lg}` (12px), pill buttons use `{rounded.full}` — no in-between corner softening +- Vibrant testimonial card (`{colors.testimonial-orange}`) breaks color rhythm intentionally for emotional impact + +## Colors + +> Source pages: mintlify.com/ (homepage), /startups (program page), /pricing (comparison), /docs/components/tabs (live documentation). Token coverage was identical across all four pages. + +### Brand & Accent +- **Mintlify Mint** ({colors.brand-green}): Signature accent — used on hero "Get started" pill button, green checkmarks in feature lists, featured pricing tier border accent, sidebar active indicator dots. +- **Deep Mint** ({colors.brand-green-deep}): Pressed/active variant of the mint accent. +- **Soft Mint** ({colors.brand-green-soft}): Subtle background tint for success states and confirmation surfaces. +- **Brand Tag** ({colors.brand-tag}): Documentation tag and reference color (used in `` JSX-style annotations and code-tag chips). +- **Brand Annotate** ({colors.brand-annotate}): Inline code annotation green (used in twoslash code annotation system). +- **Brand Warn** ({colors.brand-warn}): Code warning highlight (deprecated, caution). +- **Brand Error** ({colors.brand-error}): Red used for required-field labels and error highlight. +- **Testimonial Orange** ({colors.testimonial-orange}): Warm coral-orange used on the "Cursor" testimonial card and warm callout surfaces. + +### Surface +- **Canvas White** ({colors.canvas}): Primary page and card background. +- **Canvas Dark** ({colors.canvas-dark}): Promo banner, dark inversion surfaces, code editor wrapper. +- **Surface** ({colors.surface}): Subtle section backgrounds, search-pill rest, code-inline background, sidebar active state. +- **Surface Soft** ({colors.surface-soft}): Quieter section backgrounds and FAQ accordion. +- **Surface Code** ({colors.surface-code}): Dark code-block wrapper background. +- **Hairline** ({colors.hairline}): 1px borders and primary dividers. +- **Hairline Soft** ({colors.hairline-soft}): Quieter table-row dividers and secondary section breaks. + +### Hero Atmospheric +- **Hero Sky From / To** ({colors.hero-sky-from}, {colors.hero-sky-to}): Atmospheric sky-blue to soft cream gradient on the homepage hero. +- **Hero Dark From / To** ({colors.hero-dark-from}, {colors.hero-dark-to}): Dark teal to mint gradient on the startups hero. + +### Text +- **Ink** ({colors.ink}): Primary headlines and CTA text. +- **Charcoal** ({colors.charcoal}): Body text, code-inline foreground. +- **Slate** ({colors.slate}): Secondary text and metadata. +- **Steel** ({colors.steel}): Tertiary text, table headers, sidebar inactive items, footer links. +- **Stone** ({colors.stone}): Captions, twoslash cursor color, muted labels. +- **Muted** ({colors.muted}): De-emphasized labels and disabled text. +- **On Dark** ({colors.on-dark}): White text on dark surfaces (hero bands, code blocks, promo banner). +- **On Dark Muted** ({colors.on-dark-muted}): Reduced-opacity white for code-block headers and metadata on dark. + +### Semantic +- Error tones derive from `{colors.brand-error}` for input borders, required-field labels, and validation messaging. + +## Typography + +### Font Family +**Inter** (primary): Variable typeface optimized for UI legibility. Used across every UI surface — body, headings, navigation, button labels, captions. Fallbacks: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif. + +**Geist Mono** (code): Monospace typeface used inside code blocks, inline code references, type signatures (e.g. `string`, `number`, `boolean`), and property names in API documentation. Fallbacks: 'SF Mono', Menlo, Consolas, 'Geist Mono Fallback', monospace. + +The brand uses no italic variants of either face — emphasis comes from weight (500/600), color shift, or background highlighting (in code references). + +### Hierarchy + +| Token | Size | Weight | Line Height | Letter Spacing | Use | +|---|---|---|---|---|---| +| `{typography.hero-display}` | 72px | 600 | 1.05 | -2px | Marketing hero display ("The intelligent Knowledge Platform") | +| `{typography.display-lg}` | 56px | 600 | 1.10 | -1.5px | Major section opener ("Built for the intelligence age") | +| `{typography.heading-1}` | 48px | 600 | 1.10 | -1px | Page-level headlines ("Pricing on your terms") | +| `{typography.heading-2}` | 36px | 600 | 1.20 | -0.5px | Section headlines ("Apply to the Mintlify startup program") | +| `{typography.heading-3}` | 28px | 600 | 1.25 | 0 | Subsection headers, "Tabs" docs page title | +| `{typography.heading-4}` | 22px | 600 | 1.30 | 0 | Card titles, larger feature headers | +| `{typography.heading-5}` | 18px | 600 | 1.40 | 0 | Smaller feature headers, FAQ question titles | +| `{typography.subtitle}` | 18px | 400 | 1.50 | 0 | Hero subtitle, lead body | +| `{typography.body-md}` | 16px | 400 | 1.50 | 0 | Primary body text | +| `{typography.body-md-medium}` | 16px | 500 | 1.50 | 0 | Body emphasis | +| `{typography.body-sm}` | 14px | 400 | 1.50 | 0 | Secondary body, table cells, navigation | +| `{typography.body-sm-medium}` | 14px | 500 | 1.50 | 0 | Active sidebar nav, button labels, tab labels | +| `{typography.caption}` | 13px | 400 | 1.40 | 0 | Helper text, fine print, code-block headers | +| `{typography.caption-bold}` | 13px | 600 | 1.40 | 0 | Badge labels | +| `{typography.micro}` | 12px | 500 | 1.40 | 0 | Footer microcopy, label chips | +| `{typography.micro-uppercase}` | 11px | 600 | 1.40 | 0.5px | Sidebar section headers, "REQUIRED" labels | +| `{typography.button-md}` | 14px | 500 | 1.30 | 0 | Pill button labels | +| `{typography.code-md}` | 14px | 400 | 1.50 | 0 | Code block content | +| `{typography.code-sm}` | 13px | 400 | 1.40 | 0 | Smaller code, type signatures | +| `{typography.code-inline}` | 13px | 500 | 1.30 | 0 | Inline `` references in body | + +### Principles +- **Tight hero leading** (1.05) creates magazine-grade display headlines on the 72px hero +- **Negative letter-spacing** progresses inversely with size — display sizes use -2px to -1.5px; smaller headings relax to 0 +- **Documentation-grade body** (1.50 line-height on 14–16px) ensures comfortable long-form reading in dense docs surfaces +- **Inter / Geist Mono pairing** — Inter for everything else, Geist Mono surgically for code references; the contrast between the two is the brand's developer-respect signal +- **Uppercase micro labels** with +0.5px letter-spacing carry sidebar section headers and "REQUIRED" annotation tags + +## Layout + +### Spacing System +- **Base unit**: 4px (8px primary increment) +- **Tokens**: `{spacing.xxs}` (4px) · `{spacing.xs}` (8px) · `{spacing.sm}` (12px) · `{spacing.md}` (16px) · `{spacing.lg}` (20px) · `{spacing.xl}` (24px) · `{spacing.xxl}` (32px) · `{spacing.xxxl}` (40px) · `{spacing.section-sm}` (48px) · `{spacing.section}` (64px) · `{spacing.section-lg}` (96px) · `{spacing.hero}` (120px) +- **Section rhythm**: Marketing pages use `{spacing.section-lg}` (96px) between major bands; pricing comparison tightens to `{spacing.section}` (64px); documentation surfaces use `{spacing.xxl}` (32px) between subsections +- **Card internal padding**: Standard `{spacing.xl}` (24px) for compact cards; `{spacing.xxl}` (32px) for pricing cards and feature panels; testimonial card pushes to `{spacing.section}` (64px) for hero-card presence + +### Grid & Container +- Marketing pages use a 1280px max-width with 32px gutters +- Hero and feature bands often use 2-column splits (text left, illustration/mockup right) +- Pricing page renders 3 tier cards in a row at desktop (FREE / Lift Off / Custom), then a comprehensive feature comparison table below +- Documentation pages use a strict 3-column grid: left sidebar nav (~240px), center prose (~720px max-width), right TOC (~200px) +- Logo walls use 6-up rows of customer logos at 80–100px height each + +### Whitespace Philosophy +Marketing surfaces give content generous breathing room — `{spacing.hero}` (120px) above-the-fold creates space for atmospheric gradient backdrops to read clearly. Documentation tightens dramatically: section gaps drop to `{spacing.xxl}` (32px), table rows pack to `{spacing.md}` (16px), sidebar nav compresses to `{spacing.xs}` (8px) vertical rhythm. + +## Elevation & Depth + +The system runs predominantly flat with strategic atmospheric depth. + +| Level | Treatment | Use | +|---|---|---| +| 0 (flat) | No shadow; `{colors.hairline}` border | Default cards, table rows, form inputs | +| 1 (subtle) | `rgba(0, 0, 0, 0.04) 0px 1px 2px 0px` | Hover-elevated tiles, subtle highlights | +| 2 (card) | `rgba(0, 0, 0, 0.08) 0px 4px 12px 0px` | Standard feature cards | +| 3 (mockup) | `rgba(0, 0, 0, 0.12) 0px 24px 48px -8px` | Hero product mockup framing — the deep diffuse drop on the homepage hero docs preview | +| 4 (brand-tinted) | `rgba(0, 212, 164, 0.08) 0px 8px 24px` | Featured pricing tier glow | + +### Decorative Depth +- The homepage hero uses an atmospheric photographic backdrop (cloud illustration on sky-gradient) for depth — no shadow needed; the imagery does the work +- The startups hero uses a similar treatment with a rocket-launch illustration cutting across the dark teal gradient +- Code blocks carry their own internal depth via syntax-highlighting color hierarchy on the dark surface; no shadow used + +## Shapes + +### Border Radius Scale + +| Token | Value | Use | +|---|---|---| +| `{rounded.xs}` | 4px | Inline code chips, micro tags | +| `{rounded.sm}` | 6px | Sidebar nav items, type badges | +| `{rounded.md}` | 8px | Inputs, search pill, code blocks, secondary cards | +| `{rounded.lg}` | 12px | Standard cards, pricing tiers, hero mockup, FAQ items | +| `{rounded.xl}` | 16px | Larger feature panels | +| `{rounded.xxl}` | 24px | Featured product showcase tiles | +| `{rounded.full}` | 9999px | All buttons, pill tabs, badges | + +The radius scale is tightly disciplined — the brand never uses a corner softening between `{rounded.md}` (8px) and `{rounded.lg}` (12px) for the same component family. Pill buttons (`{rounded.full}`) are used universally; rectangular cards use `{rounded.lg}` (12px) consistently. + +### Photography Geometry +- Hero illustrations (cloud, rocket) sit on full-bleed gradient backdrops with no internal framing +- Customer logo walls use 1:1 ratio cells without rounding (logos are presented inline as wordmarks) +- Testimonial photos use 1:1 aspect with `{rounded.md}` (8px) softening +- Code editor mockup hero image uses `{rounded.lg}` (12px) corners on a hairline-bordered card with a deep diffuse drop shadow + +## Components + +> Per the no-hover policy, hover states are NOT documented. Default and pressed/active states only. + +### Buttons + +**`button-primary`** — Black pill primary CTA, the dominant action across all surfaces. +- Background `{colors.primary}`, text `{colors.on-primary}`, typography `{typography.button-md}`, padding `10px 20px`, rounded `{rounded.full}`. +- Pressed state `button-primary-pressed` lifts to `{colors.charcoal}`. +- Disabled state `button-primary-disabled` uses `{colors.hairline}` background and `{colors.muted}` text. + +**`button-accent-green`** — Mint green pill for brand-emphasis CTAs (hero "Get started", featured pricing CTA). +- Background `{colors.brand-green}`, text `{colors.primary}`, typography `{typography.button-md}`, padding `10px 20px`, rounded `{rounded.full}`. + +**`button-on-dark`** — White pill for use on dark hero bands (startups page "Get started"). +- Background `{colors.on-dark}`, text `{colors.primary}`, typography `{typography.button-md}`, padding `10px 20px`, rounded `{rounded.full}`. + +**`button-secondary`** — Outlined pill for secondary actions. +- Background transparent, text `{colors.ink}`, border `1px solid {colors.hairline}`, typography `{typography.button-md}`, padding `10px 20px`, rounded `{rounded.full}`. + +**`button-ghost`** — Quieter rectangular ghost button (sidebar action, tertiary nav). +- Background transparent, text `{colors.ink}`, typography `{typography.button-md}`, padding `8px 12px`, rounded `{rounded.md}`. + +**`button-link`** — Inline text link styled as a subtle button. +- Background transparent, text `{colors.ink}`, typography `{typography.body-sm-medium}`, padding `0`. Underline appears on activation. + +**`button-icon-circular`** — 32×32px circular utility button (close, copy, arrow). +- Background `{colors.canvas}`, text `{colors.ink}`, border `1px solid {colors.hairline}`, rounded `{rounded.full}`. + +### Cards & Containers + +**`card-base`** — Standard documentation/feature card. +- Background `{colors.canvas}`, rounded `{rounded.lg}`, padding `{spacing.xl}`, border `1px solid {colors.hairline}`. + +**`card-feature`** — Feature panel on light gray surface. +- Background `{colors.surface}`, rounded `{rounded.lg}`, padding `{spacing.xxl}`. + +**`card-help`** — "Need help?" CTA cards below the pricing comparison ("Quickstart guide", "Guide to technical writing", "Founder", "Sales"). +- Background `{colors.canvas}`, rounded `{rounded.lg}`, padding `{spacing.xl}`, border `1px solid {colors.hairline}`. + +**`card-startup-perk`** — Startup-program perk grid item ("Discounts and credits", "Priority support", "Startup pack", "Founder community"). +- Background `{colors.canvas}`, rounded `{rounded.lg}`, padding `{spacing.xl}`, border `1px solid {colors.hairline}`. Carries an icon at top, heading `{typography.heading-5}`, description `{typography.body-sm}` `{colors.steel}`. + +**`pricing-card`** — Standard pricing tier card. +- Background `{colors.canvas}`, rounded `{rounded.lg}`, padding `{spacing.xxl}`, border `1px solid {colors.hairline}`. +- Title `{typography.heading-3}`, price `{typography.display-lg}`, feature list `{typography.body-sm}` with green checkmark icons. + +**`pricing-card-featured`** — Highlighted pricing tier (Lift Off / featured plan). +- Background `{colors.canvas}`, rounded `{rounded.lg}`, padding `{spacing.xxl}`, border `2px solid {colors.brand-green}`, soft brand-tinted shadow `rgba(0, 212, 164, 0.08) 0px 8px 24px`. + +**`testimonial-card-feature`** — Bright orange large testimonial card with photo + quote ("Cursor — Every YC batch we consistently see the top performing startups use Mintlify to build their docs."). +- Background `{colors.testimonial-orange}`, text `{colors.on-dark}`, rounded `{rounded.lg}`, padding `{spacing.section}`. Photo on right, large quote in `{typography.heading-3}` left, attribution below in `{typography.body-sm-medium}`. + +**`testimonial-card-quote`** — Smaller white testimonial card on the startups page. +- Background `{colors.canvas}`, rounded `{rounded.lg}`, padding `{spacing.xxl}`, border `1px solid {colors.hairline}`. + +**`founder-quote-card`** — Cursor founder testimonial card variant on the orange surface. +- Background `{colors.testimonial-orange}`, text `{colors.on-dark}`, rounded `{rounded.lg}`, padding `{spacing.xxl}`. Carries the specific founder portrait + quote treatment. + +**`startup-program-card`** — Larger application/program card containing perks grid + apply CTA. +- Background `{colors.canvas}`, rounded `{rounded.lg}`, padding `{spacing.xxl}`, border `1px solid {colors.hairline}`. + +### Inputs & Forms + +**`text-input`** — Standard text field. +- Background `{colors.canvas}`, text `{colors.ink}`, border `1px solid {colors.hairline}`, rounded `{rounded.md}`, padding `{spacing.sm} {spacing.md}`, height 40px. + +**`text-input-focused`** — Activated state. +- Border switches to `2px solid {colors.brand-green}` — focus uses the brand mint as the activation signal. + +**`search-pill`** — Documentation top-bar search. +- Background `{colors.surface}`, text `{colors.steel}`, typography `{typography.body-sm}`, rounded `{rounded.md}`, height 36px, border `1px solid {colors.hairline}`. + +### Tabs + +**`segmented-tab`** + **`segmented-tab-active`** — Underline-style tab navigation (used inside docs Tabs component for "First tab / Second tab / Third tab"). +- Inactive: text `{colors.steel}`, transparent background, padding `{spacing.sm} {spacing.md}`. Active: text `{colors.ink}`, 2px bottom border in `{colors.ink}`. + +**`pill-tab`** + **`pill-tab-active`** — Pill-style tab nav (top of pricing page: "Pricing / Roadmap"). +- Inactive: background `{colors.canvas}`, text `{colors.steel}`, border `1px solid {colors.hairline}`, padding `8px 16px`, rounded `{rounded.full}`. +- Active: background `{colors.primary}`, text `{colors.on-primary}`, no border. + +**`toggle-monthly-yearly`** — Two-state pill toggle (Monthly / Annual on pricing page). +- Background `{colors.surface}`, rounded `{rounded.full}`, padding `4px`. Active state moves a white pill thumb to the selected position. + +### Badges & Status + +**`badge-discount`** — Small green "Save 20%" badge attached to annual toggle. +- Background `{colors.brand-green}`, text `{colors.primary}`, typography `{typography.caption-bold}`, rounded `{rounded.full}`, padding `2px 8px`. + +**`badge-required`** — Red "REQUIRED" label on documentation property rows. +- Background `{colors.brand-error}`, text `{colors.on-dark}`, typography `{typography.micro-uppercase}`, rounded `{rounded.sm}`, padding `2px 6px`. + +**`badge-type`** — Type signature chip in documentation (e.g. `string`, `number`, `boolean`). +- Background `{colors.surface}`, text `{colors.steel}`, typography `{typography.code-sm}`, rounded `{rounded.sm}`, padding `2px 6px`. + +**`badge-tag`** — Documentation tag chip (e.g. `` reference highlighted in body text). +- Background `rgba(55, 114, 207, 0.15)`, text `{colors.brand-tag}`, typography `{typography.caption-bold}`, rounded `{rounded.sm}`, padding `2px 8px`. + +**`promo-banner`** — Sticky black promo strip ABOVE the top nav (when present). +- Background `{colors.canvas-dark}`, text `{colors.on-dark}`, typography `{typography.body-sm-medium}`, padding `{spacing.sm} {spacing.md}`. + +### Code + +**`code-block`** — Syntax-highlighted code container. +- Background `{colors.surface-code}`, text `{colors.on-dark}`, typography `{typography.code-md}`, rounded `{rounded.md}`, padding `{spacing.md}`. + +**`code-block-header`** — Header bar above the code with language label + copy button. +- Background `{colors.surface-code}`, text `{colors.on-dark-muted}`, typography `{typography.caption}`, padding `{spacing.xs} {spacing.md}`, bottom border `1px solid {colors.hairline-dark}`. + +**`code-inline`** — Inline `` reference in body prose. +- Background `{colors.surface}`, text `{colors.charcoal}`, typography `{typography.code-inline}`, rounded `{rounded.xs}`, padding `2px 6px`, border `1px solid {colors.hairline}`. + +**`copy-code-button`** — "Copy code" button in code-block header. +- Background transparent, text `{colors.on-dark-muted}`, typography `{typography.caption}`, rounded `{rounded.sm}`, padding `{spacing.xxs} {spacing.xs}`, border `1px solid {colors.hairline-dark}`. + +### Documentation Components + +**`property-row`** — API property documentation row (e.g. `defaultIndex` on the Tabs page). +- Background transparent, text `{colors.ink}`, typography `{typography.body-sm}`, padding `{spacing.md} 0`, bottom border `1px solid {colors.hairline-soft}`. +- Layout: property name in `{typography.code-inline}` + type badge + optional REQUIRED badge + description below in `{typography.body-sm}` `{colors.steel}`. + +**`feature-comparison-table`** — Detailed pricing-page feature comparison table. +- Background `{colors.canvas}`, text `{colors.ink}`, typography `{typography.body-sm}`, rounded `{rounded.md}`, border `1px solid {colors.hairline}`. + +**`feature-comparison-row`** — Individual row inside the comparison table. +- Background `{colors.canvas}`, text `{colors.ink}`, padding `{spacing.md} {spacing.lg}`, bottom border `1px solid {colors.hairline-soft}`. Section dividers in `{typography.micro-uppercase}` `{colors.steel}`. + +**`sidebar-nav-item`** + **`sidebar-nav-item-active`** — Documentation left rail link entries. +- Inactive: background transparent, text `{colors.steel}`, typography `{typography.body-sm}`, rounded `{rounded.sm}`, padding `{spacing.xs} {spacing.md}`. +- Active: background `{colors.surface}`, text `{colors.ink}`, typography `{typography.body-sm-medium}`. + +**`sidebar-section-header`** — Uppercase section header inside sidebar (e.g. "COMPONENTS", "PRIMITIVES"). +- Background transparent, text `{colors.steel}`, typography `{typography.micro-uppercase}`, padding `{spacing.md} {spacing.md} {spacing.xs}`. + +**`doc-toc-item`** + **`doc-toc-item-active`** — Right-rail table-of-contents links. +- Inactive: background transparent, text `{colors.steel}`, typography `{typography.body-sm}`, padding `{spacing.xxs} 0`. +- Active: text `{colors.ink}`, typography `{typography.body-sm-medium}`, optional left-border accent in `{colors.brand-green}`. + +### Navigation + +**Top Navigation (Marketing)** — Sticky white bar with logo, link list, and right-side CTAs. +- Background `{colors.canvas}`, height ~64px, bottom border `1px solid {colors.hairline-soft}`. +- Left: Mintlify wordmark + horizontal link list (Solutions, Pricing, Customers, Documentation, Changelog). +- Right: secondary "Talk to sales" + black-pill "Get Started". + +**Top Navigation (Documentation)** — Compressed nav with center search-pill and right-side account/upgrade CTAs. +- Background `{colors.canvas}`, height ~56px. Search-pill at center, "Documentation / Guides / API Reference / Changelog" links + "Talk to us" + green "Get started" right. + +### Signature Components + +**`hero-band-sky`** — Homepage hero with atmospheric sky-blue to cream gradient and cloud illustrations. +- Background gradient `linear-gradient(180deg, {colors.hero-sky-from} 0%, {colors.hero-sky-to} 100%)`, text `{colors.on-dark}` (early portion of gradient) shifting to `{colors.ink}` further down, padding `{spacing.hero}`. +- Layout: centered hero headline in `{typography.hero-display}`, centered subtitle in `{typography.subtitle}`, centered button row (`button-accent-green` "Get started" + `button-secondary` "Talk to us"), product mockup below the buttons. + +**`hero-band-dark`** — Startups hero with dark teal-to-mint gradient and rocket launch illustration. +- Background gradient `linear-gradient(135deg, {colors.hero-dark-from} 0%, {colors.hero-dark-to} 100%)`, text `{colors.on-dark}`, padding `{spacing.hero}`. +- Layout: hero headline left in `{typography.hero-display}` `{colors.on-dark}`, illustration right (rocket cutting across the gradient), button row uses `button-on-dark` (white pill) + ghost link. + +**`hero-product-mockup`** — Code-editor mockup framed inside the homepage hero. +- Background `{colors.canvas}`, rounded `{rounded.lg}`, border `1px solid {colors.hairline-soft}`, deep shadow `rgba(0, 0, 0, 0.12) 0px 24px 48px -8px`. +- Carries a documentation page preview inside (sidebar on left, prose body, mock UI controls). + +**`logo-wall-item`** — Customer logo cell in 6-up trust-row grids ("Anthropic / Cognition / Mintlify / Vercel / react / Lovable", "Stripe / Block / PayPal / Compound / Auth"). +- Background transparent, text `{colors.steel}`, typography `{typography.body-md-medium}`, padding `{spacing.lg}`. +- Logos rendered as wordmarks with consistent vertical centering. + +**`faq-accordion-item`** — Frequently-asked-questions panel item (visible on pricing page). +- Background `{colors.canvas}`, rounded `{rounded.md}`, padding `{spacing.xl}`, border `1px solid {colors.hairline-soft}`. +- Question in `{typography.heading-5}`, expanded answer in `{typography.body-md}` `{colors.steel}`, chevron icon in `{colors.steel}` 16px. + +**`footer-region`** — Multi-column site footer. +- Background `{colors.canvas}`, top border `1px solid {colors.hairline}`, padding `{spacing.section} {spacing.xxl}`. +- 5 column groups (Explore / Resources / Company / Legal + brand mark column). +- Section headers in `{typography.body-sm-medium}` `{colors.ink}`, link items in `{typography.body-sm}` `{colors.steel}`. + +**`footer-link`** — Individual link entry in the footer. +- Background transparent, text `{colors.steel}`, typography `{typography.body-sm}`, padding `{spacing.xxs} 0`. + +## Do's and Don'ts + +### Do +- Reserve `{colors.brand-green}` (Mintlify mint) for accent CTAs and active state indicators only — even one accent button per viewport carries weight +- Use `{colors.primary}` (black) as the dominant CTA on light backgrounds; switch to `button-on-dark` (white pill) on dark hero bands +- Apply `{rounded.full}` to every button and pill; never soften pill corners +- Pair Inter (UI prose) with Geist Mono (code) — never introduce a third typeface +- Use atmospheric gradient hero bands sparingly (only the homepage and startups page); keep deeper surfaces flat and dense +- Apply `{rounded.lg}` (12px) consistently on cards; use `{rounded.md}` (8px) only on compact UI like search pills and code blocks +- Keep documentation prose at `{typography.body-md}` (16px) with 1.50 line-height — never compress + +### Don't +- Don't use `{colors.brand-green}` on body text or large surfaces — it loses signal +- Don't introduce additional accent colors beyond mint, tag-blue, error-red, and the testimonial orange +- Don't apply heavy shadows on flat documentation cards; reserve elevation for the hero product mockup +- Don't reduce documentation line-height below 1.50 — long-form readability suffers +- Don't combine atmospheric gradients with multiple competing color accents in the same hero — the sky/dark gradient is the brand mood; let it breathe +- Don't use Inter for code or Geist Mono for prose — the typeface assignment IS the brand voice + +## Responsive Behavior + +### Breakpoints +| Name | Width | Key Changes | +|---|---|---| +| Mobile (small) | < 480px | Single column. Hero scales to 36px. Pill nav collapses to hamburger. Pricing tiers stack 1-up. Footer 1-column accordion. | +| Mobile (large) | 480 – 767px | Same as small but feature tiles render 2-up. Hero scales to 44px. | +| Tablet | 768 – 1023px | 2-column feature grids. Pill-tab nav returns. Documentation sidebar collapses to drawer. Hero scales to 56px. | +| Desktop | 1024 – 1279px | Full 3-column docs grid (sidebar / body / TOC). 3-tier pricing card row. Hero at 72px. | +| Wide Desktop | ≥ 1280px | Wider hero gutters, larger product mockup, fixed 240px sidebar. | + +### Touch Targets +- Pill buttons render at 36–40px effective height — bumps to 44px on mobile via padding override +- Circular icon buttons: 32×32px desktop → 44×44px mobile +- Form inputs render at 40px height; bumps to 44px mobile +- Sidebar nav items render at ~32px tall — bump to 44px mobile drawers + +### Collapsing Strategy +- **Promo banner** stays full-width; truncates at < 480px +- **Top nav** below 1024px collapses to hamburger; horizontal links move into drawer +- **Hero band**: 2-column hero (text + mockup) collapses to stacked at < 1024px; mockup rendered below text on mobile +- **Documentation grid**: 3-column desktop → sidebar-drawer at < 1024px → single-column at < 768px +- **Pricing comparison**: 3-column tiers → 1-column stacked at < 768px; comparison table becomes horizontal-scroll +- **Hero typography**: `{typography.hero-display}` (72px) → 56px tablet → 44px mobile-large → 36px mobile-small +- **Customer logo wall**: 6-up → 3-up at tablet → 2-up at mobile +- **Footer**: 5-column desktop → 2-column tablet → accordion at mobile + +### Image Behavior +- Hero illustrations (cloud, rocket) lazy-load with the hero band; remain crisp at all breakpoints (SVG-based) +- Product mockup retains its aspect ratio across breakpoints; scales proportionally +- Customer logos use SVG wordmarks; remain crisp on retina displays + +## Iteration Guide + +1. Focus on ONE component at a time. The system has high internal consistency. +2. Reference component names and tokens directly (`{colors.primary}`, `{component-name}-pressed`, `{rounded.full}`) — do not paraphrase. +3. Run `npx @google/design.md lint DESIGN.md` after edits to catch broken refs and contrast issues. +4. Add new variants as separate `components:` entries (`-pressed`, `-disabled`, `-focused`, `-active`). +5. Default to `{typography.body-md}` for body and `{typography.subtitle}` for emphasis. Headlines step down `hero-display → display-lg → heading-1 → heading-2 → heading-3 → heading-4 → heading-5`. +6. Keep `{colors.brand-green}` confined to accent moments. If it appears on a generic surface, ask whether it earned that role. +7. Pill-shaped buttons (`{rounded.full}`) always; squared buttons signal "third-party widget" in this language. +8. Documentation prose belongs in `{typography.body-md}` 16px with 1.50 line-height — anything denser breaks the reading experience. + +## Known Gaps + +- Specific dark-mode token values for canvas, surface, ink, and hairline are not surfaced on these pages; the brand has not yet shipped a published dark-mode palette +- Animation/transition timings are not extracted; recommend 150–200ms ease for hover/focus state transitions +- Form validation success state is not explicitly captured beyond defaults — implement following standard green-border + success badge patterns +- Code syntax highlighting palette inside docs is not formalized; documentation samples carry their own twoslash-style annotation system tokens (e.g. `{colors.brand-tag}`, `{colors.brand-annotate}`, `{colors.brand-warn}`) but the full highlight scheme is not enumerated diff --git a/README.md b/README.md index e215bc4..e2c5660 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,481 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# 个人博客 · v0.1 -## Getting Started +一个使用 Next.js + PostgreSQL 构建的个人博客,支持 Markdown / MDX 写作、代码高亮、 +多语言路由,并带有一套完整的文章 CRUD REST 接口。 -First, run the development server: +> 设计规范见 [`DESIGN.md`](./DESIGN.md),第三方库文档见 [`文档地址.md`](./文档地址.md), +> 功能范围见 [`v0.1功能文档.md`](./v0.1功能文档.md)。 + +--- + +## 快速开始 ```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev +pnpm install + +# 1. 配置数据库连接(见下方「环境变量」) +# 2. 建表 +pnpm db:migrate + +# 3. 导入示例文章(把 content/posts/ 下的 Markdown 写入数据库) +pnpm seed + +pnpm dev # 开发服务器 http://localhost:3000 ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +生产构建与本地预览: -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +```bash +pnpm build # 构建(含类型检查) +pnpm start # 启动生产服务器 +pnpm lint # ESLint +pnpm typecheck # 仅类型检查 +``` -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +> 首次访问 `/` 会 307 重定向到默认语言(`/zh`),这是 next-intl 的正常行为。 -## Learn More +--- -To learn more about Next.js, take a look at the following resources: +## 环境变量 -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +在 `.env.local` 中配置: -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +| 变量 | 必填 | 说明 | +| --- | --- | --- | +| `DATABASE_URL` | 是 | PostgreSQL 连接串。本地通常为 `postgresql://user:pass@localhost:5432/blog?sslmode=disable`;托管库(Neon / Supabase 等)用其提供的连接串(一般带 `?sslmode=require`)。 | +| `NEXT_PUBLIC_SITE_URL` | 是 | 站点公开地址,用于 `sitemap.xml`、`robots.txt`、canonical 与 Open Graph 的绝对 URL。部署后改成正式域名即可,无需改代码。 | +| `DATABASE_POOL_MAX` | 否 | 连接池上限,默认 `10`。Serverless 环境建议调小。 | +| `DATABASE_STATEMENT_TIMEOUT_MS` | 否 | 单条 SQL 超时,默认 `15000`。 | -## Deploy on Vercel +> **SSL 说明**:本地 Postgres 默认不启用 SSL,若连接串写了 `sslmode=require` +> 会报 `The server does not support SSL connections`。用 `pnpm db:diagnose` +> 可以自动判断该用哪种配置。 -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +`.env.local` 已加入 `.gitignore`,不会被提交。 -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +--- + +## 数据库 + +### 为什么用 `pg` 而不是 `@neondatabase/serverless` + +脚手架原本使用 `drizzle-orm/neon-http` + `@neondatabase/serverless`。 +该驱动走的是 **Neon 的 HTTP 代理端点**,不是 PostgreSQL 的 TCP 协议, +因此**无法连接本地或自建的普通 Postgres**(实测报 `fetch failed`)。 + +本项目改用 `pg`(node-postgres)连接池,既能连本地 Postgres, +也能连 Neon / Supabase / Railway 等任何标准 Postgres—— +部署到 Neon 时只需使用其 TCP 连接串即可,代码无需改动。 + +### 数据模型 + +单表 `posts`: + +| 列 | 类型 | 说明 | +| --- | --- | --- | +| `id` | serial | 主键 | +| `slug` | varchar(200) | URL 标识,**唯一**,格式 `^[a-z0-9]+(-[a-z0-9]+)*$` | +| `title` | text | 标题 | +| `summary` | text | 摘要(卡片与 SEO description) | +| `content` | text | Markdown / MDX 正文 | +| `tags` | text[] | 标签数组,默认空数组 | +| `status` | varchar(20) | `draft` \| `published` | +| `reading_minutes` | integer | 阅读时长,写入时计算并缓存 | +| `cover` | text | 封面图路径 | +| `description` | text | 覆盖默认 SEO 描述 | +| `published_at` | timestamptz | 发布时间;草稿为 `null` | +| `created_at` / `updated_at` | timestamptz | 时间戳 | + +**索引** + +| 索引 | 用途 | +| --- | --- | +| `posts_slug_unique_idx` | slug 唯一性 + 按 slug 查询 | +| `posts_status_published_at_idx` | 列表页主查询:按状态过滤 + 按发布时间倒序 | +| `posts_tags_gin_idx` | 标签筛选(GIN 支持 `tags @> ARRAY['x']`) | + +**约束**(写在迁移 SQL 中,Drizzle 的 schema 回调不生成 CHECK 约束) + +- `posts_status_check`:`status IN ('draft', 'published')` +- `posts_slug_format_check`:slug 必须匹配 `^[a-z0-9]+(-[a-z0-9]+)*$` + +> 约束同时存在于数据库与应用层(Zod)是有意为之:应用层给出友好的字段级报错, +> 数据库层保证任何写入路径(脚本、手工 SQL)都无法绕过。 + +### 常用命令 + +```bash +pnpm db:check # 探测连通性并列出表与行数 +pnpm db:diagnose # 诊断驱动 / SSL 配置是否正确 +pnpm db:generate # 由 schema.ts 生成迁移 SQL +pnpm db:migrate # 执行迁移 +pnpm db:verify # 校验约束与索引是否真的生效(含"坏写入"测试) +pnpm db:studio # Drizzle Studio 可视化查看数据 +pnpm seed # 把 content/posts/*.mdx 导入数据库(幂等 upsert) +``` + +--- + +## API + +所有接口遵循统一响应格式: + +```jsonc +// 成功 +{ "data": ..., "meta": { ... } } + +// 失败 +{ "error": { "code": "VALIDATION_ERROR", "message": "...", "details": [{ "field": "slug", "message": "..." }] } } +``` + +错误码:`VALIDATION_ERROR`(400) · `BAD_REQUEST`(400) · `NOT_FOUND`(404) · +`CONFLICT`(409) · `METHOD_NOT_ALLOWED`(405) · `INTERNAL_ERROR`(500) · +`SERVICE_UNAVAILABLE`(503) + +### `GET /api/posts` — 文章列表 + +| 参数 | 默认 | 说明 | +| --- | --- | --- | +| `limit` | 20 | 1–100 | +| `offset` | 0 | 偏移量 | +| `tag` | — | 按标签过滤 | +| `status` | — | `draft` \| `published` | +| `includeDrafts` | `false` | 是否包含草稿 | + +```bash +curl "http://localhost:3000/api/posts?limit=5" +curl "http://localhost:3000/api/posts?tag=Next.js" +``` + +返回 `meta: { total, limit, offset, hasMore }`。**默认只返回已发布文章。** + +### `POST /api/posts` — 新建文章 + +```bash +curl -X POST http://localhost:3000/api/posts \ + -H "content-type: application/json" \ + -d '{ + "slug": "hello-world", + "title": "你好世界", + "summary": "第一篇文章", + "content": "## 正文\n\n支持 **Markdown**。", + "tags": ["随笔"], + "status": "published" + }' +``` + +必填:`slug` / `title` / `summary` / `content`;其余可选。 +`status` 默认 `draft`。返回 **201**。 + +### `GET /api/posts/[slug]` — 文章详情 + +含正文。草稿与不存在的 slug 返回 **404**。传 `?includeDrafts=true` 可读草稿。 + +### `PATCH /api/posts/[slug]` — 更新文章 + +部分更新,只传要改的字段。也支持 `PUT`(同义)。 + +- 改 `slug` 时会校验新 slug 未被占用 +- 改 `content` 会自动重算 `reading_minutes` +- `draft → published` 自动补 `published_at`;`published → draft` 清空它 +- 空 body `{}` 返回 **400** + +### `DELETE /api/posts/[slug]` — 删除文章 + +成功返回 **204**,不存在返回 **404**。 + +### `GET /api/tags` — 标签聚合 + +返回全部已发布文章的标签及数量,按数量倒序。 + +### `GET /api/health` — 健康检查 + +返回数据库连通性与延迟;数据库不可用时返回 **503**,便于监控直接判定不健康。 + +--- + +## 项目结构 + +``` +app/ +├── layout.tsx # 根布局: / + 全局字体变量 +├── globals.css # 设计令牌(@theme)+ 基础层 + .prose-blog 排版 +├── not-found.tsx # 全局 404(无语言前缀时兜底) +├── robots.ts # 动态生成 robots.txt +├── sitemap.ts # 动态生成 sitemap.xml +├── api/ +│ ├── health/route.ts # GET 健康检查 +│ ├── posts/route.ts # GET 列表 / POST 新建 +│ ├── posts/[slug]/route.ts # GET 详情 / PATCH·PUT 更新 / DELETE 删除 +│ └── tags/route.ts # GET 标签聚合 +└── [locale]/ # 语言段:en / zh / ja + ├── layout.tsx # i18n Provider + 页头页脚外壳 + ├── page.tsx # 首页:Hero + 文章列表 + ├── error.tsx # 错误边界 + ├── not-found.tsx # 语言段内 404(带站点外壳) + ├── about/page.tsx # 关于页 + └── posts/[slug]/page.tsx # 文章详情页 + +components/ +├── ui/ # 基础组件:Button / Card / Badge / Container / Section +├── layout/ # 站点壳:SiteHeader / SiteFooter / LocaleSwitcher +├── home/hero.tsx # 首页 Hero(氛围渐变) +├── post/ # 文章卡片、目录 +├── mdx/ # MDX 元素映射:CodeBlock / Callout +└── motion/ # 动画:Reveal / MotionItem / ReadingProgress + +lib/ +├── api/response.ts # 统一响应格式、错误码、错误包装器 +├── db/ +│ ├── index.ts # pg 连接池 + 健康检查(server-only) +│ └── schema.ts # Drizzle 表定义 +├── validation/post.ts # Zod 校验契约(API 与脚本共用) +├── posts/ +│ ├── repository.ts # 仓储层:分页 / 筛选 / 排序 / 草稿过滤 +│ ├── index.ts # 页面使用的门面(server-only) +│ └── types.ts # 领域类型 +├── fonts.ts # 字体配置(next/font 自托管) +├── site-config.ts # 站点配置(单一事实来源) +├── mdx.ts # MDX 编译管线(remark + rehype + Shiki) +├── toc.ts # 从 MDX 提取目录 +└── utils.ts # cn / 日期格式化 / URL 拼接 + +content/posts/ # 文章导入源(.mdx,通过 pnpm seed 写入数据库) +drizzle/migrations/ # 数据库迁移 SQL +messages/ # i18n 文案(en / zh / ja) +scripts/ # 开发期验证脚本(见下文) +proxy.ts # 语言路由中间件(Next.js 16 新约定) +``` + +--- + +## 写作方式 + +内容已迁移到 **PostgreSQL**,有两种写入方式: + +### 方式一:调用 API(推荐) + +见上文 [API](#api) 一节的 `POST /api/posts` / `PATCH /api/posts/[slug]`。 + +### 方式二:写 Markdown 文件后导入 + +在 `content/posts/` 下新增 `.mdx` 文件(**文件名即 slug**),然后运行 +`pnpm seed` 导入数据库。该命令是**幂等 upsert**:已存在的 slug 会被更新, +不会产生重复数据。 + +```mdx +--- +title: "文章标题" +date: "2025-03-08" +summary: "一句话摘要,用于卡片与 SEO description。" +tags: ["Next.js", "性能"] +draft: false +--- + +正文使用 Markdown 编写,支持 GFM 表格、任务列表与删除线。 +``` + +### frontmatter 字段 + +| 字段 | 必填 | 类型 | 对应数据库列 | +| --- | --- | --- | --- | +| `title` | 是 | string | `title` | +| `date` | 是 | string(`YYYY-MM-DD`) | `published_at` | +| `summary` | 是 | string | `summary` | +| `tags` | 否 | string[] | `tags` | +| `draft` | 否 | boolean | `status`(`true` → `draft`) | +| `cover` | 否 | string | `cover` | +| `description` | 否 | string | `description` | + +**校验分两层**:`pnpm seed` 导入前会校验 frontmatter 并指出具体文件与字段; +数据库层还有 CHECK 约束兜底。API 写入则由 Zod 校验。 + +> `content/posts/` 现在只是**导入源**,不再是运行时内容源。 +> 页面的数据全部来自数据库。 + +### 草稿机制 + +`status = 'draft'` 的文章: + +- 不出现在首页列表与 `GET /api/posts`(除非显式传 `includeDrafts=true`) +- 不出现在 `sitemap.xml` +- 直接访问其 URL 返回**真正的 HTTP 404** +- `GET /api/posts/[slug]` 同样返回 404;需传 `?includeDrafts=true` 才能读到 + +> **实现要点**:详情页必须**不使用路由级 `loading.tsx`**。 +> 路由级 `loading.tsx` 会创建 Suspense 边界,Next.js 会立即以 **200** 状态 +> 流式输出骨架屏,之后页面内的 `notFound()` 只能替换内容、无法再改写状态码, +> 结果就是「草稿/不存在的文章返回 200、内容却是 404 页」—— +> 搜索引擎会把这些 URL 当作有效页面收录。 +> 详见 `components/post/post-skeleton.tsx` 的注释。 + +### 在正文中使用组件 + +MDX 中可直接使用内置组件(正文存在数据库里,同样会被编译): + +```mdx + + 支持 info / tip / warning / danger 四种类型。 + +``` + +--- + +## 架构要点 + +### 字体:构建期自托管 + +中文 Noto Sans SC、英文 Inter、代码 IBM Plex Mono,全部通过 `next/font/google` +在**构建期下载并自托管**(输出到 `.next/static/media`),运行时不会向 Google 发请求, +因此没有第三方请求、没有 FOUT,也符合隐私要求。 + +每个字体都显式配置了 `fallback` 回退栈与 `adjustFontFallback`: + +| 字体 | 角色 | 回退栈 | +| --- | --- | --- | +| Inter | 英文 / UI | `-apple-system, BlinkMacSystemFont, Segoe UI, Helvetica Neue, Arial, sans-serif` | +| Noto Sans SC | 中文 | `PingFang SC, Hiragino Sans GB, Microsoft YaHei, Source Han Sans SC, WenQuanYi Micro Hei, sans-serif` | +| IBM Plex Mono | 代码 | `SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, Courier New, monospace` | + +> Noto Sans SC 在 Google Fonts 上按权重分片(非可变字体),因此显式声明 4 个权重, +> 由 next/font 生成 `unicode-range` 分片,浏览器只下载实际用到的子集。 +> 因其体积较大,设置了 `preload: false`,避免抢占首屏关键资源。 + +### 设计令牌:单一事实来源 + +`DESIGN.md` 中的 `{colors.*}` / `{rounded.*}` / `{spacing.*}` 全部映射为 +`app/globals.css` 里 `@theme` 声明的 CSS 变量,因此 Tailwind 工具类 +(`bg-canvas`、`rounded-lg`、`p-xl`)与手写 CSS 读取的是**同一份变量**。 + +### 渲染策略 + +| 页面 | 渲染方式 | +| --- | --- | +| 首页 / 关于页 | SSG(`generateStaticParams` 预生成 3 种语言) | +| 文章详情页 | SSG(构建期编译 MDX + Shiki 高亮 + 提取目录) | +| sitemap / robots | 构建期静态生成 | +| `/[locale]/posts/[slug]` | `dynamicParams = false`,未预生成的 slug 返回 404 | + +文章正文与代码高亮结果都是静态 HTML,运行时零额外渲染开销。 + +### 动画的渐进增强 + +`components/motion/` 下的动画**没有**使用 motion 的 `whileInView`。 +它在服务端会输出 `opacity: 0`,只有 JS 执行且元素滚入视口后才显示—— +一旦 JS 加载失败,文章列表会**永久不可见**,搜索引擎抓到的也是空内容。 + +改为渐进增强策略:服务端输出完全可见的 HTML,客户端挂载后, +仅当元素位于视口下方时才叠加一次过渡动画。**可读性是默认状态,动画是叠加效果。** + +--- + +## 验证脚本 + +### 一次性运行全部测试 + +```bash +pnpm test # 校验单测 + 404 状态码 + API 端到端(需先 pnpm start) +``` + +或分别运行: + +```bash +pnpm test:validation # Zod 校验规则单测(无需服务器/数据库) +pnpm test:404 # 检查草稿与不存在的文章是否返回真正的 404 +pnpm test:api # API 端到端(CRUD + 分页 + 筛选 + 错误分支) +pnpm db:verify # 数据库约束与索引是否真的生效(含"坏写入"测试) +``` + +`test:api` / `test:404` 需要先启动服务器: + +```bash +pnpm start --port 3000 +node scripts/test-api.mjs http://localhost:3000 +``` + +> 这些脚本的测试数据统一使用 `zz-` 前缀,跑完会自动清理,不影响真实内容。 + +### 视觉问题的排查脚本 + +横向溢出、代码块滚动行为这类问题,单看截图难以定位到具体元素, +因此通过 Chrome DevTools Protocol 直接向浏览器查询元素的 `boundingRect`: + +```bash +# 1. 启动带调试端口的 headless Chrome +chrome --headless=new --remote-debugging-port=9222 --window-size=390,900 about:blank + +# 2. 从 http://127.0.0.1:9222/json 取 webSocketDebuggerUrl,设为环境变量 +export CDP_WS="ws://127.0.0.1:9222/devtools/page/" + +# 3. 测量指定视口下的横向溢出 +node scripts/measure-overflow.mjs http://localhost:3000/zh 390 900 + +# 4. 检查代码块是否在内部滚动而非撑破布局 +node scripts/check-code-blocks.mjs http://localhost:3000/zh/posts/ 390 + +# 5. 在精确视口下截图(含整页) +node scripts/screenshot.mjs http://localhost:3000/zh 390 844 out.png --full + +# 6. 诊断 notFound() 的状态码行为 +node scripts/diagnose-404.mjs http://localhost:3000 +``` + +> **为什么需要 `screenshot.mjs`**:`chrome --headless --screenshot --window-size=390,900` +> 的输出位图宽度与 CSS 视口宽度可能不一致,会让移动端截图看起来"内容被截断", +> 而实际测量 `document.scrollWidth` 却是正常的。用 CDP 的 +> `Emulation.setDeviceMetricsOverride` 才能得到与真实设备一致的截图。 + +--- + +## 已知限制 + +1. **API 尚无鉴权。** `POST` / `PATCH` / `DELETE` 目前任何人都能调用, + 仅适合本地开发或内网使用。上线前必须加鉴权(Session + HttpOnly Cookie 或 + API Key),并给写操作加上速率限制。 + +2. **`` 固定为 `zh-CN`。** 根布局 `app/layout.tsx` 渲染 ``, + 而语言信息要到 `app/[locale]/layout.tsx` 才能确定,因此无法直接改写该属性。 + 实际语言已通过 canonical、`og:locale`、`hreflang` 正确告知搜索引擎。 + +3. **文章内容不随语言切换。** 语言切换只影响 UI 文案,文章本身是单一版本。 + 如需多语言文章,可给 `posts` 表加 `locale` 列并调整唯一索引为 `(slug, locale)`。 + +4. **列表接口用 offset 分页。** 数据量增大后 offset 会变慢 + (需扫描并丢弃前 N 行),届时应改为基于 `published_at + id` 的游标分页, + 接口形状可以保持不变(`meta.hasMore` 已预留)。 + +5. **详情页未启用 ISR 缓存。** 为保证返回真正的 404,详情页使用 + `dynamic = "force-dynamic"`。查询走 slug 唯一索引,开销很小; + 若要进一步提速,可在数据层加 Redis 缓存。 + +6. **没有路由级 `loading.tsx`。** 为保证 `notFound()` 能正确设置 404 状态码, + 全局骨架屏被移除(原因见上文「草稿机制」)。如需加载态, + 请在页面内部用 `` 包裹具体的数据区块。 + +7. **标签目前仅作展示,没有标签聚合页。** `GET /api/tags` 与 `listTags()` 已实现, + 加标签页时可直接复用。 + +8. **无内容版本历史。** 更新会直接覆盖原记录(`updated_at` 会变化), + 没有 revision 表。如需回溯,应增加 `post_revisions` 表。 + +9. **暗色模式未实现。** `DESIGN.md` 明确说明上游品牌尚未发布暗色令牌, + 因此 `globals.css` 中固定 `color-scheme: light`,未做自动反转。 + +--- + +## 部署到 Vercel + +1. 将仓库推送到 GitHub / GitLab +2. 在 Vercel 导入项目(会自动识别 Next.js) +3. 配置环境变量: + - `DATABASE_URL`:生产数据库连接串(Neon / Supabase 等) + - `NEXT_PUBLIC_SITE_URL`:正式域名,如 `https://blog.example.com` +4. 首次部署后,在本地对生产库执行一次迁移:`pnpm db:migrate` +5. 在 Vercel 的 Domains 中添加自定义域名,HTTPS 证书自动签发 + +> **Serverless 注意**:Vercel 的每个函数实例都会建立自己的连接池。 +> 建议使用 Neon 的 pooled 连接串(`-pooler` 主机名), +> 并把 `DATABASE_POOL_MAX` 调小(如 `2`)以避免耗尽数据库连接数。 + +`pnpm build` 已包含类型检查,构建失败会直接阻断部署。 +构建过程**不需要**数据库可用(页面为动态渲染,不在构建期取数)。 diff --git a/app/[locale]/about/page.tsx b/app/[locale]/about/page.tsx new file mode 100644 index 0000000..20d763b --- /dev/null +++ b/app/[locale]/about/page.tsx @@ -0,0 +1,234 @@ +import type { Metadata } from "next"; +import { getTranslations, setRequestLocale } from "next-intl/server"; +import { + Briefcase, + MapPin, + Mail, + Code2, + Gauge, + Palette, + Wrench, +} from "lucide-react"; +import { Container, Section } from "@/components/ui/layout"; +import { Card } from "@/components/ui/card"; +import { buttonClassName } from "@/components/ui/button-variants"; +import { Reveal } from "@/components/motion/reveal"; +import { siteConfig } from "@/lib/site-config"; + +/** + * 关于页渲染策略:ISR 缓存 60 秒。 + * 路由段配置必须是字面量(Next.js 会静态分析)。 + */ +export const revalidate = 60; + +type AboutPageProps = { + params: Promise<{ locale: string }>; +}; + +export async function generateMetadata({ + params, +}: AboutPageProps): Promise { + const { locale } = await params; + const t = await getTranslations({ locale, namespace: "About" }); + + return { + title: t("title"), + description: t("description"), + alternates: { canonical: `/${locale}/about` }, + openGraph: { + type: "profile", + title: t("title"), + description: t("description"), + url: `${siteConfig.url}/${locale}/about`, + images: [{ url: siteConfig.ogImage }], + }, + }; +} + +/** 关注方向 —— 图标仅为装饰,语义由文本承载 */ +const FOCUS_AREAS = [ + { + icon: Gauge, + title: "Web 性能", + description: + "关注核心网页指标、渲染路径与打包体积,把「快」当作可度量的工程目标而不是感觉。", + }, + { + icon: Code2, + title: "开发者体验", + description: + "好的工具应该让正确的事情变简单。投入最多的地方是构建流程、类型系统与文档。", + }, + { + icon: Palette, + title: "设计系统", + description: + "把设计规范翻译成可执行的代码约束,让一致性由类型系统保证,而不是靠代码评审。", + }, + { + icon: Wrench, + title: "工程化实践", + description: + "测试策略、CI/CD、可观测性——那些看不见但决定了项目能走多远的基建。", + }, +] as const; + +export default async function AboutPage({ params }: AboutPageProps) { + const { locale } = await params; + setRequestLocale(locale); + + const t = await getTranslations("About"); + const { author } = siteConfig; + + const facts = [ + { icon: Briefcase, label: t("role"), value: author.jobTitle }, + { icon: MapPin, label: t("location"), value: author.location }, + { icon: Mail, label: t("email"), value: author.email }, + ]; + + return ( + <> + {/* 页头 */} +
+ +

+ {t("eyebrow")} +

+

+ {t("title")} +

+

+ {t("description")} +

+
+
+ +
+ +
+ {/* 主栏:简介 + 关注方向 */} +
+
+

+ {t("introTitle")} +

+ +
+

{author.bio}

+

+ 这个博客用来记录我在日常工作中遇到的问题和解决思路。 + 我倾向于写下那些「当时找不到答案、后来自己想明白了」的内容, + 因为这类经验对别人往往也最有价值。 +

+

+ 写作风格上,我更在意准确而不是完整:与其罗列所有可能性, + 不如把一条路径讲透,并说清楚它为什么成立、在什么情况下会失效。 +

+
+
+ +
+

+ {t("techTitle")} +

+

+ {t("techDescription")} +

+ +
+ {FOCUS_AREAS.map((area, index) => ( + + + + + ))} +
+
+
+ + {/* 侧栏:基本信息 + 联系方式 */} + +
+
+
+ + ); +} diff --git a/app/[locale]/error.tsx b/app/[locale]/error.tsx new file mode 100644 index 0000000..8277a2b --- /dev/null +++ b/app/[locale]/error.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { useEffect } from "react"; +import { RotateCcw, TriangleAlert } from "lucide-react"; + +/** + * 错误边界(语言段内)。 + * + * 必须是客户端组件。捕获渲染期间的运行时错误, + * 提供重试入口,并把错误上报到控制台便于排查。 + * + * 注意:`error.message` 在生产环境会被脱敏,因此面向用户只展示通用文案, + * 详细信息(digest)留给开发者排查。 + */ +export default function LocaleError({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + useEffect(() => { + console.error("[LocaleError]", error); + }, [error]); + + return ( +
+ + +

+ 出了点问题 +

+ +

+ 页面加载时发生了意外错误,请稍后重试。如果问题持续出现,请通过「关于」页的联系方式告诉我。 +

+ + {error.digest ? ( +

+ 错误编号:{error.digest} +

+ ) : null} + + +
+ ); +} diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx new file mode 100644 index 0000000..4b7bb41 --- /dev/null +++ b/app/[locale]/layout.tsx @@ -0,0 +1,117 @@ +import type { Metadata } from "next"; +import { NextIntlClientProvider } from "next-intl"; +import { + getMessages, + getTranslations, + setRequestLocale, +} from "next-intl/server"; +import { notFound } from "next/navigation"; +import { routing } from "@/i18n/routing"; +import { SiteHeader } from "@/components/layout/site-header"; +import { SiteFooter } from "@/components/layout/site-footer"; +import { siteConfig } from "@/lib/site-config"; + +/** + * 语言段布局。 + * + * 文档骨架( / )由根布局 app/layout.tsx 提供, + * 这里只负责:校验 locale、注入 i18n Provider、渲染站点壳(页头 / 页脚)。 + * + * 注意:因为根布局固定了 ,实际请求语言通过 + * `app/[locale]/layout.tsx` 无法直接改写 ;如需精确的 lang 属性, + * 参见 README 的「已知限制」一节。 + */ + +/** 预生成所有语言的路由 */ +export function generateStaticParams() { + return routing.locales.map((locale) => ({ locale })); +} + +type Locale = (typeof routing.locales)[number]; + +type LocaleLayoutProps = { + children: React.ReactNode; + params: Promise<{ locale: string }>; +}; + +/** 校验 locale 合法性,非法则交给 404 */ +function assertLocale(locale: string): asserts locale is Locale { + if (!routing.locales.includes(locale as Locale)) { + notFound(); + } +} + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }>; +}): Promise { + const { locale } = await params; + assertLocale(locale); + + const t = await getTranslations({ locale, namespace: "Site" }); + + const title = `${siteConfig.name} · ${t("tagline")}`; + + return { + title: { + default: title, + template: `%s · ${siteConfig.name}`, + }, + description: siteConfig.description, + alternates: { + canonical: `/${locale}`, + languages: Object.fromEntries( + routing.locales.map((item) => [item, `/${item}`]), + ), + }, + openGraph: { + type: "website", + siteName: siteConfig.name, + locale, + url: `${siteConfig.url}/${locale}`, + title, + description: siteConfig.description, + images: [{ url: siteConfig.ogImage }], + }, + twitter: { + card: "summary_large_image", + title, + description: siteConfig.description, + images: [siteConfig.ogImage], + }, + }; +} + +export default async function LocaleLayout({ + children, + params, +}: LocaleLayoutProps) { + const { locale } = await params; + assertLocale(locale); + + // 启用静态渲染(next-intl 要求显式声明当前请求的 locale) + setRequestLocale(locale); + + const messages = await getMessages(); + const t = await getTranslations({ locale, namespace: "Nav" }); + + return ( + + + {t("skipToContent")} + + +
+ +
+ {children} +
+ +
+
+ ); +} diff --git a/app/[locale]/not-found.tsx b/app/[locale]/not-found.tsx new file mode 100644 index 0000000..bf37ffe --- /dev/null +++ b/app/[locale]/not-found.tsx @@ -0,0 +1,86 @@ +import { Link } from "@/i18n/navigation"; +import { getTranslations } from "next-intl/server"; +import { ArrowLeft, Compass } from "lucide-react"; +import { Container, Section } from "@/components/ui/layout"; +import { buttonClassName } from "@/components/ui/button-variants"; +import { getAllPosts } from "@/lib/posts"; + +/** + * 404 页面(语言段内)。 + * + * 位于 `app/[locale]/not-found.tsx`,因此能访问 next-intl 上下文并使用 + * 站点的页头 / 页脚布局。同时列出最新文章作为"继续浏览"的出口, + * 避免用户走进死胡同。 + */ +export default async function NotFound() { + const t = await getTranslations("NotFound"); + const posts = (await getAllPosts()).slice(0, 3); + + return ( +
+ + + +

+ {t("eyebrow")} +

+ +

+ {t("title")} +

+ +

+ {t("description")} +

+ +
+ +
+ + {posts.length > 0 ? ( +
+

+ 也许你想看看这些 +

+ +
    + {posts.map((post) => ( +
  • + + + {post.title} + + + {post.summary} + + +
  • + ))} +
+
+ ) : null} +
+
+ ); +} diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx new file mode 100644 index 0000000..8790ee8 --- /dev/null +++ b/app/[locale]/page.tsx @@ -0,0 +1,135 @@ +import type { Metadata } from "next"; +import { Link } from "@/i18n/navigation"; +import { getTranslations, setRequestLocale } from "next-intl/server"; +import { ArrowRight } from "lucide-react"; +import { Container, Section } from "@/components/ui/layout"; +import { SectionHeading } from "@/components/ui/section-heading"; +import { buttonClassName } from "@/components/ui/button-variants"; +import { PostCard } from "@/components/post/post-card"; +import { MotionItem } from "@/components/motion/motion-item"; +import { HomeHero } from "@/components/home/hero"; +import { getAllPosts, getAllTags } from "@/lib/posts"; +import { siteConfig } from "@/lib/site-config"; + +/** + * 首页渲染策略:ISR 缓存 60 秒。 + * + * 内容来自数据库,因此不在构建期固化;访问时生成并缓存, + * 更新文章后最多 60 秒自动生效。 + * + * 注意:Next.js 会静态分析路由段配置,这里必须写成**字面量**, + * 不能引用从其他模块导入的常量,否则会报 + * "Invalid segment configuration export detected"。 + */ +export const revalidate = 60; + +type HomePageProps = { + params: Promise<{ locale: string }>; +}; + +export async function generateMetadata({ + params, +}: HomePageProps): Promise { + const { locale } = await params; + const t = await getTranslations({ locale, namespace: "Site" }); + + return { + title: `${siteConfig.name} · ${t("tagline")}`, + description: siteConfig.description, + alternates: { canonical: `/${locale}` }, + }; +} + +export default async function HomePage({ params }: HomePageProps) { + const { locale } = await params; + setRequestLocale(locale); + + const t = await getTranslations("Home"); + + // Hero 需要的统计数据量很小,直接在此等待; + // 文章列表区块用 Suspense 包裹,先出骨架再填充内容。 + const [posts, tags] = await Promise.all([getAllPosts(), getAllTags()]); + const earliestPost = posts.at(-1); + + return ( + <> + + +
+ + {t("latestTitle")}} + description={t("latestDescription")} + className="mb-xxl" + /> + + {posts.length === 0 ? ( + + ) : ( +
+ {posts.map((post, index) => ( + + + + ))} +
+ )} + +
+ + {t("ctaSecondary")} +
+
+
+ + ); +} + +/** 无文章时的空状态(skill 要求前端具备加载态 / 空态 / 错误态) */ +function EmptyState() { + return ( +
+

还没有已发布的文章

+ +

+ 调用{" "} + + POST /api/posts + {" "} + 创建文章(或运行{" "} + + pnpm seed + {" "} + 导入示例内容),状态为{" "} + + published + {" "} + 的文章就会出现在这里。 +

+
+ ); +} diff --git a/app/[locale]/posts/[slug]/page.tsx b/app/[locale]/posts/[slug]/page.tsx new file mode 100644 index 0000000..149ee79 --- /dev/null +++ b/app/[locale]/posts/[slug]/page.tsx @@ -0,0 +1,250 @@ +import type { Metadata } from "next"; +import { notFound } from "next/navigation"; +import { Link } from "@/i18n/navigation"; +import { getLocale, getTranslations, setRequestLocale } from "next-intl/server"; +import { ArrowLeft, ArrowRight, CalendarDays, Clock } from "lucide-react"; +import { routing } from "@/i18n/routing"; +import { Container, Section } from "@/components/ui/layout"; +import { Badge } from "@/components/ui/badge"; +import { ReadingProgress } from "@/components/motion/reading-progress"; +import { TableOfContents } from "@/components/post/table-of-contents"; +import { getAdjacentPosts, getPostBySlug } from "@/lib/posts"; +import { compilePostMdx } from "@/lib/mdx"; +import { extractToc } from "@/lib/toc"; +import { formatDate, toDateTimeAttribute } from "@/lib/utils"; +import { siteConfig } from "@/lib/site-config"; + +/** + * 文章详情页。 + * + * 数据流:从数据库读取正文 → 编译 MDX(Shiki 语法高亮) + * → 提取 TOC → 渲染。 + * + * 整页为 Server Component:正文与高亮结果都是服务端渲染的 HTML, + * 只有阅读进度条与目录高亮这两个交互点进入客户端 bundle。 + */ + +type PostPageProps = { + params: Promise<{ locale: string; slug: string }>; +}; + +/** + * 渲染策略:按需动态渲染,不做 ISR 缓存。 + * + * ## 为什么这里不能用 `revalidate` + * + * 详情页需要根据文章是否存在返回**真正的 HTTP 404**。但一旦启用 ISR, + * Next.js 会把该路由视为可静态生成:首次请求时先以 200 开始渲染, + * 之后 `notFound()` 只替换页面内容,**状态码已经发出去了**, + * 结果就是「不存在/草稿的文章 → 内容显示 404、状态码却是 200」。 + * + * 这会让搜索引擎把不存在的 URL 当成有效页面收录,属于 SEO 缺陷。 + * 因此详情页保持动态渲染(内容本身仍很快,SQL 走 slug 唯一索引)。 + * + * 首页、关于页、sitemap 不存在这个问题,仍使用 ISR 缓存。 + */ +export const dynamic = "force-dynamic"; + +export async function generateMetadata({ + params, +}: PostPageProps): Promise { + const { locale, slug } = await params; + const post = await getPostBySlug(slug); + + if (!post) { + return { title: "文章不存在" }; + } + + const description = post.description ?? post.summary; + const url = `${siteConfig.url}/${locale}/posts/${post.slug}`; + + return { + title: post.title, + description, + keywords: post.tags, + authors: [{ name: siteConfig.author.name }], + alternates: { + canonical: `/${locale}/posts/${post.slug}`, + languages: Object.fromEntries( + routing.locales.map((item) => [item, `/${item}/posts/${post.slug}`]), + ), + }, + openGraph: { + type: "article", + url, + title: post.title, + description, + publishedTime: new Date(`${post.date}T00:00:00Z`).toISOString(), + authors: [siteConfig.author.name], + tags: post.tags, + images: [{ url: post.cover ?? siteConfig.ogImage }], + }, + twitter: { + card: "summary_large_image", + title: post.title, + description, + images: [post.cover ?? siteConfig.ogImage], + }, + }; +} + +export default async function PostPage({ params }: PostPageProps) { + const { locale, slug } = await params; + setRequestLocale(locale); + + const post = await getPostBySlug(slug); + + // 草稿与不存在的 slug 一律 404 + if (!post) { + notFound(); + } + + const t = await getTranslations("Post"); + const currentLocale = await getLocale(); + + const { content } = await compilePostMdx(post.content); + const toc = extractToc(post.content); + const { previous, next } = await getAdjacentPosts(post.slug); + + // 结构化数据,帮助搜索引擎理解文章内容 + const jsonLd = { + "@context": "https://schema.org", + "@type": "BlogPosting", + headline: post.title, + description: post.description ?? post.summary, + datePublished: new Date(`${post.date}T00:00:00Z`).toISOString(), + author: { + "@type": "Person", + name: siteConfig.author.name, + }, + keywords: post.tags?.join(", "), + inLanguage: locale, + mainEntityOfPage: { + "@type": "WebPage", + "@id": `${siteConfig.url}/${locale}/posts/${post.slug}`, + }, + }; + + return ( + <> + + +