skills: fold agent-skills into the monorepo, single install path, promote ms-unslop (plan phase D) #1319
@@ -6,13 +6,13 @@ Complete agent skill fleet for Mosaic Stack. 95 skills across 12 domains — cod
|
||||
|
||||
All skills were reviewed on 2026-02-16. Findings:
|
||||
|
||||
| ID | Severity | Skill | Issue | Action |
|
||||
|----|----------|-------|-------|--------|
|
||||
| C-001 | **CRITICAL** | `vercel-deploy` | Uploads entire project to external endpoint via `curl` | **REMOVED** |
|
||||
| C-002 | **ANNOTATED** | `docx`, `pptx`, `xlsx` | LD_PRELOAD shim compiles C at runtime to hook `socket()` | Security warnings added — legitimate sandbox workaround, should never activate on Docker Swarm |
|
||||
| W-001 | WARNING | `using-superpowers` | Forces aggressive auto-loading via `<EXTREMELY-IMPORTANT>` tags | Awareness only — review before enabling |
|
||||
| W-002 | WARNING | `mcp-builder` | Can connect to arbitrary MCP servers | Awareness only — review server URLs |
|
||||
| W-003 | WARNING | `create-agent` | Uses `Function()` constructor (eval equivalent) | Awareness only — review generated code |
|
||||
| ID | Severity | Skill | Issue | Action |
|
||||
| ----- | ------------- | ---------------------- | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
||||
| C-001 | **CRITICAL** | `vercel-deploy` | Uploads entire project to external endpoint via `curl` | **REMOVED** |
|
||||
| C-002 | **ANNOTATED** | `docx`, `pptx`, `xlsx` | LD_PRELOAD shim compiles C at runtime to hook `socket()` | Security warnings added — legitimate sandbox workaround, should never activate on Docker Swarm |
|
||||
| W-001 | WARNING | `using-superpowers` | Forces aggressive auto-loading via `<EXTREMELY-IMPORTANT>` tags | Awareness only — review before enabling |
|
||||
| W-002 | WARNING | `mcp-builder` | Can connect to arbitrary MCP servers | Awareness only — review server URLs |
|
||||
| W-003 | WARNING | `create-agent` | Uses `Function()` constructor (eval equivalent) | Awareness only — review generated code |
|
||||
|
||||
88 of 93 audited skills passed all checks as clean instruction-only SKILL.md files.
|
||||
|
||||
@@ -20,179 +20,179 @@ All skills were reviewed on 2026-02-16. Findings:
|
||||
|
||||
### Code Quality & Review (6)
|
||||
|
||||
| Skill | Purpose | Origin |
|
||||
|-------|---------|--------|
|
||||
| `lint` | Zero-tolerance linting — detect linter, fix ALL violations, never disable rules | Mosaic Stack |
|
||||
| `pr-reviewer` | Structured PR code review workflow (Gitea/GitHub) | Adapted from SpillwaveSolutions |
|
||||
| `code-review-excellence` | Code review methodology and checklists | awesome-skills |
|
||||
| `verification-before-completion` | Evidence-based completion claims | obra/superpowers |
|
||||
| `receiving-code-review` | How to receive and respond to code reviews | obra/superpowers |
|
||||
| `requesting-code-review` | How to request effective code reviews | obra/superpowers |
|
||||
| Skill | Purpose | Origin |
|
||||
| -------------------------------- | ------------------------------------------------------------------------------- | ------------------------------- |
|
||||
| `lint` | Zero-tolerance linting — detect linter, fix ALL violations, never disable rules | Mosaic Stack |
|
||||
| `pr-reviewer` | Structured PR code review workflow (Gitea/GitHub) | Adapted from SpillwaveSolutions |
|
||||
| `code-review-excellence` | Code review methodology and checklists | awesome-skills |
|
||||
| `verification-before-completion` | Evidence-based completion claims | obra/superpowers |
|
||||
| `receiving-code-review` | How to receive and respond to code reviews | obra/superpowers |
|
||||
| `requesting-code-review` | How to request effective code reviews | obra/superpowers |
|
||||
|
||||
### Frontend & UI (8)
|
||||
|
||||
| Skill | Purpose | Origin |
|
||||
|-------|---------|--------|
|
||||
| `next-best-practices` | Next.js 15+ — RSC, async, self-hosting, data patterns | vercel-labs/next-skills |
|
||||
| `vercel-react-best-practices` | React/Next.js performance (57 rules) | vercel-labs |
|
||||
| `vercel-composition-patterns` | React composition and component patterns | vercel-labs |
|
||||
| `vercel-react-native-skills` | React Native development patterns | vercel-labs |
|
||||
| `shadcn-ui` | Component patterns — forms, dialogs, tables, charts | developer-kit |
|
||||
| `tailwind-design-system` | Tailwind CSS v4 design system patterns | wshobson |
|
||||
| `ui-animation` | Motion design — performance, accessibility, easing | mblode |
|
||||
| `web-design-guidelines` | Web design principles and guidelines | vercel-labs |
|
||||
| Skill | Purpose | Origin |
|
||||
| ----------------------------- | ----------------------------------------------------- | ----------------------- |
|
||||
| `next-best-practices` | Next.js 15+ — RSC, async, self-hosting, data patterns | vercel-labs/next-skills |
|
||||
| `vercel-react-best-practices` | React/Next.js performance (57 rules) | vercel-labs |
|
||||
| `vercel-composition-patterns` | React composition and component patterns | vercel-labs |
|
||||
| `vercel-react-native-skills` | React Native development patterns | vercel-labs |
|
||||
| `shadcn-ui` | Component patterns — forms, dialogs, tables, charts | developer-kit |
|
||||
| `tailwind-design-system` | Tailwind CSS v4 design system patterns | wshobson |
|
||||
| `ui-animation` | Motion design — performance, accessibility, easing | mblode |
|
||||
| `web-design-guidelines` | Web design principles and guidelines | vercel-labs |
|
||||
|
||||
### Backend & API (4)
|
||||
|
||||
| Skill | Purpose | Origin |
|
||||
|-------|---------|--------|
|
||||
| `nestjs-best-practices` | NestJS — 40 rules, 10 categories, priority-ranked | kadajett |
|
||||
| `fastapi` | FastAPI + Pydantic v2 + async SQLAlchemy 2.0 | jezweb |
|
||||
| `architecture-patterns` | Clean Architecture, Hexagonal, DDD | wshobson |
|
||||
| `python-performance-optimization` | Profiling, memory, parallelization | wshobson |
|
||||
| Skill | Purpose | Origin |
|
||||
| --------------------------------- | ------------------------------------------------- | -------- |
|
||||
| `nestjs-best-practices` | NestJS — 40 rules, 10 categories, priority-ranked | kadajett |
|
||||
| `fastapi` | FastAPI + Pydantic v2 + async SQLAlchemy 2.0 | jezweb |
|
||||
| `architecture-patterns` | Clean Architecture, Hexagonal, DDD | wshobson |
|
||||
| `python-performance-optimization` | Profiling, memory, parallelization | wshobson |
|
||||
|
||||
### Authentication (5)
|
||||
|
||||
| Skill | Purpose | Origin |
|
||||
|-------|---------|--------|
|
||||
| `better-auth-best-practices` | Better-Auth — Drizzle, sessions, plugins, security | better-auth |
|
||||
| `create-auth-skill` | Creating custom Better-Auth skills | better-auth |
|
||||
| `email-and-password-best-practices` | Email/password auth patterns | better-auth |
|
||||
| `organization-best-practices` | Multi-org/team auth patterns | better-auth |
|
||||
| `two-factor-authentication-best-practices` | 2FA implementation patterns | better-auth |
|
||||
| Skill | Purpose | Origin |
|
||||
| ------------------------------------------ | -------------------------------------------------- | ----------- |
|
||||
| `better-auth-best-practices` | Better-Auth — Drizzle, sessions, plugins, security | better-auth |
|
||||
| `create-auth-skill` | Creating custom Better-Auth skills | better-auth |
|
||||
| `email-and-password-best-practices` | Email/password auth patterns | better-auth |
|
||||
| `organization-best-practices` | Multi-org/team auth patterns | better-auth |
|
||||
| `two-factor-authentication-best-practices` | 2FA implementation patterns | better-auth |
|
||||
|
||||
### AI & Agent Building (7)
|
||||
|
||||
| Skill | Purpose | Origin |
|
||||
|-------|---------|--------|
|
||||
| `ai-sdk` | Vercel AI SDK — streaming, multi-provider, agents | vercel/ai |
|
||||
| `create-agent` | Modular agent with OpenRouter multi-model access | openrouterteam |
|
||||
| `proactive-agent` | WAL Protocol, compaction recovery, self-improvement | halthelobster |
|
||||
| `dispatching-parallel-agents` | Launching and managing parallel subagents | obra/superpowers |
|
||||
| `subagent-driven-development` | Development workflow using subagents | obra/superpowers |
|
||||
| `executing-plans` | Executing multi-step implementation plans | obra/superpowers |
|
||||
| `using-superpowers` | Overview of the superpowers skill system | obra/superpowers |
|
||||
| Skill | Purpose | Origin |
|
||||
| ----------------------------- | --------------------------------------------------- | ---------------- |
|
||||
| `ai-sdk` | Vercel AI SDK — streaming, multi-provider, agents | vercel/ai |
|
||||
| `create-agent` | Modular agent with OpenRouter multi-model access | openrouterteam |
|
||||
| `proactive-agent` | WAL Protocol, compaction recovery, self-improvement | halthelobster |
|
||||
| `dispatching-parallel-agents` | Launching and managing parallel subagents | obra/superpowers |
|
||||
| `subagent-driven-development` | Development workflow using subagents | obra/superpowers |
|
||||
| `executing-plans` | Executing multi-step implementation plans | obra/superpowers |
|
||||
| `using-superpowers` | Overview of the superpowers skill system | obra/superpowers |
|
||||
|
||||
### Development Workflow (6)
|
||||
|
||||
| Skill | Purpose | Origin |
|
||||
|-------|---------|--------|
|
||||
| `test-driven-development` | TDD Red-Green-Refactor discipline | obra/superpowers |
|
||||
| `systematic-debugging` | Structured debugging methodology | obra/superpowers |
|
||||
| `using-git-worktrees` | Git worktree patterns for parallel work | obra/superpowers |
|
||||
| `finishing-a-development-branch` | Branch cleanup, squash, merge patterns | obra/superpowers |
|
||||
| `writing-plans` | Writing effective implementation plans | obra/superpowers |
|
||||
| `brainstorming` | Structured brainstorming methodology | obra/superpowers |
|
||||
| Skill | Purpose | Origin |
|
||||
| -------------------------------- | --------------------------------------- | ---------------- |
|
||||
| `test-driven-development` | TDD Red-Green-Refactor discipline | obra/superpowers |
|
||||
| `systematic-debugging` | Structured debugging methodology | obra/superpowers |
|
||||
| `using-git-worktrees` | Git worktree patterns for parallel work | obra/superpowers |
|
||||
| `finishing-a-development-branch` | Branch cleanup, squash, merge patterns | obra/superpowers |
|
||||
| `writing-plans` | Writing effective implementation plans | obra/superpowers |
|
||||
| `brainstorming` | Structured brainstorming methodology | obra/superpowers |
|
||||
|
||||
### Document Generation (6)
|
||||
|
||||
| Skill | Purpose | Origin |
|
||||
|-------|---------|--------|
|
||||
| `pdf` | PDF document generation | anthropics |
|
||||
| `docx` | Word document generation | anthropics |
|
||||
| `pptx` | PowerPoint presentation generation | anthropics |
|
||||
| `xlsx` | Excel spreadsheet generation | anthropics |
|
||||
| `doc-coauthoring` | Collaborative document writing | anthropics |
|
||||
| `internal-comms` | Internal communications drafting | anthropics |
|
||||
| Skill | Purpose | Origin |
|
||||
| ----------------- | ---------------------------------- | ---------- |
|
||||
| `pdf` | PDF document generation | anthropics |
|
||||
| `docx` | Word document generation | anthropics |
|
||||
| `pptx` | PowerPoint presentation generation | anthropics |
|
||||
| `xlsx` | Excel spreadsheet generation | anthropics |
|
||||
| `doc-coauthoring` | Collaborative document writing | anthropics |
|
||||
| `internal-comms` | Internal communications drafting | anthropics |
|
||||
|
||||
### Design & Creative (7)
|
||||
|
||||
| Skill | Purpose | Origin |
|
||||
|-------|---------|--------|
|
||||
| `brand-guidelines` | Brand identity enforcement | anthropics |
|
||||
| `frontend-design` | Frontend design patterns and principles | anthropics |
|
||||
| `canvas-design` | Canvas/visual design patterns | anthropics |
|
||||
| `algorithmic-art` | Generative/algorithmic art creation | anthropics |
|
||||
| `theme-factory` | Theme generation and customization | anthropics |
|
||||
| `slack-gif-creator` | Animated GIF creation for Slack | anthropics |
|
||||
| `web-artifacts-builder` | Self-contained HTML artifact building | anthropics |
|
||||
| Skill | Purpose | Origin |
|
||||
| ----------------------- | --------------------------------------- | ---------- |
|
||||
| `brand-guidelines` | Brand identity enforcement | anthropics |
|
||||
| `frontend-design` | Frontend design patterns and principles | anthropics |
|
||||
| `canvas-design` | Canvas/visual design patterns | anthropics |
|
||||
| `algorithmic-art` | Generative/algorithmic art creation | anthropics |
|
||||
| `theme-factory` | Theme generation and customization | anthropics |
|
||||
| `slack-gif-creator` | Animated GIF creation for Slack | anthropics |
|
||||
| `web-artifacts-builder` | Self-contained HTML artifact building | anthropics |
|
||||
|
||||
### Marketing & Business (25)
|
||||
|
||||
| Skill | Purpose | Origin |
|
||||
|-------|---------|--------|
|
||||
| `marketing-ideas` | 139 ideas across 14 categories | coreyhaines31 |
|
||||
| `pricing-strategy` | SaaS pricing — value metrics, tiers, research | coreyhaines31 |
|
||||
| `programmatic-seo` | SEO at scale — templates, playbooks | coreyhaines31 |
|
||||
| `competitor-alternatives` | Competitor comparison pages | coreyhaines31 |
|
||||
| `referral-program` | Referral & affiliate programs | coreyhaines31 |
|
||||
| `seo-audit` | Comprehensive SEO audit methodology | coreyhaines31 |
|
||||
| `copywriting` | Marketing copywriting patterns | coreyhaines31 |
|
||||
| `copy-editing` | Copy editing and proofreading | coreyhaines31 |
|
||||
| `content-strategy` | Content strategy and planning | coreyhaines31 |
|
||||
| `social-content` | Social media content creation | coreyhaines31 |
|
||||
| `email-sequence` | Email sequence design and automation | coreyhaines31 |
|
||||
| `launch-strategy` | Product launch planning | coreyhaines31 |
|
||||
| `marketing-psychology` | Psychology-driven marketing | coreyhaines31 |
|
||||
| `product-marketing-context` | Product marketing positioning | coreyhaines31 |
|
||||
| `paid-ads` | Paid advertising campaigns | coreyhaines31 |
|
||||
| `schema-markup` | Schema.org structured data | coreyhaines31 |
|
||||
| `analytics-tracking` | Analytics setup and tracking | coreyhaines31 |
|
||||
| `ab-test-setup` | A/B testing methodology | coreyhaines31 |
|
||||
| `page-cro` | Landing page conversion optimization | coreyhaines31 |
|
||||
| `form-cro` | Form conversion optimization | coreyhaines31 |
|
||||
| `signup-flow-cro` | Signup flow conversion optimization | coreyhaines31 |
|
||||
| `onboarding-cro` | User onboarding optimization | coreyhaines31 |
|
||||
| `popup-cro` | Popup/modal conversion optimization | coreyhaines31 |
|
||||
| `paywall-upgrade-cro` | Paywall/upgrade conversion optimization | coreyhaines31 |
|
||||
| `free-tool-strategy` | Free tool as marketing strategy | coreyhaines31 |
|
||||
| Skill | Purpose | Origin |
|
||||
| --------------------------- | --------------------------------------------- | ------------- |
|
||||
| `marketing-ideas` | 139 ideas across 14 categories | coreyhaines31 |
|
||||
| `pricing-strategy` | SaaS pricing — value metrics, tiers, research | coreyhaines31 |
|
||||
| `programmatic-seo` | SEO at scale — templates, playbooks | coreyhaines31 |
|
||||
| `competitor-alternatives` | Competitor comparison pages | coreyhaines31 |
|
||||
| `referral-program` | Referral & affiliate programs | coreyhaines31 |
|
||||
| `seo-audit` | Comprehensive SEO audit methodology | coreyhaines31 |
|
||||
| `copywriting` | Marketing copywriting patterns | coreyhaines31 |
|
||||
| `copy-editing` | Copy editing and proofreading | coreyhaines31 |
|
||||
| `content-strategy` | Content strategy and planning | coreyhaines31 |
|
||||
| `social-content` | Social media content creation | coreyhaines31 |
|
||||
| `email-sequence` | Email sequence design and automation | coreyhaines31 |
|
||||
| `launch-strategy` | Product launch planning | coreyhaines31 |
|
||||
| `marketing-psychology` | Psychology-driven marketing | coreyhaines31 |
|
||||
| `product-marketing-context` | Product marketing positioning | coreyhaines31 |
|
||||
| `paid-ads` | Paid advertising campaigns | coreyhaines31 |
|
||||
| `schema-markup` | Schema.org structured data | coreyhaines31 |
|
||||
| `analytics-tracking` | Analytics setup and tracking | coreyhaines31 |
|
||||
| `ab-test-setup` | A/B testing methodology | coreyhaines31 |
|
||||
| `page-cro` | Landing page conversion optimization | coreyhaines31 |
|
||||
| `form-cro` | Form conversion optimization | coreyhaines31 |
|
||||
| `signup-flow-cro` | Signup flow conversion optimization | coreyhaines31 |
|
||||
| `onboarding-cro` | User onboarding optimization | coreyhaines31 |
|
||||
| `popup-cro` | Popup/modal conversion optimization | coreyhaines31 |
|
||||
| `paywall-upgrade-cro` | Paywall/upgrade conversion optimization | coreyhaines31 |
|
||||
| `free-tool-strategy` | Free tool as marketing strategy | coreyhaines31 |
|
||||
|
||||
### Vue/Vite Ecosystem (16)
|
||||
|
||||
| Skill | Purpose | Origin |
|
||||
|-------|---------|--------|
|
||||
| `vue` | Vue.js development patterns | antfu |
|
||||
| `vue-best-practices` | Vue.js best practices and conventions | antfu |
|
||||
| `vue-router-best-practices` | Vue Router patterns and guards | antfu |
|
||||
| `vue-testing-best-practices` | Vue component testing patterns | antfu |
|
||||
| `vueuse-functions` | VueUse composable function patterns | antfu |
|
||||
| `nuxt` | Nuxt.js framework patterns | antfu |
|
||||
| `vite` | Vite build tool configuration and plugins | antfu |
|
||||
| `vitest` | Vitest testing framework patterns | antfu |
|
||||
| `vitepress` | VitePress documentation site patterns | antfu |
|
||||
| `slidev` | Slidev presentation framework | antfu |
|
||||
| `pnpm` | pnpm package manager patterns | antfu |
|
||||
| `turborepo` | Turborepo monorepo patterns | antfu |
|
||||
| `unocss` | UnoCSS atomic CSS engine | antfu |
|
||||
| `tsdown` | tsdown TypeScript bundler | antfu |
|
||||
| `pinia` | Pinia state management | antfu |
|
||||
| `antfu` | Anthony Fu's coding conventions | antfu |
|
||||
| Skill | Purpose | Origin |
|
||||
| ---------------------------- | ----------------------------------------- | ------ |
|
||||
| `vue` | Vue.js development patterns | antfu |
|
||||
| `vue-best-practices` | Vue.js best practices and conventions | antfu |
|
||||
| `vue-router-best-practices` | Vue Router patterns and guards | antfu |
|
||||
| `vue-testing-best-practices` | Vue component testing patterns | antfu |
|
||||
| `vueuse-functions` | VueUse composable function patterns | antfu |
|
||||
| `nuxt` | Nuxt.js framework patterns | antfu |
|
||||
| `vite` | Vite build tool configuration and plugins | antfu |
|
||||
| `vitest` | Vitest testing framework patterns | antfu |
|
||||
| `vitepress` | VitePress documentation site patterns | antfu |
|
||||
| `slidev` | Slidev presentation framework | antfu |
|
||||
| `pnpm` | pnpm package manager patterns | antfu |
|
||||
| `turborepo` | Turborepo monorepo patterns | antfu |
|
||||
| `unocss` | UnoCSS atomic CSS engine | antfu |
|
||||
| `tsdown` | tsdown TypeScript bundler | antfu |
|
||||
| `pinia` | Pinia state management | antfu |
|
||||
| `antfu` | Anthony Fu's coding conventions | antfu |
|
||||
|
||||
### Orchestration (1)
|
||||
|
||||
| Skill | Purpose | Origin |
|
||||
|-------|---------|--------|
|
||||
| Skill | Purpose | Origin |
|
||||
| ----------- | ------------------------------------------------------------------------------------------ | ------------ |
|
||||
| `kickstart` | Launch orchestrator for milestone/issue/task — auto-discovers context, bootstraps tracking | Mosaic Stack |
|
||||
|
||||
### Meta / Skill Authoring (4)
|
||||
|
||||
| Skill | Purpose | Origin |
|
||||
|-------|---------|--------|
|
||||
| `writing-skills` | TDD-based skill authoring methodology | obra/superpowers |
|
||||
| `skill-creator` | Anthropic's skill creation guide | anthropics |
|
||||
| `mcp-builder` | Building MCP (Model Context Protocol) servers | anthropics |
|
||||
| `webapp-testing` | Web application testing patterns | anthropics |
|
||||
| Skill | Purpose | Origin |
|
||||
| ---------------- | --------------------------------------------- | ---------------- |
|
||||
| `writing-skills` | TDD-based skill authoring methodology | obra/superpowers |
|
||||
| `skill-creator` | Anthropic's skill creation guide | anthropics |
|
||||
| `mcp-builder` | Building MCP (Model Context Protocol) servers | anthropics |
|
||||
| `webapp-testing` | Web application testing patterns | anthropics |
|
||||
|
||||
## Source Repositories
|
||||
|
||||
| Repository | Skills | Domain Focus |
|
||||
|-----------|--------|-------------|
|
||||
| [anthropics/skills](https://github.com/anthropics/skills) | 16 | Documents, design, MCP, testing |
|
||||
| [obra/superpowers](https://github.com/obra/superpowers) | 14 | Agent workflows, TDD, code review, planning |
|
||||
| [coreyhaines31/marketingskills](https://github.com/coreyhaines31/marketingskills) | 25 | Marketing, CRO, SEO, growth |
|
||||
| [antfu/skills](https://github.com/antfu/skills) | 16 | Vue, Vite, Vitest, pnpm, Nuxt |
|
||||
| [better-auth/skills](https://github.com/better-auth/skills) | 5 | Authentication patterns |
|
||||
| [vercel-labs/agent-skills](https://github.com/vercel-labs/agent-skills) | 4 | React, design |
|
||||
| [vercel-labs/next-skills](https://github.com/vercel-labs/next-skills) | 1 | Next.js 15+ |
|
||||
| [vercel/ai](https://github.com/vercel/ai) | 1 | AI SDK |
|
||||
| [halthelobster/proactive-agent](https://github.com/halthelobster/proactive-agent) | 1 | Agent architecture |
|
||||
| [openrouterteam/agent-skills](https://github.com/openrouterteam/agent-skills) | 1 | Agent building |
|
||||
| [kadajett/agent-nestjs-skills](https://github.com/kadajett/agent-nestjs-skills) | 1 | NestJS |
|
||||
| [jezweb/claude-skills](https://github.com/jezweb/claude-skills) | 1 | FastAPI |
|
||||
| [wshobson/agents](https://github.com/wshobson/agents) | 3 | Architecture, Python, Tailwind |
|
||||
| [mblode/agent-skills](https://github.com/mblode/agent-skills) | 1 | UI animation |
|
||||
| [giuseppe-trisciuoglio/developer-kit](https://github.com/giuseppe-trisciuoglio/developer-kit) | 1 | shadcn/ui |
|
||||
| Mosaic Stack (original) | 4 | PR review, code review, orchestration, linting |
|
||||
| Repository | Skills | Domain Focus |
|
||||
| --------------------------------------------------------------------------------------------- | ------ | ---------------------------------------------- |
|
||||
| [anthropics/skills](https://github.com/anthropics/skills) | 16 | Documents, design, MCP, testing |
|
||||
| [obra/superpowers](https://github.com/obra/superpowers) | 14 | Agent workflows, TDD, code review, planning |
|
||||
| [coreyhaines31/marketingskills](https://github.com/coreyhaines31/marketingskills) | 25 | Marketing, CRO, SEO, growth |
|
||||
| [antfu/skills](https://github.com/antfu/skills) | 16 | Vue, Vite, Vitest, pnpm, Nuxt |
|
||||
| [better-auth/skills](https://github.com/better-auth/skills) | 5 | Authentication patterns |
|
||||
| [vercel-labs/agent-skills](https://github.com/vercel-labs/agent-skills) | 4 | React, design |
|
||||
| [vercel-labs/next-skills](https://github.com/vercel-labs/next-skills) | 1 | Next.js 15+ |
|
||||
| [vercel/ai](https://github.com/vercel/ai) | 1 | AI SDK |
|
||||
| [halthelobster/proactive-agent](https://github.com/halthelobster/proactive-agent) | 1 | Agent architecture |
|
||||
| [openrouterteam/agent-skills](https://github.com/openrouterteam/agent-skills) | 1 | Agent building |
|
||||
| [kadajett/agent-nestjs-skills](https://github.com/kadajett/agent-nestjs-skills) | 1 | NestJS |
|
||||
| [jezweb/claude-skills](https://github.com/jezweb/claude-skills) | 1 | FastAPI |
|
||||
| [wshobson/agents](https://github.com/wshobson/agents) | 3 | Architecture, Python, Tailwind |
|
||||
| [mblode/agent-skills](https://github.com/mblode/agent-skills) | 1 | UI animation |
|
||||
| [giuseppe-trisciuoglio/developer-kit](https://github.com/giuseppe-trisciuoglio/developer-kit) | 1 | shadcn/ui |
|
||||
| Mosaic Stack (original) | 4 | PR review, code review, orchestration, linting |
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
@@ -24,20 +24,24 @@ Before designing a test, understand:
|
||||
## Core Principles
|
||||
|
||||
### 1. Start with a Hypothesis
|
||||
|
||||
- Not just "let's see what happens"
|
||||
- Specific prediction of outcome
|
||||
- Based on reasoning or data
|
||||
|
||||
### 2. Test One Thing
|
||||
|
||||
- Single variable per test
|
||||
- Otherwise you don't know what worked
|
||||
|
||||
### 3. Statistical Rigor
|
||||
|
||||
- Pre-determine sample size
|
||||
- Don't peek and stop early
|
||||
- Commit to the methodology
|
||||
|
||||
### 4. Measure What Matters
|
||||
|
||||
- Primary metric tied to business value
|
||||
- Secondary metrics for context
|
||||
- Guardrail metrics to prevent harm
|
||||
@@ -66,12 +70,12 @@ We'll know this is true when [metrics].
|
||||
|
||||
## Test Types
|
||||
|
||||
| Type | Description | Traffic Needed |
|
||||
|------|-------------|----------------|
|
||||
| A/B | Two versions, single change | Moderate |
|
||||
| A/B/n | Multiple variants | Higher |
|
||||
| MVT | Multiple changes in combinations | Very high |
|
||||
| Split URL | Different URLs for variants | Moderate |
|
||||
| Type | Description | Traffic Needed |
|
||||
| --------- | -------------------------------- | -------------- |
|
||||
| A/B | Two versions, single change | Moderate |
|
||||
| A/B/n | Multiple variants | Higher |
|
||||
| MVT | Multiple changes in combinations | Very high |
|
||||
| Split URL | Different URLs for variants | Moderate |
|
||||
|
||||
---
|
||||
|
||||
@@ -79,14 +83,15 @@ We'll know this is true when [metrics].
|
||||
|
||||
### Quick Reference
|
||||
|
||||
| Baseline | 10% Lift | 20% Lift | 50% Lift |
|
||||
|----------|----------|----------|----------|
|
||||
| 1% | 150k/variant | 39k/variant | 6k/variant |
|
||||
| 3% | 47k/variant | 12k/variant | 2k/variant |
|
||||
| 5% | 27k/variant | 7k/variant | 1.2k/variant |
|
||||
| 10% | 12k/variant | 3k/variant | 550/variant |
|
||||
| Baseline | 10% Lift | 20% Lift | 50% Lift |
|
||||
| -------- | ------------ | ----------- | ------------ |
|
||||
| 1% | 150k/variant | 39k/variant | 6k/variant |
|
||||
| 3% | 47k/variant | 12k/variant | 2k/variant |
|
||||
| 5% | 27k/variant | 7k/variant | 1.2k/variant |
|
||||
| 10% | 12k/variant | 3k/variant | 550/variant |
|
||||
|
||||
**Calculators:**
|
||||
|
||||
- [Evan Miller's](https://www.evanmiller.org/ab-testing/sample-size.html)
|
||||
- [Optimizely's](https://www.optimizely.com/sample-size-calculator/)
|
||||
|
||||
@@ -97,19 +102,23 @@ We'll know this is true when [metrics].
|
||||
## Metrics Selection
|
||||
|
||||
### Primary Metric
|
||||
|
||||
- Single metric that matters most
|
||||
- Directly tied to hypothesis
|
||||
- What you'll use to call the test
|
||||
|
||||
### Secondary Metrics
|
||||
|
||||
- Support primary metric interpretation
|
||||
- Explain why/how the change worked
|
||||
|
||||
### Guardrail Metrics
|
||||
|
||||
- Things that shouldn't get worse
|
||||
- Stop test if significantly negative
|
||||
|
||||
### Example: Pricing Page Test
|
||||
|
||||
- **Primary**: Plan selection rate
|
||||
- **Secondary**: Time on page, plan distribution
|
||||
- **Guardrail**: Support tickets, refund rate
|
||||
@@ -120,14 +129,15 @@ We'll know this is true when [metrics].
|
||||
|
||||
### What to Vary
|
||||
|
||||
| Category | Examples |
|
||||
|----------|----------|
|
||||
| Headlines/Copy | Message angle, value prop, specificity, tone |
|
||||
| Visual Design | Layout, color, images, hierarchy |
|
||||
| CTA | Button copy, size, placement, number |
|
||||
| Content | Information included, order, amount, social proof |
|
||||
| Category | Examples |
|
||||
| -------------- | ------------------------------------------------- |
|
||||
| Headlines/Copy | Message angle, value prop, specificity, tone |
|
||||
| Visual Design | Layout, color, images, hierarchy |
|
||||
| CTA | Button copy, size, placement, number |
|
||||
| Content | Information included, order, amount, social proof |
|
||||
|
||||
### Best Practices
|
||||
|
||||
- Single, meaningful change
|
||||
- Bold enough to make a difference
|
||||
- True to the hypothesis
|
||||
@@ -136,13 +146,14 @@ We'll know this is true when [metrics].
|
||||
|
||||
## Traffic Allocation
|
||||
|
||||
| Approach | Split | When to Use |
|
||||
|----------|-------|-------------|
|
||||
| Standard | 50/50 | Default for A/B |
|
||||
| Conservative | 90/10, 80/20 | Limit risk of bad variant |
|
||||
| Ramping | Start small, increase | Technical risk mitigation |
|
||||
| Approach | Split | When to Use |
|
||||
| ------------ | --------------------- | ------------------------- |
|
||||
| Standard | 50/50 | Default for A/B |
|
||||
| Conservative | 90/10, 80/20 | Limit risk of bad variant |
|
||||
| Ramping | Start small, increase | Technical risk mitigation |
|
||||
|
||||
**Considerations:**
|
||||
|
||||
- Consistency: Users see same variant on return
|
||||
- Balanced exposure across time of day/week
|
||||
|
||||
@@ -151,11 +162,13 @@ We'll know this is true when [metrics].
|
||||
## Implementation
|
||||
|
||||
### Client-Side
|
||||
|
||||
- JavaScript modifies page after load
|
||||
- Quick to implement, can cause flicker
|
||||
- Tools: PostHog, Optimizely, VWO
|
||||
|
||||
### Server-Side
|
||||
|
||||
- Variant determined before render
|
||||
- No flicker, requires dev work
|
||||
- Tools: PostHog, LaunchDarkly, Split
|
||||
@@ -165,6 +178,7 @@ We'll know this is true when [metrics].
|
||||
## Running the Test
|
||||
|
||||
### Pre-Launch Checklist
|
||||
|
||||
- [ ] Hypothesis documented
|
||||
- [ ] Primary metric defined
|
||||
- [ ] Sample size calculated
|
||||
@@ -175,16 +189,19 @@ We'll know this is true when [metrics].
|
||||
### During the Test
|
||||
|
||||
**DO:**
|
||||
|
||||
- Monitor for technical issues
|
||||
- Check segment quality
|
||||
- Document external factors
|
||||
|
||||
**DON'T:**
|
||||
|
||||
- Peek at results and stop early
|
||||
- Make changes to variants
|
||||
- Add traffic from new sources
|
||||
|
||||
### The Peeking Problem
|
||||
|
||||
Looking at results before reaching sample size and stopping early leads to false positives and wrong decisions. Pre-commit to sample size and trust the process.
|
||||
|
||||
---
|
||||
@@ -192,6 +209,7 @@ Looking at results before reaching sample size and stopping early leads to false
|
||||
## Analyzing Results
|
||||
|
||||
### Statistical Significance
|
||||
|
||||
- 95% confidence = p-value < 0.05
|
||||
- Means <5% chance result is random
|
||||
- Not a guarantee—just a threshold
|
||||
@@ -207,18 +225,19 @@ Looking at results before reaching sample size and stopping early leads to false
|
||||
|
||||
### Interpreting Results
|
||||
|
||||
| Result | Conclusion |
|
||||
|--------|------------|
|
||||
| Significant winner | Implement variant |
|
||||
| Significant loser | Keep control, learn why |
|
||||
| Result | Conclusion |
|
||||
| ------------------------- | -------------------------------- |
|
||||
| Significant winner | Implement variant |
|
||||
| Significant loser | Keep control, learn why |
|
||||
| No significant difference | Need more traffic or bolder test |
|
||||
| Mixed signals | Dig deeper, maybe segment |
|
||||
| Mixed signals | Dig deeper, maybe segment |
|
||||
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
Document every test with:
|
||||
|
||||
- Hypothesis
|
||||
- Variants (with screenshots)
|
||||
- Results (sample, metrics, significance)
|
||||
@@ -231,16 +250,19 @@ Document every test with:
|
||||
## Common Mistakes
|
||||
|
||||
### Test Design
|
||||
|
||||
- Testing too small a change (undetectable)
|
||||
- Testing too many things (can't isolate)
|
||||
- No clear hypothesis
|
||||
|
||||
### Execution
|
||||
|
||||
- Stopping early
|
||||
- Changing things mid-test
|
||||
- Not checking implementation
|
||||
|
||||
### Analysis
|
||||
|
||||
- Ignoring confidence intervals
|
||||
- Cherry-picking segments
|
||||
- Over-interpreting inconclusive results
|
||||
|
||||
@@ -16,6 +16,7 @@ Reference for calculating sample sizes and test duration.
|
||||
**Baseline conversion rate**: If your page converts at 5%, that's your baseline.
|
||||
|
||||
**MDE (Minimum Detectable Effect)**: The smallest improvement you care about detecting. Set this based on:
|
||||
|
||||
- Business impact (is a 5% lift meaningful?)
|
||||
- Implementation cost (worth the effort?)
|
||||
- Realistic expectations (what have past tests shown?)
|
||||
@@ -30,53 +31,53 @@ Reference for calculating sample sizes and test duration.
|
||||
|
||||
### Conversion Rate: 1%
|
||||
|
||||
| Lift to Detect | Sample per Variant | Total Sample |
|
||||
|----------------|-------------------|--------------|
|
||||
| 5% (1% → 1.05%) | 1,500,000 | 3,000,000 |
|
||||
| 10% (1% → 1.1%) | 380,000 | 760,000 |
|
||||
| 20% (1% → 1.2%) | 97,000 | 194,000 |
|
||||
| 50% (1% → 1.5%) | 16,000 | 32,000 |
|
||||
| 100% (1% → 2%) | 4,200 | 8,400 |
|
||||
| Lift to Detect | Sample per Variant | Total Sample |
|
||||
| --------------- | ------------------ | ------------ |
|
||||
| 5% (1% → 1.05%) | 1,500,000 | 3,000,000 |
|
||||
| 10% (1% → 1.1%) | 380,000 | 760,000 |
|
||||
| 20% (1% → 1.2%) | 97,000 | 194,000 |
|
||||
| 50% (1% → 1.5%) | 16,000 | 32,000 |
|
||||
| 100% (1% → 2%) | 4,200 | 8,400 |
|
||||
|
||||
### Conversion Rate: 3%
|
||||
|
||||
| Lift to Detect | Sample per Variant | Total Sample |
|
||||
|----------------|-------------------|--------------|
|
||||
| 5% (3% → 3.15%) | 480,000 | 960,000 |
|
||||
| 10% (3% → 3.3%) | 120,000 | 240,000 |
|
||||
| 20% (3% → 3.6%) | 31,000 | 62,000 |
|
||||
| 50% (3% → 4.5%) | 5,200 | 10,400 |
|
||||
| 100% (3% → 6%) | 1,400 | 2,800 |
|
||||
| Lift to Detect | Sample per Variant | Total Sample |
|
||||
| --------------- | ------------------ | ------------ |
|
||||
| 5% (3% → 3.15%) | 480,000 | 960,000 |
|
||||
| 10% (3% → 3.3%) | 120,000 | 240,000 |
|
||||
| 20% (3% → 3.6%) | 31,000 | 62,000 |
|
||||
| 50% (3% → 4.5%) | 5,200 | 10,400 |
|
||||
| 100% (3% → 6%) | 1,400 | 2,800 |
|
||||
|
||||
### Conversion Rate: 5%
|
||||
|
||||
| Lift to Detect | Sample per Variant | Total Sample |
|
||||
|----------------|-------------------|--------------|
|
||||
| 5% (5% → 5.25%) | 280,000 | 560,000 |
|
||||
| 10% (5% → 5.5%) | 72,000 | 144,000 |
|
||||
| 20% (5% → 6%) | 18,000 | 36,000 |
|
||||
| 50% (5% → 7.5%) | 3,100 | 6,200 |
|
||||
| 100% (5% → 10%) | 810 | 1,620 |
|
||||
| Lift to Detect | Sample per Variant | Total Sample |
|
||||
| --------------- | ------------------ | ------------ |
|
||||
| 5% (5% → 5.25%) | 280,000 | 560,000 |
|
||||
| 10% (5% → 5.5%) | 72,000 | 144,000 |
|
||||
| 20% (5% → 6%) | 18,000 | 36,000 |
|
||||
| 50% (5% → 7.5%) | 3,100 | 6,200 |
|
||||
| 100% (5% → 10%) | 810 | 1,620 |
|
||||
|
||||
### Conversion Rate: 10%
|
||||
|
||||
| Lift to Detect | Sample per Variant | Total Sample |
|
||||
|----------------|-------------------|--------------|
|
||||
| 5% (10% → 10.5%) | 130,000 | 260,000 |
|
||||
| 10% (10% → 11%) | 34,000 | 68,000 |
|
||||
| 20% (10% → 12%) | 8,700 | 17,400 |
|
||||
| 50% (10% → 15%) | 1,500 | 3,000 |
|
||||
| 100% (10% → 20%) | 400 | 800 |
|
||||
| Lift to Detect | Sample per Variant | Total Sample |
|
||||
| ---------------- | ------------------ | ------------ |
|
||||
| 5% (10% → 10.5%) | 130,000 | 260,000 |
|
||||
| 10% (10% → 11%) | 34,000 | 68,000 |
|
||||
| 20% (10% → 12%) | 8,700 | 17,400 |
|
||||
| 50% (10% → 15%) | 1,500 | 3,000 |
|
||||
| 100% (10% → 20%) | 400 | 800 |
|
||||
|
||||
### Conversion Rate: 20%
|
||||
|
||||
| Lift to Detect | Sample per Variant | Total Sample |
|
||||
|----------------|-------------------|--------------|
|
||||
| 5% (20% → 21%) | 60,000 | 120,000 |
|
||||
| 10% (20% → 22%) | 16,000 | 32,000 |
|
||||
| 20% (20% → 24%) | 4,000 | 8,000 |
|
||||
| 50% (20% → 30%) | 700 | 1,400 |
|
||||
| 100% (20% → 40%) | 200 | 400 |
|
||||
| Lift to Detect | Sample per Variant | Total Sample |
|
||||
| ---------------- | ------------------ | ------------ |
|
||||
| 5% (20% → 21%) | 60,000 | 120,000 |
|
||||
| 10% (20% → 22%) | 16,000 | 32,000 |
|
||||
| 20% (20% → 24%) | 4,000 | 8,000 |
|
||||
| 50% (20% → 30%) | 700 | 1,400 |
|
||||
| 100% (20% → 40%) | 200 | 400 |
|
||||
|
||||
---
|
||||
|
||||
@@ -91,18 +92,21 @@ Duration (days) = (Sample per variant × Number of variants) / (Daily traffic ×
|
||||
### Examples
|
||||
|
||||
**Scenario 1: High-traffic page**
|
||||
|
||||
- Need: 10,000 per variant (2 variants = 20,000 total)
|
||||
- Daily traffic: 5,000 visitors
|
||||
- 100% exposed to test
|
||||
- Duration: 20,000 / 5,000 = **4 days**
|
||||
|
||||
**Scenario 2: Medium-traffic page**
|
||||
|
||||
- Need: 30,000 per variant (60,000 total)
|
||||
- Daily traffic: 2,000 visitors
|
||||
- 100% exposed
|
||||
- Duration: 60,000 / 2,000 = **30 days**
|
||||
|
||||
**Scenario 3: Low-traffic with partial exposure**
|
||||
|
||||
- Need: 15,000 per variant (30,000 total)
|
||||
- Daily traffic: 500 visitors
|
||||
- 50% exposed to test
|
||||
@@ -112,6 +116,7 @@ Duration (days) = (Sample per variant × Number of variants) / (Daily traffic ×
|
||||
### Minimum Duration Rules
|
||||
|
||||
Even with sufficient sample size, run tests for at least:
|
||||
|
||||
- **1 full week**: To capture day-of-week variation
|
||||
- **2 business cycles**: If B2B (weekday vs. weekend patterns)
|
||||
- **Through paydays**: If e-commerce (beginning/end of month)
|
||||
@@ -119,6 +124,7 @@ Even with sufficient sample size, run tests for at least:
|
||||
### Maximum Duration Guidelines
|
||||
|
||||
Avoid running tests longer than 4-8 weeks:
|
||||
|
||||
- Novelty effects wear off
|
||||
- External factors intervene
|
||||
- Opportunity cost of other tests
|
||||
@@ -131,21 +137,25 @@ Avoid running tests longer than 4-8 weeks:
|
||||
|
||||
**Evan Miller's Calculator**
|
||||
https://www.evanmiller.org/ab-testing/sample-size.html
|
||||
|
||||
- Simple interface
|
||||
- Bookmark-worthy
|
||||
|
||||
**Optimizely's Calculator**
|
||||
https://www.optimizely.com/sample-size-calculator/
|
||||
|
||||
- Business-friendly language
|
||||
- Duration estimates
|
||||
|
||||
**AB Test Guide Calculator**
|
||||
https://www.abtestguide.com/calc/
|
||||
|
||||
- Includes Bayesian option
|
||||
- Multiple test types
|
||||
|
||||
**VWO Duration Calculator**
|
||||
https://vwo.com/tools/ab-test-duration-calculator/
|
||||
|
||||
- Duration-focused
|
||||
- Good for planning
|
||||
|
||||
@@ -155,14 +165,15 @@ https://vwo.com/tools/ab-test-duration-calculator/
|
||||
|
||||
With more than 2 variants (A/B/n tests), you need more sample:
|
||||
|
||||
| Variants | Multiplier |
|
||||
|----------|------------|
|
||||
| 2 (A/B) | 1x |
|
||||
| 3 (A/B/C) | ~1.5x |
|
||||
| 4 (A/B/C/D) | ~2x |
|
||||
| 5+ | Consider reducing variants |
|
||||
| Variants | Multiplier |
|
||||
| ----------- | -------------------------- |
|
||||
| 2 (A/B) | 1x |
|
||||
| 3 (A/B/C) | ~1.5x |
|
||||
| 4 (A/B/C/D) | ~2x |
|
||||
| 5+ | Consider reducing variants |
|
||||
|
||||
**Why?** More comparisons increase chance of false positives. You're comparing:
|
||||
|
||||
- A vs B
|
||||
- A vs C
|
||||
- B vs C (sometimes)
|
||||
@@ -174,22 +185,27 @@ Apply Bonferroni correction or use tools that handle this automatically.
|
||||
## Common Sample Size Mistakes
|
||||
|
||||
### 1. Underpowered tests
|
||||
|
||||
**Problem**: Not enough sample to detect realistic effects
|
||||
**Fix**: Be realistic about MDE, get more traffic, or don't test
|
||||
|
||||
### 2. Overpowered tests
|
||||
|
||||
**Problem**: Waiting for sample size when you already have significance
|
||||
**Fix**: This is actually fine—you committed to sample size, honor it
|
||||
|
||||
### 3. Wrong baseline rate
|
||||
|
||||
**Problem**: Using wrong conversion rate for calculation
|
||||
**Fix**: Use the specific metric and page, not site-wide averages
|
||||
|
||||
### 4. Ignoring segments
|
||||
|
||||
**Problem**: Calculating for full traffic, then analyzing segments
|
||||
**Fix**: If you plan segment analysis, calculate sample for smallest segment
|
||||
|
||||
### 5. Testing too many things
|
||||
|
||||
**Problem**: Dividing traffic too many ways
|
||||
**Fix**: Prioritize ruthlessly, run fewer concurrent tests
|
||||
|
||||
@@ -214,19 +230,23 @@ Options when you can't get enough traffic:
|
||||
If you must check results before reaching sample size:
|
||||
|
||||
### What is it?
|
||||
|
||||
Statistical method that adjusts for multiple looks at data.
|
||||
|
||||
### When to use
|
||||
|
||||
- High-risk changes
|
||||
- Need to stop bad variants early
|
||||
- Time-sensitive decisions
|
||||
|
||||
### Tools that support it
|
||||
|
||||
- Optimizely (Stats Accelerator)
|
||||
- VWO (SmartStats)
|
||||
- PostHog (Bayesian approach)
|
||||
|
||||
### Tradeoff
|
||||
|
||||
- More flexibility to stop early
|
||||
- Slightly larger sample size requirement
|
||||
- More complex analysis
|
||||
|
||||
@@ -8,6 +8,7 @@ Templates for planning, documenting, and analyzing experiments.
|
||||
# A/B Test: [Name]
|
||||
|
||||
## Overview
|
||||
|
||||
- **Owner**: [Name]
|
||||
- **Test ID**: [ID in testing tool]
|
||||
- **Page/Feature**: [What's being tested]
|
||||
@@ -23,24 +24,28 @@ We'll know this is true when [metrics].
|
||||
|
||||
## Test Design
|
||||
|
||||
| Element | Details |
|
||||
|---------|---------|
|
||||
| Test type | A/B / A/B/n / MVT |
|
||||
| Duration | X weeks |
|
||||
| Sample size | X per variant |
|
||||
| Traffic allocation | 50/50 |
|
||||
| Tool | [Tool name] |
|
||||
| Implementation | Client-side / Server-side |
|
||||
| Element | Details |
|
||||
| ------------------ | ------------------------- |
|
||||
| Test type | A/B / A/B/n / MVT |
|
||||
| Duration | X weeks |
|
||||
| Sample size | X per variant |
|
||||
| Traffic allocation | 50/50 |
|
||||
| Tool | [Tool name] |
|
||||
| Implementation | Client-side / Server-side |
|
||||
|
||||
## Variants
|
||||
|
||||
### Control (A)
|
||||
|
||||
[Screenshot]
|
||||
|
||||
- Current experience
|
||||
- [Key details about current state]
|
||||
|
||||
### Variant (B)
|
||||
|
||||
[Screenshot or mockup]
|
||||
|
||||
- [Specific change #1]
|
||||
- [Specific change #2]
|
||||
- Rationale: [Why we think this will win]
|
||||
@@ -48,32 +53,38 @@ We'll know this is true when [metrics].
|
||||
## Metrics
|
||||
|
||||
### Primary
|
||||
|
||||
- **Metric**: [metric name]
|
||||
- **Definition**: [how it's calculated]
|
||||
- **Current baseline**: [X%]
|
||||
- **Minimum detectable effect**: [X%]
|
||||
|
||||
### Secondary
|
||||
|
||||
- [Metric 1]: [what it tells us]
|
||||
- [Metric 2]: [what it tells us]
|
||||
- [Metric 3]: [what it tells us]
|
||||
|
||||
### Guardrails
|
||||
|
||||
- [Metric that shouldn't get worse]
|
||||
- [Another safety metric]
|
||||
|
||||
## Segment Analysis Plan
|
||||
|
||||
- Mobile vs. desktop
|
||||
- New vs. returning visitors
|
||||
- Traffic source
|
||||
- [Other relevant segments]
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Winner: [Primary metric improves by X% with 95% confidence]
|
||||
- Loser: [Primary metric decreases significantly]
|
||||
- Inconclusive: [What we'll do if no significant result]
|
||||
|
||||
## Pre-Launch Checklist
|
||||
|
||||
- [ ] Hypothesis documented and reviewed
|
||||
- [ ] Primary metric defined and trackable
|
||||
- [ ] Sample size calculated
|
||||
@@ -93,46 +104,50 @@ We'll know this is true when [metrics].
|
||||
# A/B Test Results: [Name]
|
||||
|
||||
## Summary
|
||||
| Element | Value |
|
||||
|---------|-------|
|
||||
| Test ID | [ID] |
|
||||
| Dates | [Start] - [End] |
|
||||
| Duration | X days |
|
||||
| Result | Winner / Loser / Inconclusive |
|
||||
| Decision | [What we're doing] |
|
||||
|
||||
| Element | Value |
|
||||
| -------- | ----------------------------- |
|
||||
| Test ID | [ID] |
|
||||
| Dates | [Start] - [End] |
|
||||
| Duration | X days |
|
||||
| Result | Winner / Loser / Inconclusive |
|
||||
| Decision | [What we're doing] |
|
||||
|
||||
## Hypothesis (Reminder)
|
||||
|
||||
[Copy from test plan]
|
||||
|
||||
## Results
|
||||
|
||||
### Sample Size
|
||||
|
||||
| Variant | Target | Actual | % of target |
|
||||
|---------|--------|--------|-------------|
|
||||
| Control | X | Y | Z% |
|
||||
| Variant | X | Y | Z% |
|
||||
| ------- | ------ | ------ | ----------- |
|
||||
| Control | X | Y | Z% |
|
||||
| Variant | X | Y | Z% |
|
||||
|
||||
### Primary Metric: [Metric Name]
|
||||
| Variant | Value | 95% CI | vs. Control |
|
||||
|---------|-------|--------|-------------|
|
||||
| Control | X% | [X%, Y%] | — |
|
||||
| Variant | X% | [X%, Y%] | +X% |
|
||||
|
||||
| Variant | Value | 95% CI | vs. Control |
|
||||
| ------- | ----- | -------- | ----------- |
|
||||
| Control | X% | [X%, Y%] | — |
|
||||
| Variant | X% | [X%, Y%] | +X% |
|
||||
|
||||
**Statistical significance**: p = X.XX (95% = sig / not sig)
|
||||
**Practical significance**: [Is this lift meaningful for the business?]
|
||||
|
||||
### Secondary Metrics
|
||||
|
||||
| Metric | Control | Variant | Change | Significant? |
|
||||
|--------|---------|---------|--------|--------------|
|
||||
| [Metric 1] | X | Y | +Z% | Yes/No |
|
||||
| [Metric 2] | X | Y | +Z% | Yes/No |
|
||||
| Metric | Control | Variant | Change | Significant? |
|
||||
| ---------- | ------- | ------- | ------ | ------------ |
|
||||
| [Metric 1] | X | Y | +Z% | Yes/No |
|
||||
| [Metric 2] | X | Y | +Z% | Yes/No |
|
||||
|
||||
### Guardrail Metrics
|
||||
|
||||
| Metric | Control | Variant | Change | Concern? |
|
||||
|--------|---------|---------|--------|----------|
|
||||
| [Metric 1] | X | Y | +Z% | Yes/No |
|
||||
| Metric | Control | Variant | Change | Concern? |
|
||||
| ---------- | ------- | ------- | ------ | -------- |
|
||||
| [Metric 1] | X | Y | +Z% | Yes/No |
|
||||
|
||||
### Segment Analysis
|
||||
|
||||
@@ -151,12 +166,15 @@ We'll know this is true when [metrics].
|
||||
## Interpretation
|
||||
|
||||
### What happened?
|
||||
|
||||
[Explanation of results in plain language]
|
||||
|
||||
### Why do we think this happened?
|
||||
|
||||
[Analysis and reasoning]
|
||||
|
||||
### Caveats
|
||||
|
||||
[Any limitations, external factors, or concerns]
|
||||
|
||||
## Decision
|
||||
@@ -170,14 +188,17 @@ We'll know this is true when [metrics].
|
||||
## Learnings
|
||||
|
||||
### What we learned
|
||||
|
||||
- [Key insight 1]
|
||||
- [Key insight 2]
|
||||
|
||||
### What to test next
|
||||
|
||||
- [Follow-up test idea 1]
|
||||
- [Follow-up test idea 2]
|
||||
|
||||
### Impact
|
||||
|
||||
- **Projected lift**: [X% improvement in Y metric]
|
||||
- **Business impact**: [Revenue, conversions, etc.]
|
||||
```
|
||||
@@ -189,11 +210,11 @@ We'll know this is true when [metrics].
|
||||
For tracking all tests in a central location:
|
||||
|
||||
```markdown
|
||||
| Test ID | Name | Page | Dates | Primary Metric | Result | Lift | Link |
|
||||
|---------|------|------|-------|----------------|--------|------|------|
|
||||
| 001 | Hero headline test | Homepage | 1/1-1/15 | CTR | Winner | +12% | [Link] |
|
||||
| 002 | Pricing table layout | Pricing | 1/10-1/31 | Plan selection | Loser | -5% | [Link] |
|
||||
| 003 | Signup form fields | Signup | 2/1-2/14 | Completion | Inconclusive | +2% | [Link] |
|
||||
| Test ID | Name | Page | Dates | Primary Metric | Result | Lift | Link |
|
||||
| ------- | -------------------- | -------- | --------- | -------------- | ------------ | ---- | ------ |
|
||||
| 001 | Hero headline test | Homepage | 1/1-1/15 | CTR | Winner | +12% | [Link] |
|
||||
| 002 | Pricing table layout | Pricing | 1/10-1/31 | Plan selection | Loser | -5% | [Link] |
|
||||
| 003 | Signup form fields | Signup | 2/1-2/14 | Completion | Inconclusive | +2% | [Link] |
|
||||
```
|
||||
|
||||
---
|
||||
@@ -225,15 +246,18 @@ For simple tests that don't need full documentation:
|
||||
**Current sample**: X% of target
|
||||
|
||||
### Preliminary observations
|
||||
|
||||
[What we're seeing - without making decisions yet]
|
||||
|
||||
### Next steps
|
||||
|
||||
[What happens next]
|
||||
|
||||
### Timeline
|
||||
|
||||
- [Date]: Analysis complete
|
||||
- [Date]: Decision and recommendation
|
||||
- [Date]: Implementation (if winner)
|
||||
- [Date]: Implementation 'if winner'
|
||||
```
|
||||
|
||||
---
|
||||
@@ -242,14 +266,14 @@ For simple tests that don't need full documentation:
|
||||
|
||||
For deciding which tests to run:
|
||||
|
||||
| Factor | Weight | Test A | Test B | Test C |
|
||||
|--------|--------|--------|--------|--------|
|
||||
| Potential impact | 30% | | | |
|
||||
| Confidence in hypothesis | 25% | | | |
|
||||
| Ease of implementation | 20% | | | |
|
||||
| Risk if wrong | 15% | | | |
|
||||
| Strategic alignment | 10% | | | |
|
||||
| **Total** | | | | |
|
||||
| Factor | Weight | Test A | Test B | Test C |
|
||||
| ------------------------ | ------ | ------ | ------ | ------ |
|
||||
| Potential impact | 30% | | | |
|
||||
| Confidence in hypothesis | 25% | | | |
|
||||
| Ease of implementation | 20% | | | |
|
||||
| Risk if wrong | 15% | | | |
|
||||
| Strategic alignment | 10% | | | |
|
||||
| **Total** | | | | |
|
||||
|
||||
Scoring: 1-5 (5 = best)
|
||||
|
||||
@@ -260,9 +284,9 @@ Scoring: 1-5 (5 = best)
|
||||
For collecting test ideas:
|
||||
|
||||
```markdown
|
||||
| ID | Page/Area | Observation | Hypothesis | Potential Impact | Status |
|
||||
|----|-----------|-------------|------------|------------------|--------|
|
||||
| H1 | Homepage | Low scroll depth | Shorter hero will increase scroll | High | Testing |
|
||||
| H2 | Pricing | Users compare plans | Comparison table will help | Medium | Backlog |
|
||||
| H3 | Signup | Drop-off at email | Social login will increase completion | Medium | Backlog |
|
||||
| ID | Page/Area | Observation | Hypothesis | Potential Impact | Status |
|
||||
| --- | --------- | ------------------- | ------------------------------------- | ---------------- | ------- |
|
||||
| H1 | Homepage | Low scroll depth | Shorter hero will increase scroll | High | Testing |
|
||||
| H2 | Pricing | Users compare plans | Comparison table will help | Medium | Backlog |
|
||||
| H3 | Signup | Drop-off at email | Social login will increase completion | Medium | Backlog |
|
||||
```
|
||||
|
||||
@@ -225,7 +225,7 @@ export default function Page() {
|
||||
transport: new DefaultChatTransport({ api: '/api/chat' }),
|
||||
});
|
||||
|
||||
const handleSubmit = e => {
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
sendMessage({ text: input });
|
||||
setInput('');
|
||||
@@ -233,7 +233,7 @@ export default function Page() {
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input value={input} onChange={e => setInput(e.target.value)} />
|
||||
<input value={input} onChange={(e) => setInput(e.target.value)} />
|
||||
<button type="submit">Send</button>
|
||||
</form>
|
||||
);
|
||||
@@ -259,18 +259,14 @@ Typed tool parts also use different property names:
|
||||
case 'text':
|
||||
return <div key={`${message.id}-${i}`}>{part.text}</div>;
|
||||
case 'tool-invocation': // deprecated: use typed tool parts instead
|
||||
return (
|
||||
<pre key={`${message.id}-${i}`}>
|
||||
{JSON.stringify(part.toolInvocation, null, 2)}
|
||||
</pre>
|
||||
);
|
||||
return <pre key={`${message.id}-${i}`}>{JSON.stringify(part.toolInvocation, null, 2)}</pre>;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ Correct - using typed tool parts (recommended)
|
||||
{
|
||||
message.parts.map(part => {
|
||||
message.parts.map((part) => {
|
||||
switch (part.type) {
|
||||
case 'text':
|
||||
return part.text;
|
||||
@@ -288,7 +284,7 @@ Typed tool parts also use different property names:
|
||||
import { isToolUIPart } from 'ai';
|
||||
|
||||
{
|
||||
message.parts.map(part => {
|
||||
message.parts.map((part) => {
|
||||
if (part.type === 'text') {
|
||||
return part.text;
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ export function Chat() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
{messages.map(message => (
|
||||
{messages.map((message) => (
|
||||
<Message key={message.id} message={message} />
|
||||
))}
|
||||
</div>
|
||||
@@ -161,11 +161,7 @@ Then import only the type in your component:
|
||||
// components/weather-tool.tsx
|
||||
import type { WeatherToolInvocation } from '@/lib/tools/weather-tool';
|
||||
|
||||
export function WeatherToolComponent({
|
||||
invocation,
|
||||
}: {
|
||||
invocation: WeatherToolInvocation;
|
||||
}) {
|
||||
export function WeatherToolComponent({ invocation }: { invocation: WeatherToolInvocation }) {
|
||||
// invocation.input and invocation.output are fully typed
|
||||
if (invocation.state === 'output-available') {
|
||||
return (
|
||||
|
||||
@@ -7,6 +7,7 @@ license: Complete terms in LICENSE.txt
|
||||
Algorithmic philosophies are computational aesthetic movements that are then expressed through code. Output .md files (philosophy), .html files (interactive viewer), and .js files (generative algorithms).
|
||||
|
||||
This happens in two steps:
|
||||
|
||||
1. Algorithmic Philosophy Creation (.md file)
|
||||
2. Express by creating p5.js generative art (.html + .js files)
|
||||
|
||||
@@ -15,17 +16,20 @@ First, undertake this task:
|
||||
## ALGORITHMIC PHILOSOPHY CREATION
|
||||
|
||||
To begin, create an ALGORITHMIC PHILOSOPHY (not static images or templates) that will be interpreted through:
|
||||
|
||||
- Computational processes, emergent behavior, mathematical beauty
|
||||
- Seeded randomness, noise fields, organic systems
|
||||
- Particles, flows, fields, forces
|
||||
- Parametric variation and controlled chaos
|
||||
|
||||
### THE CRITICAL UNDERSTANDING
|
||||
|
||||
- What is received: Some subtle input or instructions by the user to take into account, but use as a foundation; it should not constrain creative freedom.
|
||||
- What is created: An algorithmic philosophy/generative aesthetic movement.
|
||||
- What happens next: The same version receives the philosophy and EXPRESSES IT IN CODE - creating p5.js sketches that are 90% algorithmic generation, 10% essential parameters.
|
||||
|
||||
Consider this approach:
|
||||
|
||||
- Write a manifesto for a generative art movement
|
||||
- The next phase involves writing the algorithm that brings it to life
|
||||
|
||||
@@ -38,6 +42,7 @@ The philosophy must emphasize: Algorithmic expression. Emergent behavior. Comput
|
||||
**Articulate the philosophy** (4-6 paragraphs - concise but complete):
|
||||
|
||||
To capture the ALGORITHMIC essence, express how this philosophy manifests through:
|
||||
|
||||
- Computational processes and mathematical relationships?
|
||||
- Noise functions and randomness patterns?
|
||||
- Particle behaviors and field dynamics?
|
||||
@@ -45,6 +50,7 @@ To capture the ALGORITHMIC essence, express how this philosophy manifests throug
|
||||
- Parametric variation and emergent complexity?
|
||||
|
||||
**CRITICAL GUIDELINES:**
|
||||
|
||||
- **Avoid redundancy**: Each algorithmic aspect should be mentioned once. Avoid repeating concepts about noise theory, particle dynamics, or mathematical principles unless adding new depth.
|
||||
- **Emphasize craftsmanship REPEATEDLY**: The philosophy MUST stress multiple times that the final algorithm should appear as though it took countless hours to develop, was refined with care, and comes from someone at the absolute top of their field. This framing is essential - repeat phrases like "meticulously crafted algorithm," "the product of deep computational expertise," "painstaking optimization," "master-level implementation."
|
||||
- **Leave creative space**: Be specific about the algorithmic direction, but concise enough that the next Claude has room to make interpretive implementation choices at an extremely high level of craftsmanship.
|
||||
@@ -73,9 +79,10 @@ Algorithmic expression: Vector fields constructed from mathematical functions or
|
||||
Philosophy: Random processes crystallizing into ordered structures.
|
||||
Algorithmic expression: Randomized circle packing or Voronoi tessellation. Start with random points, let them evolve through relaxation algorithms. Cells push apart until equilibrium. Color based on cell size, neighbor count, or distance from center. The organic tiling that emerges feels both random and inevitable. Every seed produces unique crystalline beauty - the mark of a master-level generative algorithm.
|
||||
|
||||
*These are condensed examples. The actual algorithmic philosophy should be 4-6 substantial paragraphs.*
|
||||
_These are condensed examples. The actual algorithmic philosophy should be 4-6 substantial paragraphs._
|
||||
|
||||
### ESSENTIAL PRINCIPLES
|
||||
|
||||
- **ALGORITHMIC PHILOSOPHY**: Creating a computational worldview to be expressed through code
|
||||
- **PROCESS OVER PRODUCT**: Always emphasize that beauty emerges from the algorithm's execution - each run is unique
|
||||
- **PARAMETRIC EXPRESSION**: Ideas communicate through mathematical relationships, forces, behaviors - not static composition
|
||||
@@ -113,12 +120,14 @@ With the philosophy AND conceptual framework established, express it through cod
|
||||
5. **Replace only the VARIABLE sections** marked in the file's comments (algorithm, parameters, UI controls for parameters)
|
||||
|
||||
**Avoid:**
|
||||
|
||||
- ❌ Creating HTML from scratch
|
||||
- ❌ Inventing custom styling or color schemes
|
||||
- ❌ Using system fonts or dark themes
|
||||
- ❌ Changing the sidebar structure
|
||||
|
||||
**Follow these practices:**
|
||||
|
||||
- ✅ Copy the template's exact HTML structure
|
||||
- ✅ Keep Anthropic branding (Poppins/Lora fonts, light colors, gradient backdrop)
|
||||
- ✅ Maintain the sidebar layout (Seed → Parameters → Colors? → Actions)
|
||||
@@ -133,6 +142,7 @@ To create gallery-quality computational art that lives and breathes, use the alg
|
||||
### TECHNICAL REQUIREMENTS
|
||||
|
||||
**Seeded Randomness (Art Blocks Pattern)**:
|
||||
|
||||
```javascript
|
||||
// ALWAYS use a seed for reproducibility
|
||||
let seed = 12345; // or hash from user input
|
||||
@@ -146,7 +156,7 @@ To establish parameters that emerge naturally from the algorithmic philosophy, c
|
||||
|
||||
```javascript
|
||||
let params = {
|
||||
seed: 12345, // Always include seed for reproducibility
|
||||
seed: 12345, // Always include seed for reproducibility
|
||||
// colors
|
||||
// Add parameters that control YOUR algorithm:
|
||||
// - Quantities (how many?)
|
||||
@@ -167,16 +177,19 @@ let params = {
|
||||
To express the philosophy through code, avoid thinking "which pattern should I use?" and instead think "how to express this philosophy through code?"
|
||||
|
||||
If the philosophy is about **organic emergence**, consider using:
|
||||
|
||||
- Elements that accumulate or grow over time
|
||||
- Random processes constrained by natural rules
|
||||
- Feedback loops and interactions
|
||||
|
||||
If the philosophy is about **mathematical beauty**, consider using:
|
||||
|
||||
- Geometric relationships and ratios
|
||||
- Trigonometric functions and harmonics
|
||||
- Precise calculations creating unexpected patterns
|
||||
|
||||
If the philosophy is about **controlled chaos**, consider using:
|
||||
|
||||
- Random variation within strict boundaries
|
||||
- Bifurcation and phase transitions
|
||||
- Order emerging from disorder
|
||||
@@ -186,6 +199,7 @@ If the philosophy is about **controlled chaos**, consider using:
|
||||
To guide the implementation, let the conceptual essence inform creative and original choices. Build something that expresses the vision for this particular request.
|
||||
|
||||
**Canvas Setup**: Standard p5.js structure:
|
||||
|
||||
```javascript
|
||||
function setup() {
|
||||
createCanvas(1200, 1200);
|
||||
@@ -211,6 +225,7 @@ function draw() {
|
||||
### OUTPUT FORMAT
|
||||
|
||||
Output:
|
||||
|
||||
1. **Algorithmic Philosophy** - As markdown or text explaining the generative aesthetic
|
||||
2. **Single HTML Artifact** - Self-contained interactive generative art built from `templates/viewer.html` (see STEP 0 and next section)
|
||||
|
||||
@@ -229,6 +244,7 @@ To allow exploration of the generative art, create a single, self-contained HTML
|
||||
The `templates/viewer.html` file is the foundation. It contains the exact structure and styling needed.
|
||||
|
||||
**FIXED (always include exactly as shown):**
|
||||
|
||||
- Layout structure (header, sidebar, main canvas area)
|
||||
- Anthropic branding (UI colors, fonts, gradients)
|
||||
- Seed section in sidebar:
|
||||
@@ -241,6 +257,7 @@ The `templates/viewer.html` file is the foundation. It contains the exact struct
|
||||
- Reset button
|
||||
|
||||
**VARIABLE (customize for each artwork):**
|
||||
|
||||
- The entire p5.js algorithm (setup/draw/classes)
|
||||
- The parameters object (define what the art needs)
|
||||
- The Parameters section in sidebar:
|
||||
@@ -259,12 +276,14 @@ The `templates/viewer.html` file is the foundation. It contains the exact struct
|
||||
### REQUIRED FEATURES
|
||||
|
||||
**1. Parameter Controls**
|
||||
|
||||
- Sliders for numeric parameters (particle count, noise scale, speed, etc.)
|
||||
- Color pickers for palette colors
|
||||
- Real-time updates when parameters change
|
||||
- Reset button to restore defaults
|
||||
|
||||
**2. Seed Navigation**
|
||||
|
||||
- Display current seed number
|
||||
- "Previous" and "Next" buttons to cycle through seeds
|
||||
- "Random" button for random seed
|
||||
@@ -272,30 +291,31 @@ The `templates/viewer.html` file is the foundation. It contains the exact struct
|
||||
- Generate 100 variations when requested (seeds 1-100)
|
||||
|
||||
**3. Single Artifact Structure**
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- p5.js from CDN - always available -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/p5.min.js"></script>
|
||||
<style>
|
||||
/* All styling inline - clean, minimal */
|
||||
/* Canvas on top, controls below */
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="canvas-container"></div>
|
||||
<div id="controls">
|
||||
<!-- All parameter controls -->
|
||||
</div>
|
||||
<script>
|
||||
// ALL p5.js code inline here
|
||||
// Parameter objects, classes, functions
|
||||
// setup() and draw()
|
||||
// UI handlers
|
||||
// Everything self-contained
|
||||
</script>
|
||||
</body>
|
||||
<head>
|
||||
<!-- p5.js from CDN - always available -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/p5.min.js"></script>
|
||||
<style>
|
||||
/* All styling inline - clean, minimal */
|
||||
/* Canvas on top, controls below */
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="canvas-container"></div>
|
||||
<div id="controls">
|
||||
<!-- All parameter controls -->
|
||||
</div>
|
||||
<script>
|
||||
// ALL p5.js code inline here
|
||||
// Parameter objects, classes, functions
|
||||
// setup() and draw()
|
||||
// UI handlers
|
||||
// Everything self-contained
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
@@ -306,30 +326,44 @@ The `templates/viewer.html` file is the foundation. It contains the exact struct
|
||||
The sidebar structure:
|
||||
|
||||
**1. Seed (FIXED)** - Always include exactly as shown:
|
||||
|
||||
- Seed display
|
||||
- Prev/Next/Random/Jump buttons
|
||||
|
||||
**2. Parameters (VARIABLE)** - Create controls for the art:
|
||||
|
||||
```html
|
||||
<div class="control-group">
|
||||
<label>Parameter Name</label>
|
||||
<input type="range" id="param" min="..." max="..." step="..." value="..." oninput="updateParam('param', this.value)">
|
||||
<span class="value-display" id="param-value">...</span>
|
||||
<label>Parameter Name</label>
|
||||
<input
|
||||
type="range"
|
||||
id="param"
|
||||
min="..."
|
||||
max="..."
|
||||
step="..."
|
||||
value="..."
|
||||
oninput="updateParam('param', this.value)"
|
||||
/>
|
||||
<span class="value-display" id="param-value">...</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
Add as many control-group divs as there are parameters.
|
||||
|
||||
**3. Colors (OPTIONAL/VARIABLE)** - Include if the art needs adjustable colors:
|
||||
|
||||
- Add color pickers if users should control palette
|
||||
- Skip this section if the art uses fixed colors
|
||||
- Skip if the art is monochrome
|
||||
|
||||
**4. Actions (FIXED)** - Always include exactly as shown:
|
||||
|
||||
- Regenerate button
|
||||
- Reset button
|
||||
- Download PNG button
|
||||
|
||||
**Requirements**:
|
||||
|
||||
- Seed controls must work (prev/next/random/jump/display)
|
||||
- All parameters must have UI controls
|
||||
- Regenerate, Reset, Download buttons must work
|
||||
@@ -338,6 +372,7 @@ Add as many control-group divs as there are parameters.
|
||||
### USING THE ARTIFACT
|
||||
|
||||
The HTML artifact works immediately:
|
||||
|
||||
1. **In claude.ai**: Displayed as an interactive artifact - runs instantly
|
||||
2. **As a file**: Save and open in any browser - no server needed
|
||||
3. **Sharing**: Send the HTML file - it's completely self-contained
|
||||
@@ -369,11 +404,13 @@ Each request is unique. The process involves:
|
||||
5. **Build matching UI controls** - Sliders/inputs for those parameters
|
||||
|
||||
**The constants**:
|
||||
|
||||
- Anthropic branding (colors, fonts, layout)
|
||||
- Seed navigation (always present)
|
||||
- Self-contained HTML artifact
|
||||
|
||||
**Everything else is variable**:
|
||||
|
||||
- The algorithm itself
|
||||
- The parameters
|
||||
- The UI controls
|
||||
@@ -399,7 +436,8 @@ This skill includes helpful templates and documentation:
|
||||
- Embed algorithms inline in the HTML artifact (don't create separate .js files)
|
||||
|
||||
**Critical reminder**:
|
||||
|
||||
- The **template is the STARTING POINT**, not inspiration
|
||||
- The **algorithm is where to create** something unique
|
||||
- Don't copy the flow field example - build what the philosophy demands
|
||||
- But DO keep the exact UI structure and Anthropic branding from the template
|
||||
- But DO keep the exact UI structure and Anthropic branding from the template
|
||||
|
||||
@@ -24,21 +24,25 @@ Before implementing tracking, understand:
|
||||
## Core Principles
|
||||
|
||||
### 1. Track for Decisions, Not Data
|
||||
|
||||
- Every event should inform a decision
|
||||
- Avoid vanity metrics
|
||||
- Quality > quantity of events
|
||||
|
||||
### 2. Start with the Questions
|
||||
|
||||
- What do you need to know?
|
||||
- What actions will you take based on this data?
|
||||
- Work backwards to what you need to track
|
||||
|
||||
### 3. Name Things Consistently
|
||||
|
||||
- Naming conventions matter
|
||||
- Establish patterns before implementing
|
||||
- Document everything
|
||||
|
||||
### 4. Maintain Data Quality
|
||||
|
||||
- Validate implementation
|
||||
- Monitor for issues
|
||||
- Clean data > more data
|
||||
@@ -56,12 +60,12 @@ Event Name | Category | Properties | Trigger | Notes
|
||||
|
||||
### Event Types
|
||||
|
||||
| Type | Examples |
|
||||
|------|----------|
|
||||
| Pageviews | Automatic, enhanced with metadata |
|
||||
| User Actions | Button clicks, form submissions, feature usage |
|
||||
| System Events | Signup completed, purchase, subscription changed |
|
||||
| Custom Conversions | Goal completions, funnel stages |
|
||||
| Type | Examples |
|
||||
| ------------------ | ------------------------------------------------ |
|
||||
| Pageviews | Automatic, enhanced with metadata |
|
||||
| User Actions | Button clicks, form submissions, feature usage |
|
||||
| System Events | Signup completed, purchase, subscription changed |
|
||||
| Custom Conversions | Goal completions, funnel stages |
|
||||
|
||||
**For comprehensive event lists**: See [references/event-library.md](references/event-library.md)
|
||||
|
||||
@@ -80,6 +84,7 @@ checkout_payment_completed
|
||||
```
|
||||
|
||||
### Best Practices
|
||||
|
||||
- Lowercase with underscores
|
||||
- Be specific: `cta_hero_clicked` vs. `button_clicked`
|
||||
- Include context in properties, not event name
|
||||
@@ -92,21 +97,21 @@ checkout_payment_completed
|
||||
|
||||
### Marketing Site
|
||||
|
||||
| Event | Properties |
|
||||
|-------|------------|
|
||||
| cta_clicked | button_text, location |
|
||||
| form_submitted | form_type |
|
||||
| signup_completed | method, source |
|
||||
| demo_requested | - |
|
||||
| Event | Properties |
|
||||
| ---------------- | --------------------- |
|
||||
| cta_clicked | button_text, location |
|
||||
| form_submitted | form_type |
|
||||
| signup_completed | method, source |
|
||||
| demo_requested | - |
|
||||
|
||||
### Product/App
|
||||
|
||||
| Event | Properties |
|
||||
|-------|------------|
|
||||
| Event | Properties |
|
||||
| ------------------------- | ---------------------- |
|
||||
| onboarding_step_completed | step_number, step_name |
|
||||
| feature_used | feature_name |
|
||||
| purchase_completed | plan, value |
|
||||
| subscription_cancelled | reason |
|
||||
| feature_used | feature_name |
|
||||
| purchase_completed | plan, value |
|
||||
| subscription_cancelled | reason |
|
||||
|
||||
**For full event library by business type**: See [references/event-library.md](references/event-library.md)
|
||||
|
||||
@@ -116,14 +121,15 @@ checkout_payment_completed
|
||||
|
||||
### Standard Properties
|
||||
|
||||
| Category | Properties |
|
||||
|----------|------------|
|
||||
| Page | page_title, page_location, page_referrer |
|
||||
| User | user_id, user_type, account_id, plan_type |
|
||||
| Campaign | source, medium, campaign, content, term |
|
||||
| Product | product_id, product_name, category, price |
|
||||
| Category | Properties |
|
||||
| -------- | ----------------------------------------- |
|
||||
| Page | page_title, page_location, page_referrer |
|
||||
| User | user_id, user_type, account_id, plan_type |
|
||||
| Campaign | source, medium, campaign, content, term |
|
||||
| Product | product_id, product_name, category, price |
|
||||
|
||||
### Best Practices
|
||||
|
||||
- Use consistent property names
|
||||
- Include relevant context
|
||||
- Don't duplicate automatic properties
|
||||
@@ -145,8 +151,8 @@ checkout_payment_completed
|
||||
|
||||
```javascript
|
||||
gtag('event', 'signup_completed', {
|
||||
'method': 'email',
|
||||
'plan': 'free'
|
||||
method: 'email',
|
||||
plan: 'free',
|
||||
});
|
||||
```
|
||||
|
||||
@@ -158,19 +164,19 @@ gtag('event', 'signup_completed', {
|
||||
|
||||
### Container Structure
|
||||
|
||||
| Component | Purpose |
|
||||
|-----------|---------|
|
||||
| Tags | Code that executes (GA4, pixels) |
|
||||
| Triggers | When tags fire (page view, click) |
|
||||
| Component | Purpose |
|
||||
| --------- | --------------------------------------- |
|
||||
| Tags | Code that executes (GA4, pixels) |
|
||||
| Triggers | When tags fire (page view, click) |
|
||||
| Variables | Dynamic values (click text, data layer) |
|
||||
|
||||
### Data Layer Pattern
|
||||
|
||||
```javascript
|
||||
dataLayer.push({
|
||||
'event': 'form_submitted',
|
||||
'form_name': 'contact',
|
||||
'form_location': 'footer'
|
||||
event: 'form_submitted',
|
||||
form_name: 'contact',
|
||||
form_location: 'footer',
|
||||
});
|
||||
```
|
||||
|
||||
@@ -182,15 +188,16 @@ dataLayer.push({
|
||||
|
||||
### Standard Parameters
|
||||
|
||||
| Parameter | Purpose | Example |
|
||||
|-----------|---------|---------|
|
||||
| utm_source | Traffic source | google, newsletter |
|
||||
| utm_medium | Marketing medium | cpc, email, social |
|
||||
| utm_campaign | Campaign name | spring_sale |
|
||||
| utm_content | Differentiate versions | hero_cta |
|
||||
| utm_term | Paid search keywords | running+shoes |
|
||||
| Parameter | Purpose | Example |
|
||||
| ------------ | ---------------------- | ------------------ |
|
||||
| utm_source | Traffic source | google, newsletter |
|
||||
| utm_medium | Marketing medium | cpc, email, social |
|
||||
| utm_campaign | Campaign name | spring_sale |
|
||||
| utm_content | Differentiate versions | hero_cta |
|
||||
| utm_term | Paid search keywords | running+shoes |
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
- Lowercase everything
|
||||
- Use underscores or hyphens consistently
|
||||
- Be specific but concise: `blog_footer_cta`, not `cta1`
|
||||
@@ -202,10 +209,10 @@ dataLayer.push({
|
||||
|
||||
### Testing Tools
|
||||
|
||||
| Tool | Use For |
|
||||
|------|---------|
|
||||
| GA4 DebugView | Real-time event monitoring |
|
||||
| GTM Preview Mode | Test triggers before publish |
|
||||
| Tool | Use For |
|
||||
| ------------------ | ---------------------------------- |
|
||||
| GA4 DebugView | Real-time event monitoring |
|
||||
| GTM Preview Mode | Test triggers before publish |
|
||||
| Browser Extensions | Tag Assistant, dataLayer Inspector |
|
||||
|
||||
### Validation Checklist
|
||||
@@ -219,23 +226,25 @@ dataLayer.push({
|
||||
|
||||
### Common Issues
|
||||
|
||||
| Issue | Check |
|
||||
|-------|-------|
|
||||
| Events not firing | Trigger config, GTM loaded |
|
||||
| Wrong values | Variable path, data layer structure |
|
||||
| Duplicate events | Multiple containers, trigger firing twice |
|
||||
| Issue | Check |
|
||||
| ----------------- | ----------------------------------------- |
|
||||
| Events not firing | Trigger config, GTM loaded |
|
||||
| Wrong values | Variable path, data layer structure |
|
||||
| Duplicate events | Multiple containers, trigger firing twice |
|
||||
|
||||
---
|
||||
|
||||
## Privacy and Compliance
|
||||
|
||||
### Considerations
|
||||
|
||||
- Cookie consent required in EU/UK/CA
|
||||
- No PII in analytics properties
|
||||
- Data retention settings
|
||||
- User deletion capabilities
|
||||
|
||||
### Implementation
|
||||
|
||||
- Use consent mode (wait for consent)
|
||||
- IP anonymization
|
||||
- Only collect what you need
|
||||
@@ -251,26 +260,27 @@ dataLayer.push({
|
||||
# [Site/Product] Tracking Plan
|
||||
|
||||
## Overview
|
||||
|
||||
- Tools: GA4, GTM
|
||||
- Last updated: [Date]
|
||||
|
||||
## Events
|
||||
|
||||
| Event Name | Description | Properties | Trigger |
|
||||
|------------|-------------|------------|---------|
|
||||
| Event Name | Description | Properties | Trigger |
|
||||
| ---------------- | --------------------- | ------------ | ------------ |
|
||||
| signup_completed | User completes signup | method, plan | Success page |
|
||||
|
||||
## Custom Dimensions
|
||||
|
||||
| Name | Scope | Parameter |
|
||||
|------|-------|-----------|
|
||||
| user_type | User | user_type |
|
||||
| Name | Scope | Parameter |
|
||||
| --------- | ----- | --------- |
|
||||
| user_type | User | user_type |
|
||||
|
||||
## Conversions
|
||||
|
||||
| Conversion | Event | Counting |
|
||||
|------------|-------|----------|
|
||||
| Signup | signup_completed | Once per session |
|
||||
| Conversion | Event | Counting |
|
||||
| ---------- | ---------------- | ---------------- |
|
||||
| Signup | signup_completed | Once per session |
|
||||
```
|
||||
|
||||
---
|
||||
@@ -290,13 +300,13 @@ dataLayer.push({
|
||||
|
||||
For implementation, see the [tools registry](../../tools/REGISTRY.md). Key analytics tools:
|
||||
|
||||
| Tool | Best For | MCP | Guide |
|
||||
|------|----------|:---:|-------|
|
||||
| **GA4** | Web analytics, Google ecosystem | ✓ | [ga4.md](../../tools/integrations/ga4.md) |
|
||||
| **Mixpanel** | Product analytics, event tracking | - | [mixpanel.md](../../tools/integrations/mixpanel.md) |
|
||||
| **Amplitude** | Product analytics, cohort analysis | - | [amplitude.md](../../tools/integrations/amplitude.md) |
|
||||
| **PostHog** | Open-source analytics, session replay | - | [posthog.md](../../tools/integrations/posthog.md) |
|
||||
| **Segment** | Customer data platform, routing | - | [segment.md](../../tools/integrations/segment.md) |
|
||||
| Tool | Best For | MCP | Guide |
|
||||
| ------------- | ------------------------------------- | :-: | ----------------------------------------------------- |
|
||||
| **GA4** | Web analytics, Google ecosystem | ✓ | [ga4.md](../../tools/integrations/ga4.md) |
|
||||
| **Mixpanel** | Product analytics, event tracking | - | [mixpanel.md](../../tools/integrations/mixpanel.md) |
|
||||
| **Amplitude** | Product analytics, cohort analysis | - | [amplitude.md](../../tools/integrations/amplitude.md) |
|
||||
| **PostHog** | Open-source analytics, session replay | - | [posthog.md](../../tools/integrations/posthog.md) |
|
||||
| **Segment** | Customer data platform, routing | - | [segment.md](../../tools/integrations/segment.md) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -6,36 +6,36 @@ Comprehensive list of events to track by business type and context.
|
||||
|
||||
### Navigation & Engagement
|
||||
|
||||
| Event Name | Description | Properties |
|
||||
|------------|-------------|------------|
|
||||
| page_view | Page loaded (enhanced) | page_title, page_location, content_group |
|
||||
| scroll_depth | User scrolled to threshold | depth (25, 50, 75, 100) |
|
||||
| outbound_link_clicked | Click to external site | link_url, link_text |
|
||||
| internal_link_clicked | Click within site | link_url, link_text, location |
|
||||
| video_played | Video started | video_id, video_title, duration |
|
||||
| video_completed | Video finished | video_id, video_title, duration |
|
||||
| Event Name | Description | Properties |
|
||||
| --------------------- | -------------------------- | ---------------------------------------- |
|
||||
| page_view | Page loaded (enhanced) | page_title, page_location, content_group |
|
||||
| scroll_depth | User scrolled to threshold | depth (25, 50, 75, 100) |
|
||||
| outbound_link_clicked | Click to external site | link_url, link_text |
|
||||
| internal_link_clicked | Click within site | link_url, link_text, location |
|
||||
| video_played | Video started | video_id, video_title, duration |
|
||||
| video_completed | Video finished | video_id, video_title, duration |
|
||||
|
||||
### CTA & Form Interactions
|
||||
|
||||
| Event Name | Description | Properties |
|
||||
|------------|-------------|------------|
|
||||
| cta_clicked | Call to action clicked | button_text, cta_location, page |
|
||||
| form_started | User began form | form_name, form_location |
|
||||
| form_field_completed | Field filled | form_name, field_name |
|
||||
| form_submitted | Form successfully sent | form_name, form_location |
|
||||
| form_error | Form validation failed | form_name, error_type |
|
||||
| resource_downloaded | Asset downloaded | resource_name, resource_type |
|
||||
| Event Name | Description | Properties |
|
||||
| -------------------- | ---------------------- | ------------------------------- |
|
||||
| cta_clicked | Call to action clicked | button_text, cta_location, page |
|
||||
| form_started | User began form | form_name, form_location |
|
||||
| form_field_completed | Field filled | form_name, field_name |
|
||||
| form_submitted | Form successfully sent | form_name, form_location |
|
||||
| form_error | Form validation failed | form_name, error_type |
|
||||
| resource_downloaded | Asset downloaded | resource_name, resource_type |
|
||||
|
||||
### Conversion Events
|
||||
|
||||
| Event Name | Description | Properties |
|
||||
|------------|-------------|------------|
|
||||
| signup_started | Initiated signup | source, page |
|
||||
| signup_completed | Finished signup | method, plan, source |
|
||||
| demo_requested | Demo form submitted | company_size, industry |
|
||||
| contact_submitted | Contact form sent | inquiry_type |
|
||||
| newsletter_subscribed | Email list signup | source, list_name |
|
||||
| trial_started | Free trial began | plan, source |
|
||||
| Event Name | Description | Properties |
|
||||
| --------------------- | ------------------- | ---------------------- |
|
||||
| signup_started | Initiated signup | source, page |
|
||||
| signup_completed | Finished signup | method, plan, source |
|
||||
| demo_requested | Demo form submitted | company_size, industry |
|
||||
| contact_submitted | Contact form sent | inquiry_type |
|
||||
| newsletter_subscribed | Email list signup | source, list_name |
|
||||
| trial_started | Free trial began | plan, source |
|
||||
|
||||
---
|
||||
|
||||
@@ -43,37 +43,37 @@ Comprehensive list of events to track by business type and context.
|
||||
|
||||
### Onboarding
|
||||
|
||||
| Event Name | Description | Properties |
|
||||
|------------|-------------|------------|
|
||||
| signup_completed | Account created | method, referral_source |
|
||||
| onboarding_started | Began onboarding | - |
|
||||
| onboarding_step_completed | Step finished | step_number, step_name |
|
||||
| onboarding_completed | All steps done | steps_completed, time_to_complete |
|
||||
| onboarding_skipped | User skipped onboarding | step_skipped_at |
|
||||
| first_key_action_completed | Aha moment reached | action_type |
|
||||
| Event Name | Description | Properties |
|
||||
| -------------------------- | ----------------------- | --------------------------------- |
|
||||
| signup_completed | Account created | method, referral_source |
|
||||
| onboarding_started | Began onboarding | - |
|
||||
| onboarding_step_completed | Step finished | step_number, step_name |
|
||||
| onboarding_completed | All steps done | steps_completed, time_to_complete |
|
||||
| onboarding_skipped | User skipped onboarding | step_skipped_at |
|
||||
| first_key_action_completed | Aha moment reached | action_type |
|
||||
|
||||
### Core Usage
|
||||
|
||||
| Event Name | Description | Properties |
|
||||
|------------|-------------|------------|
|
||||
| session_started | App session began | session_number |
|
||||
| feature_used | Feature interaction | feature_name, feature_category |
|
||||
| action_completed | Core action done | action_type, count |
|
||||
| content_created | User created content | content_type |
|
||||
| content_edited | User modified content | content_type |
|
||||
| content_deleted | User removed content | content_type |
|
||||
| search_performed | In-app search | query, results_count |
|
||||
| settings_changed | Settings modified | setting_name, new_value |
|
||||
| invite_sent | User invited others | invite_type, count |
|
||||
| Event Name | Description | Properties |
|
||||
| ---------------- | --------------------- | ------------------------------ |
|
||||
| session_started | App session began | session_number |
|
||||
| feature_used | Feature interaction | feature_name, feature_category |
|
||||
| action_completed | Core action done | action_type, count |
|
||||
| content_created | User created content | content_type |
|
||||
| content_edited | User modified content | content_type |
|
||||
| content_deleted | User removed content | content_type |
|
||||
| search_performed | In-app search | query, results_count |
|
||||
| settings_changed | Settings modified | setting_name, new_value |
|
||||
| invite_sent | User invited others | invite_type, count |
|
||||
|
||||
### Errors & Support
|
||||
|
||||
| Event Name | Description | Properties |
|
||||
|------------|-------------|------------|
|
||||
| error_occurred | Error experienced | error_type, error_message, page |
|
||||
| help_opened | Help accessed | help_type, page |
|
||||
| support_contacted | Support request made | contact_method, issue_type |
|
||||
| feedback_submitted | User feedback given | feedback_type, rating |
|
||||
| Event Name | Description | Properties |
|
||||
| ------------------ | -------------------- | ------------------------------- |
|
||||
| error_occurred | Error experienced | error_type, error_message, page |
|
||||
| help_opened | Help accessed | help_type, page |
|
||||
| support_contacted | Support request made | contact_method, issue_type |
|
||||
| feedback_submitted | User feedback given | feedback_type, rating |
|
||||
|
||||
---
|
||||
|
||||
@@ -81,26 +81,26 @@ Comprehensive list of events to track by business type and context.
|
||||
|
||||
### Pricing & Checkout
|
||||
|
||||
| Event Name | Description | Properties |
|
||||
|------------|-------------|------------|
|
||||
| pricing_viewed | Pricing page seen | source |
|
||||
| plan_selected | Plan chosen | plan_name, billing_cycle |
|
||||
| checkout_started | Began checkout | plan, value |
|
||||
| payment_info_entered | Payment submitted | payment_method |
|
||||
| purchase_completed | Purchase successful | plan, value, currency, transaction_id |
|
||||
| purchase_failed | Purchase failed | error_reason, plan |
|
||||
| Event Name | Description | Properties |
|
||||
| -------------------- | ------------------- | ------------------------------------- |
|
||||
| pricing_viewed | Pricing page seen | source |
|
||||
| plan_selected | Plan chosen | plan_name, billing_cycle |
|
||||
| checkout_started | Began checkout | plan, value |
|
||||
| payment_info_entered | Payment submitted | payment_method |
|
||||
| purchase_completed | Purchase successful | plan, value, currency, transaction_id |
|
||||
| purchase_failed | Purchase failed | error_reason, plan |
|
||||
|
||||
### Subscription Management
|
||||
|
||||
| Event Name | Description | Properties |
|
||||
|------------|-------------|------------|
|
||||
| trial_started | Trial began | plan, trial_length |
|
||||
| trial_ended | Trial expired | plan, converted (bool) |
|
||||
| subscription_upgraded | Plan upgraded | from_plan, to_plan, value |
|
||||
| subscription_downgraded | Plan downgraded | from_plan, to_plan |
|
||||
| subscription_cancelled | Cancelled | plan, reason, tenure |
|
||||
| subscription_renewed | Renewed | plan, value |
|
||||
| billing_updated | Payment method changed | - |
|
||||
| Event Name | Description | Properties |
|
||||
| ----------------------- | ---------------------- | ------------------------- |
|
||||
| trial_started | Trial began | plan, trial_length |
|
||||
| trial_ended | Trial expired | plan, converted (bool) |
|
||||
| subscription_upgraded | Plan upgraded | from_plan, to_plan, value |
|
||||
| subscription_downgraded | Plan downgraded | from_plan, to_plan |
|
||||
| subscription_cancelled | Cancelled | plan, reason, tenure |
|
||||
| subscription_renewed | Renewed | plan, value |
|
||||
| billing_updated | Payment method changed | - |
|
||||
|
||||
---
|
||||
|
||||
@@ -108,41 +108,41 @@ Comprehensive list of events to track by business type and context.
|
||||
|
||||
### Browsing
|
||||
|
||||
| Event Name | Description | Properties |
|
||||
|------------|-------------|------------|
|
||||
| product_viewed | Product page viewed | product_id, product_name, category, price |
|
||||
| product_list_viewed | Category/list viewed | list_name, products[] |
|
||||
| product_searched | Search performed | query, results_count |
|
||||
| product_filtered | Filters applied | filter_type, filter_value |
|
||||
| product_sorted | Sort applied | sort_by, sort_order |
|
||||
| Event Name | Description | Properties |
|
||||
| ------------------- | -------------------- | ----------------------------------------- |
|
||||
| product_viewed | Product page viewed | product_id, product_name, category, price |
|
||||
| product_list_viewed | Category/list viewed | list_name, products[] |
|
||||
| product_searched | Search performed | query, results_count |
|
||||
| product_filtered | Filters applied | filter_type, filter_value |
|
||||
| product_sorted | Sort applied | sort_by, sort_order |
|
||||
|
||||
### Cart
|
||||
|
||||
| Event Name | Description | Properties |
|
||||
|------------|-------------|------------|
|
||||
| product_added_to_cart | Item added | product_id, product_name, price, quantity |
|
||||
| product_removed_from_cart | Item removed | product_id, product_name, price, quantity |
|
||||
| cart_viewed | Cart page viewed | cart_value, items_count |
|
||||
| Event Name | Description | Properties |
|
||||
| ------------------------- | ---------------- | ----------------------------------------- |
|
||||
| product_added_to_cart | Item added | product_id, product_name, price, quantity |
|
||||
| product_removed_from_cart | Item removed | product_id, product_name, price, quantity |
|
||||
| cart_viewed | Cart page viewed | cart_value, items_count |
|
||||
|
||||
### Checkout
|
||||
|
||||
| Event Name | Description | Properties |
|
||||
|------------|-------------|------------|
|
||||
| checkout_started | Checkout began | cart_value, items_count |
|
||||
| checkout_step_completed | Step finished | step_number, step_name |
|
||||
| shipping_info_entered | Address entered | shipping_method |
|
||||
| payment_info_entered | Payment entered | payment_method |
|
||||
| coupon_applied | Coupon used | coupon_code, discount_value |
|
||||
| purchase_completed | Order placed | transaction_id, value, currency, items[] |
|
||||
| Event Name | Description | Properties |
|
||||
| ----------------------- | --------------- | ---------------------------------------- |
|
||||
| checkout_started | Checkout began | cart_value, items_count |
|
||||
| checkout_step_completed | Step finished | step_number, step_name |
|
||||
| shipping_info_entered | Address entered | shipping_method |
|
||||
| payment_info_entered | Payment entered | payment_method |
|
||||
| coupon_applied | Coupon used | coupon_code, discount_value |
|
||||
| purchase_completed | Order placed | transaction_id, value, currency, items[] |
|
||||
|
||||
### Post-Purchase
|
||||
|
||||
| Event Name | Description | Properties |
|
||||
|------------|-------------|------------|
|
||||
| order_confirmed | Confirmation viewed | transaction_id |
|
||||
| refund_requested | Refund initiated | transaction_id, reason |
|
||||
| refund_completed | Refund processed | transaction_id, value |
|
||||
| review_submitted | Product reviewed | product_id, rating |
|
||||
| Event Name | Description | Properties |
|
||||
| ---------------- | ------------------- | ---------------------- |
|
||||
| order_confirmed | Confirmation viewed | transaction_id |
|
||||
| refund_requested | Refund initiated | transaction_id, reason |
|
||||
| refund_completed | Refund processed | transaction_id, value |
|
||||
| review_submitted | Product reviewed | product_id, rating |
|
||||
|
||||
---
|
||||
|
||||
@@ -150,30 +150,30 @@ Comprehensive list of events to track by business type and context.
|
||||
|
||||
### Team & Collaboration
|
||||
|
||||
| Event Name | Description | Properties |
|
||||
|------------|-------------|------------|
|
||||
| team_created | New team/org made | team_size, plan |
|
||||
| team_member_invited | Invite sent | role, invite_method |
|
||||
| team_member_joined | Member accepted | role |
|
||||
| team_member_removed | Member removed | role |
|
||||
| role_changed | Permissions updated | user_id, old_role, new_role |
|
||||
| Event Name | Description | Properties |
|
||||
| ------------------- | ------------------- | --------------------------- |
|
||||
| team_created | New team/org made | team_size, plan |
|
||||
| team_member_invited | Invite sent | role, invite_method |
|
||||
| team_member_joined | Member accepted | role |
|
||||
| team_member_removed | Member removed | role |
|
||||
| role_changed | Permissions updated | user_id, old_role, new_role |
|
||||
|
||||
### Integration Events
|
||||
|
||||
| Event Name | Description | Properties |
|
||||
|------------|-------------|------------|
|
||||
| integration_viewed | Integration page seen | integration_name |
|
||||
| integration_started | Setup began | integration_name |
|
||||
| integration_connected | Successfully connected | integration_name |
|
||||
| integration_disconnected | Removed integration | integration_name, reason |
|
||||
| Event Name | Description | Properties |
|
||||
| ------------------------ | ---------------------- | ------------------------ |
|
||||
| integration_viewed | Integration page seen | integration_name |
|
||||
| integration_started | Setup began | integration_name |
|
||||
| integration_connected | Successfully connected | integration_name |
|
||||
| integration_disconnected | Removed integration | integration_name, reason |
|
||||
|
||||
### Account Events
|
||||
|
||||
| Event Name | Description | Properties |
|
||||
|------------|-------------|------------|
|
||||
| account_created | New account | source, plan |
|
||||
| account_upgraded | Plan upgrade | from_plan, to_plan |
|
||||
| account_churned | Account closed | reason, tenure, mrr_lost |
|
||||
| Event Name | Description | Properties |
|
||||
| ------------------- | ----------------- | ------------------------- |
|
||||
| account_created | New account | source, plan |
|
||||
| account_upgraded | Plan upgrade | from_plan, to_plan |
|
||||
| account_churned | Account closed | reason, tenure, mrr_lost |
|
||||
| account_reactivated | Returned customer | previous_tenure, new_plan |
|
||||
|
||||
---
|
||||
@@ -183,6 +183,7 @@ Comprehensive list of events to track by business type and context.
|
||||
### Standard Properties to Include
|
||||
|
||||
**User Context:**
|
||||
|
||||
```
|
||||
user_id: "12345"
|
||||
user_type: "free" | "trial" | "paid"
|
||||
@@ -191,6 +192,7 @@ plan_type: "starter" | "pro" | "enterprise"
|
||||
```
|
||||
|
||||
**Session Context:**
|
||||
|
||||
```
|
||||
session_id: "sess_abc"
|
||||
session_number: 5
|
||||
@@ -199,6 +201,7 @@ referrer: "https://google.com"
|
||||
```
|
||||
|
||||
**Campaign Context:**
|
||||
|
||||
```
|
||||
source: "google"
|
||||
medium: "cpc"
|
||||
@@ -207,6 +210,7 @@ content: "hero_cta"
|
||||
```
|
||||
|
||||
**Product Context (E-commerce):**
|
||||
|
||||
```
|
||||
product_id: "SKU123"
|
||||
product_name: "Product Name"
|
||||
@@ -217,6 +221,7 @@ currency: "USD"
|
||||
```
|
||||
|
||||
**Timing:**
|
||||
|
||||
```
|
||||
timestamp: "2024-01-15T10:30:00Z"
|
||||
time_on_page: 45
|
||||
@@ -228,6 +233,7 @@ session_duration: 300
|
||||
## Funnel Event Sequences
|
||||
|
||||
### Signup Funnel
|
||||
|
||||
1. signup_started
|
||||
2. signup_step_completed (email)
|
||||
3. signup_step_completed (password)
|
||||
@@ -235,6 +241,7 @@ session_duration: 300
|
||||
5. onboarding_started
|
||||
|
||||
### Purchase Funnel
|
||||
|
||||
1. pricing_viewed
|
||||
2. plan_selected
|
||||
3. checkout_started
|
||||
@@ -242,6 +249,7 @@ session_duration: 300
|
||||
5. purchase_completed
|
||||
|
||||
### E-commerce Funnel
|
||||
|
||||
1. product_viewed
|
||||
2. product_added_to_cart
|
||||
3. cart_viewed
|
||||
|
||||
+67
-48
@@ -13,25 +13,27 @@ Detailed implementation guide for Google Analytics 4.
|
||||
|
||||
### Enhanced Measurement Events (Automatic)
|
||||
|
||||
| Event | Description | Configuration |
|
||||
|-------|-------------|---------------|
|
||||
| page_view | Page loads | Automatic |
|
||||
| scroll | 90% scroll depth | Toggle on/off |
|
||||
| outbound_click | Click to external domain | Automatic |
|
||||
| site_search | Search query used | Configure parameter |
|
||||
| video_engagement | YouTube video plays | Toggle on/off |
|
||||
| file_download | PDF, docs, etc. | Configurable extensions |
|
||||
| Event | Description | Configuration |
|
||||
| ---------------- | ------------------------ | ----------------------- |
|
||||
| page_view | Page loads | Automatic |
|
||||
| scroll | 90% scroll depth | Toggle on/off |
|
||||
| outbound_click | Click to external domain | Automatic |
|
||||
| site_search | Search query used | Configure parameter |
|
||||
| video_engagement | YouTube video plays | Toggle on/off |
|
||||
| file_download | PDF, docs, etc. | Configurable extensions |
|
||||
|
||||
### Recommended Events
|
||||
|
||||
Use Google's predefined events when possible for enhanced reporting:
|
||||
|
||||
**All properties:**
|
||||
|
||||
- login, sign_up
|
||||
- share
|
||||
- search
|
||||
|
||||
**E-commerce:**
|
||||
|
||||
- view_item, view_item_list
|
||||
- add_to_cart, remove_from_cart
|
||||
- begin_checkout
|
||||
@@ -39,6 +41,7 @@ Use Google's predefined events when possible for enhanced reporting:
|
||||
- purchase, refund
|
||||
|
||||
**Games:**
|
||||
|
||||
- level_up, unlock_achievement
|
||||
- post_score, spend_virtual_currency
|
||||
|
||||
@@ -53,31 +56,33 @@ Reference: https://support.google.com/analytics/answer/9267735
|
||||
```javascript
|
||||
// Basic event
|
||||
gtag('event', 'signup_completed', {
|
||||
'method': 'email',
|
||||
'plan': 'free'
|
||||
method: 'email',
|
||||
plan: 'free',
|
||||
});
|
||||
|
||||
// Event with value
|
||||
gtag('event', 'purchase', {
|
||||
'transaction_id': 'T12345',
|
||||
'value': 99.99,
|
||||
'currency': 'USD',
|
||||
'items': [{
|
||||
'item_id': 'SKU123',
|
||||
'item_name': 'Product Name',
|
||||
'price': 99.99
|
||||
}]
|
||||
transaction_id: 'T12345',
|
||||
value: 99.99,
|
||||
currency: 'USD',
|
||||
items: [
|
||||
{
|
||||
item_id: 'SKU123',
|
||||
item_name: 'Product Name',
|
||||
price: 99.99,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// User properties
|
||||
gtag('set', 'user_properties', {
|
||||
'user_type': 'premium',
|
||||
'plan_name': 'pro'
|
||||
user_type: 'premium',
|
||||
plan_name: 'pro',
|
||||
});
|
||||
|
||||
// User ID (for logged-in users)
|
||||
gtag('config', 'GA_MEASUREMENT_ID', {
|
||||
'user_id': 'USER_ID'
|
||||
user_id: 'USER_ID',
|
||||
});
|
||||
```
|
||||
|
||||
@@ -86,40 +91,42 @@ gtag('config', 'GA_MEASUREMENT_ID', {
|
||||
```javascript
|
||||
// Custom event
|
||||
dataLayer.push({
|
||||
'event': 'signup_completed',
|
||||
'method': 'email',
|
||||
'plan': 'free'
|
||||
event: 'signup_completed',
|
||||
method: 'email',
|
||||
plan: 'free',
|
||||
});
|
||||
|
||||
// Set user properties
|
||||
dataLayer.push({
|
||||
'user_id': '12345',
|
||||
'user_type': 'premium'
|
||||
user_id: '12345',
|
||||
user_type: 'premium',
|
||||
});
|
||||
|
||||
// E-commerce purchase
|
||||
dataLayer.push({
|
||||
'event': 'purchase',
|
||||
'ecommerce': {
|
||||
'transaction_id': 'T12345',
|
||||
'value': 99.99,
|
||||
'currency': 'USD',
|
||||
'items': [{
|
||||
'item_id': 'SKU123',
|
||||
'item_name': 'Product Name',
|
||||
'price': 99.99,
|
||||
'quantity': 1
|
||||
}]
|
||||
}
|
||||
event: 'purchase',
|
||||
ecommerce: {
|
||||
transaction_id: 'T12345',
|
||||
value: 99.99,
|
||||
currency: 'USD',
|
||||
items: [
|
||||
{
|
||||
item_id: 'SKU123',
|
||||
item_name: 'Product Name',
|
||||
price: 99.99,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
// Clear ecommerce before sending (best practice)
|
||||
dataLayer.push({ ecommerce: null });
|
||||
dataLayer.push({
|
||||
'event': 'view_item',
|
||||
'ecommerce': {
|
||||
event: 'view_item',
|
||||
ecommerce: {
|
||||
// ...
|
||||
}
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -141,8 +148,8 @@ dataLayer.push({
|
||||
```javascript
|
||||
// Event with conversion value
|
||||
gtag('event', 'purchase', {
|
||||
'value': 99.99,
|
||||
'currency': 'USD'
|
||||
value: 99.99,
|
||||
currency: 'USD',
|
||||
});
|
||||
```
|
||||
|
||||
@@ -155,11 +162,13 @@ Or set default value in GA4 Admin when marking conversion.
|
||||
### When to Use
|
||||
|
||||
**Custom dimensions:**
|
||||
|
||||
- Properties you want to segment/filter by
|
||||
- User attributes (plan type, industry)
|
||||
- Content attributes (author, category)
|
||||
|
||||
**Custom metrics:**
|
||||
|
||||
- Numeric values to aggregate
|
||||
- Scores, counts, durations
|
||||
|
||||
@@ -175,11 +184,11 @@ Or set default value in GA4 Admin when marking conversion.
|
||||
|
||||
### Examples
|
||||
|
||||
| Dimension | Scope | Parameter | Description |
|
||||
|-----------|-------|-----------|-------------|
|
||||
| User Type | User | user_type | Free, trial, paid |
|
||||
| Content Author | Event | author | Blog post author |
|
||||
| Product Category | Item | item_category | E-commerce category |
|
||||
| Dimension | Scope | Parameter | Description |
|
||||
| ---------------- | ----- | ------------- | ------------------- |
|
||||
| User Type | User | user_type | Free, trial, paid |
|
||||
| Content Author | Event | author | Blog post author |
|
||||
| Product Category | Item | item_category | E-commerce category |
|
||||
|
||||
---
|
||||
|
||||
@@ -190,6 +199,7 @@ Or set default value in GA4 Admin when marking conversion.
|
||||
Admin > Data display > Audiences
|
||||
|
||||
**Use cases:**
|
||||
|
||||
- Remarketing audiences (export to Ads)
|
||||
- Segment analysis
|
||||
- Trigger-based events
|
||||
@@ -197,15 +207,18 @@ Admin > Data display > Audiences
|
||||
### Audience Examples
|
||||
|
||||
**High-intent visitors:**
|
||||
|
||||
- Viewed pricing page
|
||||
- Did not convert
|
||||
- In last 7 days
|
||||
|
||||
**Engaged users:**
|
||||
|
||||
- 3+ sessions
|
||||
- Or 5+ minutes total engagement
|
||||
|
||||
**Purchasers:**
|
||||
|
||||
- Purchase event
|
||||
- For exclusion or lookalike
|
||||
|
||||
@@ -216,6 +229,7 @@ Admin > Data display > Audiences
|
||||
### DebugView
|
||||
|
||||
Enable with:
|
||||
|
||||
- URL parameter: `?debug_mode=true`
|
||||
- Chrome extension: GA Debugger
|
||||
- gtag: `'debug_mode': true` in config
|
||||
@@ -230,16 +244,19 @@ Reports > Real-time
|
||||
### Common Issues
|
||||
|
||||
**Events not appearing:**
|
||||
|
||||
- Check DebugView first
|
||||
- Verify gtag/GTM firing
|
||||
- Check filter exclusions
|
||||
|
||||
**Parameter values missing:**
|
||||
|
||||
- Custom dimension not created
|
||||
- Parameter name mismatch
|
||||
- Data still processing (24-48 hrs)
|
||||
|
||||
**Conversions not recording:**
|
||||
|
||||
- Event not marked as conversion
|
||||
- Event name doesn't match
|
||||
- Counting method (once vs. every)
|
||||
@@ -253,6 +270,7 @@ Reports > Real-time
|
||||
Admin > Data streams > [Stream] > Configure tag settings > Define internal traffic
|
||||
|
||||
**Exclude:**
|
||||
|
||||
- Internal IP addresses
|
||||
- Developer traffic
|
||||
- Testing environments
|
||||
@@ -285,6 +303,7 @@ Admin > Data streams > [Stream] > Configure tag settings
|
||||
### Audience Export
|
||||
|
||||
Audiences created in GA4 can be used in Google Ads for:
|
||||
|
||||
- Remarketing campaigns
|
||||
- Customer match
|
||||
- Similar audiences
|
||||
|
||||
+94
-64
@@ -9,6 +9,7 @@ Detailed guide for implementing tracking via Google Tag Manager.
|
||||
Tags are code snippets that execute when triggered.
|
||||
|
||||
**Common tag types:**
|
||||
|
||||
- GA4 Configuration (base setup)
|
||||
- GA4 Event (custom events)
|
||||
- Google Ads Conversion
|
||||
@@ -21,6 +22,7 @@ Tags are code snippets that execute when triggered.
|
||||
Triggers define when tags fire.
|
||||
|
||||
**Built-in triggers:**
|
||||
|
||||
- Page View: All Pages, DOM Ready, Window Loaded
|
||||
- Click: All Elements, Just Links
|
||||
- Form Submission
|
||||
@@ -29,6 +31,7 @@ Triggers define when tags fire.
|
||||
- Element Visibility
|
||||
|
||||
**Custom triggers:**
|
||||
|
||||
- Custom Event (from dataLayer)
|
||||
- Trigger Groups (multiple conditions)
|
||||
|
||||
@@ -37,12 +40,14 @@ Triggers define when tags fire.
|
||||
Variables capture dynamic values.
|
||||
|
||||
**Built-in (enable as needed):**
|
||||
|
||||
- Click Text, Click URL, Click ID, Click Classes
|
||||
- Page Path, Page URL, Page Hostname
|
||||
- Referrer
|
||||
- Form Element, Form ID
|
||||
|
||||
**User-defined:**
|
||||
|
||||
- Data Layer variables
|
||||
- JavaScript variables
|
||||
- Lookup tables
|
||||
@@ -88,9 +93,9 @@ window.dataLayer = window.dataLayer || [];
|
||||
|
||||
// Push event
|
||||
dataLayer.push({
|
||||
'event': 'event_name',
|
||||
'property1': 'value1',
|
||||
'property2': 'value2'
|
||||
event: 'event_name',
|
||||
property1: 'value1',
|
||||
property2: 'value2',
|
||||
});
|
||||
```
|
||||
|
||||
@@ -100,24 +105,24 @@ dataLayer.push({
|
||||
// Set on page load (before GTM container)
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
dataLayer.push({
|
||||
'pageType': 'product',
|
||||
'contentGroup': 'products',
|
||||
'user': {
|
||||
'loggedIn': true,
|
||||
'userId': '12345',
|
||||
'userType': 'premium'
|
||||
}
|
||||
pageType: 'product',
|
||||
contentGroup: 'products',
|
||||
user: {
|
||||
loggedIn: true,
|
||||
userId: '12345',
|
||||
userType: 'premium',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Form Submission
|
||||
|
||||
```javascript
|
||||
document.querySelector('#contact-form').addEventListener('submit', function() {
|
||||
document.querySelector('#contact-form').addEventListener('submit', function () {
|
||||
dataLayer.push({
|
||||
'event': 'form_submitted',
|
||||
'formName': 'contact',
|
||||
'formLocation': 'footer'
|
||||
event: 'form_submitted',
|
||||
formName: 'contact',
|
||||
formLocation: 'footer',
|
||||
});
|
||||
});
|
||||
```
|
||||
@@ -125,11 +130,11 @@ document.querySelector('#contact-form').addEventListener('submit', function() {
|
||||
### Button Click
|
||||
|
||||
```javascript
|
||||
document.querySelector('.cta-button').addEventListener('click', function() {
|
||||
document.querySelector('.cta-button').addEventListener('click', function () {
|
||||
dataLayer.push({
|
||||
'event': 'cta_clicked',
|
||||
'ctaText': this.innerText,
|
||||
'ctaLocation': 'hero'
|
||||
event: 'cta_clicked',
|
||||
ctaText: this.innerText,
|
||||
ctaLocation: 'hero',
|
||||
});
|
||||
});
|
||||
```
|
||||
@@ -140,49 +145,55 @@ document.querySelector('.cta-button').addEventListener('click', function() {
|
||||
// Product view
|
||||
dataLayer.push({ ecommerce: null }); // Clear previous
|
||||
dataLayer.push({
|
||||
'event': 'view_item',
|
||||
'ecommerce': {
|
||||
'items': [{
|
||||
'item_id': 'SKU123',
|
||||
'item_name': 'Product Name',
|
||||
'price': 99.99,
|
||||
'item_category': 'Category',
|
||||
'quantity': 1
|
||||
}]
|
||||
}
|
||||
event: 'view_item',
|
||||
ecommerce: {
|
||||
items: [
|
||||
{
|
||||
item_id: 'SKU123',
|
||||
item_name: 'Product Name',
|
||||
price: 99.99,
|
||||
item_category: 'Category',
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
// Add to cart
|
||||
dataLayer.push({ ecommerce: null });
|
||||
dataLayer.push({
|
||||
'event': 'add_to_cart',
|
||||
'ecommerce': {
|
||||
'items': [{
|
||||
'item_id': 'SKU123',
|
||||
'item_name': 'Product Name',
|
||||
'price': 99.99,
|
||||
'quantity': 1
|
||||
}]
|
||||
}
|
||||
event: 'add_to_cart',
|
||||
ecommerce: {
|
||||
items: [
|
||||
{
|
||||
item_id: 'SKU123',
|
||||
item_name: 'Product Name',
|
||||
price: 99.99,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
// Purchase
|
||||
dataLayer.push({ ecommerce: null });
|
||||
dataLayer.push({
|
||||
'event': 'purchase',
|
||||
'ecommerce': {
|
||||
'transaction_id': 'T12345',
|
||||
'value': 99.99,
|
||||
'currency': 'USD',
|
||||
'tax': 5.00,
|
||||
'shipping': 10.00,
|
||||
'items': [{
|
||||
'item_id': 'SKU123',
|
||||
'item_name': 'Product Name',
|
||||
'price': 99.99,
|
||||
'quantity': 1
|
||||
}]
|
||||
}
|
||||
event: 'purchase',
|
||||
ecommerce: {
|
||||
transaction_id: 'T12345',
|
||||
value: 99.99,
|
||||
currency: 'USD',
|
||||
tax: 5.0,
|
||||
shipping: 10.0,
|
||||
items: [
|
||||
{
|
||||
item_id: 'SKU123',
|
||||
item_name: 'Product Name',
|
||||
price: 99.99,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -195,6 +206,7 @@ dataLayer.push({
|
||||
**Tag Type:** Google Analytics: GA4 Configuration
|
||||
|
||||
**Settings:**
|
||||
|
||||
- Measurement ID: G-XXXXXXXX
|
||||
- Send page view: Checked (for pageviews)
|
||||
- User Properties: Add any user-level dimensions
|
||||
@@ -206,6 +218,7 @@ dataLayer.push({
|
||||
**Tag Type:** Google Analytics: GA4 Event
|
||||
|
||||
**Settings:**
|
||||
|
||||
- Configuration Tag: Select your config tag
|
||||
- Event Name: {{DL - event_name}} or hardcode
|
||||
- Event Parameters: Add parameters from dataLayer
|
||||
@@ -218,14 +231,22 @@ dataLayer.push({
|
||||
|
||||
```html
|
||||
<script>
|
||||
!function(f,b,e,v,n,t,s)
|
||||
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
|
||||
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
|
||||
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
|
||||
n.queue=[];t=b.createElement(e);t.async=!0;
|
||||
t.src=v;s=b.getElementsByTagName(e)[0];
|
||||
s.parentNode.insertBefore(t,s)}(window, document,'script',
|
||||
'https://connect.facebook.net/en_US/fbevents.js');
|
||||
!(function (f, b, e, v, n, t, s) {
|
||||
if (f.fbq) return;
|
||||
n = f.fbq = function () {
|
||||
n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments);
|
||||
};
|
||||
if (!f._fbq) f._fbq = n;
|
||||
n.push = n;
|
||||
n.loaded = !0;
|
||||
n.version = '2.0';
|
||||
n.queue = [];
|
||||
t = b.createElement(e);
|
||||
t.async = !0;
|
||||
t.src = v;
|
||||
s = b.getElementsByTagName(e)[0];
|
||||
s.parentNode.insertBefore(t, s);
|
||||
})(window, document, 'script', 'https://connect.facebook.net/en_US/fbevents.js');
|
||||
fbq('init', 'YOUR_PIXEL_ID');
|
||||
fbq('track', 'PageView');
|
||||
</script>
|
||||
@@ -240,7 +261,7 @@ dataLayer.push({
|
||||
```html
|
||||
<script>
|
||||
fbq('track', 'Lead', {
|
||||
content_name: '{{DL - form_name}}'
|
||||
content_name: '{{DL - form_name}}',
|
||||
});
|
||||
</script>
|
||||
```
|
||||
@@ -258,6 +279,7 @@ dataLayer.push({
|
||||
3. GTM debug panel opens at bottom
|
||||
|
||||
**What to check:**
|
||||
|
||||
- Tags fired on this event
|
||||
- Tags not fired (and why)
|
||||
- Variables and their values
|
||||
@@ -266,16 +288,19 @@ dataLayer.push({
|
||||
### Debug Tips
|
||||
|
||||
**Tag not firing:**
|
||||
|
||||
- Check trigger conditions
|
||||
- Verify data layer push
|
||||
- Check tag sequencing
|
||||
|
||||
**Wrong variable value:**
|
||||
|
||||
- Check data layer structure
|
||||
- Verify variable path (nested objects)
|
||||
- Check timing (data may not exist yet)
|
||||
|
||||
**Multiple firings:**
|
||||
|
||||
- Check trigger uniqueness
|
||||
- Look for duplicate tags
|
||||
- Check tag firing options
|
||||
@@ -287,6 +312,7 @@ dataLayer.push({
|
||||
### Workspaces
|
||||
|
||||
Use workspaces for team collaboration:
|
||||
|
||||
- Default workspace for production
|
||||
- Separate workspaces for large changes
|
||||
- Merge when ready
|
||||
@@ -294,12 +320,14 @@ Use workspaces for team collaboration:
|
||||
### Version Management
|
||||
|
||||
**Best practices:**
|
||||
|
||||
- Name every version descriptively
|
||||
- Add notes explaining changes
|
||||
- Review changes before publish
|
||||
- Keep production version noted
|
||||
|
||||
**Version notes example:**
|
||||
|
||||
```
|
||||
v15: Added purchase conversion tracking
|
||||
- New tag: GA4 - Event - Purchase
|
||||
@@ -317,15 +345,15 @@ v15: Added purchase conversion tracking
|
||||
```javascript
|
||||
// Default state (before consent)
|
||||
gtag('consent', 'default', {
|
||||
'analytics_storage': 'denied',
|
||||
'ad_storage': 'denied'
|
||||
analytics_storage: 'denied',
|
||||
ad_storage: 'denied',
|
||||
});
|
||||
|
||||
// Update on consent
|
||||
function grantConsent() {
|
||||
gtag('consent', 'update', {
|
||||
'analytics_storage': 'granted',
|
||||
'ad_storage': 'granted'
|
||||
analytics_storage: 'granted',
|
||||
ad_storage: 'granted',
|
||||
});
|
||||
}
|
||||
```
|
||||
@@ -346,6 +374,7 @@ function grantConsent() {
|
||||
Tag Configuration > Advanced Settings > Tag Sequencing
|
||||
|
||||
**Use cases:**
|
||||
|
||||
- Config tag before event tags
|
||||
- Pixel initialization before tracking
|
||||
- Cleanup after conversion
|
||||
@@ -353,6 +382,7 @@ Tag Configuration > Advanced Settings > Tag Sequencing
|
||||
### Exception Handling
|
||||
|
||||
**Trigger exceptions** - Prevent tag from firing:
|
||||
|
||||
- Exclude certain pages
|
||||
- Exclude internal traffic
|
||||
- Exclude during testing
|
||||
|
||||
@@ -3,7 +3,7 @@ name: antfu
|
||||
description: Anthony Fu's opinionated tooling and conventions for JavaScript/TypeScript projects. Use when setting up new projects, configuring ESLint/Prettier alternatives, monorepos, library publishing, or when the user mentions Anthony Fu's preferences.
|
||||
metadata:
|
||||
author: Anthony Fu
|
||||
version: "2026.02.03"
|
||||
version: '2026.02.03'
|
||||
---
|
||||
|
||||
## Coding Practices
|
||||
@@ -48,15 +48,15 @@ metadata:
|
||||
|
||||
### @antfu/ni Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `ni` | Install dependencies |
|
||||
| `ni <pkg>` / `ni -D <pkg>` | Add dependency / dev dependency |
|
||||
| `nr <script>` | Run script |
|
||||
| `nu` | Upgrade dependencies |
|
||||
| `nun <pkg>` | Uninstall dependency |
|
||||
| `nci` | Clean install (`pnpm i --frozen-lockfile`) |
|
||||
| `nlx <pkg>` | Execute package (`npx`) |
|
||||
| Command | Description |
|
||||
| -------------------------- | ------------------------------------------ |
|
||||
| `ni` | Install dependencies |
|
||||
| `ni <pkg>` / `ni -D <pkg>` | Add dependency / dev dependency |
|
||||
| `nr <script>` | Run script |
|
||||
| `nu` | Upgrade dependencies |
|
||||
| `nun <pkg>` | Uninstall dependency |
|
||||
| `nci` | Clean install (`pnpm i --frozen-lockfile`) |
|
||||
| `nlx <pkg>` | Execute package (`npx`) |
|
||||
|
||||
### TypeScript Config
|
||||
|
||||
@@ -80,12 +80,11 @@ metadata:
|
||||
|
||||
```js
|
||||
// eslint.config.mjs
|
||||
import antfu from '@antfu/eslint-config'
|
||||
import antfu from '@antfu/eslint-config';
|
||||
|
||||
export default antfu()
|
||||
export default antfu();
|
||||
```
|
||||
|
||||
|
||||
When completing tasks, run `pnpm run lint --fix` to format the code and fix coding style.
|
||||
|
||||
For detailed configuration options: [antfu-eslint-config](references/antfu-eslint-config.md)
|
||||
@@ -108,12 +107,12 @@ For detailed configuration options: [antfu-eslint-config](references/antfu-eslin
|
||||
|
||||
Use named catalogs in `pnpm-workspace.yaml` for version management:
|
||||
|
||||
| Catalog | Purpose |
|
||||
|---------|---------|
|
||||
| `prod` | Production dependencies |
|
||||
| `inlined` | Bundler-inlined dependencies |
|
||||
| `dev` | Dev tools (linter, bundler, testing) |
|
||||
| `frontend` | Frontend libraries |
|
||||
| Catalog | Purpose |
|
||||
| ---------- | ------------------------------------ |
|
||||
| `prod` | Production dependencies |
|
||||
| `inlined` | Bundler-inlined dependencies |
|
||||
| `dev` | Dev tools (linter, bundler, testing) |
|
||||
| `frontend` | Frontend libraries |
|
||||
|
||||
Avoid the default catalog. Catalog names can be adjusted per project needs.
|
||||
|
||||
@@ -121,10 +120,10 @@ Avoid the default catalog. Catalog names can be adjusted per project needs.
|
||||
|
||||
## References
|
||||
|
||||
| Topic | Description | Reference |
|
||||
|-------|-------------|-----------|
|
||||
| ESLint Config | Framework support, formatters, rule overrides, VS Code settings | [antfu-eslint-config](references/antfu-eslint-config.md) |
|
||||
| Project Setup | .gitignore, GitHub Actions, VS Code extensions | [setting-up](references/setting-up.md) |
|
||||
| App Development | Vue/Nuxt/UnoCSS conventions and patterns | [app-development](references/app-development.md) |
|
||||
| Library Development | tsdown bundling, pure ESM publishing | [library-development](references/library-development.md) |
|
||||
| Monorepo | pnpm workspaces, centralized alias, Turborepo | [monorepo](references/monorepo.md) |
|
||||
| Topic | Description | Reference |
|
||||
| ------------------- | --------------------------------------------------------------- | -------------------------------------------------------- |
|
||||
| ESLint Config | Framework support, formatters, rule overrides, VS Code settings | [antfu-eslint-config](references/antfu-eslint-config.md) |
|
||||
| Project Setup | .gitignore, GitHub Actions, VS Code extensions | [setting-up](references/setting-up.md) |
|
||||
| App Development | Vue/Nuxt/UnoCSS conventions and patterns | [app-development](references/app-development.md) |
|
||||
| Library Development | tsdown bundling, pure ESM publishing | [library-development](references/library-development.md) |
|
||||
| Monorepo | pnpm workspaces, centralized alias, Turborepo | [monorepo](references/monorepo.md) |
|
||||
|
||||
@@ -12,7 +12,7 @@ Handles both linting and formatting (no Prettier needed). Auto-detects TypeScrip
|
||||
## Configuration Options
|
||||
|
||||
```js
|
||||
import antfu from '@antfu/eslint-config'
|
||||
import antfu from '@antfu/eslint-config';
|
||||
|
||||
export default antfu({
|
||||
// Project type: 'lib' for libraries, 'app' (default) for applications
|
||||
@@ -23,7 +23,7 @@ export default antfu({
|
||||
|
||||
// Stylistic options
|
||||
stylistic: {
|
||||
indent: 2, // 2, 4, or 'tab'
|
||||
indent: 2, // 2, 4, or 'tab'
|
||||
quotes: 'single', // or 'double'
|
||||
},
|
||||
|
||||
@@ -34,7 +34,7 @@ export default antfu({
|
||||
// Disable specific language support
|
||||
jsonc: false,
|
||||
yaml: false,
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
## Framework Support
|
||||
@@ -46,9 +46,9 @@ Vue accessibility:
|
||||
```js
|
||||
export default antfu({
|
||||
vue: {
|
||||
a11y: true
|
||||
a11y: true,
|
||||
},
|
||||
})
|
||||
});
|
||||
// Requires: pnpm add -D eslint-plugin-vuejs-accessibility
|
||||
```
|
||||
|
||||
@@ -57,7 +57,7 @@ export default antfu({
|
||||
```js
|
||||
export default antfu({
|
||||
react: true,
|
||||
})
|
||||
});
|
||||
// Requires: pnpm add -D @eslint-react/eslint-plugin eslint-plugin-react-hooks eslint-plugin-react-refresh
|
||||
```
|
||||
|
||||
@@ -66,7 +66,7 @@ export default antfu({
|
||||
```js
|
||||
export default antfu({
|
||||
nextjs: true,
|
||||
})
|
||||
});
|
||||
// Requires: pnpm add -D @next/eslint-plugin-next
|
||||
```
|
||||
|
||||
@@ -75,7 +75,7 @@ export default antfu({
|
||||
```js
|
||||
export default antfu({
|
||||
svelte: true,
|
||||
})
|
||||
});
|
||||
// Requires: pnpm add -D eslint-plugin-svelte
|
||||
```
|
||||
|
||||
@@ -84,7 +84,7 @@ export default antfu({
|
||||
```js
|
||||
export default antfu({
|
||||
astro: true,
|
||||
})
|
||||
});
|
||||
// Requires: pnpm add -D eslint-plugin-astro
|
||||
```
|
||||
|
||||
@@ -93,7 +93,7 @@ export default antfu({
|
||||
```js
|
||||
export default antfu({
|
||||
solid: true,
|
||||
})
|
||||
});
|
||||
// Requires: pnpm add -D eslint-plugin-solid
|
||||
```
|
||||
|
||||
@@ -102,7 +102,7 @@ export default antfu({
|
||||
```js
|
||||
export default antfu({
|
||||
unocss: true,
|
||||
})
|
||||
});
|
||||
// Requires: pnpm add -D @unocss/eslint-plugin
|
||||
```
|
||||
|
||||
@@ -113,11 +113,11 @@ For files ESLint doesn't handle natively:
|
||||
```js
|
||||
export default antfu({
|
||||
formatters: {
|
||||
css: true, // Format CSS, LESS, SCSS (uses Prettier)
|
||||
html: true, // Format HTML (uses Prettier)
|
||||
markdown: 'prettier' // or 'dprint'
|
||||
}
|
||||
})
|
||||
css: true, // Format CSS, LESS, SCSS (uses Prettier)
|
||||
html: true, // Format HTML (uses Prettier)
|
||||
markdown: 'prettier', // or 'dprint'
|
||||
},
|
||||
});
|
||||
// Requires: pnpm add -D eslint-plugin-format
|
||||
```
|
||||
|
||||
@@ -135,8 +135,8 @@ export default antfu(
|
||||
rules: {
|
||||
'style/semi': ['error', 'never'],
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
### Per-integration overrides
|
||||
@@ -153,7 +153,7 @@ export default antfu({
|
||||
'ts/consistent-type-definitions': ['error', 'interface'],
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### File-specific overrides
|
||||
@@ -166,29 +166,29 @@ export default antfu(
|
||||
rules: {
|
||||
'vue/operator-linebreak': ['error', 'before'],
|
||||
},
|
||||
}
|
||||
)
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
## Plugin Prefix Renaming
|
||||
|
||||
The config renames plugin prefixes for consistency:
|
||||
|
||||
| New Prefix | Original |
|
||||
|------------|----------|
|
||||
| `ts/*` | `@typescript-eslint/*` |
|
||||
| `style/*` | `@stylistic/*` |
|
||||
| `import/*` | `import-lite/*` |
|
||||
| `node/*` | `n/*` |
|
||||
| `yaml/*` | `yml/*` |
|
||||
| `test/*` | `vitest/*` |
|
||||
| `next/*` | `@next/next` |
|
||||
| New Prefix | Original |
|
||||
| ---------- | ---------------------- |
|
||||
| `ts/*` | `@typescript-eslint/*` |
|
||||
| `style/*` | `@stylistic/*` |
|
||||
| `import/*` | `import-lite/*` |
|
||||
| `node/*` | `n/*` |
|
||||
| `yaml/*` | `yml/*` |
|
||||
| `test/*` | `vitest/*` |
|
||||
| `next/*` | `@next/next` |
|
||||
|
||||
Use the new prefix when overriding or disabling rules:
|
||||
|
||||
```ts
|
||||
// eslint-disable-next-line ts/consistent-type-definitions
|
||||
type Foo = { bar: 2 }
|
||||
type Foo = { bar: 2 };
|
||||
```
|
||||
|
||||
## Type-Aware Rules
|
||||
@@ -200,7 +200,7 @@ export default antfu({
|
||||
typescript: {
|
||||
tsconfigPath: 'tsconfig.json',
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
## Config Composer API
|
||||
@@ -213,11 +213,11 @@ export default antfu()
|
||||
.override('antfu/stylistic/rules', {
|
||||
rules: {
|
||||
'style/generator-star-spacing': ['error', { after: true, before: false }],
|
||||
}
|
||||
},
|
||||
})
|
||||
.renamePlugins({
|
||||
'old-prefix': 'new-prefix',
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
## Less Opinionated Mode
|
||||
@@ -226,8 +226,8 @@ Disable Anthony's most opinionated rules:
|
||||
|
||||
```js
|
||||
export default antfu({
|
||||
lessOpinionated: true
|
||||
})
|
||||
lessOpinionated: true,
|
||||
});
|
||||
```
|
||||
|
||||
## Lint-Staged Setup
|
||||
@@ -258,7 +258,7 @@ Add to `.vscode/settings.json`:
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
"source.organizeImports": "never",
|
||||
},
|
||||
"eslint.rules.customizations": [
|
||||
{ "rule": "style/*", "severity": "off", "fixable": true },
|
||||
@@ -270,7 +270,7 @@ Add to `.vscode/settings.json`:
|
||||
{ "rule": "*-dangle", "severity": "off", "fixable": true },
|
||||
{ "rule": "*-newline", "severity": "off", "fixable": true },
|
||||
{ "rule": "*quotes", "severity": "off", "fixable": true },
|
||||
{ "rule": "*semi", "severity": "off", "fixable": true }
|
||||
{ "rule": "*semi", "severity": "off", "fixable": true },
|
||||
],
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
@@ -289,12 +289,12 @@ Add to `.vscode/settings.json`:
|
||||
"svelte",
|
||||
"css",
|
||||
"less",
|
||||
"scss"
|
||||
]
|
||||
"scss",
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
<!--
|
||||
<!--
|
||||
Source references:
|
||||
- https://github.com/antfu/eslint-config
|
||||
- https://raw.githubusercontent.com/antfu/eslint-config/refs/heads/main/README.md
|
||||
|
||||
@@ -7,39 +7,39 @@ description: Vue/Nuxt/UnoCSS application conventions. Use when building web apps
|
||||
|
||||
## Framework Selection
|
||||
|
||||
| Use Case | Choice |
|
||||
|----------|--------|
|
||||
| SPA, client-only, library playgrounds | Vite + Vue |
|
||||
| SSR, SSG, SEO-critical, file-based routing, API routes | Nuxt |
|
||||
| Use Case | Choice |
|
||||
| ------------------------------------------------------ | ---------- |
|
||||
| SPA, client-only, library playgrounds | Vite + Vue |
|
||||
| SSR, SSG, SEO-critical, file-based routing, API routes | Nuxt |
|
||||
|
||||
## Vue Conventions
|
||||
|
||||
| Convention | Preference |
|
||||
|------------|------------|
|
||||
| Script syntax | Always `<script setup lang="ts">` |
|
||||
| State | Prefer `shallowRef()` over `ref()` |
|
||||
| Objects | Use `ref()`, avoid `reactive()` |
|
||||
| Styling | UnoCSS |
|
||||
| Utilities | VueUse |
|
||||
| Convention | Preference |
|
||||
| ------------- | ---------------------------------- |
|
||||
| Script syntax | Always `<script setup lang="ts">` |
|
||||
| State | Prefer `shallowRef()` over `ref()` |
|
||||
| Objects | Use `ref()`, avoid `reactive()` |
|
||||
| Styling | UnoCSS |
|
||||
| Utilities | VueUse |
|
||||
|
||||
### Props and Emits
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
title: string
|
||||
count?: number
|
||||
title: string;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update', value: number): void
|
||||
(e: 'close'): void
|
||||
(e: 'update', value: number): void;
|
||||
(e: 'close'): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
count: 0,
|
||||
})
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
const emit = defineEmits<Emits>();
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -5,11 +5,11 @@ description: Building and publishing TypeScript libraries with tsdown. Use when
|
||||
|
||||
# Library Development
|
||||
|
||||
| Aspect | Choice |
|
||||
|--------|--------|
|
||||
| Bundler | tsdown |
|
||||
| Output | Pure ESM only (no CJS) |
|
||||
| DTS | Generated via tsdown |
|
||||
| Aspect | Choice |
|
||||
| ------- | ------------------------- |
|
||||
| Bundler | tsdown |
|
||||
| Output | Pure ESM only (no CJS) |
|
||||
| DTS | Generated via tsdown |
|
||||
| Exports | Auto-generated via tsdown |
|
||||
|
||||
## tsdown Configuration
|
||||
@@ -18,34 +18,31 @@ Use tsdown with these options enabled:
|
||||
|
||||
```ts
|
||||
// tsdown.config.ts
|
||||
import { defineConfig } from 'tsdown'
|
||||
import { defineConfig } from 'tsdown';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
format: ['esm'],
|
||||
dts: true,
|
||||
exports: true,
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
| Option | Value | Purpose |
|
||||
|--------|-------|---------|
|
||||
| `format` | `['esm']` | Pure ESM, no CommonJS |
|
||||
| `dts` | `true` | Generate `.d.ts` files |
|
||||
| `exports` | `true` | Auto-update `exports` field in `package.json` |
|
||||
| Option | Value | Purpose |
|
||||
| --------- | --------- | --------------------------------------------- |
|
||||
| `format` | `['esm']` | Pure ESM, no CommonJS |
|
||||
| `dts` | `true` | Generate `.d.ts` files |
|
||||
| `exports` | `true` | Auto-update `exports` field in `package.json` |
|
||||
|
||||
### Multiple Entry Points
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
entry: [
|
||||
'src/index.ts',
|
||||
'src/utils.ts',
|
||||
],
|
||||
entry: ['src/index.ts', 'src/utils.ts'],
|
||||
format: ['esm'],
|
||||
dts: true,
|
||||
exports: true,
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
The `exports: true` option auto-generates the `exports` field in `package.json` when running `tsdown`.
|
||||
|
||||
@@ -45,7 +45,6 @@ In each package's `package.json`, add the scripts.
|
||||
|
||||
## ESLint Cache
|
||||
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
@@ -66,38 +65,35 @@ For better DX across Vite, Nuxt, Vitest configs, create a centralized `alias.ts`
|
||||
|
||||
```ts
|
||||
// alias.ts
|
||||
import fs from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { join, relative } from 'pathe'
|
||||
import fs from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { join, relative } from 'pathe';
|
||||
|
||||
const root = fileURLToPath(new URL('.', import.meta.url))
|
||||
const r = (path: string) => fileURLToPath(new URL(`./packages/${path}`, import.meta.url))
|
||||
const root = fileURLToPath(new URL('.', import.meta.url));
|
||||
const r = (path: string) => fileURLToPath(new URL(`./packages/${path}`, import.meta.url));
|
||||
|
||||
export const alias = {
|
||||
'@myorg/core': r('core/src/index.ts'),
|
||||
'@myorg/utils': r('utils/src/index.ts'),
|
||||
'@myorg/ui': r('ui/src/index.ts'),
|
||||
// Add more aliases as needed
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-update tsconfig.alias.json paths
|
||||
const raw = fs.readFileSync(join(root, 'tsconfig.alias.json'), 'utf-8').trim()
|
||||
const tsconfig = JSON.parse(raw)
|
||||
const raw = fs.readFileSync(join(root, 'tsconfig.alias.json'), 'utf-8').trim();
|
||||
const tsconfig = JSON.parse(raw);
|
||||
tsconfig.compilerOptions.paths = Object.fromEntries(
|
||||
Object.entries(alias).map(([key, value]) => [key, [`./${relative(root, value)}`]]),
|
||||
)
|
||||
const newRaw = JSON.stringify(tsconfig, null, 2)
|
||||
if (newRaw !== raw)
|
||||
fs.writeFileSync(join(root, 'tsconfig.alias.json'), `${newRaw}\n`, 'utf-8')
|
||||
);
|
||||
const newRaw = JSON.stringify(tsconfig, null, 2);
|
||||
if (newRaw !== raw) fs.writeFileSync(join(root, 'tsconfig.alias.json'), `${newRaw}\n`, 'utf-8');
|
||||
```
|
||||
|
||||
Then update the `tsconfig.json` to use the alias file:
|
||||
|
||||
```json
|
||||
{
|
||||
"extends": [
|
||||
"./tsconfig.alias.json"
|
||||
]
|
||||
"extends": ["./tsconfig.alias.json"]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -107,18 +103,18 @@ Reference the centralized alias in all config files:
|
||||
|
||||
```ts
|
||||
// vite.config.ts
|
||||
import { alias } from './alias'
|
||||
import { alias } from './alias';
|
||||
|
||||
export default defineConfig({
|
||||
resolve: { alias },
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
```ts
|
||||
// nuxt.config.ts
|
||||
import { alias } from './alias'
|
||||
import { alias } from './alias';
|
||||
|
||||
export default defineNuxtConfig({
|
||||
alias,
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
@@ -109,11 +109,11 @@ Configure in `.vscode/extensions.json`:
|
||||
}
|
||||
```
|
||||
|
||||
| Extension | Description |
|
||||
|-----------|-------------|
|
||||
| `dbaeumer.vscode-eslint` | ESLint integration for linting and formatting |
|
||||
| `antfu.pnpm-catalog-lens` | Shows pnpm catalog version hints inline |
|
||||
| `antfu.iconify` | Iconify icon preview and autocomplete |
|
||||
| `antfu.unocss` | UnoCSS IntelliSense and syntax highlighting |
|
||||
| `antfu.slidev` | Slidev preview and syntax highlighting |
|
||||
| `vue.volar` | Vue Language Features |
|
||||
| Extension | Description |
|
||||
| ------------------------- | --------------------------------------------- |
|
||||
| `dbaeumer.vscode-eslint` | ESLint integration for linting and formatting |
|
||||
| `antfu.pnpm-catalog-lens` | Shows pnpm catalog version hints inline |
|
||||
| `antfu.iconify` | Iconify icon preview and autocomplete |
|
||||
| `antfu.unocss` | UnoCSS IntelliSense and syntax highlighting |
|
||||
| `antfu.slidev` | Slidev preview and syntax highlighting |
|
||||
| `vue.volar` | Vue Language Features |
|
||||
|
||||
@@ -14,15 +14,18 @@ Better Auth is a TypeScript-first, framework-agnostic auth framework supporting
|
||||
## Quick Reference
|
||||
|
||||
### Environment Variables
|
||||
|
||||
- `BETTER_AUTH_SECRET` - Encryption secret (min 32 chars). Generate: `openssl rand -base64 32`
|
||||
- `BETTER_AUTH_URL` - Base URL (e.g., `https://example.com`)
|
||||
|
||||
Only define `baseURL`/`secret` in config if env vars are NOT set.
|
||||
|
||||
### File Location
|
||||
|
||||
CLI looks for `auth.ts` in: `./`, `./lib`, `./utils`, or under `./src`. Use `--config` for custom path.
|
||||
|
||||
### CLI Commands
|
||||
|
||||
- `npx @better-auth/cli@latest migrate` - Apply schema (built-in adapter)
|
||||
- `npx @better-auth/cli@latest generate` - Generate schema for Prisma/Drizzle
|
||||
- `npx @better-auth/cli mcp --cursor` - Add MCP to AI tools
|
||||
@@ -33,18 +36,18 @@ CLI looks for `auth.ts` in: `./`, `./lib`, `./utils`, or under `./src`. Use `--c
|
||||
|
||||
## Core Config Options
|
||||
|
||||
| Option | Notes |
|
||||
|--------|-------|
|
||||
| `appName` | Optional display name |
|
||||
| `baseURL` | Only if `BETTER_AUTH_URL` not set |
|
||||
| `basePath` | Default `/api/auth`. Set `/` for root. |
|
||||
| `secret` | Only if `BETTER_AUTH_SECRET` not set |
|
||||
| `database` | Required for most features. See adapters docs. |
|
||||
| `secondaryStorage` | Redis/KV for sessions & rate limits |
|
||||
| `emailAndPassword` | `{ enabled: true }` to activate |
|
||||
| `socialProviders` | `{ google: { clientId, clientSecret }, ... }` |
|
||||
| `plugins` | Array of plugins |
|
||||
| `trustedOrigins` | CSRF whitelist |
|
||||
| Option | Notes |
|
||||
| ------------------ | ---------------------------------------------- |
|
||||
| `appName` | Optional display name |
|
||||
| `baseURL` | Only if `BETTER_AUTH_URL` not set |
|
||||
| `basePath` | Default `/api/auth`. Set `/` for root. |
|
||||
| `secret` | Only if `BETTER_AUTH_SECRET` not set |
|
||||
| `database` | Required for most features. See adapters docs. |
|
||||
| `secondaryStorage` | Redis/KV for sessions & rate limits |
|
||||
| `emailAndPassword` | `{ enabled: true }` to activate |
|
||||
| `socialProviders` | `{ google: { clientId, clientSecret }, ... }` |
|
||||
| `plugins` | Array of plugins |
|
||||
| `trustedOrigins` | CSRF whitelist |
|
||||
|
||||
---
|
||||
|
||||
@@ -61,11 +64,13 @@ CLI looks for `auth.ts` in: `./`, `./lib`, `./utils`, or under `./src`. Use `--c
|
||||
## Session Management
|
||||
|
||||
**Storage priority:**
|
||||
|
||||
1. If `secondaryStorage` defined → sessions go there (not DB)
|
||||
2. Set `session.storeSessionInDatabase: true` to also persist to DB
|
||||
3. No database + `cookieCache` → fully stateless mode
|
||||
|
||||
**Cookie cache strategies:**
|
||||
|
||||
- `compact` (default) - Base64url + HMAC. Smallest.
|
||||
- `jwt` - Standard JWT. Readable but signed.
|
||||
- `jwe` - Encrypted. Maximum security.
|
||||
@@ -95,9 +100,10 @@ CLI looks for `auth.ts` in: `./`, `./lib`, `./utils`, or under `./src`. Use `--c
|
||||
## Security
|
||||
|
||||
**In `advanced`:**
|
||||
|
||||
- `useSecureCookies` - Force HTTPS cookies
|
||||
- `disableCSRFCheck` - ⚠️ Security risk
|
||||
- `disableOriginCheck` - ⚠️ Security risk
|
||||
- `disableOriginCheck` - ⚠️ Security risk
|
||||
- `crossSubDomainCookies.enabled` - Share cookies across subdomains
|
||||
- `ipAddress.ipAddressHeaders` - Custom IP headers for proxies
|
||||
- `database.generateId` - Custom ID generation or `"serial"`/`"uuid"`/`false`
|
||||
@@ -119,9 +125,11 @@ CLI looks for `auth.ts` in: `./`, `./lib`, `./utils`, or under `./src`. Use `--c
|
||||
## Plugins
|
||||
|
||||
**Import from dedicated paths for tree-shaking:**
|
||||
|
||||
```
|
||||
import { twoFactor } from "better-auth/plugins/two-factor"
|
||||
```
|
||||
|
||||
NOT `from "better-auth/plugins"`.
|
||||
|
||||
**Popular plugins:** `twoFactor`, `organization`, `passkey`, `magicLink`, `emailOtp`, `username`, `phoneNumber`, `admin`, `apiKey`, `bearer`, `jwt`, `multiSession`, `sso`, `oauthProvider`, `oidcProvider`, `openAPI`, `genericOAuth`.
|
||||
@@ -163,4 +171,4 @@ For separate client/server projects: `createAuthClient<typeof auth>()`.
|
||||
- [Options Reference](https://better-auth.com/docs/reference/options)
|
||||
- [LLMs.txt](https://better-auth.com/llms.txt)
|
||||
- [GitHub](https://github.com/better-auth/better-auth)
|
||||
- [Init Options Source](https://github.com/better-auth/better-auth/blob/main/packages/core/src/types/init-options.ts)
|
||||
- [Init Options Source](https://github.com/better-auth/better-auth/blob/main/packages/core/src/types/init-options.ts)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: brainstorming
|
||||
description: "You MUST use this before any creative work - creating features, building components, adding functionality, or modifying behavior. Explores user intent, requirements and design before implementation."
|
||||
description: 'You MUST use this before any creative work - creating features, building components, adding functionality, or modifying behavior. Explores user intent, requirements and design before implementation.'
|
||||
---
|
||||
|
||||
# Brainstorming Ideas Into Designs
|
||||
@@ -57,6 +57,7 @@ digraph brainstorming {
|
||||
## The Process
|
||||
|
||||
**Understanding the idea:**
|
||||
|
||||
- Check out the current project state first (files, docs, recent commits)
|
||||
- Ask questions one at a time to refine the idea
|
||||
- Prefer multiple choice questions when possible, but open-ended is fine too
|
||||
@@ -64,11 +65,13 @@ digraph brainstorming {
|
||||
- Focus on understanding: purpose, constraints, success criteria
|
||||
|
||||
**Exploring approaches:**
|
||||
|
||||
- Propose 2-3 different approaches with trade-offs
|
||||
- Present options conversationally with your recommendation and reasoning
|
||||
- Lead with your recommended option and explain why
|
||||
|
||||
**Presenting the design:**
|
||||
|
||||
- Once you believe you understand what you're building, present the design
|
||||
- Scale each section to its complexity: a few sentences if straightforward, up to 200-300 words if nuanced
|
||||
- Ask after each section whether it looks right so far
|
||||
@@ -78,11 +81,13 @@ digraph brainstorming {
|
||||
## After the Design
|
||||
|
||||
**Documentation:**
|
||||
|
||||
- Write the validated design to `docs/plans/YYYY-MM-DD-<topic>-design.md`
|
||||
- Use elements-of-style:writing-clearly-and-concisely skill if available
|
||||
- Commit the design document to git
|
||||
|
||||
**Implementation:**
|
||||
|
||||
- Invoke the writing-plans skill to create a detailed implementation plan
|
||||
- Do NOT invoke any other skill. writing-plans is the next step.
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ license: Complete terms in LICENSE.txt
|
||||
These are instructions for creating design philosophies - aesthetic movements that are then EXPRESSED VISUALLY. Output only .md files, .pdf files, and .png files.
|
||||
|
||||
Complete this in two steps:
|
||||
|
||||
1. Design Philosophy Creation (.md file)
|
||||
2. Express by creating it on a canvas (.pdf file or .png file)
|
||||
|
||||
@@ -15,16 +16,19 @@ First, undertake this task:
|
||||
## DESIGN PHILOSOPHY CREATION
|
||||
|
||||
To begin, create a VISUAL PHILOSOPHY (not layouts or templates) that will be interpreted through:
|
||||
|
||||
- Form, space, color, composition
|
||||
- Images, graphics, shapes, patterns
|
||||
- Minimal text as visual accent
|
||||
|
||||
### THE CRITICAL UNDERSTANDING
|
||||
|
||||
- What is received: Some subtle input or instructions by the user that should be taken into account, but used as a foundation; it should not constrain creative freedom.
|
||||
- What is created: A design philosophy/aesthetic movement.
|
||||
- What happens next: Then, the same version receives the philosophy and EXPRESSES IT VISUALLY - creating artifacts that are 90% visual design, 10% essential text.
|
||||
|
||||
Consider this approach:
|
||||
|
||||
- Write a manifesto for an art movement
|
||||
- The next phase involves making the artwork
|
||||
|
||||
@@ -37,6 +41,7 @@ The philosophy must emphasize: Visual expression. Spatial communication. Artisti
|
||||
**Articulate the philosophy** (4-6 paragraphs - concise but complete):
|
||||
|
||||
To capture the VISUAL essence, express how the philosophy manifests through:
|
||||
|
||||
- Space and form
|
||||
- Color and material
|
||||
- Scale and rhythm
|
||||
@@ -44,6 +49,7 @@ To capture the VISUAL essence, express how the philosophy manifests through:
|
||||
- Visual hierarchy
|
||||
|
||||
**CRITICAL GUIDELINES:**
|
||||
|
||||
- **Avoid redundancy**: Each design aspect should be mentioned once. Avoid repeating points about color theory, spatial relationships, or typographic principles unless adding new depth.
|
||||
- **Emphasize craftsmanship REPEATEDLY**: The philosophy MUST stress multiple times that the final work should appear as though it took countless hours to create, was labored over with care, and comes from someone at the absolute top of their field. This framing is essential - repeat phrases like "meticulously crafted," "the product of deep expertise," "painstaking attention," "master-level execution."
|
||||
- **Leave creative space**: Remain specific about the aesthetic direction, but concise enough that the next Claude has room to make interpretive choices also at a extremely high level of craftmanship.
|
||||
@@ -72,9 +78,10 @@ Visual expression: Rounded forms, organic arrangements, color from nature throug
|
||||
Philosophy: Pure order and restraint.
|
||||
Visual expression: Grid-based precision, bold photography or stark graphics, dramatic negative space. Typography precise but minimal - small essential text, large quiet zones. Swiss formalism meets Brutalist material honesty. Structure communicates, not words. Every alignment the work of countless refinements.
|
||||
|
||||
*These are condensed examples. The actual design philosophy should be 4-6 substantial paragraphs.*
|
||||
_These are condensed examples. The actual design philosophy should be 4-6 substantial paragraphs._
|
||||
|
||||
### ESSENTIAL PRINCIPLES
|
||||
|
||||
- **VISUAL PHILOSOPHY**: Create an aesthetic worldview to be expressed through design
|
||||
- **MINIMAL TEXT**: Always emphasize that text is sparse, essential-only, integrated as visual element - never lengthy
|
||||
- **SPATIAL EXPRESSION**: Ideas communicate through space, form, color, composition - not paragraphs
|
||||
@@ -103,13 +110,13 @@ With both the philosophy and the conceptual framework established, express it on
|
||||
|
||||
**IMPORTANT**: For any type of content, even if the user requests something for a movie/game/book, the approach should still be sophisticated. Never lose sight of the idea that this should be art, not something that's cartoony or amateur.
|
||||
|
||||
To create museum or magazine quality work, use the design philosophy as the foundation. Create one single page, highly visual, design-forward PDF or PNG output (unless asked for more pages). Generally use repeating patterns and perfect shapes. Treat the abstract philosophical design as if it were a scientific bible, borrowing the visual language of systematic observation—dense accumulation of marks, repeated elements, or layered patterns that build meaning through patient repetition and reward sustained viewing. Add sparse, clinical typography and systematic reference markers that suggest this could be a diagram from an imaginary discipline, treating the invisible subject with the same reverence typically reserved for documenting observable phenomena. Anchor the piece with simple phrase(s) or details positioned subtly, using a limited color palette that feels intentional and cohesive. Embrace the paradox of using analytical visual language to express ideas about human experience: the result should feel like an artifact that proves something ephemeral can be studied, mapped, and understood through careful attention. This is true art.
|
||||
To create museum or magazine quality work, use the design philosophy as the foundation. Create one single page, highly visual, design-forward PDF or PNG output (unless asked for more pages). Generally use repeating patterns and perfect shapes. Treat the abstract philosophical design as if it were a scientific bible, borrowing the visual language of systematic observation—dense accumulation of marks, repeated elements, or layered patterns that build meaning through patient repetition and reward sustained viewing. Add sparse, clinical typography and systematic reference markers that suggest this could be a diagram from an imaginary discipline, treating the invisible subject with the same reverence typically reserved for documenting observable phenomena. Anchor the piece with simple phrase(s) or details positioned subtly, using a limited color palette that feels intentional and cohesive. Embrace the paradox of using analytical visual language to express ideas about human experience: the result should feel like an artifact that proves something ephemeral can be studied, mapped, and understood through careful attention. This is true art.
|
||||
|
||||
**Text as a contextual element**: Text is always minimal and visual-first, but let context guide whether that means whisper-quiet labels or bold typographic gestures. A punk venue poster might have larger, more aggressive type than a minimalist ceramics studio identity. Most of the time, font should be thin. All use of fonts must be design-forward and prioritize visual communication. Regardless of text scale, nothing falls off the page and nothing overlaps. Every element must be contained within the canvas boundaries with proper margins. Check carefully that all text, graphics, and visual elements have breathing room and clear separation. This is non-negotiable for professional execution. **IMPORTANT: Use different fonts if writing text. Search the `./canvas-fonts` directory. Regardless of approach, sophistication is non-negotiable.**
|
||||
|
||||
Download and use whatever fonts are needed to make this a reality. Get creative by making the typography actually part of the art itself -- if the art is abstract, bring the font onto the canvas, not typeset digitally.
|
||||
|
||||
To push boundaries, follow design instinct/intuition while using the philosophy as a guiding principle. Embrace ultimate design freedom and choice. Push aesthetics and design to the frontier.
|
||||
To push boundaries, follow design instinct/intuition while using the philosophy as a guiding principle. Embrace ultimate design freedom and choice. Push aesthetics and design to the frontier.
|
||||
|
||||
**CRITICAL**: To achieve human-crafted quality (not AI-generated), create work that looks like it took countless hours. Make it appear as though someone at the absolute top of their field labored over every detail with painstaking care. Ensure the composition, spacing, color choices, typography - everything screams expert-level craftsmanship. Double-check that nothing overlaps, formatting is flawless, every detail perfect. Create something that could be shown to people to prove expertise and rank as undeniably impressive.
|
||||
|
||||
@@ -127,4 +134,4 @@ Take a second pass. Go back to the code and refine/polish further to make this a
|
||||
|
||||
## MULTI-PAGE OPTION
|
||||
|
||||
To create additional pages when requested, create more creative pages along the same lines as the design philosophy but distinctly different as well. Bundle those pages in the same .pdf or many .pngs. Treat the first page as just a single page in a whole coffee table book waiting to be filled. Make the next pages unique twists and memories of the original. Have them almost tell a story in a very tasteful way. Exercise full creative freedom.
|
||||
To create additional pages when requested, create more creative pages along the same lines as the design philosophy but distinctly different as well. Bundle those pages in the same .pdf or many .pngs. Treat the first page as just a single page in a whole coffee table book waiting to be filled. Make the next pages unique twists and memories of the original. Have them almost tell a story in a very tasteful way. Exercise full creative freedom.
|
||||
|
||||
@@ -10,8 +10,8 @@ allowed-tools:
|
||||
- Read
|
||||
- Grep
|
||||
- Glob
|
||||
- Bash # 运行 lint/test/build 命令验证代码质量
|
||||
- WebFetch # 查阅最新文档和最佳实践
|
||||
- Bash # 运行 lint/test/build 命令验证代码质量
|
||||
- WebFetch # 查阅最新文档和最佳实践
|
||||
---
|
||||
|
||||
# Code Review Excellence
|
||||
@@ -34,6 +34,7 @@ Transform code reviews from gatekeeping to knowledge sharing through constructiv
|
||||
### 1. The Review Mindset
|
||||
|
||||
**Goals of Code Review:**
|
||||
|
||||
- Catch bugs and edge cases
|
||||
- Ensure code maintainability
|
||||
- Share knowledge across team
|
||||
@@ -42,6 +43,7 @@ Transform code reviews from gatekeeping to knowledge sharing through constructiv
|
||||
- Build team culture
|
||||
|
||||
**Not the Goals:**
|
||||
|
||||
- Show off knowledge
|
||||
- Nitpick formatting (use linters)
|
||||
- Block progress unnecessarily
|
||||
@@ -50,6 +52,7 @@ Transform code reviews from gatekeeping to knowledge sharing through constructiv
|
||||
### 2. Effective Feedback
|
||||
|
||||
**Good Feedback is:**
|
||||
|
||||
- Specific and actionable
|
||||
- Educational, not judgmental
|
||||
- Focused on the code, not the person
|
||||
@@ -59,20 +62,21 @@ Transform code reviews from gatekeeping to knowledge sharing through constructiv
|
||||
```markdown
|
||||
❌ Bad: "This is wrong."
|
||||
✅ Good: "This could cause a race condition when multiple users
|
||||
access simultaneously. Consider using a mutex here."
|
||||
access simultaneously. Consider using a mutex here."
|
||||
|
||||
❌ Bad: "Why didn't you use X pattern?"
|
||||
✅ Good: "Have you considered the Repository pattern? It would
|
||||
make this easier to test. Here's an example: [link]"
|
||||
make this easier to test. Here's an example: [link]"
|
||||
|
||||
❌ Bad: "Rename this variable."
|
||||
✅ Good: "[nit] Consider `userCount` instead of `uc` for
|
||||
clarity. Not blocking if you prefer to keep it."
|
||||
clarity. Not blocking if you prefer to keep it."
|
||||
```
|
||||
|
||||
### 3. Review Scope
|
||||
|
||||
**What to Review:**
|
||||
|
||||
- Logic correctness and edge cases
|
||||
- Security vulnerabilities
|
||||
- Performance implications
|
||||
@@ -83,6 +87,7 @@ Transform code reviews from gatekeeping to knowledge sharing through constructiv
|
||||
- Architectural fit
|
||||
|
||||
**What Not to Review Manually:**
|
||||
|
||||
- Code formatting (use Prettier, Black, etc.)
|
||||
- Import organization
|
||||
- Linting violations
|
||||
@@ -93,6 +98,7 @@ Transform code reviews from gatekeeping to knowledge sharing through constructiv
|
||||
### Phase 1: Context Gathering (2-3 minutes)
|
||||
|
||||
Before diving into code, understand:
|
||||
|
||||
1. Read PR description and linked issue
|
||||
2. Check PR size (>400 lines? Ask to split)
|
||||
3. Review CI/CD status (tests passing?)
|
||||
@@ -113,6 +119,7 @@ Before diving into code, understand:
|
||||
### Phase 3: Line-by-Line Review (10-20 minutes)
|
||||
|
||||
For each file, check:
|
||||
|
||||
- **Logic & Correctness** - Edge cases, off-by-one, null checks, race conditions
|
||||
- **Security** - Input validation, injection risks, XSS, sensitive data
|
||||
- **Performance** - N+1 queries, unnecessary loops, memory leaks
|
||||
@@ -173,19 +180,19 @@ Use labels to indicate priority:
|
||||
|
||||
根据审查的代码语言,查阅对应的详细指南:
|
||||
|
||||
| Language/Framework | Reference File | Key Topics |
|
||||
|-------------------|----------------|------------|
|
||||
| **React** | [React Guide](reference/react.md) | Hooks, useEffect, React 19 Actions, RSC, Suspense, TanStack Query v5 |
|
||||
| **Vue 3** | [Vue Guide](reference/vue.md) | Composition API, 响应性系统, Props/Emits, Watchers, Composables |
|
||||
| **Rust** | [Rust Guide](reference/rust.md) | 所有权/借用, Unsafe 审查, 异步代码, 错误处理 |
|
||||
| **TypeScript** | [TypeScript Guide](reference/typescript.md) | 类型安全, async/await, 不可变性 |
|
||||
| **Python** | [Python Guide](reference/python.md) | 可变默认参数, 异常处理, 类属性 |
|
||||
| **Java** | [Java Guide](reference/java.md) | Java 17/21 新特性, Spring Boot 3, 虚拟线程, Stream/Optional |
|
||||
| **Go** | [Go Guide](reference/go.md) | 错误处理, goroutine/channel, context, 接口设计 |
|
||||
| **C** | [C Guide](reference/c.md) | 指针/缓冲区, 内存安全, UB, 错误处理 |
|
||||
| **C++** | [C++ Guide](reference/cpp.md) | RAII, 生命周期, Rule of 0/3/5, 异常安全 |
|
||||
| **CSS/Less/Sass** | [CSS Guide](reference/css-less-sass.md) | 变量规范, !important, 性能优化, 响应式, 兼容性 |
|
||||
| **Qt** | [Qt Guide](reference/qt.md) | 对象模型, 信号/槽, 内存管理, 线程安全, 性能 |
|
||||
| Language/Framework | Reference File | Key Topics |
|
||||
| ------------------ | ------------------------------------------- | -------------------------------------------------------------------- |
|
||||
| **React** | [React Guide](reference/react.md) | Hooks, useEffect, React 19 Actions, RSC, Suspense, TanStack Query v5 |
|
||||
| **Vue 3** | [Vue Guide](reference/vue.md) | Composition API, 响应性系统, Props/Emits, Watchers, Composables |
|
||||
| **Rust** | [Rust Guide](reference/rust.md) | 所有权/借用, Unsafe 审查, 异步代码, 错误处理 |
|
||||
| **TypeScript** | [TypeScript Guide](reference/typescript.md) | 类型安全, async/await, 不可变性 |
|
||||
| **Python** | [Python Guide](reference/python.md) | 可变默认参数, 异常处理, 类属性 |
|
||||
| **Java** | [Java Guide](reference/java.md) | Java 17/21 新特性, Spring Boot 3, 虚拟线程, Stream/Optional |
|
||||
| **Go** | [Go Guide](reference/go.md) | 错误处理, goroutine/channel, context, 接口设计 |
|
||||
| **C** | [C Guide](reference/c.md) | 指针/缓冲区, 内存安全, UB, 错误处理 |
|
||||
| **C++** | [C++ Guide](reference/cpp.md) | RAII, 生命周期, Rule of 0/3/5, 异常安全 |
|
||||
| **CSS/Less/Sass** | [CSS Guide](reference/css-less-sass.md) | 变量规范, !important, 性能优化, 响应式, 兼容性 |
|
||||
| **Qt** | [Qt Guide](reference/qt.md) | 对象模型, 信号/槽, 内存管理, 线程安全, 性能 |
|
||||
|
||||
## Additional Resources
|
||||
|
||||
|
||||
@@ -20,15 +20,18 @@ Copy and use this template for your code reviews.
|
||||
## Required Changes
|
||||
|
||||
🔴 **[blocking]** [Issue description]
|
||||
|
||||
> [Code location or example]
|
||||
> [Suggested fix or explanation]
|
||||
|
||||
🔴 **[blocking]** [Issue description]
|
||||
|
||||
> [Details]
|
||||
|
||||
## Important Suggestions
|
||||
|
||||
🟡 **[important]** [Issue description]
|
||||
|
||||
> [Why this matters]
|
||||
> [Suggested approach]
|
||||
|
||||
@@ -68,6 +71,7 @@ Copy and use this template for your code reviews.
|
||||
## Quick Copy Templates
|
||||
|
||||
### Blocking Issue
|
||||
|
||||
```
|
||||
🔴 **[blocking]** [Title]
|
||||
|
||||
@@ -82,6 +86,7 @@ Copy and use this template for your code reviews.
|
||||
```
|
||||
|
||||
### Important Suggestion
|
||||
|
||||
```
|
||||
🟡 **[important]** [Title]
|
||||
|
||||
@@ -93,6 +98,7 @@ Copy and use this template for your code reviews.
|
||||
```
|
||||
|
||||
### Minor Suggestion
|
||||
|
||||
```
|
||||
🟢 **[nit]** [Suggestion]
|
||||
|
||||
@@ -100,6 +106,7 @@ Not blocking, but consider [improvement].
|
||||
```
|
||||
|
||||
### Praise
|
||||
|
||||
```
|
||||
🎉 **[praise]** Great work on [specific thing]!
|
||||
|
||||
@@ -107,6 +114,7 @@ Not blocking, but consider [improvement].
|
||||
```
|
||||
|
||||
### Question
|
||||
|
||||
```
|
||||
❓ **[question]** [Your question]
|
||||
|
||||
|
||||
+22
-22
@@ -70,38 +70,38 @@ Quick reference checklist for code reviews.
|
||||
|
||||
## Severity Labels
|
||||
|
||||
| Label | Meaning | Action |
|
||||
|-------|---------|--------|
|
||||
| 🔴 `[blocking]` | Must fix | Block merge |
|
||||
| 🟡 `[important]` | Should fix | Discuss if disagree |
|
||||
| 🟢 `[nit]` | Nice to have | Non-blocking |
|
||||
| 💡 `[suggestion]` | Alternative | Consider |
|
||||
| ❓ `[question]` | Need clarity | Respond |
|
||||
| 🎉 `[praise]` | Good work | Celebrate! |
|
||||
| Label | Meaning | Action |
|
||||
| ----------------- | ------------ | ------------------- |
|
||||
| 🔴 `[blocking]` | Must fix | Block merge |
|
||||
| 🟡 `[important]` | Should fix | Discuss if disagree |
|
||||
| 🟢 `[nit]` | Nice to have | Non-blocking |
|
||||
| 💡 `[suggestion]` | Alternative | Consider |
|
||||
| ❓ `[question]` | Need clarity | Respond |
|
||||
| 🎉 `[praise]` | Good work | Celebrate! |
|
||||
|
||||
---
|
||||
|
||||
## Decision Matrix
|
||||
|
||||
| Situation | Decision |
|
||||
|-----------|----------|
|
||||
| Critical security issue | 🔴 Block, fix immediately |
|
||||
| Breaking change without migration | 🔴 Block |
|
||||
| Missing error handling | 🟡 Should fix |
|
||||
| No tests for new code | 🟡 Should fix |
|
||||
| Style preference | 🟢 Non-blocking |
|
||||
| Minor naming improvement | 🟢 Non-blocking |
|
||||
| Clever but working code | 💡 Suggest simpler |
|
||||
| Situation | Decision |
|
||||
| --------------------------------- | ------------------------- |
|
||||
| Critical security issue | 🔴 Block, fix immediately |
|
||||
| Breaking change without migration | 🔴 Block |
|
||||
| Missing error handling | 🟡 Should fix |
|
||||
| No tests for new code | 🟡 Should fix |
|
||||
| Style preference | 🟢 Non-blocking |
|
||||
| Minor naming improvement | 🟢 Non-blocking |
|
||||
| Clever but working code | 💡 Suggest simpler |
|
||||
|
||||
---
|
||||
|
||||
## Time Budget
|
||||
|
||||
| PR Size | Target Time |
|
||||
|---------|-------------|
|
||||
| < 100 lines | 10-15 min |
|
||||
| 100-400 lines | 20-40 min |
|
||||
| > 400 lines | Ask to split |
|
||||
| PR Size | Target Time |
|
||||
| ------------- | ------------ |
|
||||
| < 100 lines | 10-15 min |
|
||||
| 100-400 lines | 20-40 min |
|
||||
| > 400 lines | Ask to split |
|
||||
|
||||
---
|
||||
|
||||
|
||||
+79
-53
@@ -7,11 +7,13 @@
|
||||
### S - 单一职责原则 (SRP)
|
||||
|
||||
**检查要点:**
|
||||
|
||||
- 这个类/模块是否只有一个改变的理由?
|
||||
- 类中的方法是否都服务于同一个目的?
|
||||
- 如果要向非技术人员描述这个类,能否用一句话说清楚?
|
||||
|
||||
**代码审查中的识别信号:**
|
||||
|
||||
```
|
||||
⚠️ 类名包含 "And"、"Manager"、"Handler"、"Processor" 等泛化词汇
|
||||
⚠️ 一个类超过 200-300 行代码
|
||||
@@ -20,17 +22,20 @@
|
||||
```
|
||||
|
||||
**审查问题:**
|
||||
|
||||
- "这个类负责哪些事情?能否拆分?"
|
||||
- "如果 X 需求变化,哪些方法需要改?如果 Y 需求变化呢?"
|
||||
|
||||
### O - 开闭原则 (OCP)
|
||||
|
||||
**检查要点:**
|
||||
|
||||
- 添加新功能时,是否需要修改现有代码?
|
||||
- 是否可以通过扩展(继承、组合)来添加新行为?
|
||||
- 是否存在大量的 if/else 或 switch 语句来处理不同类型?
|
||||
|
||||
**代码审查中的识别信号:**
|
||||
|
||||
```
|
||||
⚠️ switch/if-else 链处理不同类型
|
||||
⚠️ 添加新功能需要修改核心类
|
||||
@@ -38,17 +43,20 @@
|
||||
```
|
||||
|
||||
**审查问题:**
|
||||
|
||||
- "如果要添加新的 X 类型,需要修改哪些文件?"
|
||||
- "这个 switch 语句会随着新类型增加而增长吗?"
|
||||
|
||||
### L - 里氏替换原则 (LSP)
|
||||
|
||||
**检查要点:**
|
||||
|
||||
- 子类是否可以完全替代父类使用?
|
||||
- 子类是否改变了父类方法的预期行为?
|
||||
- 是否存在子类抛出父类未声明的异常?
|
||||
|
||||
**代码审查中的识别信号:**
|
||||
|
||||
```
|
||||
⚠️ 显式类型转换 (casting)
|
||||
⚠️ 子类方法抛出 NotImplementedException
|
||||
@@ -57,17 +65,20 @@
|
||||
```
|
||||
|
||||
**审查问题:**
|
||||
|
||||
- "如果用子类替换父类,调用方代码是否需要修改?"
|
||||
- "这个方法在子类中的行为是否符合父类的契约?"
|
||||
|
||||
### I - 接口隔离原则 (ISP)
|
||||
|
||||
**检查要点:**
|
||||
|
||||
- 接口是否足够小且专注?
|
||||
- 实现类是否被迫实现不需要的方法?
|
||||
- 客户端是否依赖了它不使用的方法?
|
||||
|
||||
**代码审查中的识别信号:**
|
||||
|
||||
```
|
||||
⚠️ 接口超过 5-7 个方法
|
||||
⚠️ 实现类有空方法或抛出 NotImplementedException
|
||||
@@ -76,17 +87,20 @@
|
||||
```
|
||||
|
||||
**审查问题:**
|
||||
|
||||
- "这个接口的所有方法是否都被每个实现类使用?"
|
||||
- "能否将这个大接口拆分为更小的专用接口?"
|
||||
|
||||
### D - 依赖倒置原则 (DIP)
|
||||
|
||||
**检查要点:**
|
||||
|
||||
- 高层模块是否依赖于抽象而非具体实现?
|
||||
- 是否使用依赖注入而非直接 new 对象?
|
||||
- 抽象是否由高层模块定义而非低层模块?
|
||||
|
||||
**代码审查中的识别信号:**
|
||||
|
||||
```
|
||||
⚠️ 高层模块直接 new 低层模块的具体类
|
||||
⚠️ 导入具体实现类而非接口/抽象类
|
||||
@@ -95,6 +109,7 @@
|
||||
```
|
||||
|
||||
**审查问题:**
|
||||
|
||||
- "这个类的依赖能否在测试时被 mock 替换?"
|
||||
- "如果要更换数据库/API 实现,需要修改多少地方?"
|
||||
|
||||
@@ -104,21 +119,21 @@
|
||||
|
||||
### 致命反模式
|
||||
|
||||
| 反模式 | 识别信号 | 影响 |
|
||||
|--------|----------|------|
|
||||
| **大泥球 (Big Ball of Mud)** | 没有清晰的模块边界,任何代码都可能调用任何其他代码 | 难以理解、修改和测试 |
|
||||
| **上帝类 (God Object)** | 单个类承担过多职责,知道太多、做太多 | 高耦合,难以重用和测试 |
|
||||
| **意大利面条代码** | 控制流程混乱,goto 或深层嵌套,难以追踪执行路径 | 难以理解和维护 |
|
||||
| **熔岩流 (Lava Flow)** | 没人敢动的古老代码,缺乏文档和测试 | 技术债务累积 |
|
||||
| 反模式 | 识别信号 | 影响 |
|
||||
| ---------------------------- | -------------------------------------------------- | ---------------------- |
|
||||
| **大泥球 (Big Ball of Mud)** | 没有清晰的模块边界,任何代码都可能调用任何其他代码 | 难以理解、修改和测试 |
|
||||
| **上帝类 (God Object)** | 单个类承担过多职责,知道太多、做太多 | 高耦合,难以重用和测试 |
|
||||
| **意大利面条代码** | 控制流程混乱,goto 或深层嵌套,难以追踪执行路径 | 难以理解和维护 |
|
||||
| **熔岩流 (Lava Flow)** | 没人敢动的古老代码,缺乏文档和测试 | 技术债务累积 |
|
||||
|
||||
### 设计反模式
|
||||
|
||||
| 反模式 | 识别信号 | 建议 |
|
||||
|--------|----------|------|
|
||||
| **金锤子 (Golden Hammer)** | 对所有问题使用同一种技术/模式 | 根据问题选择合适的解决方案 |
|
||||
| **过度工程 (Gas Factory)** | 简单问题用复杂方案解决,滥用设计模式 | YAGNI 原则,先简单后复杂 |
|
||||
| **船锚 (Boat Anchor)** | 为"将来可能需要"而写的未使用代码 | 删除未使用代码,需要时再写 |
|
||||
| **复制粘贴编程** | 相同逻辑出现在多处 | 提取公共方法或模块 |
|
||||
| 反模式 | 识别信号 | 建议 |
|
||||
| -------------------------- | ------------------------------------ | -------------------------- |
|
||||
| **金锤子 (Golden Hammer)** | 对所有问题使用同一种技术/模式 | 根据问题选择合适的解决方案 |
|
||||
| **过度工程 (Gas Factory)** | 简单问题用复杂方案解决,滥用设计模式 | YAGNI 原则,先简单后复杂 |
|
||||
| **船锚 (Boat Anchor)** | 为"将来可能需要"而写的未使用代码 | 删除未使用代码,需要时再写 |
|
||||
| **复制粘贴编程** | 相同逻辑出现在多处 | 提取公共方法或模块 |
|
||||
|
||||
### 审查问题
|
||||
|
||||
@@ -134,25 +149,25 @@
|
||||
|
||||
### 耦合类型(从好到差)
|
||||
|
||||
| 类型 | 描述 | 示例 |
|
||||
|------|------|------|
|
||||
| **消息耦合** ✅ | 通过参数传递数据 | `calculate(price, quantity)` |
|
||||
| **数据耦合** ✅ | 共享简单数据结构 | `processOrder(orderDTO)` |
|
||||
| 类型 | 描述 | 示例 |
|
||||
| --------------- | -------------------------- | ----------------------------- |
|
||||
| **消息耦合** ✅ | 通过参数传递数据 | `calculate(price, quantity)` |
|
||||
| **数据耦合** ✅ | 共享简单数据结构 | `processOrder(orderDTO)` |
|
||||
| **印记耦合** ⚠️ | 共享复杂数据结构但只用部分 | 传入整个 User 对象但只用 name |
|
||||
| **控制耦合** ⚠️ | 传递控制标志影响行为 | `process(data, isAdmin=true)` |
|
||||
| **公共耦合** ❌ | 共享全局变量 | 多个模块读写同一个全局状态 |
|
||||
| **内容耦合** ❌ | 直接访问另一模块的内部 | 直接操作另一个类的私有属性 |
|
||||
| **控制耦合** ⚠️ | 传递控制标志影响行为 | `process(data, isAdmin=true)` |
|
||||
| **公共耦合** ❌ | 共享全局变量 | 多个模块读写同一个全局状态 |
|
||||
| **内容耦合** ❌ | 直接访问另一模块的内部 | 直接操作另一个类的私有属性 |
|
||||
|
||||
### 内聚类型(从好到差)
|
||||
|
||||
| 类型 | 描述 | 质量 |
|
||||
|------|------|------|
|
||||
| **功能内聚** | 所有元素完成单一任务 | ✅ 最佳 |
|
||||
| **顺序内聚** | 输出作为下一步输入 | ✅ 良好 |
|
||||
| **通信内聚** | 操作相同数据 | ⚠️ 可接受 |
|
||||
| **时间内聚** | 同时执行的任务 | ⚠️ 较差 |
|
||||
| **逻辑内聚** | 逻辑相关但功能不同 | ❌ 差 |
|
||||
| **偶然内聚** | 没有明显关系 | ❌ 最差 |
|
||||
| 类型 | 描述 | 质量 |
|
||||
| ------------ | -------------------- | --------- |
|
||||
| **功能内聚** | 所有元素完成单一任务 | ✅ 最佳 |
|
||||
| **顺序内聚** | 输出作为下一步输入 | ✅ 良好 |
|
||||
| **通信内聚** | 操作相同数据 | ⚠️ 可接受 |
|
||||
| **时间内聚** | 同时执行的任务 | ⚠️ 较差 |
|
||||
| **逻辑内聚** | 逻辑相关但功能不同 | ❌ 差 |
|
||||
| **偶然内聚** | 没有明显关系 | ❌ 最差 |
|
||||
|
||||
### 度量指标参考
|
||||
|
||||
@@ -220,19 +235,23 @@ interface UserRepository {
|
||||
|
||||
// infrastructure/MySQLUserRepository.ts (实现)
|
||||
class MySQLUserRepository implements UserRepository {
|
||||
findById(id: string): Promise<User> { /* ... */ }
|
||||
findById(id: string): Promise<User> {
|
||||
/* ... */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 审查清单
|
||||
|
||||
**层次边界检查:**
|
||||
|
||||
- [ ] Domain 层是否有外部依赖(数据库、HTTP、文件系统)?
|
||||
- [ ] Application 层是否直接操作数据库或调用外部 API?
|
||||
- [ ] Controller 是否包含业务逻辑?
|
||||
- [ ] 是否存在跨层调用(UI 直接调用 Repository)?
|
||||
|
||||
**关注点分离检查:**
|
||||
|
||||
- [ ] 业务逻辑是否与展示逻辑分离?
|
||||
- [ ] 数据访问是否封装在专门的层?
|
||||
- [ ] 配置和环境相关代码是否集中管理?
|
||||
@@ -251,13 +270,13 @@ class MySQLUserRepository implements UserRepository {
|
||||
|
||||
### 何时使用设计模式
|
||||
|
||||
| 模式 | 适用场景 | 不适用场景 |
|
||||
|------|----------|------------|
|
||||
| **Factory** | 需要创建不同类型对象,类型在运行时确定 | 只有一种类型,或类型固定不变 |
|
||||
| **Strategy** | 算法需要在运行时切换,有多种可互换的行为 | 只有一种算法,或算法不会变化 |
|
||||
| **Observer** | 一对多依赖,状态变化需要通知多个对象 | 简单的直接调用即可满足需求 |
|
||||
| **Singleton** | 确实需要全局唯一实例,如配置管理 | 可以通过依赖注入传递的对象 |
|
||||
| **Decorator** | 需要动态添加职责,避免继承爆炸 | 职责固定,不需要动态组合 |
|
||||
| 模式 | 适用场景 | 不适用场景 |
|
||||
| ------------- | ---------------------------------------- | ---------------------------- |
|
||||
| **Factory** | 需要创建不同类型对象,类型在运行时确定 | 只有一种类型,或类型固定不变 |
|
||||
| **Strategy** | 算法需要在运行时切换,有多种可互换的行为 | 只有一种算法,或算法不会变化 |
|
||||
| **Observer** | 一对多依赖,状态变化需要通知多个对象 | 简单的直接调用即可满足需求 |
|
||||
| **Singleton** | 确实需要全局唯一实例,如配置管理 | 可以通过依赖注入传递的对象 |
|
||||
| **Decorator** | 需要动态添加职责,避免继承爆炸 | 职责固定,不需要动态组合 |
|
||||
|
||||
### 过度设计警告信号
|
||||
|
||||
@@ -275,11 +294,13 @@ class MySQLUserRepository implements UserRepository {
|
||||
|
||||
```markdown
|
||||
✅ 正确使用模式:
|
||||
|
||||
- 解决了实际的可扩展性问题
|
||||
- 代码更容易理解和测试
|
||||
- 添加新功能变得更简单
|
||||
|
||||
❌ 过度使用模式:
|
||||
|
||||
- 为了使用模式而使用
|
||||
- 增加了不必要的复杂度
|
||||
- 违反了 YAGNI 原则
|
||||
@@ -298,16 +319,19 @@ class MySQLUserRepository implements UserRepository {
|
||||
### 扩展性检查清单
|
||||
|
||||
**功能扩展性:**
|
||||
|
||||
- [ ] 添加新功能是否需要修改核心代码?
|
||||
- [ ] 是否提供了扩展点(hooks、plugins、events)?
|
||||
- [ ] 配置是否外部化(配置文件、环境变量)?
|
||||
|
||||
**数据扩展性:**
|
||||
|
||||
- [ ] 数据模型是否支持新增字段?
|
||||
- [ ] 是否考虑了数据量增长的场景?
|
||||
- [ ] 查询是否有合适的索引?
|
||||
|
||||
**负载扩展性:**
|
||||
|
||||
- [ ] 是否可以水平扩展(添加更多实例)?
|
||||
- [ ] 是否有状态依赖(session、本地缓存)?
|
||||
- [ ] 数据库连接是否使用连接池?
|
||||
@@ -330,9 +354,9 @@ class OrderService {
|
||||
// ❌ 差的扩展设计:硬编码所有行为
|
||||
class OrderService {
|
||||
async createOrder(order: Order) {
|
||||
await this.sendEmail(order); // 硬编码
|
||||
await this.updateInventory(order); // 硬编码
|
||||
await this.notifyWarehouse(order); // 硬编码
|
||||
await this.sendEmail(order); // 硬编码
|
||||
await this.updateInventory(order); // 硬编码
|
||||
await this.notifyWarehouse(order); // 硬编码
|
||||
return await this.save(order);
|
||||
}
|
||||
}
|
||||
@@ -353,6 +377,7 @@ class OrderService {
|
||||
### 目录组织
|
||||
|
||||
**按功能/领域组织(推荐):**
|
||||
|
||||
```
|
||||
src/
|
||||
├── user/
|
||||
@@ -370,6 +395,7 @@ src/
|
||||
```
|
||||
|
||||
**按技术层组织(不推荐):**
|
||||
|
||||
```
|
||||
src/
|
||||
├── controllers/ ← 不同领域混在一起
|
||||
@@ -382,13 +408,13 @@ src/
|
||||
|
||||
### 命名约定检查
|
||||
|
||||
| 类型 | 约定 | 示例 |
|
||||
|------|------|------|
|
||||
| 类名 | PascalCase,名词 | `UserService`, `OrderRepository` |
|
||||
| 方法名 | camelCase,动词 | `createUser`, `findOrderById` |
|
||||
| 接口名 | I 前缀或无前缀 | `IUserService` 或 `UserService` |
|
||||
| 常量 | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT` |
|
||||
| 私有属性 | 下划线前缀或无 | `_cache` 或 `#cache` |
|
||||
| 类型 | 约定 | 示例 |
|
||||
| -------- | ---------------- | -------------------------------- |
|
||||
| 类名 | PascalCase,名词 | `UserService`, `OrderRepository` |
|
||||
| 方法名 | camelCase,动词 | `createUser`, `findOrderById` |
|
||||
| 接口名 | I 前缀或无前缀 | `IUserService` 或 `UserService` |
|
||||
| 常量 | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT` |
|
||||
| 私有属性 | 下划线前缀或无 | `_cache` 或 `#cache` |
|
||||
|
||||
### 文件大小指南
|
||||
|
||||
@@ -452,14 +478,14 @@ src/
|
||||
|
||||
## 工具推荐
|
||||
|
||||
| 工具 | 用途 | 语言支持 |
|
||||
|------|------|----------|
|
||||
| **SonarQube** | 代码质量、耦合度分析 | 多语言 |
|
||||
| **NDepend** | 依赖分析、架构规则 | .NET |
|
||||
| **JDepend** | 包依赖分析 | Java |
|
||||
| **Madge** | 模块依赖图 | JavaScript/TypeScript |
|
||||
| **ESLint** | 代码规范、复杂度检查 | JavaScript/TypeScript |
|
||||
| **CodeScene** | 技术债务、热点分析 | 多语言 |
|
||||
| 工具 | 用途 | 语言支持 |
|
||||
| ------------- | -------------------- | --------------------- |
|
||||
| **SonarQube** | 代码质量、耦合度分析 | 多语言 |
|
||||
| **NDepend** | 依赖分析、架构规则 | .NET |
|
||||
| **JDepend** | 包依赖分析 | Java |
|
||||
| **Madge** | 模块依赖图 | JavaScript/TypeScript |
|
||||
| **ESLint** | 代码规范、复杂度检查 | JavaScript/TypeScript |
|
||||
| **CodeScene** | 技术债务、热点分析 | 多语言 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -264,22 +264,26 @@ clang-format -i src/*.c include/*.h
|
||||
## Review Checklist
|
||||
|
||||
### Memory and UB
|
||||
|
||||
- [ ] All buffers have explicit size parameters
|
||||
- [ ] No out-of-bounds access or pointer arithmetic past objects
|
||||
- [ ] No use after free or uninitialized reads
|
||||
- [ ] Signed overflow and shift rules are respected
|
||||
|
||||
### API and Design
|
||||
|
||||
- [ ] Ownership rules are documented and consistent
|
||||
- [ ] const-correctness is applied for inputs
|
||||
- [ ] Error contracts are clear and consistent
|
||||
|
||||
### Concurrency
|
||||
|
||||
- [ ] No data races on shared state
|
||||
- [ ] volatile is not used for synchronization
|
||||
- [ ] Locks are held for minimal time
|
||||
|
||||
### Tooling and Tests
|
||||
|
||||
- [ ] Builds clean with warnings enabled
|
||||
- [ ] Sanitizers run on critical code paths
|
||||
- [ ] Static analysis results are addressed
|
||||
|
||||
+19
-5
@@ -7,6 +7,7 @@ Comprehensive guidelines for conducting effective code reviews.
|
||||
### Goals of Code Review
|
||||
|
||||
**Primary Goals:**
|
||||
|
||||
- Catch bugs and edge cases before production
|
||||
- Ensure code maintainability and readability
|
||||
- Share knowledge across the team
|
||||
@@ -14,6 +15,7 @@ Comprehensive guidelines for conducting effective code reviews.
|
||||
- Improve design and architecture decisions
|
||||
|
||||
**Secondary Goals:**
|
||||
|
||||
- Mentor junior developers
|
||||
- Build team culture and trust
|
||||
- Document design decisions through discussions
|
||||
@@ -29,11 +31,11 @@ Comprehensive guidelines for conducting effective code reviews.
|
||||
|
||||
### When to Review
|
||||
|
||||
| Trigger | Action |
|
||||
|---------|--------|
|
||||
| PR opened | Review within 24 hours, ideally same day |
|
||||
| Changes requested | Re-review within 4 hours |
|
||||
| Blocking issue found | Communicate immediately |
|
||||
| Trigger | Action |
|
||||
| -------------------- | ---------------------------------------- |
|
||||
| PR opened | Review within 24 hours, ideally same day |
|
||||
| Changes requested | Re-review within 4 hours |
|
||||
| Blocking issue found | Communicate immediately |
|
||||
|
||||
### Time Allocation
|
||||
|
||||
@@ -44,18 +46,21 @@ Comprehensive guidelines for conducting effective code reviews.
|
||||
## Review Depth Levels
|
||||
|
||||
### Level 1: Skim Review (5 minutes)
|
||||
|
||||
- Check PR description and linked issues
|
||||
- Verify CI/CD status
|
||||
- Look at file changes overview
|
||||
- Identify if deeper review needed
|
||||
|
||||
### Level 2: Standard Review (20-30 minutes)
|
||||
|
||||
- Full code walkthrough
|
||||
- Logic verification
|
||||
- Test coverage check
|
||||
- Security scan
|
||||
|
||||
### Level 3: Deep Review (60+ minutes)
|
||||
|
||||
- Architecture evaluation
|
||||
- Performance analysis
|
||||
- Security audit
|
||||
@@ -66,11 +71,13 @@ Comprehensive guidelines for conducting effective code reviews.
|
||||
### Tone and Language
|
||||
|
||||
**Use collaborative language:**
|
||||
|
||||
- "What do you think about..." instead of "You should..."
|
||||
- "Could we consider..." instead of "This is wrong"
|
||||
- "I'm curious about..." instead of "Why didn't you..."
|
||||
|
||||
**Be specific and actionable:**
|
||||
|
||||
- Include code examples when suggesting changes
|
||||
- Link to documentation or past discussions
|
||||
- Explain the "why" behind suggestions
|
||||
@@ -86,6 +93,7 @@ Comprehensive guidelines for conducting effective code reviews.
|
||||
## Review Prioritization
|
||||
|
||||
### Must Fix (Blocking)
|
||||
|
||||
- Security vulnerabilities
|
||||
- Data corruption risks
|
||||
- Breaking changes without migration
|
||||
@@ -93,6 +101,7 @@ Comprehensive guidelines for conducting effective code reviews.
|
||||
- Missing error handling for user-facing features
|
||||
|
||||
### Should Fix (Important)
|
||||
|
||||
- Test coverage gaps
|
||||
- Moderate performance concerns
|
||||
- Code duplication
|
||||
@@ -100,6 +109,7 @@ Comprehensive guidelines for conducting effective code reviews.
|
||||
- Missing documentation for complex logic
|
||||
|
||||
### Nice to Have (Non-blocking)
|
||||
|
||||
- Style preferences beyond linting
|
||||
- Minor optimizations
|
||||
- Additional test cases
|
||||
@@ -108,6 +118,7 @@ Comprehensive guidelines for conducting effective code reviews.
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
### Reviewer Anti-Patterns
|
||||
|
||||
- **Rubber stamping**: Approving without actually reviewing
|
||||
- **Bike shedding**: Debating trivial details extensively
|
||||
- **Scope creep**: "While you're at it, can you also..."
|
||||
@@ -115,6 +126,7 @@ Comprehensive guidelines for conducting effective code reviews.
|
||||
- **Perfectionism**: Blocking for minor style preferences
|
||||
|
||||
### Author Anti-Patterns
|
||||
|
||||
- **Mega PRs**: Submitting 1000+ line changes
|
||||
- **No context**: Missing PR description or linked issues
|
||||
- **Defensive responses**: Arguing every suggestion
|
||||
@@ -123,6 +135,7 @@ Comprehensive guidelines for conducting effective code reviews.
|
||||
## Metrics and Improvement
|
||||
|
||||
### Track These Metrics
|
||||
|
||||
- Time to first review
|
||||
- Review cycle time
|
||||
- Number of review rounds
|
||||
@@ -130,6 +143,7 @@ Comprehensive guidelines for conducting effective code reviews.
|
||||
- Review coverage percentage
|
||||
|
||||
### Continuous Improvement
|
||||
|
||||
- Hold retrospectives on review process
|
||||
- Share learnings from escaped bugs
|
||||
- Update checklists based on common issues
|
||||
|
||||
+126
-75
@@ -5,6 +5,7 @@ Language-specific bugs and issues to watch for during code review.
|
||||
## Universal Issues
|
||||
|
||||
### Logic Errors
|
||||
|
||||
- [ ] Off-by-one errors in loops and array access
|
||||
- [ ] Incorrect boolean logic (De Morgan's law violations)
|
||||
- [ ] Missing null/undefined checks
|
||||
@@ -14,6 +15,7 @@ Language-specific bugs and issues to watch for during code review.
|
||||
- [ ] Floating point comparison issues
|
||||
|
||||
### Resource Management
|
||||
|
||||
- [ ] Memory leaks (unclosed connections, listeners)
|
||||
- [ ] File handles not closed
|
||||
- [ ] Database connections not released
|
||||
@@ -21,6 +23,7 @@ Language-specific bugs and issues to watch for during code review.
|
||||
- [ ] Timers/intervals not cleared
|
||||
|
||||
### Error Handling
|
||||
|
||||
- [ ] Swallowed exceptions (empty catch blocks)
|
||||
- [ ] Generic exception handling hiding specific errors
|
||||
- [ ] Missing error propagation
|
||||
@@ -30,26 +33,34 @@ Language-specific bugs and issues to watch for during code review.
|
||||
## TypeScript/JavaScript
|
||||
|
||||
### Type Issues
|
||||
|
||||
```typescript
|
||||
// ❌ Using any defeats type safety
|
||||
function process(data: any) { return data.value; }
|
||||
function process(data: any) {
|
||||
return data.value;
|
||||
}
|
||||
|
||||
// ✅ Use proper types
|
||||
interface Data { value: string; }
|
||||
function process(data: Data) { return data.value; }
|
||||
interface Data {
|
||||
value: string;
|
||||
}
|
||||
function process(data: Data) {
|
||||
return data.value;
|
||||
}
|
||||
```
|
||||
|
||||
### Async/Await Pitfalls
|
||||
|
||||
```typescript
|
||||
// ❌ Missing await
|
||||
async function fetch() {
|
||||
const data = fetchData(); // Missing await!
|
||||
const data = fetchData(); // Missing await!
|
||||
return data.json();
|
||||
}
|
||||
|
||||
// ❌ Unhandled promise rejection
|
||||
async function risky() {
|
||||
const result = await fetchData(); // No try-catch
|
||||
const result = await fetchData(); // No try-catch
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -68,11 +79,12 @@ async function safe() {
|
||||
### React Specific
|
||||
|
||||
#### Hooks 规则违反
|
||||
|
||||
```tsx
|
||||
// ❌ 条件调用 Hooks — 违反 Hooks 规则
|
||||
function BadComponent({ show }) {
|
||||
if (show) {
|
||||
const [value, setValue] = useState(0); // Error!
|
||||
const [value, setValue] = useState(0); // Error!
|
||||
}
|
||||
return <div>...</div>;
|
||||
}
|
||||
@@ -86,36 +98,35 @@ function GoodComponent({ show }) {
|
||||
|
||||
// ❌ 循环中调用 Hooks
|
||||
function BadLoop({ items }) {
|
||||
items.forEach(item => {
|
||||
const [selected, setSelected] = useState(false); // Error!
|
||||
items.forEach((item) => {
|
||||
const [selected, setSelected] = useState(false); // Error!
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ 将状态提升或使用不同的数据结构
|
||||
function GoodLoop({ items }) {
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||
return items.map(item => (
|
||||
<Item key={item.id} selected={selectedIds.has(item.id)} />
|
||||
));
|
||||
return items.map((item) => <Item key={item.id} selected={selectedIds.has(item.id)} />);
|
||||
}
|
||||
```
|
||||
|
||||
#### useEffect 常见错误
|
||||
|
||||
```tsx
|
||||
// ❌ 依赖数组不完整 — stale closure
|
||||
function StaleClosureExample({ userId, onSuccess }) {
|
||||
const [data, setData] = useState(null);
|
||||
useEffect(() => {
|
||||
fetchData(userId).then(result => {
|
||||
fetchData(userId).then((result) => {
|
||||
setData(result);
|
||||
onSuccess(result); // onSuccess 可能是 stale 的!
|
||||
onSuccess(result); // onSuccess 可能是 stale 的!
|
||||
});
|
||||
}, [userId]); // 缺少 onSuccess 依赖
|
||||
}, [userId]); // 缺少 onSuccess 依赖
|
||||
}
|
||||
|
||||
// ✅ 完整的依赖数组
|
||||
useEffect(() => {
|
||||
fetchData(userId).then(result => {
|
||||
fetchData(userId).then((result) => {
|
||||
setData(result);
|
||||
onSuccess(result);
|
||||
});
|
||||
@@ -125,15 +136,15 @@ useEffect(() => {
|
||||
function InfiniteLoop() {
|
||||
const [count, setCount] = useState(0);
|
||||
useEffect(() => {
|
||||
setCount(count + 1); // 触发重渲染,又触发 effect
|
||||
}, [count]); // 无限循环!
|
||||
setCount(count + 1); // 触发重渲染,又触发 effect
|
||||
}, [count]); // 无限循环!
|
||||
}
|
||||
|
||||
// ❌ 缺少清理函数 — 内存泄漏
|
||||
function MemoryLeak({ userId }) {
|
||||
const [user, setUser] = useState(null);
|
||||
useEffect(() => {
|
||||
fetchUser(userId).then(setUser); // 组件卸载后仍然调用 setUser
|
||||
fetchUser(userId).then(setUser); // 组件卸载后仍然调用 setUser
|
||||
}, [userId]);
|
||||
}
|
||||
|
||||
@@ -142,10 +153,12 @@ function NoLeak({ userId }) {
|
||||
const [user, setUser] = useState(null);
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
fetchUser(userId).then(data => {
|
||||
fetchUser(userId).then((data) => {
|
||||
if (!cancelled) setUser(data);
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [userId]);
|
||||
}
|
||||
|
||||
@@ -154,22 +167,19 @@ function BadDerived({ items }) {
|
||||
const [total, setTotal] = useState(0);
|
||||
useEffect(() => {
|
||||
setTotal(items.reduce((a, b) => a + b.price, 0));
|
||||
}, [items]); // 不必要的 effect + 额外渲染
|
||||
}, [items]); // 不必要的 effect + 额外渲染
|
||||
}
|
||||
|
||||
// ✅ 直接计算或用 useMemo
|
||||
function GoodDerived({ items }) {
|
||||
const total = useMemo(
|
||||
() => items.reduce((a, b) => a + b.price, 0),
|
||||
[items]
|
||||
);
|
||||
const total = useMemo(() => items.reduce((a, b) => a + b.price, 0), [items]);
|
||||
}
|
||||
|
||||
// ❌ useEffect 用于事件响应
|
||||
function BadEvent() {
|
||||
const [query, setQuery] = useState('');
|
||||
useEffect(() => {
|
||||
if (query) logSearch(query); // 应该在事件处理器中
|
||||
if (query) logSearch(query); // 应该在事件处理器中
|
||||
}, [query]);
|
||||
}
|
||||
|
||||
@@ -183,11 +193,12 @@ function GoodEvent() {
|
||||
```
|
||||
|
||||
#### useMemo / useCallback 误用
|
||||
|
||||
```tsx
|
||||
// ❌ 过度优化 — 常量不需要 memo
|
||||
function OverOptimized() {
|
||||
const config = useMemo(() => ({ api: '/v1' }), []); // 无意义
|
||||
const noop = useCallback(() => {}, []); // 无意义
|
||||
const config = useMemo(() => ({ api: '/v1' }), []); // 无意义
|
||||
const noop = useCallback(() => {}, []); // 无意义
|
||||
}
|
||||
|
||||
// ❌ 空依赖的 useMemo(可能隐藏 bug)
|
||||
@@ -200,7 +211,7 @@ function EmptyDeps({ user }) {
|
||||
function UselessCallback({ data }) {
|
||||
const process = useCallback(() => {
|
||||
return data.map(transform);
|
||||
}, [data]); // 如果 data 每次都是新引用,完全无效
|
||||
}, [data]); // 如果 data 每次都是新引用,完全无效
|
||||
}
|
||||
|
||||
// ❌ useMemo/useCallback 没有配合 React.memo
|
||||
@@ -224,6 +235,7 @@ function Parent() {
|
||||
```
|
||||
|
||||
#### 组件设计问题
|
||||
|
||||
```tsx
|
||||
// ❌ 在组件内定义组件
|
||||
function Parent() {
|
||||
@@ -242,21 +254,22 @@ function Parent() {
|
||||
function BadProps() {
|
||||
return (
|
||||
<MemoComponent
|
||||
style={{ color: 'red' }} // 每次渲染新对象
|
||||
onClick={() => handle()} // 每次渲染新函数
|
||||
items={data.filter(x => x)} // 每次渲染新数组
|
||||
style={{ color: 'red' }} // 每次渲染新对象
|
||||
onClick={() => handle()} // 每次渲染新函数
|
||||
items={data.filter((x) => x)} // 每次渲染新数组
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// ❌ 直接修改 props
|
||||
function MutateProps({ user }) {
|
||||
user.name = 'Changed'; // 永远不要这样做!
|
||||
user.name = 'Changed'; // 永远不要这样做!
|
||||
return <div>{user.name}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
#### Server Components 错误 (React 19+)
|
||||
|
||||
```tsx
|
||||
// ❌ 在 Server Component 中使用客户端 API
|
||||
// app/page.tsx (默认是 Server Component)
|
||||
@@ -288,10 +301,11 @@ export default function Layout({ children }) { ... }
|
||||
```
|
||||
|
||||
#### 测试常见错误
|
||||
|
||||
```tsx
|
||||
// ❌ 使用 container 查询
|
||||
const { container } = render(<Component />);
|
||||
const button = container.querySelector('button'); // 不推荐
|
||||
const button = container.querySelector('button'); // 不推荐
|
||||
|
||||
// ✅ 使用 screen 和语义查询
|
||||
render(<Component />);
|
||||
@@ -310,13 +324,14 @@ expect(component.state.isOpen).toBe(true);
|
||||
expect(screen.getByRole('dialog')).toBeVisible();
|
||||
|
||||
// ❌ 等待同步查询
|
||||
await screen.getByText('Hello'); // getBy 是同步的
|
||||
await screen.getByText('Hello'); // getBy 是同步的
|
||||
|
||||
// ✅ 异步用 findBy
|
||||
await screen.findByText('Hello'); // findBy 会等待
|
||||
await screen.findByText('Hello'); // findBy 会等待
|
||||
```
|
||||
|
||||
### React Common Mistakes Checklist
|
||||
|
||||
- [ ] Hooks 不在顶层调用(条件/循环中)
|
||||
- [ ] useEffect 依赖数组不完整
|
||||
- [ ] useEffect 缺少清理函数
|
||||
@@ -339,18 +354,18 @@ await screen.findByText('Hello'); // findBy 会等待
|
||||
|
||||
// ❌ 在 Action 中直接 setState 而不是返回状态
|
||||
const [state, action] = useActionState(async (prev, formData) => {
|
||||
setSomeState(newValue); // 错误!应该返回新状态
|
||||
setSomeState(newValue); // 错误!应该返回新状态
|
||||
}, initialState);
|
||||
|
||||
// ✅ 返回新状态
|
||||
const [state, action] = useActionState(async (prev, formData) => {
|
||||
const result = await submitForm(formData);
|
||||
return { ...prev, data: result }; // 返回新状态
|
||||
return { ...prev, data: result }; // 返回新状态
|
||||
}, initialState);
|
||||
|
||||
// ❌ 忘记处理 isPending
|
||||
const [state, action] = useActionState(submitAction, null);
|
||||
return <button>Submit</button>; // 用户可以重复点击
|
||||
return <button>Submit</button>; // 用户可以重复点击
|
||||
|
||||
// ✅ 使用 isPending 禁用按钮
|
||||
const [state, action, isPending] = useActionState(submitAction, null);
|
||||
@@ -360,8 +375,12 @@ return <button disabled={isPending}>Submit</button>;
|
||||
|
||||
// ❌ 在 form 同级调用 useFormStatus
|
||||
function Form() {
|
||||
const { pending } = useFormStatus(); // 永远是 undefined!
|
||||
return <form><button disabled={pending}>Submit</button></form>;
|
||||
const { pending } = useFormStatus(); // 永远是 undefined!
|
||||
return (
|
||||
<form>
|
||||
<button disabled={pending}>Submit</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
// ✅ 在子组件中调用
|
||||
@@ -370,7 +389,11 @@ function SubmitButton() {
|
||||
return <button disabled={pending}>Submit</button>;
|
||||
}
|
||||
function Form() {
|
||||
return <form><SubmitButton /></form>;
|
||||
return (
|
||||
<form>
|
||||
<SubmitButton />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
// === useOptimistic 错误 ===
|
||||
@@ -379,7 +402,7 @@ function Form() {
|
||||
function PaymentButton() {
|
||||
const [optimisticPaid, setPaid] = useOptimistic(false);
|
||||
const handlePay = async () => {
|
||||
setPaid(true); // 危险:显示已支付但可能失败
|
||||
setPaid(true); // 危险:显示已支付但可能失败
|
||||
await processPayment();
|
||||
};
|
||||
}
|
||||
@@ -394,12 +417,13 @@ const handleLike = async () => {
|
||||
try {
|
||||
await likePost();
|
||||
} catch {
|
||||
toast.error('点赞失败,请重试'); // 通知用户
|
||||
toast.error('点赞失败,请重试'); // 通知用户
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### React 19 Forms Checklist
|
||||
|
||||
- [ ] useActionState 返回新状态而不是 setState
|
||||
- [ ] useActionState 正确使用 isPending 禁用提交
|
||||
- [ ] useFormStatus 在 form 子组件中调用
|
||||
@@ -416,9 +440,9 @@ const handleLike = async () => {
|
||||
function BadPage() {
|
||||
return (
|
||||
<Suspense fallback={<FullPageLoader />}>
|
||||
<FastHeader /> {/* 快 */}
|
||||
<FastHeader /> {/* 快 */}
|
||||
<SlowMainContent /> {/* 慢——阻塞整个页面 */}
|
||||
<FastFooter /> {/* 快 */}
|
||||
<FastFooter /> {/* 快 */}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -440,7 +464,7 @@ function GoodPage() {
|
||||
function NoErrorHandling() {
|
||||
return (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<DataFetcher /> {/* 抛错导致白屏 */}
|
||||
<DataFetcher /> {/* 抛错导致白屏 */}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -460,7 +484,7 @@ function WithErrorHandling() {
|
||||
|
||||
// ❌ 在组件外创建 Promise(每次渲染新 Promise)
|
||||
function BadUse() {
|
||||
const data = use(fetchData()); // 每次渲染都创建新 Promise!
|
||||
const data = use(fetchData()); // 每次渲染都创建新 Promise!
|
||||
return <div>{data}</div>;
|
||||
}
|
||||
|
||||
@@ -479,7 +503,7 @@ function Child({ dataPromise }) {
|
||||
// ❌ 在 layout.tsx 中 await 慢数据——阻塞所有子页面
|
||||
// app/layout.tsx
|
||||
export default async function Layout({ children }) {
|
||||
const config = await fetchSlowConfig(); // 阻塞整个应用!
|
||||
const config = await fetchSlowConfig(); // 阻塞整个应用!
|
||||
return <ConfigProvider value={config}>{children}</ConfigProvider>;
|
||||
}
|
||||
|
||||
@@ -495,6 +519,7 @@ export default function Layout({ children }) {
|
||||
```
|
||||
|
||||
### Suspense Checklist
|
||||
|
||||
- [ ] 慢内容有独立的 Suspense 边界
|
||||
- [ ] 每个 Suspense 有对应的 Error Boundary
|
||||
- [ ] fallback 是有意义的骨架屏(不是简单 spinner)
|
||||
@@ -510,7 +535,7 @@ export default function Layout({ children }) {
|
||||
// ❌ queryKey 不包含查询参数
|
||||
function BadQuery({ userId, filters }) {
|
||||
const { data } = useQuery({
|
||||
queryKey: ['users'], // 缺少 userId 和 filters!
|
||||
queryKey: ['users'], // 缺少 userId 和 filters!
|
||||
queryFn: () => fetchUsers(userId, filters),
|
||||
});
|
||||
// userId 或 filters 变化时数据不会更新
|
||||
@@ -535,7 +560,7 @@ const { data } = useQuery({
|
||||
const { data } = useQuery({
|
||||
queryKey: ['data'],
|
||||
queryFn: fetchData,
|
||||
staleTime: 5 * 60 * 1000, // 5 分钟内不会自动 refetch
|
||||
staleTime: 5 * 60 * 1000, // 5 分钟内不会自动 refetch
|
||||
});
|
||||
|
||||
// === useSuspenseQuery 错误 ===
|
||||
@@ -544,7 +569,7 @@ const { data } = useQuery({
|
||||
const { data } = useSuspenseQuery({
|
||||
queryKey: ['user', userId],
|
||||
queryFn: () => fetchUser(userId),
|
||||
enabled: !!userId, // 错误!useSuspenseQuery 不支持 enabled
|
||||
enabled: !!userId, // 错误!useSuspenseQuery 不支持 enabled
|
||||
});
|
||||
|
||||
// ✅ 条件渲染实现
|
||||
@@ -610,7 +635,7 @@ const mutation = useMutation({
|
||||
// === v5 迁移错误 ===
|
||||
|
||||
// ❌ 使用废弃的 API
|
||||
const { data, isLoading } = useQuery(['key'], fetchFn); // v4 语法
|
||||
const { data, isLoading } = useQuery(['key'], fetchFn); // v4 语法
|
||||
|
||||
// ✅ v5 单一对象参数
|
||||
const { data, isPending } = useQuery({
|
||||
@@ -623,12 +648,13 @@ if (isLoading) return <Spinner />;
|
||||
// v5 中 isLoading = isPending && isFetching
|
||||
|
||||
// ✅ 根据意图选择
|
||||
if (isPending) return <Spinner />; // 没有缓存数据
|
||||
if (isPending) return <Spinner />; // 没有缓存数据
|
||||
// 或
|
||||
if (isFetching) return <Refreshing />; // 正在后台刷新
|
||||
if (isFetching) return <Refreshing />; // 正在后台刷新
|
||||
```
|
||||
|
||||
### TanStack Query Checklist
|
||||
|
||||
- [ ] queryKey 包含所有影响数据的参数
|
||||
- [ ] 设置了合理的 staleTime(不是默认 0)
|
||||
- [ ] useSuspenseQuery 不使用 enabled
|
||||
@@ -638,6 +664,7 @@ if (isFetching) return <Refreshing />; // 正在后台刷新
|
||||
- [ ] 理解 isPending vs isLoading vs isFetching
|
||||
|
||||
### TypeScript/JavaScript Common Mistakes
|
||||
|
||||
- [ ] `==` instead of `===`
|
||||
- [ ] Modifying array/object during iteration
|
||||
- [ ] `this` context lost in callbacks
|
||||
@@ -648,21 +675,23 @@ if (isFetching) return <Refreshing />; // 正在后台刷新
|
||||
## Vue 3
|
||||
|
||||
### 响应性丢失
|
||||
|
||||
```vue
|
||||
<!-- ❌ 解构 reactive 丢失响应性 -->
|
||||
<script setup>
|
||||
const state = reactive({ count: 0 })
|
||||
const { count } = state // count 不是响应式的!
|
||||
const state = reactive({ count: 0 });
|
||||
const { count } = state; // count 不是响应式的!
|
||||
</script>
|
||||
|
||||
<!-- ✅ 使用 toRefs -->
|
||||
<script setup>
|
||||
const state = reactive({ count: 0 })
|
||||
const { count } = toRefs(state) // count.value 是响应式的
|
||||
const state = reactive({ count: 0 });
|
||||
const { count } = toRefs(state); // count.value 是响应式的
|
||||
</script>
|
||||
```
|
||||
|
||||
### Props 响应性传递
|
||||
|
||||
```vue
|
||||
<!-- ❌ 传递 props 值到 composable 丢失响应性 -->
|
||||
<script setup>
|
||||
@@ -680,48 +709,53 @@ const { data } = useFetch(toRef(props, 'id'))
|
||||
```
|
||||
|
||||
### Watch 清理
|
||||
|
||||
```vue
|
||||
<!-- ❌ 异步 watch 无清理,导致竞态 -->
|
||||
<script setup>
|
||||
watch(id, async (newId) => {
|
||||
const data = await fetchData(newId)
|
||||
result.value = data // 旧请求可能覆盖新结果!
|
||||
})
|
||||
const data = await fetchData(newId);
|
||||
result.value = data; // 旧请求可能覆盖新结果!
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- ✅ 使用 onCleanup 取消旧请求 -->
|
||||
<script setup>
|
||||
watch(id, async (newId, _, onCleanup) => {
|
||||
const controller = new AbortController()
|
||||
onCleanup(() => controller.abort())
|
||||
const controller = new AbortController();
|
||||
onCleanup(() => controller.abort());
|
||||
|
||||
const data = await fetchData(newId, controller.signal)
|
||||
result.value = data
|
||||
})
|
||||
const data = await fetchData(newId, controller.signal);
|
||||
result.value = data;
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### Computed 副作用
|
||||
|
||||
```vue
|
||||
<!-- ❌ computed 中修改其他状态 -->
|
||||
<script setup>
|
||||
const total = computed(() => {
|
||||
sideEffect.value++ // 副作用!每次访问都会执行
|
||||
return items.value.reduce((a, b) => a + b, 0)
|
||||
})
|
||||
sideEffect.value++; // 副作用!每次访问都会执行
|
||||
return items.value.reduce((a, b) => a + b, 0);
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- ✅ computed 只做纯计算 -->
|
||||
<script setup>
|
||||
const total = computed(() => {
|
||||
return items.value.reduce((a, b) => a + b, 0)
|
||||
})
|
||||
return items.value.reduce((a, b) => a + b, 0);
|
||||
});
|
||||
// 副作用放 watch
|
||||
watch(total, () => { sideEffect.value++ })
|
||||
watch(total, () => {
|
||||
sideEffect.value++;
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### 模板常见错误
|
||||
|
||||
```vue
|
||||
<!-- ❌ v-if 和 v-for 同时使用(v-if 优先级更高) -->
|
||||
<template>
|
||||
@@ -739,6 +773,7 @@ watch(total, () => { sideEffect.value++ })
|
||||
```
|
||||
|
||||
### Common Mistakes
|
||||
|
||||
- [ ] 解构 reactive 对象丢失响应性
|
||||
- [ ] props 传递给 composable 时未保持响应性
|
||||
- [ ] watch 异步回调无清理函数
|
||||
@@ -753,6 +788,7 @@ watch(total, () => { sideEffect.value++ })
|
||||
## Python
|
||||
|
||||
### Mutable Default Arguments
|
||||
|
||||
```python
|
||||
# ❌ Bug: List shared across all calls
|
||||
def add_item(item, items=[]):
|
||||
@@ -768,6 +804,7 @@ def add_item(item, items=None):
|
||||
```
|
||||
|
||||
### Exception Handling
|
||||
|
||||
```python
|
||||
# ❌ Catching everything, including KeyboardInterrupt
|
||||
try:
|
||||
@@ -784,6 +821,7 @@ except ValueError as e:
|
||||
```
|
||||
|
||||
### Class Attributes
|
||||
|
||||
```python
|
||||
# ❌ Shared mutable class attribute
|
||||
class User:
|
||||
@@ -796,6 +834,7 @@ class User:
|
||||
```
|
||||
|
||||
### Common Mistakes
|
||||
|
||||
- [ ] Using `is` instead of `==` for value comparison
|
||||
- [ ] Forgetting `self` parameter in methods
|
||||
- [ ] Modifying list while iterating
|
||||
@@ -1139,18 +1178,21 @@ struct Good<'a> {
|
||||
### Rust 审查清单
|
||||
|
||||
**所有权与借用**
|
||||
|
||||
- [ ] clone() 是有意为之,不是绕过借用检查器
|
||||
- [ ] 避免在结构体中存储借用(除非必要)
|
||||
- [ ] Rc/Arc 使用合理,没有隐藏不必要的共享状态
|
||||
- [ ] 没有不必要的 RefCell(运行时检查 vs 编译时)
|
||||
|
||||
**Unsafe 代码**
|
||||
|
||||
- [ ] 每个 unsafe 块有 SAFETY 注释
|
||||
- [ ] unsafe fn 有 # Safety 文档
|
||||
- [ ] 安全不变量被清晰记录
|
||||
- [ ] unsafe 边界尽可能小
|
||||
|
||||
**异步/并发**
|
||||
|
||||
- [ ] 没有在异步上下文中阻塞
|
||||
- [ ] 没有跨 .await 持有 std::sync 锁
|
||||
- [ ] spawn 的任务满足 'static 约束
|
||||
@@ -1158,25 +1200,29 @@ struct Good<'a> {
|
||||
- [ ] 锁的顺序一致(避免死锁)
|
||||
|
||||
**错误处理**
|
||||
|
||||
- [ ] 库代码使用 thiserror,应用代码使用 anyhow
|
||||
- [ ] 错误有足够的上下文信息
|
||||
- [ ] 没有在生产代码中 unwrap/expect
|
||||
- [ ] must_use 返回值被正确处理
|
||||
|
||||
**性能**
|
||||
|
||||
- [ ] 避免不必要的 collect()
|
||||
- [ ] 大数据结构传引用
|
||||
- [ ] 字符串拼接使用 String::with_capacity 或 write!
|
||||
- [ ] impl Trait 优于 Box<dyn Trait>(当可能时)
|
||||
|
||||
**类型系统**
|
||||
|
||||
- [ ] 善用 newtype 模式增加类型安全
|
||||
- [ ] 枚举穷尽匹配(没有 _ 通配符隐藏新变体)
|
||||
- [ ] 枚举穷尽匹配(没有 \_ 通配符隐藏新变体)
|
||||
- [ ] 生命周期尽可能简化
|
||||
|
||||
## SQL
|
||||
|
||||
### Injection Vulnerabilities
|
||||
|
||||
```sql
|
||||
-- ❌ String concatenation (SQL injection risk)
|
||||
query = "SELECT * FROM users WHERE id = " + user_id
|
||||
@@ -1187,13 +1233,15 @@ cursor.execute(query, (user_id,))
|
||||
```
|
||||
|
||||
### Performance Issues
|
||||
|
||||
- [ ] Missing indexes on filtered/joined columns
|
||||
- [ ] SELECT * instead of specific columns
|
||||
- [ ] SELECT \* instead of specific columns
|
||||
- [ ] N+1 query patterns
|
||||
- [ ] Missing LIMIT on large tables
|
||||
- [ ] Inefficient subqueries vs JOINs
|
||||
|
||||
### Common Mistakes
|
||||
|
||||
- [ ] Not handling NULL comparisons correctly
|
||||
- [ ] Missing transactions for related operations
|
||||
- [ ] Incorrect JOIN types
|
||||
@@ -1203,6 +1251,7 @@ cursor.execute(query, (user_id,))
|
||||
## API Design
|
||||
|
||||
### REST Issues
|
||||
|
||||
- [ ] Inconsistent resource naming
|
||||
- [ ] Wrong HTTP methods (POST for idempotent operations)
|
||||
- [ ] Missing pagination for list endpoints
|
||||
@@ -1210,6 +1259,7 @@ cursor.execute(query, (user_id,))
|
||||
- [ ] Missing rate limiting
|
||||
|
||||
### Data Validation
|
||||
|
||||
- [ ] Missing input validation
|
||||
- [ ] Incorrect data type validation
|
||||
- [ ] Missing length/range checks
|
||||
@@ -1219,6 +1269,7 @@ cursor.execute(query, (user_id,))
|
||||
## Testing
|
||||
|
||||
### Test Quality Issues
|
||||
|
||||
- [ ] Testing implementation details instead of behavior
|
||||
- [ ] Missing edge case tests
|
||||
- [ ] Flaky tests (non-deterministic)
|
||||
|
||||
@@ -357,6 +357,7 @@ clang-format -i src/*.cpp include/*.h
|
||||
## Review Checklist
|
||||
|
||||
### Safety and Lifetime
|
||||
|
||||
- [ ] Ownership is explicit (RAII, unique_ptr by default)
|
||||
- [ ] No dangling references or views
|
||||
- [ ] Rule of 0/3/5 followed for resource-owning types
|
||||
@@ -364,22 +365,26 @@ clang-format -i src/*.cpp include/*.h
|
||||
- [ ] Destructors are noexcept and do not throw
|
||||
|
||||
### API and Design
|
||||
|
||||
- [ ] const-correctness is applied consistently
|
||||
- [ ] Constructors are explicit where needed
|
||||
- [ ] Override/final used for virtual functions
|
||||
- [ ] No object slicing (pass by ref or pointer)
|
||||
|
||||
### Concurrency
|
||||
|
||||
- [ ] Shared data is protected (mutex or atomics)
|
||||
- [ ] Locking order is consistent
|
||||
- [ ] No blocking while holding locks
|
||||
|
||||
### Performance
|
||||
|
||||
- [ ] Unnecessary allocations avoided (reserve, move)
|
||||
- [ ] Copies avoided in hot paths
|
||||
- [ ] Algorithmic complexity is reasonable
|
||||
|
||||
### Tooling and Tests
|
||||
|
||||
- [ ] Builds clean with warnings enabled
|
||||
- [ ] Sanitizers run on critical code paths
|
||||
- [ ] Static analysis (clang-tidy) results are addressed
|
||||
|
||||
+87
-56
@@ -105,8 +105,12 @@ CSS 及预处理器代码审查指南,覆盖性能、可维护性、响应式
|
||||
|
||||
```css
|
||||
/* ✅ 工具类 - 明确需要覆盖 */
|
||||
.hidden { display: none !important; }
|
||||
.sr-only { position: absolute !important; }
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
.sr-only {
|
||||
position: absolute !important;
|
||||
}
|
||||
|
||||
/* ✅ 覆盖第三方库样式(无法修改源码时) */
|
||||
.third-party-modal {
|
||||
@@ -115,7 +119,9 @@ CSS 及预处理器代码审查指南,覆盖性能、可维护性、响应式
|
||||
|
||||
/* ✅ 打印样式 */
|
||||
@media print {
|
||||
.no-print { display: none !important; }
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -124,16 +130,20 @@ CSS 及预处理器代码审查指南,覆盖性能、可维护性、响应式
|
||||
```css
|
||||
/* ❌ 解决特异性问题 - 应该重构选择器 */
|
||||
.button {
|
||||
background: blue !important; /* 为什么需要 !important? */
|
||||
background: blue !important; /* 为什么需要 !important? */
|
||||
}
|
||||
|
||||
/* ❌ 覆盖自己写的样式 */
|
||||
.card { padding: 20px; }
|
||||
.card { padding: 30px !important; } /* 直接修改原规则 */
|
||||
.card {
|
||||
padding: 20px;
|
||||
}
|
||||
.card {
|
||||
padding: 30px !important;
|
||||
} /* 直接修改原规则 */
|
||||
|
||||
/* ❌ 在组件样式中 */
|
||||
.my-component .title {
|
||||
font-size: 24px !important; /* 破坏组件封装 */
|
||||
font-size: 24px !important; /* 破坏组件封装 */
|
||||
}
|
||||
```
|
||||
|
||||
@@ -159,10 +169,10 @@ button.my-btn {
|
||||
|
||||
/* ✅ 使用 :where() 降低被覆盖样式的特异性 */
|
||||
:where(.btn) {
|
||||
background: blue; /* 特异性为 0 */
|
||||
background: blue; /* 特异性为 0 */
|
||||
}
|
||||
.my-btn {
|
||||
background: red; /* 可以正常覆盖 */
|
||||
background: red; /* 可以正常覆盖 */
|
||||
}
|
||||
```
|
||||
|
||||
@@ -190,7 +200,9 @@ button.my-btn {
|
||||
|
||||
/* ✅ 明确指定属性 */
|
||||
.button {
|
||||
transition: background-color 0.3s ease, transform 0.3s ease;
|
||||
transition:
|
||||
background-color 0.3s ease,
|
||||
transform 0.3s ease;
|
||||
}
|
||||
|
||||
/* ✅ 多属性时使用变量 */
|
||||
@@ -208,11 +220,11 @@ button.my-btn {
|
||||
```css
|
||||
/* ❌ 每帧触发重绘 - 严重影响性能 */
|
||||
.card {
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
.card:hover {
|
||||
box-shadow: 0 8px 16px rgba(0,0,0,0.2);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* ✅ 使用伪元素 + opacity */
|
||||
@@ -223,7 +235,7 @@ button.my-btn {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
box-shadow: 0 8px 16px rgba(0,0,0,0.2);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
@@ -239,24 +251,31 @@ button.my-btn {
|
||||
```css
|
||||
/* ❌ 动画这些属性会触发布局重计算 */
|
||||
.bad-animation {
|
||||
transition: width 0.3s, height 0.3s, top 0.3s, left 0.3s, margin 0.3s;
|
||||
transition:
|
||||
width 0.3s,
|
||||
height 0.3s,
|
||||
top 0.3s,
|
||||
left 0.3s,
|
||||
margin 0.3s;
|
||||
}
|
||||
|
||||
/* ✅ 只动画 transform 和 opacity(仅触发合成) */
|
||||
.good-animation {
|
||||
transition: transform 0.3s, opacity 0.3s;
|
||||
transition:
|
||||
transform 0.3s,
|
||||
opacity 0.3s;
|
||||
}
|
||||
|
||||
/* 位移用 translate 代替 top/left */
|
||||
.move {
|
||||
transform: translateX(100px); /* ✅ */
|
||||
/* left: 100px; */ /* ❌ */
|
||||
transform: translateX(100px); /* ✅ */
|
||||
/* left: 100px; */ /* ❌ */
|
||||
}
|
||||
|
||||
/* 缩放用 scale 代替 width/height */
|
||||
.grow {
|
||||
transform: scale(1.1); /* ✅ */
|
||||
/* width: 110%; */ /* ❌ */
|
||||
transform: scale(1.1); /* ✅ */
|
||||
/* width: 110%; */ /* ❌ */
|
||||
}
|
||||
```
|
||||
|
||||
@@ -276,11 +295,17 @@ button.my-btn {
|
||||
}
|
||||
|
||||
/* ❌ 通配符选择器 */
|
||||
* { box-sizing: border-box; } /* 影响所有元素 */
|
||||
[class*="icon-"] { display: inline; } /* 属性选择器较慢 */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
} /* 影响所有元素 */
|
||||
[class*='icon-'] {
|
||||
display: inline;
|
||||
} /* 属性选择器较慢 */
|
||||
|
||||
/* ✅ 限制范围 */
|
||||
.icon-box * { box-sizing: border-box; }
|
||||
.icon-box * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
```
|
||||
|
||||
#### 大量阴影和滤镜
|
||||
@@ -289,17 +314,17 @@ button.my-btn {
|
||||
/* ⚠️ 复杂阴影影响渲染性能 */
|
||||
.heavy-shadow {
|
||||
box-shadow:
|
||||
0 1px 2px rgba(0,0,0,0.1),
|
||||
0 2px 4px rgba(0,0,0,0.1),
|
||||
0 4px 8px rgba(0,0,0,0.1),
|
||||
0 8px 16px rgba(0,0,0,0.1),
|
||||
0 16px 32px rgba(0,0,0,0.1); /* 5 层阴影 */
|
||||
0 1px 2px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px rgba(0, 0, 0, 0.1),
|
||||
0 4px 8px rgba(0, 0, 0, 0.1),
|
||||
0 8px 16px rgba(0, 0, 0, 0.1),
|
||||
0 16px 32px rgba(0, 0, 0, 0.1); /* 5 层阴影 */
|
||||
}
|
||||
|
||||
/* ⚠️ 滤镜消耗 GPU */
|
||||
.blur-heavy {
|
||||
filter: blur(20px) brightness(1.2) contrast(1.1);
|
||||
backdrop-filter: blur(10px); /* 更消耗性能 */
|
||||
backdrop-filter: blur(10px); /* 更消耗性能 */
|
||||
}
|
||||
```
|
||||
|
||||
@@ -318,7 +343,7 @@ button.my-btn {
|
||||
|
||||
/* 使用 contain 限制重绘范围 */
|
||||
.card {
|
||||
contain: layout paint; /* 告诉浏览器内部变化不影响外部 */
|
||||
contain: layout paint; /* 告诉浏览器内部变化不影响外部 */
|
||||
}
|
||||
```
|
||||
|
||||
@@ -387,16 +412,20 @@ button.my-btn {
|
||||
```css
|
||||
/* 推荐断点(基于内容而非设备) */
|
||||
:root {
|
||||
--breakpoint-sm: 640px; /* 大手机 */
|
||||
--breakpoint-md: 768px; /* 平板竖屏 */
|
||||
--breakpoint-lg: 1024px; /* 平板横屏/小笔记本 */
|
||||
--breakpoint-xl: 1280px; /* 桌面 */
|
||||
--breakpoint-sm: 640px; /* 大手机 */
|
||||
--breakpoint-md: 768px; /* 平板竖屏 */
|
||||
--breakpoint-lg: 1024px; /* 平板横屏/小笔记本 */
|
||||
--breakpoint-xl: 1280px; /* 桌面 */
|
||||
--breakpoint-2xl: 1536px; /* 大桌面 */
|
||||
}
|
||||
|
||||
/* 使用示例 */
|
||||
@media (min-width: 768px) { /* md */ }
|
||||
@media (min-width: 1024px) { /* lg */ }
|
||||
@media (min-width: 768px) {
|
||||
/* md */
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
/* lg */
|
||||
}
|
||||
```
|
||||
|
||||
### 响应式审查清单
|
||||
@@ -425,7 +454,7 @@ button.my-btn {
|
||||
|
||||
/* ❌ 固定高度的文本容器 */
|
||||
.text-box {
|
||||
height: 100px; /* 文字可能溢出 */
|
||||
height: 100px; /* 文字可能溢出 */
|
||||
}
|
||||
|
||||
/* ✅ 最小高度 */
|
||||
@@ -435,7 +464,7 @@ button.my-btn {
|
||||
|
||||
/* ❌ 小触摸目标 */
|
||||
.small-button {
|
||||
padding: 4px 8px; /* 太小,难以点击 */
|
||||
padding: 4px 8px; /* 太小,难以点击 */
|
||||
}
|
||||
|
||||
/* ✅ 足够的触摸区域 */
|
||||
@@ -452,22 +481,22 @@ button.my-btn {
|
||||
|
||||
### 需要检查的特性
|
||||
|
||||
| 特性 | 兼容性 | 建议 |
|
||||
|------|--------|------|
|
||||
| CSS Grid | 现代浏览器 ✅ | IE 需要 Autoprefixer + 测试 |
|
||||
| Flexbox | 广泛支持 ✅ | 旧版需要前缀 |
|
||||
| CSS Variables | 现代浏览器 ✅ | IE 不支持,需要回退 |
|
||||
| `gap` (flexbox) | 较新 ⚠️ | Safari 14.1+ |
|
||||
| `:has()` | 较新 ⚠️ | Firefox 121+ |
|
||||
| `container queries` | 较新 ⚠️ | 2023 年后的浏览器 |
|
||||
| `@layer` | 较新 ⚠️ | 检查目标浏览器 |
|
||||
| 特性 | 兼容性 | 建议 |
|
||||
| ------------------- | ------------- | --------------------------- |
|
||||
| CSS Grid | 现代浏览器 ✅ | IE 需要 Autoprefixer + 测试 |
|
||||
| Flexbox | 广泛支持 ✅ | 旧版需要前缀 |
|
||||
| CSS Variables | 现代浏览器 ✅ | IE 不支持,需要回退 |
|
||||
| `gap` (flexbox) | 较新 ⚠️ | Safari 14.1+ |
|
||||
| `:has()` | 较新 ⚠️ | Firefox 121+ |
|
||||
| `container queries` | 较新 ⚠️ | 2023 年后的浏览器 |
|
||||
| `@layer` | 较新 ⚠️ | 检查目标浏览器 |
|
||||
|
||||
### 回退策略
|
||||
|
||||
```css
|
||||
/* CSS 变量回退 */
|
||||
.button {
|
||||
background: #3b82f6; /* 回退值 */
|
||||
background: #3b82f6; /* 回退值 */
|
||||
background: var(--color-primary); /* 现代浏览器 */
|
||||
}
|
||||
|
||||
@@ -540,7 +569,7 @@ module.exports = {
|
||||
.content {
|
||||
.article {
|
||||
.title {
|
||||
color: red; // 编译为 .page .container .content .article .title
|
||||
color: red; // 编译为 .page .container .content .article .title
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -554,7 +583,9 @@ module.exports = {
|
||||
}
|
||||
|
||||
&__content {
|
||||
p { margin-bottom: 1em; }
|
||||
p {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -637,13 +668,13 @@ $primary-color: #3b82f6;
|
||||
|
||||
## 工具推荐
|
||||
|
||||
| 工具 | 用途 |
|
||||
|------|------|
|
||||
| [Stylelint](https://stylelint.io/) | CSS 代码检查 |
|
||||
| [PurgeCSS](https://purgecss.com/) | 移除未使用 CSS |
|
||||
| [Autoprefixer](https://autoprefixer.github.io/) | 自动添加前缀 |
|
||||
| [CSS Stats](https://cssstats.com/) | 分析 CSS 统计 |
|
||||
| [Can I Use](https://caniuse.com/) | 浏览器兼容性查询 |
|
||||
| 工具 | 用途 |
|
||||
| ----------------------------------------------- | ---------------- |
|
||||
| [Stylelint](https://stylelint.io/) | CSS 代码检查 |
|
||||
| [PurgeCSS](https://purgecss.com/) | 移除未使用 CSS |
|
||||
| [Autoprefixer](https://autoprefixer.github.io/) | 自动添加前缀 |
|
||||
| [CSS Stats](https://cssstats.com/) | 分析 CSS 统计 |
|
||||
| [Can I Use](https://caniuse.com/) | 浏览器兼容性查询 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
## 快速审查清单
|
||||
|
||||
### 必查项
|
||||
|
||||
- [ ] 错误是否正确处理(不忽略、有上下文)
|
||||
- [ ] goroutine 是否有退出机制(避免泄漏)
|
||||
- [ ] context 是否正确传递和取消
|
||||
@@ -12,6 +13,7 @@
|
||||
- [ ] 是否使用 `gofmt` 格式化代码
|
||||
|
||||
### 高频问题
|
||||
|
||||
- [ ] 循环变量捕获问题(Go < 1.22)
|
||||
- [ ] nil 检查是否完整
|
||||
- [ ] map 是否初始化后使用
|
||||
|
||||
@@ -377,29 +377,34 @@ class UserRepositoryTest {
|
||||
## Review Checklist
|
||||
|
||||
### 基础与规范
|
||||
|
||||
- [ ] 遵循 Java 17/21 新特性(Switch 表达式, Records, 文本块)
|
||||
- [ ] 避免使用已过时的类(Date, Calendar, SimpleDateFormat)
|
||||
- [ ] 集合操作是否优先使用了 Stream API 或 Collections 方法?
|
||||
- [ ] Optional 仅用于返回值,未用于字段或参数
|
||||
|
||||
### Spring Boot
|
||||
|
||||
- [ ] 使用构造器注入而非 @Autowired 字段注入
|
||||
- [ ] 配置属性使用了 @ConfigurationProperties
|
||||
- [ ] Controller 职责单一,业务逻辑下沉到 Service
|
||||
- [ ] 全局异常处理使用了 @ControllerAdvice / ProblemDetail
|
||||
|
||||
### 数据库 & 事务
|
||||
|
||||
- [ ] 读操作事务标记了 `@Transactional(readOnly = true)`
|
||||
- [ ] 检查是否存在 N+1 查询(EAGER fetch 或循环调用)
|
||||
- [ ] Entity 类未使用 @Data,正确实现了 equals/hashCode
|
||||
- [ ] 数据库索引是否覆盖了查询条件
|
||||
|
||||
### 并发与性能
|
||||
|
||||
- [ ] I/O 密集型任务是否考虑了虚拟线程?
|
||||
- [ ] 线程安全类是否使用正确(ConcurrentHashMap vs HashMap)
|
||||
- [ ] 锁的粒度是否合理?避免在锁内进行 I/O 操作
|
||||
|
||||
### 可维护性
|
||||
|
||||
- [ ] 关键业务逻辑有充分的单元测试
|
||||
- [ ] 日志记录恰当(使用 Slf4j,避免 System.out)
|
||||
- [ ] 魔法值提取为常量或枚举
|
||||
|
||||
+89
-81
@@ -18,13 +18,13 @@
|
||||
|
||||
### 2024 核心指标
|
||||
|
||||
| 指标 | 全称 | 目标值 | 含义 |
|
||||
|------|------|--------|------|
|
||||
| **LCP** | Largest Contentful Paint | ≤ 2.5s | 最大内容绘制时间 |
|
||||
| **INP** | Interaction to Next Paint | ≤ 200ms | 交互响应时间(2024 年替代 FID)|
|
||||
| **CLS** | Cumulative Layout Shift | ≤ 0.1 | 累积布局偏移 |
|
||||
| **FCP** | First Contentful Paint | ≤ 1.8s | 首次内容绘制 |
|
||||
| **TBT** | Total Blocking Time | ≤ 200ms | 主线程阻塞时间 |
|
||||
| 指标 | 全称 | 目标值 | 含义 |
|
||||
| ------- | ------------------------- | ------- | ------------------------------- |
|
||||
| **LCP** | Largest Contentful Paint | ≤ 2.5s | 最大内容绘制时间 |
|
||||
| **INP** | Interaction to Next Paint | ≤ 200ms | 交互响应时间(2024 年替代 FID) |
|
||||
| **CLS** | Cumulative Layout Shift | ≤ 0.1 | 累积布局偏移 |
|
||||
| **FCP** | First Contentful Paint | ≤ 1.8s | 首次内容绘制 |
|
||||
| **TBT** | Total Blocking Time | ≤ 200ms | 主线程阻塞时间 |
|
||||
|
||||
### LCP 优化检查
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
```
|
||||
|
||||
**审查要点:**
|
||||
|
||||
- [ ] LCP 元素是否设置 `fetchpriority="high"`?
|
||||
- [ ] 是否使用 WebP/AVIF 格式?
|
||||
- [ ] 是否有服务端渲染或静态生成?
|
||||
@@ -59,21 +60,17 @@
|
||||
<link rel="stylesheet" href="all-styles.css" />
|
||||
|
||||
<!-- ✅ 关键 CSS 内联 + 异步加载其余 -->
|
||||
<style>/* 首屏关键样式 */</style>
|
||||
<style>
|
||||
/* 首屏关键样式 */
|
||||
</style>
|
||||
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'" />
|
||||
|
||||
<!-- ❌ 阻塞渲染的字体 -->
|
||||
@font-face {
|
||||
font-family: 'CustomFont';
|
||||
src: url('font.woff2');
|
||||
}
|
||||
@font-face { font-family: 'CustomFont'; src: url('font.woff2'); }
|
||||
|
||||
<!-- ✅ 字体显示优化 -->
|
||||
@font-face {
|
||||
font-family: 'CustomFont';
|
||||
src: url('font.woff2');
|
||||
font-display: swap; /* 先用系统字体,加载后切换 */
|
||||
}
|
||||
@font-face { font-family: 'CustomFont'; src: url('font.woff2'); font-display: swap; /*
|
||||
先用系统字体,加载后切换 */ }
|
||||
```
|
||||
|
||||
### INP 优化检查
|
||||
@@ -89,7 +86,7 @@ button.addEventListener('click', () => {
|
||||
// ✅ 拆分长任务
|
||||
button.addEventListener('click', async () => {
|
||||
// 让出主线程
|
||||
await scheduler.yield?.() ?? new Promise(r => setTimeout(r, 0));
|
||||
(await scheduler.yield?.()) ?? new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
// 分批处理
|
||||
for (const chunk of chunks) {
|
||||
@@ -109,7 +106,9 @@ worker.onmessage = (e) => updateUI(e.data);
|
||||
|
||||
```css
|
||||
/* ❌ 未指定尺寸的媒体 */
|
||||
img { width: 100%; }
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ✅ 预留空间 */
|
||||
img {
|
||||
@@ -118,7 +117,8 @@ img {
|
||||
}
|
||||
|
||||
/* ❌ 动态插入内容导致布局偏移 */
|
||||
.ad-container { }
|
||||
.ad-container {
|
||||
}
|
||||
|
||||
/* ✅ 预留固定高度 */
|
||||
.ad-container {
|
||||
@@ -127,6 +127,7 @@ img {
|
||||
```
|
||||
|
||||
**CLS 审查清单:**
|
||||
|
||||
- [ ] 图片/视频是否有 width/height 或 aspect-ratio?
|
||||
- [ ] 字体加载是否使用 `font-display: swap`?
|
||||
- [ ] 动态内容是否预留空间?
|
||||
@@ -175,7 +176,7 @@ import { format } from 'date-fns';
|
||||
// ❌ 未使用 Tree Shaking
|
||||
export default {
|
||||
fn1() {},
|
||||
fn2() {}, // 未使用但被打包
|
||||
fn2() {}, // 未使用但被打包
|
||||
};
|
||||
|
||||
// ✅ 命名导出支持 Tree Shaking
|
||||
@@ -184,6 +185,7 @@ export function fn2() {}
|
||||
```
|
||||
|
||||
**Bundle 审查清单:**
|
||||
|
||||
- [ ] 是否使用动态 import() 进行代码分割?
|
||||
- [ ] 大型库是否按需导入?
|
||||
- [ ] 是否分析过 bundle 大小?(webpack-bundle-analyzer)
|
||||
@@ -196,9 +198,11 @@ export function fn2() {}
|
||||
function List({ items }) {
|
||||
return (
|
||||
<ul>
|
||||
{items.map(item => <li key={item.id}>{item.name}</li>)}
|
||||
{items.map((item) => (
|
||||
<li key={item.id}>{item.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
); // 10000 条数据 = 10000 个 DOM 节点
|
||||
); // 10000 条数据 = 10000 个 DOM 节点
|
||||
}
|
||||
|
||||
// ✅ 虚拟列表 - 只渲染可见项
|
||||
@@ -206,20 +210,15 @@ import { FixedSizeList } from 'react-window';
|
||||
|
||||
function VirtualList({ items }) {
|
||||
return (
|
||||
<FixedSizeList
|
||||
height={400}
|
||||
itemCount={items.length}
|
||||
itemSize={35}
|
||||
>
|
||||
{({ index, style }) => (
|
||||
<div style={style}>{items[index].name}</div>
|
||||
)}
|
||||
<FixedSizeList height={400} itemCount={items.length} itemSize={35}>
|
||||
{({ index, style }) => <div style={style}>{items[index].name}</div>}
|
||||
</FixedSizeList>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**大数据审查要点:**
|
||||
|
||||
- [ ] 列表超过 100 项是否使用虚拟滚动?
|
||||
- [ ] 表格是否支持分页或虚拟化?
|
||||
- [ ] 是否有不必要的全量渲染?
|
||||
@@ -276,7 +275,7 @@ function createHandler() {
|
||||
// ✅ 只保留必要数据
|
||||
function createHandler() {
|
||||
const largeData = new Array(1000000).fill('x');
|
||||
const length = largeData.length; // 只保留需要的值
|
||||
const length = largeData.length; // 只保留需要的值
|
||||
|
||||
return function handler() {
|
||||
console.log(length);
|
||||
@@ -314,11 +313,11 @@ useEffect(() => {
|
||||
|
||||
### 检测工具
|
||||
|
||||
| 工具 | 用途 |
|
||||
|------|------|
|
||||
| Chrome DevTools Memory | 堆快照分析 |
|
||||
| MemLab (Meta) | 自动化内存泄漏检测 |
|
||||
| Performance Monitor | 实时内存监控 |
|
||||
| 工具 | 用途 |
|
||||
| ---------------------- | ------------------ |
|
||||
| Chrome DevTools Memory | 堆快照分析 |
|
||||
| MemLab (Meta) | 自动化内存泄漏检测 |
|
||||
| Performance Monitor | 实时内存监控 |
|
||||
|
||||
---
|
||||
|
||||
@@ -346,7 +345,7 @@ posts = Post.objects.prefetch_related('tags').all()
|
||||
// ❌ N+1 问题
|
||||
const users = await userRepository.find();
|
||||
for (const user of users) {
|
||||
const posts = await user.posts; // 每次循环都查询
|
||||
const posts = await user.posts; // 每次循环都查询
|
||||
}
|
||||
|
||||
// ✅ Eager Loading
|
||||
@@ -405,12 +404,14 @@ cursor.execute("SELECT * FROM users WHERE id IN %s", (tuple(user_ids),))
|
||||
|
||||
```markdown
|
||||
🔴 必须检查:
|
||||
|
||||
- [ ] 是否存在 N+1 查询?
|
||||
- [ ] WHERE 子句列是否有索引?
|
||||
- [ ] 是否避免了 SELECT *?
|
||||
- [ ] 是否避免了 SELECT \*?
|
||||
- [ ] 大表查询是否有 LIMIT?
|
||||
|
||||
🟡 建议检查:
|
||||
|
||||
- [ ] 是否使用了 EXPLAIN 分析查询计划?
|
||||
- [ ] 复合索引列顺序是否正确?
|
||||
- [ ] 是否有未使用的索引?
|
||||
@@ -426,14 +427,14 @@ cursor.execute("SELECT * FROM users WHERE id IN %s", (tuple(user_ids),))
|
||||
```javascript
|
||||
// ❌ 返回全部数据
|
||||
app.get('/users', async (req, res) => {
|
||||
const users = await User.findAll(); // 可能返回 100000 条
|
||||
const users = await User.findAll(); // 可能返回 100000 条
|
||||
res.json(users);
|
||||
});
|
||||
|
||||
// ✅ 分页 + 限制最大数量
|
||||
app.get('/users', async (req, res) => {
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = Math.min(parseInt(req.query.limit) || 20, 100); // 最大 100
|
||||
const limit = Math.min(parseInt(req.query.limit) || 20, 100); // 最大 100
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const { rows, count } = await User.findAndCountAll({
|
||||
@@ -479,8 +480,8 @@ async function getUser(id) {
|
||||
// ✅ HTTP 缓存头
|
||||
app.get('/static-data', (req, res) => {
|
||||
res.set({
|
||||
'Cache-Control': 'public, max-age=86400', // 24 小时
|
||||
'ETag': 'abc123',
|
||||
'Cache-Control': 'public, max-age=86400', // 24 小时
|
||||
ETag: 'abc123',
|
||||
});
|
||||
res.json(data);
|
||||
});
|
||||
@@ -511,8 +512,8 @@ app.get('/users', async (req, res) => {
|
||||
const rateLimit = require('express-rate-limit');
|
||||
|
||||
const limiter = rateLimit({
|
||||
windowMs: 60 * 1000, // 1 分钟
|
||||
max: 100, // 最多 100 次请求
|
||||
windowMs: 60 * 1000, // 1 分钟
|
||||
max: 100, // 最多 100 次请求
|
||||
message: { error: 'Too many requests, please try again later.' },
|
||||
});
|
||||
|
||||
@@ -536,14 +537,14 @@ app.use('/api/', limiter);
|
||||
|
||||
### 常见复杂度对比
|
||||
|
||||
| 复杂度 | 名称 | 10 条 | 1000 条 | 100 万条 | 示例 |
|
||||
|--------|------|-------|---------|----------|------|
|
||||
| O(1) | 常数 | 1 | 1 | 1 | 哈希查找 |
|
||||
| O(log n) | 对数 | 3 | 10 | 20 | 二分查找 |
|
||||
| O(n) | 线性 | 10 | 1000 | 100 万 | 遍历数组 |
|
||||
| O(n log n) | 线性对数 | 33 | 10000 | 2000 万 | 快速排序 |
|
||||
| O(n²) | 平方 | 100 | 100 万 | 1 万亿 | 嵌套循环 |
|
||||
| O(2ⁿ) | 指数 | 1024 | ∞ | ∞ | 递归斐波那契 |
|
||||
| 复杂度 | 名称 | 10 条 | 1000 条 | 100 万条 | 示例 |
|
||||
| ---------- | -------- | ----- | ------- | -------- | ------------ |
|
||||
| O(1) | 常数 | 1 | 1 | 1 | 哈希查找 |
|
||||
| O(log n) | 对数 | 3 | 10 | 20 | 二分查找 |
|
||||
| O(n) | 线性 | 10 | 1000 | 100 万 | 遍历数组 |
|
||||
| O(n log n) | 线性对数 | 33 | 10000 | 2000 万 | 快速排序 |
|
||||
| O(n²) | 平方 | 100 | 100 万 | 1 万亿 | 嵌套循环 |
|
||||
| O(2ⁿ) | 指数 | 1024 | ∞ | ∞ | 递归斐波那契 |
|
||||
|
||||
### 代码审查中的识别
|
||||
|
||||
@@ -580,7 +581,8 @@ function findDuplicates(arr) {
|
||||
function removeDuplicates(arr) {
|
||||
const result = [];
|
||||
for (const item of arr) {
|
||||
if (!result.includes(item)) { // includes 是 O(n)
|
||||
if (!result.includes(item)) {
|
||||
// includes 是 O(n)
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
@@ -613,7 +615,7 @@ function getUser(id) {
|
||||
|
||||
```javascript
|
||||
// ⚠️ O(n) 空间 - 创建新数组
|
||||
const doubled = arr.map(x => x * 2);
|
||||
const doubled = arr.map((x) => x * 2);
|
||||
|
||||
// ✅ O(1) 空间 - 原地修改(如果允许)
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
@@ -623,7 +625,7 @@ for (let i = 0; i < arr.length; i++) {
|
||||
// ⚠️ 递归深度过大可能栈溢出
|
||||
function factorial(n) {
|
||||
if (n <= 1) return 1;
|
||||
return n * factorial(n - 1); // O(n) 栈空间
|
||||
return n * factorial(n - 1); // O(n) 栈空间
|
||||
}
|
||||
|
||||
// ✅ 迭代版本 O(1) 空间
|
||||
@@ -651,34 +653,40 @@ function factorial(n) {
|
||||
### 🔴 必须检查(阻塞级)
|
||||
|
||||
**前端:**
|
||||
|
||||
- [ ] LCP 图片是否懒加载?(不应该)
|
||||
- [ ] 是否有 `transition: all`?
|
||||
- [ ] 是否动画 width/height/top/left?
|
||||
- [ ] 列表 >100 项是否虚拟化?
|
||||
|
||||
**后端:**
|
||||
|
||||
- [ ] 是否存在 N+1 查询?
|
||||
- [ ] 列表接口是否有分页?
|
||||
- [ ] 是否有 SELECT * 查大表?
|
||||
- [ ] 是否有 SELECT \* 查大表?
|
||||
|
||||
**通用:**
|
||||
|
||||
- [ ] 是否有 O(n²) 或更差的嵌套循环?
|
||||
- [ ] useEffect/事件监听是否有清理?
|
||||
|
||||
### 🟡 建议检查(重要级)
|
||||
|
||||
**前端:**
|
||||
|
||||
- [ ] 是否使用代码分割?
|
||||
- [ ] 大型库是否按需导入?
|
||||
- [ ] 图片是否使用 WebP/AVIF?
|
||||
- [ ] 是否有未使用的依赖?
|
||||
|
||||
**后端:**
|
||||
|
||||
- [ ] 热点数据是否有缓存?
|
||||
- [ ] WHERE 列是否有索引?
|
||||
- [ ] 是否有慢查询监控?
|
||||
|
||||
**API:**
|
||||
|
||||
- [ ] 是否启用响应压缩?
|
||||
- [ ] 是否有速率限制?
|
||||
- [ ] 是否只返回必要字段?
|
||||
@@ -696,21 +704,21 @@ function factorial(n) {
|
||||
|
||||
### 前端指标
|
||||
|
||||
| 指标 | 好 | 需改进 | 差 |
|
||||
|------|-----|--------|-----|
|
||||
| LCP | ≤ 2.5s | 2.5-4s | > 4s |
|
||||
| INP | ≤ 200ms | 200-500ms | > 500ms |
|
||||
| CLS | ≤ 0.1 | 0.1-0.25 | > 0.25 |
|
||||
| FCP | ≤ 1.8s | 1.8-3s | > 3s |
|
||||
| 指标 | 好 | 需改进 | 差 |
|
||||
| ---------------- | ------- | --------- | ------- |
|
||||
| LCP | ≤ 2.5s | 2.5-4s | > 4s |
|
||||
| INP | ≤ 200ms | 200-500ms | > 500ms |
|
||||
| CLS | ≤ 0.1 | 0.1-0.25 | > 0.25 |
|
||||
| FCP | ≤ 1.8s | 1.8-3s | > 3s |
|
||||
| Bundle Size (JS) | < 200KB | 200-500KB | > 500KB |
|
||||
|
||||
### 后端指标
|
||||
|
||||
| 指标 | 好 | 需改进 | 差 |
|
||||
|------|-----|--------|-----|
|
||||
| 指标 | 好 | 需改进 | 差 |
|
||||
| ------------ | ------- | --------- | ------- |
|
||||
| API 响应时间 | < 100ms | 100-500ms | > 500ms |
|
||||
| 数据库查询 | < 50ms | 50-200ms | > 200ms |
|
||||
| 页面加载 | < 3s | 3-5s | > 5s |
|
||||
| 数据库查询 | < 50ms | 50-200ms | > 200ms |
|
||||
| 页面加载 | < 3s | 3-5s | > 5s |
|
||||
|
||||
---
|
||||
|
||||
@@ -718,27 +726,27 @@ function factorial(n) {
|
||||
|
||||
### 前端性能
|
||||
|
||||
| 工具 | 用途 |
|
||||
|------|------|
|
||||
| [Lighthouse](https://developer.chrome.com/docs/lighthouse/) | Core Web Vitals 测试 |
|
||||
| [WebPageTest](https://www.webpagetest.org/) | 详细性能分析 |
|
||||
| [webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer) | Bundle 分析 |
|
||||
| [Chrome DevTools Performance](https://developer.chrome.com/docs/devtools/performance/) | 运行时性能分析 |
|
||||
| 工具 | 用途 |
|
||||
| -------------------------------------------------------------------------------------- | -------------------- |
|
||||
| [Lighthouse](https://developer.chrome.com/docs/lighthouse/) | Core Web Vitals 测试 |
|
||||
| [WebPageTest](https://www.webpagetest.org/) | 详细性能分析 |
|
||||
| [webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer) | Bundle 分析 |
|
||||
| [Chrome DevTools Performance](https://developer.chrome.com/docs/devtools/performance/) | 运行时性能分析 |
|
||||
|
||||
### 内存检测
|
||||
|
||||
| 工具 | 用途 |
|
||||
|------|------|
|
||||
| 工具 | 用途 |
|
||||
| ----------------------------------------------------- | ------------------ |
|
||||
| [MemLab](https://github.com/facebookincubator/memlab) | 自动化内存泄漏检测 |
|
||||
| Chrome Memory Tab | 堆快照分析 |
|
||||
| Chrome Memory Tab | 堆快照分析 |
|
||||
|
||||
### 后端性能
|
||||
|
||||
| 工具 | 用途 |
|
||||
|------|------|
|
||||
| EXPLAIN | 数据库查询计划分析 |
|
||||
| [pganalyze](https://pganalyze.com/) | PostgreSQL 性能监控 |
|
||||
| [New Relic](https://newrelic.com/) / [Datadog](https://www.datadoghq.com/) | APM 监控 |
|
||||
| 工具 | 用途 |
|
||||
| -------------------------------------------------------------------------- | ------------------- |
|
||||
| EXPLAIN | 数据库查询计划分析 |
|
||||
| [pganalyze](https://pganalyze.com/) | PostgreSQL 性能监控 |
|
||||
| [New Relic](https://newrelic.com/) / [Datadog](https://www.datadoghq.com/) | APM 监控 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1023,6 +1023,7 @@ def handle_response(response: dict):
|
||||
## Review Checklist
|
||||
|
||||
### 类型安全
|
||||
|
||||
- [ ] 函数有类型注解(参数和返回值)
|
||||
- [ ] 使用 `Optional` 明确可能为 None
|
||||
- [ ] 泛型类型正确使用
|
||||
@@ -1030,6 +1031,7 @@ def handle_response(response: dict):
|
||||
- [ ] 避免使用 `Any`,必要时添加注释说明
|
||||
|
||||
### 异步代码
|
||||
|
||||
- [ ] async/await 正确配对使用
|
||||
- [ ] 没有在异步代码中使用阻塞调用
|
||||
- [ ] 正确处理 `CancelledError`
|
||||
@@ -1037,18 +1039,21 @@ def handle_response(response: dict):
|
||||
- [ ] 资源正确清理(async context manager)
|
||||
|
||||
### 异常处理
|
||||
|
||||
- [ ] 捕获特定异常类型,不使用裸 `except:`
|
||||
- [ ] 异常链使用 `from` 保留原因
|
||||
- [ ] 自定义异常继承自合适的基类
|
||||
- [ ] 异常信息有意义,便于调试
|
||||
|
||||
### 数据结构
|
||||
|
||||
- [ ] 没有使用可变默认参数(list、dict、set)
|
||||
- [ ] 类属性不是可变对象
|
||||
- [ ] 选择正确的数据结构(set vs list 查找)
|
||||
- [ ] 大数据集使用生成器而非列表
|
||||
|
||||
### 测试
|
||||
|
||||
- [ ] 测试覆盖率达标(建议 ≥80%)
|
||||
- [ ] 测试命名清晰描述测试场景
|
||||
- [ ] 边界情况有测试覆盖
|
||||
@@ -1056,6 +1061,7 @@ def handle_response(response: dict):
|
||||
- [ ] 异步代码有对应的异步测试
|
||||
|
||||
### 代码风格
|
||||
|
||||
- [ ] 遵循 PEP 8 风格指南
|
||||
- [ ] 函数和类有 docstring
|
||||
- [ ] 导入顺序正确(标准库、第三方、本地)
|
||||
@@ -1063,6 +1069,7 @@ def handle_response(response: dict):
|
||||
- [ ] 使用现代 Python 特性(f-string、walrus operator 等)
|
||||
|
||||
### 性能
|
||||
|
||||
- [ ] 避免循环中重复创建对象
|
||||
- [ ] 字符串拼接使用 join
|
||||
- [ ] 合理使用缓存(@lru_cache)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
## Object Model & Memory Management
|
||||
|
||||
### Use Parent-Child Ownership Mechanism
|
||||
|
||||
Qt's `QObject` hierarchy automatically manages memory. For `QObject`, prefer setting a parent object over manual `delete` or smart pointers.
|
||||
|
||||
```cpp
|
||||
@@ -32,6 +33,7 @@ QLabel* l = new QLabel(w); // Owned by 'w'
|
||||
```
|
||||
|
||||
### Use Smart Pointers with QObject
|
||||
|
||||
If a `QObject` has no parent, use `QScopedPointer` or `std::unique_ptr` with a custom deleter (use `deleteLater` if cross-thread). Avoid `std::shared_ptr` for `QObject` unless necessary, as it confuses the parent-child ownership system.
|
||||
|
||||
```cpp
|
||||
@@ -46,6 +48,7 @@ if (safePtr) {
|
||||
```
|
||||
|
||||
### Use `deleteLater()`
|
||||
|
||||
For asynchronous deletion, especially in slots or event handlers, use `deleteLater()` instead of `delete` to ensure pending events in the event loop are processed.
|
||||
|
||||
---
|
||||
@@ -53,6 +56,7 @@ For asynchronous deletion, especially in slots or event handlers, use `deleteLat
|
||||
## Signals & Slots
|
||||
|
||||
### Prefer Function Pointer Syntax
|
||||
|
||||
Use compile-time checked syntax (Qt 5+).
|
||||
|
||||
```cpp
|
||||
@@ -64,12 +68,15 @@ connect(sender, &Sender::valueChanged, receiver, &Receiver::updateValue);
|
||||
```
|
||||
|
||||
### Connection Types
|
||||
|
||||
Be explicit or aware of connection types when crossing threads.
|
||||
|
||||
- `Qt::AutoConnection` (Default): Direct if same thread, Queued if different thread.
|
||||
- `Qt::QueuedConnection`: Always posts event (thread-safe across threads).
|
||||
- `Qt::DirectConnection`: Immediate call (dangerous if accessing non-thread-safe data across threads).
|
||||
|
||||
### Avoid Loops
|
||||
|
||||
Check logic that might cause infinite signal loops (e.g., `valueChanged` -> `setValue` -> `valueChanged`). Block signals or check for equality before setting values.
|
||||
|
||||
```cpp
|
||||
@@ -85,6 +92,7 @@ void MyClass::setValue(int v) {
|
||||
## Containers & Strings
|
||||
|
||||
### QString Efficiency
|
||||
|
||||
- Use `QStringLiteral("...")` for compile-time string creation to avoid runtime allocation.
|
||||
- Use `QLatin1String` for comparison with ASCII literals (in Qt 5).
|
||||
- Prefer `arg()` for formatting (or `QStringBuilder`'s `%` operator).
|
||||
@@ -99,14 +107,15 @@ if (str == u"test"_s) ... // Qt 6
|
||||
```
|
||||
|
||||
### Container Selection
|
||||
|
||||
- **Qt 6**: `QList` is now the default choice (unified with `QVector`).
|
||||
- **Qt 5**: Prefer `QVector` over `QList` for contiguous memory and cache performance, unless stable references are needed.
|
||||
- Be aware of Implicit Sharing (Copy-on-Write). Passing containers by value is cheap *until* modified. Use `const &` for read-only access.
|
||||
- Be aware of Implicit Sharing (Copy-on-Write). Passing containers by value is cheap _until_ modified. Use `const &` for read-only access.
|
||||
|
||||
```cpp
|
||||
// ❌ Forces deep copy if function modifies 'list'
|
||||
void process(QVector<int> list) {
|
||||
list[0] = 1;
|
||||
list[0] = 1;
|
||||
}
|
||||
|
||||
// ✅ Read-only reference
|
||||
@@ -118,12 +127,13 @@ void process(const QVector<int>& list) { ... }
|
||||
## Threads & Concurrency
|
||||
|
||||
### Subclassing QThread vs Worker Object
|
||||
|
||||
Prefer the "Worker Object" pattern over subclassing `QThread` implementation details.
|
||||
|
||||
```cpp
|
||||
// ❌ Business logic inside QThread::run()
|
||||
class MyThread : public QThread {
|
||||
void run() override { ... }
|
||||
void run() override { ... }
|
||||
};
|
||||
|
||||
// ✅ Worker object moved to thread
|
||||
@@ -135,6 +145,7 @@ thread->start();
|
||||
```
|
||||
|
||||
### GUI Thread Safety
|
||||
|
||||
**NEVER** access UI widgets (`QWidget` and subclasses) from a background thread. Use signals/slots to communicate updates to the main thread.
|
||||
|
||||
---
|
||||
@@ -142,13 +153,17 @@ thread->start();
|
||||
## GUI & Widgets
|
||||
|
||||
### Logic Separation
|
||||
|
||||
Keep business logic out of UI classes (`MainWindow`, `Dialog`). UI classes should only handle display and user input forwarding.
|
||||
|
||||
### Layouts
|
||||
|
||||
Avoid fixed sizes (`setGeometry`, `resize`). Use layouts (`QVBoxLayout`, `QGridLayout`) to handle different DPIs and window resizing gracefully.
|
||||
|
||||
### Blocking Event Loop
|
||||
|
||||
Never execute long-running operations on the main thread (freezes GUI).
|
||||
|
||||
- **Bad**: `Sleep()`, `while(busy)`, synchronous network calls.
|
||||
- **Good**: `QProcess`, `QThread`, `QtConcurrent`, or asynchronous APIs (`QNetworkAccessManager`).
|
||||
|
||||
@@ -157,6 +172,7 @@ Never execute long-running operations on the main thread (freezes GUI).
|
||||
## Meta-Object System
|
||||
|
||||
### Properties & Enums
|
||||
|
||||
Use `Q_PROPERTY` for values exposed to QML or needing introspection.
|
||||
Use `Q_ENUM` to enable string conversion for enums.
|
||||
|
||||
@@ -172,6 +188,7 @@ public:
|
||||
```
|
||||
|
||||
### qobject_cast
|
||||
|
||||
Use `qobject_cast<T*>` for QObjects instead of `dynamic_cast`. It is faster and doesn't require RTTI.
|
||||
|
||||
---
|
||||
@@ -183,4 +200,4 @@ Use `qobject_cast<T*>` for QObjects instead of `dynamic_cast`. It is faster and
|
||||
- [ ] **Threads**: Is UI accessed only from main thread? Are long tasks offloaded?
|
||||
- [ ] **Strings**: Are `QStringLiteral` or `tr()` used appropriately?
|
||||
- [ ] **Style**: Naming conventions (camelCase for methods, PascalCase for classes).
|
||||
- [ ] **Resources**: Are resources (images, styles) loaded from `.qrc`?
|
||||
- [ ] **Resources**: Are resources (images, styles) loaded from `.qrc`?
|
||||
|
||||
@@ -23,7 +23,7 @@ React 审查重点:Hooks 规则、性能优化的适度性、组件设计、
|
||||
// ❌ 条件调用 Hooks — 违反 Hooks 规则
|
||||
function BadComponent({ isLoggedIn }) {
|
||||
if (isLoggedIn) {
|
||||
const [user, setUser] = useState(null); // Error!
|
||||
const [user, setUser] = useState(null); // Error!
|
||||
}
|
||||
return <div>...</div>;
|
||||
}
|
||||
@@ -46,7 +46,7 @@ function BadEffect({ userId }) {
|
||||
const [user, setUser] = useState(null);
|
||||
useEffect(() => {
|
||||
fetchUser(userId).then(setUser);
|
||||
}, []); // 缺少 userId 依赖!
|
||||
}, []); // 缺少 userId 依赖!
|
||||
}
|
||||
|
||||
// ✅ 完整的依赖数组
|
||||
@@ -54,10 +54,12 @@ function GoodEffect({ userId }) {
|
||||
const [user, setUser] = useState(null);
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
fetchUser(userId).then(data => {
|
||||
fetchUser(userId).then((data) => {
|
||||
if (!cancelled) setUser(data);
|
||||
});
|
||||
return () => { cancelled = true; }; // 清理函数
|
||||
return () => {
|
||||
cancelled = true;
|
||||
}; // 清理函数
|
||||
}, [userId]);
|
||||
}
|
||||
|
||||
@@ -65,17 +67,14 @@ function GoodEffect({ userId }) {
|
||||
function BadDerived({ items }) {
|
||||
const [filteredItems, setFilteredItems] = useState([]);
|
||||
useEffect(() => {
|
||||
setFilteredItems(items.filter(i => i.active));
|
||||
}, [items]); // 不必要的 effect + 额外渲染
|
||||
setFilteredItems(items.filter((i) => i.active));
|
||||
}, [items]); // 不必要的 effect + 额外渲染
|
||||
return <List items={filteredItems} />;
|
||||
}
|
||||
|
||||
// ✅ 直接在渲染时计算,或用 useMemo
|
||||
function GoodDerived({ items }) {
|
||||
const filteredItems = useMemo(
|
||||
() => items.filter(i => i.active),
|
||||
[items]
|
||||
);
|
||||
const filteredItems = useMemo(() => items.filter((i) => i.active), [items]);
|
||||
return <List items={filteredItems} />;
|
||||
}
|
||||
|
||||
@@ -84,7 +83,7 @@ function BadEventEffect() {
|
||||
const [query, setQuery] = useState('');
|
||||
useEffect(() => {
|
||||
if (query) {
|
||||
analytics.track('search', { query }); // 应该在事件处理器中
|
||||
analytics.track('search', { query }); // 应该在事件处理器中
|
||||
}
|
||||
}, [query]);
|
||||
}
|
||||
@@ -106,15 +105,15 @@ function GoodEvent() {
|
||||
```tsx
|
||||
// ❌ 过度优化 — 常量不需要 useMemo
|
||||
function OverOptimized() {
|
||||
const config = useMemo(() => ({ timeout: 5000 }), []); // 无意义
|
||||
const config = useMemo(() => ({ timeout: 5000 }), []); // 无意义
|
||||
const handleClick = useCallback(() => {
|
||||
console.log('clicked');
|
||||
}, []); // 如果不传给 memo 组件,无意义
|
||||
}, []); // 如果不传给 memo 组件,无意义
|
||||
}
|
||||
|
||||
// ✅ 只在需要时优化
|
||||
function ProperlyOptimized() {
|
||||
const config = { timeout: 5000 }; // 简单对象直接定义
|
||||
const config = { timeout: 5000 }; // 简单对象直接定义
|
||||
const handleClick = () => console.log('clicked');
|
||||
}
|
||||
|
||||
@@ -147,7 +146,8 @@ function Parent({ rawItems }) {
|
||||
```tsx
|
||||
// ❌ 在组件内定义组件 — 每次渲染都创建新组件
|
||||
function BadParent() {
|
||||
function ChildComponent() { // 每次渲染都是新函数!
|
||||
function ChildComponent() {
|
||||
// 每次渲染都是新函数!
|
||||
return <div>child</div>;
|
||||
}
|
||||
return <ChildComponent />;
|
||||
@@ -165,8 +165,8 @@ function GoodParent() {
|
||||
function BadProps() {
|
||||
return (
|
||||
<MemoizedComponent
|
||||
style={{ color: 'red' }} // 每次渲染新对象
|
||||
onClick={() => {}} // 每次渲染新函数
|
||||
style={{ color: 'red' }} // 每次渲染新对象
|
||||
onClick={() => {}} // 每次渲染新函数
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -188,7 +188,7 @@ function GoodProps() {
|
||||
function BadApp() {
|
||||
return (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<DataComponent /> {/* 错误会导致整个应用崩溃 */}
|
||||
<DataComponent /> {/* 错误会导致整个应用崩溃 */}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -287,15 +287,13 @@ function NewForm() {
|
||||
return { success: false, error: e.message };
|
||||
}
|
||||
},
|
||||
{ success: false, data: null, error: null }
|
||||
{ success: false, data: null, error: null },
|
||||
);
|
||||
|
||||
return (
|
||||
<form action={formAction}>
|
||||
<input name="email" />
|
||||
<button disabled={isPending}>
|
||||
{isPending ? 'Submitting...' : 'Submit'}
|
||||
</button>
|
||||
<button disabled={isPending}>{isPending ? 'Submitting...' : 'Submit'}</button>
|
||||
{state.error && <p className="error">{state.error}</p>}
|
||||
</form>
|
||||
);
|
||||
@@ -316,16 +314,12 @@ import { useFormStatus } from 'react-dom';
|
||||
function SubmitButton() {
|
||||
const { pending, data, method, action } = useFormStatus();
|
||||
// 注意:必须在 <form> 内部的子组件中使用
|
||||
return (
|
||||
<button disabled={pending}>
|
||||
{pending ? 'Submitting...' : 'Submit'}
|
||||
</button>
|
||||
);
|
||||
return <button disabled={pending}>{pending ? 'Submitting...' : 'Submit'}</button>;
|
||||
}
|
||||
|
||||
// ❌ useFormStatus 在 form 同级组件中调用——不工作
|
||||
function BadForm() {
|
||||
const { pending } = useFormStatus(); // 这里无法获取状态!
|
||||
const { pending } = useFormStatus(); // 这里无法获取状态!
|
||||
return (
|
||||
<form action={action}>
|
||||
<button disabled={pending}>Submit</button>
|
||||
@@ -337,7 +331,7 @@ function BadForm() {
|
||||
function GoodForm() {
|
||||
return (
|
||||
<form action={action}>
|
||||
<SubmitButton /> {/* useFormStatus 在这里面调用 */}
|
||||
<SubmitButton /> {/* useFormStatus 在这里面调用 */}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -353,7 +347,7 @@ function SlowLike({ postId, likes }) {
|
||||
|
||||
const handleLike = async () => {
|
||||
setIsPending(true);
|
||||
const newCount = await likePost(postId); // 等待...
|
||||
const newCount = await likePost(postId); // 等待...
|
||||
setLikeCount(newCount);
|
||||
setIsPending(false);
|
||||
};
|
||||
@@ -365,13 +359,13 @@ import { useOptimistic } from 'react';
|
||||
function FastLike({ postId, likes }) {
|
||||
const [optimisticLikes, addOptimisticLike] = useOptimistic(
|
||||
likes,
|
||||
(currentLikes, increment: number) => currentLikes + increment
|
||||
(currentLikes, increment: number) => currentLikes + increment,
|
||||
);
|
||||
|
||||
const handleLike = async () => {
|
||||
addOptimisticLike(1); // 立即更新 UI
|
||||
addOptimisticLike(1); // 立即更新 UI
|
||||
try {
|
||||
await likePost(postId); // 后台同步
|
||||
await likePost(postId); // 后台同步
|
||||
} catch {
|
||||
// React 自动回滚到 likes 原值
|
||||
}
|
||||
@@ -398,7 +392,7 @@ function ClientForm() {
|
||||
|
||||
// ✅ Server Action + useActionState
|
||||
// actions.ts
|
||||
'use server';
|
||||
('use server');
|
||||
export async function createPost(prevState: any, formData: FormData) {
|
||||
const title = formData.get('title');
|
||||
await db.posts.create({ title });
|
||||
@@ -407,7 +401,7 @@ export async function createPost(prevState: any, formData: FormData) {
|
||||
}
|
||||
|
||||
// form.tsx
|
||||
'use client';
|
||||
('use client');
|
||||
import { createPost } from './actions';
|
||||
|
||||
function PostForm() {
|
||||
@@ -436,7 +430,9 @@ function OldComponent() {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData().then(setData).finally(() => setIsLoading(false));
|
||||
fetchData()
|
||||
.then(setData)
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
if (isLoading) return <Spinner />;
|
||||
@@ -447,7 +443,7 @@ function OldComponent() {
|
||||
function NewComponent() {
|
||||
return (
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<DataView /> {/* 内部使用 use() 或支持 Suspense 的数据获取 */}
|
||||
<DataView /> {/* 内部使用 use() 或支持 Suspense 的数据获取 */}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -461,8 +457,8 @@ function BadLayout() {
|
||||
return (
|
||||
<Suspense fallback={<FullPageSpinner />}>
|
||||
<Header />
|
||||
<MainContent /> {/* 慢 */}
|
||||
<Sidebar /> {/* 快 */}
|
||||
<MainContent /> {/* 慢 */}
|
||||
<Sidebar /> {/* 快 */}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -471,13 +467,13 @@ function BadLayout() {
|
||||
function GoodLayout() {
|
||||
return (
|
||||
<>
|
||||
<Header /> {/* 立即显示 */}
|
||||
<Header /> {/* 立即显示 */}
|
||||
<div className="flex">
|
||||
<Suspense fallback={<ContentSkeleton />}>
|
||||
<MainContent /> {/* 独立加载 */}
|
||||
<MainContent /> {/* 独立加载 */}
|
||||
</Suspense>
|
||||
<Suspense fallback={<SidebarSkeleton />}>
|
||||
<Sidebar /> {/* 独立加载 */}
|
||||
<Sidebar /> {/* 独立加载 */}
|
||||
</Suspense>
|
||||
</div>
|
||||
</>
|
||||
@@ -508,17 +504,19 @@ export default function Loading() {
|
||||
import { use } from 'react';
|
||||
|
||||
function Comments({ commentsPromise }) {
|
||||
const comments = use(commentsPromise); // 自动触发 Suspense
|
||||
const comments = use(commentsPromise); // 自动触发 Suspense
|
||||
return (
|
||||
<ul>
|
||||
{comments.map(c => <li key={c.id}>{c.text}</li>)}
|
||||
{comments.map((c) => (
|
||||
<li key={c.id}>{c.text}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
// 父组件创建 Promise,子组件消费
|
||||
function Post({ postId }) {
|
||||
const commentsPromise = fetchComments(postId); // 不 await
|
||||
const commentsPromise = fetchComments(postId); // 不 await
|
||||
return (
|
||||
<article>
|
||||
<PostContent id={postId} />
|
||||
@@ -540,16 +538,16 @@ TanStack Query 是 React 生态中最流行的数据获取库,v5 是当前稳
|
||||
|
||||
```tsx
|
||||
// ❌ 不正确的默认配置
|
||||
const queryClient = new QueryClient(); // 默认配置可能不适合
|
||||
const queryClient = new QueryClient(); // 默认配置可能不适合
|
||||
|
||||
// ✅ 生产环境推荐配置
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 1000 * 60 * 5, // 5 分钟内数据视为新鲜
|
||||
gcTime: 1000 * 60 * 30, // 30 分钟后垃圾回收(v5 重命名)
|
||||
staleTime: 1000 * 60 * 5, // 5 分钟内数据视为新鲜
|
||||
gcTime: 1000 * 60 * 30, // 30 分钟后垃圾回收(v5 重命名)
|
||||
retry: 3,
|
||||
refetchOnWindowFocus: false, // 根据需求决定
|
||||
refetchOnWindowFocus: false, // 根据需求决定
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -568,8 +566,8 @@ function Component1() {
|
||||
|
||||
function prefetchUser(queryClient, userId) {
|
||||
queryClient.prefetchQuery({
|
||||
queryKey: ['users', userId], // 重复!
|
||||
queryFn: () => fetchUser(userId), // 重复!
|
||||
queryKey: ['users', userId], // 重复!
|
||||
queryFn: () => fetchUser(userId), // 重复!
|
||||
});
|
||||
}
|
||||
|
||||
@@ -608,21 +606,21 @@ useQuery({
|
||||
useQuery({
|
||||
queryKey: ['data'],
|
||||
queryFn: fetchData,
|
||||
staleTime: 1000 * 60, // 1 分钟内不会重新请求
|
||||
staleTime: 1000 * 60, // 1 分钟内不会重新请求
|
||||
});
|
||||
|
||||
// ❌ 在 queryFn 中使用不稳定的引用
|
||||
function BadQuery({ filters }) {
|
||||
useQuery({
|
||||
queryKey: ['items'], // queryKey 没有包含 filters!
|
||||
queryFn: () => fetchItems(filters), // filters 变化不会触发重新请求
|
||||
queryKey: ['items'], // queryKey 没有包含 filters!
|
||||
queryFn: () => fetchItems(filters), // filters 变化不会触发重新请求
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ queryKey 包含所有影响数据的参数
|
||||
function GoodQuery({ filters }) {
|
||||
useQuery({
|
||||
queryKey: ['items', filters], // filters 是 queryKey 的一部分
|
||||
queryKey: ['items', filters], // filters 是 queryKey 的一部分
|
||||
queryFn: () => fetchItems(filters),
|
||||
});
|
||||
}
|
||||
@@ -634,13 +632,13 @@ function GoodQuery({ filters }) {
|
||||
|
||||
#### useSuspenseQuery 的限制
|
||||
|
||||
| 特性 | useQuery | useSuspenseQuery |
|
||||
|------|----------|------------------|
|
||||
| `enabled` 选项 | ✅ 支持 | ❌ 不支持 |
|
||||
| `placeholderData` | ✅ 支持 | ❌ 不支持 |
|
||||
| `data` 类型 | `T \| undefined` | `T`(保证有值)|
|
||||
| 错误处理 | `error` 属性 | 抛出到 Error Boundary |
|
||||
| 加载状态 | `isLoading` 属性 | 挂起到 Suspense |
|
||||
| 特性 | useQuery | useSuspenseQuery |
|
||||
| ----------------- | ---------------- | --------------------- |
|
||||
| `enabled` 选项 | ✅ 支持 | ❌ 不支持 |
|
||||
| `placeholderData` | ✅ 支持 | ❌ 不支持 |
|
||||
| `data` 类型 | `T \| undefined` | `T`(保证有值) |
|
||||
| 错误处理 | `error` 属性 | 抛出到 Error Boundary |
|
||||
| 加载状态 | `isLoading` 属性 | 挂起到 Suspense |
|
||||
|
||||
#### 不支持 enabled 的替代方案
|
||||
|
||||
@@ -650,7 +648,7 @@ function BadSuspenseQuery({ userId }) {
|
||||
const { data } = useSuspenseQuery({
|
||||
queryKey: ['user', userId],
|
||||
queryFn: () => fetchUser(userId),
|
||||
enabled: !!userId, // useSuspenseQuery 不支持 enabled!
|
||||
enabled: !!userId, // useSuspenseQuery 不支持 enabled!
|
||||
});
|
||||
}
|
||||
|
||||
@@ -764,7 +762,9 @@ function TodoList() {
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{todos?.map(todo => <TodoItem key={todo.id} todo={todo} />)}
|
||||
{todos?.map((todo) => (
|
||||
<TodoItem key={todo.id} todo={todo} />
|
||||
))}
|
||||
{/* 乐观显示正在添加的 todo */}
|
||||
{isPending && <TodoItem todo={variables} isOptimistic />}
|
||||
</ul>
|
||||
@@ -867,5 +867,5 @@ if (isLoading) return <Spinner />; // 首次加载中
|
||||
- [ ] 使用 @testing-library/react
|
||||
- [ ] 用 screen 查询元素
|
||||
- [ ] 用 userEvent 代替 fireEvent
|
||||
- [ ] 优先使用 *ByRole 查询
|
||||
- [ ] 优先使用 \*ByRole 查询
|
||||
- [ ] 测试行为而非实现细节
|
||||
|
||||
@@ -764,11 +764,13 @@ fn create_handler() -> impl Handler {
|
||||
### 编译器不能捕获的问题
|
||||
|
||||
**业务逻辑正确性**
|
||||
|
||||
- [ ] 边界条件处理正确
|
||||
- [ ] 状态机转换完整
|
||||
- [ ] 并发场景下的竞态条件
|
||||
|
||||
**API 设计**
|
||||
|
||||
- [ ] 公共 API 难以误用
|
||||
- [ ] 类型签名清晰表达意图
|
||||
- [ ] 错误类型粒度合适
|
||||
|
||||
+50
-28
@@ -5,6 +5,7 @@ Security-focused code review checklist based on OWASP Top 10 and best practices.
|
||||
## Authentication & Authorization
|
||||
|
||||
### Authentication
|
||||
|
||||
- [ ] Passwords hashed with strong algorithm (bcrypt, argon2)
|
||||
- [ ] Password complexity requirements enforced
|
||||
- [ ] Account lockout after failed attempts
|
||||
@@ -14,6 +15,7 @@ Security-focused code review checklist based on OWASP Top 10 and best practices.
|
||||
- [ ] Session timeout implemented
|
||||
|
||||
### Authorization
|
||||
|
||||
- [ ] Authorization checks on every request
|
||||
- [ ] Principle of least privilege applied
|
||||
- [ ] Role-based access control (RBAC) properly implemented
|
||||
@@ -22,6 +24,7 @@ Security-focused code review checklist based on OWASP Top 10 and best practices.
|
||||
- [ ] API endpoints protected appropriately
|
||||
|
||||
### JWT Security
|
||||
|
||||
```typescript
|
||||
// ❌ Insecure JWT configuration
|
||||
jwt.sign(payload, 'weak-secret');
|
||||
@@ -31,23 +34,24 @@ jwt.sign(payload, process.env.JWT_SECRET, {
|
||||
algorithm: 'RS256',
|
||||
expiresIn: '15m',
|
||||
issuer: 'your-app',
|
||||
audience: 'your-api'
|
||||
audience: 'your-api',
|
||||
});
|
||||
|
||||
// ❌ Not verifying JWT properly
|
||||
const decoded = jwt.decode(token); // No signature verification!
|
||||
const decoded = jwt.decode(token); // No signature verification!
|
||||
|
||||
// ✅ Verify signature and claims
|
||||
const decoded = jwt.verify(token, publicKey, {
|
||||
algorithms: ['RS256'],
|
||||
issuer: 'your-app',
|
||||
audience: 'your-api'
|
||||
audience: 'your-api',
|
||||
});
|
||||
```
|
||||
|
||||
## Input Validation
|
||||
|
||||
### SQL Injection Prevention
|
||||
|
||||
```python
|
||||
# ❌ Vulnerable to SQL injection
|
||||
query = f"SELECT * FROM users WHERE id = {user_id}"
|
||||
@@ -60,6 +64,7 @@ User.objects.filter(id=user_id)
|
||||
```
|
||||
|
||||
### XSS Prevention
|
||||
|
||||
```typescript
|
||||
// ❌ Vulnerable to XSS
|
||||
element.innerHTML = userInput;
|
||||
@@ -76,6 +81,7 @@ return <div dangerouslySetInnerHTML={{__html: userInput}} />; // Dangerous!
|
||||
```
|
||||
|
||||
### Command Injection Prevention
|
||||
|
||||
```python
|
||||
# ❌ Vulnerable to command injection
|
||||
os.system(f"convert {filename} output.png")
|
||||
@@ -89,6 +95,7 @@ safe_filename = shlex.quote(filename)
|
||||
```
|
||||
|
||||
### Path Traversal Prevention
|
||||
|
||||
```typescript
|
||||
// ❌ Vulnerable to path traversal
|
||||
const filePath = `./uploads/${req.params.filename}`;
|
||||
@@ -107,6 +114,7 @@ if (!filePath.startsWith(path.resolve('./uploads'))) {
|
||||
## Data Protection
|
||||
|
||||
### Sensitive Data Handling
|
||||
|
||||
- [ ] No secrets in source code
|
||||
- [ ] Secrets stored in environment variables or secret manager
|
||||
- [ ] Sensitive data encrypted at rest
|
||||
@@ -116,6 +124,7 @@ if (!filePath.startsWith(path.resolve('./uploads'))) {
|
||||
- [ ] Secure data deletion when required
|
||||
|
||||
### Configuration Security
|
||||
|
||||
```yaml
|
||||
# ❌ Secrets in config files
|
||||
database:
|
||||
@@ -127,6 +136,7 @@ database:
|
||||
```
|
||||
|
||||
### Error Messages
|
||||
|
||||
```typescript
|
||||
// ❌ Leaking sensitive information
|
||||
catch (error) {
|
||||
@@ -148,45 +158,53 @@ catch (error) {
|
||||
## API Security
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
- [ ] Rate limiting on all public endpoints
|
||||
- [ ] Stricter limits on authentication endpoints
|
||||
- [ ] Per-user and per-IP limits
|
||||
- [ ] Graceful handling when limits exceeded
|
||||
|
||||
### CORS Configuration
|
||||
|
||||
```typescript
|
||||
// ❌ Overly permissive CORS
|
||||
app.use(cors({ origin: '*' }));
|
||||
|
||||
// ✅ Restrictive CORS
|
||||
app.use(cors({
|
||||
origin: ['https://your-app.com'],
|
||||
methods: ['GET', 'POST'],
|
||||
credentials: true
|
||||
}));
|
||||
app.use(
|
||||
cors({
|
||||
origin: ['https://your-app.com'],
|
||||
methods: ['GET', 'POST'],
|
||||
credentials: true,
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
### HTTP Headers
|
||||
|
||||
```typescript
|
||||
// Security headers to set
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
scriptSrc: ["'self'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||
}
|
||||
},
|
||||
hsts: { maxAge: 31536000, includeSubDomains: true },
|
||||
noSniff: true,
|
||||
xssFilter: true,
|
||||
frameguard: { action: 'deny' }
|
||||
}));
|
||||
app.use(
|
||||
helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
scriptSrc: ["'self'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||
},
|
||||
},
|
||||
hsts: { maxAge: 31536000, includeSubDomains: true },
|
||||
noSniff: true,
|
||||
xssFilter: true,
|
||||
frameguard: { action: 'deny' },
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
## Cryptography
|
||||
|
||||
### Secure Practices
|
||||
|
||||
- [ ] Using well-established algorithms (AES-256, RSA-2048+)
|
||||
- [ ] Not implementing custom cryptography
|
||||
- [ ] Using cryptographically secure random number generation
|
||||
@@ -194,6 +212,7 @@ app.use(helmet({
|
||||
- [ ] Secure key storage (HSM, KMS)
|
||||
|
||||
### Common Mistakes
|
||||
|
||||
```typescript
|
||||
// ❌ Weak random generation
|
||||
const token = Math.random().toString(36);
|
||||
@@ -213,6 +232,7 @@ const hash = await bcrypt.hash(password, 12);
|
||||
## Dependency Security
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] Dependencies from trusted sources only
|
||||
- [ ] No known vulnerabilities (npm audit, cargo audit)
|
||||
- [ ] Dependencies kept up to date
|
||||
@@ -221,6 +241,7 @@ const hash = await bcrypt.hash(password, 12);
|
||||
- [ ] License compliance verified
|
||||
|
||||
### Audit Commands
|
||||
|
||||
```bash
|
||||
# Node.js
|
||||
npm audit
|
||||
@@ -240,6 +261,7 @@ snyk test
|
||||
## Logging & Monitoring
|
||||
|
||||
### Secure Logging
|
||||
|
||||
- [ ] No sensitive data in logs (passwords, tokens, PII)
|
||||
- [ ] Logs protected from tampering
|
||||
- [ ] Appropriate log retention
|
||||
@@ -256,10 +278,10 @@ logger.info('User login attempt', { email, success: true });
|
||||
|
||||
## Security Review Severity Levels
|
||||
|
||||
| Severity | Description | Action |
|
||||
|----------|-------------|--------|
|
||||
| **Critical** | Immediate exploitation possible, data breach risk | Block merge, fix immediately |
|
||||
| **High** | Significant vulnerability, requires specific conditions | Block merge, fix before release |
|
||||
| **Medium** | Moderate risk, defense in depth concern | Should fix, can merge with tracking |
|
||||
| **Low** | Minor issue, best practice violation | Nice to fix, non-blocking |
|
||||
| **Info** | Suggestion for improvement | Optional enhancement |
|
||||
| Severity | Description | Action |
|
||||
| ------------ | ------------------------------------------------------- | ----------------------------------- |
|
||||
| **Critical** | Immediate exploitation possible, data breach risk | Block merge, fix immediately |
|
||||
| **High** | Significant vulnerability, requires specific conditions | Block merge, fix before release |
|
||||
| **Medium** | Moderate risk, defense in depth concern | Should fix, can merge with tracking |
|
||||
| **Low** | Minor issue, best practice violation | Nice to fix, non-blocking |
|
||||
| **Info** | Suggestion for improvement | Optional enhancement |
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
```typescript
|
||||
// ❌ Using any defeats type safety
|
||||
function processData(data: any) {
|
||||
return data.value; // 无类型检查,运行时可能崩溃
|
||||
return data.value; // 无类型检查,运行时可能崩溃
|
||||
}
|
||||
|
||||
// ✅ Use proper types
|
||||
@@ -47,7 +47,7 @@ function processUnknown(data: unknown) {
|
||||
```typescript
|
||||
// ❌ 不安全的类型断言
|
||||
function getLength(value: string | string[]) {
|
||||
return (value as string[]).length; // 如果是 string 会出错
|
||||
return (value as string[]).length; // 如果是 string 会出错
|
||||
}
|
||||
|
||||
// ✅ 使用类型守卫
|
||||
@@ -59,8 +59,12 @@ function getLength(value: string | string[]): number {
|
||||
}
|
||||
|
||||
// ✅ 使用 in 操作符
|
||||
interface Dog { bark(): void }
|
||||
interface Cat { meow(): void }
|
||||
interface Dog {
|
||||
bark(): void;
|
||||
}
|
||||
interface Cat {
|
||||
meow(): void;
|
||||
}
|
||||
|
||||
function speak(animal: Dog | Cat) {
|
||||
if ('bark' in animal) {
|
||||
@@ -117,7 +121,7 @@ function getFirst<T>(arr: T[]): T | undefined {
|
||||
```typescript
|
||||
// ❌ 泛型没有约束,无法访问属性
|
||||
function getProperty<T>(obj: T, key: string) {
|
||||
return obj[key]; // Error: 无法索引
|
||||
return obj[key]; // Error: 无法索引
|
||||
}
|
||||
|
||||
// ✅ 使用 keyof 约束
|
||||
@@ -126,9 +130,9 @@ function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
|
||||
}
|
||||
|
||||
const user = { name: 'Alice', age: 30 };
|
||||
getProperty(user, 'name'); // 返回类型是 string
|
||||
getProperty(user, 'age'); // 返回类型是 number
|
||||
getProperty(user, 'foo'); // Error: 'foo' 不在 keyof User
|
||||
getProperty(user, 'name'); // 返回类型是 string
|
||||
getProperty(user, 'age'); // 返回类型是 number
|
||||
getProperty(user, 'foo'); // Error: 'foo' 不在 keyof User
|
||||
```
|
||||
|
||||
### 泛型默认值
|
||||
@@ -157,13 +161,13 @@ interface User {
|
||||
email: string;
|
||||
}
|
||||
|
||||
type PartialUser = Partial<User>; // 所有属性可选
|
||||
type RequiredUser = Required<User>; // 所有属性必需
|
||||
type ReadonlyUser = Readonly<User>; // 所有属性只读
|
||||
type UserKeys = keyof User; // 'id' | 'name' | 'email'
|
||||
type NameOnly = Pick<User, 'name'>; // { name: string }
|
||||
type WithoutId = Omit<User, 'id'>; // { name: string; email: string }
|
||||
type UserRecord = Record<string, User>; // { [key: string]: User }
|
||||
type PartialUser = Partial<User>; // 所有属性可选
|
||||
type RequiredUser = Required<User>; // 所有属性必需
|
||||
type ReadonlyUser = Readonly<User>; // 所有属性只读
|
||||
type UserKeys = keyof User; // 'id' | 'name' | 'email'
|
||||
type NameOnly = Pick<User, 'name'>; // { name: string }
|
||||
type WithoutId = Omit<User, 'id'>; // { name: string; email: string }
|
||||
type UserRecord = Record<string, User>; // { [key: string]: User }
|
||||
```
|
||||
|
||||
---
|
||||
@@ -176,13 +180,13 @@ type UserRecord = Record<string, User>; // { [key: string]: User }
|
||||
// ✅ 根据输入类型返回不同类型
|
||||
type IsString<T> = T extends string ? true : false;
|
||||
|
||||
type A = IsString<string>; // true
|
||||
type B = IsString<number>; // false
|
||||
type A = IsString<string>; // true
|
||||
type B = IsString<number>; // false
|
||||
|
||||
// ✅ 提取数组元素类型
|
||||
type ElementType<T> = T extends (infer U)[] ? U : never;
|
||||
|
||||
type Elem = ElementType<string[]>; // string
|
||||
type Elem = ElementType<string[]>; // string
|
||||
|
||||
// ✅ 提取函数返回类型(内置 ReturnType)
|
||||
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
|
||||
@@ -223,23 +227,21 @@ type HandlerName = `on${Capitalize<EventName>}`;
|
||||
|
||||
// ✅ API 路由类型
|
||||
type ApiRoute = `/api/${string}`;
|
||||
const route: ApiRoute = '/api/users'; // OK
|
||||
const badRoute: ApiRoute = '/users'; // Error
|
||||
const route: ApiRoute = '/api/users'; // OK
|
||||
const badRoute: ApiRoute = '/users'; // Error
|
||||
```
|
||||
|
||||
### Discriminated Unions
|
||||
|
||||
```typescript
|
||||
// ✅ 使用判别属性实现类型安全
|
||||
type Result<T, E> =
|
||||
| { success: true; data: T }
|
||||
| { success: false; error: E };
|
||||
type Result<T, E> = { success: true; data: T } | { success: false; error: E };
|
||||
|
||||
function handleResult(result: Result<User, Error>) {
|
||||
if (result.success) {
|
||||
console.log(result.data.name); // TypeScript 知道 data 存在
|
||||
console.log(result.data.name); // TypeScript 知道 data 存在
|
||||
} else {
|
||||
console.log(result.error.message); // TypeScript 知道 error 存在
|
||||
console.log(result.error.message); // TypeScript 知道 error 存在
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,11 +254,11 @@ type Action =
|
||||
function reducer(state: number, action: Action): number {
|
||||
switch (action.type) {
|
||||
case 'INCREMENT':
|
||||
return state + action.payload; // payload 类型已知
|
||||
return state + action.payload; // payload 类型已知
|
||||
case 'DECREMENT':
|
||||
return state - action.payload;
|
||||
case 'RESET':
|
||||
return 0; // 这里没有 payload
|
||||
return 0; // 这里没有 payload
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -296,10 +298,10 @@ function reducer(state: number, action: Action): number {
|
||||
// tsconfig: "noUncheckedIndexedAccess": true
|
||||
|
||||
const arr = [1, 2, 3];
|
||||
const first = arr[0]; // 类型是 number | undefined
|
||||
const first = arr[0]; // 类型是 number | undefined
|
||||
|
||||
// ❌ 直接使用可能出错
|
||||
console.log(first.toFixed(2)); // Error: 可能是 undefined
|
||||
console.log(first.toFixed(2)); // Error: 可能是 undefined
|
||||
|
||||
// ✅ 先检查
|
||||
if (first !== undefined) {
|
||||
@@ -320,7 +322,7 @@ console.log(arr[0]!.toFixed(2));
|
||||
// ❌ Not handling async errors
|
||||
async function fetchUser(id: string) {
|
||||
const response = await fetch(`/api/users/${id}`);
|
||||
return response.json(); // 网络错误未处理
|
||||
return response.json(); // 网络错误未处理
|
||||
}
|
||||
|
||||
// ✅ Handle errors properly
|
||||
@@ -346,7 +348,7 @@ async function fetchUser(id: string): Promise<User> {
|
||||
// ❌ Promise.all 一个失败全部失败
|
||||
async function fetchAllUsers(ids: string[]) {
|
||||
const users = await Promise.all(ids.map(fetchUser));
|
||||
return users; // 一个失败就全部失败
|
||||
return users; // 一个失败就全部失败
|
||||
}
|
||||
|
||||
// ✅ Promise.allSettled 获取所有结果
|
||||
@@ -378,8 +380,8 @@ function useSearch() {
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`/api/search?q=${query}`)
|
||||
.then(r => r.json())
|
||||
.then(setResults); // 旧请求可能后返回!
|
||||
.then((r) => r.json())
|
||||
.then(setResults); // 旧请求可能后返回!
|
||||
}, [query]);
|
||||
}
|
||||
|
||||
@@ -392,9 +394,9 @@ function useSearch() {
|
||||
const controller = new AbortController();
|
||||
|
||||
fetch(`/api/search?q=${query}`, { signal: controller.signal })
|
||||
.then(r => r.json())
|
||||
.then((r) => r.json())
|
||||
.then(setResults)
|
||||
.catch(e => {
|
||||
.catch((e) => {
|
||||
if (e.name !== 'AbortError') throw e;
|
||||
});
|
||||
|
||||
@@ -412,7 +414,7 @@ function useSearch() {
|
||||
```typescript
|
||||
// ❌ 可变参数可能被意外修改
|
||||
function processUsers(users: User[]) {
|
||||
users.sort((a, b) => a.name.localeCompare(b.name)); // 修改了原数组!
|
||||
users.sort((a, b) => a.name.localeCompare(b.name)); // 修改了原数组!
|
||||
return users;
|
||||
}
|
||||
|
||||
@@ -452,7 +454,7 @@ module.exports = {
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:@typescript-eslint/recommended-requiring-type-checking',
|
||||
'plugin:@typescript-eslint/strict'
|
||||
'plugin:@typescript-eslint/strict',
|
||||
],
|
||||
rules: {
|
||||
// ✅ 类型安全
|
||||
@@ -471,8 +473,8 @@ module.exports = {
|
||||
// ✅ 代码风格
|
||||
'@typescript-eslint/consistent-type-imports': 'error',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'error',
|
||||
'@typescript-eslint/prefer-optional-chain': 'error'
|
||||
}
|
||||
'@typescript-eslint/prefer-optional-chain': 'error',
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
@@ -509,6 +511,7 @@ await Promise.all(items.map(processItem));
|
||||
## Review Checklist
|
||||
|
||||
### 类型系统
|
||||
|
||||
- [ ] 没有使用 `any`(使用 `unknown` + 类型守卫代替)
|
||||
- [ ] 接口和类型定义完整且有意义的命名
|
||||
- [ ] 使用泛型提高代码复用性
|
||||
@@ -516,16 +519,19 @@ await Promise.all(items.map(processItem));
|
||||
- [ ] 善用工具类型(Partial、Pick、Omit 等)
|
||||
|
||||
### 泛型
|
||||
|
||||
- [ ] 泛型有适当的约束(extends)
|
||||
- [ ] 泛型参数有合理的默认值
|
||||
- [ ] 避免过度泛型化(KISS 原则)
|
||||
|
||||
### Strict 模式
|
||||
|
||||
- [ ] tsconfig.json 启用了 strict: true
|
||||
- [ ] 启用了 noUncheckedIndexedAccess
|
||||
- [ ] 没有使用 @ts-ignore(改用 @ts-expect-error)
|
||||
|
||||
### 异步代码
|
||||
|
||||
- [ ] async 函数有错误处理
|
||||
- [ ] Promise rejection 被正确处理
|
||||
- [ ] 没有 floating promises(未处理的 Promise)
|
||||
@@ -533,11 +539,13 @@ await Promise.all(items.map(processItem));
|
||||
- [ ] 竞态条件使用 AbortController 处理
|
||||
|
||||
### 不可变性
|
||||
|
||||
- [ ] 不直接修改函数参数
|
||||
- [ ] 使用 spread 操作符创建新对象/数组
|
||||
- [ ] 考虑使用 readonly 修饰符
|
||||
|
||||
### ESLint
|
||||
|
||||
- [ ] 使用 @typescript-eslint/recommended
|
||||
- [ ] 没有 ESLint 警告或错误
|
||||
- [ ] 使用 consistent-type-imports
|
||||
|
||||
@@ -22,11 +22,11 @@
|
||||
```vue
|
||||
<!-- ✅ 基本类型用 ref -->
|
||||
<script setup lang="ts">
|
||||
const count = ref(0)
|
||||
const name = ref('Vue')
|
||||
const count = ref(0);
|
||||
const name = ref('Vue');
|
||||
|
||||
// ref 需要 .value 访问
|
||||
count.value++
|
||||
count.value++;
|
||||
</script>
|
||||
|
||||
<!-- ✅ 对象/数组用 reactive(可选)-->
|
||||
@@ -34,18 +34,18 @@ count.value++
|
||||
const state = reactive({
|
||||
user: null,
|
||||
loading: false,
|
||||
error: null
|
||||
})
|
||||
error: null,
|
||||
});
|
||||
|
||||
// reactive 直接访问
|
||||
state.loading = true
|
||||
state.loading = true;
|
||||
</script>
|
||||
|
||||
<!-- 💡 现代最佳实践:全部使用 ref,保持一致性 -->
|
||||
<script setup lang="ts">
|
||||
const user = ref<User | null>(null)
|
||||
const loading = ref(false)
|
||||
const error = ref<Error | null>(null)
|
||||
const user = ref<User | null>(null);
|
||||
const loading = ref(false);
|
||||
const error = ref<Error | null>(null);
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -54,17 +54,17 @@ const error = ref<Error | null>(null)
|
||||
```vue
|
||||
<!-- ❌ 解构 reactive 会丢失响应性 -->
|
||||
<script setup lang="ts">
|
||||
const state = reactive({ count: 0, name: 'Vue' })
|
||||
const { count, name } = state // 丢失响应性!
|
||||
const state = reactive({ count: 0, name: 'Vue' });
|
||||
const { count, name } = state; // 丢失响应性!
|
||||
</script>
|
||||
|
||||
<!-- ✅ 使用 toRefs 保持响应性 -->
|
||||
<script setup lang="ts">
|
||||
const state = reactive({ count: 0, name: 'Vue' })
|
||||
const { count, name } = toRefs(state) // 保持响应性
|
||||
const state = reactive({ count: 0, name: 'Vue' });
|
||||
const { count, name } = toRefs(state); // 保持响应性
|
||||
// 或者直接使用 ref
|
||||
const count = ref(0)
|
||||
const name = ref('Vue')
|
||||
const count = ref(0);
|
||||
const name = ref('Vue');
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -74,21 +74,21 @@ const name = ref('Vue')
|
||||
<!-- ❌ computed 中产生副作用 -->
|
||||
<script setup lang="ts">
|
||||
const fullName = computed(() => {
|
||||
console.log('Computing...') // 副作用!
|
||||
otherRef.value = 'changed' // 修改其他状态!
|
||||
return `${firstName.value} ${lastName.value}`
|
||||
})
|
||||
console.log('Computing...'); // 副作用!
|
||||
otherRef.value = 'changed'; // 修改其他状态!
|
||||
return `${firstName.value} ${lastName.value}`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- ✅ computed 只用于派生状态 -->
|
||||
<script setup lang="ts">
|
||||
const fullName = computed(() => {
|
||||
return `${firstName.value} ${lastName.value}`
|
||||
})
|
||||
return `${firstName.value} ${lastName.value}`;
|
||||
});
|
||||
// 副作用放在 watch 或事件处理中
|
||||
watch(fullName, (name) => {
|
||||
console.log('Name changed:', name)
|
||||
})
|
||||
console.log('Name changed:', name);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -97,25 +97,25 @@ watch(fullName, (name) => {
|
||||
```vue
|
||||
<!-- ❌ 大型对象使用 ref 会深度转换 -->
|
||||
<script setup lang="ts">
|
||||
const largeData = ref(hugeNestedObject) // 深度响应式,性能开销大
|
||||
const largeData = ref(hugeNestedObject); // 深度响应式,性能开销大
|
||||
</script>
|
||||
|
||||
<!-- ✅ 使用 shallowRef 避免深度转换 -->
|
||||
<script setup lang="ts">
|
||||
const largeData = shallowRef(hugeNestedObject)
|
||||
const largeData = shallowRef(hugeNestedObject);
|
||||
|
||||
// 整体替换才会触发更新
|
||||
function updateData(newData) {
|
||||
largeData.value = newData // ✅ 触发更新
|
||||
largeData.value = newData; // ✅ 触发更新
|
||||
}
|
||||
|
||||
// ❌ 修改嵌套属性不会触发更新
|
||||
// largeData.value.nested.prop = 'new'
|
||||
|
||||
// 需要手动触发时使用 triggerRef
|
||||
import { triggerRef } from 'vue'
|
||||
largeData.value.nested.prop = 'new'
|
||||
triggerRef(largeData)
|
||||
import { triggerRef } from 'vue';
|
||||
largeData.value.nested.prop = 'new';
|
||||
triggerRef(largeData);
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -128,17 +128,17 @@ triggerRef(largeData)
|
||||
```vue
|
||||
<!-- ❌ 直接修改 props -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ user: User }>()
|
||||
props.user.name = 'New Name' // 永远不要直接修改 props!
|
||||
const props = defineProps<{ user: User }>();
|
||||
props.user.name = 'New Name'; // 永远不要直接修改 props!
|
||||
</script>
|
||||
|
||||
<!-- ✅ 使用 emit 通知父组件更新 -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ user: User }>()
|
||||
const props = defineProps<{ user: User }>();
|
||||
const emit = defineEmits<{
|
||||
update: [name: string]
|
||||
}>()
|
||||
const updateName = (name: string) => emit('update', name)
|
||||
update: [name: string];
|
||||
}>();
|
||||
const updateName = (name: string) => emit('update', name);
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -147,20 +147,20 @@ const updateName = (name: string) => emit('update', name)
|
||||
```vue
|
||||
<!-- ❌ defineProps 缺少类型声明 -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps(['title', 'count']) // 无类型检查
|
||||
const props = defineProps(['title', 'count']); // 无类型检查
|
||||
</script>
|
||||
|
||||
<!-- ✅ 使用类型声明 + withDefaults -->
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
title: string
|
||||
count?: number
|
||||
items?: string[]
|
||||
title: string;
|
||||
count?: number;
|
||||
items?: string[];
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
count: 0,
|
||||
items: () => [] // 对象/数组默认值需要工厂函数
|
||||
})
|
||||
items: () => [], // 对象/数组默认值需要工厂函数
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -169,21 +169,21 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
```vue
|
||||
<!-- ❌ defineEmits 缺少类型 -->
|
||||
<script setup lang="ts">
|
||||
const emit = defineEmits(['update', 'delete']) // 无类型检查
|
||||
emit('update', someValue) // 参数类型不安全
|
||||
const emit = defineEmits(['update', 'delete']); // 无类型检查
|
||||
emit('update', someValue); // 参数类型不安全
|
||||
</script>
|
||||
|
||||
<!-- ✅ 完整的类型定义 -->
|
||||
<script setup lang="ts">
|
||||
const emit = defineEmits<{
|
||||
update: [id: number, value: string]
|
||||
delete: [id: number]
|
||||
'custom-event': [payload: CustomPayload]
|
||||
}>()
|
||||
update: [id: number, value: string];
|
||||
delete: [id: number];
|
||||
'custom-event': [payload: CustomPayload];
|
||||
}>();
|
||||
|
||||
// 现在有完整的类型检查
|
||||
emit('update', 1, 'new value') // ✅
|
||||
emit('update', 'wrong') // ❌ TypeScript 报错
|
||||
emit('update', 1, 'new value'); // ✅
|
||||
emit('update', 'wrong'); // ❌ TypeScript 报错
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -196,22 +196,25 @@ emit('update', 'wrong') // ❌ TypeScript 报错
|
||||
```vue
|
||||
<!-- Vue 3.5 之前:解构会丢失响应性 -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ count: number }>()
|
||||
const props = defineProps<{ count: number }>();
|
||||
// 需要使用 props.count 或 toRefs
|
||||
</script>
|
||||
|
||||
<!-- ✅ Vue 3.5+:解构保持响应性 -->
|
||||
<script setup lang="ts">
|
||||
const { count, name = 'default' } = defineProps<{
|
||||
count: number
|
||||
name?: string
|
||||
}>()
|
||||
count: number;
|
||||
name?: string;
|
||||
}>();
|
||||
|
||||
// count 和 name 自动保持响应性!
|
||||
// 可以直接在模板和 watch 中使用
|
||||
watch(() => count, (newCount) => {
|
||||
console.log('Count changed:', newCount)
|
||||
})
|
||||
watch(
|
||||
() => count,
|
||||
(newCount) => {
|
||||
console.log('Count changed:', newCount);
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<!-- ✅ 配合默认值使用 -->
|
||||
@@ -219,12 +222,12 @@ watch(() => count, (newCount) => {
|
||||
const {
|
||||
title,
|
||||
count = 0,
|
||||
items = () => [] // 函数作为默认值(对象/数组)
|
||||
items = () => [], // 函数作为默认值(对象/数组)
|
||||
} = defineProps<{
|
||||
title: string
|
||||
count?: number
|
||||
items?: () => string[]
|
||||
}>()
|
||||
title: string;
|
||||
count?: number;
|
||||
items?: () => string[];
|
||||
}>();
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -233,23 +236,23 @@ const {
|
||||
```vue
|
||||
<!-- ❌ 传统 v-model 实现:冗长 -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ modelValue: string }>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
|
||||
const props = defineProps<{ modelValue: string }>();
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: string] }>();
|
||||
|
||||
// 需要 computed 来双向绑定
|
||||
const value = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
set: (val) => emit('update:modelValue', val),
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- ✅ defineModel:简洁的 v-model 实现 -->
|
||||
<script setup lang="ts">
|
||||
// 自动处理 props 和 emit
|
||||
const model = defineModel<string>()
|
||||
const model = defineModel<string>();
|
||||
|
||||
// 直接使用
|
||||
model.value = 'new value' // 自动 emit
|
||||
model.value = 'new value'; // 自动 emit
|
||||
</script>
|
||||
<template>
|
||||
<input v-model="model" />
|
||||
@@ -258,19 +261,19 @@ model.value = 'new value' // 自动 emit
|
||||
<!-- ✅ 命名 v-model -->
|
||||
<script setup lang="ts">
|
||||
// v-model:title 的实现
|
||||
const title = defineModel<string>('title')
|
||||
const title = defineModel<string>('title');
|
||||
|
||||
// 带默认值和选项
|
||||
const count = defineModel<number>('count', {
|
||||
default: 0,
|
||||
required: false
|
||||
})
|
||||
required: false,
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- ✅ 多个 v-model -->
|
||||
<script setup lang="ts">
|
||||
const firstName = defineModel<string>('firstName')
|
||||
const lastName = defineModel<string>('lastName')
|
||||
const firstName = defineModel<string>('firstName');
|
||||
const lastName = defineModel<string>('lastName');
|
||||
</script>
|
||||
<template>
|
||||
<!-- 父组件使用:<MyInput v-model:first-name="first" v-model:last-name="last" /> -->
|
||||
@@ -278,7 +281,7 @@ const lastName = defineModel<string>('lastName')
|
||||
|
||||
<!-- ✅ v-model 修饰符 -->
|
||||
<script setup lang="ts">
|
||||
const [model, modifiers] = defineModel<string>()
|
||||
const [model, modifiers] = defineModel<string>();
|
||||
|
||||
// 检查修饰符
|
||||
if (modifiers.capitalize) {
|
||||
@@ -292,7 +295,7 @@ if (modifiers.capitalize) {
|
||||
```vue
|
||||
<!-- 传统方式:ref 属性与变量同名 -->
|
||||
<script setup lang="ts">
|
||||
const inputRef = ref<HTMLInputElement | null>(null)
|
||||
const inputRef = ref<HTMLInputElement | null>(null);
|
||||
</script>
|
||||
<template>
|
||||
<input ref="inputRef" />
|
||||
@@ -300,13 +303,13 @@ const inputRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
<!-- ✅ useTemplateRef:更清晰的模板引用 -->
|
||||
<script setup lang="ts">
|
||||
import { useTemplateRef } from 'vue'
|
||||
import { useTemplateRef } from 'vue';
|
||||
|
||||
const input = useTemplateRef<HTMLInputElement>('my-input')
|
||||
const input = useTemplateRef<HTMLInputElement>('my-input');
|
||||
|
||||
onMounted(() => {
|
||||
input.value?.focus()
|
||||
})
|
||||
input.value?.focus();
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<input ref="my-input" />
|
||||
@@ -314,8 +317,8 @@ onMounted(() => {
|
||||
|
||||
<!-- ✅ 动态 ref -->
|
||||
<script setup lang="ts">
|
||||
const refKey = ref('input-a')
|
||||
const dynamicInput = useTemplateRef<HTMLInputElement>(refKey)
|
||||
const refKey = ref('input-a');
|
||||
const dynamicInput = useTemplateRef<HTMLInputElement>(refKey);
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -324,14 +327,14 @@ const dynamicInput = useTemplateRef<HTMLInputElement>(refKey)
|
||||
```vue
|
||||
<!-- ❌ 手动生成 ID 可能冲突 -->
|
||||
<script setup lang="ts">
|
||||
const id = `input-${Math.random()}` // SSR 不一致!
|
||||
const id = `input-${Math.random()}`; // SSR 不一致!
|
||||
</script>
|
||||
|
||||
<!-- ✅ useId:SSR 安全的唯一 ID -->
|
||||
<script setup lang="ts">
|
||||
import { useId } from 'vue'
|
||||
import { useId } from 'vue';
|
||||
|
||||
const id = useId() // 例如:'v-0'
|
||||
const id = useId(); // 例如:'v-0'
|
||||
</script>
|
||||
<template>
|
||||
<label :for="id">Name</label>
|
||||
@@ -340,15 +343,12 @@ const id = useId() // 例如:'v-0'
|
||||
|
||||
<!-- ✅ 表单组件中使用 -->
|
||||
<script setup lang="ts">
|
||||
const inputId = useId()
|
||||
const errorId = useId()
|
||||
const inputId = useId();
|
||||
const errorId = useId();
|
||||
</script>
|
||||
<template>
|
||||
<label :for="inputId">Email</label>
|
||||
<input
|
||||
:id="inputId"
|
||||
:aria-describedby="errorId"
|
||||
/>
|
||||
<input :id="inputId" :aria-describedby="errorId" />
|
||||
<span :id="errorId" class="error">{{ error }}</span>
|
||||
</template>
|
||||
```
|
||||
@@ -359,28 +359,28 @@ const errorId = useId()
|
||||
<!-- 传统方式:watch 第三个参数 -->
|
||||
<script setup lang="ts">
|
||||
watch(source, async (value, oldValue, onCleanup) => {
|
||||
const controller = new AbortController()
|
||||
onCleanup(() => controller.abort())
|
||||
const controller = new AbortController();
|
||||
onCleanup(() => controller.abort());
|
||||
// ...
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- ✅ onWatcherCleanup:更灵活的清理 -->
|
||||
<script setup lang="ts">
|
||||
import { onWatcherCleanup } from 'vue'
|
||||
import { onWatcherCleanup } from 'vue';
|
||||
|
||||
watch(source, async (value) => {
|
||||
const controller = new AbortController()
|
||||
onWatcherCleanup(() => controller.abort())
|
||||
const controller = new AbortController();
|
||||
onWatcherCleanup(() => controller.abort());
|
||||
|
||||
// 可以在任意位置调用,不限于回调开头
|
||||
if (someCondition) {
|
||||
const anotherResource = createResource()
|
||||
onWatcherCleanup(() => anotherResource.dispose())
|
||||
const anotherResource = createResource();
|
||||
onWatcherCleanup(() => anotherResource.dispose());
|
||||
}
|
||||
|
||||
await fetchData(value, controller.signal)
|
||||
})
|
||||
await fetchData(value, controller.signal);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -415,15 +415,15 @@ watch(source, async (value) => {
|
||||
watch(
|
||||
() => props.userId,
|
||||
async (userId) => {
|
||||
user.value = await fetchUser(userId)
|
||||
}
|
||||
)
|
||||
user.value = await fetchUser(userId);
|
||||
},
|
||||
);
|
||||
|
||||
// ✅ watchEffect:自动收集依赖,立即执行
|
||||
watchEffect(async () => {
|
||||
// 自动追踪 props.userId
|
||||
user.value = await fetchUser(props.userId)
|
||||
})
|
||||
user.value = await fetchUser(props.userId);
|
||||
});
|
||||
|
||||
// 💡 选择指南:
|
||||
// - 需要旧值?用 watch
|
||||
@@ -438,30 +438,30 @@ watchEffect(async () => {
|
||||
<!-- ❌ watch 缺少清理函数,可能内存泄漏 -->
|
||||
<script setup lang="ts">
|
||||
watch(searchQuery, async (query) => {
|
||||
const controller = new AbortController()
|
||||
const controller = new AbortController();
|
||||
const data = await fetch(`/api/search?q=${query}`, {
|
||||
signal: controller.signal
|
||||
})
|
||||
results.value = await data.json()
|
||||
signal: controller.signal,
|
||||
});
|
||||
results.value = await data.json();
|
||||
// 如果 query 快速变化,旧请求不会被取消!
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- ✅ 使用 onCleanup 清理副作用 -->
|
||||
<script setup lang="ts">
|
||||
watch(searchQuery, async (query, _, onCleanup) => {
|
||||
const controller = new AbortController()
|
||||
onCleanup(() => controller.abort()) // 取消旧请求
|
||||
const controller = new AbortController();
|
||||
onCleanup(() => controller.abort()); // 取消旧请求
|
||||
|
||||
try {
|
||||
const data = await fetch(`/api/search?q=${query}`, {
|
||||
signal: controller.signal
|
||||
})
|
||||
results.value = await data.json()
|
||||
signal: controller.signal,
|
||||
});
|
||||
results.value = await data.json();
|
||||
} catch (e) {
|
||||
if (e.name !== 'AbortError') throw e
|
||||
if (e.name !== 'AbortError') throw e;
|
||||
}
|
||||
})
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -473,19 +473,19 @@ watch(searchQuery, async (query, _, onCleanup) => {
|
||||
watch(
|
||||
userId,
|
||||
async (id) => {
|
||||
user.value = await fetchUser(id)
|
||||
user.value = await fetchUser(id);
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
// ✅ deep:深度监听(性能开销大,谨慎使用)
|
||||
watch(
|
||||
state,
|
||||
(newState) => {
|
||||
console.log('State changed deeply')
|
||||
console.log('State changed deeply');
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
// ✅ flush: 'post':DOM 更新后执行
|
||||
watch(
|
||||
@@ -494,17 +494,17 @@ watch(
|
||||
// 可以安全访问更新后的 DOM
|
||||
// nextTick 不再需要
|
||||
},
|
||||
{ flush: 'post' }
|
||||
)
|
||||
{ flush: 'post' },
|
||||
);
|
||||
|
||||
// ✅ once: true (Vue 3.4+):只执行一次
|
||||
watch(
|
||||
source,
|
||||
(value) => {
|
||||
console.log('只会执行一次:', value)
|
||||
console.log('只会执行一次:', value);
|
||||
},
|
||||
{ once: true }
|
||||
)
|
||||
{ once: true },
|
||||
);
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -513,20 +513,17 @@ watch(
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
// ✅ 监听多个 ref
|
||||
watch(
|
||||
[firstName, lastName],
|
||||
([newFirst, newLast], [oldFirst, oldLast]) => {
|
||||
console.log(`Name changed from ${oldFirst} ${oldLast} to ${newFirst} ${newLast}`)
|
||||
}
|
||||
)
|
||||
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
|
||||
console.log(`Name changed from ${oldFirst} ${oldLast} to ${newFirst} ${newLast}`);
|
||||
});
|
||||
|
||||
// ✅ 监听 reactive 对象的特定属性
|
||||
watch(
|
||||
() => [state.count, state.name],
|
||||
([count, name]) => {
|
||||
console.log(`count: ${count}, name: ${name}`)
|
||||
}
|
||||
)
|
||||
console.log(`count: ${count}, name: ${name}`);
|
||||
},
|
||||
);
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -571,9 +568,7 @@ watch(
|
||||
|
||||
<!-- ✅ 使用 computed 过滤 -->
|
||||
<script setup lang="ts">
|
||||
const activeUsers = computed(() =>
|
||||
users.value.filter(user => user.active)
|
||||
)
|
||||
const activeUsers = computed(() => users.value.filter((user) => user.active));
|
||||
</script>
|
||||
<template>
|
||||
<li v-for="user in activeUsers" :key="user.id">
|
||||
@@ -596,7 +591,12 @@ const activeUsers = computed(() =>
|
||||
```vue
|
||||
<!-- ❌ 内联复杂逻辑 -->
|
||||
<template>
|
||||
<button @click="items = items.filter(i => i.id !== item.id); count--">
|
||||
<button
|
||||
@click="
|
||||
items = items.filter((i) => i.id !== item.id);
|
||||
count--;
|
||||
"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</template>
|
||||
@@ -604,9 +604,9 @@ const activeUsers = computed(() =>
|
||||
<!-- ✅ 使用方法 -->
|
||||
<script setup lang="ts">
|
||||
const deleteItem = (id: number) => {
|
||||
items.value = items.value.filter(i => i.id !== id)
|
||||
count.value--
|
||||
}
|
||||
items.value = items.value.filter((i) => i.id !== id);
|
||||
count.value--;
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<button @click="deleteItem(item.id)">Delete</button>
|
||||
@@ -637,27 +637,27 @@ const deleteItem = (id: number) => {
|
||||
```typescript
|
||||
// ✅ 好的 composable 设计
|
||||
export function useCounter(initialValue = 0) {
|
||||
const count = ref(initialValue)
|
||||
const count = ref(initialValue);
|
||||
|
||||
const increment = () => count.value++
|
||||
const decrement = () => count.value--
|
||||
const reset = () => count.value = initialValue
|
||||
const increment = () => count.value++;
|
||||
const decrement = () => count.value--;
|
||||
const reset = () => (count.value = initialValue);
|
||||
|
||||
// 返回响应式引用和方法
|
||||
return {
|
||||
count: readonly(count), // 只读防止外部修改
|
||||
count: readonly(count), // 只读防止外部修改
|
||||
increment,
|
||||
decrement,
|
||||
reset
|
||||
}
|
||||
reset,
|
||||
};
|
||||
}
|
||||
|
||||
// ❌ 不要返回 .value
|
||||
export function useBadCounter() {
|
||||
const count = ref(0)
|
||||
const count = ref(0);
|
||||
return {
|
||||
count: count.value // ❌ 丢失响应性!
|
||||
}
|
||||
count: count.value, // ❌ 丢失响应性!
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
@@ -666,21 +666,21 @@ export function useBadCounter() {
|
||||
```vue
|
||||
<!-- ❌ 传递 props 到 composable 丢失响应性 -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ userId: string }>()
|
||||
const { user } = useUser(props.userId) // 丢失响应性!
|
||||
const props = defineProps<{ userId: string }>();
|
||||
const { user } = useUser(props.userId); // 丢失响应性!
|
||||
</script>
|
||||
|
||||
<!-- ✅ 使用 toRef 或 computed 保持响应性 -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ userId: string }>()
|
||||
const userIdRef = toRef(props, 'userId')
|
||||
const { user } = useUser(userIdRef) // 保持响应性
|
||||
const props = defineProps<{ userId: string }>();
|
||||
const userIdRef = toRef(props, 'userId');
|
||||
const { user } = useUser(userIdRef); // 保持响应性
|
||||
// 或使用 computed
|
||||
const { user } = useUser(computed(() => props.userId))
|
||||
const { user } = useUser(computed(() => props.userId));
|
||||
|
||||
// ✅ Vue 3.5+:直接解构使用
|
||||
const { userId } = defineProps<{ userId: string }>()
|
||||
const { user } = useUser(() => userId) // getter 函数
|
||||
const { userId } = defineProps<{ userId: string }>();
|
||||
const { user } = useUser(() => userId); // getter 函数
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -689,43 +689,43 @@ const { user } = useUser(() => userId) // getter 函数
|
||||
```typescript
|
||||
// ✅ 异步 composable 模式
|
||||
export function useFetch<T>(url: MaybeRefOrGetter<string>) {
|
||||
const data = ref<T | null>(null)
|
||||
const error = ref<Error | null>(null)
|
||||
const loading = ref(false)
|
||||
const data = ref<T | null>(null);
|
||||
const error = ref<Error | null>(null);
|
||||
const loading = ref(false);
|
||||
|
||||
const execute = async () => {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const response = await fetch(toValue(url))
|
||||
const response = await fetch(toValue(url));
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`)
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
data.value = await response.json()
|
||||
data.value = await response.json();
|
||||
} catch (e) {
|
||||
error.value = e as Error
|
||||
error.value = e as Error;
|
||||
} finally {
|
||||
loading.value = false
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 响应式 URL 时自动重新获取
|
||||
watchEffect(() => {
|
||||
toValue(url) // 追踪依赖
|
||||
execute()
|
||||
})
|
||||
toValue(url); // 追踪依赖
|
||||
execute();
|
||||
});
|
||||
|
||||
return {
|
||||
data: readonly(data),
|
||||
error: readonly(error),
|
||||
loading: readonly(loading),
|
||||
refetch: execute
|
||||
}
|
||||
refetch: execute,
|
||||
};
|
||||
}
|
||||
|
||||
// 使用
|
||||
const { data, loading, error, refetch } = useFetch<User[]>('/api/users')
|
||||
const { data, loading, error, refetch } = useFetch<User[]>('/api/users');
|
||||
```
|
||||
|
||||
### 生命周期与清理
|
||||
@@ -735,34 +735,40 @@ const { data, loading, error, refetch } = useFetch<User[]>('/api/users')
|
||||
export function useEventListener(
|
||||
target: MaybeRefOrGetter<EventTarget>,
|
||||
event: string,
|
||||
handler: EventListener
|
||||
handler: EventListener,
|
||||
) {
|
||||
// 组件挂载后添加
|
||||
onMounted(() => {
|
||||
toValue(target).addEventListener(event, handler)
|
||||
})
|
||||
toValue(target).addEventListener(event, handler);
|
||||
});
|
||||
|
||||
// 组件卸载时移除
|
||||
onUnmounted(() => {
|
||||
toValue(target).removeEventListener(event, handler)
|
||||
})
|
||||
toValue(target).removeEventListener(event, handler);
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ 使用 effectScope 管理副作用
|
||||
export function useFeature() {
|
||||
const scope = effectScope()
|
||||
const scope = effectScope();
|
||||
|
||||
scope.run(() => {
|
||||
// 所有响应式效果都在这个 scope 内
|
||||
const state = ref(0)
|
||||
watch(state, () => { /* ... */ })
|
||||
watchEffect(() => { /* ... */ })
|
||||
})
|
||||
const state = ref(0);
|
||||
watch(state, () => {
|
||||
/* ... */
|
||||
});
|
||||
watchEffect(() => {
|
||||
/* ... */
|
||||
});
|
||||
});
|
||||
|
||||
// 清理所有效果
|
||||
onUnmounted(() => scope.stop())
|
||||
onUnmounted(() => scope.stop());
|
||||
|
||||
return { /* ... */ }
|
||||
return {
|
||||
/* ... */
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
@@ -783,11 +789,7 @@ export function useFeature() {
|
||||
|
||||
<!-- ✅ 配合 v-for 使用 -->
|
||||
<template>
|
||||
<div
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
v-memo="[item.name, item.status]"
|
||||
>
|
||||
<div v-for="item in list" :key="item.id" v-memo="[item.name, item.status]">
|
||||
<!-- 只有 name 或 status 变化时重新渲染 -->
|
||||
</div>
|
||||
</template>
|
||||
@@ -797,21 +799,19 @@ export function useFeature() {
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
|
||||
// ✅ 懒加载组件
|
||||
const HeavyChart = defineAsyncComponent(() =>
|
||||
import('./components/HeavyChart.vue')
|
||||
)
|
||||
const HeavyChart = defineAsyncComponent(() => import('./components/HeavyChart.vue'));
|
||||
|
||||
// ✅ 带加载和错误状态
|
||||
const AsyncModal = defineAsyncComponent({
|
||||
loader: () => import('./components/Modal.vue'),
|
||||
loadingComponent: LoadingSpinner,
|
||||
errorComponent: ErrorDisplay,
|
||||
delay: 200, // 延迟显示 loading(避免闪烁)
|
||||
timeout: 3000 // 超时时间
|
||||
})
|
||||
delay: 200, // 延迟显示 loading(避免闪烁)
|
||||
timeout: 3000, // 超时时间
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -839,13 +839,13 @@ const AsyncModal = defineAsyncComponent({
|
||||
// KeepAlive 组件的生命周期钩子
|
||||
onActivated(() => {
|
||||
// 组件被激活时(从缓存恢复)
|
||||
refreshData()
|
||||
})
|
||||
refreshData();
|
||||
});
|
||||
|
||||
onDeactivated(() => {
|
||||
// 组件被停用时(进入缓存)
|
||||
pauseTimers()
|
||||
})
|
||||
pauseTimers();
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -854,12 +854,9 @@ onDeactivated(() => {
|
||||
```vue
|
||||
<!-- ✅ 大型列表使用虚拟滚动 -->
|
||||
<script setup lang="ts">
|
||||
import { useVirtualList } from '@vueuse/core'
|
||||
import { useVirtualList } from '@vueuse/core';
|
||||
|
||||
const { list, containerProps, wrapperProps } = useVirtualList(
|
||||
items,
|
||||
{ itemHeight: 50 }
|
||||
)
|
||||
const { list, containerProps, wrapperProps } = useVirtualList(items, { itemHeight: 50 });
|
||||
</script>
|
||||
<template>
|
||||
<div v-bind="containerProps" style="height: 400px; overflow: auto">
|
||||
@@ -877,6 +874,7 @@ const { list, containerProps, wrapperProps } = useVirtualList(
|
||||
## Review Checklist
|
||||
|
||||
### 响应性系统
|
||||
|
||||
- [ ] ref 用于基本类型,reactive 用于对象(或统一用 ref)
|
||||
- [ ] 没有解构 reactive 对象(或使用了 toRefs)
|
||||
- [ ] props 传递给 composable 时保持了响应性
|
||||
@@ -884,6 +882,7 @@ const { list, containerProps, wrapperProps } = useVirtualList(
|
||||
- [ ] computed 中没有副作用
|
||||
|
||||
### Props & Emits
|
||||
|
||||
- [ ] defineProps 使用 TypeScript 类型声明
|
||||
- [ ] 复杂默认值使用 withDefaults + 工厂函数
|
||||
- [ ] defineEmits 有完整的类型定义
|
||||
@@ -891,12 +890,14 @@ const { list, containerProps, wrapperProps } = useVirtualList(
|
||||
- [ ] 考虑使用 defineModel 简化 v-model(Vue 3.4+)
|
||||
|
||||
### Vue 3.5 新特性(如适用)
|
||||
|
||||
- [ ] 使用 Reactive Props Destructure 简化 props 访问
|
||||
- [ ] 使用 useTemplateRef 替代 ref 属性
|
||||
- [ ] 表单使用 useId 生成 SSR 安全的 ID
|
||||
- [ ] 使用 onWatcherCleanup 处理复杂清理逻辑
|
||||
|
||||
### Watchers
|
||||
|
||||
- [ ] watch/watchEffect 有适当的清理函数
|
||||
- [ ] 异步 watch 处理了竞态条件
|
||||
- [ ] flush: 'post' 用于 DOM 操作的 watcher
|
||||
@@ -904,12 +905,14 @@ const { list, containerProps, wrapperProps } = useVirtualList(
|
||||
- [ ] 考虑 once: true 用于一次性监听
|
||||
|
||||
### 模板
|
||||
|
||||
- [ ] v-for 使用唯一且稳定的 key
|
||||
- [ ] v-if 和 v-for 没有在同一元素上
|
||||
- [ ] 事件处理使用方法而非内联复杂逻辑
|
||||
- [ ] 大型列表使用虚拟滚动
|
||||
|
||||
### Composables
|
||||
|
||||
- [ ] 相关逻辑提取到 composables
|
||||
- [ ] composables 返回响应式引用(不是 .value)
|
||||
- [ ] 纯函数不要包装成 composable
|
||||
@@ -917,6 +920,7 @@ const { list, containerProps, wrapperProps } = useVirtualList(
|
||||
- [ ] 使用 effectScope 管理复杂副作用
|
||||
|
||||
### 性能
|
||||
|
||||
- [ ] 大型组件拆分为小组件
|
||||
- [ ] 使用 defineAsyncComponent 懒加载
|
||||
- [ ] 避免不必要的响应式转换
|
||||
|
||||
@@ -39,24 +39,28 @@ Before creating competitor pages, understand:
|
||||
## Core Principles
|
||||
|
||||
### 1. Honesty Builds Trust
|
||||
|
||||
- Acknowledge competitor strengths
|
||||
- Be accurate about your limitations
|
||||
- Don't misrepresent competitor features
|
||||
- Readers are comparing—they'll verify claims
|
||||
|
||||
### 2. Depth Over Surface
|
||||
|
||||
- Go beyond feature checklists
|
||||
- Explain *why* differences matter
|
||||
- Explain _why_ differences matter
|
||||
- Include use cases and scenarios
|
||||
- Show, don't just tell
|
||||
|
||||
### 3. Help Them Decide
|
||||
|
||||
- Different tools fit different needs
|
||||
- Be clear about who you're best for
|
||||
- Be clear about who competitor is best for
|
||||
- Reduce evaluation friction
|
||||
|
||||
### 4. Modular Content Architecture
|
||||
|
||||
- Competitor data should be centralized
|
||||
- Updates propagate to all pages
|
||||
- Single source of truth per competitor
|
||||
@@ -74,6 +78,7 @@ Before creating competitor pages, understand:
|
||||
**Target keywords**: "[Competitor] alternative", "alternative to [Competitor]", "switch from [Competitor]"
|
||||
|
||||
**Page structure**:
|
||||
|
||||
1. Why people look for alternatives (validate their pain)
|
||||
2. Summary: You as the alternative (quick positioning)
|
||||
3. Detailed comparison (features, service, pricing)
|
||||
@@ -93,6 +98,7 @@ Before creating competitor pages, understand:
|
||||
**Target keywords**: "[Competitor] alternatives", "best [Competitor] alternatives", "tools like [Competitor]"
|
||||
|
||||
**Page structure**:
|
||||
|
||||
1. Why people look for alternatives (common pain points)
|
||||
2. What to look for in an alternative (criteria framework)
|
||||
3. List of alternatives (you first, but include real options)
|
||||
@@ -114,6 +120,7 @@ Before creating competitor pages, understand:
|
||||
**Target keywords**: "[You] vs [Competitor]", "[Competitor] vs [You]"
|
||||
|
||||
**Page structure**:
|
||||
|
||||
1. TL;DR summary (key differences in 2-3 sentences)
|
||||
2. At-a-glance comparison table
|
||||
3. Detailed comparison by category (Features, Pricing, Support, Ease of use, Integrations)
|
||||
@@ -132,6 +139,7 @@ Before creating competitor pages, understand:
|
||||
**URL pattern**: `/compare/[competitor-a]-vs-[competitor-b]`
|
||||
|
||||
**Page structure**:
|
||||
|
||||
1. Overview of both products
|
||||
2. Comparison by category
|
||||
3. Who each is best for
|
||||
@@ -146,21 +154,27 @@ Before creating competitor pages, understand:
|
||||
## Essential Sections
|
||||
|
||||
### TL;DR Summary
|
||||
|
||||
Start every page with a quick summary for scanners—key differences in 2-3 sentences.
|
||||
|
||||
### Paragraph Comparisons
|
||||
|
||||
Go beyond tables. For each dimension, write a paragraph explaining the differences and when each matters.
|
||||
|
||||
### Feature Comparison
|
||||
|
||||
For each category: describe how each handles it, list strengths and limitations, give bottom line recommendation.
|
||||
|
||||
### Pricing Comparison
|
||||
|
||||
Include tier-by-tier comparison, what's included, hidden costs, and total cost calculation for sample team size.
|
||||
|
||||
### Who It's For
|
||||
|
||||
Be explicit about ideal customer for each option. Honest recommendations build trust.
|
||||
|
||||
### Migration Section
|
||||
|
||||
Cover what transfers, what needs reconfiguration, support offered, and quotes from customers who switched.
|
||||
|
||||
**For detailed templates**: See [references/templates.md](references/templates.md)
|
||||
@@ -170,7 +184,9 @@ Cover what transfers, what needs reconfiguration, support offered, and quotes fr
|
||||
## Content Architecture
|
||||
|
||||
### Centralized Competitor Data
|
||||
|
||||
Create a single source of truth for each competitor with:
|
||||
|
||||
- Positioning and target audience
|
||||
- Pricing (all tiers)
|
||||
- Feature ratings
|
||||
@@ -207,19 +223,21 @@ For each competitor, gather:
|
||||
|
||||
### Keyword Targeting
|
||||
|
||||
| Format | Primary Keywords |
|
||||
|--------|-----------------|
|
||||
| Alternative (singular) | [Competitor] alternative, alternative to [Competitor] |
|
||||
| Alternatives (plural) | [Competitor] alternatives, best [Competitor] alternatives |
|
||||
| You vs Competitor | [You] vs [Competitor], [Competitor] vs [You] |
|
||||
| Competitor vs Competitor | [A] vs [B], [B] vs [A] |
|
||||
| Format | Primary Keywords |
|
||||
| ------------------------ | --------------------------------------------------------- |
|
||||
| Alternative (singular) | [Competitor] alternative, alternative to [Competitor] |
|
||||
| Alternatives (plural) | [Competitor] alternatives, best [Competitor] alternatives |
|
||||
| You vs Competitor | [You] vs [Competitor], [Competitor] vs [You] |
|
||||
| Competitor vs Competitor | [A] vs [B], [B] vs [A] |
|
||||
|
||||
### Internal Linking
|
||||
|
||||
- Link between related competitor pages
|
||||
- Link from feature pages to relevant comparisons
|
||||
- Create hub page linking to all competitor content
|
||||
|
||||
### Schema Markup
|
||||
|
||||
Consider FAQ schema for common questions like "What is the best alternative to [Competitor]?"
|
||||
|
||||
---
|
||||
@@ -227,12 +245,15 @@ Consider FAQ schema for common questions like "What is the best alternative to [
|
||||
## Output Format
|
||||
|
||||
### Competitor Data File
|
||||
|
||||
Complete competitor profile in YAML format for use across all comparison pages.
|
||||
|
||||
### Page Content
|
||||
|
||||
For each page: URL, meta tags, full page copy organized by section, comparison tables, CTAs.
|
||||
|
||||
### Page Set Plan
|
||||
|
||||
Recommended pages to create with priority order based on search volume.
|
||||
|
||||
---
|
||||
|
||||
+21
-12
@@ -23,19 +23,19 @@ Per competitor, document:
|
||||
```yaml
|
||||
name: Notion
|
||||
website: notion.so
|
||||
tagline: "The all-in-one workspace"
|
||||
tagline: 'The all-in-one workspace'
|
||||
founded: 2016
|
||||
headquarters: San Francisco
|
||||
|
||||
# Positioning
|
||||
primary_use_case: "docs + light databases"
|
||||
target_audience: "teams wanting flexible workspace"
|
||||
market_position: "premium, feature-rich"
|
||||
primary_use_case: 'docs + light databases'
|
||||
target_audience: 'teams wanting flexible workspace'
|
||||
market_position: 'premium, feature-rich'
|
||||
|
||||
# Pricing
|
||||
pricing_model: per-seat
|
||||
free_tier: true
|
||||
free_tier_limits: "limited blocks, 1 user"
|
||||
free_tier_limits: 'limited blocks, 1 user'
|
||||
starter_price: $8/user/month
|
||||
business_price: $15/user/month
|
||||
enterprise: custom
|
||||
@@ -81,17 +81,17 @@ not_ideal_for:
|
||||
|
||||
# Common complaints (from reviews)
|
||||
common_complaints:
|
||||
- "Gets slow with lots of content"
|
||||
- "Hard to find things as workspace grows"
|
||||
- "Mobile app is clunky"
|
||||
- 'Gets slow with lots of content'
|
||||
- 'Hard to find things as workspace grows'
|
||||
- 'Mobile app is clunky'
|
||||
|
||||
# Migration notes
|
||||
migration_from:
|
||||
difficulty: medium
|
||||
data_export: "Markdown, CSV, HTML"
|
||||
what_transfers: "Pages, databases"
|
||||
what_doesnt: "Automations, integrations setup"
|
||||
time_estimate: "1-3 days for small team"
|
||||
data_export: 'Markdown, CSV, HTML'
|
||||
what_transfers: 'Pages, databases'
|
||||
what_doesnt: 'Automations, integrations setup'
|
||||
time_estimate: '1-3 days for small team'
|
||||
```
|
||||
|
||||
---
|
||||
@@ -129,6 +129,7 @@ Each page pulls from centralized data:
|
||||
- **[A] vs [B] page**: Pulls both competitor data + your data
|
||||
|
||||
**Benefits**:
|
||||
|
||||
- Update competitor pricing once, updates everywhere
|
||||
- Add new feature comparison once, appears on all pages
|
||||
- Consistent accuracy across pages
|
||||
@@ -145,6 +146,7 @@ Each page pulls from centralized data:
|
||||
**Purpose**: Lists all "[Competitor] Alternative" pages
|
||||
|
||||
**Page structure**:
|
||||
|
||||
1. Headline: "[Your Product] as an Alternative"
|
||||
2. Brief intro on why people switch to you
|
||||
3. List of all alternative pages with:
|
||||
@@ -155,6 +157,7 @@ Each page pulls from centralized data:
|
||||
5. CTA
|
||||
|
||||
**Example**:
|
||||
|
||||
```markdown
|
||||
## Explore [Your Product] as an Alternative
|
||||
|
||||
@@ -174,6 +177,7 @@ Looking to switch? See how [Your Product] compares to the tools you're evaluatin
|
||||
**Purpose**: Lists all "You vs [Competitor]" and "[A] vs [B]" pages
|
||||
|
||||
**Page structure**:
|
||||
|
||||
1. Headline: "Compare [Your Product]"
|
||||
2. Section: "[Your Product] vs Competitors" — list of direct comparisons
|
||||
3. Section: "Head-to-Head Comparisons" — list of [A] vs [B] pages
|
||||
@@ -187,22 +191,26 @@ Looking to switch? See how [Your Product] compares to the tools you're evaluatin
|
||||
**Keep them updated**: When you add a new comparison page, add it to the relevant index.
|
||||
|
||||
**Internal linking**:
|
||||
|
||||
- Link from index → individual pages
|
||||
- Link from individual pages → back to index
|
||||
- Cross-link between related comparisons
|
||||
|
||||
**SEO value**:
|
||||
|
||||
- Index pages can rank for broad terms like "project management tool comparisons"
|
||||
- Pass link equity to individual comparison pages
|
||||
- Help search engines discover all comparison content
|
||||
|
||||
**Sorting options**:
|
||||
|
||||
- By popularity (search volume)
|
||||
- Alphabetically
|
||||
- By category/use case
|
||||
- By date added (show freshness)
|
||||
|
||||
**Include on index pages**:
|
||||
|
||||
- Last updated date for credibility
|
||||
- Number of pages/comparisons available
|
||||
- Quick filters if you have many comparisons
|
||||
@@ -243,6 +251,7 @@ Footer
|
||||
```
|
||||
|
||||
**Guidelines**:
|
||||
|
||||
- Include up to 8 links per column (top competitors by search volume)
|
||||
- Add "View all" link to the full index page
|
||||
- Only create columns for formats you've actually built pages for
|
||||
|
||||
@@ -42,10 +42,12 @@ Go beyond checkmarks:
|
||||
### [Feature Category]
|
||||
|
||||
**[Competitor]**: [2-3 sentence description of how they handle this]
|
||||
|
||||
- Strengths: [specific]
|
||||
- Limitations: [specific]
|
||||
|
||||
**[Your product]**: [2-3 sentence description]
|
||||
|
||||
- Strengths: [specific]
|
||||
- Limitations: [specific]
|
||||
|
||||
@@ -59,12 +61,12 @@ Go beyond checkmarks:
|
||||
```markdown
|
||||
## Pricing
|
||||
|
||||
| | [Competitor] | [Your Product] |
|
||||
|---|---|---|
|
||||
| Free tier | [Details] | [Details] |
|
||||
| Starting price | $X/user/mo | $X/user/mo |
|
||||
| Business tier | $X/user/mo | $X/user/mo |
|
||||
| Enterprise | Custom | Custom |
|
||||
| | [Competitor] | [Your Product] |
|
||||
| -------------- | ------------ | -------------- |
|
||||
| Free tier | [Details] | [Details] |
|
||||
| Starting price | $X/user/mo | $X/user/mo |
|
||||
| Business tier | $X/user/mo | $X/user/mo |
|
||||
| Enterprise | Custom | Custom |
|
||||
|
||||
**What's included**: [Competitor]'s $X plan includes [features], while
|
||||
[Your product]'s $X plan includes [features].
|
||||
@@ -84,13 +86,13 @@ $X/year while [Your product] costs $Y/year, with [key differences in what you ge
|
||||
```markdown
|
||||
## Service & Support
|
||||
|
||||
| | [Competitor] | [Your Product] |
|
||||
|---|---|---|
|
||||
| Documentation | [Quality assessment] | [Quality assessment] |
|
||||
| Response time | [SLA if known] | [Your SLA] |
|
||||
| Support channels | [List] | [List] |
|
||||
| Onboarding | [What they offer] | [What you offer] |
|
||||
| CSM included | [At what tier] | [At what tier] |
|
||||
| | [Competitor] | [Your Product] |
|
||||
| ---------------- | -------------------- | -------------------- |
|
||||
| Documentation | [Quality assessment] | [Quality assessment] |
|
||||
| Response time | [SLA if known] | [Your SLA] |
|
||||
| Support channels | [List] | [List] |
|
||||
| Onboarding | [What they offer] | [What you offer] |
|
||||
| CSM included | [At what tier] | [At what tier] |
|
||||
|
||||
**Support quality**: Based on [G2/Capterra reviews, your research],
|
||||
[Competitor] support is described as [assessment]. Common feedback includes
|
||||
@@ -108,6 +110,7 @@ response time, dedicated CSM, implementation help].
|
||||
## Who Should Choose [Competitor]
|
||||
|
||||
[Competitor] is the right choice if:
|
||||
|
||||
- [Specific use case or need]
|
||||
- [Team type or size]
|
||||
- [Workflow or requirement]
|
||||
@@ -118,6 +121,7 @@ response time, dedicated CSM, implementation help].
|
||||
## Who Should Choose [Your Product]
|
||||
|
||||
[Your product] is built for teams who:
|
||||
|
||||
- [Specific use case or need]
|
||||
- [Team type or size]
|
||||
- [Workflow or requirement]
|
||||
@@ -134,16 +138,19 @@ response time, dedicated CSM, implementation help].
|
||||
## Switching from [Competitor]
|
||||
|
||||
### What transfers
|
||||
|
||||
- [Data type]: [How easily, any caveats]
|
||||
- [Data type]: [How easily, any caveats]
|
||||
|
||||
### What needs reconfiguration
|
||||
|
||||
- [Thing]: [Why and effort level]
|
||||
- [Thing]: [Why and effort level]
|
||||
|
||||
### Migration support
|
||||
|
||||
We offer [migration support details]:
|
||||
|
||||
- [Free data import tool / white-glove migration]
|
||||
- [Documentation / migration guide]
|
||||
- [Timeline expectation]
|
||||
@@ -173,6 +180,7 @@ Focus on switchers:
|
||||
> — [Name], [Role] at [Company]
|
||||
|
||||
### Results after switching
|
||||
|
||||
- [Company] saw [specific result]
|
||||
- [Company] reduced [metric] by [amount]
|
||||
```
|
||||
@@ -198,6 +206,7 @@ Do this:
|
||||
### Organize by Category
|
||||
|
||||
Group features into meaningful categories:
|
||||
|
||||
- Core functionality
|
||||
- Collaboration
|
||||
- Integrations
|
||||
@@ -206,7 +215,7 @@ Group features into meaningful categories:
|
||||
|
||||
### Include Ratings Where Useful
|
||||
|
||||
| Category | You | Competitor | Notes |
|
||||
|----------|-----|-----------|-------|
|
||||
| Ease of use | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | [Brief note] |
|
||||
| Feature depth | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | [Brief note] |
|
||||
| Category | You | Competitor | Notes |
|
||||
| ------------- | ---------- | ---------- | ------------ |
|
||||
| Ease of use | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | [Brief note] |
|
||||
| Feature depth | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | [Brief note] |
|
||||
|
||||
@@ -16,23 +16,27 @@ If `.mosaic/product-marketing-context.md` exists, read it before asking question
|
||||
Gather this context (ask if not provided):
|
||||
|
||||
### 1. Business Context
|
||||
|
||||
- What does the company do?
|
||||
- Who is the ideal customer?
|
||||
- What's the primary goal for content? (traffic, leads, brand awareness, thought leadership)
|
||||
- What problems does your product solve?
|
||||
|
||||
### 2. Customer Research
|
||||
|
||||
- What questions do customers ask before buying?
|
||||
- What objections come up in sales calls?
|
||||
- What topics appear repeatedly in support tickets?
|
||||
- What language do customers use to describe their problems?
|
||||
|
||||
### 3. Current State
|
||||
|
||||
- Do you have existing content? What's working?
|
||||
- What resources do you have? (writers, budget, time)
|
||||
- What content formats can you produce? (written, video, audio)
|
||||
|
||||
### 4. Competitive Landscape
|
||||
|
||||
- Who are your main competitors?
|
||||
- What content gaps exist in your market?
|
||||
|
||||
@@ -74,24 +78,28 @@ Every piece of content must be searchable, shareable, or both. Prioritize in tha
|
||||
|
||||
**Use-Case Content**
|
||||
Formula: [persona] + [use-case]. Targets long-tail keywords.
|
||||
|
||||
- "Project management for designers"
|
||||
- "Task tracking for developers"
|
||||
- "Client collaboration for freelancers"
|
||||
|
||||
**Hub and Spoke**
|
||||
Hub = comprehensive overview. Spokes = related subtopics.
|
||||
|
||||
```
|
||||
/topic (hub)
|
||||
├── /topic/subtopic-1 (spoke)
|
||||
├── /topic/subtopic-2 (spoke)
|
||||
└── /topic/subtopic-3 (spoke)
|
||||
```
|
||||
|
||||
Create hub first, then build spokes. Interlink strategically.
|
||||
|
||||
**Note:** Most content works fine under `/blog`. Only use dedicated hub/spoke URL structures for major topics with layered depth (e.g., Atlassian's `/agile` guide). For typical blog posts, `/blog/post-title` is sufficient.
|
||||
|
||||
**Template Libraries**
|
||||
High-intent keywords + product adoption.
|
||||
|
||||
- Target searches like "marketing plan template"
|
||||
- Provide immediate standalone value
|
||||
- Show how product enhances the template
|
||||
@@ -99,11 +107,13 @@ High-intent keywords + product adoption.
|
||||
### Shareable Content Types
|
||||
|
||||
**Thought Leadership**
|
||||
|
||||
- Articulate concepts everyone feels but hasn't named
|
||||
- Challenge conventional wisdom with evidence
|
||||
- Share vulnerable, honest experiences
|
||||
|
||||
**Data-Driven Content**
|
||||
|
||||
- Product data analysis (anonymized insights)
|
||||
- Public data analysis (uncover patterns)
|
||||
- Original research (run experiments, share results)
|
||||
@@ -155,6 +165,7 @@ Pillar Topic (Hub)
|
||||
### Pillar Criteria
|
||||
|
||||
Good pillars should:
|
||||
|
||||
- Align with your product/service
|
||||
- Match what your audience cares about
|
||||
- Have search volume and/or social interest
|
||||
@@ -167,33 +178,41 @@ Good pillars should:
|
||||
Map topics to the buyer's journey using proven keyword modifiers:
|
||||
|
||||
### Awareness Stage
|
||||
|
||||
Modifiers: "what is," "how to," "guide to," "introduction to"
|
||||
|
||||
Example: If customers ask about project management basics:
|
||||
|
||||
- "What is Agile Project Management"
|
||||
- "Guide to Sprint Planning"
|
||||
- "How to Run a Standup Meeting"
|
||||
|
||||
### Consideration Stage
|
||||
|
||||
Modifiers: "best," "top," "vs," "alternatives," "comparison"
|
||||
|
||||
Example: If customers evaluate multiple tools:
|
||||
|
||||
- "Best Project Management Tools for Remote Teams"
|
||||
- "Asana vs Trello vs Monday"
|
||||
- "Basecamp Alternatives"
|
||||
|
||||
### Decision Stage
|
||||
|
||||
Modifiers: "pricing," "reviews," "demo," "trial," "buy"
|
||||
|
||||
Example: If pricing comes up in sales calls:
|
||||
|
||||
- "Project Management Tool Pricing Comparison"
|
||||
- "How to Choose the Right Plan"
|
||||
- "[Product] Reviews"
|
||||
|
||||
### Implementation Stage
|
||||
|
||||
Modifiers: "templates," "examples," "tutorial," "how to use," "setup"
|
||||
|
||||
Example: If support tickets show implementation struggles:
|
||||
|
||||
- "Project Template Library"
|
||||
- "Step-by-Step Setup Tutorial"
|
||||
- "How to Use [Feature]"
|
||||
@@ -205,6 +224,7 @@ Example: If support tickets show implementation struggles:
|
||||
### 1. Keyword Data
|
||||
|
||||
If user provides keyword exports (Ahrefs, SEMrush, GSC), analyze for:
|
||||
|
||||
- Topic clusters (group related keywords)
|
||||
- Buyer stage (awareness/consideration/decision/implementation)
|
||||
- Search intent (informational, commercial, transactional)
|
||||
@@ -217,6 +237,7 @@ Output as prioritized table:
|
||||
### 2. Call Transcripts
|
||||
|
||||
If user provides sales or customer call transcripts, extract:
|
||||
|
||||
- Questions asked → FAQ content or blog posts
|
||||
- Pain points → problems in their own words
|
||||
- Objections → content to address proactively
|
||||
@@ -228,6 +249,7 @@ Output content ideas with supporting quotes.
|
||||
### 3. Survey Responses
|
||||
|
||||
If user provides survey data, mine for:
|
||||
|
||||
- Open-ended responses (topics and language)
|
||||
- Common themes (30%+ mention = high priority)
|
||||
- Resource requests (what they wish existed)
|
||||
@@ -238,11 +260,13 @@ If user provides survey data, mine for:
|
||||
Use web search to find content ideas:
|
||||
|
||||
**Reddit:** `site:reddit.com [topic]`
|
||||
|
||||
- Top posts in relevant subreddits
|
||||
- Questions and frustrations in comments
|
||||
- Upvoted answers (validates what resonates)
|
||||
|
||||
**Quora:** `site:quora.com [topic]`
|
||||
|
||||
- Most-followed questions
|
||||
- Highly upvoted answers
|
||||
|
||||
@@ -257,6 +281,7 @@ Use web search to analyze competitor content:
|
||||
**Find their content:** `site:competitor.com/blog`
|
||||
|
||||
**Analyze:**
|
||||
|
||||
- Top-performing posts (comments, shares)
|
||||
- Topics covered repeatedly
|
||||
- Gaps they haven't covered
|
||||
@@ -264,6 +289,7 @@ Use web search to analyze competitor content:
|
||||
- Content structure (pillars, categories, formats)
|
||||
|
||||
**Identify opportunities:**
|
||||
|
||||
- Topics you can cover better
|
||||
- Angles they're missing
|
||||
- Outdated content to improve on
|
||||
@@ -271,6 +297,7 @@ Use web search to analyze competitor content:
|
||||
### 6. Sales and Support Input
|
||||
|
||||
Extract from customer-facing teams:
|
||||
|
||||
- Common objections
|
||||
- Repeated questions
|
||||
- Support ticket patterns
|
||||
@@ -284,34 +311,38 @@ Extract from customer-facing teams:
|
||||
Score each idea on four factors:
|
||||
|
||||
### 1. Customer Impact (40%)
|
||||
|
||||
- How frequently did this topic come up in research?
|
||||
- What percentage of customers face this challenge?
|
||||
- How emotionally charged was this pain point?
|
||||
- What's the potential LTV of customers with this need?
|
||||
|
||||
### 2. Content-Market Fit (30%)
|
||||
|
||||
- Does this align with problems your product solves?
|
||||
- Can you offer unique insights from customer research?
|
||||
- Do you have customer stories to support this?
|
||||
- Will this naturally lead to product interest?
|
||||
|
||||
### 3. Search Potential (20%)
|
||||
|
||||
- What's the monthly search volume?
|
||||
- How competitive is this topic?
|
||||
- Are there related long-tail opportunities?
|
||||
- Is search interest growing or declining?
|
||||
|
||||
### 4. Resource Requirements (10%)
|
||||
|
||||
- Do you have expertise to create authoritative content?
|
||||
- What additional research is needed?
|
||||
- What assets (graphics, data, examples) will you need?
|
||||
|
||||
### Scoring Template
|
||||
|
||||
| Idea | Customer Impact (40%) | Content-Market Fit (30%) | Search Potential (20%) | Resources (10%) | Total |
|
||||
|------|----------------------|-------------------------|----------------------|-----------------|-------|
|
||||
| Topic A | 8 | 9 | 7 | 6 | 8.0 |
|
||||
| Topic B | 6 | 7 | 9 | 8 | 7.1 |
|
||||
| Idea | Customer Impact (40%) | Content-Market Fit (30%) | Search Potential (20%) | Resources (10%) | Total |
|
||||
| ------- | --------------------- | ------------------------ | ---------------------- | --------------- | ----- |
|
||||
| Topic A | 8 | 9 | 7 | 6 | 8.0 |
|
||||
| Topic B | 6 | 7 | 9 | 8 | 7.1 |
|
||||
|
||||
---
|
||||
|
||||
@@ -320,12 +351,15 @@ Score each idea on four factors:
|
||||
When creating a content strategy, provide:
|
||||
|
||||
### 1. Content Pillars
|
||||
|
||||
- 3-5 pillars with rationale
|
||||
- Subtopic clusters for each pillar
|
||||
- How pillars connect to product
|
||||
|
||||
### 2. Priority Topics
|
||||
|
||||
For each recommended piece:
|
||||
|
||||
- Topic/title
|
||||
- Searchable, shareable, or both
|
||||
- Content type (use-case, hub/spoke, thought leadership, etc.)
|
||||
@@ -333,6 +367,7 @@ For each recommended piece:
|
||||
- Why this topic (customer research backing)
|
||||
|
||||
### 3. Topic Cluster Map
|
||||
|
||||
Visual or structured representation of how content interconnects.
|
||||
|
||||
---
|
||||
|
||||
@@ -16,6 +16,7 @@ If `.mosaic/product-marketing-context.md` exists, read it before editing. Use br
|
||||
Good copy editing isn't about rewriting—it's about enhancing. Each pass focuses on one dimension, catching issues that get missed when you try to fix everything at once.
|
||||
|
||||
**Key principles:**
|
||||
|
||||
- Don't change the core message; focus on enhancing it
|
||||
- Multiple focused passes beat one unfocused review
|
||||
- Each edit should have a clear reason
|
||||
@@ -32,6 +33,7 @@ Edit copy through seven sequential passes, each focusing on one dimension. After
|
||||
**Focus:** Can the reader understand what you're saying?
|
||||
|
||||
**What to check:**
|
||||
|
||||
- Confusing sentence structures
|
||||
- Unclear pronoun references
|
||||
- Jargon or insider language
|
||||
@@ -39,12 +41,14 @@ Edit copy through seven sequential passes, each focusing on one dimension. After
|
||||
- Missing context
|
||||
|
||||
**Common clarity killers:**
|
||||
|
||||
- Sentences trying to say too much
|
||||
- Abstract language instead of concrete
|
||||
- Assuming reader knowledge they don't have
|
||||
- Burying the point in qualifications
|
||||
|
||||
**Process:**
|
||||
|
||||
1. Read through quickly, highlighting unclear parts
|
||||
2. Don't correct yet—just note problem areas
|
||||
3. After marking issues, recommend specific edits
|
||||
@@ -59,18 +63,21 @@ Edit copy through seven sequential passes, each focusing on one dimension. After
|
||||
**Focus:** Is the copy consistent in how it sounds?
|
||||
|
||||
**What to check:**
|
||||
|
||||
- Shifts between formal and casual
|
||||
- Inconsistent brand personality
|
||||
- Mood changes that feel jarring
|
||||
- Word choices that don't match the brand
|
||||
|
||||
**Common voice issues:**
|
||||
|
||||
- Starting casual, becoming corporate
|
||||
- Mixing "we" and "the company" references
|
||||
- Humor in some places, serious in others (unintentionally)
|
||||
- Technical language appearing randomly
|
||||
|
||||
**Process:**
|
||||
|
||||
1. Read aloud to hear inconsistencies
|
||||
2. Mark where tone shifts unexpectedly
|
||||
3. Recommend edits that smooth transitions
|
||||
@@ -85,6 +92,7 @@ Edit copy through seven sequential passes, each focusing on one dimension. After
|
||||
**Focus:** Does every claim answer "why should I care?"
|
||||
|
||||
**What to check:**
|
||||
|
||||
- Features without benefits
|
||||
- Claims without consequences
|
||||
- Statements that don't connect to reader's life
|
||||
@@ -94,16 +102,18 @@ Edit copy through seven sequential passes, each focusing on one dimension. After
|
||||
For every statement, ask "Okay, so what?" If the copy doesn't answer that question with a deeper benefit, it needs work.
|
||||
|
||||
❌ "Our platform uses AI-powered analytics"
|
||||
*So what?*
|
||||
_So what?_
|
||||
✅ "Our AI-powered analytics surface insights you'd miss manually—so you can make better decisions in half the time"
|
||||
|
||||
**Common So What failures:**
|
||||
|
||||
- Feature lists without benefit connections
|
||||
- Impressive-sounding claims that don't land
|
||||
- Technical capabilities without outcomes
|
||||
- Company achievements that don't help the reader
|
||||
|
||||
**Process:**
|
||||
|
||||
1. Read each claim and literally ask "so what?"
|
||||
2. Highlight claims missing the answer
|
||||
3. Add the benefit bridge or deeper meaning
|
||||
@@ -118,12 +128,14 @@ For every statement, ask "Okay, so what?" If the copy doesn't answer that questi
|
||||
**Focus:** Is every claim supported with evidence?
|
||||
|
||||
**What to check:**
|
||||
|
||||
- Unsubstantiated claims
|
||||
- Missing social proof
|
||||
- Assertions without backup
|
||||
- "Best" or "leading" without evidence
|
||||
|
||||
**Types of proof to look for:**
|
||||
|
||||
- Testimonials with names and specifics
|
||||
- Case study references
|
||||
- Statistics and data
|
||||
@@ -133,12 +145,14 @@ For every statement, ask "Okay, so what?" If the copy doesn't answer that questi
|
||||
- Review scores
|
||||
|
||||
**Common proof gaps:**
|
||||
|
||||
- "Trusted by thousands" (which thousands?)
|
||||
- "Industry-leading" (according to whom?)
|
||||
- "Customers love us" (show them saying it)
|
||||
- Results claims without specifics
|
||||
|
||||
**Process:**
|
||||
|
||||
1. Identify every claim that needs proof
|
||||
2. Check if proof exists nearby
|
||||
3. Flag unsupported assertions
|
||||
@@ -153,6 +167,7 @@ For every statement, ask "Okay, so what?" If the copy doesn't answer that questi
|
||||
**Focus:** Is the copy concrete enough to be compelling?
|
||||
|
||||
**What to check:**
|
||||
|
||||
- Vague language ("improve," "enhance," "optimize")
|
||||
- Generic statements that could apply to anyone
|
||||
- Round numbers that feel made up
|
||||
@@ -160,21 +175,23 @@ For every statement, ask "Okay, so what?" If the copy doesn't answer that questi
|
||||
|
||||
**Specificity upgrades:**
|
||||
|
||||
| Vague | Specific |
|
||||
|-------|----------|
|
||||
| Save time | Save 4 hours every week |
|
||||
| Many customers | 2,847 teams |
|
||||
| Fast results | Results in 14 days |
|
||||
| Vague | Specific |
|
||||
| --------------------- | ------------------------------- |
|
||||
| Save time | Save 4 hours every week |
|
||||
| Many customers | 2,847 teams |
|
||||
| Fast results | Results in 14 days |
|
||||
| Improve your workflow | Cut your reporting time in half |
|
||||
| Great support | Response within 2 hours |
|
||||
| Great support | Response within 2 hours |
|
||||
|
||||
**Common specificity issues:**
|
||||
|
||||
- Adjectives doing the work nouns should do
|
||||
- Benefits without quantification
|
||||
- Outcomes without timeframes
|
||||
- Claims without concrete examples
|
||||
|
||||
**Process:**
|
||||
|
||||
1. Highlight vague words and phrases
|
||||
2. Ask "Can this be more specific?"
|
||||
3. Add numbers, timeframes, or examples
|
||||
@@ -189,12 +206,14 @@ For every statement, ask "Okay, so what?" If the copy doesn't answer that questi
|
||||
**Focus:** Does the copy make the reader feel something?
|
||||
|
||||
**What to check:**
|
||||
|
||||
- Flat, informational language
|
||||
- Missing emotional triggers
|
||||
- Pain points mentioned but not felt
|
||||
- Aspirations stated but not evoked
|
||||
|
||||
**Emotional dimensions to consider:**
|
||||
|
||||
- Pain of the current state
|
||||
- Frustration with alternatives
|
||||
- Fear of missing out
|
||||
@@ -203,6 +222,7 @@ For every statement, ask "Okay, so what?" If the copy doesn't answer that questi
|
||||
- Relief from solving the problem
|
||||
|
||||
**Techniques for heightening emotion:**
|
||||
|
||||
- Paint the "before" state vividly
|
||||
- Use sensory language
|
||||
- Tell micro-stories
|
||||
@@ -210,6 +230,7 @@ For every statement, ask "Okay, so what?" If the copy doesn't answer that questi
|
||||
- Ask questions that prompt reflection
|
||||
|
||||
**Process:**
|
||||
|
||||
1. Read for emotional impact—does it move you?
|
||||
2. Identify flat sections that should resonate
|
||||
3. Add emotional texture while staying authentic
|
||||
@@ -224,6 +245,7 @@ For every statement, ask "Okay, so what?" If the copy doesn't answer that questi
|
||||
**Focus:** Have we removed every barrier to action?
|
||||
|
||||
**What to check:**
|
||||
|
||||
- Friction near CTAs
|
||||
- Unanswered objections
|
||||
- Missing trust signals
|
||||
@@ -231,6 +253,7 @@ For every statement, ask "Okay, so what?" If the copy doesn't answer that questi
|
||||
- Hidden costs or surprises
|
||||
|
||||
**Risk reducers to look for:**
|
||||
|
||||
- Money-back guarantees
|
||||
- Free trials
|
||||
- "No credit card required"
|
||||
@@ -240,12 +263,14 @@ For every statement, ask "Okay, so what?" If the copy doesn't answer that questi
|
||||
- Privacy assurances
|
||||
|
||||
**Common risk issues:**
|
||||
|
||||
- CTA asks for commitment without earning trust
|
||||
- Objections raised but not addressed
|
||||
- Fine print that creates doubt
|
||||
- Vague "Contact us" instead of clear next step
|
||||
|
||||
**Process:**
|
||||
|
||||
1. Focus on sections near CTAs
|
||||
2. List every reason someone might hesitate
|
||||
3. Check if the copy addresses each concern
|
||||
@@ -262,6 +287,7 @@ Use these for faster reviews when a full seven-sweep process isn't needed.
|
||||
### Word-Level Checks
|
||||
|
||||
**Cut these words:**
|
||||
|
||||
- Very, really, extremely, incredibly (weak intensifiers)
|
||||
- Just, actually, basically (filler)
|
||||
- In order to (use "to")
|
||||
@@ -270,18 +296,19 @@ Use these for faster reviews when a full seven-sweep process isn't needed.
|
||||
|
||||
**Replace these:**
|
||||
|
||||
| Weak | Strong |
|
||||
|------|--------|
|
||||
| Utilize | Use |
|
||||
| Implement | Set up |
|
||||
| Leverage | Use |
|
||||
| Facilitate | Help |
|
||||
| Innovative | New |
|
||||
| Robust | Strong |
|
||||
| Seamless | Smooth |
|
||||
| Weak | Strong |
|
||||
| ------------ | ---------- |
|
||||
| Utilize | Use |
|
||||
| Implement | Set up |
|
||||
| Leverage | Use |
|
||||
| Facilitate | Help |
|
||||
| Innovative | New |
|
||||
| Robust | Strong |
|
||||
| Seamless | Smooth |
|
||||
| Cutting-edge | New/Modern |
|
||||
|
||||
**Watch for:**
|
||||
|
||||
- Adverbs (usually unnecessary)
|
||||
- Passive voice (switch to active)
|
||||
- Nominalizations (verb → noun: "make a decision" → "decide")
|
||||
@@ -307,54 +334,63 @@ Use these for faster reviews when a full seven-sweep process isn't needed.
|
||||
## Copy Editing Checklist
|
||||
|
||||
### Before You Start
|
||||
|
||||
- [ ] Understand the goal of this copy
|
||||
- [ ] Know the target audience
|
||||
- [ ] Identify the desired action
|
||||
- [ ] Read through once without editing
|
||||
|
||||
### Clarity (Sweep 1)
|
||||
|
||||
- [ ] Every sentence is immediately understandable
|
||||
- [ ] No jargon without explanation
|
||||
- [ ] Pronouns have clear references
|
||||
- [ ] No sentences trying to do too much
|
||||
|
||||
### Voice & Tone (Sweep 2)
|
||||
|
||||
- [ ] Consistent formality level throughout
|
||||
- [ ] Brand personality maintained
|
||||
- [ ] No jarring shifts in mood
|
||||
- [ ] Reads well aloud
|
||||
|
||||
### So What (Sweep 3)
|
||||
|
||||
- [ ] Every feature connects to a benefit
|
||||
- [ ] Claims answer "why should I care?"
|
||||
- [ ] Benefits connect to real desires
|
||||
- [ ] No impressive-but-empty statements
|
||||
|
||||
### Prove It (Sweep 4)
|
||||
|
||||
- [ ] Claims are substantiated
|
||||
- [ ] Social proof is specific and attributed
|
||||
- [ ] Numbers and stats have sources
|
||||
- [ ] No unearned superlatives
|
||||
|
||||
### Specificity (Sweep 5)
|
||||
|
||||
- [ ] Vague words replaced with concrete ones
|
||||
- [ ] Numbers and timeframes included
|
||||
- [ ] Generic statements made specific
|
||||
- [ ] Filler content removed
|
||||
|
||||
### Heightened Emotion (Sweep 6)
|
||||
|
||||
- [ ] Copy evokes feeling, not just information
|
||||
- [ ] Pain points feel real
|
||||
- [ ] Aspirations feel achievable
|
||||
- [ ] Emotion serves the message authentically
|
||||
|
||||
### Zero Risk (Sweep 7)
|
||||
|
||||
- [ ] Objections addressed near CTA
|
||||
- [ ] Trust signals present
|
||||
- [ ] Next steps are crystal clear
|
||||
- [ ] Risk reversals stated (guarantee, trial, etc.)
|
||||
|
||||
### Final Checks
|
||||
|
||||
- [ ] No typos or grammatical errors
|
||||
- [ ] Consistent formatting
|
||||
- [ ] Links work (if applicable)
|
||||
@@ -365,34 +401,42 @@ Use these for faster reviews when a full seven-sweep process isn't needed.
|
||||
## Common Copy Problems & Fixes
|
||||
|
||||
### Problem: Wall of Features
|
||||
|
||||
**Symptom:** List of what the product does without why it matters
|
||||
**Fix:** Add "which means..." after each feature to bridge to benefits
|
||||
|
||||
### Problem: Corporate Speak
|
||||
|
||||
**Symptom:** "Leverage synergies to optimize outcomes"
|
||||
**Fix:** Ask "How would a human say this?" and use those words
|
||||
|
||||
### Problem: Weak Opening
|
||||
|
||||
**Symptom:** Starting with company history or vague statements
|
||||
**Fix:** Lead with the reader's problem or desired outcome
|
||||
|
||||
### Problem: Buried CTA
|
||||
|
||||
**Symptom:** The ask comes after too much buildup, or isn't clear
|
||||
**Fix:** Make the CTA obvious, early, and repeated
|
||||
|
||||
### Problem: No Proof
|
||||
|
||||
**Symptom:** "Customers love us" with no evidence
|
||||
**Fix:** Add specific testimonials, numbers, or case references
|
||||
|
||||
### Problem: Generic Claims
|
||||
|
||||
**Symptom:** "We help businesses grow"
|
||||
**Fix:** Specify who, how, and by how much
|
||||
|
||||
### Problem: Mixed Audiences
|
||||
|
||||
**Symptom:** Copy tries to speak to everyone, resonates with no one
|
||||
**Fix:** Pick one audience and write directly to them
|
||||
|
||||
### Problem: Feature Overload
|
||||
|
||||
**Symptom:** Listing every capability, overwhelming the reader
|
||||
**Fix:** Focus on 3-5 key benefits that matter most to the audience
|
||||
|
||||
@@ -438,9 +482,9 @@ This iterative process ensures each edit doesn't create new problems while respe
|
||||
|
||||
## When to Use Each Skill
|
||||
|
||||
| Task | Skill to Use |
|
||||
|------|--------------|
|
||||
| Writing new page copy from scratch | copywriting |
|
||||
| Task | Skill to Use |
|
||||
| ------------------------------------- | ------------------------- |
|
||||
| Writing new page copy from scratch | copywriting |
|
||||
| Reviewing and improving existing copy | copy-editing (this skill) |
|
||||
| Editing copy you just wrote | copy-editing (this skill) |
|
||||
| Structural or strategic page changes | page-cro |
|
||||
| Editing copy you just wrote | copy-editing (this skill) |
|
||||
| Structural or strategic page changes | page-cro |
|
||||
|
||||
+268
-268
@@ -8,344 +8,344 @@ Source: Plain English Campaign A-Z of Alternative Words (2001), Australian Gover
|
||||
|
||||
## A
|
||||
|
||||
| Complex | Plain Alternative |
|
||||
|---------|-------------------|
|
||||
| (an) absence of | no, none |
|
||||
| abundance | enough, plenty, many |
|
||||
| accede to | allow, agree to |
|
||||
| accelerate | speed up |
|
||||
| accommodate | meet, hold, house |
|
||||
| accomplish | do, finish, complete |
|
||||
| accordingly | so, therefore |
|
||||
| acknowledge | thank you for, confirm |
|
||||
| acquire | get, buy, obtain |
|
||||
| additional | extra, more |
|
||||
| adjacent | next to |
|
||||
| advantageous | useful, helpful |
|
||||
| advise | tell, say, inform |
|
||||
| aforesaid | this, earlier |
|
||||
| aggregate | total |
|
||||
| alleviate | ease, reduce |
|
||||
| allocate | give, share, assign |
|
||||
| alternative | other, choice |
|
||||
| ameliorate | improve |
|
||||
| anticipate | expect |
|
||||
| apparent | clear, obvious |
|
||||
| appreciable | large, noticeable |
|
||||
| appropriate | proper, right, suitable |
|
||||
| approximately | about, roughly |
|
||||
| ascertain | find out |
|
||||
| assistance | help |
|
||||
| at the present time | now |
|
||||
| attempt | try |
|
||||
| authorise | allow, let |
|
||||
| Complex | Plain Alternative |
|
||||
| ------------------- | ----------------------- |
|
||||
| (an) absence of | no, none |
|
||||
| abundance | enough, plenty, many |
|
||||
| accede to | allow, agree to |
|
||||
| accelerate | speed up |
|
||||
| accommodate | meet, hold, house |
|
||||
| accomplish | do, finish, complete |
|
||||
| accordingly | so, therefore |
|
||||
| acknowledge | thank you for, confirm |
|
||||
| acquire | get, buy, obtain |
|
||||
| additional | extra, more |
|
||||
| adjacent | next to |
|
||||
| advantageous | useful, helpful |
|
||||
| advise | tell, say, inform |
|
||||
| aforesaid | this, earlier |
|
||||
| aggregate | total |
|
||||
| alleviate | ease, reduce |
|
||||
| allocate | give, share, assign |
|
||||
| alternative | other, choice |
|
||||
| ameliorate | improve |
|
||||
| anticipate | expect |
|
||||
| apparent | clear, obvious |
|
||||
| appreciable | large, noticeable |
|
||||
| appropriate | proper, right, suitable |
|
||||
| approximately | about, roughly |
|
||||
| ascertain | find out |
|
||||
| assistance | help |
|
||||
| at the present time | now |
|
||||
| attempt | try |
|
||||
| authorise | allow, let |
|
||||
|
||||
---
|
||||
|
||||
## B
|
||||
|
||||
| Complex | Plain Alternative |
|
||||
|---------|-------------------|
|
||||
| belated | late |
|
||||
| beneficial | helpful, useful |
|
||||
| bestow | give |
|
||||
| by means of | by |
|
||||
| Complex | Plain Alternative |
|
||||
| ----------- | ----------------- |
|
||||
| belated | late |
|
||||
| beneficial | helpful, useful |
|
||||
| bestow | give |
|
||||
| by means of | by |
|
||||
|
||||
---
|
||||
|
||||
## C
|
||||
|
||||
| Complex | Plain Alternative |
|
||||
|---------|-------------------|
|
||||
| calculate | work out |
|
||||
| cease | stop, end |
|
||||
| circumvent | avoid, get around |
|
||||
| clarification | explanation |
|
||||
| commence | start, begin |
|
||||
| communicate | tell, talk, write |
|
||||
| competent | able |
|
||||
| compile | collect, make |
|
||||
| complete | fill in, finish |
|
||||
| component | part |
|
||||
| comprise | include, make up |
|
||||
| (it is) compulsory | (you) must |
|
||||
| conceal | hide |
|
||||
| concerning | about |
|
||||
| consequently | so |
|
||||
| considerable | large, great, much |
|
||||
| constitute | make up, form |
|
||||
| consult | ask, talk to |
|
||||
| consumption | use |
|
||||
| currently | now |
|
||||
| Complex | Plain Alternative |
|
||||
| ------------------ | ------------------ |
|
||||
| calculate | work out |
|
||||
| cease | stop, end |
|
||||
| circumvent | avoid, get around |
|
||||
| clarification | explanation |
|
||||
| commence | start, begin |
|
||||
| communicate | tell, talk, write |
|
||||
| competent | able |
|
||||
| compile | collect, make |
|
||||
| complete | fill in, finish |
|
||||
| component | part |
|
||||
| comprise | include, make up |
|
||||
| (it is) compulsory | (you) must |
|
||||
| conceal | hide |
|
||||
| concerning | about |
|
||||
| consequently | so |
|
||||
| considerable | large, great, much |
|
||||
| constitute | make up, form |
|
||||
| consult | ask, talk to |
|
||||
| consumption | use |
|
||||
| currently | now |
|
||||
|
||||
---
|
||||
|
||||
## D
|
||||
|
||||
| Complex | Plain Alternative |
|
||||
|---------|-------------------|
|
||||
| deduct | take off |
|
||||
| deem | treat as, consider |
|
||||
| defer | delay, put off |
|
||||
| deficiency | lack |
|
||||
| delete | remove, cross out |
|
||||
| demonstrate | show, prove |
|
||||
| denote | show, mean |
|
||||
| designate | name, appoint |
|
||||
| despatch/dispatch | send |
|
||||
| determine | decide, find out |
|
||||
| detrimental | harmful |
|
||||
| diminish | reduce, lessen |
|
||||
| discontinue | stop |
|
||||
| disseminate | spread, distribute |
|
||||
| documentation | papers, documents |
|
||||
| due to the fact that | because |
|
||||
| duration | time, length |
|
||||
| dwelling | home |
|
||||
| Complex | Plain Alternative |
|
||||
| -------------------- | ------------------ |
|
||||
| deduct | take off |
|
||||
| deem | treat as, consider |
|
||||
| defer | delay, put off |
|
||||
| deficiency | lack |
|
||||
| delete | remove, cross out |
|
||||
| demonstrate | show, prove |
|
||||
| denote | show, mean |
|
||||
| designate | name, appoint |
|
||||
| despatch/dispatch | send |
|
||||
| determine | decide, find out |
|
||||
| detrimental | harmful |
|
||||
| diminish | reduce, lessen |
|
||||
| discontinue | stop |
|
||||
| disseminate | spread, distribute |
|
||||
| documentation | papers, documents |
|
||||
| due to the fact that | because |
|
||||
| duration | time, length |
|
||||
| dwelling | home |
|
||||
|
||||
---
|
||||
|
||||
## E
|
||||
|
||||
| Complex | Plain Alternative |
|
||||
|---------|-------------------|
|
||||
| economical | cheap, good value |
|
||||
| eligible | allowed, qualified |
|
||||
| elucidate | explain |
|
||||
| enable | allow |
|
||||
| encounter | meet |
|
||||
| endeavour | try |
|
||||
| enquire | ask |
|
||||
| ensure | make sure |
|
||||
| entitlement | right |
|
||||
| envisage | expect |
|
||||
| equivalent | equal, the same |
|
||||
| erroneous | wrong |
|
||||
| establish | set up, show |
|
||||
| evaluate | assess, test |
|
||||
| excessive | too much |
|
||||
| exclusively | only |
|
||||
| exempt | free from |
|
||||
| expedite | speed up |
|
||||
| expenditure | spending |
|
||||
| expire | run out |
|
||||
| Complex | Plain Alternative |
|
||||
| ----------- | ------------------ |
|
||||
| economical | cheap, good value |
|
||||
| eligible | allowed, qualified |
|
||||
| elucidate | explain |
|
||||
| enable | allow |
|
||||
| encounter | meet |
|
||||
| endeavour | try |
|
||||
| enquire | ask |
|
||||
| ensure | make sure |
|
||||
| entitlement | right |
|
||||
| envisage | expect |
|
||||
| equivalent | equal, the same |
|
||||
| erroneous | wrong |
|
||||
| establish | set up, show |
|
||||
| evaluate | assess, test |
|
||||
| excessive | too much |
|
||||
| exclusively | only |
|
||||
| exempt | free from |
|
||||
| expedite | speed up |
|
||||
| expenditure | spending |
|
||||
| expire | run out |
|
||||
|
||||
---
|
||||
|
||||
## F
|
||||
|
||||
| Complex | Plain Alternative |
|
||||
|---------|-------------------|
|
||||
| fabricate | make |
|
||||
| facilitate | help, make possible |
|
||||
| finalise | finish, complete |
|
||||
| following | after |
|
||||
| for the purpose of | to, for |
|
||||
| for the reason that | because |
|
||||
| forthwith | now, at once |
|
||||
| forward | send |
|
||||
| frequently | often |
|
||||
| furnish | give, provide |
|
||||
| furthermore | also, and |
|
||||
| Complex | Plain Alternative |
|
||||
| ------------------- | ------------------- |
|
||||
| fabricate | make |
|
||||
| facilitate | help, make possible |
|
||||
| finalise | finish, complete |
|
||||
| following | after |
|
||||
| for the purpose of | to, for |
|
||||
| for the reason that | because |
|
||||
| forthwith | now, at once |
|
||||
| forward | send |
|
||||
| frequently | often |
|
||||
| furnish | give, provide |
|
||||
| furthermore | also, and |
|
||||
|
||||
---
|
||||
|
||||
## G-H
|
||||
|
||||
| Complex | Plain Alternative |
|
||||
|---------|-------------------|
|
||||
| generate | produce, create |
|
||||
| henceforth | from now on |
|
||||
| hitherto | until now |
|
||||
| Complex | Plain Alternative |
|
||||
| ---------- | ----------------- |
|
||||
| generate | produce, create |
|
||||
| henceforth | from now on |
|
||||
| hitherto | until now |
|
||||
|
||||
---
|
||||
|
||||
## I
|
||||
|
||||
| Complex | Plain Alternative |
|
||||
|---------|-------------------|
|
||||
| if and when | if, when |
|
||||
| illustrate | show |
|
||||
| immediately | at once, now |
|
||||
| implement | carry out, do |
|
||||
| imply | suggest |
|
||||
| in accordance with | under, following |
|
||||
| in addition to | and, also |
|
||||
| in conjunction with | with |
|
||||
| in excess of | more than |
|
||||
| in lieu of | instead of |
|
||||
| in order to | to |
|
||||
| in receipt of | receive |
|
||||
| in relation to | about |
|
||||
| in respect of | about, for |
|
||||
| in the event of | if |
|
||||
| in the majority of instances | most, usually |
|
||||
| in the near future | soon |
|
||||
| in view of the fact that | because |
|
||||
| inception | start |
|
||||
| indicate | show, suggest |
|
||||
| inform | tell |
|
||||
| initiate | start, begin |
|
||||
| insert | put in |
|
||||
| instances | cases |
|
||||
| irrespective of | despite |
|
||||
| issue | give, send |
|
||||
| Complex | Plain Alternative |
|
||||
| ---------------------------- | ----------------- |
|
||||
| if and when | if, when |
|
||||
| illustrate | show |
|
||||
| immediately | at once, now |
|
||||
| implement | carry out, do |
|
||||
| imply | suggest |
|
||||
| in accordance with | under, following |
|
||||
| in addition to | and, also |
|
||||
| in conjunction with | with |
|
||||
| in excess of | more than |
|
||||
| in lieu of | instead of |
|
||||
| in order to | to |
|
||||
| in receipt of | receive |
|
||||
| in relation to | about |
|
||||
| in respect of | about, for |
|
||||
| in the event of | if |
|
||||
| in the majority of instances | most, usually |
|
||||
| in the near future | soon |
|
||||
| in view of the fact that | because |
|
||||
| inception | start |
|
||||
| indicate | show, suggest |
|
||||
| inform | tell |
|
||||
| initiate | start, begin |
|
||||
| insert | put in |
|
||||
| instances | cases |
|
||||
| irrespective of | despite |
|
||||
| issue | give, send |
|
||||
|
||||
---
|
||||
|
||||
## L-M
|
||||
|
||||
| Complex | Plain Alternative |
|
||||
|---------|-------------------|
|
||||
| (a) large number of | many |
|
||||
| liaise with | work with, talk to |
|
||||
| locality | place, area |
|
||||
| locate | find |
|
||||
| magnitude | size |
|
||||
| (it is) mandatory | (you) must |
|
||||
| manner | way |
|
||||
| modification | change |
|
||||
| moreover | also, and |
|
||||
| Complex | Plain Alternative |
|
||||
| ------------------- | ------------------ |
|
||||
| (a) large number of | many |
|
||||
| liaise with | work with, talk to |
|
||||
| locality | place, area |
|
||||
| locate | find |
|
||||
| magnitude | size |
|
||||
| (it is) mandatory | (you) must |
|
||||
| manner | way |
|
||||
| modification | change |
|
||||
| moreover | also, and |
|
||||
|
||||
---
|
||||
|
||||
## N-O
|
||||
|
||||
| Complex | Plain Alternative |
|
||||
|---------|-------------------|
|
||||
| negligible | small |
|
||||
| nevertheless | but, however |
|
||||
| notify | tell |
|
||||
| notwithstanding | despite, even if |
|
||||
| numerous | many |
|
||||
| objective | aim, goal |
|
||||
| (it is) obligatory | (you) must |
|
||||
| obtain | get |
|
||||
| occasioned by | caused by |
|
||||
| on behalf of | for |
|
||||
| on numerous occasions | often |
|
||||
| on receipt of | when you get |
|
||||
| on the grounds that | because |
|
||||
| operate | work, run |
|
||||
| optimum | best |
|
||||
| option | choice |
|
||||
| otherwise | or |
|
||||
| outstanding | unpaid |
|
||||
| owing to | because |
|
||||
| Complex | Plain Alternative |
|
||||
| --------------------- | ----------------- |
|
||||
| negligible | small |
|
||||
| nevertheless | but, however |
|
||||
| notify | tell |
|
||||
| notwithstanding | despite, even if |
|
||||
| numerous | many |
|
||||
| objective | aim, goal |
|
||||
| (it is) obligatory | (you) must |
|
||||
| obtain | get |
|
||||
| occasioned by | caused by |
|
||||
| on behalf of | for |
|
||||
| on numerous occasions | often |
|
||||
| on receipt of | when you get |
|
||||
| on the grounds that | because |
|
||||
| operate | work, run |
|
||||
| optimum | best |
|
||||
| option | choice |
|
||||
| otherwise | or |
|
||||
| outstanding | unpaid |
|
||||
| owing to | because |
|
||||
|
||||
---
|
||||
|
||||
## P
|
||||
|
||||
| Complex | Plain Alternative |
|
||||
|---------|-------------------|
|
||||
| partially | partly |
|
||||
| participate | take part |
|
||||
| particulars | details |
|
||||
| per annum | a year |
|
||||
| perform | do |
|
||||
| permit | let, allow |
|
||||
| personnel | staff, people |
|
||||
| peruse | read |
|
||||
| possess | have, own |
|
||||
| practically | almost |
|
||||
| predominant | main |
|
||||
| prescribe | set |
|
||||
| preserve | keep |
|
||||
| previous | earlier, before |
|
||||
| principal | main |
|
||||
| prior to | before |
|
||||
| proceed | go ahead |
|
||||
| procure | get |
|
||||
| prohibit | ban, stop |
|
||||
| promptly | quickly |
|
||||
| provide | give |
|
||||
| provided that | if |
|
||||
| provisions | rules, terms |
|
||||
| proximity | nearness |
|
||||
| purchase | buy |
|
||||
| pursuant to | under |
|
||||
| Complex | Plain Alternative |
|
||||
| ------------- | ----------------- |
|
||||
| partially | partly |
|
||||
| participate | take part |
|
||||
| particulars | details |
|
||||
| per annum | a year |
|
||||
| perform | do |
|
||||
| permit | let, allow |
|
||||
| personnel | staff, people |
|
||||
| peruse | read |
|
||||
| possess | have, own |
|
||||
| practically | almost |
|
||||
| predominant | main |
|
||||
| prescribe | set |
|
||||
| preserve | keep |
|
||||
| previous | earlier, before |
|
||||
| principal | main |
|
||||
| prior to | before |
|
||||
| proceed | go ahead |
|
||||
| procure | get |
|
||||
| prohibit | ban, stop |
|
||||
| promptly | quickly |
|
||||
| provide | give |
|
||||
| provided that | if |
|
||||
| provisions | rules, terms |
|
||||
| proximity | nearness |
|
||||
| purchase | buy |
|
||||
| pursuant to | under |
|
||||
|
||||
---
|
||||
|
||||
## R
|
||||
|
||||
| Complex | Plain Alternative |
|
||||
|---------|-------------------|
|
||||
| reconsider | think again |
|
||||
| reduction | cut |
|
||||
| referred to as | called |
|
||||
| regarding | about |
|
||||
| reimburse | repay |
|
||||
| reiterate | repeat |
|
||||
| relating to | about |
|
||||
| remain | stay |
|
||||
| remainder | rest |
|
||||
| remuneration | pay |
|
||||
| render | make, give |
|
||||
| represent | stand for |
|
||||
| request | ask |
|
||||
| require | need |
|
||||
| residence | home |
|
||||
| retain | keep |
|
||||
| revised | changed, new |
|
||||
| Complex | Plain Alternative |
|
||||
| -------------- | ----------------- |
|
||||
| reconsider | think again |
|
||||
| reduction | cut |
|
||||
| referred to as | called |
|
||||
| regarding | about |
|
||||
| reimburse | repay |
|
||||
| reiterate | repeat |
|
||||
| relating to | about |
|
||||
| remain | stay |
|
||||
| remainder | rest |
|
||||
| remuneration | pay |
|
||||
| render | make, give |
|
||||
| represent | stand for |
|
||||
| request | ask |
|
||||
| require | need |
|
||||
| residence | home |
|
||||
| retain | keep |
|
||||
| revised | changed, new |
|
||||
|
||||
---
|
||||
|
||||
## S
|
||||
|
||||
| Complex | Plain Alternative |
|
||||
|---------|-------------------|
|
||||
| scrutinise | examine, check |
|
||||
| select | choose |
|
||||
| solely | only |
|
||||
| specified | given, stated |
|
||||
| state | say |
|
||||
| statutory | legal, by law |
|
||||
| subject to | depending on |
|
||||
| submit | send, give |
|
||||
| subsequent to | after |
|
||||
| subsequently | later |
|
||||
| substantial | large, much |
|
||||
| sufficient | enough |
|
||||
| supplement | add to |
|
||||
| supplementary | extra |
|
||||
| Complex | Plain Alternative |
|
||||
| ------------- | ----------------- |
|
||||
| scrutinise | examine, check |
|
||||
| select | choose |
|
||||
| solely | only |
|
||||
| specified | given, stated |
|
||||
| state | say |
|
||||
| statutory | legal, by law |
|
||||
| subject to | depending on |
|
||||
| submit | send, give |
|
||||
| subsequent to | after |
|
||||
| subsequently | later |
|
||||
| substantial | large, much |
|
||||
| sufficient | enough |
|
||||
| supplement | add to |
|
||||
| supplementary | extra |
|
||||
|
||||
---
|
||||
|
||||
## T-U
|
||||
|
||||
| Complex | Plain Alternative |
|
||||
|---------|-------------------|
|
||||
| terminate | end, stop |
|
||||
| thereafter | then |
|
||||
| thereby | by this |
|
||||
| thus | so |
|
||||
| to date | so far |
|
||||
| transfer | move |
|
||||
| transmit | send |
|
||||
| ultimately | in the end |
|
||||
| undertake | agree, do |
|
||||
| uniform | same |
|
||||
| utilise | use |
|
||||
| Complex | Plain Alternative |
|
||||
| ---------- | ----------------- |
|
||||
| terminate | end, stop |
|
||||
| thereafter | then |
|
||||
| thereby | by this |
|
||||
| thus | so |
|
||||
| to date | so far |
|
||||
| transfer | move |
|
||||
| transmit | send |
|
||||
| ultimately | in the end |
|
||||
| undertake | agree, do |
|
||||
| uniform | same |
|
||||
| utilise | use |
|
||||
|
||||
---
|
||||
|
||||
## V-Z
|
||||
|
||||
| Complex | Plain Alternative |
|
||||
|---------|-------------------|
|
||||
| variation | change |
|
||||
| virtually | almost |
|
||||
| visualise | imagine, see |
|
||||
| ways and means | ways |
|
||||
| whatsoever | any |
|
||||
| with a view to | to |
|
||||
| with effect from | from |
|
||||
| with reference to | about |
|
||||
| with regard to | about |
|
||||
| with respect to | about |
|
||||
| zone | area |
|
||||
| Complex | Plain Alternative |
|
||||
| ----------------- | ----------------- |
|
||||
| variation | change |
|
||||
| virtually | almost |
|
||||
| visualise | imagine, see |
|
||||
| ways and means | ways |
|
||||
| whatsoever | any |
|
||||
| with a view to | to |
|
||||
| with effect from | from |
|
||||
| with reference to | about |
|
||||
| with regard to | about |
|
||||
| with respect to | about |
|
||||
| zone | area |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -16,22 +16,26 @@ If `.mosaic/product-marketing-context.md` exists, read it before asking question
|
||||
Gather this context (ask if not provided):
|
||||
|
||||
### 1. Page Purpose
|
||||
|
||||
- What type of page? (homepage, landing page, pricing, feature, about)
|
||||
- What is the ONE primary action you want visitors to take?
|
||||
|
||||
### 2. Audience
|
||||
|
||||
- Who is the ideal customer?
|
||||
- What problem are they trying to solve?
|
||||
- What objections or hesitations do they have?
|
||||
- What language do they use to describe their problem?
|
||||
|
||||
### 3. Product/Offer
|
||||
|
||||
- What are you selling or offering?
|
||||
- What makes it different from alternatives?
|
||||
- What's the key transformation or outcome?
|
||||
- Any proof points (numbers, testimonials, case studies)?
|
||||
|
||||
### 4. Context
|
||||
|
||||
- Where is traffic coming from? (ads, organic, email)
|
||||
- What do visitors already know before arriving?
|
||||
|
||||
@@ -40,19 +44,24 @@ Gather this context (ask if not provided):
|
||||
## Copywriting Principles
|
||||
|
||||
### Clarity Over Cleverness
|
||||
|
||||
If you have to choose between clear and creative, choose clear.
|
||||
|
||||
### Benefits Over Features
|
||||
|
||||
Features: What it does. Benefits: What that means for the customer.
|
||||
|
||||
### Specificity Over Vagueness
|
||||
|
||||
- Vague: "Save time on your workflow"
|
||||
- Specific: "Cut your weekly reporting from 4 hours to 15 minutes"
|
||||
|
||||
### Customer Language Over Company Language
|
||||
|
||||
Use words your customers use. Mirror voice-of-customer from reviews, interviews, support tickets.
|
||||
|
||||
### One Idea Per Section
|
||||
|
||||
Each section should advance one argument. Build a logical flow down the page.
|
||||
|
||||
---
|
||||
@@ -83,6 +92,7 @@ For thorough line-by-line review, use the **copy-editing** skill after your draf
|
||||
## Best Practices
|
||||
|
||||
### Be Direct
|
||||
|
||||
Get to the point. Don't bury the value in qualifications.
|
||||
|
||||
❌ Slack lets you share files instantly, from documents to images, directly in your conversations
|
||||
@@ -90,14 +100,18 @@ Get to the point. Don't bury the value in qualifications.
|
||||
✅ Need to share a screenshot? Send as many documents, images, and audio files as your heart desires.
|
||||
|
||||
### Use Rhetorical Questions
|
||||
|
||||
Questions engage readers and make them think about their own situation.
|
||||
|
||||
- "Hate returning stuff to Amazon?"
|
||||
- "Tired of chasing approvals?"
|
||||
|
||||
### Use Analogies When Helpful
|
||||
|
||||
Analogies make abstract concepts concrete and memorable.
|
||||
|
||||
### Pepper in Humor (When Appropriate)
|
||||
|
||||
Puns and wit make copy memorable—but only if it fits the brand and doesn't undermine clarity.
|
||||
|
||||
---
|
||||
@@ -107,11 +121,13 @@ Puns and wit make copy memorable—but only if it fits the brand and doesn't und
|
||||
### Above the Fold
|
||||
|
||||
**Headline**
|
||||
|
||||
- Your single most important message
|
||||
- Communicate core value proposition
|
||||
- Specific > generic
|
||||
|
||||
**Example formulas:**
|
||||
|
||||
- "{Achieve outcome} without {pain point}"
|
||||
- "The {category} for {audience}"
|
||||
- "Never {unpleasant event} again"
|
||||
@@ -122,24 +138,26 @@ Puns and wit make copy memorable—but only if it fits the brand and doesn't und
|
||||
**For natural transition phrases**: See [references/natural-transitions.md](references/natural-transitions.md)
|
||||
|
||||
**Subheadline**
|
||||
|
||||
- Expands on headline
|
||||
- Adds specificity
|
||||
- 1-2 sentences max
|
||||
|
||||
**Primary CTA**
|
||||
|
||||
- Action-oriented button text
|
||||
- Communicate what they get: "Start Free Trial" > "Sign Up"
|
||||
|
||||
### Core Sections
|
||||
|
||||
| Section | Purpose |
|
||||
|---------|---------|
|
||||
| Social Proof | Build credibility (logos, stats, testimonials) |
|
||||
| Problem/Pain | Show you understand their situation |
|
||||
| Solution/Benefits | Connect to outcomes (3-5 key benefits) |
|
||||
| How It Works | Reduce perceived complexity (3-4 steps) |
|
||||
| Objection Handling | FAQ, comparisons, guarantees |
|
||||
| Final CTA | Recap value, repeat CTA, risk reversal |
|
||||
| Section | Purpose |
|
||||
| ------------------ | ---------------------------------------------- |
|
||||
| Social Proof | Build credibility (logos, stats, testimonials) |
|
||||
| Problem/Pain | Show you understand their situation |
|
||||
| Solution/Benefits | Connect to outcomes (3-5 key benefits) |
|
||||
| How It Works | Reduce perceived complexity (3-4 steps) |
|
||||
| Objection Handling | FAQ, comparisons, guarantees |
|
||||
| Final CTA | Recap value, repeat CTA, risk reversal |
|
||||
|
||||
**For detailed section types and page templates**: See [references/copy-frameworks.md](references/copy-frameworks.md)
|
||||
|
||||
@@ -148,9 +166,11 @@ Puns and wit make copy memorable—but only if it fits the brand and doesn't und
|
||||
## CTA Copy Guidelines
|
||||
|
||||
**Weak CTAs (avoid):**
|
||||
|
||||
- Submit, Sign Up, Learn More, Click Here, Get Started
|
||||
|
||||
**Strong CTAs (use):**
|
||||
|
||||
- Start Free Trial
|
||||
- Get [Specific Thing]
|
||||
- See [Product] in Action
|
||||
@@ -160,6 +180,7 @@ Puns and wit make copy memorable—but only if it fits the brand and doesn't und
|
||||
**Formula:** [Action Verb] + [What They Get] + [Qualifier if needed]
|
||||
|
||||
Examples:
|
||||
|
||||
- "Start My Free Trial"
|
||||
- "Get the Complete Checklist"
|
||||
- "See Pricing for My Team"
|
||||
@@ -169,26 +190,31 @@ Examples:
|
||||
## Page-Specific Guidance
|
||||
|
||||
### Homepage
|
||||
|
||||
- Serve multiple audiences without being generic
|
||||
- Lead with broadest value proposition
|
||||
- Provide clear paths for different visitor intents
|
||||
|
||||
### Landing Page
|
||||
|
||||
- Single message, single CTA
|
||||
- Match headline to ad/traffic source
|
||||
- Complete argument on one page
|
||||
|
||||
### Pricing Page
|
||||
|
||||
- Help visitors choose the right plan
|
||||
- Address "which is right for me?" anxiety
|
||||
- Make recommended plan obvious
|
||||
|
||||
### Feature Page
|
||||
|
||||
- Connect feature → benefit → outcome
|
||||
- Show use cases and examples
|
||||
- Clear path to try or buy
|
||||
|
||||
### About Page
|
||||
|
||||
- Tell the story of why you exist
|
||||
- Connect mission to customer benefit
|
||||
- Still include a CTA
|
||||
@@ -200,16 +226,19 @@ Examples:
|
||||
Before writing, establish:
|
||||
|
||||
**Formality level:**
|
||||
|
||||
- Casual/conversational
|
||||
- Professional but friendly
|
||||
- Formal/enterprise
|
||||
|
||||
**Brand personality:**
|
||||
|
||||
- Playful or serious?
|
||||
- Bold or understated?
|
||||
- Technical or accessible?
|
||||
|
||||
Maintain consistency, but adjust intensity:
|
||||
|
||||
- Headlines can be bolder
|
||||
- Body copy should be clearer
|
||||
- CTAs should be action-oriented
|
||||
@@ -221,22 +250,29 @@ Maintain consistency, but adjust intensity:
|
||||
When writing copy, provide:
|
||||
|
||||
### Page Copy
|
||||
|
||||
Organized by section:
|
||||
|
||||
- Headline, Subheadline, CTA
|
||||
- Section headers and body copy
|
||||
- Secondary CTAs
|
||||
|
||||
### Annotations
|
||||
|
||||
For key elements, explain:
|
||||
|
||||
- Why you made this choice
|
||||
- What principle it applies
|
||||
|
||||
### Alternatives
|
||||
|
||||
For headlines and CTAs, provide 2-3 options:
|
||||
|
||||
- Option A: [copy] — [rationale]
|
||||
- Option B: [copy] — [rationale]
|
||||
|
||||
### Meta Content (if relevant)
|
||||
|
||||
- Page title (for SEO)
|
||||
- Meta description
|
||||
|
||||
|
||||
@@ -7,15 +7,19 @@ Headline formulas, page section types, and structural templates.
|
||||
### Outcome-Focused
|
||||
|
||||
**{Achieve desirable outcome} without {pain point}**
|
||||
|
||||
> Understand how users are really experiencing your site without drowning in numbers
|
||||
|
||||
**{Achieve desirable outcome} by {how product makes it possible}**
|
||||
|
||||
> Generate more leads by seeing which companies visit your site
|
||||
|
||||
**Turn {input} into {outcome}**
|
||||
|
||||
> Turn your hard-earned sales into repeat customers
|
||||
|
||||
**[Achieve outcome] in [timeframe]**
|
||||
|
||||
> Get your tax refund in 10 days
|
||||
|
||||
---
|
||||
@@ -23,12 +27,15 @@ Headline formulas, page section types, and structural templates.
|
||||
### Problem-Focused
|
||||
|
||||
**Never {unpleasant event} again**
|
||||
|
||||
> Never miss a sales opportunity again
|
||||
|
||||
**{Question highlighting the main pain point}**
|
||||
|
||||
> Hate returning stuff to Amazon?
|
||||
|
||||
**Stop [pain]. Start [pleasure].**
|
||||
|
||||
> Stop chasing invoices. Start getting paid on time.
|
||||
|
||||
---
|
||||
@@ -36,12 +43,15 @@ Headline formulas, page section types, and structural templates.
|
||||
### Audience-Focused
|
||||
|
||||
**{Key feature/product type} for {target audience}**
|
||||
|
||||
> Advanced analytics for Shopify e-commerce
|
||||
|
||||
**{Key feature/product type} for {target audience} to {what it's used for}**
|
||||
|
||||
> An online whiteboard for teams to ideate and brainstorm together
|
||||
|
||||
**You don't have to {skills or resources} to {achieve desirable outcome}**
|
||||
|
||||
> With Ahrefs, you don't have to be an SEO pro to rank higher and get more traffic
|
||||
|
||||
---
|
||||
@@ -49,9 +59,11 @@ Headline formulas, page section types, and structural templates.
|
||||
### Differentiation-Focused
|
||||
|
||||
**The {opposite of usual process} way to {achieve desirable outcome}**
|
||||
|
||||
> The easiest way to turn your passion into income
|
||||
|
||||
**The [category] that [key differentiator]**
|
||||
|
||||
> The CRM that updates itself
|
||||
|
||||
---
|
||||
@@ -59,9 +71,11 @@ Headline formulas, page section types, and structural templates.
|
||||
### Proof-Focused
|
||||
|
||||
**[Number] [people] use [product] to [outcome]**
|
||||
|
||||
> 50,000 marketers use Drip to send better emails
|
||||
|
||||
**{Key benefit of your product}**
|
||||
|
||||
> Sound clear in online meetings
|
||||
|
||||
---
|
||||
@@ -69,27 +83,35 @@ Headline formulas, page section types, and structural templates.
|
||||
### Additional Formulas
|
||||
|
||||
**The simple way to {outcome}**
|
||||
|
||||
> The simple way to track your time
|
||||
|
||||
**Finally, {category} that {benefit}**
|
||||
|
||||
> Finally, accounting software that doesn't suck
|
||||
|
||||
**{Outcome} without {common pain}**
|
||||
|
||||
> Build your website without writing code
|
||||
|
||||
**Get {benefit} from your {thing}**
|
||||
|
||||
> Get more revenue from your existing traffic
|
||||
|
||||
**{Action verb} your {thing} like {admirable example}**
|
||||
|
||||
> Market your SaaS like a Fortune 500
|
||||
|
||||
**What if you could {desirable outcome}?**
|
||||
|
||||
> What if you could close deals 30% faster?
|
||||
|
||||
**Everything you need to {outcome}**
|
||||
|
||||
> Everything you need to launch your course
|
||||
|
||||
**The {adjective} {category} built for {audience}**
|
||||
|
||||
> The lightweight CRM built for startups
|
||||
|
||||
---
|
||||
@@ -99,33 +121,39 @@ Headline formulas, page section types, and structural templates.
|
||||
### Core Sections
|
||||
|
||||
**Hero (Above the Fold)**
|
||||
|
||||
- Headline + subheadline
|
||||
- Primary CTA
|
||||
- Supporting visual (product screenshot, hero image)
|
||||
- Optional: Social proof bar
|
||||
|
||||
**Social Proof Bar**
|
||||
|
||||
- Customer logos (recognizable > many)
|
||||
- Key metric ("10,000+ teams")
|
||||
- Star rating with review count
|
||||
- Short testimonial snippet
|
||||
|
||||
**Problem/Pain Section**
|
||||
|
||||
- Articulate their problem better than they can
|
||||
- Create recognition ("that's exactly my situation")
|
||||
- Hint at cost of not solving it
|
||||
|
||||
**Solution/Benefits Section**
|
||||
|
||||
- Bridge from problem to your solution
|
||||
- 3-5 key benefits (not 10)
|
||||
- Each: headline + explanation + proof if available
|
||||
|
||||
**How It Works**
|
||||
|
||||
- 3-4 numbered steps
|
||||
- Reduces perceived complexity
|
||||
- Each step: action + outcome
|
||||
|
||||
**Final CTA Section**
|
||||
|
||||
- Recap value proposition
|
||||
- Repeat primary CTA
|
||||
- Risk reversal (guarantee, free trial)
|
||||
@@ -135,67 +163,79 @@ Headline formulas, page section types, and structural templates.
|
||||
### Supporting Sections
|
||||
|
||||
**Testimonials**
|
||||
|
||||
- Full quotes with names, roles, companies
|
||||
- Photos when possible
|
||||
- Specific results over vague praise
|
||||
- Formats: quote cards, video, tweet embeds
|
||||
|
||||
**Case Studies**
|
||||
|
||||
- Problem → Solution → Results
|
||||
- Specific metrics and outcomes
|
||||
- Customer name and context
|
||||
- Can be snippets with "Read more" links
|
||||
|
||||
**Use Cases**
|
||||
|
||||
- Different ways product is used
|
||||
- Helps visitors self-identify
|
||||
- "For marketers who need X" format
|
||||
|
||||
**Personas / "Built For" Sections**
|
||||
|
||||
- Explicitly call out target audience
|
||||
- "Perfect for [role]" blocks
|
||||
- Addresses "Is this for me?" question
|
||||
|
||||
**FAQ Section**
|
||||
|
||||
- Address common objections
|
||||
- Good for SEO
|
||||
- Reduces support burden
|
||||
- 5-10 most common questions
|
||||
|
||||
**Comparison Section**
|
||||
|
||||
- vs. competitors (name them or don't)
|
||||
- vs. status quo (spreadsheets, manual processes)
|
||||
- Tables or side-by-side format
|
||||
|
||||
**Integrations / Partners**
|
||||
|
||||
- Logos of tools you connect with
|
||||
- "Works with your stack" messaging
|
||||
- Builds credibility
|
||||
|
||||
**Founder Story / Manifesto**
|
||||
|
||||
- Why you built this
|
||||
- What you believe
|
||||
- Emotional connection
|
||||
- Differentiates from faceless competitors
|
||||
|
||||
**Demo / Product Tour**
|
||||
|
||||
- Interactive demos
|
||||
- Video walkthroughs
|
||||
- GIF previews
|
||||
- Shows product in action
|
||||
|
||||
**Pricing Preview**
|
||||
|
||||
- Teaser even on non-pricing pages
|
||||
- Starting price or "from $X/mo"
|
||||
- Moves decision-makers forward
|
||||
|
||||
**Guarantee / Risk Reversal**
|
||||
|
||||
- Money-back guarantee
|
||||
- Free trial terms
|
||||
- "Cancel anytime"
|
||||
- Reduces friction
|
||||
|
||||
**Stats Section**
|
||||
|
||||
- Key metrics that build credibility
|
||||
- "10,000+ customers"
|
||||
- "4.9/5 rating"
|
||||
@@ -295,12 +335,14 @@ Good for ProductHunt, launches, or announcements.
|
||||
### Problem Section
|
||||
|
||||
Start with phrases like:
|
||||
|
||||
- "You know the feeling..."
|
||||
- "If you're like most [role]..."
|
||||
- "Every day, [audience] struggles with..."
|
||||
- "We've all been there..."
|
||||
|
||||
Then describe:
|
||||
|
||||
- The specific frustration
|
||||
- The time/money wasted
|
||||
- The impact on their work/life
|
||||
@@ -308,6 +350,7 @@ Then describe:
|
||||
### Benefits Section
|
||||
|
||||
For each benefit, include:
|
||||
|
||||
- **Headline**: The outcome they get
|
||||
- **Body**: How it works (1-2 sentences)
|
||||
- **Proof**: Number, testimonial, or example (optional)
|
||||
@@ -315,11 +358,13 @@ For each benefit, include:
|
||||
### How It Works Section
|
||||
|
||||
Each step should be:
|
||||
|
||||
- **Numbered**: Creates sense of progress
|
||||
- **Simple verb**: "Connect," "Set up," "Get"
|
||||
- **Outcome-oriented**: What they get from this step
|
||||
|
||||
Example:
|
||||
|
||||
1. Connect your tools (takes 2 minutes)
|
||||
2. Set your preferences
|
||||
3. Get automated reports every Monday
|
||||
@@ -327,12 +372,14 @@ Example:
|
||||
### Testimonial Selection
|
||||
|
||||
Best testimonials include:
|
||||
|
||||
- Specific results ("increased conversions by 32%")
|
||||
- Before/after context ("We used to spend hours...")
|
||||
- Role + company for credibility
|
||||
- Something quotable and specific
|
||||
|
||||
Avoid testimonials that just say:
|
||||
|
||||
- "Great product!"
|
||||
- "Love it!"
|
||||
- "Easy to use!"
|
||||
|
||||
@@ -138,6 +138,7 @@ Use to connect ideas and reinforce key points:
|
||||
Use when citing sources, data, or expert opinions:
|
||||
|
||||
### Neutral attribution
|
||||
|
||||
- According to [Source],...
|
||||
- [Source] reports that...
|
||||
- Research shows that...
|
||||
@@ -145,12 +146,14 @@ Use when citing sources, data, or expert opinions:
|
||||
- A study by [Source] found...
|
||||
|
||||
### Expert quotes
|
||||
|
||||
- As [Expert] puts it,...
|
||||
- [Expert] explains,...
|
||||
- In the words of [Expert],...
|
||||
- [Expert] notes that...
|
||||
|
||||
### Supporting claims
|
||||
|
||||
- This is backed by...
|
||||
- Evidence suggests...
|
||||
- The numbers confirm...
|
||||
|
||||
@@ -121,19 +121,18 @@ export interface Message {
|
||||
export interface AgentEvents {
|
||||
'message:user': (message: Message) => void;
|
||||
'message:assistant': (message: Message) => void;
|
||||
'item:update': (item: StreamableOutputItem) => void; // Items emitted with same ID, replace by ID
|
||||
'item:update': (item: StreamableOutputItem) => void; // Items emitted with same ID, replace by ID
|
||||
'stream:start': () => void;
|
||||
'stream:delta': (delta: string, accumulated: string) => void;
|
||||
'stream:end': (fullText: string) => void;
|
||||
'tool:call': (name: string, args: unknown) => void;
|
||||
'tool:result': (name: string, result: unknown) => void;
|
||||
'reasoning:update': (text: string) => void; // Extended thinking content
|
||||
'error': (error: Error) => void;
|
||||
'reasoning:update': (text: string) => void; // Extended thinking content
|
||||
error: (error: Error) => void;
|
||||
'thinking:start': () => void;
|
||||
'thinking:end': () => void;
|
||||
}
|
||||
|
||||
|
||||
// Agent configuration
|
||||
export interface AgentConfig {
|
||||
apiKey: string;
|
||||
@@ -211,7 +210,9 @@ export class Agent extends EventEmitter<AgentEvents> {
|
||||
switch (item.type) {
|
||||
case 'message':
|
||||
// Message items contain progressively updated content
|
||||
const textContent = item.content?.find((c: { type: string }) => c.type === 'output_text');
|
||||
const textContent = item.content?.find(
|
||||
(c: { type: string }) => c.type === 'output_text',
|
||||
);
|
||||
if (textContent && 'text' in textContent) {
|
||||
const newText = textContent.text;
|
||||
if (newText !== fullText) {
|
||||
@@ -232,7 +233,9 @@ export class Agent extends EventEmitter<AgentEvents> {
|
||||
break;
|
||||
case 'reasoning':
|
||||
// Extended thinking/reasoning content
|
||||
const reasoningText = item.content?.find((c: { type: string }) => c.type === 'reasoning_text');
|
||||
const reasoningText = item.content?.find(
|
||||
(c: { type: string }) => c.type === 'reasoning_text',
|
||||
);
|
||||
if (reasoningText && 'text' in reasoningText) {
|
||||
this.emit('reasoning:update', reasoningText.text);
|
||||
}
|
||||
@@ -426,7 +429,9 @@ function ItemRenderer({ item }: { item: StreamableOutputItem }) {
|
||||
const text = textContent && 'text' in textContent ? textContent.text : '';
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold color="green">◀ Assistant</Text>
|
||||
<Text bold color="green">
|
||||
◀ Assistant
|
||||
</Text>
|
||||
<Text wrap="wrap">{text}</Text>
|
||||
{item.status !== 'completed' && <Text color="gray">▌</Text>}
|
||||
</Box>
|
||||
@@ -440,12 +445,18 @@ function ItemRenderer({ item }: { item: StreamableOutputItem }) {
|
||||
</Text>
|
||||
);
|
||||
case 'reasoning': {
|
||||
const reasoningText = item.content?.find((c: { type: string }) => c.type === 'reasoning_text');
|
||||
const reasoningText = item.content?.find(
|
||||
(c: { type: string }) => c.type === 'reasoning_text',
|
||||
);
|
||||
const text = reasoningText && 'text' in reasoningText ? reasoningText.text : '';
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold color="magenta">💭 Thinking</Text>
|
||||
<Text wrap="wrap" color="gray">{text}</Text>
|
||||
<Text bold color="magenta">
|
||||
💭 Thinking
|
||||
</Text>
|
||||
<Text wrap="wrap" color="gray">
|
||||
{text}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -539,7 +550,9 @@ function App() {
|
||||
return (
|
||||
<Box flexDirection="column" padding={1}>
|
||||
<Box marginBottom={1}>
|
||||
<Text bold color="magenta">🤖 OpenRouter Agent</Text>
|
||||
<Text bold color="magenta">
|
||||
🤖 OpenRouter Agent
|
||||
</Text>
|
||||
<Text color="gray"> (Esc to exit)</Text>
|
||||
</Box>
|
||||
|
||||
@@ -556,12 +569,7 @@ function App() {
|
||||
</Box>
|
||||
|
||||
<Box borderStyle="single" borderColor="gray" paddingX={1}>
|
||||
<InputField
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
onSubmit={sendMessage}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<InputField value={input} onChange={setInput} onSubmit={sendMessage} disabled={isLoading} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
@@ -601,24 +609,27 @@ For function calls, arguments stream progressively:
|
||||
### Why Items Are Better
|
||||
|
||||
**Traditional (accumulation required):**
|
||||
|
||||
```typescript
|
||||
let text = '';
|
||||
for await (const chunk of result.getTextStream()) {
|
||||
text += chunk; // Manual accumulation
|
||||
text += chunk; // Manual accumulation
|
||||
updateUI(text);
|
||||
}
|
||||
```
|
||||
|
||||
**Items (complete replacement):**
|
||||
|
||||
```typescript
|
||||
const items = new Map<string, StreamableOutputItem>();
|
||||
for await (const item of result.getItemsStream()) {
|
||||
items.set(item.id, item); // Replace by ID
|
||||
items.set(item.id, item); // Replace by ID
|
||||
updateUI(items);
|
||||
}
|
||||
```
|
||||
|
||||
Benefits:
|
||||
|
||||
- **No manual chunk management** - each item is complete
|
||||
- **Handles concurrent outputs** - function calls and messages can stream in parallel
|
||||
- **Full TypeScript inference** for all item types
|
||||
@@ -710,55 +721,55 @@ discord.login(process.env.DISCORD_TOKEN);
|
||||
|
||||
### Constructor Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| apiKey | string | required | OpenRouter API key |
|
||||
| model | string | 'openrouter/auto' | Model to use |
|
||||
| instructions | string | 'You are a helpful assistant.' | System prompt |
|
||||
| tools | Tool[] | [] | Available tools |
|
||||
| maxSteps | number | 5 | Max agentic loop iterations |
|
||||
| Option | Type | Default | Description |
|
||||
| ------------ | ------ | ------------------------------ | --------------------------- |
|
||||
| apiKey | string | required | OpenRouter API key |
|
||||
| model | string | 'openrouter/auto' | Model to use |
|
||||
| instructions | string | 'You are a helpful assistant.' | System prompt |
|
||||
| tools | Tool[] | [] | Available tools |
|
||||
| maxSteps | number | 5 | Max agentic loop iterations |
|
||||
|
||||
### Methods
|
||||
|
||||
| Method | Returns | Description |
|
||||
|--------|---------|-------------|
|
||||
| `send(content)` | Promise<string> | Send message with streaming |
|
||||
| `sendSync(content)` | Promise<string> | Send message without streaming |
|
||||
| `getMessages()` | Message[] | Get conversation history |
|
||||
| `clearHistory()` | void | Clear conversation |
|
||||
| `setInstructions(text)` | void | Update system prompt |
|
||||
| `addTool(tool)` | void | Add tool at runtime |
|
||||
| Method | Returns | Description |
|
||||
| ----------------------- | --------------- | ------------------------------ |
|
||||
| `send(content)` | Promise<string> | Send message with streaming |
|
||||
| `sendSync(content)` | Promise<string> | Send message without streaming |
|
||||
| `getMessages()` | Message[] | Get conversation history |
|
||||
| `clearHistory()` | void | Clear conversation |
|
||||
| `setInstructions(text)` | void | Update system prompt |
|
||||
| `addTool(tool)` | void | Add tool at runtime |
|
||||
|
||||
### Events
|
||||
|
||||
| Event | Payload | Description |
|
||||
|-------|---------|-------------|
|
||||
| `message:user` | Message | User message added |
|
||||
| `message:assistant` | Message | Assistant response complete |
|
||||
| `item:update` | StreamableOutputItem | Item emitted (replace by ID, don't accumulate) |
|
||||
| `stream:start` | - | Streaming started |
|
||||
| `stream:delta` | (delta, accumulated) | New text chunk |
|
||||
| `stream:end` | fullText | Streaming complete |
|
||||
| `tool:call` | (name, args) | Tool being called |
|
||||
| `tool:result` | (name, result) | Tool returned result |
|
||||
| `reasoning:update` | text | Extended thinking content |
|
||||
| `thinking:start` | - | Agent processing |
|
||||
| `thinking:end` | - | Agent done processing |
|
||||
| `error` | Error | Error occurred |
|
||||
| Event | Payload | Description |
|
||||
| ------------------- | -------------------- | ---------------------------------------------- |
|
||||
| `message:user` | Message | User message added |
|
||||
| `message:assistant` | Message | Assistant response complete |
|
||||
| `item:update` | StreamableOutputItem | Item emitted (replace by ID, don't accumulate) |
|
||||
| `stream:start` | - | Streaming started |
|
||||
| `stream:delta` | (delta, accumulated) | New text chunk |
|
||||
| `stream:end` | fullText | Streaming complete |
|
||||
| `tool:call` | (name, args) | Tool being called |
|
||||
| `tool:result` | (name, result) | Tool returned result |
|
||||
| `reasoning:update` | text | Extended thinking content |
|
||||
| `thinking:start` | - | Agent processing |
|
||||
| `thinking:end` | - | Agent done processing |
|
||||
| `error` | Error | Error occurred |
|
||||
|
||||
### Item Types (from getItemsStream)
|
||||
|
||||
The SDK uses an items-based streaming model where items are emitted multiple times with the same ID but progressively updated content. Replace items by their ID rather than accumulating chunks.
|
||||
|
||||
| Type | Purpose |
|
||||
|------|---------|
|
||||
| `message` | Assistant text responses |
|
||||
| `function_call` | Tool invocations with streaming arguments |
|
||||
| `function_call_output` | Results from executed tools |
|
||||
| `reasoning` | Extended thinking content |
|
||||
| `web_search_call` | Web search operations |
|
||||
| `file_search_call` | File search operations |
|
||||
| `image_generation_call` | Image generation operations |
|
||||
| Type | Purpose |
|
||||
| ----------------------- | ----------------------------------------- |
|
||||
| `message` | Assistant text responses |
|
||||
| `function_call` | Tool invocations with streaming arguments |
|
||||
| `function_call_output` | Results from executed tools |
|
||||
| `reasoning` | Extended thinking content |
|
||||
| `web_search_call` | Web search operations |
|
||||
| `file_search_call` | File search operations |
|
||||
| `image_generation_call` | Image generation operations |
|
||||
|
||||
## Discovering Models
|
||||
|
||||
@@ -784,8 +795,8 @@ async function fetchModels(): Promise<OpenRouterModel[]> {
|
||||
|
||||
// Find models by criteria
|
||||
async function findModels(filter: {
|
||||
author?: string; // e.g., 'anthropic', 'openai', 'google'
|
||||
minContext?: number; // e.g., 100000 for 100k context
|
||||
author?: string; // e.g., 'anthropic', 'openai', 'google'
|
||||
minContext?: number; // e.g., 100000 for 100k context
|
||||
maxPromptPrice?: number; // e.g., 0.001 for cheap models
|
||||
}): Promise<OpenRouterModel[]> {
|
||||
const models = await fetchModels();
|
||||
@@ -821,7 +832,7 @@ const bestModel = models.find((m) => m.id.includes('claude')) || models[0];
|
||||
|
||||
const agent = createAgent({
|
||||
apiKey: process.env.OPENROUTER_API_KEY!,
|
||||
model: bestModel.id, // Use discovered model
|
||||
model: bestModel.id, // Use discovered model
|
||||
instructions: 'You are a helpful assistant.',
|
||||
});
|
||||
```
|
||||
@@ -834,7 +845,7 @@ available model for your request:
|
||||
```typescript
|
||||
const agent = createAgent({
|
||||
apiKey: process.env.OPENROUTER_API_KEY!,
|
||||
model: 'openrouter/auto', // Auto-selects best model
|
||||
model: 'openrouter/auto', // Auto-selects best model
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -73,15 +73,18 @@ Add OAuth secrets as needed: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE
|
||||
**Location:** `lib/auth.ts` or `src/lib/auth.ts`
|
||||
|
||||
**Minimal config needs:**
|
||||
|
||||
- `database` - Connection or adapter
|
||||
- `emailAndPassword: { enabled: true }` - For email/password auth
|
||||
|
||||
**Standard config adds:**
|
||||
|
||||
- `socialProviders` - OAuth providers (google, github, etc.)
|
||||
- `emailVerification.sendVerificationEmail` - Email verification handler
|
||||
- `emailAndPassword.sendResetPassword` - Password reset handler
|
||||
|
||||
**Full config adds:**
|
||||
|
||||
- `plugins` - Array of feature plugins
|
||||
- `session` - Expiry, cookie cache settings
|
||||
- `account.accountLinking` - Multi-provider linking
|
||||
@@ -110,14 +113,14 @@ Add OAuth secrets as needed: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE
|
||||
|
||||
## Route Handler Setup
|
||||
|
||||
| Framework | File | Handler |
|
||||
|-----------|------|---------|
|
||||
| Framework | File | Handler |
|
||||
| ------------------ | -------------------------------- | ------------------------------------------------ |
|
||||
| Next.js App Router | `app/api/auth/[...all]/route.ts` | `toNextJsHandler(auth)` → export `{ GET, POST }` |
|
||||
| Next.js Pages | `pages/api/auth/[...all].ts` | `toNextJsHandler(auth)` → default export |
|
||||
| Express | Any file | `app.all("/api/auth/*", toNodeHandler(auth))` |
|
||||
| SvelteKit | `src/hooks.server.ts` | `svelteKitHandler(auth)` |
|
||||
| SolidStart | Route file | `solidStartHandler(auth)` |
|
||||
| Hono | Route file | `auth.handler(c.req.raw)` |
|
||||
| Next.js Pages | `pages/api/auth/[...all].ts` | `toNextJsHandler(auth)` → default export |
|
||||
| Express | Any file | `app.all("/api/auth/*", toNodeHandler(auth))` |
|
||||
| SvelteKit | `src/hooks.server.ts` | `svelteKitHandler(auth)` |
|
||||
| SolidStart | Route file | `solidStartHandler(auth)` |
|
||||
| Hono | Route file | `auth.handler(c.req.raw)` |
|
||||
|
||||
**Next.js Server Components:** Add `nextCookies()` plugin to auth config.
|
||||
|
||||
@@ -125,11 +128,11 @@ Add OAuth secrets as needed: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE
|
||||
|
||||
## Database Migrations
|
||||
|
||||
| Adapter | Command |
|
||||
|---------|---------|
|
||||
| Built-in Kysely | `npx @better-auth/cli@latest migrate` (applies directly) |
|
||||
| Prisma | `npx @better-auth/cli@latest generate --output prisma/schema.prisma` then `npx prisma migrate dev` |
|
||||
| Drizzle | `npx @better-auth/cli@latest generate --output src/db/auth-schema.ts` then `npx drizzle-kit push` |
|
||||
| Adapter | Command |
|
||||
| --------------- | -------------------------------------------------------------------------------------------------- |
|
||||
| Built-in Kysely | `npx @better-auth/cli@latest migrate` (applies directly) |
|
||||
| Prisma | `npx @better-auth/cli@latest generate --output prisma/schema.prisma` then `npx prisma migrate dev` |
|
||||
| Drizzle | `npx @better-auth/cli@latest generate --output src/db/auth-schema.ts` then `npx drizzle-kit push` |
|
||||
|
||||
**Re-run after adding plugins.**
|
||||
|
||||
@@ -137,28 +140,28 @@ Add OAuth secrets as needed: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE
|
||||
|
||||
## Database Adapters
|
||||
|
||||
| Database | Setup |
|
||||
|----------|-------|
|
||||
| SQLite | Pass `better-sqlite3` or `bun:sqlite` instance directly |
|
||||
| PostgreSQL | Pass `pg.Pool` instance directly |
|
||||
| MySQL | Pass `mysql2` pool directly |
|
||||
| Prisma | `prismaAdapter(prisma, { provider: "postgresql" })` from `better-auth/adapters/prisma` |
|
||||
| Drizzle | `drizzleAdapter(db, { provider: "pg" })` from `better-auth/adapters/drizzle` |
|
||||
| MongoDB | `mongodbAdapter(db)` from `better-auth/adapters/mongodb` |
|
||||
| Database | Setup |
|
||||
| ---------- | -------------------------------------------------------------------------------------- |
|
||||
| SQLite | Pass `better-sqlite3` or `bun:sqlite` instance directly |
|
||||
| PostgreSQL | Pass `pg.Pool` instance directly |
|
||||
| MySQL | Pass `mysql2` pool directly |
|
||||
| Prisma | `prismaAdapter(prisma, { provider: "postgresql" })` from `better-auth/adapters/prisma` |
|
||||
| Drizzle | `drizzleAdapter(db, { provider: "pg" })` from `better-auth/adapters/drizzle` |
|
||||
| MongoDB | `mongodbAdapter(db)` from `better-auth/adapters/mongodb` |
|
||||
|
||||
---
|
||||
|
||||
## Common Plugins
|
||||
|
||||
| Plugin | Server Import | Client Import | Purpose |
|
||||
|--------|---------------|---------------|---------|
|
||||
| `twoFactor` | `better-auth/plugins` | `twoFactorClient` | 2FA with TOTP/OTP |
|
||||
| `organization` | `better-auth/plugins` | `organizationClient` | Teams/orgs |
|
||||
| `admin` | `better-auth/plugins` | `adminClient` | User management |
|
||||
| `bearer` | `better-auth/plugins` | - | API token auth |
|
||||
| `openAPI` | `better-auth/plugins` | - | API docs |
|
||||
| `passkey` | `@better-auth/passkey` | `passkeyClient` | WebAuthn |
|
||||
| `sso` | `@better-auth/sso` | - | Enterprise SSO |
|
||||
| Plugin | Server Import | Client Import | Purpose |
|
||||
| -------------- | ---------------------- | -------------------- | ----------------- |
|
||||
| `twoFactor` | `better-auth/plugins` | `twoFactorClient` | 2FA with TOTP/OTP |
|
||||
| `organization` | `better-auth/plugins` | `organizationClient` | Teams/orgs |
|
||||
| `admin` | `better-auth/plugins` | `adminClient` | User management |
|
||||
| `bearer` | `better-auth/plugins` | - | API token auth |
|
||||
| `openAPI` | `better-auth/plugins` | - | API docs |
|
||||
| `passkey` | `@better-auth/passkey` | `passkeyClient` | WebAuthn |
|
||||
| `sso` | `@better-auth/sso` | - | Enterprise SSO |
|
||||
|
||||
**Plugin pattern:** Server plugin + client plugin + run migrations.
|
||||
|
||||
@@ -167,6 +170,7 @@ Add OAuth secrets as needed: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE
|
||||
## Auth UI Implementation
|
||||
|
||||
**Sign in flow:**
|
||||
|
||||
1. `signIn.email({ email, password })` or `signIn.social({ provider, callbackURL })`
|
||||
2. Handle `error` in response
|
||||
3. Redirect on success
|
||||
@@ -195,13 +199,13 @@ Add OAuth secrets as needed: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Issue | Fix |
|
||||
|-------|-----|
|
||||
| "Secret not set" | Add `BETTER_AUTH_SECRET` env var |
|
||||
| "Invalid Origin" | Add domain to `trustedOrigins` |
|
||||
| Cookies not setting | Check `baseURL` matches domain; enable secure cookies in prod |
|
||||
| OAuth callback errors | Verify redirect URIs in provider dashboard |
|
||||
| Type errors after adding plugin | Re-run CLI generate/migrate |
|
||||
| Issue | Fix |
|
||||
| ------------------------------- | ------------------------------------------------------------- |
|
||||
| "Secret not set" | Add `BETTER_AUTH_SECRET` env var |
|
||||
| "Invalid Origin" | Add domain to `trustedOrigins` |
|
||||
| Cookies not setting | Check `baseURL` matches domain; enable secure cookies in prod |
|
||||
| OAuth callback errors | Verify redirect URIs in provider dashboard |
|
||||
| Type errors after adding plugin | Re-run CLI generate/migrate |
|
||||
|
||||
---
|
||||
|
||||
@@ -211,4 +215,4 @@ Add OAuth secrets as needed: `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GOOGLE
|
||||
- [Examples](https://github.com/better-auth/examples)
|
||||
- [Plugins](https://better-auth.com/docs/concepts/plugins)
|
||||
- [CLI](https://better-auth.com/docs/concepts/cli)
|
||||
- [Migration Guides](https://better-auth.com/docs/guides)
|
||||
- [Migration Guides](https://better-auth.com/docs/guides)
|
||||
|
||||
@@ -32,12 +32,14 @@ digraph when_to_use {
|
||||
```
|
||||
|
||||
**Use when:**
|
||||
|
||||
- 3+ test files failing with different root causes
|
||||
- Multiple subsystems broken independently
|
||||
- Each problem can be understood without context from others
|
||||
- No shared state between investigations
|
||||
|
||||
**Don't use when:**
|
||||
|
||||
- Failures are related (fix one might fix others)
|
||||
- Need to understand full system state
|
||||
- Agents would interfere with each other
|
||||
@@ -47,6 +49,7 @@ digraph when_to_use {
|
||||
### 1. Identify Independent Domains
|
||||
|
||||
Group failures by what's broken:
|
||||
|
||||
- File A tests: Tool approval flow
|
||||
- File B tests: Batch completion behavior
|
||||
- File C tests: Abort functionality
|
||||
@@ -56,6 +59,7 @@ Each domain is independent - fixing tool approval doesn't affect abort tests.
|
||||
### 2. Create Focused Agent Tasks
|
||||
|
||||
Each agent gets:
|
||||
|
||||
- **Specific scope:** One test file or subsystem
|
||||
- **Clear goal:** Make these tests pass
|
||||
- **Constraints:** Don't change other code
|
||||
@@ -65,15 +69,16 @@ Each agent gets:
|
||||
|
||||
```typescript
|
||||
// In Claude Code / AI environment
|
||||
Task("Fix agent-tool-abort.test.ts failures")
|
||||
Task("Fix batch-completion-behavior.test.ts failures")
|
||||
Task("Fix tool-approval-race-conditions.test.ts failures")
|
||||
Task('Fix agent-tool-abort.test.ts failures');
|
||||
Task('Fix batch-completion-behavior.test.ts failures');
|
||||
Task('Fix tool-approval-race-conditions.test.ts failures');
|
||||
// All three run concurrently
|
||||
```
|
||||
|
||||
### 4. Review and Integrate
|
||||
|
||||
When agents return:
|
||||
|
||||
- Read each summary
|
||||
- Verify fixes don't conflict
|
||||
- Run full test suite
|
||||
@@ -82,6 +87,7 @@ When agents return:
|
||||
## Agent Prompt Structure
|
||||
|
||||
Good agent prompts are:
|
||||
|
||||
1. **Focused** - One clear problem domain
|
||||
2. **Self-contained** - All context needed to understand the problem
|
||||
3. **Specific about output** - What should the agent return?
|
||||
@@ -133,6 +139,7 @@ Return: Summary of what you found and what you fixed.
|
||||
**Scenario:** 6 test failures across 3 files after major refactoring
|
||||
|
||||
**Failures:**
|
||||
|
||||
- agent-tool-abort.test.ts: 3 failures (timing issues)
|
||||
- batch-completion-behavior.test.ts: 2 failures (tools not executing)
|
||||
- tool-approval-race-conditions.test.ts: 1 failure (execution count = 0)
|
||||
@@ -140,6 +147,7 @@ Return: Summary of what you found and what you fixed.
|
||||
**Decision:** Independent domains - abort logic separate from batch completion separate from race conditions
|
||||
|
||||
**Dispatch:**
|
||||
|
||||
```
|
||||
Agent 1 → Fix agent-tool-abort.test.ts
|
||||
Agent 2 → Fix batch-completion-behavior.test.ts
|
||||
@@ -147,6 +155,7 @@ Agent 3 → Fix tool-approval-race-conditions.test.ts
|
||||
```
|
||||
|
||||
**Results:**
|
||||
|
||||
- Agent 1: Replaced timeouts with event-based waiting
|
||||
- Agent 2: Fixed event structure bug (threadId in wrong place)
|
||||
- Agent 3: Added wait for async tool execution to complete
|
||||
@@ -165,6 +174,7 @@ Agent 3 → Fix tool-approval-race-conditions.test.ts
|
||||
## Verification
|
||||
|
||||
After agents return:
|
||||
|
||||
1. **Review each summary** - Understand what changed
|
||||
2. **Check for conflicts** - Did agents edit same code?
|
||||
3. **Run full suite** - Verify all fixes work together
|
||||
@@ -173,6 +183,7 @@ After agents return:
|
||||
## Real-World Impact
|
||||
|
||||
From debugging session (2025-10-03):
|
||||
|
||||
- 6 failures across 3 files
|
||||
- 3 agents dispatched in parallel
|
||||
- All investigations completed concurrently
|
||||
|
||||
@@ -10,6 +10,7 @@ This skill provides a structured workflow for guiding users through collaborativ
|
||||
## When to Offer This Workflow
|
||||
|
||||
**Trigger conditions:**
|
||||
|
||||
- User mentions writing documentation: "write a doc", "draft a proposal", "create a spec", "write up"
|
||||
- User mentions specific doc types: "PRD", "design doc", "decision doc", "RFC"
|
||||
- User seems to be starting a substantial writing task
|
||||
@@ -42,11 +43,13 @@ Start by asking the user for meta-context about the document:
|
||||
Inform them they can answer in shorthand or dump information however works best for them.
|
||||
|
||||
**If user provides a template or mentions a doc type:**
|
||||
|
||||
- Ask if they have a template document to share
|
||||
- If they provide a link to a shared document, use the appropriate integration to fetch it
|
||||
- If they provide a file, read it
|
||||
|
||||
**If user mentions editing an existing shared document:**
|
||||
|
||||
- Use the appropriate integration to read the current state
|
||||
- Check for images without alt-text
|
||||
- If images exist without alt-text, explain that when others use Claude to understand the doc, Claude won't be able to see them. Ask if they want alt-text generated. If so, request they paste each image into chat for descriptive alt-text generation.
|
||||
@@ -54,6 +57,7 @@ Inform them they can answer in shorthand or dump information however works best
|
||||
### Info Dumping
|
||||
|
||||
Once initial questions are answered, encourage the user to dump all the context they have. Request information such as:
|
||||
|
||||
- Background on the project/problem
|
||||
- Related team discussions or shared documents
|
||||
- Why alternative solutions aren't being used
|
||||
@@ -63,6 +67,7 @@ Once initial questions are answered, encourage the user to dump all the context
|
||||
- Stakeholder concerns
|
||||
|
||||
Advise them not to worry about organizing it - just get it all out. Offer multiple ways to provide context:
|
||||
|
||||
- Info dump stream-of-consciousness
|
||||
- Point to team channels or threads to read
|
||||
- Link to shared documents
|
||||
@@ -107,6 +112,7 @@ If user wants to add more, let them. When ready, proceed to Stage 2.
|
||||
|
||||
**Instructions to user:**
|
||||
Explain that the document will be built section by section. For each section:
|
||||
|
||||
1. Clarifying questions will be asked about what to include
|
||||
2. 5-20 options will be brainstormed
|
||||
3. User will indicate what to keep/remove/combine
|
||||
@@ -162,6 +168,7 @@ Inform them they can answer in shorthand or just indicate what's important to co
|
||||
### Step 2: Brainstorming
|
||||
|
||||
For the [SECTION NAME] section, brainstorm [5-20] things that might be included, depending on the section's complexity. Look for:
|
||||
|
||||
- Context shared that might have been forgotten
|
||||
- Angles or considerations not yet mentioned
|
||||
|
||||
@@ -172,6 +179,7 @@ Generate 5-20 numbered options based on section complexity. At the end, offer to
|
||||
Ask which points should be kept, removed, or combined. Request brief justifications to help learn priorities for the next sections.
|
||||
|
||||
Provide examples:
|
||||
|
||||
- "Keep 1,4,7,9"
|
||||
- "Remove 3 (duplicates 1)"
|
||||
- "Remove 6 (audience already knows this)"
|
||||
@@ -205,6 +213,7 @@ Provide a note: Instead of editing the doc directly, ask them to indicate what t
|
||||
### Step 6: Iterative Refinement
|
||||
|
||||
As user provides feedback:
|
||||
|
||||
- Use `str_replace` to make edits (never reprint the whole doc)
|
||||
- **If using artifacts:** Provide link to artifact after each edit
|
||||
- **If using files:** Just confirm edits are complete
|
||||
@@ -223,6 +232,7 @@ When section is done, confirm [SECTION NAME] is complete. Ask if ready to move t
|
||||
### Near Completion
|
||||
|
||||
As approaching completion (80%+ of sections done), announce intention to re-read the entire document and check for:
|
||||
|
||||
- Flow and consistency across sections
|
||||
- Redundancy or contradictions
|
||||
- Anything that feels like "slop" or generic filler
|
||||
@@ -300,11 +310,13 @@ Generate 5-10 questions that readers would realistically ask.
|
||||
### Step 2: Setup Testing
|
||||
|
||||
Provide testing instructions:
|
||||
|
||||
1. Open a fresh Claude conversation: https://claude.ai
|
||||
2. Paste or share the document content (if using a shared doc platform with connectors enabled, provide the link)
|
||||
3. Ask Reader Claude the generated questions
|
||||
|
||||
For each question, instruct Reader Claude to provide:
|
||||
|
||||
- The answer
|
||||
- Whether anything was ambiguous or unclear
|
||||
- What knowledge/context the doc assumes is already known
|
||||
@@ -314,6 +326,7 @@ Check if Reader Claude gives correct answers or misinterprets anything.
|
||||
### Step 3: Additional Checks
|
||||
|
||||
Also ask Reader Claude:
|
||||
|
||||
- "What in this doc might be ambiguous or unclear to readers?"
|
||||
- "What knowledge or context does this doc assume readers already have?"
|
||||
- "Are there any internal contradictions or inconsistencies?"
|
||||
@@ -343,6 +356,7 @@ Ask if they want one more review, or if the work is done.
|
||||
|
||||
**If user wants final review, provide it. Otherwise:**
|
||||
Announce document completion. Provide a few final tips:
|
||||
|
||||
- Consider linking this conversation in an appendix so readers can see how the doc was developed
|
||||
- Use appendices to provide depth without bloating the main doc
|
||||
- Update the doc as feedback is received from real readers
|
||||
@@ -350,26 +364,31 @@ Announce document completion. Provide a few final tips:
|
||||
## Tips for Effective Guidance
|
||||
|
||||
**Tone:**
|
||||
|
||||
- Be direct and procedural
|
||||
- Explain rationale briefly when it affects user behavior
|
||||
- Don't try to "sell" the approach - just execute it
|
||||
|
||||
**Handling Deviations:**
|
||||
|
||||
- If user wants to skip a stage: Ask if they want to skip this and write freeform
|
||||
- If user seems frustrated: Acknowledge this is taking longer than expected. Suggest ways to move faster
|
||||
- Always give user agency to adjust the process
|
||||
|
||||
**Context Management:**
|
||||
|
||||
- Throughout, if context is missing on something mentioned, proactively ask
|
||||
- Don't let gaps accumulate - address them as they come up
|
||||
|
||||
**Artifact Management:**
|
||||
|
||||
- Use `create_file` for drafting full sections
|
||||
- Use `str_replace` for all edits
|
||||
- Provide artifact link after every change
|
||||
- Never use artifacts for brainstorming lists - that's just conversation
|
||||
|
||||
**Quality over Speed:**
|
||||
|
||||
- Don't rush through stages
|
||||
- Each iteration should make meaningful improvements
|
||||
- The goal is a document that actually works for readers
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: docx
|
||||
description: "Use this skill whenever the user wants to create, read, edit, or manipulate Word documents (.docx files). Triggers include: any mention of \"Word doc\", \"word document\", \".docx\", or requests to produce professional documents with formatting like tables of contents, headings, page numbers, or letterheads. Also use when extracting or reorganizing content from .docx files, inserting or replacing images in documents, performing find-and-replace in Word files, working with tracked changes or comments, or converting content into a polished Word document. If the user asks for a \"report\", \"memo\", \"letter\", \"template\", or similar deliverable as a Word or .docx file, use this skill. Do NOT use for PDFs, spreadsheets, Google Docs, or general coding tasks unrelated to document generation."
|
||||
description: 'Use this skill whenever the user wants to create, read, edit, or manipulate Word documents (.docx files). Triggers include: any mention of "Word doc", "word document", ".docx", or requests to produce professional documents with formatting like tables of contents, headings, page numbers, or letterheads. Also use when extracting or reorganizing content from .docx files, inserting or replacing images in documents, performing find-and-replace in Word files, working with tracked changes or comments, or converting content into a polished Word document. If the user asks for a "report", "memo", "letter", "template", or similar deliverable as a Word or .docx file, use this skill. Do NOT use for PDFs, spreadsheets, Google Docs, or general coding tasks unrelated to document generation.'
|
||||
license: Proprietary. LICENSE.txt has complete terms
|
||||
---
|
||||
|
||||
@@ -12,10 +12,10 @@ A .docx file is a ZIP archive containing XML files.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Task | Approach |
|
||||
|------|----------|
|
||||
| Read/analyze content | `pandoc` or unpack for raw XML |
|
||||
| Create new document | Use `docx-js` - see Creating New Documents below |
|
||||
| Task | Approach |
|
||||
| ---------------------- | ----------------------------------------------------------------- |
|
||||
| Read/analyze content | `pandoc` or unpack for raw XML |
|
||||
| Create new document | Use `docx-js` - see Creating New Documents below |
|
||||
| Edit existing document | Unpack → edit XML → repack - see Editing Existing Documents below |
|
||||
|
||||
### Converting .doc to .docx
|
||||
@@ -58,18 +58,49 @@ python scripts/accept_changes.py input.docx output.docx
|
||||
Generate .docx files with JavaScript, then validate. Install: `npm install -g docx`
|
||||
|
||||
### Setup
|
||||
```javascript
|
||||
const { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, ImageRun,
|
||||
Header, Footer, AlignmentType, PageOrientation, LevelFormat, ExternalHyperlink,
|
||||
TableOfContents, HeadingLevel, BorderStyle, WidthType, ShadingType,
|
||||
VerticalAlign, PageNumber, PageBreak } = require('docx');
|
||||
|
||||
const doc = new Document({ sections: [{ children: [/* content */] }] });
|
||||
Packer.toBuffer(doc).then(buffer => fs.writeFileSync("doc.docx", buffer));
|
||||
```javascript
|
||||
const {
|
||||
Document,
|
||||
Packer,
|
||||
Paragraph,
|
||||
TextRun,
|
||||
Table,
|
||||
TableRow,
|
||||
TableCell,
|
||||
ImageRun,
|
||||
Header,
|
||||
Footer,
|
||||
AlignmentType,
|
||||
PageOrientation,
|
||||
LevelFormat,
|
||||
ExternalHyperlink,
|
||||
TableOfContents,
|
||||
HeadingLevel,
|
||||
BorderStyle,
|
||||
WidthType,
|
||||
ShadingType,
|
||||
VerticalAlign,
|
||||
PageNumber,
|
||||
PageBreak,
|
||||
} = require('docx');
|
||||
|
||||
const doc = new Document({
|
||||
sections: [
|
||||
{
|
||||
children: [
|
||||
/* content */
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
Packer.toBuffer(doc).then((buffer) => fs.writeFileSync('doc.docx', buffer));
|
||||
```
|
||||
|
||||
### Validation
|
||||
|
||||
After creating the file, validate it. If validation fails, unpack, fix the XML, and repack.
|
||||
|
||||
```bash
|
||||
python scripts/office/validate.py doc.docx
|
||||
```
|
||||
@@ -79,28 +110,33 @@ python scripts/office/validate.py doc.docx
|
||||
```javascript
|
||||
// CRITICAL: docx-js defaults to A4, not US Letter
|
||||
// Always set page size explicitly for consistent results
|
||||
sections: [{
|
||||
properties: {
|
||||
page: {
|
||||
size: {
|
||||
width: 12240, // 8.5 inches in DXA
|
||||
height: 15840 // 11 inches in DXA
|
||||
sections: [
|
||||
{
|
||||
properties: {
|
||||
page: {
|
||||
size: {
|
||||
width: 12240, // 8.5 inches in DXA
|
||||
height: 15840, // 11 inches in DXA
|
||||
},
|
||||
margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 }, // 1 inch margins
|
||||
},
|
||||
margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } // 1 inch margins
|
||||
}
|
||||
},
|
||||
children: [
|
||||
/* content */
|
||||
],
|
||||
},
|
||||
children: [/* content */]
|
||||
}]
|
||||
];
|
||||
```
|
||||
|
||||
**Common page sizes (DXA units, 1440 DXA = 1 inch):**
|
||||
|
||||
| Paper | Width | Height | Content Width (1" margins) |
|
||||
|-------|-------|--------|---------------------------|
|
||||
| US Letter | 12,240 | 15,840 | 9,360 |
|
||||
| A4 (default) | 11,906 | 16,838 | 9,026 |
|
||||
| Paper | Width | Height | Content Width (1" margins) |
|
||||
| ------------ | ------ | ------ | -------------------------- |
|
||||
| US Letter | 12,240 | 15,840 | 9,360 |
|
||||
| A4 (default) | 11,906 | 16,838 | 9,026 |
|
||||
|
||||
**Landscape orientation:** docx-js swaps width/height internally, so pass portrait dimensions and let it handle the swap:
|
||||
|
||||
```javascript
|
||||
size: {
|
||||
width: 12240, // Pass SHORT edge as width
|
||||
@@ -117,22 +153,36 @@ Use Arial as the default font (universally supported). Keep titles black for rea
|
||||
```javascript
|
||||
const doc = new Document({
|
||||
styles: {
|
||||
default: { document: { run: { font: "Arial", size: 24 } } }, // 12pt default
|
||||
default: { document: { run: { font: 'Arial', size: 24 } } }, // 12pt default
|
||||
paragraphStyles: [
|
||||
// IMPORTANT: Use exact IDs to override built-in styles
|
||||
{ id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true,
|
||||
run: { size: 32, bold: true, font: "Arial" },
|
||||
paragraph: { spacing: { before: 240, after: 240 }, outlineLevel: 0 } }, // outlineLevel required for TOC
|
||||
{ id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true,
|
||||
run: { size: 28, bold: true, font: "Arial" },
|
||||
paragraph: { spacing: { before: 180, after: 180 }, outlineLevel: 1 } },
|
||||
]
|
||||
{
|
||||
id: 'Heading1',
|
||||
name: 'Heading 1',
|
||||
basedOn: 'Normal',
|
||||
next: 'Normal',
|
||||
quickFormat: true,
|
||||
run: { size: 32, bold: true, font: 'Arial' },
|
||||
paragraph: { spacing: { before: 240, after: 240 }, outlineLevel: 0 },
|
||||
}, // outlineLevel required for TOC
|
||||
{
|
||||
id: 'Heading2',
|
||||
name: 'Heading 2',
|
||||
basedOn: 'Normal',
|
||||
next: 'Normal',
|
||||
quickFormat: true,
|
||||
run: { size: 28, bold: true, font: 'Arial' },
|
||||
paragraph: { spacing: { before: 180, after: 180 }, outlineLevel: 1 },
|
||||
},
|
||||
],
|
||||
},
|
||||
sections: [{
|
||||
children: [
|
||||
new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun("Title")] }),
|
||||
]
|
||||
}]
|
||||
sections: [
|
||||
{
|
||||
children: [
|
||||
new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun('Title')] }),
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
@@ -140,29 +190,53 @@ const doc = new Document({
|
||||
|
||||
```javascript
|
||||
// ❌ WRONG - never manually insert bullet characters
|
||||
new Paragraph({ children: [new TextRun("• Item")] }) // BAD
|
||||
new Paragraph({ children: [new TextRun("\u2022 Item")] }) // BAD
|
||||
new Paragraph({ children: [new TextRun('• Item')] }); // BAD
|
||||
new Paragraph({ children: [new TextRun('\u2022 Item')] }); // BAD
|
||||
|
||||
// ✅ CORRECT - use numbering config with LevelFormat.BULLET
|
||||
const doc = new Document({
|
||||
numbering: {
|
||||
config: [
|
||||
{ reference: "bullets",
|
||||
levels: [{ level: 0, format: LevelFormat.BULLET, text: "•", alignment: AlignmentType.LEFT,
|
||||
style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] },
|
||||
{ reference: "numbers",
|
||||
levels: [{ level: 0, format: LevelFormat.DECIMAL, text: "%1.", alignment: AlignmentType.LEFT,
|
||||
style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] },
|
||||
]
|
||||
{
|
||||
reference: 'bullets',
|
||||
levels: [
|
||||
{
|
||||
level: 0,
|
||||
format: LevelFormat.BULLET,
|
||||
text: '•',
|
||||
alignment: AlignmentType.LEFT,
|
||||
style: { paragraph: { indent: { left: 720, hanging: 360 } } },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
reference: 'numbers',
|
||||
levels: [
|
||||
{
|
||||
level: 0,
|
||||
format: LevelFormat.DECIMAL,
|
||||
text: '%1.',
|
||||
alignment: AlignmentType.LEFT,
|
||||
style: { paragraph: { indent: { left: 720, hanging: 360 } } },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
sections: [{
|
||||
children: [
|
||||
new Paragraph({ numbering: { reference: "bullets", level: 0 },
|
||||
children: [new TextRun("Bullet item")] }),
|
||||
new Paragraph({ numbering: { reference: "numbers", level: 0 },
|
||||
children: [new TextRun("Numbered item")] }),
|
||||
]
|
||||
}]
|
||||
sections: [
|
||||
{
|
||||
children: [
|
||||
new Paragraph({
|
||||
numbering: { reference: 'bullets', level: 0 },
|
||||
children: [new TextRun('Bullet item')],
|
||||
}),
|
||||
new Paragraph({
|
||||
numbering: { reference: 'numbers', level: 0 },
|
||||
children: [new TextRun('Numbered item')],
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// ⚠️ Each reference creates INDEPENDENT numbering
|
||||
@@ -177,7 +251,7 @@ const doc = new Document({
|
||||
```javascript
|
||||
// CRITICAL: Always set table width for consistent rendering
|
||||
// CRITICAL: Use ShadingType.CLEAR (not SOLID) to prevent black backgrounds
|
||||
const border = { style: BorderStyle.SINGLE, size: 1, color: "CCCCCC" };
|
||||
const border = { style: BorderStyle.SINGLE, size: 1, color: 'CCCCCC' };
|
||||
const borders = { top: border, bottom: border, left: border, right: border };
|
||||
|
||||
new Table({
|
||||
@@ -189,14 +263,14 @@ new Table({
|
||||
new TableCell({
|
||||
borders,
|
||||
width: { size: 4680, type: WidthType.DXA }, // Also set on each cell
|
||||
shading: { fill: "D5E8F0", type: ShadingType.CLEAR }, // CLEAR not SOLID
|
||||
shading: { fill: 'D5E8F0', type: ShadingType.CLEAR }, // CLEAR not SOLID
|
||||
margins: { top: 80, bottom: 80, left: 120, right: 120 }, // Cell padding (internal, not added to width)
|
||||
children: [new Paragraph({ children: [new TextRun("Cell")] })]
|
||||
})
|
||||
]
|
||||
})
|
||||
]
|
||||
})
|
||||
children: [new Paragraph({ children: [new TextRun('Cell')] })],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
**Table width calculation:**
|
||||
@@ -211,6 +285,7 @@ columnWidths: [7000, 2360] // Must sum to table width
|
||||
```
|
||||
|
||||
**Width rules:**
|
||||
|
||||
- **Always use `WidthType.DXA`** — never `WidthType.PERCENTAGE` (incompatible with Google Docs)
|
||||
- Table width must equal the sum of `columnWidths`
|
||||
- Cell `width` must match corresponding `columnWidth`
|
||||
@@ -222,49 +297,59 @@ columnWidths: [7000, 2360] // Must sum to table width
|
||||
```javascript
|
||||
// CRITICAL: type parameter is REQUIRED
|
||||
new Paragraph({
|
||||
children: [new ImageRun({
|
||||
type: "png", // Required: png, jpg, jpeg, gif, bmp, svg
|
||||
data: fs.readFileSync("image.png"),
|
||||
transformation: { width: 200, height: 150 },
|
||||
altText: { title: "Title", description: "Desc", name: "Name" } // All three required
|
||||
})]
|
||||
})
|
||||
children: [
|
||||
new ImageRun({
|
||||
type: 'png', // Required: png, jpg, jpeg, gif, bmp, svg
|
||||
data: fs.readFileSync('image.png'),
|
||||
transformation: { width: 200, height: 150 },
|
||||
altText: { title: 'Title', description: 'Desc', name: 'Name' }, // All three required
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Page Breaks
|
||||
|
||||
```javascript
|
||||
// CRITICAL: PageBreak must be inside a Paragraph
|
||||
new Paragraph({ children: [new PageBreak()] })
|
||||
new Paragraph({ children: [new PageBreak()] });
|
||||
|
||||
// Or use pageBreakBefore
|
||||
new Paragraph({ pageBreakBefore: true, children: [new TextRun("New page")] })
|
||||
new Paragraph({ pageBreakBefore: true, children: [new TextRun('New page')] });
|
||||
```
|
||||
|
||||
### Table of Contents
|
||||
|
||||
```javascript
|
||||
// CRITICAL: Headings must use HeadingLevel ONLY - no custom styles
|
||||
new TableOfContents("Table of Contents", { hyperlink: true, headingStyleRange: "1-3" })
|
||||
new TableOfContents('Table of Contents', { hyperlink: true, headingStyleRange: '1-3' });
|
||||
```
|
||||
|
||||
### Headers/Footers
|
||||
|
||||
```javascript
|
||||
sections: [{
|
||||
properties: {
|
||||
page: { margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } } // 1440 = 1 inch
|
||||
sections: [
|
||||
{
|
||||
properties: {
|
||||
page: { margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } }, // 1440 = 1 inch
|
||||
},
|
||||
headers: {
|
||||
default: new Header({ children: [new Paragraph({ children: [new TextRun('Header')] })] }),
|
||||
},
|
||||
footers: {
|
||||
default: new Footer({
|
||||
children: [
|
||||
new Paragraph({
|
||||
children: [new TextRun('Page '), new TextRun({ children: [PageNumber.CURRENT] })],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
},
|
||||
children: [
|
||||
/* content */
|
||||
],
|
||||
},
|
||||
headers: {
|
||||
default: new Header({ children: [new Paragraph({ children: [new TextRun("Header")] })] })
|
||||
},
|
||||
footers: {
|
||||
default: new Footer({ children: [new Paragraph({
|
||||
children: [new TextRun("Page "), new TextRun({ children: [PageNumber.CURRENT] })]
|
||||
})] })
|
||||
},
|
||||
children: [/* content */]
|
||||
}]
|
||||
];
|
||||
```
|
||||
|
||||
### Critical Rules for docx-js
|
||||
@@ -291,9 +376,11 @@ sections: [{
|
||||
**Follow all 3 steps in order.**
|
||||
|
||||
### Step 1: Unpack
|
||||
|
||||
```bash
|
||||
python scripts/office/unpack.py document.docx unpacked/
|
||||
```
|
||||
|
||||
Extracts XML, pretty-prints, merges adjacent runs, and converts smart quotes to XML entities (`“` etc.) so they survive editing. Use `--merge-runs false` to skip run merging.
|
||||
|
||||
### Step 2: Edit XML
|
||||
@@ -305,36 +392,44 @@ Edit files in `unpacked/word/`. See XML Reference below for patterns.
|
||||
**Use the Edit tool directly for string replacement. Do not write Python scripts.** Scripts introduce unnecessary complexity. The Edit tool shows exactly what is being replaced.
|
||||
|
||||
**CRITICAL: Use smart quotes for new content.** When adding text with apostrophes or quotes, use XML entities to produce smart quotes:
|
||||
|
||||
```xml
|
||||
<!-- Use these entities for professional typography -->
|
||||
<w:t>Here’s a quote: “Hello”</w:t>
|
||||
```
|
||||
| Entity | Character |
|
||||
|--------|-----------|
|
||||
| `‘` | ‘ (left single) |
|
||||
|
||||
| Entity | Character |
|
||||
| ---------- | ----------------------------- |
|
||||
| `‘` | ‘ (left single) |
|
||||
| `’` | ’ (right single / apostrophe) |
|
||||
| `“` | “ (left double) |
|
||||
| `”` | ” (right double) |
|
||||
| `“` | “ (left double) |
|
||||
| `”` | ” (right double) |
|
||||
|
||||
**Adding comments:** Use `comment.py` to handle boilerplate across multiple XML files (text must be pre-escaped XML):
|
||||
|
||||
```bash
|
||||
python scripts/comment.py unpacked/ 0 "Comment text with & and ’"
|
||||
python scripts/comment.py unpacked/ 1 "Reply text" --parent 0 # reply to comment 0
|
||||
python scripts/comment.py unpacked/ 0 "Text" --author "Custom Author" # custom author name
|
||||
```
|
||||
|
||||
Then add markers to document.xml (see Comments in XML Reference).
|
||||
|
||||
### Step 3: Pack
|
||||
|
||||
```bash
|
||||
python scripts/office/pack.py unpacked/ output.docx --original document.docx
|
||||
```
|
||||
|
||||
Validates with auto-repair, condenses XML, and creates DOCX. Use `--validate false` to skip.
|
||||
|
||||
**Auto-repair will fix:**
|
||||
|
||||
- `durableId` >= 0x7FFFFFFF (regenerates valid ID)
|
||||
- Missing `xml:space="preserve"` on `<w:t>` with whitespace
|
||||
|
||||
**Auto-repair won't fix:**
|
||||
|
||||
- Malformed XML, invalid element nesting, missing relationships, schema violations
|
||||
|
||||
### Common Pitfalls
|
||||
@@ -355,6 +450,7 @@ Validates with auto-repair, condenses XML, and creates DOCX. Use `--validate fal
|
||||
### Tracked Changes
|
||||
|
||||
**Insertion:**
|
||||
|
||||
```xml
|
||||
<w:ins w:id="1" w:author="Claude" w:date="2025-01-01T00:00:00Z">
|
||||
<w:r><w:t>inserted text</w:t></w:r>
|
||||
@@ -362,6 +458,7 @@ Validates with auto-repair, condenses XML, and creates DOCX. Use `--validate fal
|
||||
```
|
||||
|
||||
**Deletion:**
|
||||
|
||||
```xml
|
||||
<w:del w:id="2" w:author="Claude" w:date="2025-01-01T00:00:00Z">
|
||||
<w:r><w:delText>deleted text</w:delText></w:r>
|
||||
@@ -371,6 +468,7 @@ Validates with auto-repair, condenses XML, and creates DOCX. Use `--validate fal
|
||||
**Inside `<w:del>`**: Use `<w:delText>` instead of `<w:t>`, and `<w:delInstrText>` instead of `<w:instrText>`.
|
||||
|
||||
**Minimal edits** - only mark what changes:
|
||||
|
||||
```xml
|
||||
<!-- Change "30 days" to "60 days" -->
|
||||
<w:r><w:t>The term is </w:t></w:r>
|
||||
@@ -384,6 +482,7 @@ Validates with auto-repair, condenses XML, and creates DOCX. Use `--validate fal
|
||||
```
|
||||
|
||||
**Deleting entire paragraphs/list items** - when removing ALL content from a paragraph, also mark the paragraph mark as deleted so it merges with the next paragraph. Add `<w:del/>` inside `<w:pPr><w:rPr>`:
|
||||
|
||||
```xml
|
||||
<w:p>
|
||||
<w:pPr>
|
||||
@@ -397,9 +496,11 @@ Validates with auto-repair, condenses XML, and creates DOCX. Use `--validate fal
|
||||
</w:del>
|
||||
</w:p>
|
||||
```
|
||||
|
||||
Without the `<w:del/>` in `<w:pPr><w:rPr>`, accepting changes leaves an empty paragraph/list item.
|
||||
|
||||
**Rejecting another author's insertion** - nest deletion inside their insertion:
|
||||
|
||||
```xml
|
||||
<w:ins w:author="Jane" w:id="5">
|
||||
<w:del w:author="Claude" w:id="10">
|
||||
@@ -409,6 +510,7 @@ Without the `<w:del/>` in `<w:pPr><w:rPr>`, accepting changes leaves an empty pa
|
||||
```
|
||||
|
||||
**Restoring another author's deletion** - add insertion after (don't modify their deletion):
|
||||
|
||||
```xml
|
||||
<w:del w:author="Jane" w:id="5">
|
||||
<w:r><w:delText>deleted text</w:delText></w:r>
|
||||
@@ -448,14 +550,19 @@ After running `comment.py` (see Step 2), add markers to document.xml. For replie
|
||||
|
||||
1. Add image file to `word/media/`
|
||||
2. Add relationship to `word/_rels/document.xml.rels`:
|
||||
|
||||
```xml
|
||||
<Relationship Id="rId5" Type=".../image" Target="media/image1.png"/>
|
||||
```
|
||||
|
||||
3. Add content type to `[Content_Types].xml`:
|
||||
|
||||
```xml
|
||||
<Default Extension="png" ContentType="image/png"/>
|
||||
```
|
||||
|
||||
4. Reference in document.xml:
|
||||
|
||||
```xml
|
||||
<w:drawing>
|
||||
<wp:inline>
|
||||
|
||||
@@ -8,15 +8,15 @@ description: This skill provides guidance and enforcement rules for implementing
|
||||
When enabling email/password authentication, configure `emailVerification.sendVerificationEmail` to verify user email addresses. This helps prevent fake sign-ups and ensures users have access to the email they registered with.
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { sendEmail } from "./email"; // your email sending function
|
||||
import { betterAuth } from 'better-auth';
|
||||
import { sendEmail } from './email'; // your email sending function
|
||||
|
||||
export const auth = betterAuth({
|
||||
emailVerification: {
|
||||
sendVerificationEmail: async ({ user, url, token }, request) => {
|
||||
await sendEmail({
|
||||
to: user.email,
|
||||
subject: "Verify your email address",
|
||||
subject: 'Verify your email address',
|
||||
text: `Click the link to verify your email: ${url}`,
|
||||
});
|
||||
},
|
||||
@@ -53,7 +53,7 @@ Always use absolute URLs (including the origin) for callback URLs in sign-up and
|
||||
|
||||
```ts
|
||||
const { data, error } = await authClient.signUp.email({
|
||||
callbackURL: "https://example.com/callback", // absolute URL with origin
|
||||
callbackURL: 'https://example.com/callback', // absolute URL with origin
|
||||
});
|
||||
```
|
||||
|
||||
@@ -64,8 +64,8 @@ Password reset flows are essential to any email/password system, we recommend se
|
||||
To allow users to reset a password first you need to provide `sendResetPassword` function to the email and password authenticator.
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { sendEmail } from "./email"; // your email sending function
|
||||
import { betterAuth } from 'better-auth';
|
||||
import { sendEmail } from './email'; // your email sending function
|
||||
|
||||
export const auth = betterAuth({
|
||||
emailAndPassword: {
|
||||
@@ -74,7 +74,7 @@ export const auth = betterAuth({
|
||||
sendResetPassword: async ({ user, url, token }, request) => {
|
||||
void sendEmail({
|
||||
to: user.email,
|
||||
subject: "Reset your password",
|
||||
subject: 'Reset your password',
|
||||
text: `Click the link to reset your password: ${url}`,
|
||||
});
|
||||
},
|
||||
@@ -148,6 +148,7 @@ The `redirectTo` parameter is validated against your `trustedOrigins` configurat
|
||||
#### Password requirements
|
||||
|
||||
During password reset, the new password must meet length requirements:
|
||||
|
||||
- **Minimum**: 8 characters (default), configurable via `minPasswordLength`
|
||||
- **Maximum**: 128 characters (default), configurable via `maxPasswordLength`
|
||||
|
||||
@@ -168,8 +169,8 @@ Once the password reset configurations are set-up, you can now call the `request
|
||||
```ts
|
||||
const data = await auth.api.requestPasswordReset({
|
||||
body: {
|
||||
email: "john.doe@example.com", // required
|
||||
redirectTo: "https://example.com/reset-password",
|
||||
email: 'john.doe@example.com', // required
|
||||
redirectTo: 'https://example.com/reset-password',
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -178,8 +179,8 @@ Or authClient:
|
||||
|
||||
```ts
|
||||
const { data, error } = await authClient.requestPasswordReset({
|
||||
email: "john.doe@example.com", // required
|
||||
redirectTo: "https://example.com/reset-password",
|
||||
email: 'john.doe@example.com', // required
|
||||
redirectTo: 'https://example.com/reset-password',
|
||||
});
|
||||
```
|
||||
|
||||
@@ -198,8 +199,8 @@ Better Auth uses `scrypt` by default for password hashing. This is a solid choic
|
||||
To use a different algorithm (e.g., Argon2id), provide custom `hash` and `verify` functions in the `emailAndPassword.password` configuration:
|
||||
|
||||
```ts
|
||||
import { betterAuth } from "better-auth";
|
||||
import { hash, verify, type Options } from "@node-rs/argon2";
|
||||
import { betterAuth } from 'better-auth';
|
||||
import { hash, verify, type Options } from '@node-rs/argon2';
|
||||
|
||||
const argon2Options: Options = {
|
||||
memoryCost: 65536, // 64 MiB
|
||||
@@ -214,8 +215,7 @@ export const auth = betterAuth({
|
||||
enabled: true,
|
||||
password: {
|
||||
hash: (password) => hash(password, argon2Options),
|
||||
verify: ({ password, hash: storedHash }) =>
|
||||
verify(storedHash, password, argon2Options),
|
||||
verify: ({ password, hash: storedHash }) => verify(storedHash, password, argon2Options),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -41,21 +41,25 @@ Before creating a sequence, understand:
|
||||
## Core Principles
|
||||
|
||||
### 1. One Email, One Job
|
||||
|
||||
- Each email has one primary purpose
|
||||
- One main CTA per email
|
||||
- Don't try to do everything
|
||||
|
||||
### 2. Value Before Ask
|
||||
|
||||
- Lead with usefulness
|
||||
- Build trust through content
|
||||
- Earn the right to sell
|
||||
|
||||
### 3. Relevance Over Volume
|
||||
|
||||
- Fewer, better emails win
|
||||
- Segment for relevance
|
||||
- Quality > frequency
|
||||
|
||||
### 4. Clear Path Forward
|
||||
|
||||
- Every email moves them somewhere
|
||||
- Links should do something useful
|
||||
- Make next steps obvious
|
||||
@@ -65,28 +69,33 @@ Before creating a sequence, understand:
|
||||
## Email Sequence Strategy
|
||||
|
||||
### Sequence Length
|
||||
|
||||
- Welcome: 3-7 emails
|
||||
- Lead nurture: 5-10 emails
|
||||
- Onboarding: 5-10 emails
|
||||
- Re-engagement: 3-5 emails
|
||||
|
||||
Depends on:
|
||||
|
||||
- Sales cycle length
|
||||
- Product complexity
|
||||
- Relationship stage
|
||||
|
||||
### Timing/Delays
|
||||
|
||||
- Welcome email: Immediately
|
||||
- Early sequence: 1-2 days apart
|
||||
- Nurture: 2-4 days apart
|
||||
- Long-term: Weekly or bi-weekly
|
||||
|
||||
Consider:
|
||||
|
||||
- B2B: Avoid weekends
|
||||
- B2C: Test weekends
|
||||
- Time zones: Send at local time
|
||||
|
||||
### Subject Line Strategy
|
||||
|
||||
- Clear > Clever
|
||||
- Specific > Vague
|
||||
- Benefit or curiosity-driven
|
||||
@@ -94,6 +103,7 @@ Consider:
|
||||
- Test emoji (they're polarizing)
|
||||
|
||||
**Patterns that work:**
|
||||
|
||||
- Question: "Still struggling with X?"
|
||||
- How-to: "How to [achieve outcome] in [timeframe]"
|
||||
- Number: "3 ways to [benefit]"
|
||||
@@ -101,6 +111,7 @@ Consider:
|
||||
- Story tease: "The mistake I made with [topic]"
|
||||
|
||||
### Preview Text
|
||||
|
||||
- Extends the subject line
|
||||
- ~90-140 characters
|
||||
- Don't repeat subject line
|
||||
@@ -111,10 +122,12 @@ Consider:
|
||||
## Sequence Types Overview
|
||||
|
||||
### Welcome Sequence (Post-Signup)
|
||||
|
||||
**Length**: 5-7 emails over 12-14 days
|
||||
**Goal**: Activate, build trust, convert
|
||||
|
||||
Key emails:
|
||||
|
||||
1. Welcome + deliver promised value (immediate)
|
||||
2. Quick win (day 1-2)
|
||||
3. Story/Why (day 3-4)
|
||||
@@ -124,10 +137,12 @@ Key emails:
|
||||
7. Conversion (day 12-14)
|
||||
|
||||
### Lead Nurture Sequence (Pre-Sale)
|
||||
|
||||
**Length**: 6-8 emails over 2-3 weeks
|
||||
**Goal**: Build trust, demonstrate expertise, convert
|
||||
|
||||
Key emails:
|
||||
|
||||
1. Deliver lead magnet + intro (immediate)
|
||||
2. Expand on topic (day 2-3)
|
||||
3. Problem deep-dive (day 4-5)
|
||||
@@ -138,22 +153,26 @@ Key emails:
|
||||
8. Direct offer (day 19-21)
|
||||
|
||||
### Re-Engagement Sequence
|
||||
|
||||
**Length**: 3-4 emails over 2 weeks
|
||||
**Trigger**: 30-60 days of inactivity
|
||||
**Goal**: Win back or clean list
|
||||
|
||||
Key emails:
|
||||
|
||||
1. Check-in (genuine concern)
|
||||
2. Value reminder (what's new)
|
||||
3. Incentive (special offer)
|
||||
4. Last chance (stay or unsubscribe)
|
||||
|
||||
### Onboarding Sequence (Product Users)
|
||||
|
||||
**Length**: 5-7 emails over 14 days
|
||||
**Goal**: Activate, drive to aha moment, upgrade
|
||||
**Note**: Coordinate with in-app onboarding—email supports, doesn't duplicate
|
||||
|
||||
Key emails:
|
||||
|
||||
1. Welcome + first step (immediate)
|
||||
2. Getting started help (day 1)
|
||||
3. Feature highlight (day 2-3)
|
||||
@@ -169,12 +188,14 @@ Key emails:
|
||||
## Email Types by Category
|
||||
|
||||
### Onboarding Emails
|
||||
|
||||
- New users series
|
||||
- New customers series
|
||||
- Key onboarding step reminders
|
||||
- New user invites
|
||||
|
||||
### Retention Emails
|
||||
|
||||
- Upgrade to paid
|
||||
- Upgrade to higher plan
|
||||
- Ask for review
|
||||
@@ -184,21 +205,25 @@ Key emails:
|
||||
- Referral program
|
||||
|
||||
### Billing Emails
|
||||
|
||||
- Switch to annual
|
||||
- Failed payment recovery
|
||||
- Cancellation survey
|
||||
- Upcoming renewal reminders
|
||||
|
||||
### Usage Emails
|
||||
|
||||
- Daily/weekly/monthly summaries
|
||||
- Key event notifications
|
||||
- Milestone celebrations
|
||||
|
||||
### Win-Back Emails
|
||||
|
||||
- Expired trials
|
||||
- Cancelled customers
|
||||
|
||||
### Campaign Emails
|
||||
|
||||
- Monthly roundup / newsletter
|
||||
- Seasonal promotions
|
||||
- Product updates
|
||||
@@ -212,6 +237,7 @@ Key emails:
|
||||
## Email Copy Guidelines
|
||||
|
||||
### Structure
|
||||
|
||||
1. **Hook**: First line grabs attention
|
||||
2. **Context**: Why this matters to them
|
||||
3. **Value**: The useful content
|
||||
@@ -219,6 +245,7 @@ Key emails:
|
||||
5. **Sign-off**: Human, warm close
|
||||
|
||||
### Formatting
|
||||
|
||||
- Short paragraphs (1-3 sentences)
|
||||
- White space between sections
|
||||
- Bullet points for scanability
|
||||
@@ -226,17 +253,20 @@ Key emails:
|
||||
- Mobile-first (most read on phone)
|
||||
|
||||
### Tone
|
||||
|
||||
- Conversational, not formal
|
||||
- First-person (I/we) and second-person (you)
|
||||
- Active voice
|
||||
- Read it out loud—does it sound human?
|
||||
|
||||
### Length
|
||||
|
||||
- 50-125 words for transactional
|
||||
- 150-300 words for educational
|
||||
- 300-500 words for story-driven
|
||||
|
||||
### CTA Guidelines
|
||||
|
||||
- Buttons for primary actions
|
||||
- Links for secondary actions
|
||||
- One clear primary CTA per email
|
||||
@@ -249,6 +279,7 @@ Key emails:
|
||||
## Output Format
|
||||
|
||||
### Sequence Overview
|
||||
|
||||
```
|
||||
Sequence Name: [Name]
|
||||
Trigger: [What starts the sequence]
|
||||
@@ -259,6 +290,7 @@ Exit Conditions: [When they leave the sequence]
|
||||
```
|
||||
|
||||
### For Each Email
|
||||
|
||||
```
|
||||
Email [#]: [Name/Purpose]
|
||||
Send: [Timing]
|
||||
@@ -270,6 +302,7 @@ Segment/Conditions: [If applicable]
|
||||
```
|
||||
|
||||
### Metrics Plan
|
||||
|
||||
What to measure and benchmarks
|
||||
|
||||
---
|
||||
@@ -288,13 +321,13 @@ What to measure and benchmarks
|
||||
|
||||
For implementation, see the [tools registry](../../tools/REGISTRY.md). Key email tools:
|
||||
|
||||
| Tool | Best For | MCP | Guide |
|
||||
|------|----------|:---:|-------|
|
||||
| **Customer.io** | Behavior-based automation | - | [customer-io.md](../../tools/integrations/customer-io.md) |
|
||||
| **Mailchimp** | SMB email marketing | ✓ | [mailchimp.md](../../tools/integrations/mailchimp.md) |
|
||||
| **Resend** | Developer-friendly transactional | ✓ | [resend.md](../../tools/integrations/resend.md) |
|
||||
| **SendGrid** | Transactional email at scale | - | [sendgrid.md](../../tools/integrations/sendgrid.md) |
|
||||
| **Kit** | Creator/newsletter focused | - | [kit.md](../../tools/integrations/kit.md) |
|
||||
| Tool | Best For | MCP | Guide |
|
||||
| --------------- | -------------------------------- | :-: | --------------------------------------------------------- |
|
||||
| **Customer.io** | Behavior-based automation | - | [customer-io.md](../../tools/integrations/customer-io.md) |
|
||||
| **Mailchimp** | SMB email marketing | ✓ | [mailchimp.md](../../tools/integrations/mailchimp.md) |
|
||||
| **Resend** | Developer-friendly transactional | ✓ | [resend.md](../../tools/integrations/resend.md) |
|
||||
| **SendGrid** | Transactional email at scale | - | [sendgrid.md](../../tools/integrations/sendgrid.md) |
|
||||
| **Kit** | Creator/newsletter focused | - | [kit.md](../../tools/integrations/kit.md) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -44,16 +44,19 @@
|
||||
## Personalization
|
||||
|
||||
### Merge Fields
|
||||
|
||||
- First name (fallback to "there" or "friend")
|
||||
- Company name (B2B)
|
||||
- Relevant data (usage, plan, etc.)
|
||||
|
||||
### Dynamic Content
|
||||
|
||||
- Based on segment
|
||||
- Based on behavior
|
||||
- Based on stage
|
||||
|
||||
### Triggered Emails
|
||||
|
||||
- Action-based sends
|
||||
- More relevant than time-based
|
||||
- Examples: Feature used, milestone hit, inactivity
|
||||
@@ -63,16 +66,19 @@
|
||||
## Segmentation Strategies
|
||||
|
||||
### By Behavior
|
||||
|
||||
- Openers vs. non-openers
|
||||
- Clickers vs. non-clickers
|
||||
- Active vs. inactive
|
||||
|
||||
### By Stage
|
||||
|
||||
- Trial vs. paid
|
||||
- New vs. long-term
|
||||
- Engaged vs. at-risk
|
||||
|
||||
### By Profile
|
||||
|
||||
- Industry/role (B2B)
|
||||
- Use case / goal
|
||||
- Company size
|
||||
@@ -82,6 +88,7 @@
|
||||
## Testing and Optimization
|
||||
|
||||
### What to Test
|
||||
|
||||
- Subject lines (highest impact)
|
||||
- Send times
|
||||
- Email length
|
||||
@@ -90,12 +97,14 @@
|
||||
- Sequence timing
|
||||
|
||||
### How to Test
|
||||
|
||||
- A/B test one variable at a time
|
||||
- Sufficient sample size
|
||||
- Statistical significance
|
||||
- Document learnings
|
||||
|
||||
### Metrics to Track
|
||||
|
||||
- Open rate (benchmark: 20-40%)
|
||||
- Click rate (benchmark: 2-5%)
|
||||
- Unsubscribe rate (keep under 0.5%)
|
||||
|
||||
@@ -5,6 +5,7 @@ A comprehensive guide to lifecycle and campaign emails. Use this as an audit che
|
||||
## Onboarding Emails
|
||||
|
||||
### New Users Series
|
||||
|
||||
**Trigger**: User signs up (free or trial)
|
||||
**Goal**: Activate user, drive to aha moment
|
||||
**Typical sequence**: 5-7 emails over 14 days
|
||||
@@ -22,6 +23,7 @@ A comprehensive guide to lifecycle and campaign emails. Use this as an audit che
|
||||
---
|
||||
|
||||
### New Customers Series
|
||||
|
||||
**Trigger**: User converts to paid
|
||||
**Goal**: Reinforce purchase decision, drive adoption, reduce early churn
|
||||
**Typical sequence**: 3-5 emails over 14 days
|
||||
@@ -37,16 +39,19 @@ A comprehensive guide to lifecycle and campaign emails. Use this as an audit che
|
||||
---
|
||||
|
||||
### Key Onboarding Step Reminder
|
||||
|
||||
**Trigger**: User hasn't completed critical setup step after X time
|
||||
**Goal**: Nudge completion of high-value action
|
||||
**Format**: Single email or 2-3 email mini-sequence
|
||||
|
||||
**Example triggers**:
|
||||
|
||||
- Hasn't connected integration after 48 hours
|
||||
- Hasn't invited team member after 3 days
|
||||
- Hasn't completed profile after 24 hours
|
||||
|
||||
**Copy approach**:
|
||||
|
||||
- Remind them what they started
|
||||
- Explain why this step matters
|
||||
- Make it easy (direct link to complete)
|
||||
@@ -55,6 +60,7 @@ A comprehensive guide to lifecycle and campaign emails. Use this as an audit che
|
||||
---
|
||||
|
||||
### New User Invite
|
||||
|
||||
**Trigger**: Existing user invites teammate
|
||||
**Goal**: Activate the invited user
|
||||
**Recipient**: The person being invited
|
||||
@@ -64,6 +70,7 @@ A comprehensive guide to lifecycle and campaign emails. Use this as an audit che
|
||||
- Email 3: Final reminder (day 5)
|
||||
|
||||
**Copy approach**:
|
||||
|
||||
- Personalize with inviter's name
|
||||
- Explain what they're joining
|
||||
- Single CTA to accept invite
|
||||
@@ -74,16 +81,19 @@ A comprehensive guide to lifecycle and campaign emails. Use this as an audit che
|
||||
## Retention Emails
|
||||
|
||||
### Upgrade to Paid
|
||||
|
||||
**Trigger**: Free user shows engagement, or trial ending
|
||||
**Goal**: Convert free to paid
|
||||
**Typical sequence**: 3-5 emails
|
||||
|
||||
**Trigger options**:
|
||||
|
||||
- Time-based (trial day 10, 12, 14)
|
||||
- Behavior-based (hit usage limit, used premium feature)
|
||||
- Engagement-based (highly active free user)
|
||||
|
||||
**Sequence structure**:
|
||||
|
||||
- Value summary: What they've accomplished
|
||||
- Feature comparison: What they're missing
|
||||
- Social proof: Who else upgraded
|
||||
@@ -93,17 +103,20 @@ A comprehensive guide to lifecycle and campaign emails. Use this as an audit che
|
||||
---
|
||||
|
||||
### Upgrade to Higher Plan
|
||||
|
||||
**Trigger**: User approaching plan limits or using features available on higher tier
|
||||
**Goal**: Upsell to next tier
|
||||
**Format**: Single email or 2-3 email sequence
|
||||
|
||||
**Trigger examples**:
|
||||
|
||||
- 80% of seat limit reached
|
||||
- 90% of storage/usage limit
|
||||
- Tried to use higher-tier feature
|
||||
- Power user behavior patterns
|
||||
|
||||
**Copy approach**:
|
||||
|
||||
- Acknowledge their growth (positive framing)
|
||||
- Show what next tier unlocks
|
||||
- Quantify value vs. cost
|
||||
@@ -112,17 +125,20 @@ A comprehensive guide to lifecycle and campaign emails. Use this as an audit che
|
||||
---
|
||||
|
||||
### Ask for Review
|
||||
|
||||
**Trigger**: Customer milestone (30/60/90 days, key achievement, support resolution)
|
||||
**Goal**: Generate social proof on G2, Capterra, app stores
|
||||
**Format**: Single email
|
||||
|
||||
**Best timing**:
|
||||
|
||||
- After positive support interaction
|
||||
- After achieving measurable result
|
||||
- After renewal
|
||||
- NOT after billing issues or bugs
|
||||
|
||||
**Copy approach**:
|
||||
|
||||
- Thank them for being a customer
|
||||
- Mention specific value/milestone if possible
|
||||
- Explain why reviews matter (help others decide)
|
||||
@@ -132,17 +148,20 @@ A comprehensive guide to lifecycle and campaign emails. Use this as an audit che
|
||||
---
|
||||
|
||||
### Offer Support Proactively
|
||||
|
||||
**Trigger**: Signs of struggle (drop in usage, failed actions, error encounters)
|
||||
**Goal**: Save at-risk user, improve experience
|
||||
**Format**: Single email
|
||||
|
||||
**Trigger examples**:
|
||||
|
||||
- Usage dropped significantly week-over-week
|
||||
- Multiple failed attempts at action
|
||||
- Viewed help docs repeatedly
|
||||
- Stuck at same onboarding step
|
||||
|
||||
**Copy approach**:
|
||||
|
||||
- Genuine concern tone
|
||||
- Specific: "I noticed you..." (if data allows)
|
||||
- Offer direct help (not just link to docs)
|
||||
@@ -152,11 +171,13 @@ A comprehensive guide to lifecycle and campaign emails. Use this as an audit che
|
||||
---
|
||||
|
||||
### Product Usage Report
|
||||
|
||||
**Trigger**: Time-based (weekly, monthly, quarterly)
|
||||
**Goal**: Demonstrate value, drive engagement, reduce churn
|
||||
**Format**: Single email, recurring
|
||||
|
||||
**What to include**:
|
||||
|
||||
- Key metrics/activity summary
|
||||
- Comparison to previous period
|
||||
- Achievements/milestones
|
||||
@@ -164,6 +185,7 @@ A comprehensive guide to lifecycle and campaign emails. Use this as an audit che
|
||||
- Light CTA to explore more
|
||||
|
||||
**Examples**:
|
||||
|
||||
- "You saved X hours this month"
|
||||
- "Your team completed X projects"
|
||||
- "You're in the top X% of users"
|
||||
@@ -173,17 +195,20 @@ A comprehensive guide to lifecycle and campaign emails. Use this as an audit che
|
||||
---
|
||||
|
||||
### NPS Survey
|
||||
|
||||
**Trigger**: Time-based (quarterly) or event-based (post-milestone)
|
||||
**Goal**: Measure satisfaction, identify promoters and detractors
|
||||
**Format**: Single email
|
||||
|
||||
**Best practices**:
|
||||
|
||||
- Keep it simple: Just the NPS question initially
|
||||
- Follow-up form for "why" based on score
|
||||
- Personal sender (CEO, founder, CSM)
|
||||
- Tell them how you'll use feedback
|
||||
|
||||
**Follow-up based on score**:
|
||||
|
||||
- Promoters (9-10): Thank + ask for review/referral
|
||||
- Passives (7-8): Ask what would make it a 10
|
||||
- Detractors (0-6): Personal outreach to understand issues
|
||||
@@ -191,17 +216,20 @@ A comprehensive guide to lifecycle and campaign emails. Use this as an audit che
|
||||
---
|
||||
|
||||
### Referral Program
|
||||
|
||||
**Trigger**: Customer milestone, promoter NPS score, or campaign
|
||||
**Goal**: Generate referrals
|
||||
**Format**: Single email or periodic reminders
|
||||
|
||||
**Good timing**:
|
||||
|
||||
- After positive NPS response
|
||||
- After customer achieves result
|
||||
- After renewal
|
||||
- Seasonal campaigns
|
||||
|
||||
**Copy approach**:
|
||||
|
||||
- Remind them of their success
|
||||
- Explain the referral offer clearly
|
||||
- Make sharing easy (unique link)
|
||||
@@ -212,17 +240,20 @@ A comprehensive guide to lifecycle and campaign emails. Use this as an audit che
|
||||
## Billing Emails
|
||||
|
||||
### Switch to Annual
|
||||
|
||||
**Trigger**: Monthly subscriber at renewal time or campaign
|
||||
**Goal**: Convert monthly to annual (improve LTV, reduce churn)
|
||||
**Format**: Single email or 2-email sequence
|
||||
|
||||
**Value proposition**:
|
||||
|
||||
- Calculate exact savings
|
||||
- Additional benefits (if any)
|
||||
- Lock in current price messaging
|
||||
- Easy one-click switch
|
||||
|
||||
**Best timing**:
|
||||
|
||||
- Around monthly renewal date
|
||||
- End of year / new year
|
||||
- After 3-6 months of loyalty
|
||||
@@ -231,17 +262,20 @@ A comprehensive guide to lifecycle and campaign emails. Use this as an audit che
|
||||
---
|
||||
|
||||
### Failed Payment Recovery
|
||||
|
||||
**Trigger**: Payment fails
|
||||
**Goal**: Recover revenue, retain customer
|
||||
**Typical sequence**: 3-4 emails over 7-14 days
|
||||
|
||||
**Sequence structure**:
|
||||
|
||||
- Email 1 (Day 0): Friendly notice, update payment link
|
||||
- Email 2 (Day 3): Reminder, service may be interrupted
|
||||
- Email 3 (Day 7): Urgent, account will be suspended
|
||||
- Email 4 (Day 10-14): Final notice, what they'll lose
|
||||
|
||||
**Copy approach**:
|
||||
|
||||
- Assume it's an accident (card expired, etc.)
|
||||
- Clear, direct, no guilt
|
||||
- Single CTA to update payment
|
||||
@@ -252,16 +286,19 @@ A comprehensive guide to lifecycle and campaign emails. Use this as an audit che
|
||||
---
|
||||
|
||||
### Cancellation Survey
|
||||
|
||||
**Trigger**: User cancels subscription
|
||||
**Goal**: Learn why, opportunity to save
|
||||
**Format**: Single email (immediate)
|
||||
|
||||
**Options**:
|
||||
|
||||
- In-app survey at cancellation (better completion)
|
||||
- Follow-up email if they skip in-app
|
||||
- Personal outreach for high-value accounts
|
||||
|
||||
**Questions to ask**:
|
||||
|
||||
- Primary reason for cancelling
|
||||
- What could we have done better
|
||||
- Would anything change your mind
|
||||
@@ -272,11 +309,13 @@ A comprehensive guide to lifecycle and campaign emails. Use this as an audit che
|
||||
---
|
||||
|
||||
### Upcoming Renewal Reminder
|
||||
|
||||
**Trigger**: X days before renewal (14 or 30 days typical)
|
||||
**Goal**: No surprise charges, opportunity to expand
|
||||
**Format**: Single email
|
||||
|
||||
**What to include**:
|
||||
|
||||
- Renewal date and amount
|
||||
- What's included in renewal
|
||||
- How to update payment/plan
|
||||
@@ -290,16 +329,19 @@ A comprehensive guide to lifecycle and campaign emails. Use this as an audit che
|
||||
## Usage Emails
|
||||
|
||||
### Daily/Weekly/Monthly Summary
|
||||
|
||||
**Trigger**: Time-based
|
||||
**Goal**: Drive engagement, demonstrate value
|
||||
**Format**: Single email, recurring
|
||||
|
||||
**Content by frequency**:
|
||||
|
||||
- **Daily**: Notifications, quick stats (for high-engagement products)
|
||||
- **Weekly**: Activity summary, highlights, suggestions
|
||||
- **Monthly**: Comprehensive report, achievements, ROI if calculable
|
||||
|
||||
**Structure**:
|
||||
|
||||
- Key metrics at a glance
|
||||
- Notable achievements
|
||||
- Activity breakdown
|
||||
@@ -311,11 +353,13 @@ A comprehensive guide to lifecycle and campaign emails. Use this as an audit che
|
||||
---
|
||||
|
||||
### Key Event or Milestone Notifications
|
||||
|
||||
**Trigger**: Specific achievement or event
|
||||
**Goal**: Celebrate, drive continued engagement
|
||||
**Format**: Single email per event
|
||||
|
||||
**Milestone examples**:
|
||||
|
||||
- First [action] completed
|
||||
- 10th/100th [thing] created
|
||||
- Goal achieved
|
||||
@@ -323,6 +367,7 @@ A comprehensive guide to lifecycle and campaign emails. Use this as an audit che
|
||||
- Usage streak
|
||||
|
||||
**Copy approach**:
|
||||
|
||||
- Celebration tone
|
||||
- Specific achievement
|
||||
- Context (compared to others, compared to before)
|
||||
@@ -333,17 +378,20 @@ A comprehensive guide to lifecycle and campaign emails. Use this as an audit che
|
||||
## Win-Back Emails
|
||||
|
||||
### Expired Trials
|
||||
|
||||
**Trigger**: Trial ended without conversion
|
||||
**Goal**: Convert or re-engage
|
||||
**Typical sequence**: 3-4 emails over 30 days
|
||||
|
||||
**Sequence structure**:
|
||||
|
||||
- Email 1 (Day 1 post-expiry): Trial ended, here's what you're missing
|
||||
- Email 2 (Day 7): What held you back? (gather feedback)
|
||||
- Email 3 (Day 14): Incentive offer (discount, extended trial)
|
||||
- Email 4 (Day 30): Final reach-out, door is open
|
||||
|
||||
**Segmentation**: Different approach based on trial engagement level:
|
||||
|
||||
- High engagement: Focus on removing friction to convert
|
||||
- Low engagement: Offer fresh start, more onboarding help
|
||||
- No engagement: Ask what happened, offer demo/call
|
||||
@@ -351,16 +399,19 @@ A comprehensive guide to lifecycle and campaign emails. Use this as an audit che
|
||||
---
|
||||
|
||||
### Cancelled Customers
|
||||
|
||||
**Trigger**: Time after cancellation (30, 60, 90 days)
|
||||
**Goal**: Win back churned customers
|
||||
**Typical sequence**: 2-3 emails spread over 90 days
|
||||
|
||||
**Sequence structure**:
|
||||
|
||||
- Email 1 (Day 30): What's new since you left
|
||||
- Email 2 (Day 60): We've addressed [common reason]
|
||||
- Email 3 (Day 90): Special offer to return
|
||||
|
||||
**Copy approach**:
|
||||
|
||||
- No guilt, no desperation
|
||||
- Genuine updates and improvements
|
||||
- Personalize based on cancellation reason if known
|
||||
@@ -373,11 +424,13 @@ A comprehensive guide to lifecycle and campaign emails. Use this as an audit che
|
||||
## Campaign Emails
|
||||
|
||||
### Monthly Roundup / Newsletter
|
||||
|
||||
**Trigger**: Time-based (monthly)
|
||||
**Goal**: Engagement, brand presence, content distribution
|
||||
**Format**: Single email, recurring
|
||||
|
||||
**Content mix**:
|
||||
|
||||
- Product updates and tips
|
||||
- Customer stories
|
||||
- Educational content
|
||||
@@ -385,6 +438,7 @@ A comprehensive guide to lifecycle and campaign emails. Use this as an audit che
|
||||
- Industry insights
|
||||
|
||||
**Best practices**:
|
||||
|
||||
- Consistent send day/time
|
||||
- Scannable format
|
||||
- Mix of content types
|
||||
@@ -394,11 +448,13 @@ A comprehensive guide to lifecycle and campaign emails. Use this as an audit che
|
||||
---
|
||||
|
||||
### Seasonal Promotions
|
||||
|
||||
**Trigger**: Calendar events (Black Friday, New Year, etc.)
|
||||
**Goal**: Drive conversions with timely offer
|
||||
**Format**: Campaign burst (2-4 emails)
|
||||
|
||||
**Common opportunities**:
|
||||
|
||||
- New Year (fresh start, annual planning)
|
||||
- End of fiscal year (budget spending)
|
||||
- Black Friday / Cyber Monday
|
||||
@@ -406,6 +462,7 @@ A comprehensive guide to lifecycle and campaign emails. Use this as an audit che
|
||||
- Back to school / work
|
||||
|
||||
**Sequence structure**:
|
||||
|
||||
- Announcement: Offer reveal
|
||||
- Reminder: Midway through promotion
|
||||
- Last chance: Final hours
|
||||
@@ -413,17 +470,20 @@ A comprehensive guide to lifecycle and campaign emails. Use this as an audit che
|
||||
---
|
||||
|
||||
### Product Updates
|
||||
|
||||
**Trigger**: New feature release
|
||||
**Goal**: Adoption, engagement, demonstrate momentum
|
||||
**Format**: Single email per major release
|
||||
|
||||
**What to include**:
|
||||
|
||||
- What's new (clear and simple)
|
||||
- Why it matters (benefit, not just feature)
|
||||
- How to use it (direct link)
|
||||
- Who asked for it (community acknowledgment)
|
||||
|
||||
**Segmentation**: Consider targeting based on relevance:
|
||||
|
||||
- Users who would benefit most
|
||||
- Users who requested feature
|
||||
- Power users first (for beta feel)
|
||||
@@ -431,11 +491,13 @@ A comprehensive guide to lifecycle and campaign emails. Use this as an audit che
|
||||
---
|
||||
|
||||
### Industry News Roundup
|
||||
|
||||
**Trigger**: Time-based (weekly or monthly)
|
||||
**Goal**: Thought leadership, engagement, brand value
|
||||
**Format**: Curated newsletter
|
||||
|
||||
**Content**:
|
||||
|
||||
- Curated news and links
|
||||
- Your take / commentary
|
||||
- What it means for readers
|
||||
@@ -446,16 +508,19 @@ A comprehensive guide to lifecycle and campaign emails. Use this as an audit che
|
||||
---
|
||||
|
||||
### Pricing Update
|
||||
|
||||
**Trigger**: Price change announcement
|
||||
**Goal**: Transparent communication, minimize churn
|
||||
**Format**: Single email (or sequence for major changes)
|
||||
|
||||
**Timeline**:
|
||||
|
||||
- Announce 30-60 days before change
|
||||
- Reminder 14 days before
|
||||
- Final notice 7 days before
|
||||
|
||||
**Copy approach**:
|
||||
|
||||
- Clear, direct, transparent
|
||||
- Explain the why (value delivered, costs increased)
|
||||
- Grandfather if possible (lock in old rate)
|
||||
@@ -470,12 +535,14 @@ A comprehensive guide to lifecycle and campaign emails. Use this as an audit che
|
||||
Use this to audit your current email program:
|
||||
|
||||
### Onboarding
|
||||
|
||||
- [ ] New users series
|
||||
- [ ] New customers series
|
||||
- [ ] Key onboarding step reminders
|
||||
- [ ] New user invite sequence
|
||||
|
||||
### Retention
|
||||
|
||||
- [ ] Upgrade to paid sequence
|
||||
- [ ] Upgrade to higher plan triggers
|
||||
- [ ] Ask for review (timed properly)
|
||||
@@ -485,21 +552,25 @@ Use this to audit your current email program:
|
||||
- [ ] Referral program emails
|
||||
|
||||
### Billing
|
||||
|
||||
- [ ] Switch to annual campaign
|
||||
- [ ] Failed payment recovery sequence
|
||||
- [ ] Cancellation survey
|
||||
- [ ] Upcoming renewal reminders
|
||||
|
||||
### Usage
|
||||
|
||||
- [ ] Daily/weekly/monthly summaries
|
||||
- [ ] Key event notifications
|
||||
- [ ] Milestone celebrations
|
||||
|
||||
### Win-Back
|
||||
|
||||
- [ ] Expired trial sequence
|
||||
- [ ] Cancelled customer sequence
|
||||
|
||||
### Campaigns
|
||||
|
||||
- [ ] Monthly roundup / newsletter
|
||||
- [ ] Seasonal promotion calendar
|
||||
- [ ] Product update announcements
|
||||
|
||||
@@ -5,42 +5,49 @@ Detailed templates for common email sequences.
|
||||
## Welcome Sequence (Post-Signup)
|
||||
|
||||
**Email 1: Welcome (Immediate)**
|
||||
|
||||
- Subject: Welcome to [Product] — here's your first step
|
||||
- Deliver what was promised (lead magnet, access, etc.)
|
||||
- Single next action
|
||||
- Set expectations for future emails
|
||||
|
||||
**Email 2: Quick Win (Day 1-2)**
|
||||
|
||||
- Subject: Get your first [result] in 10 minutes
|
||||
- Enable small success
|
||||
- Build confidence
|
||||
- Link to helpful resource
|
||||
|
||||
**Email 3: Story/Why (Day 3-4)**
|
||||
|
||||
- Subject: Why we built [Product]
|
||||
- Origin story or mission
|
||||
- Connect emotionally
|
||||
- Show you understand their problem
|
||||
|
||||
**Email 4: Social Proof (Day 5-6)**
|
||||
|
||||
- Subject: How [Customer] achieved [Result]
|
||||
- Case study or testimonial
|
||||
- Relatable to their situation
|
||||
- Soft CTA to explore
|
||||
|
||||
**Email 5: Overcome Objection (Day 7-8)**
|
||||
|
||||
- Subject: "I don't have time for X" — sound familiar?
|
||||
- Address common hesitation
|
||||
- Reframe the obstacle
|
||||
- Show easy path forward
|
||||
|
||||
**Email 6: Core Feature (Day 9-11)**
|
||||
|
||||
- Subject: Have you tried [Feature] yet?
|
||||
- Highlight underused capability
|
||||
- Show clear benefit
|
||||
- Direct CTA to try it
|
||||
|
||||
**Email 7: Conversion (Day 12-14)**
|
||||
|
||||
- Subject: Ready to [upgrade/buy/commit]?
|
||||
- Summarize value
|
||||
- Clear offer
|
||||
@@ -52,41 +59,49 @@ Detailed templates for common email sequences.
|
||||
## Lead Nurture Sequence (Pre-Sale)
|
||||
|
||||
**Email 1: Deliver + Introduce (Immediate)**
|
||||
|
||||
- Deliver the lead magnet
|
||||
- Brief intro to who you are
|
||||
- Preview what's coming
|
||||
|
||||
**Email 2: Expand on Topic (Day 2-3)**
|
||||
|
||||
- Related insight to lead magnet
|
||||
- Establish expertise
|
||||
- Light CTA to content
|
||||
|
||||
**Email 3: Problem Deep-Dive (Day 4-5)**
|
||||
|
||||
- Articulate their problem deeply
|
||||
- Show you understand
|
||||
- Hint at solution
|
||||
|
||||
**Email 4: Solution Framework (Day 6-8)**
|
||||
|
||||
- Your approach/methodology
|
||||
- Educational, not salesy
|
||||
- Builds toward your product
|
||||
|
||||
**Email 5: Case Study (Day 9-11)**
|
||||
|
||||
- Real results from real customer
|
||||
- Specific and relatable
|
||||
- Soft CTA
|
||||
|
||||
**Email 6: Differentiation (Day 12-14)**
|
||||
|
||||
- Why your approach is different
|
||||
- Address alternatives
|
||||
- Build preference
|
||||
|
||||
**Email 7: Objection Handler (Day 15-18)**
|
||||
|
||||
- Common concern addressed
|
||||
- FAQ or myth-busting
|
||||
- Reduce friction
|
||||
|
||||
**Email 8: Direct Offer (Day 19-21)**
|
||||
|
||||
- Clear pitch
|
||||
- Strong value proposition
|
||||
- Specific CTA
|
||||
@@ -97,24 +112,28 @@ Detailed templates for common email sequences.
|
||||
## Re-Engagement Sequence
|
||||
|
||||
**Email 1: Check-In (Day 30-60 of inactivity)**
|
||||
|
||||
- Subject: Is everything okay, [Name]?
|
||||
- Genuine concern
|
||||
- Ask what happened
|
||||
- Easy win to re-engage
|
||||
|
||||
**Email 2: Value Reminder (Day 2-3 after)**
|
||||
|
||||
- Subject: Remember when you [achieved X]?
|
||||
- Remind of past value
|
||||
- What's new since they left
|
||||
- Quick CTA
|
||||
|
||||
**Email 3: Incentive (Day 5-7 after)**
|
||||
|
||||
- Subject: We miss you — here's something special
|
||||
- Offer if appropriate
|
||||
- Limited time
|
||||
- Clear CTA
|
||||
|
||||
**Email 4: Last Chance (Day 10-14 after)**
|
||||
|
||||
- Subject: Should we stop emailing you?
|
||||
- Honest and direct
|
||||
- One-click to stay or go
|
||||
@@ -127,36 +146,43 @@ Detailed templates for common email sequences.
|
||||
Coordinate with in-app onboarding. Email supports, doesn't duplicate.
|
||||
|
||||
**Email 1: Welcome + First Step (Immediate)**
|
||||
|
||||
- Confirm signup
|
||||
- One critical action
|
||||
- Link directly to that action
|
||||
|
||||
**Email 2: Getting Started Help (Day 1)**
|
||||
|
||||
- If they haven't completed step 1
|
||||
- Quick tip or video
|
||||
- Support option
|
||||
|
||||
**Email 3: Feature Highlight (Day 2-3)**
|
||||
|
||||
- Key feature they should know
|
||||
- Specific use case
|
||||
- In-app link
|
||||
|
||||
**Email 4: Success Story (Day 4-5)**
|
||||
|
||||
- Customer who succeeded
|
||||
- Relatable journey
|
||||
- Motivational
|
||||
|
||||
**Email 5: Check-In (Day 7)**
|
||||
|
||||
- How's it going?
|
||||
- Ask for feedback
|
||||
- Offer help
|
||||
|
||||
**Email 6: Advanced Tip (Day 10-12)**
|
||||
|
||||
- Power feature
|
||||
- For engaged users
|
||||
- Level-up content
|
||||
|
||||
**Email 7: Upgrade/Expand (Day 14+)**
|
||||
|
||||
- For trial users: conversion push
|
||||
- For free users: upgrade prompt
|
||||
- For paid: expansion opportunity
|
||||
|
||||
@@ -16,28 +16,35 @@ Load plan, review critically, execute tasks in batches, report for review betwee
|
||||
## The Process
|
||||
|
||||
### Step 1: Load and Review Plan
|
||||
|
||||
1. Read plan file
|
||||
2. Review critically - identify any questions or concerns about the plan
|
||||
3. If concerns: Raise them with your human partner before starting
|
||||
4. If no concerns: Create TodoWrite and proceed
|
||||
|
||||
### Step 2: Execute Batch
|
||||
|
||||
**Default: First 3 tasks**
|
||||
|
||||
For each task:
|
||||
|
||||
1. Mark as in_progress
|
||||
2. Follow each step exactly (plan has bite-sized steps)
|
||||
3. Run verifications as specified
|
||||
4. Mark as completed
|
||||
|
||||
### Step 3: Report
|
||||
|
||||
When batch complete:
|
||||
|
||||
- Show what was implemented
|
||||
- Show verification output
|
||||
- Say: "Ready for feedback."
|
||||
|
||||
### Step 4: Continue
|
||||
|
||||
Based on feedback:
|
||||
|
||||
- Apply changes if needed
|
||||
- Execute next batch
|
||||
- Repeat until complete
|
||||
@@ -45,6 +52,7 @@ Based on feedback:
|
||||
### Step 5: Complete Development
|
||||
|
||||
After all tasks complete and verified:
|
||||
|
||||
- Announce: "I'm using the finishing-a-development-branch skill to complete this work."
|
||||
- **REQUIRED SUB-SKILL:** Use superpowers:finishing-a-development-branch
|
||||
- Follow that skill to verify tests, present options, execute choice
|
||||
@@ -52,6 +60,7 @@ After all tasks complete and verified:
|
||||
## When to Stop and Ask for Help
|
||||
|
||||
**STOP executing immediately when:**
|
||||
|
||||
- Hit a blocker mid-batch (missing dependency, test fails, instruction unclear)
|
||||
- Plan has critical gaps preventing starting
|
||||
- You don't understand an instruction
|
||||
@@ -62,12 +71,14 @@ After all tasks complete and verified:
|
||||
## When to Revisit Earlier Steps
|
||||
|
||||
**Return to Review (Step 1) when:**
|
||||
|
||||
- Partner updates the plan based on your feedback
|
||||
- Fundamental approach needs rethinking
|
||||
|
||||
**Don't force through blockers** - stop and ask.
|
||||
|
||||
## Remember
|
||||
|
||||
- Review plan critically first
|
||||
- Follow plan steps exactly
|
||||
- Don't skip verifications
|
||||
@@ -79,6 +90,7 @@ After all tasks complete and verified:
|
||||
## Integration
|
||||
|
||||
**Required workflow skills:**
|
||||
|
||||
- **superpowers:using-git-worktrees** - REQUIRED: Set up isolated workspace before starting
|
||||
- **superpowers:writing-plans** - Creates the plan this skill executes
|
||||
- **superpowers:finishing-a-development-branch** - Complete development after all tasks
|
||||
|
||||
@@ -12,6 +12,7 @@ user-invocable: true
|
||||
Production-tested patterns for FastAPI with Pydantic v2, SQLAlchemy 2.0 async, and JWT authentication.
|
||||
|
||||
**Latest Versions** (verified January 2026):
|
||||
|
||||
- FastAPI: 0.128.0
|
||||
- Pydantic: 2.11.7
|
||||
- SQLAlchemy: 2.0.30
|
||||
@@ -19,6 +20,7 @@ Production-tested patterns for FastAPI with Pydantic v2, SQLAlchemy 2.0 async, a
|
||||
- python-jose: 3.3.0
|
||||
|
||||
**Requirements**:
|
||||
|
||||
- Python 3.9+ (Python 3.8 support dropped in FastAPI 0.125.0)
|
||||
- Pydantic v2.7.0+ (Pydantic v1 support completely removed in FastAPI 0.128.0)
|
||||
|
||||
@@ -143,6 +145,7 @@ class ItemResponse(ItemBase):
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
|
||||
- Use `Field()` for validation constraints
|
||||
- Separate Create/Update/Response schemas
|
||||
- `from_attributes=True` enables SQLAlchemy model conversion
|
||||
@@ -478,6 +481,7 @@ settings = Settings()
|
||||
```
|
||||
|
||||
Create `.env`:
|
||||
|
||||
```
|
||||
DATABASE_URL=sqlite+aiosqlite:///./database.db
|
||||
SECRET_KEY=your-super-secret-key-here
|
||||
@@ -517,6 +521,7 @@ This skill prevents **7** documented issues from official FastAPI GitHub and rel
|
||||
**Why It Happens**: Form data parsing preloads default values and passes them to the validator, making it impossible to distinguish between fields explicitly set by the user and fields using defaults. This bug ONLY affects Form data, not JSON body data.
|
||||
|
||||
**Prevention**:
|
||||
|
||||
```python
|
||||
# ✗ AVOID: Pydantic model with Form when you need field_set metadata
|
||||
from typing import Annotated
|
||||
@@ -547,6 +552,7 @@ async def endpoint(model: MyModel):
|
||||
**Why It Happens**: When you return a custom `Response` with a `background` parameter, it overwrites all tasks added to the injected `BackgroundTasks` dependency. This is not documented and causes silent failures.
|
||||
|
||||
**Prevention**:
|
||||
|
||||
```python
|
||||
# ✗ WRONG: Mixing both mechanisms
|
||||
from fastapi import BackgroundTasks
|
||||
@@ -585,6 +591,7 @@ async def endpoint():
|
||||
**Why It Happens**: Starting in FastAPI 0.114.0, optional form fields with `Literal` types fail validation when passed `None` via TestClient. Worked in 0.113.0.
|
||||
|
||||
**Prevention**:
|
||||
|
||||
```python
|
||||
from typing import Annotated, Literal, Optional
|
||||
from fastapi import Form
|
||||
@@ -620,6 +627,7 @@ async def endpoint(attribute: Annotated[str | None, Form()] = None):
|
||||
**Why It Happens**: Using Pydantic's `Json` type directly with `Form()` fails. You must accept the field as `str` and parse manually.
|
||||
|
||||
**Prevention**:
|
||||
|
||||
```python
|
||||
from typing import Annotated
|
||||
from fastapi import Form
|
||||
@@ -647,6 +655,7 @@ async def working(json_list: Annotated[str, Form()]) -> list[str]:
|
||||
**Why It Happens**: When using `Annotated` with `Depends()` and a forward reference (from `__future__ import annotations`), OpenAPI schema generation fails or produces incorrect schemas.
|
||||
|
||||
**Prevention**:
|
||||
|
||||
```python
|
||||
# ✗ PROBLEMATIC: Forward reference with Depends
|
||||
from __future__ import annotations
|
||||
@@ -693,6 +702,7 @@ def get_potato() -> Potato: # Now works ✓
|
||||
**Why It Happens**: Major breaking change when migrating from Pydantic v1 to v2. Union types with `str` in path/query parameters now always parse as `str` (worked correctly in v1).
|
||||
|
||||
**Prevention**:
|
||||
|
||||
```python
|
||||
from uuid import UUID
|
||||
|
||||
@@ -734,6 +744,7 @@ class PathParams(BaseModel):
|
||||
**Why It Happens**: When raising `ValueError` inside a Pydantic `@field_validator` with Form fields, FastAPI returns 500 Internal Server Error instead of the expected 422 Unprocessable Entity validation error.
|
||||
|
||||
**Prevention**:
|
||||
|
||||
```python
|
||||
from typing import Annotated
|
||||
from fastapi import Form
|
||||
@@ -773,11 +784,13 @@ class MyForm(BaseModel):
|
||||
**Cause**: Request body doesn't match Pydantic schema
|
||||
|
||||
**Debug**:
|
||||
|
||||
1. Check `/docs` endpoint - test there first
|
||||
2. Verify JSON structure matches schema
|
||||
3. Check required vs optional fields
|
||||
|
||||
**Fix**: Add custom validation error handler:
|
||||
|
||||
```python
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.responses import JSONResponse
|
||||
@@ -795,6 +808,7 @@ async def validation_exception_handler(request, exc):
|
||||
**Cause**: Missing or misconfigured CORS middleware
|
||||
|
||||
**Fix**:
|
||||
|
||||
```python
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
@@ -810,6 +824,7 @@ app.add_middleware(
|
||||
**Cause**: Blocking call in async route (e.g., `time.sleep()`, sync database client, CPU-bound operations)
|
||||
|
||||
**Symptoms** (production-scale):
|
||||
|
||||
- Throughput plateaus far earlier than expected
|
||||
- Latency "balloons" as concurrency increases
|
||||
- Request pattern looks almost serial under load
|
||||
@@ -817,6 +832,7 @@ app.add_middleware(
|
||||
- Small scattered blocking calls that aren't obvious (not infinite loops)
|
||||
|
||||
**Fix**: Use async alternatives:
|
||||
|
||||
```python
|
||||
# ✗ WRONG: Blocks event loop
|
||||
import time
|
||||
@@ -867,6 +883,7 @@ async def mixed_task():
|
||||
**Cause**: Using `Optional[str]` without default
|
||||
|
||||
**Fix**:
|
||||
|
||||
```python
|
||||
# Wrong
|
||||
description: Optional[str] # Still required!
|
||||
@@ -915,22 +932,26 @@ Run: `uv run pytest`
|
||||
## Deployment
|
||||
|
||||
### Uvicorn (Development)
|
||||
|
||||
```bash
|
||||
uv run fastapi dev src/main.py
|
||||
```
|
||||
|
||||
### Uvicorn (Production)
|
||||
|
||||
```bash
|
||||
uv run uvicorn src.main:app --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
### Gunicorn + Uvicorn (Production with workers)
|
||||
|
||||
```bash
|
||||
uv add gunicorn
|
||||
uv run gunicorn src.main:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
```dockerfile
|
||||
FROM python:3.12-slim
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ npm test / cargo test / pytest / go test ./...
|
||||
```
|
||||
|
||||
**If tests fail:**
|
||||
|
||||
```
|
||||
Tests failing (<N> failures). Must fix before completing:
|
||||
|
||||
@@ -114,6 +115,7 @@ Report: "Keeping branch <name>. Worktree preserved at <path>."
|
||||
#### Option 4: Discard
|
||||
|
||||
**Confirm first:**
|
||||
|
||||
```
|
||||
This will permanently delete:
|
||||
- Branch <name>
|
||||
@@ -126,6 +128,7 @@ Type 'discard' to confirm.
|
||||
Wait for exact confirmation.
|
||||
|
||||
If confirmed:
|
||||
|
||||
```bash
|
||||
git checkout <base-branch>
|
||||
git branch -D <feature-branch>
|
||||
@@ -138,11 +141,13 @@ Then: Cleanup worktree (Step 5)
|
||||
**For Options 1, 2, 4:**
|
||||
|
||||
Check if in worktree:
|
||||
|
||||
```bash
|
||||
git worktree list | grep $(git branch --show-current)
|
||||
```
|
||||
|
||||
If yes:
|
||||
|
||||
```bash
|
||||
git worktree remove <worktree-path>
|
||||
```
|
||||
@@ -151,40 +156,46 @@ git worktree remove <worktree-path>
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Option | Merge | Push | Keep Worktree | Cleanup Branch |
|
||||
|--------|-------|------|---------------|----------------|
|
||||
| 1. Merge locally | ✓ | - | - | ✓ |
|
||||
| 2. Create PR | - | ✓ | ✓ | - |
|
||||
| 3. Keep as-is | - | - | ✓ | - |
|
||||
| 4. Discard | - | - | - | ✓ (force) |
|
||||
| Option | Merge | Push | Keep Worktree | Cleanup Branch |
|
||||
| ---------------- | ----- | ---- | ------------- | -------------- |
|
||||
| 1. Merge locally | ✓ | - | - | ✓ |
|
||||
| 2. Create PR | - | ✓ | ✓ | - |
|
||||
| 3. Keep as-is | - | - | ✓ | - |
|
||||
| 4. Discard | - | - | - | ✓ (force) |
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
**Skipping test verification**
|
||||
|
||||
- **Problem:** Merge broken code, create failing PR
|
||||
- **Fix:** Always verify tests before offering options
|
||||
|
||||
**Open-ended questions**
|
||||
|
||||
- **Problem:** "What should I do next?" → ambiguous
|
||||
- **Fix:** Present exactly 4 structured options
|
||||
|
||||
**Automatic worktree cleanup**
|
||||
|
||||
- **Problem:** Remove worktree when might need it (Option 2, 3)
|
||||
- **Fix:** Only cleanup for Options 1 and 4
|
||||
|
||||
**No confirmation for discard**
|
||||
|
||||
- **Problem:** Accidentally delete work
|
||||
- **Fix:** Require typed "discard" confirmation
|
||||
|
||||
## Red Flags
|
||||
|
||||
**Never:**
|
||||
|
||||
- Proceed with failing tests
|
||||
- Merge without verifying tests on result
|
||||
- Delete work without confirmation
|
||||
- Force-push without explicit request
|
||||
|
||||
**Always:**
|
||||
|
||||
- Verify tests before offering options
|
||||
- Present exactly 4 options
|
||||
- Get typed confirmation for Option 4
|
||||
@@ -193,8 +204,10 @@ git worktree remove <worktree-path>
|
||||
## Integration
|
||||
|
||||
**Called by:**
|
||||
|
||||
- **subagent-driven-development** (Step 7) - After all tasks complete
|
||||
- **executing-plans** (Step 5) - After all batches complete
|
||||
|
||||
**Pairs with:**
|
||||
|
||||
- **using-git-worktrees** - Cleans up worktree created by that skill
|
||||
|
||||
@@ -40,22 +40,27 @@ Before providing recommendations, identify:
|
||||
## Core Principles
|
||||
|
||||
### 1. Every Field Has a Cost
|
||||
|
||||
Each field reduces completion rate. Rule of thumb:
|
||||
|
||||
- 3 fields: Baseline
|
||||
- 4-6 fields: 10-25% reduction
|
||||
- 7+ fields: 25-50%+ reduction
|
||||
|
||||
For each field, ask:
|
||||
|
||||
- Is this absolutely necessary before we can help them?
|
||||
- Can we get this information another way?
|
||||
- Can we ask this later?
|
||||
|
||||
### 2. Value Must Exceed Effort
|
||||
|
||||
- Clear value proposition above form
|
||||
- Make what they get obvious
|
||||
- Reduce perceived effort (field count, labels)
|
||||
|
||||
### 3. Reduce Cognitive Load
|
||||
|
||||
- One question per field
|
||||
- Clear, conversational labels
|
||||
- Logical grouping and order
|
||||
@@ -66,44 +71,52 @@ For each field, ask:
|
||||
## Field-by-Field Optimization
|
||||
|
||||
### Email Field
|
||||
|
||||
- Single field, no confirmation
|
||||
- Inline validation
|
||||
- Typo detection (did you mean gmail.com?)
|
||||
- Proper mobile keyboard
|
||||
|
||||
### Name Fields
|
||||
|
||||
- Single "Name" vs. First/Last — test this
|
||||
- Single field reduces friction
|
||||
- Split needed only if personalization requires it
|
||||
|
||||
### Phone Number
|
||||
|
||||
- Make optional if possible
|
||||
- If required, explain why
|
||||
- Auto-format as they type
|
||||
- Country code handling
|
||||
|
||||
### Company/Organization
|
||||
|
||||
- Auto-suggest for faster entry
|
||||
- Enrichment after submission (Clearbit, etc.)
|
||||
- Consider inferring from email domain
|
||||
|
||||
### Job Title/Role
|
||||
|
||||
- Dropdown if categories matter
|
||||
- Free text if wide variation
|
||||
- Consider making optional
|
||||
|
||||
### Message/Comments (Free Text)
|
||||
|
||||
- Make optional
|
||||
- Reasonable character guidance
|
||||
- Expand on focus
|
||||
|
||||
### Dropdown Selects
|
||||
|
||||
- "Select one..." placeholder
|
||||
- Searchable if many options
|
||||
- Consider radio buttons if < 5 options
|
||||
- "Other" option with text field
|
||||
|
||||
### Checkboxes (Multi-select)
|
||||
|
||||
- Clear, parallel labels
|
||||
- Reasonable number of options
|
||||
- Consider "Select all that apply" instruction
|
||||
@@ -113,34 +126,40 @@ For each field, ask:
|
||||
## Form Layout Optimization
|
||||
|
||||
### Field Order
|
||||
|
||||
1. Start with easiest fields (name, email)
|
||||
2. Build commitment before asking more
|
||||
3. Sensitive fields last (phone, company size)
|
||||
4. Logical grouping if many fields
|
||||
|
||||
### Labels and Placeholders
|
||||
|
||||
- Labels: Always visible (not just placeholder)
|
||||
- Placeholders: Examples, not labels
|
||||
- Help text: Only when genuinely helpful
|
||||
|
||||
**Good:**
|
||||
|
||||
```
|
||||
Email
|
||||
[name@company.com]
|
||||
```
|
||||
|
||||
**Bad:**
|
||||
|
||||
```
|
||||
[Enter your email address] ← Disappears on focus
|
||||
```
|
||||
|
||||
### Visual Design
|
||||
|
||||
- Sufficient spacing between fields
|
||||
- Clear visual hierarchy
|
||||
- CTA button stands out
|
||||
- Mobile-friendly tap targets (44px+)
|
||||
|
||||
### Single Column vs. Multi-Column
|
||||
|
||||
- Single column: Higher completion, mobile-friendly
|
||||
- Multi-column: Only for short related fields (First/Last name)
|
||||
- When in doubt, single column
|
||||
@@ -150,12 +169,14 @@ Email
|
||||
## Multi-Step Forms
|
||||
|
||||
### When to Use Multi-Step
|
||||
|
||||
- More than 5-6 fields
|
||||
- Logically distinct sections
|
||||
- Conditional paths based on answers
|
||||
- Complex forms (applications, quotes)
|
||||
|
||||
### Multi-Step Best Practices
|
||||
|
||||
- Progress indicator (step X of Y)
|
||||
- Start with easy, end with sensitive
|
||||
- One topic per step
|
||||
@@ -164,6 +185,7 @@ Email
|
||||
- Clear indication of required vs. optional
|
||||
|
||||
### Progressive Commitment Pattern
|
||||
|
||||
1. Low-friction start (just email)
|
||||
2. More detail (name, company)
|
||||
3. Qualifying questions
|
||||
@@ -174,11 +196,13 @@ Email
|
||||
## Error Handling
|
||||
|
||||
### Inline Validation
|
||||
|
||||
- Validate as they move to next field
|
||||
- Don't validate too aggressively while typing
|
||||
- Clear visual indicators (green check, red border)
|
||||
|
||||
### Error Messages
|
||||
|
||||
- Specific to the problem
|
||||
- Suggest how to fix
|
||||
- Positioned near the field
|
||||
@@ -188,6 +212,7 @@ Email
|
||||
**Bad:** "Invalid input"
|
||||
|
||||
### On Submit
|
||||
|
||||
- Focus on first error field
|
||||
- Summarize errors if multiple
|
||||
- Preserve all entered data
|
||||
@@ -198,10 +223,12 @@ Email
|
||||
## Submit Button Optimization
|
||||
|
||||
### Button Copy
|
||||
|
||||
Weak: "Submit" | "Send"
|
||||
Strong: "[Action] + [What they get]"
|
||||
|
||||
Examples:
|
||||
|
||||
- "Get My Free Quote"
|
||||
- "Download the Guide"
|
||||
- "Request Demo"
|
||||
@@ -209,12 +236,14 @@ Examples:
|
||||
- "Start Free Trial"
|
||||
|
||||
### Button Placement
|
||||
|
||||
- Immediately after last field
|
||||
- Left-aligned with fields
|
||||
- Sufficient size and contrast
|
||||
- Mobile: Sticky or clearly visible
|
||||
|
||||
### Post-Submit States
|
||||
|
||||
- Loading state (disable button, show spinner)
|
||||
- Success confirmation (clear next steps)
|
||||
- Error handling (clear message, focus on issue)
|
||||
@@ -224,18 +253,21 @@ Examples:
|
||||
## Trust and Friction Reduction
|
||||
|
||||
### Near the Form
|
||||
|
||||
- Privacy statement: "We'll never share your info"
|
||||
- Security badges if collecting sensitive data
|
||||
- Testimonial or social proof
|
||||
- Expected response time
|
||||
|
||||
### Reducing Perceived Effort
|
||||
|
||||
- "Takes 30 seconds"
|
||||
- Field count indicator
|
||||
- Remove visual clutter
|
||||
- Generous white space
|
||||
|
||||
### Addressing Objections
|
||||
|
||||
- "No spam, unsubscribe anytime"
|
||||
- "We won't share your number"
|
||||
- "No credit card required"
|
||||
@@ -245,30 +277,35 @@ Examples:
|
||||
## Form Types: Specific Guidance
|
||||
|
||||
### Lead Capture (Gated Content)
|
||||
|
||||
- Minimum viable fields (often just email)
|
||||
- Clear value proposition for what they get
|
||||
- Consider asking enrichment questions post-download
|
||||
- Test email-only vs. email + name
|
||||
|
||||
### Contact Form
|
||||
|
||||
- Essential: Email/Name + Message
|
||||
- Phone optional
|
||||
- Set response time expectations
|
||||
- Offer alternatives (chat, phone)
|
||||
|
||||
### Demo Request
|
||||
|
||||
- Name, Email, Company required
|
||||
- Phone: Optional with "preferred contact" choice
|
||||
- Use case/goal question helps personalize
|
||||
- Calendar embed can increase show rate
|
||||
|
||||
### Quote/Estimate Request
|
||||
|
||||
- Multi-step often works well
|
||||
- Start with easy questions
|
||||
- Technical details later
|
||||
- Save progress for complex forms
|
||||
|
||||
### Survey Forms
|
||||
|
||||
- Progress bar essential
|
||||
- One question per screen for engagement
|
||||
- Skip logic for relevance
|
||||
@@ -290,6 +327,7 @@ Examples:
|
||||
## Measurement
|
||||
|
||||
### Key Metrics
|
||||
|
||||
- **Form start rate**: Page views → Started form
|
||||
- **Completion rate**: Started → Submitted
|
||||
- **Field drop-off**: Which fields lose people
|
||||
@@ -298,6 +336,7 @@ Examples:
|
||||
- **Mobile vs. desktop**: Completion by device
|
||||
|
||||
### What to Track
|
||||
|
||||
- Form views
|
||||
- First field focus
|
||||
- Each field completion
|
||||
@@ -310,13 +349,16 @@ Examples:
|
||||
## Output Format
|
||||
|
||||
### Form Audit
|
||||
|
||||
For each issue:
|
||||
|
||||
- **Issue**: What's wrong
|
||||
- **Impact**: Estimated effect on conversions
|
||||
- **Fix**: Specific recommendation
|
||||
- **Priority**: High/Medium/Low
|
||||
|
||||
### Recommended Form Design
|
||||
|
||||
- **Required fields**: Justified list
|
||||
- **Optional fields**: With rationale
|
||||
- **Field order**: Recommended sequence
|
||||
@@ -325,6 +367,7 @@ For each issue:
|
||||
- **Layout**: Visual guidance
|
||||
|
||||
### Test Hypotheses
|
||||
|
||||
Ideas to A/B test with expected outcomes
|
||||
|
||||
---
|
||||
@@ -334,6 +377,7 @@ Ideas to A/B test with expected outcomes
|
||||
### Form Structure Experiments
|
||||
|
||||
**Layout & Flow**
|
||||
|
||||
- Single-step form vs. multi-step with progress bar
|
||||
- 1-column vs. 2-column field layout
|
||||
- Form embedded on page vs. separate page
|
||||
@@ -341,6 +385,7 @@ Ideas to A/B test with expected outcomes
|
||||
- Form above fold vs. after content
|
||||
|
||||
**Field Optimization**
|
||||
|
||||
- Reduce to minimum viable fields
|
||||
- Add or remove phone number field
|
||||
- Add or remove company/organization field
|
||||
@@ -349,6 +394,7 @@ Ideas to A/B test with expected outcomes
|
||||
- Hide fields for returning/known visitors
|
||||
|
||||
**Smart Forms**
|
||||
|
||||
- Add real-time validation for emails and phone numbers
|
||||
- Progressive profiling (ask more over time)
|
||||
- Conditional fields based on earlier answers
|
||||
@@ -359,17 +405,20 @@ Ideas to A/B test with expected outcomes
|
||||
### Copy & Design Experiments
|
||||
|
||||
**Labels & Microcopy**
|
||||
|
||||
- Test field label clarity and length
|
||||
- Placeholder text optimization
|
||||
- Help text: show vs. hide vs. on-hover
|
||||
- Error message tone (friendly vs. direct)
|
||||
|
||||
**CTAs & Buttons**
|
||||
|
||||
- Button text variations ("Submit" vs. "Get My Quote" vs. specific action)
|
||||
- Button color and size testing
|
||||
- Button placement relative to fields
|
||||
|
||||
**Trust Elements**
|
||||
|
||||
- Add privacy assurance near form
|
||||
- Show trust badges next to submit
|
||||
- Add testimonial near form
|
||||
@@ -380,18 +429,21 @@ Ideas to A/B test with expected outcomes
|
||||
### Form Type-Specific Experiments
|
||||
|
||||
**Demo Request Forms**
|
||||
|
||||
- Test with/without phone number requirement
|
||||
- Add "preferred contact method" choice
|
||||
- Include "What's your biggest challenge?" question
|
||||
- Test calendar embed vs. form submission
|
||||
|
||||
**Lead Capture Forms**
|
||||
|
||||
- Email-only vs. email + name
|
||||
- Test value proposition messaging above form
|
||||
- Gated vs. ungated content strategies
|
||||
- Post-submission enrichment questions
|
||||
|
||||
**Contact Forms**
|
||||
|
||||
- Add department/topic routing dropdown
|
||||
- Test with/without message field requirement
|
||||
- Show alternative contact methods (chat, phone)
|
||||
|
||||
@@ -26,35 +26,39 @@ Before designing a tool strategy, understand:
|
||||
## Core Principles
|
||||
|
||||
### 1. Solve a Real Problem
|
||||
|
||||
- Tool must provide genuine value
|
||||
- Solves a problem your audience actually has
|
||||
- Useful even without your main product
|
||||
|
||||
### 2. Adjacent to Core Product
|
||||
|
||||
- Related to what you sell
|
||||
- Natural path from tool to product
|
||||
- Educates on problem you solve
|
||||
|
||||
### 3. Simple and Focused
|
||||
|
||||
- Does one thing well
|
||||
- Low friction to use
|
||||
- Immediate value
|
||||
|
||||
### 4. Worth the Investment
|
||||
|
||||
- Lead value × expected leads > build cost + maintenance
|
||||
|
||||
---
|
||||
|
||||
## Tool Types Overview
|
||||
|
||||
| Type | Examples | Best For |
|
||||
|------|----------|----------|
|
||||
| Type | Examples | Best For |
|
||||
| ----------- | -------------------------------- | --------------------------- |
|
||||
| Calculators | ROI, savings, pricing estimators | Decisions involving numbers |
|
||||
| Generators | Templates, policies, names | Creating something quickly |
|
||||
| Analyzers | Website graders, SEO auditors | Evaluating existing work |
|
||||
| Testers | Meta tag preview, speed tests | Checking if something works |
|
||||
| Libraries | Icon sets, templates, snippets | Reference material |
|
||||
| Interactive | Tutorials, playgrounds, quizzes | Learning/understanding |
|
||||
| Generators | Templates, policies, names | Creating something quickly |
|
||||
| Analyzers | Website graders, SEO auditors | Evaluating existing work |
|
||||
| Testers | Meta tag preview, speed tests | Checking if something works |
|
||||
| Libraries | Icon sets, templates, snippets | Reference material |
|
||||
| Interactive | Tutorials, playgrounds, quizzes | Learning/understanding |
|
||||
|
||||
**For detailed tool types and examples**: See [references/tool-types.md](references/tool-types.md)
|
||||
|
||||
@@ -85,14 +89,15 @@ Before designing a tool strategy, understand:
|
||||
|
||||
### Gating Options
|
||||
|
||||
| Approach | Pros | Cons |
|
||||
|----------|------|------|
|
||||
| Fully gated | Maximum capture | Lower usage |
|
||||
| Partially gated | Balance of both | Common pattern |
|
||||
| Ungated + optional | Maximum reach | Lower capture |
|
||||
| Ungated entirely | Pure SEO/brand | No direct leads |
|
||||
| Approach | Pros | Cons |
|
||||
| ------------------ | --------------- | --------------- |
|
||||
| Fully gated | Maximum capture | Lower usage |
|
||||
| Partially gated | Balance of both | Common pattern |
|
||||
| Ungated + optional | Maximum reach | Lower capture |
|
||||
| Ungated entirely | Pure SEO/brand | No direct leads |
|
||||
|
||||
### Lead Capture Best Practices
|
||||
|
||||
- Value exchange clear: "Get your full report"
|
||||
- Minimal friction: Email only
|
||||
- Show preview of what they'll get
|
||||
@@ -103,12 +108,15 @@ Before designing a tool strategy, understand:
|
||||
## SEO Considerations
|
||||
|
||||
### Keyword Strategy
|
||||
|
||||
**Tool landing page**: "[thing] calculator", "[thing] generator", "free [tool type]"
|
||||
|
||||
**Supporting content**: "How to [use case]", "What is [concept]"
|
||||
|
||||
### Link Building
|
||||
|
||||
Free tools attract links because:
|
||||
|
||||
- Genuinely useful (people reference them)
|
||||
- Unique (can't link to just any page)
|
||||
- Shareable (social amplification)
|
||||
@@ -118,13 +126,16 @@ Free tools attract links because:
|
||||
## Build vs. Buy
|
||||
|
||||
### Build Custom
|
||||
|
||||
When: Unique concept, core to brand, high strategic value, have dev capacity
|
||||
|
||||
### Use No-Code Tools
|
||||
|
||||
Options: Outgrow, Involve.me, Typeform, Tally, Bubble, Webflow
|
||||
When: Speed to market, limited dev resources, testing concept
|
||||
|
||||
### Embed Existing
|
||||
|
||||
When: Something good exists, white-label available, not core differentiator
|
||||
|
||||
---
|
||||
@@ -132,11 +143,13 @@ When: Something good exists, white-label available, not core differentiator
|
||||
## MVP Scope
|
||||
|
||||
### Minimum Viable Tool
|
||||
|
||||
1. Core functionality only—does the one thing, works reliably
|
||||
2. Essential UX—clear input, obvious output, mobile works
|
||||
3. Basic lead capture—email collection, leads go somewhere useful
|
||||
|
||||
### What to Skip Initially
|
||||
|
||||
Account creation, saving results, advanced features, perfect design, every edge case
|
||||
|
||||
---
|
||||
@@ -145,16 +158,16 @@ Account creation, saving results, advanced features, perfect design, every edge
|
||||
|
||||
Rate each factor 1-5:
|
||||
|
||||
| Factor | Score |
|
||||
|--------|-------|
|
||||
| Search demand exists | ___ |
|
||||
| Audience match to buyers | ___ |
|
||||
| Uniqueness vs. existing | ___ |
|
||||
| Natural path to product | ___ |
|
||||
| Build feasibility | ___ |
|
||||
| Maintenance burden (inverse) | ___ |
|
||||
| Link-building potential | ___ |
|
||||
| Share-worthiness | ___ |
|
||||
| Factor | Score |
|
||||
| ---------------------------- | ------ |
|
||||
| Search demand exists | \_\_\_ |
|
||||
| Audience match to buyers | \_\_\_ |
|
||||
| Uniqueness vs. existing | \_\_\_ |
|
||||
| Natural path to product | \_\_\_ |
|
||||
| Build feasibility | \_\_\_ |
|
||||
| Maintenance burden (inverse) | \_\_\_ |
|
||||
| Link-building potential | \_\_\_ |
|
||||
| Share-worthiness | \_\_\_ |
|
||||
|
||||
**25+**: Strong candidate | **15-24**: Promising | **<15**: Reconsider
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ Detailed guide to each type of marketing tool you can build.
|
||||
**Best for**: Decisions involving numbers, comparisons, estimates
|
||||
|
||||
**Examples**:
|
||||
|
||||
- ROI calculator
|
||||
- Savings calculator
|
||||
- Cost comparison tool
|
||||
@@ -17,12 +18,14 @@ Detailed guide to each type of marketing tool you can build.
|
||||
- Break-even calculator
|
||||
|
||||
**Why they work**:
|
||||
|
||||
- Personalized output
|
||||
- High perceived value
|
||||
- Share-worthy results
|
||||
- Clear problem → solution
|
||||
|
||||
**Implementation tips**:
|
||||
|
||||
- Keep inputs simple
|
||||
- Show calculations transparently
|
||||
- Make results shareable
|
||||
@@ -35,6 +38,7 @@ Detailed guide to each type of marketing tool you can build.
|
||||
**Best for**: Creating something useful quickly
|
||||
|
||||
**Examples**:
|
||||
|
||||
- Policy generator (privacy, terms)
|
||||
- Template generator
|
||||
- Name/tagline generator
|
||||
@@ -45,12 +49,14 @@ Detailed guide to each type of marketing tool you can build.
|
||||
- Contract generator
|
||||
|
||||
**Why they work**:
|
||||
|
||||
- Tangible output
|
||||
- Saves time
|
||||
- Easily shared
|
||||
- Repeat usage
|
||||
|
||||
**Implementation tips**:
|
||||
|
||||
- Output should be immediately usable
|
||||
- Allow customization
|
||||
- Offer download/export options
|
||||
@@ -63,6 +69,7 @@ Detailed guide to each type of marketing tool you can build.
|
||||
**Best for**: Evaluating existing work or assets
|
||||
|
||||
**Examples**:
|
||||
|
||||
- Website grader
|
||||
- SEO analyzer
|
||||
- Email subject tester
|
||||
@@ -73,12 +80,14 @@ Detailed guide to each type of marketing tool you can build.
|
||||
- Code quality analyzer
|
||||
|
||||
**Why they work**:
|
||||
|
||||
- Curiosity-driven
|
||||
- Personalized insights
|
||||
- Creates awareness of problems
|
||||
- Natural lead to solution
|
||||
|
||||
**Implementation tips**:
|
||||
|
||||
- Score or grade for gamification
|
||||
- Benchmark against averages
|
||||
- Provide actionable recommendations
|
||||
@@ -91,6 +100,7 @@ Detailed guide to each type of marketing tool you can build.
|
||||
**Best for**: Checking if something works
|
||||
|
||||
**Examples**:
|
||||
|
||||
- Meta tag preview
|
||||
- Email rendering test
|
||||
- Mobile-friendly test
|
||||
@@ -101,12 +111,14 @@ Detailed guide to each type of marketing tool you can build.
|
||||
- Broken link finder
|
||||
|
||||
**Why they work**:
|
||||
|
||||
- Immediate utility
|
||||
- Bookmark-worthy
|
||||
- Repeat usage
|
||||
- Professional necessity
|
||||
|
||||
**Implementation tips**:
|
||||
|
||||
- Fast results are essential
|
||||
- Show pass/fail clearly
|
||||
- Provide fix instructions
|
||||
@@ -119,6 +131,7 @@ Detailed guide to each type of marketing tool you can build.
|
||||
**Best for**: Reference material
|
||||
|
||||
**Examples**:
|
||||
|
||||
- Icon library
|
||||
- Template library
|
||||
- Code snippet library
|
||||
@@ -129,12 +142,14 @@ Detailed guide to each type of marketing tool you can build.
|
||||
- Font pairing tool
|
||||
|
||||
**Why they work**:
|
||||
|
||||
- High SEO value
|
||||
- Ongoing traffic
|
||||
- Establishes authority
|
||||
- Linkable asset
|
||||
|
||||
**Implementation tips**:
|
||||
|
||||
- Make searchable/filterable
|
||||
- Allow easy copying/downloading
|
||||
- Update regularly
|
||||
@@ -147,6 +162,7 @@ Detailed guide to each type of marketing tool you can build.
|
||||
**Best for**: Learning/understanding
|
||||
|
||||
**Examples**:
|
||||
|
||||
- Interactive tutorials
|
||||
- Code playgrounds
|
||||
- Visual explainers
|
||||
@@ -157,12 +173,14 @@ Detailed guide to each type of marketing tool you can build.
|
||||
- Configurators
|
||||
|
||||
**Why they work**:
|
||||
|
||||
- Engages deeply
|
||||
- Demonstrates expertise
|
||||
- Shareable
|
||||
- Memory-creating
|
||||
|
||||
**Implementation tips**:
|
||||
|
||||
- Make it hands-on
|
||||
- Show immediate feedback
|
||||
- Lead to deeper resources
|
||||
@@ -173,6 +191,7 @@ Detailed guide to each type of marketing tool you can build.
|
||||
## Tool Concept Examples by Industry
|
||||
|
||||
### SaaS Product
|
||||
|
||||
- Product ROI calculator
|
||||
- Competitor comparison tool
|
||||
- Readiness assessment quiz
|
||||
@@ -180,6 +199,7 @@ Detailed guide to each type of marketing tool you can build.
|
||||
- Feature configurator
|
||||
|
||||
### Agency/Services
|
||||
|
||||
- Industry benchmark tool
|
||||
- Project scoping calculator
|
||||
- Portfolio review tool
|
||||
@@ -187,6 +207,7 @@ Detailed guide to each type of marketing tool you can build.
|
||||
- Proposal generator
|
||||
|
||||
### E-commerce
|
||||
|
||||
- Product finder quiz
|
||||
- Comparison tool
|
||||
- Size/fit calculator
|
||||
@@ -194,6 +215,7 @@ Detailed guide to each type of marketing tool you can build.
|
||||
- Gift finder
|
||||
|
||||
### Developer Tools
|
||||
|
||||
- Code snippet library
|
||||
- Testing/preview tool
|
||||
- Documentation generator
|
||||
@@ -201,6 +223,7 @@ Detailed guide to each type of marketing tool you can build.
|
||||
- API playground
|
||||
|
||||
### Finance
|
||||
|
||||
- Financial calculators
|
||||
- Investment comparison
|
||||
- Budget planner
|
||||
|
||||
@@ -11,6 +11,7 @@ The user provides frontend requirements: a component, page, application, or inte
|
||||
## Design Thinking
|
||||
|
||||
Before coding, understand the context and commit to a BOLD aesthetic direction:
|
||||
|
||||
- **Purpose**: What problem does this interface solve? Who uses it?
|
||||
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
|
||||
- **Constraints**: Technical requirements (framework, performance, accessibility).
|
||||
@@ -19,6 +20,7 @@ Before coding, understand the context and commit to a BOLD aesthetic direction:
|
||||
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
|
||||
|
||||
Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
|
||||
|
||||
- Production-grade and functional
|
||||
- Visually striking and memorable
|
||||
- Cohesive with a clear aesthetic point-of-view
|
||||
@@ -27,6 +29,7 @@ Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
|
||||
## Frontend Aesthetics Guidelines
|
||||
|
||||
Focus on:
|
||||
|
||||
- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
|
||||
- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
|
||||
- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
|
||||
|
||||
@@ -5,7 +5,9 @@ license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
## When to use this skill
|
||||
|
||||
To write internal communications, use this skill for:
|
||||
|
||||
- 3P updates (Progress, Plans, Problems)
|
||||
- Company newsletters
|
||||
- FAQ responses
|
||||
@@ -20,13 +22,14 @@ To write any internal communication:
|
||||
|
||||
1. **Identify the communication type** from the request
|
||||
2. **Load the appropriate guideline file** from the `examples/` directory:
|
||||
- `examples/3p-updates.md` - For Progress/Plans/Problems team updates
|
||||
- `examples/company-newsletter.md` - For company-wide newsletters
|
||||
- `examples/faq-answers.md` - For answering frequently asked questions
|
||||
- `examples/general-comms.md` - For anything else that doesn't explicitly match one of the above
|
||||
- `examples/3p-updates.md` - For Progress/Plans/Problems team updates
|
||||
- `examples/company-newsletter.md` - For company-wide newsletters
|
||||
- `examples/faq-answers.md` - For answering frequently asked questions
|
||||
- `examples/general-comms.md` - For anything else that doesn't explicitly match one of the above
|
||||
3. **Follow the specific instructions** in that file for formatting, tone, and content gathering
|
||||
|
||||
If the communication type doesn't match any existing guideline, ask for clarification or more context about the desired format.
|
||||
|
||||
## Keywords
|
||||
|
||||
3P updates, company newsletter, company comms, weekly update, faqs, common questions, updates, internal comms
|
||||
|
||||
@@ -1,36 +1,38 @@
|
||||
## Instructions
|
||||
|
||||
You are being asked to write a 3P update. 3P updates stand for "Progress, Plans, Problems." The main audience is for executives, leadership, other teammates, etc. They're meant to be very succinct and to-the-point: think something you can read in 30-60sec or less. They're also for people with some, but not a lot of context on what the team does.
|
||||
|
||||
3Ps can cover a team of any size, ranging all the way up to the entire company. The bigger the team, the less granular the tasks should be. For example, "mobile team" might have "shipped feature" or "fixed bugs," whereas the company might have really meaty 3Ps, like "hired 20 new people" or "closed 10 new deals."
|
||||
3Ps can cover a team of any size, ranging all the way up to the entire company. The bigger the team, the less granular the tasks should be. For example, "mobile team" might have "shipped feature" or "fixed bugs," whereas the company might have really meaty 3Ps, like "hired 20 new people" or "closed 10 new deals."
|
||||
|
||||
They represent the work of the team across a time period, almost always one week. They include three sections:
|
||||
1) Progress: what the team has accomplished over the next time period. Focus mainly on things shipped, milestones achieved, tasks created, etc.
|
||||
2) Plans: what the team plans to do over the next time period. Focus on what things are top-of-mind, really high priority, etc. for the team.
|
||||
3) Problems: anything that is slowing the team down. This could be things like too few people, bugs or blockers that are preventing the team from moving forward, some deal that fell through, etc.
|
||||
|
||||
1. Progress: what the team has accomplished over the next time period. Focus mainly on things shipped, milestones achieved, tasks created, etc.
|
||||
2. Plans: what the team plans to do over the next time period. Focus on what things are top-of-mind, really high priority, etc. for the team.
|
||||
3. Problems: anything that is slowing the team down. This could be things like too few people, bugs or blockers that are preventing the team from moving forward, some deal that fell through, etc.
|
||||
|
||||
Before writing them, make sure that you know the team name. If it's not specified, you can ask explicitly what the team name you're writing for is.
|
||||
|
||||
|
||||
## Tools Available
|
||||
|
||||
Whenever possible, try to pull from available sources to get the information you need:
|
||||
|
||||
- Slack: posts from team members with their updates - ideally look for posts in large channels with lots of reactions
|
||||
- Google Drive: docs written from critical team members with lots of views
|
||||
- Email: emails with lots of responses of lots of content that seems relevant
|
||||
- Calendar: non-recurring meetings that have a lot of importance, like product reviews, etc.
|
||||
|
||||
|
||||
Try to gather as much context as you can, focusing on the things that covered the time period you're writing for:
|
||||
|
||||
- Progress: anything between a week ago and today
|
||||
- Plans: anything from today to the next week
|
||||
- Problems: anything between a week ago and today
|
||||
|
||||
|
||||
If you don't have access, you can ask the user for things they want to cover. They might also include these things to you directly, in which case you're mostly just formatting for this particular format.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Clarify scope**: Confirm the team name and time period (usually past week for Progress/Problems, next
|
||||
week for Plans)
|
||||
week for Plans)
|
||||
2. **Gather information**: Use available tools or ask the user directly
|
||||
3. **Draft the update**: Follow the strict formatting guidelines
|
||||
4. **Review**: Ensure it's concise (30-60 seconds to read) and data-driven
|
||||
@@ -44,4 +46,4 @@ Progress: [1-3 sentences of content]
|
||||
Plans: [1-3 sentences of content]
|
||||
Problems: [1-3 sentences of content]
|
||||
|
||||
Each section should be no more than 1-3 sentences: clear, to the point. It should be data-driven, and generally include metrics where possible. The tone should be very matter-of-fact, not super prose-heavy.
|
||||
Each section should be no more than 1-3 sentences: clear, to the point. It should be data-driven, and generally include metrics where possible. The tone should be very matter-of-fact, not super prose-heavy.
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
## Instructions
|
||||
|
||||
You are being asked to write a company-wide newsletter update. You are meant to summarize the past week/month of a company in the form of a newsletter that the entire company will read. It should be maybe ~20-25 bullet points long. It will be sent via Slack and email, so make it consumable for that.
|
||||
|
||||
Ideally it includes the following attributes:
|
||||
|
||||
- Lots of links: pulling documents from Google Drive that are very relevant, linking to prominent Slack messages in announce channels and from executives, perhgaps referencing emails that went company-wide, highlighting significant things that have happened in the company.
|
||||
- Short and to-the-point: each bullet should probably be no longer than ~1-2 sentences
|
||||
- Use the "we" tense, as you are part of the company. Many of the bullets should say "we did this" or "we did that"
|
||||
|
||||
## Tools to use
|
||||
|
||||
If you have access to the following tools, please try to use them. If not, you can also let the user know directly that their responses would be better if they gave them access.
|
||||
|
||||
- Slack: look for messages in channels with lots of people, with lots of reactions or lots of responses within the thread
|
||||
@@ -18,10 +21,13 @@ If you have access to the following tools, please try to use them. If not, you c
|
||||
If you don't have access to any of these things, you can ask the user for things they want to cover. In this case, you'll mostly just be polishing up and fitting to this format more directly.
|
||||
|
||||
## Sections
|
||||
|
||||
The company is pretty big: 1000+ people. There are a variety of different teams and initiatives going on across the company. To make sure the update works well, try breaking it into sections of similar things. You might break into clusters like {product development, go to market, finance} or {recruiting, execution, vision}, or {external news, internal news} etc. Try to make sure the different areas of the company are highlighted well.
|
||||
|
||||
## Prioritization
|
||||
|
||||
Focus on:
|
||||
|
||||
- Company-wide impact (not team-specific details)
|
||||
- Announcements from leadership
|
||||
- Major milestones and achievements
|
||||
@@ -29,6 +35,7 @@ Focus on:
|
||||
- External recognition or press
|
||||
|
||||
Avoid:
|
||||
|
||||
- Overly granular team updates (save those for 3Ps)
|
||||
- Information only relevant to small groups
|
||||
- Duplicate information already communicated
|
||||
@@ -36,30 +43,34 @@ Avoid:
|
||||
## Example Formats
|
||||
|
||||
:megaphone: Company Announcements
|
||||
|
||||
- Announcement 1
|
||||
- Announcement 2
|
||||
- Announcement 3
|
||||
|
||||
:dart: Progress on Priorities
|
||||
|
||||
- Area 1
|
||||
- Sub-area 1
|
||||
- Sub-area 2
|
||||
- Sub-area 3
|
||||
- Sub-area 1
|
||||
- Sub-area 2
|
||||
- Sub-area 3
|
||||
- Area 2
|
||||
- Sub-area 1
|
||||
- Sub-area 2
|
||||
- Sub-area 3
|
||||
- Sub-area 1
|
||||
- Sub-area 2
|
||||
- Sub-area 3
|
||||
- Area 3
|
||||
- Sub-area 1
|
||||
- Sub-area 2
|
||||
- Sub-area 3
|
||||
- Sub-area 1
|
||||
- Sub-area 2
|
||||
- Sub-area 3
|
||||
|
||||
:pillar: Leadership Updates
|
||||
|
||||
- Post 1
|
||||
- Post 2
|
||||
- Post 3
|
||||
|
||||
:thread: Social Updates
|
||||
|
||||
- Update 1
|
||||
- Update 2
|
||||
- Update 3
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
## Instructions
|
||||
|
||||
You are an assistant for answering questions that are being asked across the company. Every week, there are lots of questions that get asked across the company, and your goal is to try to summarize what those questions are. We want our company to be well-informed and on the same page, so your job is to produce a set of frequently asked questions that our employees are asking and attempt to answer them. Your singular job is to do two things:
|
||||
|
||||
- Find questions that are big sources of confusion for lots of employees at the company, generally about things that affect a large portion of the employee base
|
||||
@@ -6,25 +7,29 @@ You are an assistant for answering questions that are being asked across the com
|
||||
|
||||
Some examples of areas that may be interesting to folks: recent corporate events (fundraising, new executives, etc.), upcoming launches, hiring progress, changes to vision or focus, etc.
|
||||
|
||||
|
||||
## Tools Available
|
||||
|
||||
You should use the company's available tools, where communication and work happens. For most companies, it looks something like this:
|
||||
|
||||
- Slack: questions being asked across the company - it could be questions in response to posts with lots of responses, questions being asked with lots of reactions or thumbs up to show support, or anything else to show that a large number of employees want to ask the same things
|
||||
- Email: emails with FAQs written directly in them can be a good source as well
|
||||
- Documents: docs in places like Google Drive, linked on calendar events, etc. can also be a good source of FAQs, either directly added or inferred based on the contents of the doc
|
||||
|
||||
## Formatting
|
||||
|
||||
The formatting should be pretty basic:
|
||||
|
||||
- *Question*: [insert question - 1 sentence]
|
||||
- *Answer*: [insert answer - 1-2 sentence]
|
||||
- _Question_: [insert question - 1 sentence]
|
||||
- _Answer_: [insert answer - 1-2 sentence]
|
||||
|
||||
## Guidance
|
||||
|
||||
Make sure you're being holistic in your questions. Don't focus too much on just the user in question or the team they are a part of, but try to capture the entire company. Try to be as holistic as you can in reading all the tools available, producing responses that are relevant to all at the company.
|
||||
|
||||
## Answer Guidelines
|
||||
|
||||
- Base answers on official company communications when possible
|
||||
- If information is uncertain, indicate that clearly
|
||||
- Link to authoritative sources (docs, announcements, emails)
|
||||
- Keep tone professional but approachable
|
||||
- Flag if a question requires executive input or official response
|
||||
- Flag if a question requires executive input or official response
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
## Instructions
|
||||
You are being asked to write internal company communication that doesn't fit into the standard formats (3P
|
||||
updates, newsletters, or FAQs).
|
||||
## Instructions
|
||||
|
||||
Before proceeding:
|
||||
1. Ask the user about their target audience
|
||||
2. Understand the communication's purpose
|
||||
3. Clarify the desired tone (formal, casual, urgent, informational)
|
||||
4. Confirm any specific formatting requirements
|
||||
You are being asked to write internal company communication that doesn't fit into the standard formats (3P
|
||||
updates, newsletters, or FAQs).
|
||||
|
||||
Use these general principles:
|
||||
- Be clear and concise
|
||||
- Use active voice
|
||||
- Put the most important information first
|
||||
- Include relevant links and references
|
||||
- Match the company's communication style
|
||||
Before proceeding:
|
||||
|
||||
1. Ask the user about their target audience
|
||||
2. Understand the communication's purpose
|
||||
3. Clarify the desired tone (formal, casual, urgent, informational)
|
||||
4. Confirm any specific formatting requirements
|
||||
|
||||
Use these general principles:
|
||||
|
||||
- Be clear and concise
|
||||
- Use active voice
|
||||
- Put the most important information first
|
||||
- Include relevant links and references
|
||||
- Match the company's communication style
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: kickstart
|
||||
description: "Launch an orchestrator session for a milestone, issue, or task. Use when starting autonomous work on a milestone, orchestrating issue completion, or resuming from a handoff. Triggers on: kickstart, orchestrate, start milestone, resume orchestrator."
|
||||
description: 'Launch an orchestrator session for a milestone, issue, or task. Use when starting autonomous work on a milestone, orchestrating issue completion, or resuming from a handoff. Triggers on: kickstart, orchestrate, start milestone, resume orchestrator.'
|
||||
---
|
||||
|
||||
# Kickstart Orchestrator
|
||||
@@ -8,6 +8,7 @@ description: "Launch an orchestrator session for a milestone, issue, or task. Us
|
||||
Launch an orchestrator session with a single command. Replaces the manual boilerplate of specifying mission, quality gates, branch strategy, and tracking protocol.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```
|
||||
/kickstart — List open milestones, ask user to pick
|
||||
/kickstart 0.0.9 — Orchestrate a milestone (by version)
|
||||
@@ -25,13 +26,13 @@ Launch an orchestrator session with a single command. Replaces the manual boiler
|
||||
|
||||
Parse the argument (if any) provided after `/kickstart`:
|
||||
|
||||
| Pattern | Type | Example |
|
||||
|---------|------|---------|
|
||||
| No argument | **interactive** | `/kickstart` |
|
||||
| Starts with `#` | **issue** | `/kickstart #42` |
|
||||
| `resume` (literal) | **resume** | `/kickstart resume` |
|
||||
| Contains `-` with uppercase + digits at end | **task ID** | `/kickstart MS-SEC-001` |
|
||||
| Anything else | **milestone** | `/kickstart 0.0.9` or `/kickstart M10-Telemetry` |
|
||||
| Pattern | Type | Example |
|
||||
| ------------------------------------------- | --------------- | ------------------------------------------------ |
|
||||
| No argument | **interactive** | `/kickstart` |
|
||||
| Starts with `#` | **issue** | `/kickstart #42` |
|
||||
| `resume` (literal) | **resume** | `/kickstart resume` |
|
||||
| Contains `-` with uppercase + digits at end | **task ID** | `/kickstart MS-SEC-001` |
|
||||
| Anything else | **milestone** | `/kickstart 0.0.9` or `/kickstart M10-Telemetry` |
|
||||
|
||||
### 1b. If no argument (interactive mode)
|
||||
|
||||
@@ -42,6 +43,7 @@ List open milestones and ask the user to choose:
|
||||
```
|
||||
|
||||
Present the results and ask:
|
||||
|
||||
```
|
||||
Which target do you want to orchestrate?
|
||||
A. [milestone 1]
|
||||
@@ -70,6 +72,7 @@ git branch -r | grep -E 'origin/(develop|main)' | head -1
|
||||
```
|
||||
|
||||
Read the project's `AGENTS.md` first (and `SOUL.md` if present). If only `CLAUDE.md` exists, use it as compatibility fallback. Scan for:
|
||||
|
||||
- Quality gate commands (look for `pnpm`, `npm`, `pytest`, `lint`, `typecheck`, `test`)
|
||||
- Branch conventions
|
||||
- Task prefix conventions
|
||||
@@ -132,6 +135,7 @@ Read the issue description, labels, and any linked milestone.
|
||||
### For resume target
|
||||
|
||||
Read `docs/tasks.md` and determine:
|
||||
|
||||
- How many tasks are `done` vs `not-started` vs `in-progress` vs `failed`
|
||||
- What the next unblocked task is
|
||||
- Whether any tasks are stuck (in-progress with no agent)
|
||||
@@ -149,11 +153,13 @@ Read ~/.config/mosaic/guides/orchestrator.md
|
||||
```
|
||||
|
||||
Also load skills relevant to the project's tech stack from project docs (`AGENTS.md`/`SOUL.md`) plus `~/.config/mosaic/STANDARDS.md`. For example:
|
||||
|
||||
- NestJS project → load `nestjs-best-practices`
|
||||
- Next.js project → load `next-best-practices`, `vercel-react-best-practices`
|
||||
- Python project → load `fastapi`, `python-performance-optimization`
|
||||
|
||||
Always load these orchestrator-relevant skills:
|
||||
|
||||
- `verification-before-completion` — evidence-based completion claims
|
||||
- `dispatching-parallel-agents` — parallel worker patterns
|
||||
|
||||
@@ -183,6 +189,7 @@ Then populate `docs/tasks.md` with tasks derived from the issues:
|
||||
- Create feature branches: `feature/<milestone-slug>` as the integration branch
|
||||
|
||||
Commit the bootstrap:
|
||||
|
||||
```bash
|
||||
git add docs/tasks.md docs/orchestrator-learnings.json
|
||||
git commit -m "chore: Bootstrap orchestrator for <target>"
|
||||
@@ -192,6 +199,7 @@ git push
|
||||
### 4b. Resume (existing tasks.md)
|
||||
|
||||
Read `docs/tasks.md` and validate:
|
||||
|
||||
- Schema matches expected format (id, status, description, issue, branch, etc.)
|
||||
- No tasks stuck in `in-progress` without an active agent
|
||||
- Dependencies are consistent (no circular deps, no done tasks blocking not-started)
|
||||
@@ -199,6 +207,7 @@ Read `docs/tasks.md` and validate:
|
||||
Mark any orphaned `in-progress` tasks as `not-started` (previous agent likely lost context).
|
||||
|
||||
Report to user:
|
||||
|
||||
```
|
||||
=== Resume Status ===
|
||||
Total tasks: 15
|
||||
@@ -262,12 +271,16 @@ When spawning a worker, provide this structure:
|
||||
**Base:** {develop|main}
|
||||
|
||||
### Requirements
|
||||
|
||||
{Issue description and acceptance criteria}
|
||||
|
||||
### Quality Gates (MANDATORY — zero tolerance)
|
||||
|
||||
Run ALL of these before reporting success. Fix every failure.
|
||||
```
|
||||
|
||||
{quality gate commands from project}
|
||||
|
||||
```
|
||||
**Linting is NON-NEGOTIABLE.** Run the project linter and fix ALL violations
|
||||
in every file you touched. Do NOT leave lint warnings, do NOT disable rules,
|
||||
@@ -314,17 +327,18 @@ Then STOP COMPLETELY. Do not continue working.
|
||||
|
||||
If project docs (`AGENTS.md`/`SOUL.md`/`CLAUDE.md`) don't specify quality gates, check for these patterns:
|
||||
|
||||
| File | Likely Quality Gates |
|
||||
|------|---------------------|
|
||||
| `package.json` with `scripts.lint` | `pnpm lint` or `npm run lint` |
|
||||
| `package.json` with `scripts.test` | `pnpm test` or `npm run test` |
|
||||
| `package.json` with `scripts.typecheck` | `pnpm typecheck` |
|
||||
| `tsconfig.json` | `pnpm tsc --noEmit` |
|
||||
| `pyproject.toml` | `pytest`, `ruff check`, `mypy` |
|
||||
| `.woodpecker.yml` | Parse pipeline steps for commands |
|
||||
| `Makefile` | `make lint`, `make test` |
|
||||
| File | Likely Quality Gates |
|
||||
| --------------------------------------- | --------------------------------- |
|
||||
| `package.json` with `scripts.lint` | `pnpm lint` or `npm run lint` |
|
||||
| `package.json` with `scripts.test` | `pnpm test` or `npm run test` |
|
||||
| `package.json` with `scripts.typecheck` | `pnpm typecheck` |
|
||||
| `tsconfig.json` | `pnpm tsc --noEmit` |
|
||||
| `pyproject.toml` | `pytest`, `ruff check`, `mypy` |
|
||||
| `.woodpecker.yml` | Parse pipeline steps for commands |
|
||||
| `Makefile` | `make lint`, `make test` |
|
||||
|
||||
If no quality gates can be detected, ask the user:
|
||||
|
||||
```
|
||||
I couldn't detect quality gate commands for this project.
|
||||
What commands should workers run to verify their changes?
|
||||
|
||||
@@ -20,6 +20,7 @@ If `.mosaic/product-marketing-context.md` exists, read it before asking question
|
||||
The best companies don't just launch once—they launch again and again. Every new feature, improvement, and update is an opportunity to capture attention and engage your audience.
|
||||
|
||||
A strong launch isn't about a single moment. It's about:
|
||||
|
||||
- Getting your product into users' hands early
|
||||
- Learning from real feedback
|
||||
- Making a splash at every stage
|
||||
@@ -32,9 +33,11 @@ A strong launch isn't about a single moment. It's about:
|
||||
Structure your launch marketing across three channel types. Everything should ultimately lead back to owned channels.
|
||||
|
||||
### Owned Channels
|
||||
|
||||
You own the channel (though not the audience). Direct access without algorithms or platform rules.
|
||||
|
||||
**Examples:**
|
||||
|
||||
- Email list
|
||||
- Blog
|
||||
- Podcast
|
||||
@@ -42,12 +45,14 @@ You own the channel (though not the audience). Direct access without algorithms
|
||||
- Website/product
|
||||
|
||||
**Why they matter:**
|
||||
|
||||
- Get more effective over time
|
||||
- No algorithm changes or pay-to-play
|
||||
- Direct relationship with audience
|
||||
- Compound value from content
|
||||
|
||||
**Start with 1-2 based on audience:**
|
||||
|
||||
- Industry lacks quality content → Start a blog
|
||||
- People want direct updates → Focus on email
|
||||
- Engagement matters → Build a community
|
||||
@@ -56,15 +61,18 @@ You own the channel (though not the audience). Direct access without algorithms
|
||||
Built demand through an invite-only waitlist and one-on-one onboarding sessions. Every new user got a 30-minute live demo. This created exclusivity, FOMO, and word-of-mouth—all through owned relationships. Years later, their original onboarding materials still drive engagement.
|
||||
|
||||
### Rented Channels
|
||||
|
||||
Platforms that provide visibility but you don't control. Algorithms shift, rules change, pay-to-play increases.
|
||||
|
||||
**Examples:**
|
||||
|
||||
- Social media (Twitter/X, LinkedIn, Instagram)
|
||||
- App stores and marketplaces
|
||||
- YouTube
|
||||
- Reddit
|
||||
|
||||
**How to use correctly:**
|
||||
|
||||
- Pick 1-2 platforms where your audience is active
|
||||
- Use them to drive traffic to owned channels
|
||||
- Don't rely on them as your only strategy
|
||||
@@ -73,6 +81,7 @@ Platforms that provide visibility but you don't control. Algorithms shift, rules
|
||||
Hacked virality through Twitter, YouTube, and Reddit where productivity enthusiasts were active. Encouraged community to share templates and workflows. But they funneled all visibility into owned assets—every viral post led to signups, then targeted email onboarding.
|
||||
|
||||
**Platform-specific tactics:**
|
||||
|
||||
- Twitter/X: Threads that spark conversation → link to newsletter
|
||||
- LinkedIn: High-value posts → lead to gated content or email signup
|
||||
- Marketplaces (Shopify, Slack): Optimize listing → drive to site for more
|
||||
@@ -80,15 +89,18 @@ Hacked virality through Twitter, YouTube, and Reddit where productivity enthusia
|
||||
Rented channels give speed, not stability. Capture momentum by bringing users into your owned ecosystem.
|
||||
|
||||
### Borrowed Channels
|
||||
|
||||
Tap into someone else's audience to shortcut the hardest part—getting noticed.
|
||||
|
||||
**Examples:**
|
||||
|
||||
- Guest content (blog posts, podcast interviews, newsletter features)
|
||||
- Collaborations (webinars, co-marketing, social takeovers)
|
||||
- Speaking engagements (conferences, panels, virtual summits)
|
||||
- Influencer partnerships
|
||||
|
||||
**Be proactive, not passive:**
|
||||
|
||||
1. List industry leaders your audience follows
|
||||
2. Pitch win-win collaborations
|
||||
3. Use tools like SparkToro or Listen Notes to find audience overlap
|
||||
@@ -106,9 +118,11 @@ Borrowed channels give instant credibility, but only work if you convert borrowe
|
||||
Launching isn't a one-day event. It's a phased process that builds momentum.
|
||||
|
||||
### Phase 1: Internal Launch
|
||||
|
||||
Gather initial feedback and iron out major issues before going public.
|
||||
|
||||
**Actions:**
|
||||
|
||||
- Recruit early users one-on-one to test for free
|
||||
- Collect feedback on usability gaps and missing features
|
||||
- Ensure prototype is functional enough to demo (doesn't need to be production-ready)
|
||||
@@ -116,9 +130,11 @@ Gather initial feedback and iron out major issues before going public.
|
||||
**Goal:** Validate core functionality with friendly users.
|
||||
|
||||
### Phase 2: Alpha Launch
|
||||
|
||||
Put the product in front of external users in a controlled way.
|
||||
|
||||
**Actions:**
|
||||
|
||||
- Create landing page with early access signup form
|
||||
- Announce the product exists
|
||||
- Invite users individually to start testing
|
||||
@@ -127,14 +143,17 @@ Put the product in front of external users in a controlled way.
|
||||
**Goal:** First external validation and initial waitlist building.
|
||||
|
||||
### Phase 3: Beta Launch
|
||||
|
||||
Scale up early access while generating external buzz.
|
||||
|
||||
**Actions:**
|
||||
|
||||
- Work through early access list (some free, some paid)
|
||||
- Start marketing with teasers about problems you solve
|
||||
- Recruit friends, investors, and influencers to test and share
|
||||
|
||||
**Consider adding:**
|
||||
|
||||
- Coming soon landing page or waitlist
|
||||
- "Beta" sticker in dashboard navigation
|
||||
- Email invites to early access list
|
||||
@@ -143,29 +162,35 @@ Scale up early access while generating external buzz.
|
||||
**Goal:** Build buzz and refine product with broader feedback.
|
||||
|
||||
### Phase 4: Early Access Launch
|
||||
|
||||
Shift from small-scale testing to controlled expansion.
|
||||
|
||||
**Actions:**
|
||||
|
||||
- Leak product details: screenshots, feature GIFs, demos
|
||||
- Gather quantitative usage data and qualitative feedback
|
||||
- Run user research with engaged users (incentivize with credits)
|
||||
- Optionally run product/market fit survey to refine messaging
|
||||
|
||||
**Expansion options:**
|
||||
|
||||
- Option A: Throttle invites in batches (5-10% at a time)
|
||||
- Option B: Invite all users at once under "early access" framing
|
||||
|
||||
**Goal:** Validate at scale and prepare for full launch.
|
||||
|
||||
### Phase 5: Full Launch
|
||||
|
||||
Open the floodgates.
|
||||
|
||||
**Actions:**
|
||||
|
||||
- Open self-serve signups
|
||||
- Start charging (if not already)
|
||||
- Announce general availability across all channels
|
||||
|
||||
**Launch touchpoints:**
|
||||
|
||||
- Customer emails
|
||||
- In-app popups and product tours
|
||||
- Website banner linking to launch assets
|
||||
@@ -183,11 +208,13 @@ Open the floodgates.
|
||||
Product Hunt can be powerful for reaching early adopters, but it's not magic—it requires preparation.
|
||||
|
||||
### Pros
|
||||
|
||||
- Exposure to tech-savvy early adopter audience
|
||||
- Credibility bump (especially if Product of the Day)
|
||||
- Potential PR coverage and backlinks
|
||||
|
||||
### Cons
|
||||
|
||||
- Very competitive to rank well
|
||||
- Short-lived traffic spikes
|
||||
- Requires significant pre-launch planning
|
||||
@@ -195,6 +222,7 @@ Product Hunt can be powerful for reaching early adopters, but it's not magic—i
|
||||
### How to Launch Successfully
|
||||
|
||||
**Before launch day:**
|
||||
|
||||
1. Build relationships with influential supporters, content hubs, and communities
|
||||
2. Optimize your listing: compelling tagline, polished visuals, short demo video
|
||||
3. Study successful launches to identify what worked
|
||||
@@ -202,6 +230,7 @@ Product Hunt can be powerful for reaching early adopters, but it's not magic—i
|
||||
5. Prepare your team for all-day engagement
|
||||
|
||||
**On launch day:**
|
||||
|
||||
1. Treat it as an all-day event
|
||||
2. Respond to every comment in real-time
|
||||
3. Answer questions and spark discussions
|
||||
@@ -209,6 +238,7 @@ Product Hunt can be powerful for reaching early adopters, but it's not magic—i
|
||||
5. Direct traffic back to your site to capture signups
|
||||
|
||||
**After launch day:**
|
||||
|
||||
1. Follow up with everyone who engaged
|
||||
2. Convert Product Hunt traffic into owned relationships (email signups)
|
||||
3. Continue momentum with post-launch content
|
||||
@@ -216,12 +246,14 @@ Product Hunt can be powerful for reaching early adopters, but it's not magic—i
|
||||
### Case Studies
|
||||
|
||||
**SavvyCal** (Scheduling tool):
|
||||
|
||||
- Optimized landing page and onboarding before launch
|
||||
- Built relationships with productivity/SaaS influencers in advance
|
||||
- Responded to every comment on launch day
|
||||
- Result: #2 Product of the Month
|
||||
|
||||
**Reform** (Form builder):
|
||||
|
||||
- Studied successful launches and applied insights
|
||||
- Crafted clear tagline, polished visuals, demo video
|
||||
- Engaged in communities before launch (provided value first)
|
||||
@@ -253,6 +285,7 @@ Add dedicated sections about the new feature/product across your site.
|
||||
Create no-code interactive demo (using tools like Navattic) so visitors can explore before signing up.
|
||||
|
||||
### Keep Momentum Going
|
||||
|
||||
It's easier to build on existing momentum than start from scratch. Every touchpoint reinforces the launch.
|
||||
|
||||
---
|
||||
@@ -266,16 +299,19 @@ Don't rely on a single launch event. Regular updates and feature rollouts sustai
|
||||
Use this matrix to decide how much marketing each update deserves:
|
||||
|
||||
**Major updates** (new features, product overhauls):
|
||||
|
||||
- Full campaign across multiple channels
|
||||
- Blog post, email campaign, in-app messages, social media
|
||||
- Maximize exposure
|
||||
|
||||
**Medium updates** (new integrations, UI enhancements):
|
||||
|
||||
- Targeted announcement
|
||||
- Email to relevant segments, in-app banner
|
||||
- Don't need full fanfare
|
||||
|
||||
**Minor updates** (bug fixes, small tweaks):
|
||||
|
||||
- Changelog and release notes
|
||||
- Signal that product is improving
|
||||
- Don't dominate marketing
|
||||
@@ -299,6 +335,7 @@ Even small changelog updates remind customers your product is evolving. This bui
|
||||
## Launch Checklist
|
||||
|
||||
### Pre-Launch
|
||||
|
||||
- [ ] Landing page with clear value proposition
|
||||
- [ ] Email capture / waitlist signup
|
||||
- [ ] Early access list built
|
||||
@@ -311,6 +348,7 @@ Even small changelog updates remind customers your product is evolving. This bui
|
||||
- [ ] Analytics/tracking in place
|
||||
|
||||
### Launch Day
|
||||
|
||||
- [ ] Announcement email to list
|
||||
- [ ] Blog post published
|
||||
- [ ] Social posts scheduled and posted
|
||||
@@ -321,6 +359,7 @@ Even small changelog updates remind customers your product is evolving. This bui
|
||||
- [ ] Monitor for issues and feedback
|
||||
|
||||
### Post-Launch
|
||||
|
||||
- [ ] Onboarding email sequence active
|
||||
- [ ] Follow-up with engaged prospects
|
||||
- [ ] Roundup email includes announcement
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: lint
|
||||
description: "Enforce zero-tolerance linting on every code change. This skill MUST be followed after writing or modifying any code file. Run the project linter, fix ALL violations, and never cut corners. Triggers on: lint, delint, fix lint errors, clean up code, run linter."
|
||||
description: 'Enforce zero-tolerance linting on every code change. This skill MUST be followed after writing or modifying any code file. Run the project linter, fix ALL violations, and never cut corners. Triggers on: lint, delint, fix lint errors, clean up code, run linter.'
|
||||
---
|
||||
|
||||
# Lint — Zero Tolerance
|
||||
@@ -27,16 +27,16 @@ This is not optional. This is not "nice to have." This is a hard gate.
|
||||
|
||||
Check for these in order:
|
||||
|
||||
| File / Config | Linter | Command |
|
||||
|---------------|--------|---------|
|
||||
| `biome.json` or `biome.jsonc` | Biome | `pnpm biome check --write .` or `npx biome check --write .` |
|
||||
| `.eslintrc.*` or `eslint.config.*` or `package.json` has `eslint` | ESLint | `pnpm lint` or `npx eslint --fix .` |
|
||||
| `pyproject.toml` with `[tool.ruff]` | Ruff | `ruff check --fix .` |
|
||||
| `pyproject.toml` with `[tool.flake8]` or `.flake8` | Flake8 | `flake8 .` |
|
||||
| `pyproject.toml` with `[tool.pylint]` or `.pylintrc` | Pylint | `pylint **/*.py` |
|
||||
| `.rubocop.yml` | RuboCop | `rubocop -a .` |
|
||||
| `Cargo.toml` | Clippy | `cargo clippy --fix` |
|
||||
| `.golangci.yml` | golangci-lint | `golangci-lint run --fix` |
|
||||
| File / Config | Linter | Command |
|
||||
| ----------------------------------------------------------------- | ------------- | ----------------------------------------------------------- |
|
||||
| `biome.json` or `biome.jsonc` | Biome | `pnpm biome check --write .` or `npx biome check --write .` |
|
||||
| `.eslintrc.*` or `eslint.config.*` or `package.json` has `eslint` | ESLint | `pnpm lint` or `npx eslint --fix .` |
|
||||
| `pyproject.toml` with `[tool.ruff]` | Ruff | `ruff check --fix .` |
|
||||
| `pyproject.toml` with `[tool.flake8]` or `.flake8` | Flake8 | `flake8 .` |
|
||||
| `pyproject.toml` with `[tool.pylint]` or `.pylintrc` | Pylint | `pylint **/*.py` |
|
||||
| `.rubocop.yml` | RuboCop | `rubocop -a .` |
|
||||
| `Cargo.toml` | Clippy | `cargo clippy --fix` |
|
||||
| `.golangci.yml` | golangci-lint | `golangci-lint run --fix` |
|
||||
|
||||
Also check `package.json` scripts for a `lint` or `lint:fix` command — prefer the project's configured command over raw tool invocations.
|
||||
|
||||
@@ -64,7 +64,9 @@ If the project doesn't have a `lint` script, run the detected tool directly (see
|
||||
## Step 3: Fix All Violations
|
||||
|
||||
### Auto-fixable violations
|
||||
|
||||
Most linters have a `--fix` flag. Use it first:
|
||||
|
||||
```bash
|
||||
pnpm lint --fix
|
||||
# or
|
||||
@@ -74,25 +76,28 @@ ruff check --fix .
|
||||
```
|
||||
|
||||
### Manual violations
|
||||
|
||||
After auto-fix, re-run the linter without `--fix`. For each remaining error:
|
||||
|
||||
1. Read the rule name and understand WHY it exists
|
||||
2. Fix the code to comply with the rule
|
||||
3. Do NOT add a disable comment
|
||||
|
||||
### Common violations and correct fixes
|
||||
|
||||
| Violation | Wrong Fix | Right Fix |
|
||||
|-----------|-----------|-----------|
|
||||
| `no-unused-vars` | `// eslint-disable-next-line` | Delete the unused variable |
|
||||
| `@typescript-eslint/no-explicit-any` | `// eslint-disable` | Add a proper type |
|
||||
| `prefer-const` | Ignore it | Change `let` to `const` |
|
||||
| `no-console` | `// eslint-disable` | Use the project's logger |
|
||||
| Import order | Ignore it | Let auto-fix sort imports |
|
||||
| `any` type | `as unknown as Type` | Define the correct type |
|
||||
| Violation | Wrong Fix | Right Fix |
|
||||
| ------------------------------------ | ----------------------------- | -------------------------- |
|
||||
| `no-unused-vars` | `// eslint-disable-next-line` | Delete the unused variable |
|
||||
| `@typescript-eslint/no-explicit-any` | `// eslint-disable` | Add a proper type |
|
||||
| `prefer-const` | Ignore it | Change `let` to `const` |
|
||||
| `no-console` | `// eslint-disable` | Use the project's logger |
|
||||
| Import order | Ignore it | Let auto-fix sort imports |
|
||||
| `any` type | `as unknown as Type` | Define the correct type |
|
||||
|
||||
### The only acceptable exception
|
||||
|
||||
If fixing a violation would require a major refactor outside your task scope:
|
||||
|
||||
1. Do NOT disable the rule
|
||||
2. Document it as a deferred item with rationale
|
||||
3. Create a follow-up task/issue for the fix
|
||||
|
||||
@@ -14,6 +14,7 @@ You are a marketing strategist with a library of 139 proven marketing ideas. You
|
||||
If `.mosaic/product-marketing-context.md` exists, read it before asking questions. Use that context and only ask for information not already covered or specific to this task.
|
||||
|
||||
When asked for marketing ideas:
|
||||
|
||||
1. Ask about their product, audience, and current stage if not clear
|
||||
2. Suggest 3-5 most relevant ideas based on their context
|
||||
3. Provide details on implementation for chosen ideas
|
||||
@@ -23,25 +24,25 @@ When asked for marketing ideas:
|
||||
|
||||
## Ideas by Category (Quick Reference)
|
||||
|
||||
| Category | Ideas | Examples |
|
||||
|----------|-------|----------|
|
||||
| Content & SEO | 1-10 | Programmatic SEO, Glossary marketing, Content repurposing |
|
||||
| Competitor | 11-13 | Comparison pages, Marketing jiu-jitsu |
|
||||
| Free Tools | 14-22 | Calculators, Generators, Chrome extensions |
|
||||
| Paid Ads | 23-34 | LinkedIn, Google, Retargeting, Podcast ads |
|
||||
| Social & Community | 35-44 | LinkedIn audience, Reddit marketing, Short-form video |
|
||||
| Email | 45-53 | Founder emails, Onboarding sequences, Win-back |
|
||||
| Partnerships | 54-64 | Affiliate programs, Integration marketing, Newsletter swaps |
|
||||
| Events | 65-72 | Webinars, Conference speaking, Virtual summits |
|
||||
| PR & Media | 73-76 | Press coverage, Documentaries |
|
||||
| Launches | 77-86 | Product Hunt, Lifetime deals, Giveaways |
|
||||
| Product-Led | 87-96 | Viral loops, Powered-by marketing, Free migrations |
|
||||
| Content Formats | 97-109 | Podcasts, Courses, Annual reports, Year wraps |
|
||||
| Unconventional | 110-122 | Awards, Challenges, Guerrilla marketing |
|
||||
| Platforms | 123-130 | App marketplaces, Review sites, YouTube |
|
||||
| International | 131-132 | Expansion, Price localization |
|
||||
| Developer | 133-136 | DevRel, Certifications |
|
||||
| Audience-Specific | 137-139 | Referrals, Podcast tours, Customer language |
|
||||
| Category | Ideas | Examples |
|
||||
| ------------------ | ------- | ----------------------------------------------------------- |
|
||||
| Content & SEO | 1-10 | Programmatic SEO, Glossary marketing, Content repurposing |
|
||||
| Competitor | 11-13 | Comparison pages, Marketing jiu-jitsu |
|
||||
| Free Tools | 14-22 | Calculators, Generators, Chrome extensions |
|
||||
| Paid Ads | 23-34 | LinkedIn, Google, Retargeting, Podcast ads |
|
||||
| Social & Community | 35-44 | LinkedIn audience, Reddit marketing, Short-form video |
|
||||
| Email | 45-53 | Founder emails, Onboarding sequences, Win-back |
|
||||
| Partnerships | 54-64 | Affiliate programs, Integration marketing, Newsletter swaps |
|
||||
| Events | 65-72 | Webinars, Conference speaking, Virtual summits |
|
||||
| PR & Media | 73-76 | Press coverage, Documentaries |
|
||||
| Launches | 77-86 | Product Hunt, Lifetime deals, Giveaways |
|
||||
| Product-Led | 87-96 | Viral loops, Powered-by marketing, Free migrations |
|
||||
| Content Formats | 97-109 | Podcasts, Courses, Annual reports, Year wraps |
|
||||
| Unconventional | 110-122 | Awards, Challenges, Guerrilla marketing |
|
||||
| Platforms | 123-130 | App marketplaces, Review sites, YouTube |
|
||||
| International | 131-132 | Expansion, Price localization |
|
||||
| Developer | 133-136 | DevRel, Certifications |
|
||||
| Audience-Specific | 137-139 | Referrals, Podcast tours, Customer language |
|
||||
|
||||
**For the complete list with descriptions**: See [references/ideas-by-category.md](references/ideas-by-category.md)
|
||||
|
||||
@@ -52,21 +53,25 @@ When asked for marketing ideas:
|
||||
### By Stage
|
||||
|
||||
**Pre-launch:**
|
||||
|
||||
- Waitlist referrals (#79)
|
||||
- Early access pricing (#81)
|
||||
- Product Hunt prep (#78)
|
||||
|
||||
**Early stage:**
|
||||
|
||||
- Content & SEO (#1-10)
|
||||
- Community (#35)
|
||||
- Founder-led sales (#47)
|
||||
|
||||
**Growth stage:**
|
||||
|
||||
- Paid acquisition (#23-34)
|
||||
- Partnerships (#54-64)
|
||||
- Events (#65-72)
|
||||
|
||||
**Scale:**
|
||||
|
||||
- Brand campaigns
|
||||
- International (#131-132)
|
||||
- Media acquisitions (#73)
|
||||
@@ -74,22 +79,26 @@ When asked for marketing ideas:
|
||||
### By Budget
|
||||
|
||||
**Free:**
|
||||
|
||||
- Content & SEO
|
||||
- Community building
|
||||
- Social media
|
||||
- Comment marketing
|
||||
|
||||
**Low budget:**
|
||||
|
||||
- Targeted ads
|
||||
- Sponsorships
|
||||
- Free tools
|
||||
|
||||
**Medium budget:**
|
||||
|
||||
- Events
|
||||
- Partnerships
|
||||
- PR
|
||||
|
||||
**High budget:**
|
||||
|
||||
- Acquisitions
|
||||
- Conferences
|
||||
- Brand campaigns
|
||||
@@ -97,12 +106,15 @@ When asked for marketing ideas:
|
||||
### By Timeline
|
||||
|
||||
**Quick wins:**
|
||||
|
||||
- Ads, email, social posts
|
||||
|
||||
**Medium-term:**
|
||||
|
||||
- Content, SEO, community
|
||||
|
||||
**Long-term:**
|
||||
|
||||
- Brand, thought leadership, platform effects
|
||||
|
||||
---
|
||||
@@ -110,26 +122,31 @@ When asked for marketing ideas:
|
||||
## Top Ideas by Use Case
|
||||
|
||||
### Need Leads Fast
|
||||
|
||||
- Google Ads (#31) - High-intent search
|
||||
- LinkedIn Ads (#28) - B2B targeting
|
||||
- Engineering as Marketing (#15) - Free tool lead gen
|
||||
|
||||
### Building Authority
|
||||
|
||||
- Conference Speaking (#70)
|
||||
- Book Marketing (#104)
|
||||
- Podcasts (#107)
|
||||
|
||||
### Low Budget Growth
|
||||
|
||||
- Easy Keyword Ranking (#1)
|
||||
- Reddit Marketing (#38)
|
||||
- Comment Marketing (#44)
|
||||
|
||||
### Product-Led Growth
|
||||
|
||||
- Viral Loops (#93)
|
||||
- Powered By Marketing (#87)
|
||||
- In-App Upsells (#91)
|
||||
|
||||
### Enterprise Sales
|
||||
|
||||
- Investor Marketing (#133)
|
||||
- Expert Networks (#57)
|
||||
- Conference Sponsorship (#72)
|
||||
|
||||
@@ -246,25 +246,25 @@ Complete list of proven marketing approaches organized by category.
|
||||
|
||||
99. **Graphic Novel Marketing** - Transform complex stories into visual narratives.
|
||||
|
||||
100. **Promo Videos** - High-quality promotional videos showcase your product.
|
||||
100. **Promo Videos** - High-quality promotional videos showcase your product.
|
||||
|
||||
101. **Industry Interviews** - Interview customers, experts, and thought leaders.
|
||||
101. **Industry Interviews** - Interview customers, experts, and thought leaders.
|
||||
|
||||
102. **Social Screenshots** - Design shareable screenshot templates for social proof.
|
||||
102. **Social Screenshots** - Design shareable screenshot templates for social proof.
|
||||
|
||||
103. **Online Courses** - Educational courses establish authority while generating leads.
|
||||
103. **Online Courses** - Educational courses establish authority while generating leads.
|
||||
|
||||
104. **Book Marketing** - Author a book establishing expertise in your domain.
|
||||
104. **Book Marketing** - Author a book establishing expertise in your domain.
|
||||
|
||||
105. **Annual Reports** - Publish annual reports showcasing industry data and trends.
|
||||
105. **Annual Reports** - Publish annual reports showcasing industry data and trends.
|
||||
|
||||
106. **End of Year Wraps** - Personalized year-end summaries users want to share.
|
||||
106. **End of Year Wraps** - Personalized year-end summaries users want to share.
|
||||
|
||||
107. **Podcasts** - Launch a podcast reaching audiences during commutes.
|
||||
107. **Podcasts** - Launch a podcast reaching audiences during commutes.
|
||||
|
||||
108. **Changelogs** - Public changelogs showcase product momentum.
|
||||
108. **Changelogs** - Public changelogs showcase product momentum.
|
||||
|
||||
109. **Public Demos** - Live product demonstrations showing real usage.
|
||||
109. **Public Demos** - Live product demonstrations showing real usage.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -27,71 +27,85 @@ Mental models are thinking tools that help you make better decisions, understand
|
||||
These models sharpen your strategy and help you solve the right problems.
|
||||
|
||||
### First Principles
|
||||
|
||||
Break problems down to basic truths and build solutions from there. Instead of copying competitors, ask "why" repeatedly to find root causes. Use the 5 Whys technique to tunnel down to what really matters.
|
||||
|
||||
**Marketing application**: Don't assume you need content marketing because competitors do. Ask why you need it, what problem it solves, and whether there's a better solution.
|
||||
|
||||
### Jobs to Be Done
|
||||
|
||||
People don't buy products—they "hire" them to get a job done. Focus on the outcome customers want, not features.
|
||||
|
||||
**Marketing application**: A drill buyer doesn't want a drill—they want a hole. Frame your product around the job it accomplishes, not its specifications.
|
||||
|
||||
### Circle of Competence
|
||||
|
||||
Know what you're good at and stay within it. Venture outside only with proper learning or expert help.
|
||||
|
||||
**Marketing application**: Don't chase every channel. Double down where you have genuine expertise and competitive advantage.
|
||||
|
||||
### Inversion
|
||||
|
||||
Instead of asking "How do I succeed?", ask "What would guarantee failure?" Then avoid those things.
|
||||
|
||||
**Marketing application**: List everything that would make your campaign fail—confusing messaging, wrong audience, slow landing page—then systematically prevent each.
|
||||
|
||||
### Occam's Razor
|
||||
|
||||
The simplest explanation is usually correct. Avoid overcomplicating strategies or attributing results to complex causes when simple ones suffice.
|
||||
|
||||
**Marketing application**: If conversions dropped, check the obvious first (broken form, page speed) before assuming complex attribution issues.
|
||||
|
||||
### Pareto Principle (80/20 Rule)
|
||||
|
||||
Roughly 80% of results come from 20% of efforts. Identify and focus on the vital few.
|
||||
|
||||
**Marketing application**: Find the 20% of channels, customers, or content driving 80% of results. Cut or reduce the rest.
|
||||
|
||||
### Local vs. Global Optima
|
||||
|
||||
A local optimum is the best solution nearby, but a global optimum is the best overall. Don't get stuck optimizing the wrong thing.
|
||||
|
||||
**Marketing application**: Optimizing email subject lines (local) won't help if email isn't the right channel (global). Zoom out before zooming in.
|
||||
|
||||
### Theory of Constraints
|
||||
|
||||
Every system has one bottleneck limiting throughput. Find and fix that constraint before optimizing elsewhere.
|
||||
|
||||
**Marketing application**: If your funnel converts well but traffic is low, more conversion optimization won't help. Fix the traffic bottleneck first.
|
||||
|
||||
### Opportunity Cost
|
||||
|
||||
Every choice has a cost—what you give up by not choosing alternatives. Consider what you're saying no to.
|
||||
|
||||
**Marketing application**: Time spent on a low-ROI channel is time not spent on high-ROI activities. Always compare against alternatives.
|
||||
|
||||
### Law of Diminishing Returns
|
||||
|
||||
After a point, additional investment yields progressively smaller gains.
|
||||
|
||||
**Marketing application**: The 10th blog post won't have the same impact as the first. Know when to diversify rather than double down.
|
||||
|
||||
### Second-Order Thinking
|
||||
|
||||
Consider not just immediate effects, but the effects of those effects.
|
||||
|
||||
**Marketing application**: A flash sale boosts revenue (first order) but may train customers to wait for discounts (second order).
|
||||
|
||||
### Map ≠ Territory
|
||||
|
||||
Models and data represent reality but aren't reality itself. Don't confuse your analytics dashboard with actual customer experience.
|
||||
|
||||
**Marketing application**: Your customer persona is a useful model, but real customers are more complex. Stay in touch with actual users.
|
||||
|
||||
### Probabilistic Thinking
|
||||
|
||||
Think in probabilities, not certainties. Estimate likelihoods and plan for multiple outcomes.
|
||||
|
||||
**Marketing application**: Don't bet everything on one campaign. Spread risk and plan for scenarios where your primary strategy underperforms.
|
||||
|
||||
### Barbell Strategy
|
||||
|
||||
Combine extreme safety with small high-risk/high-reward bets. Avoid the mediocre middle.
|
||||
|
||||
**Marketing application**: Put 80% of budget into proven channels, 20% into experimental bets. Avoid moderate-risk, moderate-reward middle.
|
||||
@@ -103,111 +117,133 @@ Combine extreme safety with small high-risk/high-reward bets. Avoid the mediocre
|
||||
These models explain how customers think, decide, and behave.
|
||||
|
||||
### Fundamental Attribution Error
|
||||
|
||||
People attribute others' behavior to character, not circumstances. "They didn't buy because they're not serious" vs. "The checkout was confusing."
|
||||
|
||||
**Marketing application**: When customers don't convert, examine your process before blaming them. The problem is usually situational, not personal.
|
||||
|
||||
### Mere Exposure Effect
|
||||
|
||||
People prefer things they've seen before. Familiarity breeds liking.
|
||||
|
||||
**Marketing application**: Consistent brand presence builds preference over time. Repetition across channels creates comfort and trust.
|
||||
|
||||
### Availability Heuristic
|
||||
|
||||
People judge likelihood by how easily examples come to mind. Recent or vivid events seem more common.
|
||||
|
||||
**Marketing application**: Case studies and testimonials make success feel more achievable. Make positive outcomes easy to imagine.
|
||||
|
||||
### Confirmation Bias
|
||||
|
||||
People seek information confirming existing beliefs and ignore contradictory evidence.
|
||||
|
||||
**Marketing application**: Understand what your audience already believes and align messaging accordingly. Fighting beliefs head-on rarely works.
|
||||
|
||||
### The Lindy Effect
|
||||
|
||||
The longer something has survived, the longer it's likely to continue. Old ideas often outlast new ones.
|
||||
|
||||
**Marketing application**: Proven marketing principles (clear value props, social proof) outlast trendy tactics. Don't abandon fundamentals for fads.
|
||||
|
||||
### Mimetic Desire
|
||||
|
||||
People want things because others want them. Desire is socially contagious.
|
||||
|
||||
**Marketing application**: Show that desirable people want your product. Waitlists, exclusivity, and social proof trigger mimetic desire.
|
||||
|
||||
### Sunk Cost Fallacy
|
||||
|
||||
People continue investing in something because of past investment, even when it's no longer rational.
|
||||
|
||||
**Marketing application**: Know when to kill underperforming campaigns. Past spend shouldn't justify future spend if results aren't there.
|
||||
|
||||
### Endowment Effect
|
||||
|
||||
People value things more once they own them.
|
||||
|
||||
**Marketing application**: Free trials, samples, and freemium models let customers "own" the product, making them reluctant to give it up.
|
||||
|
||||
### IKEA Effect
|
||||
|
||||
People value things more when they've put effort into creating them.
|
||||
|
||||
**Marketing application**: Let customers customize, configure, or build something. Their investment increases perceived value and commitment.
|
||||
|
||||
### Zero-Price Effect
|
||||
|
||||
Free isn't just a low price—it's psychologically different. "Free" triggers irrational preference.
|
||||
|
||||
**Marketing application**: Free tiers, free trials, and free shipping have disproportionate appeal. The jump from $1 to $0 is bigger than $2 to $1.
|
||||
|
||||
### Hyperbolic Discounting / Present Bias
|
||||
|
||||
People strongly prefer immediate rewards over future ones, even when waiting is more rational.
|
||||
|
||||
**Marketing application**: Emphasize immediate benefits ("Start saving time today") over future ones ("You'll see ROI in 6 months").
|
||||
|
||||
### Status-Quo Bias
|
||||
|
||||
People prefer the current state of affairs. Change requires effort and feels risky.
|
||||
|
||||
**Marketing application**: Reduce friction to switch. Make the transition feel safe and easy. "Import your data in one click."
|
||||
|
||||
### Default Effect
|
||||
|
||||
People tend to accept pre-selected options. Defaults are powerful.
|
||||
|
||||
**Marketing application**: Pre-select the plan you want customers to choose. Opt-out beats opt-in for subscriptions (ethically applied).
|
||||
|
||||
### Paradox of Choice
|
||||
|
||||
Too many options overwhelm and paralyze. Fewer choices often lead to more decisions.
|
||||
|
||||
**Marketing application**: Limit options. Three pricing tiers beat seven. Recommend a single "best for most" option.
|
||||
|
||||
### Goal-Gradient Effect
|
||||
|
||||
People accelerate effort as they approach a goal. Progress visualization motivates action.
|
||||
|
||||
**Marketing application**: Show progress bars, completion percentages, and "almost there" messaging to drive completion.
|
||||
|
||||
### Peak-End Rule
|
||||
|
||||
People judge experiences by the peak (best or worst moment) and the end, not the average.
|
||||
|
||||
**Marketing application**: Design memorable peaks (surprise upgrades, delightful moments) and strong endings (thank you pages, follow-up emails).
|
||||
|
||||
### Zeigarnik Effect
|
||||
|
||||
Unfinished tasks occupy the mind more than completed ones. Open loops create tension.
|
||||
|
||||
**Marketing application**: "You're 80% done" creates pull to finish. Incomplete profiles, abandoned carts, and cliffhangers leverage this.
|
||||
|
||||
### Pratfall Effect
|
||||
|
||||
Competent people become more likable when they show a small flaw. Perfection is less relatable.
|
||||
|
||||
**Marketing application**: Admitting a weakness ("We're not the cheapest, but...") can increase trust and differentiation.
|
||||
|
||||
### Curse of Knowledge
|
||||
|
||||
Once you know something, you can't imagine not knowing it. Experts struggle to explain simply.
|
||||
|
||||
**Marketing application**: Your product seems obvious to you but confusing to newcomers. Test copy with people unfamiliar with your space.
|
||||
|
||||
### Mental Accounting
|
||||
|
||||
People treat money differently based on its source or intended use, even though money is fungible.
|
||||
|
||||
**Marketing application**: Frame costs in favorable mental accounts. "$3/day" feels different than "$90/month" even though it's the same.
|
||||
|
||||
### Regret Aversion
|
||||
|
||||
People avoid actions that might cause regret, even if the expected outcome is positive.
|
||||
|
||||
**Marketing application**: Address regret directly. Money-back guarantees, free trials, and "no commitment" messaging reduce regret fear.
|
||||
|
||||
### Bandwagon Effect / Social Proof
|
||||
|
||||
People follow what others are doing. Popularity signals quality and safety.
|
||||
|
||||
**Marketing application**: Show customer counts, testimonials, logos, reviews, and "trending" indicators. Numbers create confidence.
|
||||
@@ -219,66 +255,79 @@ People follow what others are doing. Popularity signals quality and safety.
|
||||
These models help you ethically influence customer decisions.
|
||||
|
||||
### Reciprocity Principle
|
||||
|
||||
People feel obligated to return favors. Give first, and people want to give back.
|
||||
|
||||
**Marketing application**: Free content, free tools, and generous free tiers create reciprocal obligation. Give value before asking for anything.
|
||||
|
||||
### Commitment & Consistency
|
||||
|
||||
Once people commit to something, they want to stay consistent with that commitment.
|
||||
|
||||
**Marketing application**: Get small commitments first (email signup, free trial). People who've taken one step are more likely to take the next.
|
||||
|
||||
### Authority Bias
|
||||
|
||||
People defer to experts and authority figures. Credentials and expertise create trust.
|
||||
|
||||
**Marketing application**: Feature expert endorsements, certifications, "featured in" logos, and thought leadership content.
|
||||
|
||||
### Liking / Similarity Bias
|
||||
|
||||
People say yes to those they like and those similar to themselves.
|
||||
|
||||
**Marketing application**: Use relatable spokespeople, founder stories, and community language. "Built by marketers for marketers" signals similarity.
|
||||
|
||||
### Unity Principle
|
||||
|
||||
Shared identity drives influence. "One of us" is powerful.
|
||||
|
||||
**Marketing application**: Position your brand as part of the customer's tribe. Use insider language and shared values.
|
||||
|
||||
### Scarcity / Urgency Heuristic
|
||||
|
||||
Limited availability increases perceived value. Scarcity signals desirability.
|
||||
|
||||
**Marketing application**: Limited-time offers, low-stock warnings, and exclusive access create urgency. Only use when genuine.
|
||||
|
||||
### Foot-in-the-Door Technique
|
||||
|
||||
Start with a small request, then escalate. Compliance with small requests leads to compliance with larger ones.
|
||||
|
||||
**Marketing application**: Free trial → paid plan → annual plan → enterprise. Each step builds on the last.
|
||||
|
||||
### Door-in-the-Face Technique
|
||||
|
||||
Start with an unreasonably large request, then retreat to what you actually want. The contrast makes the second request seem reasonable.
|
||||
|
||||
**Marketing application**: Show enterprise pricing first, then reveal the affordable starter plan. The contrast makes it feel like a deal.
|
||||
|
||||
### Loss Aversion / Prospect Theory
|
||||
|
||||
Losses feel roughly twice as painful as equivalent gains feel good. People will work harder to avoid losing than to gain.
|
||||
|
||||
**Marketing application**: Frame in terms of what they'll lose by not acting. "Don't miss out" beats "You could gain."
|
||||
|
||||
### Anchoring Effect
|
||||
|
||||
The first number people see heavily influences subsequent judgments.
|
||||
|
||||
**Marketing application**: Show the higher price first (original price, competitor price, enterprise tier) to anchor expectations.
|
||||
|
||||
### Decoy Effect
|
||||
|
||||
Adding a third, inferior option makes one of the original two look better.
|
||||
|
||||
**Marketing application**: A "decoy" pricing tier that's clearly worse value makes your preferred tier look like the obvious choice.
|
||||
|
||||
### Framing Effect
|
||||
|
||||
How something is presented changes how it's perceived. Same facts, different frames.
|
||||
|
||||
**Marketing application**: "90% success rate" vs. "10% failure rate" are identical but feel different. Frame positively.
|
||||
|
||||
### Contrast Effect
|
||||
|
||||
Things seem different depending on what they're compared to.
|
||||
|
||||
**Marketing application**: Show the "before" state clearly. The contrast with your "after" makes improvements vivid.
|
||||
@@ -290,26 +339,31 @@ Things seem different depending on what they're compared to.
|
||||
These models specifically address how people perceive and respond to prices.
|
||||
|
||||
### Charm Pricing / Left-Digit Effect
|
||||
|
||||
Prices ending in 9 seem significantly lower than the next round number. $99 feels much cheaper than $100.
|
||||
|
||||
**Marketing application**: Use .99 or .95 endings for value-focused products. The left digit dominates perception.
|
||||
|
||||
### Rounded-Price (Fluency) Effect
|
||||
|
||||
Round numbers feel premium and are easier to process. $100 signals quality; $99 signals value.
|
||||
|
||||
**Marketing application**: Use round prices for premium products ($500/month), charm prices for value products ($497/month).
|
||||
|
||||
### Rule of 100
|
||||
|
||||
For prices under $100, percentage discounts seem larger ("20% off"). For prices over $100, absolute discounts seem larger ("$50 off").
|
||||
|
||||
**Marketing application**: $80 product: "20% off" beats "$16 off." $500 product: "$100 off" beats "20% off."
|
||||
|
||||
### Price Relativity / Good-Better-Best
|
||||
|
||||
People judge prices relative to options presented. A middle tier seems reasonable between cheap and expensive.
|
||||
|
||||
**Marketing application**: Three tiers where the middle is your target. The expensive tier makes it look reasonable; the cheap tier provides an anchor.
|
||||
|
||||
### Mental Accounting (Pricing)
|
||||
|
||||
Framing the same price differently changes perception.
|
||||
|
||||
**Marketing application**: "$1/day" feels cheaper than "$30/month." "Less than your morning coffee" reframes the expense.
|
||||
@@ -321,51 +375,61 @@ Framing the same price differently changes perception.
|
||||
These models help you design effective marketing systems.
|
||||
|
||||
### Hick's Law
|
||||
|
||||
Decision time increases with the number and complexity of choices. More options = slower decisions = more abandonment.
|
||||
|
||||
**Marketing application**: Simplify choices. One clear CTA beats three. Fewer form fields beat more.
|
||||
|
||||
### AIDA Funnel
|
||||
|
||||
Attention → Interest → Desire → Action. The classic customer journey model.
|
||||
|
||||
**Marketing application**: Structure pages and campaigns to move through each stage. Capture attention before building desire.
|
||||
|
||||
### Rule of 7
|
||||
|
||||
Prospects need roughly 7 touchpoints before converting. One ad rarely converts; sustained presence does.
|
||||
|
||||
**Marketing application**: Build multi-touch campaigns across channels. Retargeting, email sequences, and consistent presence compound.
|
||||
|
||||
### Nudge Theory / Choice Architecture
|
||||
|
||||
Small changes in how choices are presented significantly influence decisions.
|
||||
|
||||
**Marketing application**: Default selections, strategic ordering, and friction reduction guide behavior without restricting choice.
|
||||
|
||||
### BJ Fogg Behavior Model
|
||||
|
||||
Behavior = Motivation × Ability × Prompt. All three must be present for action.
|
||||
|
||||
**Marketing application**: High motivation but hard to do = won't happen. Easy to do but no prompt = won't happen. Design for all three.
|
||||
|
||||
### EAST Framework
|
||||
|
||||
Make desired behaviors: Easy, Attractive, Social, Timely.
|
||||
|
||||
**Marketing application**: Reduce friction (easy), make it appealing (attractive), show others doing it (social), ask at the right moment (timely).
|
||||
|
||||
### COM-B Model
|
||||
|
||||
Behavior requires: Capability, Opportunity, Motivation.
|
||||
|
||||
**Marketing application**: Can they do it (capability)? Is the path clear (opportunity)? Do they want to (motivation)? Address all three.
|
||||
|
||||
### Activation Energy
|
||||
|
||||
The initial energy required to start something. High activation energy prevents action even if the task is easy overall.
|
||||
|
||||
**Marketing application**: Reduce starting friction. Pre-fill forms, offer templates, show quick wins. Make the first step trivially easy.
|
||||
|
||||
### North Star Metric
|
||||
|
||||
One metric that best captures the value you deliver to customers. Focus creates alignment.
|
||||
|
||||
**Marketing application**: Identify your North Star (active users, completed projects, revenue per customer) and align all efforts toward it.
|
||||
|
||||
### The Cobra Effect
|
||||
|
||||
When incentives backfire and produce the opposite of intended results.
|
||||
|
||||
**Marketing application**: Test incentive structures. A referral bonus might attract low-quality referrals gaming the system.
|
||||
@@ -377,41 +441,49 @@ When incentives backfire and produce the opposite of intended results.
|
||||
These models explain how marketing compounds and scales.
|
||||
|
||||
### Feedback Loops
|
||||
|
||||
Output becomes input, creating cycles. Positive loops accelerate growth; negative loops create decline.
|
||||
|
||||
**Marketing application**: Build virtuous cycles: more users → more content → better SEO → more users. Identify and strengthen positive loops.
|
||||
|
||||
### Compounding
|
||||
|
||||
Small, consistent gains accumulate into large results over time. Early gains matter most.
|
||||
|
||||
**Marketing application**: Consistent content, SEO, and brand building compound. Start early; benefits accumulate exponentially.
|
||||
|
||||
### Network Effects
|
||||
|
||||
A product becomes more valuable as more people use it.
|
||||
|
||||
**Marketing application**: Design features that improve with more users: shared workspaces, integrations, marketplaces, communities.
|
||||
|
||||
### Flywheel Effect
|
||||
|
||||
Sustained effort creates momentum that eventually maintains itself. Hard to start, easy to maintain.
|
||||
|
||||
**Marketing application**: Content → traffic → leads → customers → case studies → more content. Each element powers the next.
|
||||
|
||||
### Switching Costs
|
||||
|
||||
The price (time, money, effort, data) of changing to a competitor. High switching costs create retention.
|
||||
|
||||
**Marketing application**: Increase switching costs ethically: integrations, data accumulation, workflow customization, team adoption.
|
||||
|
||||
### Exploration vs. Exploitation
|
||||
|
||||
Balance trying new things (exploration) with optimizing what works (exploitation).
|
||||
|
||||
**Marketing application**: Don't abandon working channels for shiny new ones, but allocate some budget to experiments.
|
||||
|
||||
### Critical Mass / Tipping Point
|
||||
|
||||
The threshold after which growth becomes self-sustaining.
|
||||
|
||||
**Marketing application**: Focus resources on reaching critical mass in one segment before expanding. Depth before breadth.
|
||||
|
||||
### Survivorship Bias
|
||||
|
||||
Focusing on successes while ignoring failures that aren't visible.
|
||||
|
||||
**Marketing application**: Study failed campaigns, not just successful ones. The viral hit you're copying had 99 failures you didn't see.
|
||||
@@ -422,16 +494,16 @@ Focusing on successes while ignoring failures that aren't visible.
|
||||
|
||||
When facing a marketing challenge, consider:
|
||||
|
||||
| Challenge | Relevant Models |
|
||||
|-----------|-----------------|
|
||||
| Low conversions | Hick's Law, Activation Energy, BJ Fogg, Friction |
|
||||
| Price objections | Anchoring, Framing, Mental Accounting, Loss Aversion |
|
||||
| Building trust | Authority, Social Proof, Reciprocity, Pratfall Effect |
|
||||
| Increasing urgency | Scarcity, Loss Aversion, Zeigarnik Effect |
|
||||
| Retention/churn | Endowment Effect, Switching Costs, Status-Quo Bias |
|
||||
| Growth stalling | Theory of Constraints, Local vs Global Optima, Compounding |
|
||||
| Decision paralysis | Paradox of Choice, Default Effect, Nudge Theory |
|
||||
| Onboarding | Goal-Gradient, IKEA Effect, Commitment & Consistency |
|
||||
| Challenge | Relevant Models |
|
||||
| ------------------ | ---------------------------------------------------------- |
|
||||
| Low conversions | Hick's Law, Activation Energy, BJ Fogg, Friction |
|
||||
| Price objections | Anchoring, Framing, Mental Accounting, Loss Aversion |
|
||||
| Building trust | Authority, Social Proof, Reciprocity, Pratfall Effect |
|
||||
| Increasing urgency | Scarcity, Loss Aversion, Zeigarnik Effect |
|
||||
| Retention/churn | Endowment Effect, Switching Costs, Status-Quo Bias |
|
||||
| Growth stalling | Theory of Constraints, Local vs Global Optima, Compounding |
|
||||
| Decision paralysis | Paradox of Choice, Default Effect, Nudge Theory |
|
||||
| Onboarding | Goal-Gradient, IKEA Effect, Commitment & Consistency |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ Start with the sitemap to find relevant pages: `https://modelcontextprotocol.io/
|
||||
Then fetch specific pages with `.md` suffix for markdown format (e.g., `https://modelcontextprotocol.io/specification/draft.md`).
|
||||
|
||||
Key pages to review:
|
||||
|
||||
- Specification overview and architecture
|
||||
- Transport mechanisms (streamable HTTP, stdio)
|
||||
- Tool, resource, and prompt definitions
|
||||
@@ -50,6 +51,7 @@ Key pages to review:
|
||||
#### 1.3 Study Framework Documentation
|
||||
|
||||
**Recommended stack:**
|
||||
|
||||
- **Language**: TypeScript (high-quality SDK support and good compatibility in many execution environments e.g. MCPB. Plus AI models are good at generating TypeScript code, benefiting from its broad usage, static typing and good linting tools)
|
||||
- **Transport**: Streamable HTTP for remote servers, using stateless JSON (simpler to scale and maintain, as opposed to stateful sessions and streaming responses). stdio for local servers.
|
||||
|
||||
@@ -58,10 +60,12 @@ Key pages to review:
|
||||
- **MCP Best Practices**: [📋 View Best Practices](./reference/mcp_best_practices.md) - Core guidelines
|
||||
|
||||
**For TypeScript (recommended):**
|
||||
|
||||
- **TypeScript SDK**: Use WebFetch to load `https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/main/README.md`
|
||||
- [⚡ TypeScript Guide](./reference/node_mcp_server.md) - TypeScript patterns and examples
|
||||
|
||||
**For Python:**
|
||||
|
||||
- **Python SDK**: Use WebFetch to load `https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/main/README.md`
|
||||
- [🐍 Python Guide](./reference/python_mcp_server.md) - Python patterns and examples
|
||||
|
||||
@@ -80,12 +84,14 @@ Prioritize comprehensive API coverage. List endpoints to implement, starting wit
|
||||
#### 2.1 Set Up Project Structure
|
||||
|
||||
See language-specific guides for project setup:
|
||||
|
||||
- [⚡ TypeScript Guide](./reference/node_mcp_server.md) - Project structure, package.json, tsconfig.json
|
||||
- [🐍 Python Guide](./reference/python_mcp_server.md) - Module organization, dependencies
|
||||
|
||||
#### 2.2 Implement Core Infrastructure
|
||||
|
||||
Create shared utilities:
|
||||
|
||||
- API client with authentication
|
||||
- Error handling helpers
|
||||
- Response formatting (JSON/Markdown)
|
||||
@@ -96,27 +102,32 @@ Create shared utilities:
|
||||
For each tool:
|
||||
|
||||
**Input Schema:**
|
||||
|
||||
- Use Zod (TypeScript) or Pydantic (Python)
|
||||
- Include constraints and clear descriptions
|
||||
- Add examples in field descriptions
|
||||
|
||||
**Output Schema:**
|
||||
|
||||
- Define `outputSchema` where possible for structured data
|
||||
- Use `structuredContent` in tool responses (TypeScript SDK feature)
|
||||
- Helps clients understand and process tool outputs
|
||||
|
||||
**Tool Description:**
|
||||
|
||||
- Concise summary of functionality
|
||||
- Parameter descriptions
|
||||
- Return type schema
|
||||
|
||||
**Implementation:**
|
||||
|
||||
- Async/await for I/O operations
|
||||
- Proper error handling with actionable messages
|
||||
- Support pagination where applicable
|
||||
- Return both text content and structured data when using modern SDKs
|
||||
|
||||
**Annotations:**
|
||||
|
||||
- `readOnlyHint`: true/false
|
||||
- `destructiveHint`: true/false
|
||||
- `idempotentHint`: true/false
|
||||
@@ -129,6 +140,7 @@ For each tool:
|
||||
#### 3.1 Code Quality
|
||||
|
||||
Review for:
|
||||
|
||||
- No duplicated code (DRY principle)
|
||||
- Consistent error handling
|
||||
- Full type coverage
|
||||
@@ -137,10 +149,12 @@ Review for:
|
||||
#### 3.2 Build and Test
|
||||
|
||||
**TypeScript:**
|
||||
|
||||
- Run `npm run build` to verify compilation
|
||||
- Test with MCP Inspector: `npx @modelcontextprotocol/inspector`
|
||||
|
||||
**Python:**
|
||||
|
||||
- Verify syntax: `python -m py_compile your_server.py`
|
||||
- Test with MCP Inspector
|
||||
|
||||
@@ -170,6 +184,7 @@ To create effective evaluations, follow the process outlined in the evaluation g
|
||||
#### 4.3 Evaluation Requirements
|
||||
|
||||
Ensure each question is:
|
||||
|
||||
- **Independent**: Not dependent on other questions
|
||||
- **Read-only**: Only non-destructive operations required
|
||||
- **Complex**: Requiring multiple tool calls and deep exploration
|
||||
@@ -200,6 +215,7 @@ Create an XML file with this structure:
|
||||
Load these resources as needed during development:
|
||||
|
||||
### Core MCP Documentation (Load First)
|
||||
|
||||
- **MCP Protocol**: Start with sitemap at `https://modelcontextprotocol.io/sitemap.xml`, then fetch specific pages with `.md` suffix
|
||||
- [📋 MCP Best Practices](./reference/mcp_best_practices.md) - Universal MCP guidelines including:
|
||||
- Server and tool naming conventions
|
||||
@@ -209,10 +225,12 @@ Load these resources as needed during development:
|
||||
- Security and error handling standards
|
||||
|
||||
### SDK Documentation (Load During Phase 1/2)
|
||||
|
||||
- **Python SDK**: Fetch from `https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/main/README.md`
|
||||
- **TypeScript SDK**: Fetch from `https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/main/README.md`
|
||||
|
||||
### Language-Specific Implementation Guides (Load During Phase 2)
|
||||
|
||||
- [🐍 Python Implementation Guide](./reference/python_mcp_server.md) - Complete Python/FastMCP guide with:
|
||||
- Server initialization patterns
|
||||
- Pydantic model examples
|
||||
@@ -228,6 +246,7 @@ Load these resources as needed during development:
|
||||
- Quality checklist
|
||||
|
||||
### Evaluation Guide (Load During Phase 4)
|
||||
|
||||
- [✅ Evaluation Guide](./reference/evaluation.md) - Complete evaluation creation guide with:
|
||||
- Question creation guidelines
|
||||
- Answer verification strategies
|
||||
|
||||
@@ -9,6 +9,7 @@ This document provides guidance on creating comprehensive evaluations for MCP se
|
||||
## Quick Reference
|
||||
|
||||
### Evaluation Requirements
|
||||
|
||||
- Create 10 human-readable questions
|
||||
- Questions must be READ-ONLY, INDEPENDENT, NON-DESTRUCTIVE
|
||||
- Each question requires multiple tool calls (potentially dozens)
|
||||
@@ -16,6 +17,7 @@ This document provides guidance on creating comprehensive evaluations for MCP se
|
||||
- Answers must be STABLE (won't change over time)
|
||||
|
||||
### Output Format
|
||||
|
||||
```xml
|
||||
<evaluation>
|
||||
<qa_pair>
|
||||
@@ -34,6 +36,7 @@ The measure of quality of an MCP server is NOT how well or comprehensively the s
|
||||
## Evaluation Overview
|
||||
|
||||
Create 10 human-readable questions requiring ONLY READ-ONLY, INDEPENDENT, NON-DESTRUCTIVE, and IDEMPOTENT operations to answer. Each question should be:
|
||||
|
||||
- Realistic
|
||||
- Clear and concise
|
||||
- Unambiguous
|
||||
@@ -176,6 +179,7 @@ Create 10 human-readable questions requiring ONLY READ-ONLY, INDEPENDENT, NON-DE
|
||||
### Step 1: Documentation Inspection
|
||||
|
||||
Read the documentation of the target API to understand:
|
||||
|
||||
- Available endpoints and functionality
|
||||
- If ambiguity exists, fetch additional information from the web
|
||||
- Parallelize this step AS MUCH AS POSSIBLE
|
||||
@@ -184,6 +188,7 @@ Read the documentation of the target API to understand:
|
||||
### Step 2: Tool Inspection
|
||||
|
||||
List the tools available in the MCP server:
|
||||
|
||||
- Inspect the MCP server directly
|
||||
- Understand input/output schemas, docstrings, and descriptions
|
||||
- WITHOUT calling the tools themselves at this stage
|
||||
@@ -191,6 +196,7 @@ List the tools available in the MCP server:
|
||||
### Step 3: Developing Understanding
|
||||
|
||||
Repeat steps 1 & 2 until you have a good understanding:
|
||||
|
||||
- Iterate multiple times
|
||||
- Think about the kinds of tasks you want to create
|
||||
- Refine your understanding
|
||||
@@ -200,6 +206,7 @@ Repeat steps 1 & 2 until you have a good understanding:
|
||||
### Step 4: Read-Only Content Inspection
|
||||
|
||||
After understanding the API and tools, USE the MCP server tools:
|
||||
|
||||
- Inspect content using READ-ONLY and NON-DESTRUCTIVE operations ONLY
|
||||
- Goal: identify specific content (e.g., users, channels, messages, projects, tasks) for creating realistic questions
|
||||
- Should NOT call any tools that modify state
|
||||
@@ -214,6 +221,7 @@ After understanding the API and tools, USE the MCP server tools:
|
||||
### Step 5: Task Generation
|
||||
|
||||
After inspecting the content, create 10 human-readable questions:
|
||||
|
||||
- An LLM should be able to answer these with the MCP server
|
||||
- Follow all question and answer guidelines above
|
||||
|
||||
@@ -247,6 +255,7 @@ Each QA pair consists of a question and an answer. The output should be an XML f
|
||||
### Good Questions
|
||||
|
||||
**Example 1: Multi-hop question requiring deep exploration (GitHub MCP)**
|
||||
|
||||
```xml
|
||||
<qa_pair>
|
||||
<question>Find the repository that was archived in Q3 2023 and had previously been the most forked project in the organization. What was the primary programming language used in that repository?</question>
|
||||
@@ -255,6 +264,7 @@ Each QA pair consists of a question and an answer. The output should be an XML f
|
||||
```
|
||||
|
||||
This question is good because:
|
||||
|
||||
- Requires multiple searches to find archived repositories
|
||||
- Needs to identify which had the most forks before archival
|
||||
- Requires examining repository details for the language
|
||||
@@ -262,6 +272,7 @@ This question is good because:
|
||||
- Based on historical (closed) data that won't change
|
||||
|
||||
**Example 2: Requires understanding context without keyword matching (Project Management MCP)**
|
||||
|
||||
```xml
|
||||
<qa_pair>
|
||||
<question>Locate the initiative focused on improving customer onboarding that was completed in late 2023. The project lead created a retrospective document after completion. What was the lead's role title at that time?</question>
|
||||
@@ -270,6 +281,7 @@ This question is good because:
|
||||
```
|
||||
|
||||
This question is good because:
|
||||
|
||||
- Doesn't use specific project name ("initiative focused on improving customer onboarding")
|
||||
- Requires finding completed projects from specific timeframe
|
||||
- Needs to identify the project lead and their role
|
||||
@@ -278,6 +290,7 @@ This question is good because:
|
||||
- Based on completed work (won't change)
|
||||
|
||||
**Example 3: Complex aggregation requiring multiple steps (Issue Tracker MCP)**
|
||||
|
||||
```xml
|
||||
<qa_pair>
|
||||
<question>Among all bugs reported in January 2024 that were marked as critical priority, which assignee resolved the highest percentage of their assigned bugs within 48 hours? Provide the assignee's username.</question>
|
||||
@@ -286,6 +299,7 @@ This question is good because:
|
||||
```
|
||||
|
||||
This question is good because:
|
||||
|
||||
- Requires filtering bugs by date, priority, and status
|
||||
- Needs to group by assignee and calculate resolution rates
|
||||
- Requires understanding timestamps to determine 48-hour windows
|
||||
@@ -294,6 +308,7 @@ This question is good because:
|
||||
- Based on historical data from specific time period
|
||||
|
||||
**Example 4: Requires synthesis across multiple data types (CRM MCP)**
|
||||
|
||||
```xml
|
||||
<qa_pair>
|
||||
<question>Find the account that upgraded from the Starter to Enterprise plan in Q4 2023 and had the highest annual contract value. What industry does this account operate in?</question>
|
||||
@@ -302,6 +317,7 @@ This question is good because:
|
||||
```
|
||||
|
||||
This question is good because:
|
||||
|
||||
- Requires understanding subscription tier changes
|
||||
- Needs to identify upgrade events in specific timeframe
|
||||
- Requires comparing contract values
|
||||
@@ -312,6 +328,7 @@ This question is good because:
|
||||
### Poor Questions
|
||||
|
||||
**Example 1: Answer changes over time**
|
||||
|
||||
```xml
|
||||
<qa_pair>
|
||||
<question>How many open issues are currently assigned to the engineering team?</question>
|
||||
@@ -320,11 +337,13 @@ This question is good because:
|
||||
```
|
||||
|
||||
This question is poor because:
|
||||
|
||||
- The answer will change as issues are created, closed, or reassigned
|
||||
- Not based on stable/stationary data
|
||||
- Relies on "current state" which is dynamic
|
||||
|
||||
**Example 2: Too easy with keyword search**
|
||||
|
||||
```xml
|
||||
<qa_pair>
|
||||
<question>Find the pull request with title "Add authentication feature" and tell me who created it.</question>
|
||||
@@ -333,11 +352,13 @@ This question is poor because:
|
||||
```
|
||||
|
||||
This question is poor because:
|
||||
|
||||
- Can be solved with a straightforward keyword search for exact title
|
||||
- Doesn't require deep exploration or understanding
|
||||
- No synthesis or analysis needed
|
||||
|
||||
**Example 3: Ambiguous answer format**
|
||||
|
||||
```xml
|
||||
<qa_pair>
|
||||
<question>List all the repositories that have Python as their primary language.</question>
|
||||
@@ -346,6 +367,7 @@ This question is poor because:
|
||||
```
|
||||
|
||||
This question is poor because:
|
||||
|
||||
- Answer is a list that could be returned in any order
|
||||
- Difficult to verify with direct string comparison
|
||||
- LLM might format differently (JSON array, comma-separated, newline-separated)
|
||||
@@ -388,6 +410,7 @@ After creating your evaluation file, you can use the provided evaluation harness
|
||||
```
|
||||
|
||||
Or install manually:
|
||||
|
||||
```bash
|
||||
pip install anthropic mcp
|
||||
```
|
||||
@@ -420,6 +443,7 @@ Evaluation files use XML format with `<qa_pair>` elements:
|
||||
The evaluation script (`scripts/evaluation.py`) supports three transport types:
|
||||
|
||||
**Important:**
|
||||
|
||||
- **stdio transport**: The evaluation script automatically launches and manages the MCP server process for you. Do not run the server manually.
|
||||
- **sse/http transports**: You must start the MCP server separately before running the evaluation. The script connects to the already-running server at the specified URL.
|
||||
|
||||
@@ -436,6 +460,7 @@ python scripts/evaluation.py \
|
||||
```
|
||||
|
||||
With environment variables:
|
||||
|
||||
```bash
|
||||
python scripts/evaluation.py \
|
||||
-t stdio \
|
||||
@@ -580,6 +605,7 @@ python scripts/evaluation.py \
|
||||
### Connection Errors
|
||||
|
||||
If you get connection errors:
|
||||
|
||||
- **STDIO**: Verify the command and arguments are correct
|
||||
- **SSE/HTTP**: Check the URL is accessible and headers are correct
|
||||
- Ensure any required API keys are set in environment variables or headers
|
||||
@@ -587,6 +613,7 @@ If you get connection errors:
|
||||
### Low Accuracy
|
||||
|
||||
If many evaluations fail:
|
||||
|
||||
- Review the agent's feedback for each task
|
||||
- Check if tool descriptions are clear and comprehensive
|
||||
- Verify input parameters are well-documented
|
||||
@@ -596,7 +623,8 @@ If many evaluations fail:
|
||||
### Timeout Issues
|
||||
|
||||
If tasks are timing out:
|
||||
|
||||
- Use a more capable model (e.g., `claude-3-7-sonnet-20250219`)
|
||||
- Check if tools are returning too much data
|
||||
- Verify pagination is working correctly
|
||||
- Consider simplifying complex questions
|
||||
- Consider simplifying complex questions
|
||||
|
||||
@@ -3,25 +3,30 @@
|
||||
## Quick Reference
|
||||
|
||||
### Server Naming
|
||||
|
||||
- **Python**: `{service}_mcp` (e.g., `slack_mcp`)
|
||||
- **Node/TypeScript**: `{service}-mcp-server` (e.g., `slack-mcp-server`)
|
||||
|
||||
### Tool Naming
|
||||
|
||||
- Use snake_case with service prefix
|
||||
- Format: `{service}_{action}_{resource}`
|
||||
- Example: `slack_send_message`, `github_create_issue`
|
||||
|
||||
### Response Formats
|
||||
|
||||
- Support both JSON and Markdown formats
|
||||
- JSON for programmatic processing
|
||||
- Markdown for human readability
|
||||
|
||||
### Pagination
|
||||
|
||||
- Always respect `limit` parameter
|
||||
- Return `has_more`, `next_offset`, `total_count`
|
||||
- Default to 20-50 items
|
||||
|
||||
### Transport
|
||||
|
||||
- **Streamable HTTP**: For remote servers, multi-client scenarios
|
||||
- **stdio**: For local integrations, command-line tools
|
||||
- Avoid SSE (deprecated in favor of streamable HTTP)
|
||||
@@ -33,9 +38,11 @@
|
||||
Follow these standardized naming patterns:
|
||||
|
||||
**Python**: Use format `{service}_mcp` (lowercase with underscores)
|
||||
|
||||
- Examples: `slack_mcp`, `github_mcp`, `jira_mcp`
|
||||
|
||||
**Node/TypeScript**: Use format `{service}-mcp-server` (lowercase with hyphens)
|
||||
|
||||
- Examples: `slack-mcp-server`, `github-mcp-server`, `jira-mcp-server`
|
||||
|
||||
The name should be general, descriptive of the service being integrated, easy to infer from the task description, and without version numbers.
|
||||
@@ -67,12 +74,14 @@ The name should be general, descriptive of the service being integrated, easy to
|
||||
All tools that return data should support multiple formats:
|
||||
|
||||
### JSON Format (`response_format="json"`)
|
||||
|
||||
- Machine-readable structured data
|
||||
- Include all available fields and metadata
|
||||
- Consistent field names and types
|
||||
- Use for programmatic processing
|
||||
|
||||
### Markdown Format (`response_format="markdown"`, typically default)
|
||||
|
||||
- Human-readable formatted text
|
||||
- Use headers, lists, and formatting for clarity
|
||||
- Convert timestamps to human-readable format
|
||||
@@ -92,6 +101,7 @@ For tools that list resources:
|
||||
- **Default to reasonable limits**: 20-50 items is typical
|
||||
|
||||
Example pagination response:
|
||||
|
||||
```json
|
||||
{
|
||||
"total": 150,
|
||||
@@ -112,12 +122,14 @@ Example pagination response:
|
||||
**Best for**: Remote servers, web services, multi-client scenarios
|
||||
|
||||
**Characteristics**:
|
||||
|
||||
- Bidirectional communication over HTTP
|
||||
- Supports multiple simultaneous clients
|
||||
- Can be deployed as a web service
|
||||
- Enables server-to-client notifications
|
||||
|
||||
**Use when**:
|
||||
|
||||
- Serving multiple clients simultaneously
|
||||
- Deploying as a cloud service
|
||||
- Integration with web applications
|
||||
@@ -127,11 +139,13 @@ Example pagination response:
|
||||
**Best for**: Local integrations, command-line tools
|
||||
|
||||
**Characteristics**:
|
||||
|
||||
- Standard input/output stream communication
|
||||
- Simple setup, no network configuration needed
|
||||
- Runs as a subprocess of the client
|
||||
|
||||
**Use when**:
|
||||
|
||||
- Building tools for local development environments
|
||||
- Integrating with desktop applications
|
||||
- Single-user, single-session scenarios
|
||||
@@ -140,12 +154,12 @@ Example pagination response:
|
||||
|
||||
### Transport Selection
|
||||
|
||||
| Criterion | stdio | Streamable HTTP |
|
||||
|-----------|-------|-----------------|
|
||||
| **Deployment** | Local | Remote |
|
||||
| **Clients** | Single | Multiple |
|
||||
| **Complexity** | Low | Medium |
|
||||
| **Real-time** | No | Yes |
|
||||
| Criterion | stdio | Streamable HTTP |
|
||||
| -------------- | ------ | --------------- |
|
||||
| **Deployment** | Local | Remote |
|
||||
| **Clients** | Single | Multiple |
|
||||
| **Complexity** | Low | Medium |
|
||||
| **Real-time** | No | Yes |
|
||||
|
||||
---
|
||||
|
||||
@@ -154,11 +168,13 @@ Example pagination response:
|
||||
### Authentication and Authorization
|
||||
|
||||
**OAuth 2.1**:
|
||||
|
||||
- Use secure OAuth 2.1 with certificates from recognized authorities
|
||||
- Validate access tokens before processing requests
|
||||
- Only accept tokens specifically intended for your server
|
||||
|
||||
**API Keys**:
|
||||
|
||||
- Store API keys in environment variables, never in code
|
||||
- Validate keys on server startup
|
||||
- Provide clear error messages when authentication fails
|
||||
@@ -181,6 +197,7 @@ Example pagination response:
|
||||
### DNS Rebinding Protection
|
||||
|
||||
For streamable HTTP servers running locally:
|
||||
|
||||
- Enable DNS rebinding protection
|
||||
- Validate the `Origin` header on all incoming connections
|
||||
- Bind to `127.0.0.1` rather than `0.0.0.0`
|
||||
@@ -191,12 +208,12 @@ For streamable HTTP servers running locally:
|
||||
|
||||
Provide annotations to help clients understand tool behavior:
|
||||
|
||||
| Annotation | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `readOnlyHint` | boolean | false | Tool does not modify its environment |
|
||||
| `destructiveHint` | boolean | true | Tool may perform destructive updates |
|
||||
| `idempotentHint` | boolean | false | Repeated calls with same args have no additional effect |
|
||||
| `openWorldHint` | boolean | true | Tool interacts with external entities |
|
||||
| Annotation | Type | Default | Description |
|
||||
| ----------------- | ------- | ------- | ------------------------------------------------------- |
|
||||
| `readOnlyHint` | boolean | false | Tool does not modify its environment |
|
||||
| `destructiveHint` | boolean | true | Tool may perform destructive updates |
|
||||
| `idempotentHint` | boolean | false | Repeated calls with same args have no additional effect |
|
||||
| `openWorldHint` | boolean | true | Tool interacts with external entities |
|
||||
|
||||
**Important**: Annotations are hints, not security guarantees. Clients should not make security-critical decisions based solely on annotations.
|
||||
|
||||
@@ -211,17 +228,20 @@ Provide annotations to help clients understand tool behavior:
|
||||
- Clean up resources properly on errors
|
||||
|
||||
Example error handling:
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const result = performOperation();
|
||||
return { content: [{ type: "text", text: result }] };
|
||||
return { content: [{ type: 'text', text: result }] };
|
||||
} catch (error) {
|
||||
return {
|
||||
isError: true,
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `Error: ${error.message}. Try using filter='active_only' to reduce results.`
|
||||
}]
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error: ${error.message}. Try using filter='active_only' to reduce results.`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
@@ -9,39 +9,42 @@ This document provides Node/TypeScript-specific best practices and examples for
|
||||
## Quick Reference
|
||||
|
||||
### Key Imports
|
||||
|
||||
```typescript
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import express from "express";
|
||||
import { z } from "zod";
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import express from 'express';
|
||||
import { z } from 'zod';
|
||||
```
|
||||
|
||||
### Server Initialization
|
||||
|
||||
```typescript
|
||||
const server = new McpServer({
|
||||
name: "service-mcp-server",
|
||||
version: "1.0.0"
|
||||
name: 'service-mcp-server',
|
||||
version: '1.0.0',
|
||||
});
|
||||
```
|
||||
|
||||
### Tool Registration Pattern
|
||||
|
||||
```typescript
|
||||
server.registerTool(
|
||||
"tool_name",
|
||||
'tool_name',
|
||||
{
|
||||
title: "Tool Display Name",
|
||||
description: "What the tool does",
|
||||
title: 'Tool Display Name',
|
||||
description: 'What the tool does',
|
||||
inputSchema: { param: z.string() },
|
||||
outputSchema: { result: z.string() }
|
||||
outputSchema: { result: z.string() },
|
||||
},
|
||||
async ({ param }) => {
|
||||
const output = { result: `Processed: ${param}` };
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(output) }],
|
||||
structuredContent: output // Modern pattern for structured data
|
||||
content: [{ type: 'text', text: JSON.stringify(output) }],
|
||||
structuredContent: output, // Modern pattern for structured data
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
@@ -50,12 +53,14 @@ server.registerTool(
|
||||
## MCP TypeScript SDK
|
||||
|
||||
The official MCP TypeScript SDK provides:
|
||||
|
||||
- `McpServer` class for server initialization
|
||||
- `registerTool` method for tool registration
|
||||
- Zod schema integration for runtime input validation
|
||||
- Type-safe tool handler implementations
|
||||
|
||||
**IMPORTANT - Use Modern APIs Only:**
|
||||
|
||||
- **DO use**: `server.registerTool()`, `server.registerResource()`, `server.registerPrompt()`
|
||||
- **DO NOT use**: Old deprecated APIs such as `server.tool()`, `server.setRequestHandler(ListToolsRequestSchema, ...)`, or manual handler registration
|
||||
- The `register*` methods provide better type safety, automatic schema handling, and are the recommended approach
|
||||
@@ -65,10 +70,12 @@ See the MCP SDK documentation in the references for complete details.
|
||||
## Server Naming Convention
|
||||
|
||||
Node/TypeScript MCP servers must follow this naming pattern:
|
||||
|
||||
- **Format**: `{service}-mcp-server` (lowercase with hyphens)
|
||||
- **Examples**: `github-mcp-server`, `jira-mcp-server`, `stripe-mcp-server`
|
||||
|
||||
The name should be:
|
||||
|
||||
- General (not tied to specific features)
|
||||
- Descriptive of the service/API being integrated
|
||||
- Easy to infer from the task description
|
||||
@@ -100,6 +107,7 @@ Create the following structure for Node/TypeScript MCP servers:
|
||||
Use snake_case for tool names (e.g., "search_users", "create_project", "get_channel_info") with clear, action-oriented names.
|
||||
|
||||
**Avoid Naming Conflicts**: Include the service context to prevent overlaps:
|
||||
|
||||
- Use "slack_send_message" instead of just "send_message"
|
||||
- Use "github_create_issue" instead of just "create_issue"
|
||||
- Use "asana_list_tasks" instead of just "list_tasks"
|
||||
@@ -107,6 +115,7 @@ Use snake_case for tool names (e.g., "search_users", "create_project", "get_chan
|
||||
### Tool Structure
|
||||
|
||||
Tools are registered using the `registerTool` method with the following requirements:
|
||||
|
||||
- Use Zod schemas for runtime input validation and type safety
|
||||
- The `description` field must be explicitly provided - JSDoc comments are NOT automatically extracted
|
||||
- Explicitly provide `title`, `description`, `inputSchema`, and `annotations`
|
||||
@@ -114,43 +123,38 @@ Tools are registered using the `registerTool` method with the following requirem
|
||||
- Type all parameters and return values explicitly
|
||||
|
||||
```typescript
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { z } from "zod";
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { z } from 'zod';
|
||||
|
||||
const server = new McpServer({
|
||||
name: "example-mcp",
|
||||
version: "1.0.0"
|
||||
name: 'example-mcp',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
// Zod schema for input validation
|
||||
const UserSearchInputSchema = z.object({
|
||||
query: z.string()
|
||||
.min(2, "Query must be at least 2 characters")
|
||||
.max(200, "Query must not exceed 200 characters")
|
||||
.describe("Search string to match against names/emails"),
|
||||
limit: z.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(100)
|
||||
.default(20)
|
||||
.describe("Maximum results to return"),
|
||||
offset: z.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.default(0)
|
||||
.describe("Number of results to skip for pagination"),
|
||||
response_format: z.nativeEnum(ResponseFormat)
|
||||
.default(ResponseFormat.MARKDOWN)
|
||||
.describe("Output format: 'markdown' for human-readable or 'json' for machine-readable")
|
||||
}).strict();
|
||||
const UserSearchInputSchema = z
|
||||
.object({
|
||||
query: z
|
||||
.string()
|
||||
.min(2, 'Query must be at least 2 characters')
|
||||
.max(200, 'Query must not exceed 200 characters')
|
||||
.describe('Search string to match against names/emails'),
|
||||
limit: z.number().int().min(1).max(100).default(20).describe('Maximum results to return'),
|
||||
offset: z.number().int().min(0).default(0).describe('Number of results to skip for pagination'),
|
||||
response_format: z
|
||||
.nativeEnum(ResponseFormat)
|
||||
.default(ResponseFormat.MARKDOWN)
|
||||
.describe("Output format: 'markdown' for human-readable or 'json' for machine-readable"),
|
||||
})
|
||||
.strict();
|
||||
|
||||
// Type definition from Zod schema
|
||||
type UserSearchInput = z.infer<typeof UserSearchInputSchema>;
|
||||
|
||||
server.registerTool(
|
||||
"example_search_users",
|
||||
'example_search_users',
|
||||
{
|
||||
title: "Search Example Users",
|
||||
title: 'Search Example Users',
|
||||
description: `Search for users in the Example system by name, email, or team.
|
||||
|
||||
This tool searches across all user profiles in the Example platform, supporting partial matches and various search filters. It does NOT create or modify users, only searches existing ones.
|
||||
@@ -193,33 +197,30 @@ Error Handling:
|
||||
readOnlyHint: true,
|
||||
destructiveHint: false,
|
||||
idempotentHint: true,
|
||||
openWorldHint: true
|
||||
}
|
||||
openWorldHint: true,
|
||||
},
|
||||
},
|
||||
async (params: UserSearchInput) => {
|
||||
try {
|
||||
// Input validation is handled by Zod schema
|
||||
// Make API request using validated parameters
|
||||
const data = await makeApiRequest<any>(
|
||||
"users/search",
|
||||
"GET",
|
||||
undefined,
|
||||
{
|
||||
q: params.query,
|
||||
limit: params.limit,
|
||||
offset: params.offset
|
||||
}
|
||||
);
|
||||
const data = await makeApiRequest<any>('users/search', 'GET', undefined, {
|
||||
q: params.query,
|
||||
limit: params.limit,
|
||||
offset: params.offset,
|
||||
});
|
||||
|
||||
const users = data.users || [];
|
||||
const total = data.total || 0;
|
||||
|
||||
if (!users.length) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `No users found matching '${params.query}'`
|
||||
}]
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `No users found matching '${params.query}'`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -233,43 +234,51 @@ Error Handling:
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
...(user.team ? { team: user.team } : {}),
|
||||
active: user.active ?? true
|
||||
active: user.active ?? true,
|
||||
})),
|
||||
has_more: total > params.offset + users.length,
|
||||
...(total > params.offset + users.length ? {
|
||||
next_offset: params.offset + users.length
|
||||
} : {})
|
||||
...(total > params.offset + users.length
|
||||
? {
|
||||
next_offset: params.offset + users.length,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
// Format text representation based on requested format
|
||||
let textContent: string;
|
||||
if (params.response_format === ResponseFormat.MARKDOWN) {
|
||||
const lines = [`# User Search Results: '${params.query}'`, "",
|
||||
`Found ${total} users (showing ${users.length})`, ""];
|
||||
const lines = [
|
||||
`# User Search Results: '${params.query}'`,
|
||||
'',
|
||||
`Found ${total} users (showing ${users.length})`,
|
||||
'',
|
||||
];
|
||||
for (const user of users) {
|
||||
lines.push(`## ${user.name} (${user.id})`);
|
||||
lines.push(`- **Email**: ${user.email}`);
|
||||
if (user.team) lines.push(`- **Team**: ${user.team}`);
|
||||
lines.push("");
|
||||
lines.push('');
|
||||
}
|
||||
textContent = lines.join("\n");
|
||||
textContent = lines.join('\n');
|
||||
} else {
|
||||
textContent = JSON.stringify(output, null, 2);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: textContent }],
|
||||
structuredContent: output // Modern pattern for structured data
|
||||
content: [{ type: 'text', text: textContent }],
|
||||
structuredContent: output, // Modern pattern for structured data
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: handleApiError(error)
|
||||
}]
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: handleApiError(error),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
@@ -278,46 +287,38 @@ Error Handling:
|
||||
Zod provides runtime type validation:
|
||||
|
||||
```typescript
|
||||
import { z } from "zod";
|
||||
import { z } from 'zod';
|
||||
|
||||
// Basic schema with validation
|
||||
const CreateUserSchema = z.object({
|
||||
name: z.string()
|
||||
.min(1, "Name is required")
|
||||
.max(100, "Name must not exceed 100 characters"),
|
||||
email: z.string()
|
||||
.email("Invalid email format"),
|
||||
age: z.number()
|
||||
.int("Age must be a whole number")
|
||||
.min(0, "Age cannot be negative")
|
||||
.max(150, "Age cannot be greater than 150")
|
||||
}).strict(); // Use .strict() to forbid extra fields
|
||||
const CreateUserSchema = z
|
||||
.object({
|
||||
name: z.string().min(1, 'Name is required').max(100, 'Name must not exceed 100 characters'),
|
||||
email: z.string().email('Invalid email format'),
|
||||
age: z
|
||||
.number()
|
||||
.int('Age must be a whole number')
|
||||
.min(0, 'Age cannot be negative')
|
||||
.max(150, 'Age cannot be greater than 150'),
|
||||
})
|
||||
.strict(); // Use .strict() to forbid extra fields
|
||||
|
||||
// Enums
|
||||
enum ResponseFormat {
|
||||
MARKDOWN = "markdown",
|
||||
JSON = "json"
|
||||
MARKDOWN = 'markdown',
|
||||
JSON = 'json',
|
||||
}
|
||||
|
||||
const SearchSchema = z.object({
|
||||
response_format: z.nativeEnum(ResponseFormat)
|
||||
response_format: z
|
||||
.nativeEnum(ResponseFormat)
|
||||
.default(ResponseFormat.MARKDOWN)
|
||||
.describe("Output format")
|
||||
.describe('Output format'),
|
||||
});
|
||||
|
||||
// Optional fields with defaults
|
||||
const PaginationSchema = z.object({
|
||||
limit: z.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(100)
|
||||
.default(20)
|
||||
.describe("Maximum results to return"),
|
||||
offset: z.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.default(0)
|
||||
.describe("Number of results to skip")
|
||||
limit: z.number().int().min(1).max(100).default(20).describe('Maximum results to return'),
|
||||
offset: z.number().int().min(0).default(0).describe('Number of results to skip'),
|
||||
});
|
||||
```
|
||||
|
||||
@@ -327,19 +328,21 @@ Support multiple output formats for flexibility:
|
||||
|
||||
```typescript
|
||||
enum ResponseFormat {
|
||||
MARKDOWN = "markdown",
|
||||
JSON = "json"
|
||||
MARKDOWN = 'markdown',
|
||||
JSON = 'json',
|
||||
}
|
||||
|
||||
const inputSchema = z.object({
|
||||
query: z.string(),
|
||||
response_format: z.nativeEnum(ResponseFormat)
|
||||
response_format: z
|
||||
.nativeEnum(ResponseFormat)
|
||||
.default(ResponseFormat.MARKDOWN)
|
||||
.describe("Output format: 'markdown' for human-readable or 'json' for machine-readable")
|
||||
.describe("Output format: 'markdown' for human-readable or 'json' for machine-readable"),
|
||||
});
|
||||
```
|
||||
|
||||
**Markdown format**:
|
||||
|
||||
- Use headers, lists, and formatting for clarity
|
||||
- Convert timestamps to human-readable format
|
||||
- Show display names with IDs in parentheses
|
||||
@@ -347,6 +350,7 @@ const inputSchema = z.object({
|
||||
- Group related information logically
|
||||
|
||||
**JSON format**:
|
||||
|
||||
- Return complete, structured data suitable for programmatic processing
|
||||
- Include all available fields and metadata
|
||||
- Use consistent field names and types
|
||||
@@ -358,7 +362,7 @@ For tools that list resources:
|
||||
```typescript
|
||||
const ListSchema = z.object({
|
||||
limit: z.number().int().min(1).max(100).default(20),
|
||||
offset: z.number().int().min(0).default(0)
|
||||
offset: z.number().int().min(0).default(0),
|
||||
});
|
||||
|
||||
async function listItems(params: z.infer<typeof ListSchema>) {
|
||||
@@ -370,9 +374,10 @@ async function listItems(params: z.infer<typeof ListSchema>) {
|
||||
offset: params.offset,
|
||||
items: data.items,
|
||||
has_more: data.total > params.offset + data.items.length,
|
||||
next_offset: data.total > params.offset + data.items.length
|
||||
? params.offset + data.items.length
|
||||
: undefined
|
||||
next_offset:
|
||||
data.total > params.offset + data.items.length
|
||||
? params.offset + data.items.length
|
||||
: undefined,
|
||||
};
|
||||
|
||||
return JSON.stringify(response, null, 2);
|
||||
@@ -385,7 +390,7 @@ Add a CHARACTER_LIMIT constant to prevent overwhelming responses:
|
||||
|
||||
```typescript
|
||||
// At module level in constants.ts
|
||||
export const CHARACTER_LIMIT = 25000; // Maximum response size in characters
|
||||
export const CHARACTER_LIMIT = 25000; // Maximum response size in characters
|
||||
|
||||
async function searchTool(params: SearchInput) {
|
||||
let result = generateResponse(data);
|
||||
@@ -410,23 +415,23 @@ async function searchTool(params: SearchInput) {
|
||||
Provide clear, actionable error messages:
|
||||
|
||||
```typescript
|
||||
import axios, { AxiosError } from "axios";
|
||||
import axios, { AxiosError } from 'axios';
|
||||
|
||||
function handleApiError(error: unknown): string {
|
||||
if (error instanceof AxiosError) {
|
||||
if (error.response) {
|
||||
switch (error.response.status) {
|
||||
case 404:
|
||||
return "Error: Resource not found. Please check the ID is correct.";
|
||||
return 'Error: Resource not found. Please check the ID is correct.';
|
||||
case 403:
|
||||
return "Error: Permission denied. You don't have access to this resource.";
|
||||
case 429:
|
||||
return "Error: Rate limit exceeded. Please wait before making more requests.";
|
||||
return 'Error: Rate limit exceeded. Please wait before making more requests.';
|
||||
default:
|
||||
return `Error: API request failed with status ${error.response.status}`;
|
||||
}
|
||||
} else if (error.code === "ECONNABORTED") {
|
||||
return "Error: Request timed out. Please try again.";
|
||||
} else if (error.code === 'ECONNABORTED') {
|
||||
return 'Error: Request timed out. Please try again.';
|
||||
}
|
||||
}
|
||||
return `Error: Unexpected error occurred: ${error instanceof Error ? error.message : String(error)}`;
|
||||
@@ -441,9 +446,9 @@ Extract common functionality into reusable functions:
|
||||
// Shared API request function
|
||||
async function makeApiRequest<T>(
|
||||
endpoint: string,
|
||||
method: "GET" | "POST" | "PUT" | "DELETE" = "GET",
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
|
||||
data?: any,
|
||||
params?: any
|
||||
params?: any,
|
||||
): Promise<T> {
|
||||
try {
|
||||
const response = await axios({
|
||||
@@ -453,9 +458,9 @@ async function makeApiRequest<T>(
|
||||
params,
|
||||
timeout: 30000,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
}
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
@@ -477,8 +482,7 @@ async function fetchData(resourceId: string): Promise<ResourceData> {
|
||||
|
||||
// Bad: Promise chains
|
||||
function fetchData(resourceId: string): Promise<ResourceData> {
|
||||
return axios.get(`${API_URL}/resource/${resourceId}`)
|
||||
.then(response => response.data); // Harder to read and maintain
|
||||
return axios.get(`${API_URL}/resource/${resourceId}`).then((response) => response.data); // Harder to read and maintain
|
||||
}
|
||||
```
|
||||
|
||||
@@ -507,19 +511,19 @@ const UserSchema = z.object({
|
||||
name: z.string(),
|
||||
email: z.string().email(),
|
||||
team: z.string().optional(),
|
||||
active: z.boolean()
|
||||
active: z.boolean(),
|
||||
});
|
||||
|
||||
type User = z.infer<typeof UserSchema>;
|
||||
|
||||
async function getUser(id: string): Promise<User> {
|
||||
const data = await apiCall(`/users/${id}`);
|
||||
return UserSchema.parse(data); // Runtime validation
|
||||
return UserSchema.parse(data); // Runtime validation
|
||||
}
|
||||
|
||||
// Bad: Using any
|
||||
async function getUser(id: string): Promise<any> {
|
||||
return await apiCall(`/users/${id}`); // No type safety
|
||||
return await apiCall(`/users/${id}`); // No type safety
|
||||
}
|
||||
```
|
||||
|
||||
@@ -592,51 +596,46 @@ async function getUser(id: string): Promise<any> {
|
||||
* project management, and data export capabilities.
|
||||
*/
|
||||
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { z } from "zod";
|
||||
import axios, { AxiosError } from "axios";
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import { z } from 'zod';
|
||||
import axios, { AxiosError } from 'axios';
|
||||
|
||||
// Constants
|
||||
const API_BASE_URL = "https://api.example.com/v1";
|
||||
const API_BASE_URL = 'https://api.example.com/v1';
|
||||
const CHARACTER_LIMIT = 25000;
|
||||
|
||||
// Enums
|
||||
enum ResponseFormat {
|
||||
MARKDOWN = "markdown",
|
||||
JSON = "json"
|
||||
MARKDOWN = 'markdown',
|
||||
JSON = 'json',
|
||||
}
|
||||
|
||||
// Zod schemas
|
||||
const UserSearchInputSchema = z.object({
|
||||
query: z.string()
|
||||
.min(2, "Query must be at least 2 characters")
|
||||
.max(200, "Query must not exceed 200 characters")
|
||||
.describe("Search string to match against names/emails"),
|
||||
limit: z.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(100)
|
||||
.default(20)
|
||||
.describe("Maximum results to return"),
|
||||
offset: z.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.default(0)
|
||||
.describe("Number of results to skip for pagination"),
|
||||
response_format: z.nativeEnum(ResponseFormat)
|
||||
.default(ResponseFormat.MARKDOWN)
|
||||
.describe("Output format: 'markdown' for human-readable or 'json' for machine-readable")
|
||||
}).strict();
|
||||
const UserSearchInputSchema = z
|
||||
.object({
|
||||
query: z
|
||||
.string()
|
||||
.min(2, 'Query must be at least 2 characters')
|
||||
.max(200, 'Query must not exceed 200 characters')
|
||||
.describe('Search string to match against names/emails'),
|
||||
limit: z.number().int().min(1).max(100).default(20).describe('Maximum results to return'),
|
||||
offset: z.number().int().min(0).default(0).describe('Number of results to skip for pagination'),
|
||||
response_format: z
|
||||
.nativeEnum(ResponseFormat)
|
||||
.default(ResponseFormat.MARKDOWN)
|
||||
.describe("Output format: 'markdown' for human-readable or 'json' for machine-readable"),
|
||||
})
|
||||
.strict();
|
||||
|
||||
type UserSearchInput = z.infer<typeof UserSearchInputSchema>;
|
||||
|
||||
// Shared utility functions
|
||||
async function makeApiRequest<T>(
|
||||
endpoint: string,
|
||||
method: "GET" | "POST" | "PUT" | "DELETE" = "GET",
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
|
||||
data?: any,
|
||||
params?: any
|
||||
params?: any,
|
||||
): Promise<T> {
|
||||
try {
|
||||
const response = await axios({
|
||||
@@ -646,9 +645,9 @@ async function makeApiRequest<T>(
|
||||
params,
|
||||
timeout: 30000,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
}
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
@@ -661,16 +660,16 @@ function handleApiError(error: unknown): string {
|
||||
if (error.response) {
|
||||
switch (error.response.status) {
|
||||
case 404:
|
||||
return "Error: Resource not found. Please check the ID is correct.";
|
||||
return 'Error: Resource not found. Please check the ID is correct.';
|
||||
case 403:
|
||||
return "Error: Permission denied. You don't have access to this resource.";
|
||||
case 429:
|
||||
return "Error: Rate limit exceeded. Please wait before making more requests.";
|
||||
return 'Error: Rate limit exceeded. Please wait before making more requests.';
|
||||
default:
|
||||
return `Error: API request failed with status ${error.response.status}`;
|
||||
}
|
||||
} else if (error.code === "ECONNABORTED") {
|
||||
return "Error: Request timed out. Please try again.";
|
||||
} else if (error.code === 'ECONNABORTED') {
|
||||
return 'Error: Request timed out. Please try again.';
|
||||
}
|
||||
}
|
||||
return `Error: Unexpected error occurred: ${error instanceof Error ? error.message : String(error)}`;
|
||||
@@ -678,46 +677,46 @@ function handleApiError(error: unknown): string {
|
||||
|
||||
// Create MCP server instance
|
||||
const server = new McpServer({
|
||||
name: "example-mcp",
|
||||
version: "1.0.0"
|
||||
name: 'example-mcp',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
// Register tools
|
||||
server.registerTool(
|
||||
"example_search_users",
|
||||
'example_search_users',
|
||||
{
|
||||
title: "Search Example Users",
|
||||
title: 'Search Example Users',
|
||||
description: `[Full description as shown above]`,
|
||||
inputSchema: UserSearchInputSchema,
|
||||
annotations: {
|
||||
readOnlyHint: true,
|
||||
destructiveHint: false,
|
||||
idempotentHint: true,
|
||||
openWorldHint: true
|
||||
}
|
||||
openWorldHint: true,
|
||||
},
|
||||
},
|
||||
async (params: UserSearchInput) => {
|
||||
// Implementation as shown above
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Main function
|
||||
// For stdio (local):
|
||||
async function runStdio() {
|
||||
if (!process.env.EXAMPLE_API_KEY) {
|
||||
console.error("ERROR: EXAMPLE_API_KEY environment variable is required");
|
||||
console.error('ERROR: EXAMPLE_API_KEY environment variable is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error("MCP server running via stdio");
|
||||
console.error('MCP server running via stdio');
|
||||
}
|
||||
|
||||
// For streamable HTTP (remote):
|
||||
async function runHTTP() {
|
||||
if (!process.env.EXAMPLE_API_KEY) {
|
||||
console.error("ERROR: EXAMPLE_API_KEY environment variable is required");
|
||||
console.error('ERROR: EXAMPLE_API_KEY environment variable is required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -727,7 +726,7 @@ async function runHTTP() {
|
||||
app.post('/mcp', async (req, res) => {
|
||||
const transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: undefined,
|
||||
enableJsonResponse: true
|
||||
enableJsonResponse: true,
|
||||
});
|
||||
res.on('close', () => transport.close());
|
||||
await server.connect(transport);
|
||||
@@ -743,13 +742,13 @@ async function runHTTP() {
|
||||
// Choose transport based on environment
|
||||
const transport = process.env.TRANSPORT || 'stdio';
|
||||
if (transport === 'http') {
|
||||
runHTTP().catch(error => {
|
||||
console.error("Server error:", error);
|
||||
runHTTP().catch((error) => {
|
||||
console.error('Server error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
} else {
|
||||
runStdio().catch(error => {
|
||||
console.error("Server error:", error);
|
||||
runStdio().catch((error) => {
|
||||
console.error('Server error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
@@ -764,51 +763,54 @@ if (transport === 'http') {
|
||||
Expose data as resources for efficient, URI-based access:
|
||||
|
||||
```typescript
|
||||
import { ResourceTemplate } from "@modelcontextprotocol/sdk/types.js";
|
||||
import { ResourceTemplate } from '@modelcontextprotocol/sdk/types.js';
|
||||
|
||||
// Register a resource with URI template
|
||||
server.registerResource(
|
||||
{
|
||||
uri: "file://documents/{name}",
|
||||
name: "Document Resource",
|
||||
description: "Access documents by name",
|
||||
mimeType: "text/plain"
|
||||
uri: 'file://documents/{name}',
|
||||
name: 'Document Resource',
|
||||
description: 'Access documents by name',
|
||||
mimeType: 'text/plain',
|
||||
},
|
||||
async (uri: string) => {
|
||||
// Extract parameter from URI
|
||||
const match = uri.match(/^file:\/\/documents\/(.+)$/);
|
||||
if (!match) {
|
||||
throw new Error("Invalid URI format");
|
||||
throw new Error('Invalid URI format');
|
||||
}
|
||||
|
||||
const documentName = match[1];
|
||||
const content = await loadDocument(documentName);
|
||||
|
||||
return {
|
||||
contents: [{
|
||||
uri,
|
||||
mimeType: "text/plain",
|
||||
text: content
|
||||
}]
|
||||
contents: [
|
||||
{
|
||||
uri,
|
||||
mimeType: 'text/plain',
|
||||
text: content,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// List available resources dynamically
|
||||
server.registerResourceList(async () => {
|
||||
const documents = await getAvailableDocuments();
|
||||
return {
|
||||
resources: documents.map(doc => ({
|
||||
resources: documents.map((doc) => ({
|
||||
uri: `file://documents/${doc.name}`,
|
||||
name: doc.name,
|
||||
mimeType: "text/plain",
|
||||
description: doc.description
|
||||
}))
|
||||
mimeType: 'text/plain',
|
||||
description: doc.description,
|
||||
})),
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
**When to use Resources vs Tools:**
|
||||
|
||||
- **Resources**: For data access with simple URI-based parameters
|
||||
- **Tools**: For complex operations requiring validation and business logic
|
||||
- **Resources**: When data is relatively static or template-based
|
||||
@@ -821,8 +823,8 @@ The TypeScript SDK supports two main transport mechanisms:
|
||||
#### Streamable HTTP (Recommended for Remote Servers)
|
||||
|
||||
```typescript
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import express from "express";
|
||||
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
||||
import express from 'express';
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
@@ -831,7 +833,7 @@ app.post('/mcp', async (req, res) => {
|
||||
// Create new transport for each request (stateless, prevents request ID collisions)
|
||||
const transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: undefined,
|
||||
enableJsonResponse: true
|
||||
enableJsonResponse: true,
|
||||
});
|
||||
|
||||
res.on('close', () => transport.close());
|
||||
@@ -846,13 +848,14 @@ app.listen(3000);
|
||||
#### stdio (For Local Integrations)
|
||||
|
||||
```typescript
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
```
|
||||
|
||||
**Transport selection:**
|
||||
|
||||
- **Streamable HTTP**: Web services, remote access, multiple clients
|
||||
- **stdio**: Command-line tools, local development, subprocess integration
|
||||
|
||||
@@ -863,12 +866,12 @@ Notify clients when server state changes:
|
||||
```typescript
|
||||
// Notify when tools list changes
|
||||
server.notification({
|
||||
method: "notifications/tools/list_changed"
|
||||
method: 'notifications/tools/list_changed',
|
||||
});
|
||||
|
||||
// Notify when resources change
|
||||
server.notification({
|
||||
method: "notifications/resources/list_changed"
|
||||
method: 'notifications/resources/list_changed',
|
||||
});
|
||||
```
|
||||
|
||||
@@ -917,6 +920,7 @@ Always ensure `npm run build` completes successfully before considering the impl
|
||||
Before finalizing your Node/TypeScript MCP server implementation, ensure:
|
||||
|
||||
### Strategic Design
|
||||
|
||||
- [ ] Tools enable complete workflows, not just API endpoint wrappers
|
||||
- [ ] Tool names reflect natural task subdivisions
|
||||
- [ ] Response formats optimize for agent context efficiency
|
||||
@@ -924,6 +928,7 @@ Before finalizing your Node/TypeScript MCP server implementation, ensure:
|
||||
- [ ] Error messages guide agents toward correct usage
|
||||
|
||||
### Implementation Quality
|
||||
|
||||
- [ ] FOCUSED IMPLEMENTATION: Most important and valuable tools implemented
|
||||
- [ ] All tools registered using `registerTool` with complete configuration
|
||||
- [ ] All tools include `title`, `description`, `inputSchema`, and `annotations`
|
||||
@@ -935,6 +940,7 @@ Before finalizing your Node/TypeScript MCP server implementation, ensure:
|
||||
- [ ] Error messages are clear, actionable, and educational
|
||||
|
||||
### TypeScript Quality
|
||||
|
||||
- [ ] TypeScript interfaces are defined for all data structures
|
||||
- [ ] Strict TypeScript is enabled in tsconfig.json
|
||||
- [ ] No use of `any` type - use `unknown` or proper types instead
|
||||
@@ -942,12 +948,14 @@ Before finalizing your Node/TypeScript MCP server implementation, ensure:
|
||||
- [ ] Error handling uses proper type guards (e.g., `axios.isAxiosError`, `z.ZodError`)
|
||||
|
||||
### Advanced Features (where applicable)
|
||||
|
||||
- [ ] Resources registered for appropriate data endpoints
|
||||
- [ ] Appropriate transport configured (stdio or streamable HTTP)
|
||||
- [ ] Notifications implemented for dynamic server capabilities
|
||||
- [ ] Type-safe with SDK interfaces
|
||||
|
||||
### Project Configuration
|
||||
|
||||
- [ ] Package.json includes all necessary dependencies
|
||||
- [ ] Build script produces working JavaScript in dist/ directory
|
||||
- [ ] Main entry point is properly configured as dist/index.js
|
||||
@@ -955,6 +963,7 @@ Before finalizing your Node/TypeScript MCP server implementation, ensure:
|
||||
- [ ] tsconfig.json properly configured with strict mode
|
||||
|
||||
### Code Quality
|
||||
|
||||
- [ ] Pagination is properly implemented where applicable
|
||||
- [ ] Large responses check CHARACTER_LIMIT constant and truncate with clear messages
|
||||
- [ ] Filtering options are provided for potentially large result sets
|
||||
@@ -963,8 +972,9 @@ Before finalizing your Node/TypeScript MCP server implementation, ensure:
|
||||
- [ ] Return types are consistent across similar operations
|
||||
|
||||
### Testing and Build
|
||||
|
||||
- [ ] `npm run build` completes successfully without errors
|
||||
- [ ] dist/index.js created and executable
|
||||
- [ ] Server runs: `node dist/index.js --help`
|
||||
- [ ] All imports resolve correctly
|
||||
- [ ] Sample tool calls work as expected
|
||||
- [ ] Sample tool calls work as expected
|
||||
|
||||
@@ -9,6 +9,7 @@ This document provides Python-specific best practices and examples for implement
|
||||
## Quick Reference
|
||||
|
||||
### Key Imports
|
||||
|
||||
```python
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from pydantic import BaseModel, Field, field_validator, ConfigDict
|
||||
@@ -18,11 +19,13 @@ import httpx
|
||||
```
|
||||
|
||||
### Server Initialization
|
||||
|
||||
```python
|
||||
mcp = FastMCP("service_mcp")
|
||||
```
|
||||
|
||||
### Tool Registration Pattern
|
||||
|
||||
```python
|
||||
@mcp.tool(name="tool_name", annotations={...})
|
||||
async def tool_function(params: InputModel) -> str:
|
||||
@@ -35,6 +38,7 @@ async def tool_function(params: InputModel) -> str:
|
||||
## MCP Python SDK and FastMCP
|
||||
|
||||
The official MCP Python SDK provides FastMCP, a high-level framework for building MCP servers. It provides:
|
||||
|
||||
- Automatic description and inputSchema generation from function signatures and docstrings
|
||||
- Pydantic model integration for input validation
|
||||
- Decorator-based tool registration with `@mcp.tool`
|
||||
@@ -45,10 +49,12 @@ The official MCP Python SDK provides FastMCP, a high-level framework for buildin
|
||||
## Server Naming Convention
|
||||
|
||||
Python MCP servers must follow this naming pattern:
|
||||
|
||||
- **Format**: `{service}_mcp` (lowercase with underscores)
|
||||
- **Examples**: `github_mcp`, `jira_mcp`, `stripe_mcp`
|
||||
|
||||
The name should be:
|
||||
|
||||
- General (not tied to specific features)
|
||||
- Descriptive of the service/API being integrated
|
||||
- Easy to infer from the task description
|
||||
@@ -61,6 +67,7 @@ The name should be:
|
||||
Use snake_case for tool names (e.g., "search_users", "create_project", "get_channel_info") with clear, action-oriented names.
|
||||
|
||||
**Avoid Naming Conflicts**: Include the service context to prevent overlaps:
|
||||
|
||||
- Use "slack_send_message" instead of just "send_message"
|
||||
- Use "github_create_issue" instead of just "create_issue"
|
||||
- Use "asana_list_tasks" instead of just "list_tasks"
|
||||
@@ -168,6 +175,7 @@ class UserSearchInput(BaseModel):
|
||||
```
|
||||
|
||||
**Markdown format**:
|
||||
|
||||
- Use headers, lists, and formatting for clarity
|
||||
- Convert timestamps to human-readable format (e.g., "2024-01-15 10:30:00 UTC" instead of epoch)
|
||||
- Show display names with IDs in parentheses (e.g., "@john.doe (U123456)")
|
||||
@@ -175,6 +183,7 @@ class UserSearchInput(BaseModel):
|
||||
- Group related information logically
|
||||
|
||||
**JSON format**:
|
||||
|
||||
- Return complete, structured data suitable for programmatic processing
|
||||
- Include all available fields and metadata
|
||||
- Use consistent field names and types
|
||||
@@ -518,6 +527,7 @@ async def interactive_tool(resource_id: str, ctx: Context) -> str:
|
||||
```
|
||||
|
||||
**Context capabilities:**
|
||||
|
||||
- `ctx.report_progress(progress, message)` - Report progress for long operations
|
||||
- `ctx.log_info(message, data)` / `ctx.log_error()` / `ctx.log_debug()` - Logging
|
||||
- `ctx.elicit(prompt, input_type)` - Request input from users
|
||||
@@ -548,6 +558,7 @@ async def get_setting(key: str, ctx: Context) -> str:
|
||||
```
|
||||
|
||||
**When to use Resources vs Tools:**
|
||||
|
||||
- **Resources**: For data access with simple parameters (URI templates)
|
||||
- **Tools**: For complex operations with validation and business logic
|
||||
|
||||
@@ -631,6 +642,7 @@ if __name__ == "__main__":
|
||||
```
|
||||
|
||||
**Transport selection:**
|
||||
|
||||
- **stdio**: Command-line tools, local integrations, subprocess execution
|
||||
- **Streamable HTTP**: Web services, remote access, multiple clients
|
||||
|
||||
@@ -670,6 +682,7 @@ Your implementation MUST prioritize composability and code reuse:
|
||||
Before finalizing your Python MCP server implementation, ensure:
|
||||
|
||||
### Strategic Design
|
||||
|
||||
- [ ] Tools enable complete workflows, not just API endpoint wrappers
|
||||
- [ ] Tool names reflect natural task subdivisions
|
||||
- [ ] Response formats optimize for agent context efficiency
|
||||
@@ -677,6 +690,7 @@ Before finalizing your Python MCP server implementation, ensure:
|
||||
- [ ] Error messages guide agents toward correct usage
|
||||
|
||||
### Implementation Quality
|
||||
|
||||
- [ ] FOCUSED IMPLEMENTATION: Most important and valuable tools implemented
|
||||
- [ ] All tools have descriptive names and documentation
|
||||
- [ ] Return types are consistent across similar operations
|
||||
@@ -688,6 +702,7 @@ Before finalizing your Python MCP server implementation, ensure:
|
||||
- [ ] Outputs are properly validated and formatted
|
||||
|
||||
### Tool Configuration
|
||||
|
||||
- [ ] All tools implement 'name' and 'annotations' in the decorator
|
||||
- [ ] Annotations correctly set (readOnlyHint, destructiveHint, idempotentHint, openWorldHint)
|
||||
- [ ] All tools use Pydantic BaseModel for input validation with Field() definitions
|
||||
@@ -697,6 +712,7 @@ Before finalizing your Python MCP server implementation, ensure:
|
||||
- [ ] Pydantic models handle input validation (no manual validation needed)
|
||||
|
||||
### Advanced Features (where applicable)
|
||||
|
||||
- [ ] Context injection used for logging, progress, or elicitation
|
||||
- [ ] Resources registered for appropriate data endpoints
|
||||
- [ ] Lifespan management implemented for persistent connections
|
||||
@@ -704,6 +720,7 @@ Before finalizing your Python MCP server implementation, ensure:
|
||||
- [ ] Appropriate transport configured (stdio or streamable HTTP)
|
||||
|
||||
### Code Quality
|
||||
|
||||
- [ ] File includes proper imports including Pydantic imports
|
||||
- [ ] Pagination is properly implemented where applicable
|
||||
- [ ] Filtering options are provided for potentially large result sets
|
||||
@@ -713,7 +730,8 @@ Before finalizing your Python MCP server implementation, ensure:
|
||||
- [ ] Constants are defined at module level in UPPER_CASE
|
||||
|
||||
### Testing
|
||||
|
||||
- [ ] Server runs successfully: `python your_server.py --help`
|
||||
- [ ] All imports resolve correctly
|
||||
- [ ] Sample tool calls work as expected
|
||||
- [ ] Error scenarios handled gracefully
|
||||
- [ ] Error scenarios handled gracefully
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: mosaic-deploy
|
||||
description: "Full end-to-end deploy flow for Mosaic Stack projects: push branch → open PR → wait for CI → merge → redeploy Portainer stack. Use when deploying a feature branch to production or staging, or when asked to ship a completed feature. Orchestrates mosaic-gitea, mosaic-woodpecker, and mosaic-portainer skills."
|
||||
description: 'Full end-to-end deploy flow for Mosaic Stack projects: push branch → open PR → wait for CI → merge → redeploy Portainer stack. Use when deploying a feature branch to production or staging, or when asked to ship a completed feature. Orchestrates mosaic-gitea, mosaic-woodpecker, and mosaic-portainer skills.'
|
||||
---
|
||||
|
||||
# mosaic-deploy
|
||||
@@ -29,6 +29,7 @@ git push -u origin <branch>
|
||||
```
|
||||
|
||||
If CI fails, check:
|
||||
|
||||
```bash
|
||||
source ~/.config/mosaic/tools/_lib/credentials.sh && load_credentials woodpecker
|
||||
~/.config/mosaic/tools/woodpecker/pipeline-status.sh -r <org>/<repo>
|
||||
@@ -42,6 +43,7 @@ cd ~/src/<repo>
|
||||
```
|
||||
|
||||
For branch-protected repos (force merge):
|
||||
|
||||
```bash
|
||||
GITEA_TOKEN=$(cat ~/.config/mosaic/credentials/gitea.env | grep TOKEN | cut -d= -f2)
|
||||
curl -X POST "https://git.mosaicstack.dev/api/v1/repos/<org>/<repo>/pulls/<pr#>/merge" \
|
||||
@@ -57,6 +59,7 @@ source ~/.config/mosaic/tools/_lib/credentials.sh && load_credentials portainer
|
||||
```
|
||||
|
||||
Check deployment:
|
||||
|
||||
```bash
|
||||
~/.config/mosaic/tools/portainer/stack-status.sh -n <stack-name>
|
||||
~/.config/mosaic/tools/portainer/stack-logs.sh -n <stack-name> -l 50
|
||||
@@ -64,12 +67,12 @@ Check deployment:
|
||||
|
||||
## Stack Name Map
|
||||
|
||||
| Project | Stack Name |
|
||||
|---|---|
|
||||
| Project | Stack Name |
|
||||
| ------------ | -------------- |
|
||||
| mosaic-stack | `mosaic-stack` |
|
||||
| sage-phr | `sage-phr` |
|
||||
| openbrain | `openbrain` |
|
||||
| firefly | `firefly` |
|
||||
| sage-phr | `sage-phr` |
|
||||
| openbrain | `openbrain` |
|
||||
| firefly | `firefly` |
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
@@ -12,11 +12,13 @@ Git operations via Mosaic wrapper scripts. Platform-aware (Gitea or GitHub).
|
||||
Scripts auto-detect platform from git remote. Run from inside the repo directory.
|
||||
|
||||
For force-merge (branch protection bypass):
|
||||
|
||||
```bash
|
||||
GITEA_TOKEN=$(cat ~/.config/mosaic/credentials/gitea.env | grep TOKEN | cut -d= -f2)
|
||||
```
|
||||
|
||||
Or use the credentials loader:
|
||||
|
||||
```bash
|
||||
source ~/.config/mosaic/tools/_lib/credentials.sh
|
||||
load_credentials gitea-mosaicstack
|
||||
@@ -29,60 +31,63 @@ All scripts in `~/.config/mosaic/tools/git/`.
|
||||
|
||||
### Pull Requests
|
||||
|
||||
| Script | Purpose | Key flags |
|
||||
|---|---|---|
|
||||
| `pr-create.sh` | Create a PR | `-t "Title" [-b "Body"] [-B base] [-H head] [-i issue#]` |
|
||||
| `pr-list.sh` | List open PRs | `[-s state]` |
|
||||
| `pr-view.sh` | View PR details | `-n <pr#>` |
|
||||
| `pr-merge.sh` | Squash-merge a PR | `-n <pr#> [-d]` (delete branch) |
|
||||
| `pr-close.sh` | Close a PR | `-n <pr#>` |
|
||||
| `pr-diff.sh` | Show PR diff | `-n <pr#>` |
|
||||
| `pr-review.sh` | Submit a review | `-n <pr#> [-a approve\|request-changes]` |
|
||||
| `pr-ci-wait.sh` | Wait for CI on PR | `-n <pr#>` |
|
||||
| `pr-metadata.sh` | Get PR metadata as JSON | `-n <pr#>` |
|
||||
| Script | Purpose | Key flags |
|
||||
| ---------------- | ----------------------- | -------------------------------------------------------- |
|
||||
| `pr-create.sh` | Create a PR | `-t "Title" [-b "Body"] [-B base] [-H head] [-i issue#]` |
|
||||
| `pr-list.sh` | List open PRs | `[-s state]` |
|
||||
| `pr-view.sh` | View PR details | `-n <pr#>` |
|
||||
| `pr-merge.sh` | Squash-merge a PR | `-n <pr#> [-d]` (delete branch) |
|
||||
| `pr-close.sh` | Close a PR | `-n <pr#>` |
|
||||
| `pr-diff.sh` | Show PR diff | `-n <pr#>` |
|
||||
| `pr-review.sh` | Submit a review | `-n <pr#> [-a approve\|request-changes]` |
|
||||
| `pr-ci-wait.sh` | Wait for CI on PR | `-n <pr#>` |
|
||||
| `pr-metadata.sh` | Get PR metadata as JSON | `-n <pr#>` |
|
||||
|
||||
### Issues
|
||||
|
||||
| Script | Purpose | Key flags |
|
||||
|---|---|---|
|
||||
| `issue-create.sh` | Create an issue | `-t "Title" [-b "Body"] [-l labels] [-m milestone]` |
|
||||
| `issue-list.sh` | List issues | `[-s state] [-l label]` |
|
||||
| `issue-view.sh` | View issue | `-n <issue#>` |
|
||||
| `issue-close.sh` | Close issue | `-n <issue#>` |
|
||||
| `issue-comment.sh` | Add comment | `-n <issue#> -c "Comment"` |
|
||||
| `issue-assign.sh` | Assign issue | `-n <issue#> -u username` |
|
||||
| `issue-edit.sh` | Edit issue | `-n <issue#> [-t title] [-b body]` |
|
||||
| Script | Purpose | Key flags |
|
||||
| ------------------ | --------------- | --------------------------------------------------- |
|
||||
| `issue-create.sh` | Create an issue | `-t "Title" [-b "Body"] [-l labels] [-m milestone]` |
|
||||
| `issue-list.sh` | List issues | `[-s state] [-l label]` |
|
||||
| `issue-view.sh` | View issue | `-n <issue#>` |
|
||||
| `issue-close.sh` | Close issue | `-n <issue#>` |
|
||||
| `issue-comment.sh` | Add comment | `-n <issue#> -c "Comment"` |
|
||||
| `issue-assign.sh` | Assign issue | `-n <issue#> -u username` |
|
||||
| `issue-edit.sh` | Edit issue | `-n <issue#> [-t title] [-b body]` |
|
||||
|
||||
### Milestones
|
||||
|
||||
| Script | Purpose | Key flags |
|
||||
|---|---|---|
|
||||
| Script | Purpose | Key flags |
|
||||
| --------------------- | ---------------- | ---------------------------- |
|
||||
| `milestone-create.sh` | Create milestone | `-t "Title" [-d "due date"]` |
|
||||
| `milestone-list.sh` | List milestones | — |
|
||||
| `milestone-close.sh` | Close milestone | `-n <number>` |
|
||||
| `milestone-list.sh` | List milestones | — |
|
||||
| `milestone-close.sh` | Close milestone | `-n <number>` |
|
||||
|
||||
### CI / Queue
|
||||
|
||||
| Script | Purpose | Key flags |
|
||||
|---|---|---|
|
||||
| `ci-queue-wait.sh` | Wait for CI queue slot | `[-t timeout] [-B branch]` |
|
||||
| `detect-platform.sh` | Detect git platform | outputs `gitea` or `github` |
|
||||
| Script | Purpose | Key flags |
|
||||
| -------------------- | ---------------------- | --------------------------- |
|
||||
| `ci-queue-wait.sh` | Wait for CI queue slot | `[-t timeout] [-B branch]` |
|
||||
| `detect-platform.sh` | Detect git platform | outputs `gitea` or `github` |
|
||||
|
||||
## Common Workflows
|
||||
|
||||
**Create PR from current branch:**
|
||||
|
||||
```bash
|
||||
cd ~/src/<repo>
|
||||
~/.config/mosaic/tools/git/pr-create.sh -t "feat: my feature" -b "Description" -i <issue#>
|
||||
```
|
||||
|
||||
**Merge a PR (squash, with CI queue guard):**
|
||||
|
||||
```bash
|
||||
cd ~/src/<repo>
|
||||
~/.config/mosaic/tools/git/pr-merge.sh -n <pr#> -d
|
||||
```
|
||||
|
||||
**Force-merge bypassing branch protection:**
|
||||
|
||||
```bash
|
||||
GITEA_TOKEN=$(cat ~/.config/mosaic/credentials/gitea.env | grep TOKEN | cut -d= -f2)
|
||||
curl -X POST "https://git.mosaicstack.dev/api/v1/repos/<org>/<repo>/pulls/<PR>/merge" \
|
||||
|
||||
@@ -10,6 +10,7 @@ Mosaic mission and orchestrator session management.
|
||||
## Mandatory Pre-Flight
|
||||
|
||||
Before initiating Orchestrator mode, always read:
|
||||
|
||||
1. `~/.config/mosaic/guides/ORCHESTRATOR.md`
|
||||
2. `~/.config/mosaic/guides/E2E-DELIVERY.md`
|
||||
|
||||
@@ -47,15 +48,15 @@ mosaic doctor
|
||||
|
||||
Located in `~/.config/mosaic/tools/orchestrator/`.
|
||||
|
||||
| Script | Purpose |
|
||||
|---|---|
|
||||
| `mission-init.sh` | Initialize mission.json and TASKS.md scaffold |
|
||||
| `mission-status.sh` | Show current mission + task state |
|
||||
| `session-run.sh` | Launch an orchestrator session |
|
||||
| `session-resume.sh` | Resume a paused orchestrator session |
|
||||
| `session-status.sh` | Check session health / lock status |
|
||||
| `smoke-test.sh` | Post-deploy smoke test |
|
||||
| `continue-prompt.sh` | Generate a continue prompt for session |
|
||||
| Script | Purpose |
|
||||
| -------------------- | --------------------------------------------- |
|
||||
| `mission-init.sh` | Initialize mission.json and TASKS.md scaffold |
|
||||
| `mission-status.sh` | Show current mission + task state |
|
||||
| `session-run.sh` | Launch an orchestrator session |
|
||||
| `session-resume.sh` | Resume a paused orchestrator session |
|
||||
| `session-status.sh` | Check session health / lock status |
|
||||
| `smoke-test.sh` | Post-deploy smoke test |
|
||||
| `continue-prompt.sh` | Generate a continue prompt for session |
|
||||
|
||||
## Mission Init Pattern
|
||||
|
||||
@@ -86,6 +87,7 @@ mosaic coord run --project .
|
||||
## Completion Gates
|
||||
|
||||
A task is NOT done until all of:
|
||||
|
||||
- [ ] Code review ✓
|
||||
- [ ] Security review ✓
|
||||
- [ ] Tests GREEN ✓
|
||||
|
||||
@@ -21,31 +21,34 @@ load_credentials portainer
|
||||
|
||||
All scripts live in `~/.config/mosaic/tools/portainer/`.
|
||||
|
||||
| Script | Purpose | Key flags |
|
||||
|---|---|---|
|
||||
| `stack-list.sh` | List all stacks | — |
|
||||
| `stack-status.sh` | Status of a stack | `-n <name>` |
|
||||
| Script | Purpose | Key flags |
|
||||
| ------------------- | ---------------------------- | ------------------------------ |
|
||||
| `stack-list.sh` | List all stacks | — |
|
||||
| `stack-status.sh` | Status of a stack | `-n <name>` |
|
||||
| `stack-redeploy.sh` | Redeploy (file or git-based) | `-n <name> [-p]` (pull images) |
|
||||
| `stack-start.sh` | Start a stopped stack | `-n <name>` |
|
||||
| `stack-stop.sh` | Stop a running stack | `-n <name>` |
|
||||
| `stack-logs.sh` | Tail stack logs | `-n <name> [-l lines]` |
|
||||
| `endpoint-list.sh` | List Portainer endpoints | — |
|
||||
| `stack-start.sh` | Start a stopped stack | `-n <name>` |
|
||||
| `stack-stop.sh` | Stop a running stack | `-n <name>` |
|
||||
| `stack-logs.sh` | Tail stack logs | `-n <name> [-l lines]` |
|
||||
| `endpoint-list.sh` | List Portainer endpoints | — |
|
||||
|
||||
## Common Workflows
|
||||
|
||||
**Redeploy a stack with fresh images:**
|
||||
|
||||
```bash
|
||||
source ~/.config/mosaic/tools/_lib/credentials.sh && load_credentials portainer
|
||||
~/.config/mosaic/tools/portainer/stack-redeploy.sh -n mosaic-stack -p
|
||||
```
|
||||
|
||||
**Check all stack statuses:**
|
||||
|
||||
```bash
|
||||
source ~/.config/mosaic/tools/_lib/credentials.sh && load_credentials portainer
|
||||
~/.config/mosaic/tools/portainer/stack-list.sh
|
||||
```
|
||||
|
||||
**Tail logs for a service:**
|
||||
|
||||
```bash
|
||||
source ~/.config/mosaic/tools/_lib/credentials.sh && load_credentials portainer
|
||||
~/.config/mosaic/tools/portainer/stack-logs.sh -n mosaic-stack -l 100
|
||||
|
||||
@@ -47,10 +47,10 @@ login from the repo's `origin` host **before** running a wrapper:
|
||||
git remote get-url origin # inspect the host, then:
|
||||
```
|
||||
|
||||
| origin host | login |
|
||||
| --------------------- | -------------------------------------------- |
|
||||
| `git.uscllc.com` | `export GITEA_LOGIN=usc` |
|
||||
| `git.mosaicstack.dev` | default `mosaicstack` (no export needed) |
|
||||
| origin host | login |
|
||||
| --------------------- | ---------------------------------------- |
|
||||
| `git.uscllc.com` | `export GITEA_LOGIN=usc` |
|
||||
| `git.mosaicstack.dev` | default `mosaicstack` (no export needed) |
|
||||
|
||||
## 3. Other tool suites under `~/.config/mosaic/tools/`
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: mosaic-woodpecker
|
||||
description: "Manage Woodpecker CI pipelines for Mosaic Stack projects. Use when checking pipeline status, triggering pipelines, waiting for CI to complete, or debugging build failures. Wraps scripts in ~/.config/mosaic/tools/woodpecker/. CI URL: https://ci.mosaicstack.dev."
|
||||
description: 'Manage Woodpecker CI pipelines for Mosaic Stack projects. Use when checking pipeline status, triggering pipelines, waiting for CI to complete, or debugging build failures. Wraps scripts in ~/.config/mosaic/tools/woodpecker/. CI URL: https://ci.mosaicstack.dev.'
|
||||
---
|
||||
|
||||
# mosaic-woodpecker
|
||||
@@ -16,6 +16,7 @@ load_credentials woodpecker
|
||||
```
|
||||
|
||||
For a specific instance (e.g. `usc`):
|
||||
|
||||
```bash
|
||||
load_credentials woodpecker-usc
|
||||
```
|
||||
@@ -24,22 +25,23 @@ load_credentials woodpecker-usc
|
||||
|
||||
All scripts in `~/.config/mosaic/tools/woodpecker/`.
|
||||
|
||||
| Script | Purpose | Key flags |
|
||||
|---|---|---|
|
||||
| `pipeline-status.sh` | Check pipeline status | `[-r owner/repo] [-n number] [-f json\|table]` |
|
||||
| `pipeline-list.sh` | List recent pipelines | `[-r owner/repo] [-l limit]` |
|
||||
| `pipeline-trigger.sh` | Trigger a pipeline | `[-r owner/repo] [-b branch]` |
|
||||
| Script | Purpose | Key flags |
|
||||
| --------------------- | --------------------- | ---------------------------------------------- |
|
||||
| `pipeline-status.sh` | Check pipeline status | `[-r owner/repo] [-n number] [-f json\|table]` |
|
||||
| `pipeline-list.sh` | List recent pipelines | `[-r owner/repo] [-l limit]` |
|
||||
| `pipeline-trigger.sh` | Trigger a pipeline | `[-r owner/repo] [-b branch]` |
|
||||
|
||||
CI queue management is in `~/.config/mosaic/tools/git/`:
|
||||
|
||||
| Script | Purpose | Key flags |
|
||||
|---|---|---|
|
||||
| Script | Purpose | Key flags |
|
||||
| ------------------ | ---------------------- | ------------------------------------------------ |
|
||||
| `ci-queue-wait.sh` | Wait for CI queue slot | `[-t timeout_sec] [-B branch] [--purpose merge]` |
|
||||
| `pr-ci-wait.sh` | Wait for PR CI to pass | `-n <pr#> [-t timeout]` |
|
||||
| `pr-ci-wait.sh` | Wait for PR CI to pass | `-n <pr#> [-t timeout]` |
|
||||
|
||||
## Common Workflows
|
||||
|
||||
**Check latest pipeline on current repo:**
|
||||
|
||||
```bash
|
||||
cd ~/src/<repo>
|
||||
source ~/.config/mosaic/tools/_lib/credentials.sh && load_credentials woodpecker
|
||||
@@ -47,17 +49,20 @@ source ~/.config/mosaic/tools/_lib/credentials.sh && load_credentials woodpecker
|
||||
```
|
||||
|
||||
**Check specific pipeline number:**
|
||||
|
||||
```bash
|
||||
~/.config/mosaic/tools/woodpecker/pipeline-status.sh -n 42
|
||||
```
|
||||
|
||||
**Wait for CI before merging:**
|
||||
|
||||
```bash
|
||||
cd ~/src/<repo>
|
||||
~/.config/mosaic/tools/git/pr-ci-wait.sh -n <pr#>
|
||||
```
|
||||
|
||||
**Trigger a pipeline on a branch:**
|
||||
|
||||
```bash
|
||||
~/.config/mosaic/tools/woodpecker/pipeline-trigger.sh -r mosaic/mosaic-stack -b main
|
||||
```
|
||||
|
||||
@@ -67,9 +67,10 @@ Comprehensive best practices and architecture guide for NestJS applications, des
|
||||
- 9.2 [Use Message and Event Patterns Correctly](#92-use-message-and-event-patterns-correctly)
|
||||
- 9.3 [Use Message Queues for Background Jobs](#93-use-message-queues-for-background-jobs)
|
||||
10. [DevOps & Deployment](#10-devops-deployment) — **LOW-MEDIUM**
|
||||
- 10.1 [Implement Graceful Shutdown](#101-implement-graceful-shutdown)
|
||||
- 10.2 [Use ConfigModule for Environment Configuration](#102-use-configmodule-for-environment-configuration)
|
||||
- 10.3 [Use Structured Logging](#103-use-structured-logging)
|
||||
|
||||
- 10.1 [Implement Graceful Shutdown](#101-implement-graceful-shutdown)
|
||||
- 10.2 [Use ConfigModule for Environment Configuration](#102-use-configmodule-for-environment-configuration)
|
||||
- 10.3 [Use Structured Logging](#103-use-structured-logging)
|
||||
|
||||
---
|
||||
|
||||
@@ -596,9 +597,7 @@ Create custom repositories to encapsulate complex queries and database logic. Th
|
||||
// Complex queries in services
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(
|
||||
@InjectRepository(User) private repo: Repository<User>,
|
||||
) {}
|
||||
constructor(@InjectRepository(User) private repo: Repository<User>) {}
|
||||
|
||||
async findActiveWithOrders(minOrders: number): Promise<User[]> {
|
||||
// Complex query logic mixed with business logic
|
||||
@@ -623,9 +622,7 @@ export class UsersService {
|
||||
// Custom repository with encapsulated queries
|
||||
@Injectable()
|
||||
export class UsersRepository {
|
||||
constructor(
|
||||
@InjectRepository(User) private repo: Repository<User>,
|
||||
) {}
|
||||
constructor(@InjectRepository(User) private repo: Repository<User>) {}
|
||||
|
||||
async findById(id: string): Promise<User | null> {
|
||||
return this.repo.findOne({ where: { id } });
|
||||
@@ -825,12 +822,12 @@ export class OrdersService {
|
||||
// Testing is painful - must mock unused methods
|
||||
const mockNotificationService = {
|
||||
sendEmail: jest.fn(),
|
||||
sendSms: jest.fn(), // Never used, but required
|
||||
sendPush: jest.fn(), // Never used, but required
|
||||
sendSlack: jest.fn(), // Never used, but required
|
||||
logNotification: jest.fn(), // Never used, but required
|
||||
sendSms: jest.fn(), // Never used, but required
|
||||
sendPush: jest.fn(), // Never used, but required
|
||||
sendSlack: jest.fn(), // Never used, but required
|
||||
logNotification: jest.fn(), // Never used, but required
|
||||
getDeliveryStatus: jest.fn(), // Never used, but required
|
||||
retryFailed: jest.fn(), // Never used, but required
|
||||
retryFailed: jest.fn(), // Never used, but required
|
||||
scheduleNotification: jest.fn(), // Never used, but required
|
||||
};
|
||||
```
|
||||
@@ -1123,9 +1120,7 @@ export class OrdersService {
|
||||
|
||||
```typescript
|
||||
// Shared test suite that any implementation must pass
|
||||
function testPaymentGatewayContract(
|
||||
createGateway: () => PaymentGateway,
|
||||
) {
|
||||
function testPaymentGatewayContract(createGateway: () => PaymentGateway) {
|
||||
describe('PaymentGateway contract', () => {
|
||||
let gateway: PaymentGateway;
|
||||
|
||||
@@ -1142,13 +1137,11 @@ function testPaymentGatewayContract(
|
||||
});
|
||||
|
||||
it('throws InvalidCurrencyException for unsupported currency', async () => {
|
||||
await expect(gateway.charge(1000, 'INVALID'))
|
||||
.rejects.toThrow(InvalidCurrencyException);
|
||||
await expect(gateway.charge(1000, 'INVALID')).rejects.toThrow(InvalidCurrencyException);
|
||||
});
|
||||
|
||||
it('throws TransactionNotFoundException for invalid refund', async () => {
|
||||
await expect(gateway.refund('nonexistent'))
|
||||
.rejects.toThrow(TransactionNotFoundException);
|
||||
await expect(gateway.refund('nonexistent')).rejects.toThrow(TransactionNotFoundException);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1359,7 +1352,9 @@ interface PaymentGateway {
|
||||
|
||||
@Injectable()
|
||||
export class StripeService implements PaymentGateway {
|
||||
charge(amount: number) { /* ... */ }
|
||||
charge(amount: number) {
|
||||
/* ... */
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@@ -1398,9 +1393,7 @@ export class MockPaymentService implements PaymentGateway {
|
||||
providers: [
|
||||
{
|
||||
provide: PAYMENT_GATEWAY,
|
||||
useClass: process.env.NODE_ENV === 'test'
|
||||
? MockPaymentService
|
||||
: StripeService,
|
||||
useClass: process.env.NODE_ENV === 'test' ? MockPaymentService : StripeService,
|
||||
},
|
||||
],
|
||||
exports: [PAYMENT_GATEWAY],
|
||||
@@ -1410,9 +1403,7 @@ export class PaymentModule {}
|
||||
// Injection
|
||||
@Injectable()
|
||||
export class OrdersService {
|
||||
constructor(
|
||||
@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway,
|
||||
) {}
|
||||
constructor(@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway) {}
|
||||
|
||||
async createOrder(dto: CreateOrderDto) {
|
||||
await this.payment.charge(dto.amount);
|
||||
@@ -1774,14 +1765,10 @@ export class AllExceptionsFilter implements ExceptionFilter {
|
||||
const request = ctx.getRequest<Request>();
|
||||
|
||||
const status =
|
||||
exception instanceof HttpException
|
||||
? exception.getStatus()
|
||||
: HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
|
||||
const message =
|
||||
exception instanceof HttpException
|
||||
? exception.message
|
||||
: 'Internal server error';
|
||||
exception instanceof HttpException ? exception.message : 'Internal server error';
|
||||
|
||||
this.logger.error(
|
||||
`${request.method} ${request.url}`,
|
||||
@@ -1798,10 +1785,7 @@ export class AllExceptionsFilter implements ExceptionFilter {
|
||||
}
|
||||
|
||||
// Register globally in main.ts
|
||||
app.useGlobalFilters(
|
||||
new AllExceptionsFilter(app.get(Logger)),
|
||||
new DomainExceptionFilter(),
|
||||
);
|
||||
app.useGlobalFilters(new AllExceptionsFilter(app.get(Logger)), new DomainExceptionFilter());
|
||||
|
||||
// Or via module
|
||||
@Module({
|
||||
@@ -2387,9 +2371,9 @@ export class UsersController {
|
||||
|
||||
// DTOs without validation decorators
|
||||
export class CreateUserDto {
|
||||
name: string; // No validation
|
||||
email: string; // Could be "not-an-email"
|
||||
age: number; // Could be "abc" or -999
|
||||
name: string; // No validation
|
||||
email: string; // Could be "not-an-email"
|
||||
age: number; // Could be "abc" or -999
|
||||
}
|
||||
```
|
||||
|
||||
@@ -2402,9 +2386,9 @@ async function bootstrap() {
|
||||
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true, // Strip unknown properties
|
||||
forbidNonWhitelisted: true, // Throw on unknown properties
|
||||
transform: true, // Auto-transform to DTO types
|
||||
whitelist: true, // Strip unknown properties
|
||||
forbidNonWhitelisted: true, // Throw on unknown properties
|
||||
transform: true, // Auto-transform to DTO types
|
||||
transformOptions: {
|
||||
enableImplicitConversion: true,
|
||||
},
|
||||
@@ -2915,9 +2899,7 @@ export class UsersService {
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService) => ({
|
||||
stores: [
|
||||
new KeyvRedis(config.get('REDIS_URL')),
|
||||
],
|
||||
stores: [new KeyvRedis(config.get('REDIS_URL'))],
|
||||
ttl: 60 * 1000, // Default 60s
|
||||
}),
|
||||
}),
|
||||
@@ -3091,9 +3073,7 @@ describe('UsersController (e2e)', () => {
|
||||
|
||||
describe('/users/:id (GET)', () => {
|
||||
it('should return 404 for non-existent user', () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/users/non-existent-id')
|
||||
.expect(404);
|
||||
return request(app.getHttpServer()).get('/users/non-existent-id').expect(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3121,9 +3101,7 @@ describe('Protected Routes (e2e)', () => {
|
||||
});
|
||||
|
||||
it('should return 401 without token', () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/users/me')
|
||||
.expect(401);
|
||||
return request(app.getHttpServer()).get('/users/me').expect(401);
|
||||
});
|
||||
|
||||
it('should return user profile with valid token', () => {
|
||||
@@ -3254,9 +3232,7 @@ describe('WeatherService', () => {
|
||||
});
|
||||
|
||||
it('should handle API timeout', async () => {
|
||||
httpService.get.mockReturnValue(
|
||||
throwError(() => new Error('ETIMEDOUT')),
|
||||
);
|
||||
httpService.get.mockReturnValue(throwError(() => new Error('ETIMEDOUT')));
|
||||
|
||||
await expect(service.getWeather('NYC')).rejects.toThrow('Weather service unavailable');
|
||||
});
|
||||
@@ -3287,10 +3263,7 @@ describe('UsersService', () => {
|
||||
};
|
||||
|
||||
const module = await Test.createTestingModule({
|
||||
providers: [
|
||||
UsersService,
|
||||
{ provide: getRepositoryToken(User), useValue: mockRepo },
|
||||
],
|
||||
providers: [UsersService, { provide: getRepositoryToken(User), useValue: mockRepo }],
|
||||
}).compile();
|
||||
|
||||
service = module.get(UsersService);
|
||||
@@ -3433,9 +3406,9 @@ describe('UsersService', () => {
|
||||
it('should throw on duplicate email', async () => {
|
||||
repo.findOne.mockResolvedValue({ id: '1', email: 'test@test.com' });
|
||||
|
||||
await expect(
|
||||
service.create({ name: 'Test', email: 'test@test.com' }),
|
||||
).rejects.toThrow(ConflictException);
|
||||
await expect(service.create({ name: 'Test', email: 'test@test.com' })).rejects.toThrow(
|
||||
ConflictException,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3813,12 +3786,7 @@ export class OrdersService {
|
||||
|
||||
for (const item of items) {
|
||||
await manager.save(OrderItem, { orderId: order.id, ...item });
|
||||
await manager.decrement(
|
||||
Inventory,
|
||||
{ productId: item.productId },
|
||||
'stock',
|
||||
item.quantity,
|
||||
);
|
||||
await manager.decrement(Inventory, { productId: item.productId }, 'stock', item.quantity);
|
||||
}
|
||||
|
||||
// If this throws, everything rolls back
|
||||
@@ -3841,12 +3809,7 @@ export class TransferService {
|
||||
|
||||
try {
|
||||
// Debit source account
|
||||
await queryRunner.manager.decrement(
|
||||
Account,
|
||||
{ id: fromId },
|
||||
'balance',
|
||||
amount,
|
||||
);
|
||||
await queryRunner.manager.decrement(Account, { id: fromId }, 'balance', amount);
|
||||
|
||||
// Verify sufficient funds
|
||||
const source = await queryRunner.manager.findOne(Account, {
|
||||
@@ -3857,12 +3820,7 @@ export class TransferService {
|
||||
}
|
||||
|
||||
// Credit destination account
|
||||
await queryRunner.manager.increment(
|
||||
Account,
|
||||
{ id: toId },
|
||||
'balance',
|
||||
amount,
|
||||
);
|
||||
await queryRunner.manager.increment(Account, { id: toId }, 'balance', amount);
|
||||
|
||||
// Log the transaction
|
||||
await queryRunner.manager.save(TransactionLog, {
|
||||
@@ -3890,10 +3848,7 @@ export class UsersRepository {
|
||||
private dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
async createWithProfile(
|
||||
userData: CreateUserDto,
|
||||
profileData: CreateProfileDto,
|
||||
): Promise<User> {
|
||||
async createWithProfile(userData: CreateUserDto, profileData: CreateProfileDto): Promise<User> {
|
||||
return this.dataSource.transaction(async (manager) => {
|
||||
const user = await manager.save(User, userData);
|
||||
await manager.save(Profile, { ...profileData, userId: user.id });
|
||||
@@ -4034,7 +3989,7 @@ export class UsersController {
|
||||
@SerializeOptions({ type: UserResponseDto })
|
||||
async findAll(): Promise<UserResponseDto[]> {
|
||||
const users = await this.usersService.findAll();
|
||||
return users.map(u => plainToInstance(UserResponseDto, u));
|
||||
return users.map((u) => plainToInstance(UserResponseDto, u));
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@@ -5137,11 +5092,7 @@ import { BullModule } from '@nestjs/bullmq';
|
||||
},
|
||||
},
|
||||
}),
|
||||
BullModule.registerQueue(
|
||||
{ name: 'email' },
|
||||
{ name: 'reports' },
|
||||
{ name: 'notifications' },
|
||||
),
|
||||
BullModule.registerQueue({ name: 'email' }, { name: 'reports' }, { name: 'notifications' }),
|
||||
],
|
||||
})
|
||||
export class QueueModule {}
|
||||
@@ -5149,9 +5100,7 @@ export class QueueModule {}
|
||||
// Producer: Add jobs to queue
|
||||
@Injectable()
|
||||
export class ReportsService {
|
||||
constructor(
|
||||
@InjectQueue('reports') private reportsQueue: Queue,
|
||||
) {}
|
||||
constructor(@InjectQueue('reports') private reportsQueue: Queue) {}
|
||||
|
||||
async requestReport(dto: GenerateReportDto): Promise<{ jobId: string }> {
|
||||
// Return immediately, process in background
|
||||
@@ -5406,9 +5355,7 @@ export class DatabaseService implements OnApplicationShutdown {
|
||||
console.log(`Database service shutting down on ${signal}`);
|
||||
|
||||
// Close all connections gracefully
|
||||
await Promise.all(
|
||||
this.connections.map((conn) => conn.close()),
|
||||
);
|
||||
await Promise.all(this.connections.map((conn) => conn.close()));
|
||||
|
||||
console.log('All database connections closed');
|
||||
}
|
||||
@@ -5477,9 +5424,7 @@ export class HealthController {
|
||||
throw new ServiceUnavailableException('Shutting down');
|
||||
}
|
||||
|
||||
return this.health.check([
|
||||
() => this.db.pingCheck('database'),
|
||||
]);
|
||||
return this.health.check([() => this.db.pingCheck('database')]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5608,9 +5553,7 @@ export const appConfig = registerAs('app', () => ({
|
||||
|
||||
// config/validation.schema.ts
|
||||
export const validationSchema = Joi.object({
|
||||
NODE_ENV: Joi.string()
|
||||
.valid('development', 'production', 'test')
|
||||
.default('development'),
|
||||
NODE_ENV: Joi.string().valid('development', 'production', 'test').default('development'),
|
||||
PORT: Joi.number().default(3000),
|
||||
DB_HOST: Joi.string().required(),
|
||||
DB_PORT: Joi.number().default(5432),
|
||||
@@ -5906,10 +5849,7 @@ import { LoggerModule } from 'nestjs-pino';
|
||||
LoggerModule.forRoot({
|
||||
pinoHttp: {
|
||||
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
|
||||
transport:
|
||||
process.env.NODE_ENV !== 'production'
|
||||
? { target: 'pino-pretty' }
|
||||
: undefined,
|
||||
transport: process.env.NODE_ENV !== 'production' ? { target: 'pino-pretty' } : undefined,
|
||||
redact: ['req.headers.authorization', 'req.body.password'],
|
||||
serializers: {
|
||||
req: (req) => ({
|
||||
@@ -5955,4 +5895,4 @@ Reference: [NestJS Logger](https://docs.nestjs.com/techniques/logger)
|
||||
|
||||
---
|
||||
|
||||
*Generated by build-agents.ts on 2026-01-16*
|
||||
_Generated by build-agents.ts on 2026-01-16_
|
||||
|
||||
@@ -4,7 +4,7 @@ description: NestJS best practices and architecture patterns for building produc
|
||||
license: MIT
|
||||
metadata:
|
||||
author: Kadajett
|
||||
version: "1.1.0"
|
||||
version: '1.1.0'
|
||||
---
|
||||
|
||||
# NestJS Best Practices
|
||||
@@ -24,18 +24,18 @@ Reference these guidelines when:
|
||||
|
||||
## Rule Categories by Priority
|
||||
|
||||
| Priority | Category | Impact | Prefix |
|
||||
|----------|----------|--------|--------|
|
||||
| 1 | Architecture | CRITICAL | `arch-` |
|
||||
| 2 | Dependency Injection | CRITICAL | `di-` |
|
||||
| 3 | Error Handling | HIGH | `error-` |
|
||||
| 4 | Security | HIGH | `security-` |
|
||||
| 5 | Performance | HIGH | `perf-` |
|
||||
| 6 | Testing | MEDIUM-HIGH | `test-` |
|
||||
| 7 | Database & ORM | MEDIUM-HIGH | `db-` |
|
||||
| 8 | API Design | MEDIUM | `api-` |
|
||||
| 9 | Microservices | MEDIUM | `micro-` |
|
||||
| 10 | DevOps & Deployment | LOW-MEDIUM | `devops-` |
|
||||
| Priority | Category | Impact | Prefix |
|
||||
| -------- | -------------------- | ----------- | ----------- |
|
||||
| 1 | Architecture | CRITICAL | `arch-` |
|
||||
| 2 | Dependency Injection | CRITICAL | `di-` |
|
||||
| 3 | Error Handling | HIGH | `error-` |
|
||||
| 4 | Security | HIGH | `security-` |
|
||||
| 5 | Performance | HIGH | `perf-` |
|
||||
| 6 | Testing | MEDIUM-HIGH | `test-` |
|
||||
| 7 | Database & ORM | MEDIUM-HIGH | `db-` |
|
||||
| 8 | API Design | MEDIUM | `api-` |
|
||||
| 9 | Microservices | MEDIUM | `micro-` |
|
||||
| 10 | DevOps & Deployment | LOW-MEDIUM | `devops-` |
|
||||
|
||||
## Quick Reference
|
||||
|
||||
@@ -120,6 +120,7 @@ rules/_sections.md
|
||||
```
|
||||
|
||||
Each rule file contains:
|
||||
|
||||
- Brief explanation of why it matters
|
||||
- Incorrect code example with explanation
|
||||
- Correct code example with explanation
|
||||
|
||||
+1
-1
@@ -126,7 +126,7 @@ export class UsersController {
|
||||
@SerializeOptions({ type: UserResponseDto })
|
||||
async findAll(): Promise<UserResponseDto[]> {
|
||||
const users = await this.usersService.findAll();
|
||||
return users.map(u => plainToInstance(UserResponseDto, u));
|
||||
return users.map((u) => plainToInstance(UserResponseDto, u));
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Avoid Circular Dependencies
|
||||
impact: CRITICAL
|
||||
impactDescription: "#1 cause of runtime crashes"
|
||||
impactDescription: '#1 cause of runtime crashes'
|
||||
tags: architecture, modules, dependencies
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Organize by Feature Modules
|
||||
impact: CRITICAL
|
||||
impactDescription: "3-5x faster onboarding and development"
|
||||
impactDescription: '3-5x faster onboarding and development'
|
||||
tags: architecture, modules, organization
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Single Responsibility for Services
|
||||
impact: CRITICAL
|
||||
impactDescription: "40%+ improvement in testability"
|
||||
impactDescription: '40%+ improvement in testability'
|
||||
tags: architecture, services, single-responsibility
|
||||
---
|
||||
|
||||
|
||||
+2
-6
@@ -15,9 +15,7 @@ Create custom repositories to encapsulate complex queries and database logic. Th
|
||||
// Complex queries in services
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(
|
||||
@InjectRepository(User) private repo: Repository<User>,
|
||||
) {}
|
||||
constructor(@InjectRepository(User) private repo: Repository<User>) {}
|
||||
|
||||
async findActiveWithOrders(minOrders: number): Promise<User[]> {
|
||||
// Complex query logic mixed with business logic
|
||||
@@ -42,9 +40,7 @@ export class UsersService {
|
||||
// Custom repository with encapsulated queries
|
||||
@Injectable()
|
||||
export class UsersRepository {
|
||||
constructor(
|
||||
@InjectRepository(User) private repo: Repository<User>,
|
||||
) {}
|
||||
constructor(@InjectRepository(User) private repo: Repository<User>) {}
|
||||
|
||||
async findById(id: string): Promise<User | null> {
|
||||
return this.repo.findOne({ where: { id } });
|
||||
|
||||
+4
-22
@@ -47,12 +47,7 @@ export class OrdersService {
|
||||
|
||||
for (const item of items) {
|
||||
await manager.save(OrderItem, { orderId: order.id, ...item });
|
||||
await manager.decrement(
|
||||
Inventory,
|
||||
{ productId: item.productId },
|
||||
'stock',
|
||||
item.quantity,
|
||||
);
|
||||
await manager.decrement(Inventory, { productId: item.productId }, 'stock', item.quantity);
|
||||
}
|
||||
|
||||
// If this throws, everything rolls back
|
||||
@@ -75,12 +70,7 @@ export class TransferService {
|
||||
|
||||
try {
|
||||
// Debit source account
|
||||
await queryRunner.manager.decrement(
|
||||
Account,
|
||||
{ id: fromId },
|
||||
'balance',
|
||||
amount,
|
||||
);
|
||||
await queryRunner.manager.decrement(Account, { id: fromId }, 'balance', amount);
|
||||
|
||||
// Verify sufficient funds
|
||||
const source = await queryRunner.manager.findOne(Account, {
|
||||
@@ -91,12 +81,7 @@ export class TransferService {
|
||||
}
|
||||
|
||||
// Credit destination account
|
||||
await queryRunner.manager.increment(
|
||||
Account,
|
||||
{ id: toId },
|
||||
'balance',
|
||||
amount,
|
||||
);
|
||||
await queryRunner.manager.increment(Account, { id: toId }, 'balance', amount);
|
||||
|
||||
// Log the transaction
|
||||
await queryRunner.manager.save(TransactionLog, {
|
||||
@@ -124,10 +109,7 @@ export class UsersRepository {
|
||||
private dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
async createWithProfile(
|
||||
userData: CreateUserDto,
|
||||
profileData: CreateProfileDto,
|
||||
): Promise<User> {
|
||||
async createWithProfile(userData: CreateUserDto, profileData: CreateProfileDto): Promise<User> {
|
||||
return this.dataSource.transaction(async (manager) => {
|
||||
const user = await manager.save(User, userData);
|
||||
await manager.save(Profile, { ...profileData, userId: user.id });
|
||||
|
||||
+2
-6
@@ -79,9 +79,7 @@ export class DatabaseService implements OnApplicationShutdown {
|
||||
console.log(`Database service shutting down on ${signal}`);
|
||||
|
||||
// Close all connections gracefully
|
||||
await Promise.all(
|
||||
this.connections.map((conn) => conn.close()),
|
||||
);
|
||||
await Promise.all(this.connections.map((conn) => conn.close()));
|
||||
|
||||
console.log('All database connections closed');
|
||||
}
|
||||
@@ -150,9 +148,7 @@ export class HealthController {
|
||||
throw new ServiceUnavailableException('Shutting down');
|
||||
}
|
||||
|
||||
return this.health.check([
|
||||
() => this.db.pingCheck('database'),
|
||||
]);
|
||||
return this.health.check([() => this.db.pingCheck('database')]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-3
@@ -61,9 +61,7 @@ export const appConfig = registerAs('app', () => ({
|
||||
|
||||
// config/validation.schema.ts
|
||||
export const validationSchema = Joi.object({
|
||||
NODE_ENV: Joi.string()
|
||||
.valid('development', 'production', 'test')
|
||||
.default('development'),
|
||||
NODE_ENV: Joi.string().valid('development', 'production', 'test').default('development'),
|
||||
PORT: Joi.number().default(3000),
|
||||
DB_HOST: Joi.string().required(),
|
||||
DB_PORT: Joi.number().default(5432),
|
||||
|
||||
@@ -194,10 +194,7 @@ import { LoggerModule } from 'nestjs-pino';
|
||||
LoggerModule.forRoot({
|
||||
pinoHttp: {
|
||||
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
|
||||
transport:
|
||||
process.env.NODE_ENV !== 'production'
|
||||
? { target: 'pino-pretty' }
|
||||
: undefined,
|
||||
transport: process.env.NODE_ENV !== 'production' ? { target: 'pino-pretty' } : undefined,
|
||||
redact: ['req.headers.authorization', 'req.body.password'],
|
||||
serializers: {
|
||||
req: (req) => ({
|
||||
|
||||
+5
-5
@@ -43,12 +43,12 @@ export class OrdersService {
|
||||
// Testing is painful - must mock unused methods
|
||||
const mockNotificationService = {
|
||||
sendEmail: jest.fn(),
|
||||
sendSms: jest.fn(), // Never used, but required
|
||||
sendPush: jest.fn(), // Never used, but required
|
||||
sendSlack: jest.fn(), // Never used, but required
|
||||
logNotification: jest.fn(), // Never used, but required
|
||||
sendSms: jest.fn(), // Never used, but required
|
||||
sendPush: jest.fn(), // Never used, but required
|
||||
sendSlack: jest.fn(), // Never used, but required
|
||||
logNotification: jest.fn(), // Never used, but required
|
||||
getDeliveryStatus: jest.fn(), // Never used, but required
|
||||
retryFailed: jest.fn(), // Never used, but required
|
||||
retryFailed: jest.fn(), // Never used, but required
|
||||
scheduleNotification: jest.fn(), // Never used, but required
|
||||
};
|
||||
```
|
||||
|
||||
+3
-7
@@ -178,9 +178,7 @@ export class OrdersService {
|
||||
|
||||
```typescript
|
||||
// Shared test suite that any implementation must pass
|
||||
function testPaymentGatewayContract(
|
||||
createGateway: () => PaymentGateway,
|
||||
) {
|
||||
function testPaymentGatewayContract(createGateway: () => PaymentGateway) {
|
||||
describe('PaymentGateway contract', () => {
|
||||
let gateway: PaymentGateway;
|
||||
|
||||
@@ -197,13 +195,11 @@ function testPaymentGatewayContract(
|
||||
});
|
||||
|
||||
it('throws InvalidCurrencyException for unsupported currency', async () => {
|
||||
await expect(gateway.charge(1000, 'INVALID'))
|
||||
.rejects.toThrow(InvalidCurrencyException);
|
||||
await expect(gateway.charge(1000, 'INVALID')).rejects.toThrow(InvalidCurrencyException);
|
||||
});
|
||||
|
||||
it('throws TransactionNotFoundException for invalid refund', async () => {
|
||||
await expect(gateway.refund('nonexistent'))
|
||||
.rejects.toThrow(TransactionNotFoundException);
|
||||
await expect(gateway.refund('nonexistent')).rejects.toThrow(TransactionNotFoundException);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user