diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..cdb87a5d2 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,178 @@ +# AGENTS.md + +This file provides guidance to AI coding agents working in this repository. + +## Quality Bar (Required) + +After creating or modifying code, run typecheck, lint, and format as needed: + +- `pnpm typecheck` +- `pnpm lint` +- `pnpm format` + +## Quick Start + +- Install deps: `pnpm install` +- Start DB: `docker compose up -d` +- Initialize schema: `pnpm db:push` +- Run dev: `pnpm dev` (HTTPS, accept certs) and visit `https://localhost:6969` + +## Common Commands + +- `pnpm dev` - Start development server (requires Docker for PostgreSQL) +- `pnpm build` - Build all packages and apps +- `pnpm build:preview` - Build with preview environment (uses `.env`) +- `pnpm build:production` - Build for production (uses `.env.production`) +- `pnpm lint` - Run Biome linting across all workspaces +- `pnpm typecheck` - Run TypeScript type checking across all workspaces +- `pnpm format` - Format code using Biome +- `pnpm check` - Run Biome check with auto-fix + +## Database Commands + +- `pnpm db:push` - Push schema changes to local database (force) +- `pnpm db:mg` - Generate migration files (local env) +- `pnpm db:mp:local` - Apply migrations to local database +- `pnpm db:mp:preview` - Apply migrations to preview database +- `pnpm db:mp:production` - Apply migrations to production database +- `pnpm db:studio` - Open Drizzle Studio + +## Environment Management + +Environment variables are encrypted using dotenvx: + +- `.env.production` - Production builds +- `.env` - Local development and preview builds +- `.env.local` - Local-only settings (git-ignored) + +Set variables with: `pnpm env:set ` +Use `-f` to target a specific env file: `pnpm env:set -f .env.local` + +## Repository Layout + +This is a TypeScript monorepo using Turborepo: + +### Apps + +- `apps/www` - Main web app (TanStack Start, Vite dev server on 6969). +- `apps/seo` - SEO product app (TanStack Start + Cloudflare Workers). +- `apps/seo-www` - Marketing site for SEO app. + +### Packages + +- `packages/api-core` - Shared oRPC handlers, CORS, OpenAPI wiring. +- `packages/api-seo` - SEO oRPC API routes, context, workflows, and handlers. +- `packages/api-user-vm` - User VM service + OpenAPI client/container exports. +- `packages/auth` - Auth server/client + UI components (better-auth). +- `packages/content` - Content collections, RSS/search helpers, content UI. +- `packages/core` - Shared utilities/integrations (e.g. Octokit/Shopify helpers). +- `packages/dataforseo` - DataForSEO API client + env/types. +- `packages/db` - Drizzle ORM client, schemas, and operations. +- `packages/emails` - Unified email client. +- `packages/google-apis` - Google Search Console client. +- `packages/loro-file-system` - Loro CRDT filesystem wrapper. +- `packages/result` - Result type helpers (`ok`/`err`/`safe`). +- `packages/task` - Trigger.dev jobs/tasks (crawler/AI workflows). +- `packages/ui` - Shared UI components, hooks, and styles. + +### Tooling + +- `tooling/typescript` - Shared TS configs. +- `tooling/github` - GitHub Actions setup/utility actions. + +## Code Conventions + +- React hooks must be at component top level. +- Use kebab-case for file names (including hook files). +- Avoid enums; prefer union types. +- Do not use `as any` or casting generally. Prefer `satisfies` or explicit types. +- Do not duplicate generic formatting helpers (numbers, dates, percent, currency, string casing) inside feature files; add/reuse them in `packages/core/src/format` and import from there. +- When combining ArkType schemas, prefer attached shapes (e.g. `type({ "...": otherSchema })`) over `.merge()`. +- When using the database object, prefer `.query` over `.select().from()`. +- Create DB helpers in `packages/db/src/operations` and consume those helpers from API/routes or apps. +- Avoid god components that own unrelated queries. +- Parent/child component contract: + - Parent components should own orchestration concerns: route-level composition, open/close state, identity/context inputs (for example organization/project/entity ids), and wiring between independent features. + - Child feature components should own implementation concerns: local form state, validation, and query/mutation logic when behavior is stable across consumers. + - Shared/reusable child UI components should stay data-driven/dumb with explicit props and minimal side effects. + - Only expose callback props from child to parent when a real customization need exists (for example custom post-success navigation). Do not pass callback props preemptively. + - Avoid explicit `mode` props when mode can be derived from data presence (for example entity exists vs null/undefined). + +## Key Architecture Patterns + +### Routing (Apps) + +- TanStack Start file-based routes live under `apps/*/src/routes`. +- Use `_` prefixed folders for layout/auth groups (e.g. `_authed`). +- Use `$` for route params (e.g. `$organizationSlug`). +- Collocate UI in `-components`, hooks in `-hooks`, and helpers in `-lib`. +- `routeTree.gen.ts` is generated and used by `apps/*/src/router.tsx`. +- Use `type[]` instead of `Array`. + +### API (oRPC + ArkType) + +- API routes are in `packages/api-seo/src/routes` and composed in `packages/api-seo/src/routes/index.ts`. +- Use `base`, `protectedBase`, and `withOrganizationIdBase` from `packages/api-seo/src/context.ts` for middleware and auth. +- Input/output validation uses ArkType plus DB schemas from `packages/db/src/schema`. +- OpenAPI and RPC handlers come from `packages/api-core` (`createRpcHandler`, `createOpenAPIHandler`). + +### Data Access + +- DB schemas live in `packages/db/src/schema` using `drizzle-arktype` for insert/select/update schemas. +- Create query helpers in `packages/db/src/operations/**` and export from `packages/db/src/operations/index.ts`. +- Operations typically return `Result` (`ok`/`err`/`safe`) from `@rectangular-labs/result`. +- API routes call operations and translate failures into `ORPCError`s. + +### App ↔ API Integration + +- Client helpers live in `apps/seo/src/lib/api.ts`, using oRPC client + TanStack Query utils. +- Server-side requests use `serverClient` from `@rectangular-labs/api-seo/server` with request headers. + +## Testing + +- Root Vitest config is `vitest.config.ts`. +- App tests: `apps/seo/**/*.test.{ts,tsx}` (jsdom). +- Package tests run via `pnpm run --filter test`. + +## Environments & Deploy + +- Env is managed by dotenvx (`pnpm env:set`, `.env`, `.env.local`, `.env.production`). +- Apps use Cloudflare Workers + Wrangler; `pnpm dev` decrypts env into app `.env.local`. +- CI runs Biome, typecheck, and tests; deploys go through Cloudflare workflows. + +## Domain Context + +- `strategy-architecture.md` documents the Strategy-first model and related DB/UI changes. +- Create DB helpers in `packages/db/src/operations` and consume those helpers from API/routes or apps as relevant. + +## Package Generation + +Preferred for AI/non-interactive: use `--args` to bypass prompts. + +- Argument order: `name`, `type`, `features` +- Features: comma-separated list from `docs`, `env`, `react`, `styles` +- Use empty string `""` for no features + +Examples: + +```bash +pnpm new:package --args "my-lib" "public" "" +pnpm new:package --args "my-lib" "public" "docs" +pnpm new:package --args "my-private-lib" "private" "docs,env,react,styles" +pnpm new:package --args "my-svc" "private" "env" +pnpm new:package --args "my-svc" "private" "env,react" +``` + +Behavior: + +- Public packages get `tsup.config.ts`; private do not. +- Dependencies are normalized to latest (workspace deps remain `workspace:*`). +- After scaffolding, install, format, and lint run automatically. + +## Mind Map of full application + +We maintain a mind map of the full application in `MIND_MAP.md`. + +Always make sure that we have an up to date MIND_MAP.md file that documents the current state of the project. + +Refer to MIND_MAP_CORE.md on how to create and maintain the mind map. diff --git a/MIND_MAP.md b/MIND_MAP.md new file mode 100644 index 000000000..26d0e3ca2 --- /dev/null +++ b/MIND_MAP.md @@ -0,0 +1,49 @@ +> **For AI Agents:** This mind map is your primary knowledge index. Read overview nodes [1-5] first, then follow links [N] to find what you need. Always reference node IDs. When you encounter bugs, document your attempts in relevant nodes. When you make changes, update outdated nodes immediately—especially overview nodes since they're your springboard. Add new nodes only for genuinely new concepts. Keep it compact (20-50 nodes typical). The mind map wraps every task: consult it, rely on it, update it. + +[1] **Repository Overview** - Rectangular Labs is a TypeScript Turborepo that combines multiple web apps and shared packages into one deployable system for SEO workflows, content operations, and marketing surfaces [2][4][5]. The current architecture centers on `apps/seo` for the product, `packages/api-seo` for oRPC endpoints and Cloudflare workflows, and `packages/db` for Drizzle schemas plus operations [6][9][11]. The strategy-first model is actively implemented in schema, routes, and workflows, with deeper design rationale tracked in the architecture doc [13][14][15][23]. + +[2] **Monorepo and Tooling Spine** - Workspace boundaries are declared in `pnpm-workspace.yaml` (`apps/*`, `packages/*`, `tooling/*`), and root scripts in `package.json` orchestrate build, dev, DB commands, lint, format, and type checks across the graph [1][22]. The stack uses `turbo`, `pnpm`, TypeScript 5, Biome, and Vitest, with dotenvx-based env management and Cloudflare-compatible app runtimes [21][22]. Package-level scripts generally compile with `tsc` or `tsup`, while app-level scripts decrypt env and run TanStack Start via Vite [4][5][6]. + +[3] **End-to-End Request/Data Flow** - Browser requests enter TanStack Start routes in each app, then API calls from app clients use `@rectangular-labs/api-seo/client` or server client helpers to hit `/api/rpc/*` [4][6][9]. The API layer validates input/output with ArkType, applies auth/org middleware, and delegates persistence to `packages/db/src/operations` helpers that return `Result` objects [10][12][18]. Async work is kicked to task/workflow providers and later polled through task status APIs, with results feeding strategy/content views and snapshots [14][15][16]. + +[4] **Application Surfaces and Routing** - The repo has three primary TanStack Start apps (`apps/seo`, `apps/www`, `apps/seo-www`) with file-based routing and generated route trees [1][6][7][8]. `apps/seo` contains authenticated organization/project routes, onboarding, content tabs, settings, strategy detail/list pages, and a gated links exchange page under nested `_authed` and `$param` folders [6][13]. `apps/www` and `apps/seo-www` serve marketing/docs/blog/legal content and separate brand narratives while still sharing workspace packages for UI and content plumbing [7][8][19]. + +[5] **Backend Service Architecture** - API behavior is composed from `packages/api-core` and domain-specific packages (`api-seo`, `api-user-vm`, `task`, `db`, integrations), giving a layered model of handlers, context, routes, operations, and workflow execution [1][9][10]. `api-core` provides reusable handler/link utilities for RPC, OpenAPI, WebSocket RPC, CORS, and request/response header plugins [9][18]. `api-seo` is the main domain API, while `task` and workflow bindings support long-running generation, planning, onboarding, and strategy execution [15][16][20]. + +[6] **SEO Product App (`apps/seo`)** - `apps/seo` is the operational UI for organizations and projects, with route groups for onboarding, dashboard/content, settings, strategies, and links exchange management [4][13][14]. API access is centralized in `apps/seo/src/lib/api.ts`, which chooses server or browser clients and integrates TanStack Query utilities for data fetching [3][9]. Content now supports both table-driven drawer details and a dedicated deep-link details route (`/$organizationSlug/$projectSlug/content/$draftId`), with shared rendering logic in `-components/content-display.tsx` so drawer/page experiences stay aligned [14][19]. Trend graph rendering for clicks/impressions/CTR/position is also centralized in `-components/snapshot-trend-chart.tsx` and reused by both content details and strategy detail surfaces to avoid duplicated chart logic. Top-keyword controls and visualization are now similarly shared via `-components/top-keywords.tsx`, with common metric/search/sort/page behavior used in both content details and strategy detail tabs. Server routes `/api/rpc/$` create per-request API context then dispatch to the shared RPC handler, making app and API boundaries explicit but colocated [9][10]. + +[7] **Main Marketing App (`apps/www`)** - `apps/www` is a broader marketing/docs/blog surface for Rectangular Labs, containing marketing routes, docs routes, and blog/RSS endpoints [4][19]. It shares the app/runtime conventions of the repo (TanStack Start + Vite + decrypted env local files) but is structurally content-centric rather than operations-centric [2][21]. This app acts as brand and documentation frontage that complements, but does not replace, product workflows in `apps/seo` [1][6]. + +[8] **SEO Marketing App (`apps/seo-www`)** - `apps/seo-www` is a dedicated marketing site for the SEO product with campaign-oriented landing routes, legal pages, referral paths, and blog/search endpoints [4][7]. Its route components are organized around narrative variants (founders/original/seo-experts) while still using shared workspace utilities and content patterns [19][21]. It should be kept aligned with product messaging as strategy-first UX evolves in the main app [6][13][23]. + +[9] **API Route Composition (`packages/api-seo`)** - `packages/api-seo/src/routes/index.ts` lazily composes route modules (`project`, `chat`, `task`, `content`, `strategy`, `integrations`, auth subroutes) into a single router tree [5][10]. Domain routes use `base`, `protectedBase`, and `withOrganizationIdBase` middleware chains, then validate with ArkType and DB-derived schemas [10][18]. The strategy route family now splits snapshot behavior into a dedicated sub-router (`routes/strategy.snapshot.ts`) nested under `strategy.snapshot`, while `routes/strategy.ts` keeps list/get/create/update/phase ownership [13][14][16]. + +[10] **API Context and Middleware** - `createApiContext` wires DB, auth handler, buckets, KV, scheduler, and workflow bindings into typed request context, then oRPC middleware enriches it with session/user and logging/async storage capabilities [5][9][16]. `protectedBase` enforces authenticated requests; `withOrganizationIdBase` additionally requires active organization and is reused across organization-scoped routes [9][14]. This context model keeps domain handlers thin and pushes shared concerns (auth/session headers/env wiring) into one place [3][18]. + +[11] **Database Schema Topology (`packages/db/src/schema`)** - Drizzle schema modules define auth and SEO tables, including projects, integrations, chat, content draft/published records, task runs, and the strategy table family [1][12][13]. The strategy schema set (`strategy`, `strategy_phase`, `strategy_phase_content`, `strategy_snapshot`, `strategy_snapshot_content`) is already present with relations and typed insert/select/update schemas [13][15][18]. Indexes and soft-delete filtering conventions support query patterns used by operations and route handlers [12][14]. + +[12] **Database Operations Pattern** - Data access is intentionally centralized in `packages/db/src/operations/**`, exporting cohesive helpers and returning `ok/err/safe` results from `@rectangular-labs/result` [3][11][18]. Strategy operations now include lightweight strategy detail retrieval (`getStrategyDetailsLite`) plus snapshot-series/content-series/latest-content/keyword-aggregation helpers consumed by `strategy.snapshot.*` endpoints, in addition to existing create/update/phase/snapshot write helpers [13][14][15]. This pattern keeps SQL/Drizzle complexity out of app routes and enforces consistent error handling boundaries before converting failures to `ORPCError`s [9][10]. + +[13] **Strategy Domain Model** - Strategy is the current domain backbone: a long-lived strategy holds motivation and goal, phases model execution slices, phase-content links tie drafts/plans to work items, and snapshots capture aggregate performance over time while delta values are derived from adjacent snapshots at read-time instead of persisted columns [11][12][14][15]. Status enums in schemas/parsers support suggestion-to-active lifecycle transitions and dismissal/observation states used by UI and workflows [14][18]. This model is codified in schema files and reflected in product routes (`.../strategies`) and onboarding components [4][6][23]. + +[14] **Strategy API Behaviors** - `packages/api-seo/src/routes/strategy.ts` implements CRUD/list/detail endpoints plus phase creation/update and validates organization ownership before mutations [9][10][13]. Strategy detail responses are now intentionally lightweight (`latest 2 snapshots` with aggregate+delta only) while snapshot-heavy data is fetched through nested endpoints: `strategy.snapshot.series`, `strategy.snapshot.content.list` (unsorted payload), and `strategy.snapshot.keywords.list`; content drawer details now live under `content.getDraftDetails`, and project-wide content overview rows are served by `content.listOverview` [6][9][11][12]. Creating or activating a strategy still triggers phase generation, and manual snapshot trigger now lives under the dedicated snapshot sub-router (`strategy.snapshot.create`) [15][16]. This route family remains both a mutation surface and a workflow/scheduler boundary, so regressions here directly affect adoption and metrics freshness [6][9][22]. + +[15] **Strategy Phase Generation Workflow (`strategy-phase-generation-workflow.ts`)** - The workflow loads project/strategy context, generates the next phase with tool-assisted analysis (GSC, DataForSEO, web, strategy tools), creates phase/content artifacts through DB operations, and includes prior phase history in generation context [11][12][13]. It supports repeated phase generation, aligns phase/content creation with current parser schema, triggers writer workflows for generated/updated drafts, and now queues the dedicated snapshot workflow instead of taking snapshots inline [14][16][18]. Phase `targetCompletionDate` computation uses shared utility `packages/core/src/strategy/compute-phase-target-completion-date.ts` so cadence math stays consistent across workflow and UI [18][23]. This file remains a critical implementation node for strategy-first execution [14][23]. + +[16] **Task and Workflow Orchestration** - `packages/api-seo/src/routes/task.ts` abstracts provider differences and exposes unified task creation/status responses, mapping Trigger.dev and Cloudflare workflow states into a shared status contract [3][5][14]. Workflow bindings include planner, writer, onboarding, strategy suggestions, strategy phase generation, and strategy snapshot generation; task polling resolves provider-specific execution details and typed outputs [9][15]. This layer decouples UI progress UX from execution backend choices while preserving typed contracts via core task schemas [6][18]. + +[17] **External Integrations** - Integration route modules (`integration.gsc`, `integration.github`, `integration.shopify`, webhook handlers) connect project data to external systems and feed content/strategy workflows [9][13][16]. `packages/google-apis` supplies GSC access used in strategy metrics gathering, while `packages/dataforseo` and other integration libraries support broader SEO signal ingestion [11][20]. Integration health and credential state are prerequisites for high-quality automation outcomes in onboarding and strategy execution [6][21]. + +[18] **Shared Schemas and Contracts (`packages/core`)** - `packages/core/src/schemas` holds ArkType schemas for task and strategy parsing, including cadence, statuses, snapshot types, and workflow IO contracts consumed by both API and workers [9][11][13]. Project publishing settings now explicitly include `participateInLinkExchange` so the Links page toggle persists through `project.update` and remains consistent with settings saves/default workspace provisioning [4][6][9]. Core now also contains shared strategy scheduling logic at `packages/core/src/strategy/compute-phase-target-completion-date.ts` with dedicated tests, reducing duplicate cadence/date logic across API and app layers [6][15][22]. This shared contract+logic layer keeps validation and behavior synchronized across apps, routes, DB schema typing, and workflow implementations [3][15][16]. Architectural convention discourages ad-hoc casting and favors explicit schema-driven typing, making this package a key safety rail [2][22]. + +[19] **Shared UI/Auth/Content Packages** - `packages/ui`, `packages/auth`, and `packages/content` supply reusable front-end components, auth flows, and content/blog utilities across the three apps [4][6][7]. `auth` integrates with API context/session middleware, while `content` powers blog/docs surfaces in marketing apps [10][7]. UI primitives now also include a responsive `PopoverTooltip` (`popover` on mobile, `tooltip` on desktop) used by content keyword detail affordances in the SEO app [6][14]. Reuse through these packages prevents duplicate implementation logic and keeps app-specific routes focused on orchestration and presentation [2][5]. + +[20] **Background Tasks Package (`packages/task`)** - `packages/task` contains Trigger.dev and crawling/search helpers (`trigger/*`, `crawlers/*`, `lib/ai-tools/*`) and exposes client utilities used by API task status logic [5][16][17]. Its responsibilities include site understanding and SEO-related asynchronous jobs that complement Cloudflare workflow-based execution [3][21]. This package is part of the broader execution substrate and should stay consistent with task input/output schemas in `packages/core` [18][22]. + +[21] **Environment and Deployment Model** - Env variables are managed with dotenvx (`.env`, `.env.local`, `.env.production`), and app dev scripts decrypt env into local files before running Vite/TanStack Start [2][6][8]. Cloudflare Workers/Wrangler runtime assumptions are embedded in API/workflow code (`cloudflare:workers`, workflow bindings), and DB access expects local Docker-backed PostgreSQL during development [5][11]. Deploy/build paths (`build`, `build:preview`, `build:production`) should be reflected in operational docs and onboarding for contributors [22][23]. + +[22] **Quality and Testing Bar** - Repository policy requires running `pnpm typecheck`, `pnpm lint`, and `pnpm format` after code changes, and these commands are orchestrated through Turbo at workspace scope [2][1]. Vitest is configured at the root with app-level test patterns, while package tests run via filtered workspace commands [6][20]. For any future architecture or workflow changes, update map nodes and add/adjust tests where behavior transitions are encoded (especially strategy and task status paths) [14][16][23]. + +[23] **Living Documentation and Evolution** - `strategy-architecture.md` captures the strategy-first design intent and remains the primary narrative reference for why schema/UI/workflow shifts are being made [13][15]. `AGENTS.md` mandates keeping `MIND_MAP.md` current, so architecture updates should include node edits and link rewiring in the same change set [1][22]. When implementation diverges from the strategy architecture doc, record the delta in this map and either update the design doc or annotate interim states here [11][24]. + +[24] **Current Gaps and Maintenance Notes** - This initial project map now exists but should be refined with commit-history nodes and subsystem deep dives when major refactors land, following the methodology in `PROJECT_MIND_MAPPING.md` [23][1]. Notable watch areas are strategy lifecycle edge cases, workflow idempotency, and alignment between marketing messaging (`apps/seo-www`) and implemented product capability (`apps/seo`) [8][14][15]. Any new top-level app/package or major routing/schema rewrite should trigger immediate updates to overview nodes [1-5] before adding specialized nodes [2][4][11]. diff --git a/MIND_MAP_CORE.md b/MIND_MAP_CORE.md new file mode 100644 index 000000000..f52b2b0bf --- /dev/null +++ b/MIND_MAP_CORE.md @@ -0,0 +1,13 @@ +# Mind Map Format - Self-Documentation + +> **For AI Agents:** This mind map is your primary knowledge index. Read overview nodes [1-5] first, then follow links [N] to find what you need. Always reference node IDs. When you encounter bugs, document your attempts in relevant nodes. When you make changes, update outdated nodes immediately—especially overview nodes since they're your springboard. Add new nodes only for genuinely new concepts. Keep it compact (20-50 nodes typical). The mind map wraps every task: consult it, rely on it, update it. + +[1] **Mind Map Format Overview** - A graph-based documentation format stored as plain text files where each node is a single line containing an ID, title, and inline references [2]. The format leverages LLM familiarity with citation-style references from academic papers, making it natural to generate and edit [3]. It serves as a superset structure that can represent trees, lists, or any graph topology [4], scaling from small projects (<50 nodes) to complex systems (500+ nodes) [5]. The methodology is fully detailed in PROJECT_MIND_MAPPING.md with bootstrapping tools available. + +[2] **Node Syntax Structure** - Each node follows the format: `[N] **Node Title** - node text with [N] references inlined` [1]. Nodes are line-oriented, allowing line-by-line loading and editing by AI models [3]. The inline reference syntax `[N]` creates bidirectional navigation between concepts, with links embedded naturally within descriptive text rather than as separate metadata [1][4]. This structure is both machine-parseable and human-readable, supporting grep-based lookups for quick node retrieval [3]. + +[3] **Technical Advantages** - The format enables line-by-line overwriting of nodes without complex parsing [2], making incremental updates efficient for both humans and AI agents [1]. Grep operations allow instant node lookup by ID or keyword without loading the entire file [2]. The text-based storage ensures version control compatibility, diff-friendly editing, and zero tooling dependencies [4]. LLMs generate this format naturally because citation syntax `[N]` mirrors academic paper references they've seen extensively during training [1][5]. + +[4] **Graph Topology Benefits** - Unlike hierarchical trees or linear lists, the graph structure allows many-to-many relationships between concepts [1]. Any node can reference any other node, creating knowledge clusters around related topics [2][3]. The format accommodates cyclic references for concepts that mutually depend on each other, captures cross-cutting concerns that span multiple subsystems, and supports progressive refinement where nodes are added to densify understanding [5]. This flexibility makes it suitable as a universal knowledge representation format [1]. + +[5] **Scalability and Usage Patterns** - Small projects typically need fewer than 50 nodes to capture core architecture, data flow, and key implementations [1]. Complex topics or large codebases can scale to 500+ nodes by adding specialized deep-dive nodes for algorithms, optimizations, and subsystems [4]. The methodology includes a bootstrap prompt (linked gist) for generating initial mind maps from existing codebases automatically [1]. Scale is managed through overview nodes [1-5] that serve as navigation hubs, with detail nodes forming clusters around major concepts [3][4]. The format remains navigable at any scale due to inline linking and grep-based search [2][3]. diff --git a/PROJECT_MIND_MAPPING.md b/PROJECT_MIND_MAPPING.md new file mode 100644 index 000000000..4265d4fb1 --- /dev/null +++ b/PROJECT_MIND_MAPPING.md @@ -0,0 +1,378 @@ +# Project Mind Mapping - Methodology Guide + +A comprehensive guide for creating interconnected mind map documentation that captures both the current state and evolutionary history of software projects. + +## Overview + +Mind maps transform complex codebases into navigable knowledge graphs where each node represents a key concept and links create an interconnected understanding web. This methodology combines architectural analysis with historical context to create living documentation. + +## Mind Map Format + +### Usage Instructions Header + +**Every MIND_MAP.md file must start with this exact text at the very top before any nodes:** + +```markdown +> **For AI Agents:** This mind map is your primary knowledge index. Read overview nodes [1-5] first, then follow links [N] to find what you need. Always reference node IDs. When you encounter bugs, document your attempts in relevant nodes. When you make changes, update outdated nodes immediately—especially overview nodes since they're your springboard. Add new nodes only for genuinely new concepts. Keep it compact (20-50 nodes typical). The mind map wraps every task: consult it, rely on it, update it. +``` + +### Node Structure + +Each node follows this structure: + +```markdown +[Node Number] **Node Title** - Node text where you add [] links embedded naturally in the text. Text should be moderate size (3-8 sentences), dense with information but readable. +``` + +### Key Principles: + +1. **Natural Link Integration**: Links [1][2][3] should flow naturally within sentences, not just listed at the end +2. **Moderate Density**: Each node should be substantial but scannable - aim for 100-300 words +3. **Semantic Grouping**: Related concepts should link bidirectionally to create knowledge clusters +4. **Progressive Detail**: Start with high-level concepts, drill down to implementation details +5. **Cross-References**: Every node should link to at least 2-3 other nodes; important nodes link to 5-10 + +## Phase 1: Current State Analysis + +### Step 1: Initial Reconnaissance + +Start with broad exploration to understand project structure: + +```bash +# Explore project layout +ls -la +find . -type f -name "*.md" | head -20 +find . -type f -name "README*" +find . -type f -name "package.json" +find . -type f -name "*.config.*" +``` + +**Read these first:** +- README files (project overview, setup, usage) +- Documentation files in docs/ or top-level +- Configuration files (package.json, tsconfig.json, etc.) +- TODO or ROADMAP files + +### Step 2: Architecture Discovery + +Use semantic search and file reading to understand: + +**Core Components:** +```bash +# Find entry points +grep -r "main\|index\|app" --include="*.{ts,tsx,js,jsx,py}" -l + +# Identify key directories +ls -d */ | grep -v node_modules +``` + +**Technology Stack:** +- Frontend: Look for React, Vue, Angular, etc. in package.json +- Backend: Express, Flask, FastAPI, etc. +- Build tools: Vite, Webpack, etc. +- Key libraries: Check dependencies + +**Data Flow:** +Use `codebase_search` to trace: +- "How does data flow from input to output?" +- "Where is the main state managed?" +- "How do components communicate?" + +### Step 3: Feature Mapping + +Identify major features by exploring: +- Component files in src/components/ +- API endpoints in server/routes/ or similar +- Utility functions in src/utils/ +- Data models in types/ or models/ + +For each feature, understand: +- **What**: What does it do? +- **How**: Implementation approach +- **Why**: Design decisions +- **Dependencies**: What it connects to + +### Step 4: Implementation Details + +Dive into key algorithms, patterns, and systems: + +```bash +# Find interesting implementations +grep -r "class\|function\|const.*=.*=>|def " --include="*.{ts,js,py}" + +# Look for comments explaining complex logic +grep -r "TODO\|FIXME\|NOTE\|IMPORTANT" --include="*.{ts,js,py}" +``` + +Read critical files completely: +- Core algorithm implementations +- State management logic +- API integration code +- Data transformation pipelines + +## Phase 2: Historical Analysis + +### Step 5: Git History Exploration + +**Get the full timeline:** +```bash +# All commits for the project/folder +git log --all --date=short --pretty=format:"%h | %ad | %s" -- path/to/project/ + +# With file change stats +git log --all --date=short --stat --pretty=format:"%n=== %h | %ad | %s ===" -- path/to/project/ + +# Find first commit that created the folder +git log --all --diff-filter=A --date=short --pretty=format:"%h | %ad | %s" -- path/to/project/ | tail -5 +``` + +**Understand each major commit:** +```bash +# Show what changed in a commit +git show --stat + +# See the actual changes +git show -- path/to/specific/file +``` + +**Identify development phases:** +- Initial creation commit +- Major refactors (large insertions/deletions) +- Feature additions (new files) +- Architectural changes (file renames, deletions) +- Bug fixes and refinements (small changes) + +### Step 6: Evolution Patterns + +Look for: +- **Technology migrations**: Library changes, framework upgrades +- **Architecture shifts**: Monolith → microservices, REST → GraphQL +- **Feature expansion**: What was added over time? +- **Simplifications**: What was removed or refactored? + +**Timeline markers:** +```bash +# Commits by date with line counts +git log --all --shortstat --pretty=format:"%h | %ad | %s" --date=short -- path/to/project/ +``` + +## Phase 3: Mind Map Construction + +### Step 7: Node Planning + +Create a hierarchical outline before writing: + +**Level 1: Foundation (Nodes 1-5)** +- [1] Project Overview - What, why, high-level architecture +- [2] Core Theory/Concept - Fundamental principles or domain theory +- [3] Data Flow - How information moves through the system +- [4] Frontend/UI Architecture - User-facing components +- [5] Backend/Services Architecture - Server-side logic + +**Level 2: Systems (Nodes 6-15)** +- [6-10] Major subsystems (e.g., data schema, algorithms, file management, validation, AI integration) +- [11-15] Key features and components + +**Level 3: Implementation (Nodes 16-20)** +- [16] Technology stack details +- [17] Historical context/migrations +- [18] Development workflow +- [19] Future roadmap/TODOs +- [20] Design principles + +**Level 4: Deep Dives (Nodes 21-25+)** +- [21+] Specialized topics (specific algorithms, optimization, interpretation, error handling, performance) +- [N] Development history with commit details + +### Step 8: Writing Nodes + +For each node: + +1. **Start with a clear title**: Noun phrase that names the concept +2. **Opening sentence**: Define what this node is about, link to parent concepts [1][2] +3. **Core content**: Explain the concept with specific details +4. **Implementation details**: Code structure, file locations, key functions +5. **Link to related nodes**: Embed [N] references naturally throughout +6. **Technical specifics**: Parameters, configurations, examples + +**Writing style:** +- Dense but readable - every sentence should add information +- Use specific examples: "The PCAVisualizer class in pcaVisualizer.ts" not "the visualizer" +- Include numbers: "5-10 features", "23,000+ lines", "700 ticks" +- Reference actual filenames, function names, variable names +- Explain WHY decisions were made, not just WHAT exists + +### Step 9: Link Weaving + +After drafting all nodes: + +1. **Identify connections**: Which nodes discuss related concepts? +2. **Add forward and backward links**: If [5] mentions [12], make sure [12] mentions [5] +3. **Create knowledge clusters**: Groups of highly interconnected nodes (e.g., all visualization nodes link to each other) +4. **Build progressive paths**: [1]→[2]→[7]→[8] should form a coherent learning path +5. **Verify link accuracy**: Every [N] reference should point to a relevant node + +**Link density guidelines:** +- Overview nodes: 5-10 links to major subsystems +- System nodes: 3-7 links to related systems and implementation details +- Implementation nodes: 2-5 links to parent systems and related details +- Specialized nodes: 2-4 links to closely related concepts + +### Step 10: Historical Node Integration + +The history node should: + +1. **List all major commits chronologically** with hashes +2. **Explain what each commit did** (files changed, features added) +3. **Show line count changes** for context on commit size +4. **Identify development phases** (prototype, rewrite, refinement, migration) +5. **Connect to other nodes** - link commits to the features they introduced + +**Template:** +```markdown +[N] **Development History** - The project evolved over X days/months from DATE to DATE [parent-nodes]. +Commit HASH (DATE) created the initial folder with FILE1, FILE2, and X features [feature-nodes]. +Commit HASH (DATE) was the massive rewrite with N insertions creating SYSTEM1 [node], SYSTEM2 [node], +and SYSTEM3 [node]. Commit HASH (DATE) added FEATURE [node] with N insertions. ... +Total development: X commits, ~N lines of code, transforming from STATE1 to STATE2 [principle-nodes]. +``` + +## Quality Checklist + +### Completeness: +- [ ] All major systems/features have nodes +- [ ] Technology stack is documented +- [ ] Data flow is explained +- [ ] Every significant file/component is mentioned +- [ ] Development history is captured with commit hashes +- [ ] Future directions are noted + +### Interconnectedness: +- [ ] Every node has 2+ links +- [ ] Important nodes have 5+ links +- [ ] Links are embedded naturally in text +- [ ] Bidirectional links exist where appropriate +- [ ] Node clusters form around major concepts + +### Clarity: +- [ ] Each node has a clear, specific title +- [ ] First sentence defines the concept +- [ ] Technical terms are explained or linked +- [ ] Code examples use actual file/function names +- [ ] Node length is moderate (not too short, not overwhelming) + +### Accuracy: +- [ ] All file paths are correct +- [ ] Function/class names match the code +- [ ] Numbers and metrics are accurate +- [ ] Commit hashes are verified +- [ ] Links point to relevant nodes + +## Example Node Structures + +### System Architecture Node +```markdown +[N] **System Name** - Brief definition and purpose [parent-node]. The system consists of +COMPONENT1 which handles TASK1 [detail-node], COMPONENT2 for TASK2 [detail-node], and +COMPONENT3 managing TASK3 [detail-node]. Implementation resides in path/to/files using +TECHNOLOGY [tech-node] with KEY_PATTERN design pattern [pattern-node]. The system +integrates with EXTERNAL_SYSTEM [integration-node] through API_METHOD and processes +data using ALGORITHM [algorithm-node]. Key parameters include PARAM1 (range, default) +and PARAM2 (type, purpose) [parameter-node]. +``` + +### Implementation Detail Node +```markdown +[N] **Algorithm/Feature Name** - Technical description [parent-system-node][theory-node]. +The ClassName in path/to/file.ts implements APPROACH [architecture-node]. The algorithm +follows these steps: first, STEP1 with DETAILS [step1-node], then STEP2 involving +COMPUTATION [step2-node], and finally STEP3 producing OUTPUT [step3-node]. Parameters +include PARAM1 (type, purpose, default) and PARAM2 (range, effect) [parameter-node]. +Performance characteristics: TIME_COMPLEXITY for typical datasets with SIZE_RANGE +[performance-node]. The implementation uses LIBRARY for TASK [tech-node]. +``` + +### Historical Node +```markdown +[N] **Development History** - The project evolved over TIMESPAN from DATE1 to DATE2 +[overview-node][architecture-node]. Commit HASH1 (DATE) created the initial structure +with FRAMEWORK [theory-node], TOOL [tech-node], and N founding ITEMS [item-node]. +Commit HASH2 (DATE) was the major milestone with N insertions creating SYSTEM1 +[system1-node], SYSTEM2 [system2-node], and SYSTEM3 [system3-node]. Commit HASH3 +(DATE) introduced FEATURE with N insertions [feature-node]. Commit HASH4 (DATE) +executed the MIGRATION with N insertions [migration-node], implementing NEW_APPROACH +[approach-node] and documenting the shift in FILE [doc-node]. Total development: +N commits, ~N lines, transforming from STATE1 to STATE2 [principle-node]. +``` + +## Tools and Commands Reference + +### File Exploration +```bash +# Find all files of type +find . -name "*.ts" -not -path "*/node_modules/*" + +# Count lines of code +find . -name "*.ts" -not -path "*/node_modules/*" | xargs wc -l + +# Search for patterns +grep -r "pattern" --include="*.ts" -n +``` + +### Git Analysis +```bash +# Commit history for specific path +git log --all --oneline -- path/ + +# Detailed history with stats +git log --all --stat --date=short --pretty=format:"%h | %ad | %s" -- path/ + +# See what changed in commit +git show + +# Find when file was created +git log --diff-filter=A --follow -- path/to/file + +# Count commits +git log --all --oneline -- path/ | wc -l + +# Largest commits +git log --all --shortstat --oneline -- path/ | grep -E "file.*change" | sort -t' ' -k4 -rn | head -10 +``` + +### Code Analysis +```bash +# Find all classes/functions +grep -r "^class\|^function\|^const.*= " --include="*.ts" + +# Find imports/dependencies +grep -r "^import" --include="*.ts" | cut -d'"' -f2 | sort -u + +# Find all exports +grep -r "^export" --include="*.ts" +``` + +## Final Tips + +1. **Start broad, then drill down**: Overview → Systems → Implementation → Details +2. **Write for future you**: Assume you'll forget everything in 6 months +3. **Include the "why"**: Design decisions, not just features +4. **Link generously**: Better too many links than too few +5. **Update iteratively**: Add nodes as you discover new areas +6. **Test navigation**: Can you follow links to learn any topic? +7. **Capture uncertainty**: Note TODOs or unclear areas +8. **Balance depth and breadth**: Cover everything, deep-dive on key systems + +## Success Criteria + +A good mind map should enable someone to: +- Understand what the project does (overview nodes) +- Learn how it works (system and implementation nodes) +- Trace any feature from concept to code (following links) +- Understand design decisions (principle and history nodes) +- See how the project evolved (history node) +- Find specific implementations (detailed nodes with file paths) +- Identify areas for contribution (TODO and future nodes) + +The mind map becomes a **living index** into the codebase, making onboarding, maintenance, and evolution dramatically easier. + diff --git a/apps/seo-www/src/routes/-components/seo-experts/data.tsx b/apps/seo-www/src/routes/-components/seo-experts/data.tsx index 29286aeb8..ca710ea9f 100644 --- a/apps/seo-www/src/routes/-components/seo-experts/data.tsx +++ b/apps/seo-www/src/routes/-components/seo-experts/data.tsx @@ -3,7 +3,7 @@ import { Section } from "@rectangular-labs/ui/components/ui/section"; const cards = [ { title: "GSC-aware Chat", - body: "Connects directly to Google Search Console and DataForSEO so every plan is grounded in real demand.", + body: "Connects directly to Google Search Console so every plan is grounded in real demand.", }, { title: "Article Writer", diff --git a/apps/seo/src/routeTree.gen.ts b/apps/seo/src/routeTree.gen.ts index 915379180..8887313d1 100644 --- a/apps/seo/src/routeTree.gen.ts +++ b/apps/seo/src/routeTree.gen.ts @@ -22,22 +22,18 @@ import { Route as AuthedOrganizationSlugProjectSlugRouteRouteImport } from './ro import { Route as AuthedOrganizationSlugProjectSlugIndexRouteImport } from './routes/_authed/$organizationSlug/$projectSlug/index' import { Route as AuthedOrganizationSlugSettingsTeamRouteImport } from './routes/_authed/$organizationSlug/settings/team' import { Route as AuthedOrganizationSlugProjectSlugSettingsRouteRouteImport } from './routes/_authed/$organizationSlug/$projectSlug/settings/route' -import { Route as AuthedOrganizationSlugProjectSlugContentRouteRouteImport } from './routes/_authed/$organizationSlug/$projectSlug/content/route' +import { Route as AuthedOrganizationSlugProjectSlugStrategiesIndexRouteImport } from './routes/_authed/$organizationSlug/$projectSlug/strategies/index' import { Route as AuthedOrganizationSlugProjectSlugSettingsIndexRouteImport } from './routes/_authed/$organizationSlug/$projectSlug/settings/index' +import { Route as AuthedOrganizationSlugProjectSlugLinksIndexRouteImport } from './routes/_authed/$organizationSlug/$projectSlug/links/index' import { Route as AuthedOrganizationSlugProjectSlugContentIndexRouteImport } from './routes/_authed/$organizationSlug/$projectSlug/content/index' +import { Route as AuthedOrganizationSlugProjectSlugStrategiesStrategyIdRouteImport } from './routes/_authed/$organizationSlug/$projectSlug/strategies/$strategyId' import { Route as AuthedOrganizationSlugProjectSlugSettingsWritingSettingsRouteImport } from './routes/_authed/$organizationSlug/$projectSlug/settings/writing-settings' import { Route as AuthedOrganizationSlugProjectSlugSettingsPublishingSettingsRouteImport } from './routes/_authed/$organizationSlug/$projectSlug/settings/publishing-settings' import { Route as AuthedOrganizationSlugProjectSlugSettingsProjectRouteImport } from './routes/_authed/$organizationSlug/$projectSlug/settings/project' import { Route as AuthedOrganizationSlugProjectSlugSettingsImageSettingsRouteImport } from './routes/_authed/$organizationSlug/$projectSlug/settings/image-settings' import { Route as AuthedOrganizationSlugProjectSlugSettingsBusinessBackgroundRouteImport } from './routes/_authed/$organizationSlug/$projectSlug/settings/business-background' -import { Route as AuthedOrganizationSlugProjectSlugContentScheduledRouteImport } from './routes/_authed/$organizationSlug/$projectSlug/content/scheduled' -import { Route as AuthedOrganizationSlugProjectSlugContentPublishedRouteImport } from './routes/_authed/$organizationSlug/$projectSlug/content/published' -import { Route as AuthedOrganizationSlugProjectSlugContentReviewRouteRouteImport } from './routes/_authed/$organizationSlug/$projectSlug/content/review/route' +import { Route as AuthedOrganizationSlugProjectSlugContentDraftIdRouteImport } from './routes/_authed/$organizationSlug/$projectSlug/content/$draftId' import { Route as AuthedOrganizationSlugProjectSlugSettingsIntegrationsIndexRouteImport } from './routes/_authed/$organizationSlug/$projectSlug/settings/integrations/index' -import { Route as AuthedOrganizationSlugProjectSlugContentReviewIndexRouteImport } from './routes/_authed/$organizationSlug/$projectSlug/content/review/index' -import { Route as AuthedOrganizationSlugProjectSlugContentReviewOutlinesRouteImport } from './routes/_authed/$organizationSlug/$projectSlug/content/review/outlines' -import { Route as AuthedOrganizationSlugProjectSlugContentReviewNewArticlesRouteImport } from './routes/_authed/$organizationSlug/$projectSlug/content/review/new-articles' -import { Route as AuthedOrganizationSlugProjectSlugContentReviewArticleUpdatesRouteImport } from './routes/_authed/$organizationSlug/$projectSlug/content/review/article-updates' const LoginRoute = LoginRouteImport.update({ id: '/login', @@ -110,10 +106,10 @@ const AuthedOrganizationSlugProjectSlugSettingsRouteRoute = path: '/settings', getParentRoute: () => AuthedOrganizationSlugProjectSlugRouteRoute, } as any) -const AuthedOrganizationSlugProjectSlugContentRouteRoute = - AuthedOrganizationSlugProjectSlugContentRouteRouteImport.update({ - id: '/content', - path: '/content', +const AuthedOrganizationSlugProjectSlugStrategiesIndexRoute = + AuthedOrganizationSlugProjectSlugStrategiesIndexRouteImport.update({ + id: '/strategies/', + path: '/strategies/', getParentRoute: () => AuthedOrganizationSlugProjectSlugRouteRoute, } as any) const AuthedOrganizationSlugProjectSlugSettingsIndexRoute = @@ -122,11 +118,23 @@ const AuthedOrganizationSlugProjectSlugSettingsIndexRoute = path: '/', getParentRoute: () => AuthedOrganizationSlugProjectSlugSettingsRouteRoute, } as any) +const AuthedOrganizationSlugProjectSlugLinksIndexRoute = + AuthedOrganizationSlugProjectSlugLinksIndexRouteImport.update({ + id: '/links/', + path: '/links/', + getParentRoute: () => AuthedOrganizationSlugProjectSlugRouteRoute, + } as any) const AuthedOrganizationSlugProjectSlugContentIndexRoute = AuthedOrganizationSlugProjectSlugContentIndexRouteImport.update({ - id: '/', - path: '/', - getParentRoute: () => AuthedOrganizationSlugProjectSlugContentRouteRoute, + id: '/content/', + path: '/content/', + getParentRoute: () => AuthedOrganizationSlugProjectSlugRouteRoute, + } as any) +const AuthedOrganizationSlugProjectSlugStrategiesStrategyIdRoute = + AuthedOrganizationSlugProjectSlugStrategiesStrategyIdRouteImport.update({ + id: '/strategies/$strategyId', + path: '/strategies/$strategyId', + getParentRoute: () => AuthedOrganizationSlugProjectSlugRouteRoute, } as any) const AuthedOrganizationSlugProjectSlugSettingsWritingSettingsRoute = AuthedOrganizationSlugProjectSlugSettingsWritingSettingsRouteImport.update({ @@ -162,23 +170,11 @@ const AuthedOrganizationSlugProjectSlugSettingsBusinessBackgroundRoute = getParentRoute: () => AuthedOrganizationSlugProjectSlugSettingsRouteRoute, } as any, ) -const AuthedOrganizationSlugProjectSlugContentScheduledRoute = - AuthedOrganizationSlugProjectSlugContentScheduledRouteImport.update({ - id: '/scheduled', - path: '/scheduled', - getParentRoute: () => AuthedOrganizationSlugProjectSlugContentRouteRoute, - } as any) -const AuthedOrganizationSlugProjectSlugContentPublishedRoute = - AuthedOrganizationSlugProjectSlugContentPublishedRouteImport.update({ - id: '/published', - path: '/published', - getParentRoute: () => AuthedOrganizationSlugProjectSlugContentRouteRoute, - } as any) -const AuthedOrganizationSlugProjectSlugContentReviewRouteRoute = - AuthedOrganizationSlugProjectSlugContentReviewRouteRouteImport.update({ - id: '/review', - path: '/review', - getParentRoute: () => AuthedOrganizationSlugProjectSlugContentRouteRoute, +const AuthedOrganizationSlugProjectSlugContentDraftIdRoute = + AuthedOrganizationSlugProjectSlugContentDraftIdRouteImport.update({ + id: '/content/$draftId', + path: '/content/$draftId', + getParentRoute: () => AuthedOrganizationSlugProjectSlugRouteRoute, } as any) const AuthedOrganizationSlugProjectSlugSettingsIntegrationsIndexRoute = AuthedOrganizationSlugProjectSlugSettingsIntegrationsIndexRouteImport.update({ @@ -186,36 +182,6 @@ const AuthedOrganizationSlugProjectSlugSettingsIntegrationsIndexRoute = path: '/integrations/', getParentRoute: () => AuthedOrganizationSlugProjectSlugSettingsRouteRoute, } as any) -const AuthedOrganizationSlugProjectSlugContentReviewIndexRoute = - AuthedOrganizationSlugProjectSlugContentReviewIndexRouteImport.update({ - id: '/', - path: '/', - getParentRoute: () => - AuthedOrganizationSlugProjectSlugContentReviewRouteRoute, - } as any) -const AuthedOrganizationSlugProjectSlugContentReviewOutlinesRoute = - AuthedOrganizationSlugProjectSlugContentReviewOutlinesRouteImport.update({ - id: '/outlines', - path: '/outlines', - getParentRoute: () => - AuthedOrganizationSlugProjectSlugContentReviewRouteRoute, - } as any) -const AuthedOrganizationSlugProjectSlugContentReviewNewArticlesRoute = - AuthedOrganizationSlugProjectSlugContentReviewNewArticlesRouteImport.update({ - id: '/new-articles', - path: '/new-articles', - getParentRoute: () => - AuthedOrganizationSlugProjectSlugContentReviewRouteRoute, - } as any) -const AuthedOrganizationSlugProjectSlugContentReviewArticleUpdatesRoute = - AuthedOrganizationSlugProjectSlugContentReviewArticleUpdatesRouteImport.update( - { - id: '/article-updates', - path: '/article-updates', - getParentRoute: () => - AuthedOrganizationSlugProjectSlugContentReviewRouteRoute, - } as any, - ) export interface FileRoutesByFullPath { '/': typeof IndexRoute @@ -227,24 +193,20 @@ export interface FileRoutesByFullPath { '/api/rpc/$': typeof ApiRpcSplatRoute '/$organizationSlug/': typeof AuthedOrganizationSlugIndexRoute '/onboarding': typeof AuthedOnboardingIndexRoute - '/$organizationSlug/$projectSlug/content': typeof AuthedOrganizationSlugProjectSlugContentRouteRouteWithChildren '/$organizationSlug/$projectSlug/settings': typeof AuthedOrganizationSlugProjectSlugSettingsRouteRouteWithChildren '/$organizationSlug/settings/team': typeof AuthedOrganizationSlugSettingsTeamRoute '/$organizationSlug/$projectSlug/': typeof AuthedOrganizationSlugProjectSlugIndexRoute - '/$organizationSlug/$projectSlug/content/review': typeof AuthedOrganizationSlugProjectSlugContentReviewRouteRouteWithChildren - '/$organizationSlug/$projectSlug/content/published': typeof AuthedOrganizationSlugProjectSlugContentPublishedRoute - '/$organizationSlug/$projectSlug/content/scheduled': typeof AuthedOrganizationSlugProjectSlugContentScheduledRoute + '/$organizationSlug/$projectSlug/content/$draftId': typeof AuthedOrganizationSlugProjectSlugContentDraftIdRoute '/$organizationSlug/$projectSlug/settings/business-background': typeof AuthedOrganizationSlugProjectSlugSettingsBusinessBackgroundRoute '/$organizationSlug/$projectSlug/settings/image-settings': typeof AuthedOrganizationSlugProjectSlugSettingsImageSettingsRoute '/$organizationSlug/$projectSlug/settings/project': typeof AuthedOrganizationSlugProjectSlugSettingsProjectRoute '/$organizationSlug/$projectSlug/settings/publishing-settings': typeof AuthedOrganizationSlugProjectSlugSettingsPublishingSettingsRoute '/$organizationSlug/$projectSlug/settings/writing-settings': typeof AuthedOrganizationSlugProjectSlugSettingsWritingSettingsRoute - '/$organizationSlug/$projectSlug/content/': typeof AuthedOrganizationSlugProjectSlugContentIndexRoute + '/$organizationSlug/$projectSlug/strategies/$strategyId': typeof AuthedOrganizationSlugProjectSlugStrategiesStrategyIdRoute + '/$organizationSlug/$projectSlug/content': typeof AuthedOrganizationSlugProjectSlugContentIndexRoute + '/$organizationSlug/$projectSlug/links': typeof AuthedOrganizationSlugProjectSlugLinksIndexRoute '/$organizationSlug/$projectSlug/settings/': typeof AuthedOrganizationSlugProjectSlugSettingsIndexRoute - '/$organizationSlug/$projectSlug/content/review/article-updates': typeof AuthedOrganizationSlugProjectSlugContentReviewArticleUpdatesRoute - '/$organizationSlug/$projectSlug/content/review/new-articles': typeof AuthedOrganizationSlugProjectSlugContentReviewNewArticlesRoute - '/$organizationSlug/$projectSlug/content/review/outlines': typeof AuthedOrganizationSlugProjectSlugContentReviewOutlinesRoute - '/$organizationSlug/$projectSlug/content/review/': typeof AuthedOrganizationSlugProjectSlugContentReviewIndexRoute + '/$organizationSlug/$projectSlug/strategies': typeof AuthedOrganizationSlugProjectSlugStrategiesIndexRoute '/$organizationSlug/$projectSlug/settings/integrations': typeof AuthedOrganizationSlugProjectSlugSettingsIntegrationsIndexRoute } export interface FileRoutesByTo { @@ -257,19 +219,17 @@ export interface FileRoutesByTo { '/onboarding': typeof AuthedOnboardingIndexRoute '/$organizationSlug/settings/team': typeof AuthedOrganizationSlugSettingsTeamRoute '/$organizationSlug/$projectSlug': typeof AuthedOrganizationSlugProjectSlugIndexRoute - '/$organizationSlug/$projectSlug/content/published': typeof AuthedOrganizationSlugProjectSlugContentPublishedRoute - '/$organizationSlug/$projectSlug/content/scheduled': typeof AuthedOrganizationSlugProjectSlugContentScheduledRoute + '/$organizationSlug/$projectSlug/content/$draftId': typeof AuthedOrganizationSlugProjectSlugContentDraftIdRoute '/$organizationSlug/$projectSlug/settings/business-background': typeof AuthedOrganizationSlugProjectSlugSettingsBusinessBackgroundRoute '/$organizationSlug/$projectSlug/settings/image-settings': typeof AuthedOrganizationSlugProjectSlugSettingsImageSettingsRoute '/$organizationSlug/$projectSlug/settings/project': typeof AuthedOrganizationSlugProjectSlugSettingsProjectRoute '/$organizationSlug/$projectSlug/settings/publishing-settings': typeof AuthedOrganizationSlugProjectSlugSettingsPublishingSettingsRoute '/$organizationSlug/$projectSlug/settings/writing-settings': typeof AuthedOrganizationSlugProjectSlugSettingsWritingSettingsRoute + '/$organizationSlug/$projectSlug/strategies/$strategyId': typeof AuthedOrganizationSlugProjectSlugStrategiesStrategyIdRoute '/$organizationSlug/$projectSlug/content': typeof AuthedOrganizationSlugProjectSlugContentIndexRoute + '/$organizationSlug/$projectSlug/links': typeof AuthedOrganizationSlugProjectSlugLinksIndexRoute '/$organizationSlug/$projectSlug/settings': typeof AuthedOrganizationSlugProjectSlugSettingsIndexRoute - '/$organizationSlug/$projectSlug/content/review/article-updates': typeof AuthedOrganizationSlugProjectSlugContentReviewArticleUpdatesRoute - '/$organizationSlug/$projectSlug/content/review/new-articles': typeof AuthedOrganizationSlugProjectSlugContentReviewNewArticlesRoute - '/$organizationSlug/$projectSlug/content/review/outlines': typeof AuthedOrganizationSlugProjectSlugContentReviewOutlinesRoute - '/$organizationSlug/$projectSlug/content/review': typeof AuthedOrganizationSlugProjectSlugContentReviewIndexRoute + '/$organizationSlug/$projectSlug/strategies': typeof AuthedOrganizationSlugProjectSlugStrategiesIndexRoute '/$organizationSlug/$projectSlug/settings/integrations': typeof AuthedOrganizationSlugProjectSlugSettingsIntegrationsIndexRoute } export interface FileRoutesById { @@ -284,24 +244,20 @@ export interface FileRoutesById { '/api/rpc/$': typeof ApiRpcSplatRoute '/_authed/$organizationSlug/': typeof AuthedOrganizationSlugIndexRoute '/_authed/onboarding/': typeof AuthedOnboardingIndexRoute - '/_authed/$organizationSlug/$projectSlug/content': typeof AuthedOrganizationSlugProjectSlugContentRouteRouteWithChildren '/_authed/$organizationSlug/$projectSlug/settings': typeof AuthedOrganizationSlugProjectSlugSettingsRouteRouteWithChildren '/_authed/$organizationSlug/settings/team': typeof AuthedOrganizationSlugSettingsTeamRoute '/_authed/$organizationSlug/$projectSlug/': typeof AuthedOrganizationSlugProjectSlugIndexRoute - '/_authed/$organizationSlug/$projectSlug/content/review': typeof AuthedOrganizationSlugProjectSlugContentReviewRouteRouteWithChildren - '/_authed/$organizationSlug/$projectSlug/content/published': typeof AuthedOrganizationSlugProjectSlugContentPublishedRoute - '/_authed/$organizationSlug/$projectSlug/content/scheduled': typeof AuthedOrganizationSlugProjectSlugContentScheduledRoute + '/_authed/$organizationSlug/$projectSlug/content/$draftId': typeof AuthedOrganizationSlugProjectSlugContentDraftIdRoute '/_authed/$organizationSlug/$projectSlug/settings/business-background': typeof AuthedOrganizationSlugProjectSlugSettingsBusinessBackgroundRoute '/_authed/$organizationSlug/$projectSlug/settings/image-settings': typeof AuthedOrganizationSlugProjectSlugSettingsImageSettingsRoute '/_authed/$organizationSlug/$projectSlug/settings/project': typeof AuthedOrganizationSlugProjectSlugSettingsProjectRoute '/_authed/$organizationSlug/$projectSlug/settings/publishing-settings': typeof AuthedOrganizationSlugProjectSlugSettingsPublishingSettingsRoute '/_authed/$organizationSlug/$projectSlug/settings/writing-settings': typeof AuthedOrganizationSlugProjectSlugSettingsWritingSettingsRoute + '/_authed/$organizationSlug/$projectSlug/strategies/$strategyId': typeof AuthedOrganizationSlugProjectSlugStrategiesStrategyIdRoute '/_authed/$organizationSlug/$projectSlug/content/': typeof AuthedOrganizationSlugProjectSlugContentIndexRoute + '/_authed/$organizationSlug/$projectSlug/links/': typeof AuthedOrganizationSlugProjectSlugLinksIndexRoute '/_authed/$organizationSlug/$projectSlug/settings/': typeof AuthedOrganizationSlugProjectSlugSettingsIndexRoute - '/_authed/$organizationSlug/$projectSlug/content/review/article-updates': typeof AuthedOrganizationSlugProjectSlugContentReviewArticleUpdatesRoute - '/_authed/$organizationSlug/$projectSlug/content/review/new-articles': typeof AuthedOrganizationSlugProjectSlugContentReviewNewArticlesRoute - '/_authed/$organizationSlug/$projectSlug/content/review/outlines': typeof AuthedOrganizationSlugProjectSlugContentReviewOutlinesRoute - '/_authed/$organizationSlug/$projectSlug/content/review/': typeof AuthedOrganizationSlugProjectSlugContentReviewIndexRoute + '/_authed/$organizationSlug/$projectSlug/strategies/': typeof AuthedOrganizationSlugProjectSlugStrategiesIndexRoute '/_authed/$organizationSlug/$projectSlug/settings/integrations/': typeof AuthedOrganizationSlugProjectSlugSettingsIntegrationsIndexRoute } export interface FileRouteTypes { @@ -316,24 +272,20 @@ export interface FileRouteTypes { | '/api/rpc/$' | '/$organizationSlug/' | '/onboarding' - | '/$organizationSlug/$projectSlug/content' | '/$organizationSlug/$projectSlug/settings' | '/$organizationSlug/settings/team' | '/$organizationSlug/$projectSlug/' - | '/$organizationSlug/$projectSlug/content/review' - | '/$organizationSlug/$projectSlug/content/published' - | '/$organizationSlug/$projectSlug/content/scheduled' + | '/$organizationSlug/$projectSlug/content/$draftId' | '/$organizationSlug/$projectSlug/settings/business-background' | '/$organizationSlug/$projectSlug/settings/image-settings' | '/$organizationSlug/$projectSlug/settings/project' | '/$organizationSlug/$projectSlug/settings/publishing-settings' | '/$organizationSlug/$projectSlug/settings/writing-settings' - | '/$organizationSlug/$projectSlug/content/' + | '/$organizationSlug/$projectSlug/strategies/$strategyId' + | '/$organizationSlug/$projectSlug/content' + | '/$organizationSlug/$projectSlug/links' | '/$organizationSlug/$projectSlug/settings/' - | '/$organizationSlug/$projectSlug/content/review/article-updates' - | '/$organizationSlug/$projectSlug/content/review/new-articles' - | '/$organizationSlug/$projectSlug/content/review/outlines' - | '/$organizationSlug/$projectSlug/content/review/' + | '/$organizationSlug/$projectSlug/strategies' | '/$organizationSlug/$projectSlug/settings/integrations' fileRoutesByTo: FileRoutesByTo to: @@ -346,19 +298,17 @@ export interface FileRouteTypes { | '/onboarding' | '/$organizationSlug/settings/team' | '/$organizationSlug/$projectSlug' - | '/$organizationSlug/$projectSlug/content/published' - | '/$organizationSlug/$projectSlug/content/scheduled' + | '/$organizationSlug/$projectSlug/content/$draftId' | '/$organizationSlug/$projectSlug/settings/business-background' | '/$organizationSlug/$projectSlug/settings/image-settings' | '/$organizationSlug/$projectSlug/settings/project' | '/$organizationSlug/$projectSlug/settings/publishing-settings' | '/$organizationSlug/$projectSlug/settings/writing-settings' + | '/$organizationSlug/$projectSlug/strategies/$strategyId' | '/$organizationSlug/$projectSlug/content' + | '/$organizationSlug/$projectSlug/links' | '/$organizationSlug/$projectSlug/settings' - | '/$organizationSlug/$projectSlug/content/review/article-updates' - | '/$organizationSlug/$projectSlug/content/review/new-articles' - | '/$organizationSlug/$projectSlug/content/review/outlines' - | '/$organizationSlug/$projectSlug/content/review' + | '/$organizationSlug/$projectSlug/strategies' | '/$organizationSlug/$projectSlug/settings/integrations' id: | '__root__' @@ -372,24 +322,20 @@ export interface FileRouteTypes { | '/api/rpc/$' | '/_authed/$organizationSlug/' | '/_authed/onboarding/' - | '/_authed/$organizationSlug/$projectSlug/content' | '/_authed/$organizationSlug/$projectSlug/settings' | '/_authed/$organizationSlug/settings/team' | '/_authed/$organizationSlug/$projectSlug/' - | '/_authed/$organizationSlug/$projectSlug/content/review' - | '/_authed/$organizationSlug/$projectSlug/content/published' - | '/_authed/$organizationSlug/$projectSlug/content/scheduled' + | '/_authed/$organizationSlug/$projectSlug/content/$draftId' | '/_authed/$organizationSlug/$projectSlug/settings/business-background' | '/_authed/$organizationSlug/$projectSlug/settings/image-settings' | '/_authed/$organizationSlug/$projectSlug/settings/project' | '/_authed/$organizationSlug/$projectSlug/settings/publishing-settings' | '/_authed/$organizationSlug/$projectSlug/settings/writing-settings' + | '/_authed/$organizationSlug/$projectSlug/strategies/$strategyId' | '/_authed/$organizationSlug/$projectSlug/content/' + | '/_authed/$organizationSlug/$projectSlug/links/' | '/_authed/$organizationSlug/$projectSlug/settings/' - | '/_authed/$organizationSlug/$projectSlug/content/review/article-updates' - | '/_authed/$organizationSlug/$projectSlug/content/review/new-articles' - | '/_authed/$organizationSlug/$projectSlug/content/review/outlines' - | '/_authed/$organizationSlug/$projectSlug/content/review/' + | '/_authed/$organizationSlug/$projectSlug/strategies/' | '/_authed/$organizationSlug/$projectSlug/settings/integrations/' fileRoutesById: FileRoutesById } @@ -494,11 +440,11 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthedOrganizationSlugProjectSlugSettingsRouteRouteImport parentRoute: typeof AuthedOrganizationSlugProjectSlugRouteRoute } - '/_authed/$organizationSlug/$projectSlug/content': { - id: '/_authed/$organizationSlug/$projectSlug/content' - path: '/content' - fullPath: '/$organizationSlug/$projectSlug/content' - preLoaderRoute: typeof AuthedOrganizationSlugProjectSlugContentRouteRouteImport + '/_authed/$organizationSlug/$projectSlug/strategies/': { + id: '/_authed/$organizationSlug/$projectSlug/strategies/' + path: '/strategies' + fullPath: '/$organizationSlug/$projectSlug/strategies' + preLoaderRoute: typeof AuthedOrganizationSlugProjectSlugStrategiesIndexRouteImport parentRoute: typeof AuthedOrganizationSlugProjectSlugRouteRoute } '/_authed/$organizationSlug/$projectSlug/settings/': { @@ -508,12 +454,26 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthedOrganizationSlugProjectSlugSettingsIndexRouteImport parentRoute: typeof AuthedOrganizationSlugProjectSlugSettingsRouteRoute } + '/_authed/$organizationSlug/$projectSlug/links/': { + id: '/_authed/$organizationSlug/$projectSlug/links/' + path: '/links' + fullPath: '/$organizationSlug/$projectSlug/links' + preLoaderRoute: typeof AuthedOrganizationSlugProjectSlugLinksIndexRouteImport + parentRoute: typeof AuthedOrganizationSlugProjectSlugRouteRoute + } '/_authed/$organizationSlug/$projectSlug/content/': { id: '/_authed/$organizationSlug/$projectSlug/content/' - path: '/' - fullPath: '/$organizationSlug/$projectSlug/content/' + path: '/content' + fullPath: '/$organizationSlug/$projectSlug/content' preLoaderRoute: typeof AuthedOrganizationSlugProjectSlugContentIndexRouteImport - parentRoute: typeof AuthedOrganizationSlugProjectSlugContentRouteRoute + parentRoute: typeof AuthedOrganizationSlugProjectSlugRouteRoute + } + '/_authed/$organizationSlug/$projectSlug/strategies/$strategyId': { + id: '/_authed/$organizationSlug/$projectSlug/strategies/$strategyId' + path: '/strategies/$strategyId' + fullPath: '/$organizationSlug/$projectSlug/strategies/$strategyId' + preLoaderRoute: typeof AuthedOrganizationSlugProjectSlugStrategiesStrategyIdRouteImport + parentRoute: typeof AuthedOrganizationSlugProjectSlugRouteRoute } '/_authed/$organizationSlug/$projectSlug/settings/writing-settings': { id: '/_authed/$organizationSlug/$projectSlug/settings/writing-settings' @@ -550,26 +510,12 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthedOrganizationSlugProjectSlugSettingsBusinessBackgroundRouteImport parentRoute: typeof AuthedOrganizationSlugProjectSlugSettingsRouteRoute } - '/_authed/$organizationSlug/$projectSlug/content/scheduled': { - id: '/_authed/$organizationSlug/$projectSlug/content/scheduled' - path: '/scheduled' - fullPath: '/$organizationSlug/$projectSlug/content/scheduled' - preLoaderRoute: typeof AuthedOrganizationSlugProjectSlugContentScheduledRouteImport - parentRoute: typeof AuthedOrganizationSlugProjectSlugContentRouteRoute - } - '/_authed/$organizationSlug/$projectSlug/content/published': { - id: '/_authed/$organizationSlug/$projectSlug/content/published' - path: '/published' - fullPath: '/$organizationSlug/$projectSlug/content/published' - preLoaderRoute: typeof AuthedOrganizationSlugProjectSlugContentPublishedRouteImport - parentRoute: typeof AuthedOrganizationSlugProjectSlugContentRouteRoute - } - '/_authed/$organizationSlug/$projectSlug/content/review': { - id: '/_authed/$organizationSlug/$projectSlug/content/review' - path: '/review' - fullPath: '/$organizationSlug/$projectSlug/content/review' - preLoaderRoute: typeof AuthedOrganizationSlugProjectSlugContentReviewRouteRouteImport - parentRoute: typeof AuthedOrganizationSlugProjectSlugContentRouteRoute + '/_authed/$organizationSlug/$projectSlug/content/$draftId': { + id: '/_authed/$organizationSlug/$projectSlug/content/$draftId' + path: '/content/$draftId' + fullPath: '/$organizationSlug/$projectSlug/content/$draftId' + preLoaderRoute: typeof AuthedOrganizationSlugProjectSlugContentDraftIdRouteImport + parentRoute: typeof AuthedOrganizationSlugProjectSlugRouteRoute } '/_authed/$organizationSlug/$projectSlug/settings/integrations/': { id: '/_authed/$organizationSlug/$projectSlug/settings/integrations/' @@ -578,85 +524,9 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthedOrganizationSlugProjectSlugSettingsIntegrationsIndexRouteImport parentRoute: typeof AuthedOrganizationSlugProjectSlugSettingsRouteRoute } - '/_authed/$organizationSlug/$projectSlug/content/review/': { - id: '/_authed/$organizationSlug/$projectSlug/content/review/' - path: '/' - fullPath: '/$organizationSlug/$projectSlug/content/review/' - preLoaderRoute: typeof AuthedOrganizationSlugProjectSlugContentReviewIndexRouteImport - parentRoute: typeof AuthedOrganizationSlugProjectSlugContentReviewRouteRoute - } - '/_authed/$organizationSlug/$projectSlug/content/review/outlines': { - id: '/_authed/$organizationSlug/$projectSlug/content/review/outlines' - path: '/outlines' - fullPath: '/$organizationSlug/$projectSlug/content/review/outlines' - preLoaderRoute: typeof AuthedOrganizationSlugProjectSlugContentReviewOutlinesRouteImport - parentRoute: typeof AuthedOrganizationSlugProjectSlugContentReviewRouteRoute - } - '/_authed/$organizationSlug/$projectSlug/content/review/new-articles': { - id: '/_authed/$organizationSlug/$projectSlug/content/review/new-articles' - path: '/new-articles' - fullPath: '/$organizationSlug/$projectSlug/content/review/new-articles' - preLoaderRoute: typeof AuthedOrganizationSlugProjectSlugContentReviewNewArticlesRouteImport - parentRoute: typeof AuthedOrganizationSlugProjectSlugContentReviewRouteRoute - } - '/_authed/$organizationSlug/$projectSlug/content/review/article-updates': { - id: '/_authed/$organizationSlug/$projectSlug/content/review/article-updates' - path: '/article-updates' - fullPath: '/$organizationSlug/$projectSlug/content/review/article-updates' - preLoaderRoute: typeof AuthedOrganizationSlugProjectSlugContentReviewArticleUpdatesRouteImport - parentRoute: typeof AuthedOrganizationSlugProjectSlugContentReviewRouteRoute - } } } -interface AuthedOrganizationSlugProjectSlugContentReviewRouteRouteChildren { - AuthedOrganizationSlugProjectSlugContentReviewArticleUpdatesRoute: typeof AuthedOrganizationSlugProjectSlugContentReviewArticleUpdatesRoute - AuthedOrganizationSlugProjectSlugContentReviewNewArticlesRoute: typeof AuthedOrganizationSlugProjectSlugContentReviewNewArticlesRoute - AuthedOrganizationSlugProjectSlugContentReviewOutlinesRoute: typeof AuthedOrganizationSlugProjectSlugContentReviewOutlinesRoute - AuthedOrganizationSlugProjectSlugContentReviewIndexRoute: typeof AuthedOrganizationSlugProjectSlugContentReviewIndexRoute -} - -const AuthedOrganizationSlugProjectSlugContentReviewRouteRouteChildren: AuthedOrganizationSlugProjectSlugContentReviewRouteRouteChildren = - { - AuthedOrganizationSlugProjectSlugContentReviewArticleUpdatesRoute: - AuthedOrganizationSlugProjectSlugContentReviewArticleUpdatesRoute, - AuthedOrganizationSlugProjectSlugContentReviewNewArticlesRoute: - AuthedOrganizationSlugProjectSlugContentReviewNewArticlesRoute, - AuthedOrganizationSlugProjectSlugContentReviewOutlinesRoute: - AuthedOrganizationSlugProjectSlugContentReviewOutlinesRoute, - AuthedOrganizationSlugProjectSlugContentReviewIndexRoute: - AuthedOrganizationSlugProjectSlugContentReviewIndexRoute, - } - -const AuthedOrganizationSlugProjectSlugContentReviewRouteRouteWithChildren = - AuthedOrganizationSlugProjectSlugContentReviewRouteRoute._addFileChildren( - AuthedOrganizationSlugProjectSlugContentReviewRouteRouteChildren, - ) - -interface AuthedOrganizationSlugProjectSlugContentRouteRouteChildren { - AuthedOrganizationSlugProjectSlugContentReviewRouteRoute: typeof AuthedOrganizationSlugProjectSlugContentReviewRouteRouteWithChildren - AuthedOrganizationSlugProjectSlugContentPublishedRoute: typeof AuthedOrganizationSlugProjectSlugContentPublishedRoute - AuthedOrganizationSlugProjectSlugContentScheduledRoute: typeof AuthedOrganizationSlugProjectSlugContentScheduledRoute - AuthedOrganizationSlugProjectSlugContentIndexRoute: typeof AuthedOrganizationSlugProjectSlugContentIndexRoute -} - -const AuthedOrganizationSlugProjectSlugContentRouteRouteChildren: AuthedOrganizationSlugProjectSlugContentRouteRouteChildren = - { - AuthedOrganizationSlugProjectSlugContentReviewRouteRoute: - AuthedOrganizationSlugProjectSlugContentReviewRouteRouteWithChildren, - AuthedOrganizationSlugProjectSlugContentPublishedRoute: - AuthedOrganizationSlugProjectSlugContentPublishedRoute, - AuthedOrganizationSlugProjectSlugContentScheduledRoute: - AuthedOrganizationSlugProjectSlugContentScheduledRoute, - AuthedOrganizationSlugProjectSlugContentIndexRoute: - AuthedOrganizationSlugProjectSlugContentIndexRoute, - } - -const AuthedOrganizationSlugProjectSlugContentRouteRouteWithChildren = - AuthedOrganizationSlugProjectSlugContentRouteRoute._addFileChildren( - AuthedOrganizationSlugProjectSlugContentRouteRouteChildren, - ) - interface AuthedOrganizationSlugProjectSlugSettingsRouteRouteChildren { AuthedOrganizationSlugProjectSlugSettingsBusinessBackgroundRoute: typeof AuthedOrganizationSlugProjectSlugSettingsBusinessBackgroundRoute AuthedOrganizationSlugProjectSlugSettingsImageSettingsRoute: typeof AuthedOrganizationSlugProjectSlugSettingsImageSettingsRoute @@ -691,19 +561,31 @@ const AuthedOrganizationSlugProjectSlugSettingsRouteRouteWithChildren = ) interface AuthedOrganizationSlugProjectSlugRouteRouteChildren { - AuthedOrganizationSlugProjectSlugContentRouteRoute: typeof AuthedOrganizationSlugProjectSlugContentRouteRouteWithChildren AuthedOrganizationSlugProjectSlugSettingsRouteRoute: typeof AuthedOrganizationSlugProjectSlugSettingsRouteRouteWithChildren AuthedOrganizationSlugProjectSlugIndexRoute: typeof AuthedOrganizationSlugProjectSlugIndexRoute + AuthedOrganizationSlugProjectSlugContentDraftIdRoute: typeof AuthedOrganizationSlugProjectSlugContentDraftIdRoute + AuthedOrganizationSlugProjectSlugStrategiesStrategyIdRoute: typeof AuthedOrganizationSlugProjectSlugStrategiesStrategyIdRoute + AuthedOrganizationSlugProjectSlugContentIndexRoute: typeof AuthedOrganizationSlugProjectSlugContentIndexRoute + AuthedOrganizationSlugProjectSlugLinksIndexRoute: typeof AuthedOrganizationSlugProjectSlugLinksIndexRoute + AuthedOrganizationSlugProjectSlugStrategiesIndexRoute: typeof AuthedOrganizationSlugProjectSlugStrategiesIndexRoute } const AuthedOrganizationSlugProjectSlugRouteRouteChildren: AuthedOrganizationSlugProjectSlugRouteRouteChildren = { - AuthedOrganizationSlugProjectSlugContentRouteRoute: - AuthedOrganizationSlugProjectSlugContentRouteRouteWithChildren, AuthedOrganizationSlugProjectSlugSettingsRouteRoute: AuthedOrganizationSlugProjectSlugSettingsRouteRouteWithChildren, AuthedOrganizationSlugProjectSlugIndexRoute: AuthedOrganizationSlugProjectSlugIndexRoute, + AuthedOrganizationSlugProjectSlugContentDraftIdRoute: + AuthedOrganizationSlugProjectSlugContentDraftIdRoute, + AuthedOrganizationSlugProjectSlugStrategiesStrategyIdRoute: + AuthedOrganizationSlugProjectSlugStrategiesStrategyIdRoute, + AuthedOrganizationSlugProjectSlugContentIndexRoute: + AuthedOrganizationSlugProjectSlugContentIndexRoute, + AuthedOrganizationSlugProjectSlugLinksIndexRoute: + AuthedOrganizationSlugProjectSlugLinksIndexRoute, + AuthedOrganizationSlugProjectSlugStrategiesIndexRoute: + AuthedOrganizationSlugProjectSlugStrategiesIndexRoute, } const AuthedOrganizationSlugProjectSlugRouteRouteWithChildren = diff --git a/apps/seo/src/routes/_authed/$organizationSlug/$projectSlug/-components/connect-gsc-banner.tsx b/apps/seo/src/routes/_authed/$organizationSlug/$projectSlug/-components/connect-gsc-banner.tsx deleted file mode 100644 index cad4d7630..000000000 --- a/apps/seo/src/routes/_authed/$organizationSlug/$projectSlug/-components/connect-gsc-banner.tsx +++ /dev/null @@ -1,35 +0,0 @@ -"use client"; - -import * as Icons from "@rectangular-labs/ui/components/icon"; -import { - Alert, - AlertDescription, - AlertTitle, -} from "@rectangular-labs/ui/components/ui/alert"; -import { Link } from "@tanstack/react-router"; - -export function ConnectGscBanner() { - return ( - - - Estimated Data - -

- Data is estimated by combining search volume and estimated traffic - from various data providers. These numbers are directional only. -

-

- - Connect - {" "} - your Google Search Console property to unlock up to date data. -

-
-
- ); -} diff --git a/apps/seo/src/routes/_authed/$organizationSlug/$projectSlug/-components/content-details-drawer.tsx b/apps/seo/src/routes/_authed/$organizationSlug/$projectSlug/-components/content-details-drawer.tsx new file mode 100644 index 000000000..5f28ed72a --- /dev/null +++ b/apps/seo/src/routes/_authed/$organizationSlug/$projectSlug/-components/content-details-drawer.tsx @@ -0,0 +1,65 @@ +import { Button } from "@rectangular-labs/ui/components/ui/button"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, +} from "@rectangular-labs/ui/components/ui/sheet"; +import { Link } from "@tanstack/react-router"; +import { ContentDisplay, useContentDisplayController } from "./content-display"; + +export function ContentDetailsDrawer({ + contentDraftId, + organizationIdentifier, + organizationSlug, + projectId, + projectSlug, + onClose, +}: { + contentDraftId: string | null; + organizationIdentifier: string; + organizationSlug: string; + projectId: string; + projectSlug: string; + onClose: () => void; +}) { + const open = !!contentDraftId; + + const controller = useContentDisplayController({ + draftId: contentDraftId, + organizationIdentifier, + projectId, + }); + + return ( + !nextOpen && onClose()} open={open}> + + + + {controller.details?.contentDraft.title ?? "Content details"} + + + + + + Open page + + + ) : null + } + /> + + + ); +} diff --git a/apps/seo/src/routes/_authed/$organizationSlug/$projectSlug/-components/content-display.tsx b/apps/seo/src/routes/_authed/$organizationSlug/$projectSlug/-components/content-display.tsx new file mode 100644 index 000000000..d54aee8ea --- /dev/null +++ b/apps/seo/src/routes/_authed/$organizationSlug/$projectSlug/-components/content-display.tsx @@ -0,0 +1,757 @@ +import type { RouterOutputs } from "@rectangular-labs/api-seo/types"; +import { formatNullableCurrency } from "@rectangular-labs/core/format/currency"; +import { formatNullableNumber } from "@rectangular-labs/core/format/number"; +import { + formatNullablePercent, + formatNullableSignedPercent, +} from "@rectangular-labs/core/format/percent"; +import * as Icons from "@rectangular-labs/ui/components/icon"; +import { MarkdownEditor } from "@rectangular-labs/ui/components/markdown-editor"; +import { + Alert, + AlertDescription, + AlertTitle, +} from "@rectangular-labs/ui/components/ui/alert"; +import { Button } from "@rectangular-labs/ui/components/ui/button"; +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@rectangular-labs/ui/components/ui/card"; +import { + DialogDrawer, + DialogDrawerDescription, + DialogDrawerFooter, + DialogDrawerHeader, + DialogDrawerTitle, +} from "@rectangular-labs/ui/components/ui/dialog-drawer"; +import { PopoverTooltip } from "@rectangular-labs/ui/components/ui/popover-tooltip"; +import { Separator } from "@rectangular-labs/ui/components/ui/separator"; +import { toast } from "@rectangular-labs/ui/components/ui/sonner"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@rectangular-labs/ui/components/ui/tabs"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { type ReactNode, useEffect, useRef, useState } from "react"; +import { getApiClientRq } from "~/lib/api"; +import { LoadingError } from "~/routes/_authed/-components/loading-error"; +import { ManageContentMetadataDialog } from "./manage-content-metadata-dialog"; +import { + type SnapshotMetric, + SnapshotTrendChart, +} from "./snapshot-trend-chart"; +import { TopKeywords, type TopKeywordsSortOrder } from "./top-keywords"; + +export function useContentDisplayController({ + draftId, + organizationIdentifier, + projectId, +}: { + draftId: string | null; + organizationIdentifier: string; + projectId: string; +}) { + const api = getApiClientRq(); + const queryClient = useQueryClient(); + const hasDraft = !!draftId; + + const contentDetailsQuery = useQuery( + api.content.getDraftDetails.queryOptions({ + input: { + organizationIdentifier, + projectId, + id: draftId ?? "00000000-0000-0000-0000-000000000000", + months: 3, + }, + enabled: hasDraft, + staleTime: 1000 * 60 * 10, + gcTime: 1000 * 60 * 60, + refetchInterval: (context) => { + const draft = context.state.data?.contentDraft; + if ( + draft?.status === "queued" || + draft?.status === "planning" || + draft?.status === "writing" || + draft?.status === "reviewing-writing" + ) { + return 8_000; + } + return false; + }, + }), + ); + + const details = contentDetailsQuery.data; + const topKeywords = details?.metricSnapshot?.topKeywords ?? []; + const draft = details?.contentDraft; + + const { data: generatingOutlineStatusData } = useQuery( + api.task.getStatus.queryOptions({ + input: { id: draft?.outlineGeneratedByTaskRunId ?? "" }, + enabled: hasDraft && !!draft?.outlineGeneratedByTaskRunId, + refetchInterval: (context) => { + const task = context.state.data; + if ( + task?.status === "pending" || + task?.status === "queued" || + task?.status === "running" + ) { + return 8_000; + } + return false; + }, + }), + ); + + const { data: generatingArticleStatusData } = useQuery( + api.task.getStatus.queryOptions({ + input: { id: draft?.generatedByTaskRunId ?? "" }, + enabled: hasDraft && !!draft?.generatedByTaskRunId, + refetchInterval: (context) => { + const task = context.state.data; + if ( + task?.status === "pending" || + task?.status === "queued" || + task?.status === "running" + ) { + return 8_000; + } + return false; + }, + }), + ); + + const isGeneratingOutline = + !!draft?.outlineGeneratedByTaskRunId && + (generatingOutlineStatusData?.status === "pending" || + generatingOutlineStatusData?.status === "running" || + generatingOutlineStatusData?.status === "queued"); + + const isGeneratingArticle = + !!draft?.generatedByTaskRunId && + (generatingArticleStatusData?.status === "pending" || + generatingArticleStatusData?.status === "running" || + generatingArticleStatusData?.status === "queued"); + + const isGenerating = isGeneratingOutline || isGeneratingArticle; + const canEdit = !!draft && !isGenerating; + + const [isMetadataOpen, setIsMetadataOpen] = useState(false); + const [isRegenerateOutlineOpen, setIsRegenerateOutlineOpen] = useState(false); + const [isRegenerateArticleOpen, setIsRegenerateArticleOpen] = useState(false); + const [contentSaveIndicator, setContentSaveIndicator] = useState< + { status: "idle" } | { status: "saving" } | { status: "saved"; at: string } + >({ status: "idle" }); + const [lastSavedContentMarkdown, setLastSavedContentMarkdown] = useState(""); + const latestSaveVersionRef = useRef(0); + const latestAppliedSaveVersionRef = useRef(0); + + const [draftDetails, setDraftDetails] = useState({ + contentMarkdown: "", + }); + + const { mutateAsync: updateDraftAsync, isPending } = useMutation( + api.content.updateDraft.mutationOptions({ + onError: (error) => { + toast.error(error.message); + }, + onSuccess: async () => { + await Promise.all([ + contentDetailsQuery.refetch(), + queryClient.invalidateQueries({ + queryKey: api.content.list.queryKey({ + input: { + organizationIdentifier, + projectId, + }, + }), + }), + ]); + }, + }), + ); + + const { mutateAsync: saveContentMarkdownAsync } = useMutation( + api.content.updateDraft.mutationOptions({ + onError: () => { + toast.error("Failed to auto-save content."); + }, + }), + ); + + useEffect(() => { + if (!draft) { + setContentSaveIndicator({ status: "idle" }); + setLastSavedContentMarkdown(""); + return; + } + setDraftDetails({ + contentMarkdown: draft.contentMarkdown ?? "", + }); + setLastSavedContentMarkdown(draft.contentMarkdown ?? ""); + setContentSaveIndicator({ + status: "saved", + at: new Date(draft.updatedAt).toISOString(), + }); + latestSaveVersionRef.current = 0; + latestAppliedSaveVersionRef.current = 0; + }, [draft]); + + useEffect(() => { + if (!draft || !canEdit) return; + if (draftDetails.contentMarkdown === lastSavedContentMarkdown) return; + + setContentSaveIndicator({ status: "saving" }); + const timeoutId = window.setTimeout(() => { + const saveVersion = ++latestSaveVersionRef.current; + const markdownToSave = draftDetails.contentMarkdown; + + void saveContentMarkdownAsync({ + organizationIdentifier, + projectId, + id: draft.id, + contentMarkdown: markdownToSave, + }) + .then(() => { + if (saveVersion < latestAppliedSaveVersionRef.current) return; + latestAppliedSaveVersionRef.current = saveVersion; + setLastSavedContentMarkdown(markdownToSave); + setContentSaveIndicator({ + status: "saved", + at: new Date().toISOString(), + }); + }) + .catch(() => { + if (saveVersion < latestAppliedSaveVersionRef.current) return; + setContentSaveIndicator({ status: "idle" }); + }); + }, 800); + + return () => window.clearTimeout(timeoutId); + }, [ + canEdit, + draft, + draftDetails.contentMarkdown, + lastSavedContentMarkdown, + organizationIdentifier, + projectId, + saveContentMarkdownAsync, + ]); + + const handleRegenerateOutline = async () => { + if (!draft) return; + try { + await updateDraftAsync({ + organizationIdentifier, + projectId, + id: draft.id, + outlineGeneratedByTaskRunId: null, + }); + setIsRegenerateOutlineOpen(false); + toast.success("Outline regeneration started"); + } catch { + // mutation error handled via onError + } + }; + + const handleRegenerateArticle = async () => { + if (!draft) return; + try { + await updateDraftAsync({ + organizationIdentifier, + projectId, + id: draft.id, + status: "queued", + generatedByTaskRunId: null, + }); + setIsRegenerateArticleOpen(false); + toast.success("Article regeneration started"); + } catch { + // mutation error handled via onError + } + }; + + return { + canEdit, + contentDetailsQuery, + contentSaveIndicator, + details, + draft, + draftDetails, + handleRegenerateArticle, + handleRegenerateOutline, + isGenerating, + isGeneratingArticle, + isGeneratingOutline, + isMetadataOpen, + isPending, + isRegenerateArticleOpen, + isRegenerateOutlineOpen, + organizationIdentifier, + projectId, + setDraftDetails, + setIsMetadataOpen, + setIsRegenerateArticleOpen, + setIsRegenerateOutlineOpen, + topKeywords, + }; +} + +export function ContentDisplay({ + controller, + headerActions, +}: { + controller: ReturnType; + headerActions?: ReactNode; +}) { + const { + canEdit, + contentDetailsQuery, + contentSaveIndicator, + details, + draft, + draftDetails, + handleRegenerateArticle, + handleRegenerateOutline, + isGenerating, + isGeneratingArticle, + isGeneratingOutline, + isMetadataOpen, + isPending, + isRegenerateArticleOpen, + isRegenerateOutlineOpen, + organizationIdentifier, + projectId, + setDraftDetails, + setIsMetadataOpen, + setIsRegenerateArticleOpen, + setIsRegenerateOutlineOpen, + topKeywords, + } = controller; + + const metricSnapshot = details?.metricSnapshot?.aggregate ?? null; + const primaryKeywordOverview = details?.primaryKeywordOverview; + const hasPrimaryKeyword = !!draft?.primaryKeyword.trim(); + const snapshotCtr = + metricSnapshot && metricSnapshot.impressions > 0 + ? metricSnapshot.clicks / metricSnapshot.impressions + : 0; + const [keywordsSortOrder, setKeywordsSortOrder] = + useState("desc"); + const [keywordsMetric, setKeywordsMetric] = + useState("clicks"); + const [keywordsPage, setKeywordsPage] = useState(1); + const [keywordsPageSize, setKeywordsPageSize] = useState(25); + const [keywordsSearchInput, setKeywordsSearchInput] = useState(""); + + return ( + <> + + + {!contentDetailsQuery.isLoading && + !contentDetailsQuery.error && + details && ( +
+
+ {isGenerating && ( + + + + {isGeneratingOutline + ? "Outline is being generated" + : "Article is being generated"} + + + Editing is disabled while generation is in progress. + + + )} + +
+
+
+ {draft?.heroImage ? ( + {draft.title + ) : ( +
+ +
+ )} +
+

+ {draft?.title || "Untitled draft"} +

+
+ + /{draft?.slug || "-"} + + + + + {draft?.primaryKeyword || "-"} + + {hasPrimaryKeyword && ( + + ) : ( +
+

+ Primary keyword stats +

+

+ No keyword data is available for this + keyword yet. +

+
+ ) + } + contentClassName="border border-border bg-popover text-popover-foreground shadow-md" + > + +
+ )} +
+
+
+
+ +
+ {headerActions} +
+
+
+ + +
+ + Overview + Top keywords + Content + + +
+ + +
+ + + + +
+ + +
+ + + { + setKeywordsPage(1); + setKeywordsPageSize(pageSize); + }} + onSearchInputChange={(value) => { + setKeywordsPage(1); + setKeywordsSearchInput(value); + }} + onSortOrderChange={(sortOrder) => { + setKeywordsPage(1); + setKeywordsSortOrder(sortOrder); + }} + page={keywordsPage} + pageSize={keywordsPageSize} + rows={topKeywords.map((keyword) => ({ + avgPosition: keyword.position, + clicks: keyword.clicks, + impressions: keyword.impressions, + keyword: keyword.keyword, + }))} + searchInput={keywordsSearchInput} + sortOrder={keywordsSortOrder} + title="Top keywords" + /> + + + + + +
+
+ + Article content + + + {contentSaveIndicator.status === "saving" && ( + <> + + Saving... + + )} + {contentSaveIndicator.status === "saved" && ( + <> + + Saved{" "} + {new Date( + contentSaveIndicator.at, + ).toLocaleTimeString()} + + )} + {contentSaveIndicator.status === "idle" && + "Autosave enabled"} + +
+ +
+
+ + {!draft?.contentMarkdown && isGeneratingArticle && ( +

+ Article generation is in progress. +

+ )} + {!draft?.contentMarkdown && !isGeneratingArticle && ( +

+ No article content yet. +

+ )} + + setDraftDetails((prev) => ({ + ...prev, + contentMarkdown: nextMarkdown, + })) + } + readOnly={!canEdit} + /> +
+
+
+
+
+ + + + + + Regenerate outline + + Kick off a fresh outline for this draft. + + + + + + + + + + + Regenerate article + + Review the current outline before starting regeneration. + + +
+
+

Outline

+ +
+ +
+ + + + +
+
+ )} + + ); +} + +function MetricCard({ label, value }: { label: string; value: string }) { + return ( +
+

{label}

+

{value}

+
+ ); +} + +function PrimaryKeywordOverview({ + overview, +}: { + overview: NonNullable< + RouterOutputs["content"]["getDraftDetails"]["primaryKeywordOverview"] + >; +}) { + const backlinkInfo = overview.backlinkInfo; + + return ( +
+

Primary keyword stats

+
+

+ Search volume:{" "} + {formatNullableNumber(overview.searchVolume.monthlyAverage)} ( + {formatNullableSignedPercent( + overview.searchVolume.percentageChange?.monthly ?? null, + )}{" "} + MoM) +

+

+ CPC:{" "} + {formatNullableCurrency(overview.competition.cpc)} +

+

+ Competition:{" "} + {formatNullableNumber(overview.competition.competition)} ( + {overview.competition.competitionLevel ?? "N/A"}) +

+

+ Keyword difficulty:{" "} + {formatNullableNumber(overview.keywordDifficulty)} +

+

+ Avg backlinks (top pages):{" "} + {formatNullableNumber(backlinkInfo?.averageBacklinkCount ?? null)} +

+

+ Avg referring domains:{" "} + {formatNullableNumber( + backlinkInfo?.averageReferringDomainCount ?? null, + )} +

+
+
+ ); +} diff --git a/apps/seo/src/routes/_authed/$organizationSlug/$projectSlug/-components/content-table.tsx b/apps/seo/src/routes/_authed/$organizationSlug/$projectSlug/-components/content-table.tsx new file mode 100644 index 000000000..23f6bdf2a --- /dev/null +++ b/apps/seo/src/routes/_authed/$organizationSlug/$projectSlug/-components/content-table.tsx @@ -0,0 +1,509 @@ +import { formatNullableNumber } from "@rectangular-labs/core/format/number"; +import { formatNullablePercent } from "@rectangular-labs/core/format/percent"; +import * as Icons from "@rectangular-labs/ui/components/icon"; +import { Button } from "@rectangular-labs/ui/components/ui/button"; +import { Checkbox } from "@rectangular-labs/ui/components/ui/checkbox"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@rectangular-labs/ui/components/ui/table"; +import { + type ColumnDef, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { useMemo, useState } from "react"; + +export type SortOrder = "asc" | "desc"; + +export type ContentTableSortBy = + | "title" + | "status" + | "strategy" + | "clicks" + | "impressions" + | "ctr" + | "avgPosition" + | "primaryKeyword"; + +export type ContentTableRow = { + id: string; + title: string | null; + slug: string; + role: string | null; + status: string; + primaryKeyword: string; + strategyId: string | null; + strategyName: string | null; + aggregate: { + clicks: number; + impressions: number; + ctr: number; + avgPosition: number; + } | null; +}; + +function compareNullableNumber(a: number | null, b: number | null): number { + if (a === null && b === null) return 0; + if (a === null) return 1; + if (b === null) return -1; + return a - b; +} + +function compareNullableString(a: string | null, b: string | null): number { + return (a ?? "").localeCompare(b ?? ""); +} + +export function ContentTable({ + rows, + selectedContentDraftId, + sortBy, + sortOrder, + onChangeSort, + onChangeSortOrder, + onOpenContentDetails, + onDownloadSelected, + isDownloadingSelected = false, + showStrategyColumn, + showRoleColumn = true, +}: { + rows: ContentTableRow[]; + selectedContentDraftId: string | null; + sortBy: ContentTableSortBy | null; + sortOrder: SortOrder; + onChangeSort: (sortBy: ContentTableSortBy | null) => void; + onChangeSortOrder: (sortOrder: SortOrder) => void; + onOpenContentDetails: (contentDraftId: string) => void; + onDownloadSelected?: (contentDraftIds: string[]) => Promise | void; + isDownloadingSelected?: boolean; + showStrategyColumn?: boolean; + showRoleColumn?: boolean; +}) { + const columnWidthClassById: Record = { + clicks: "w-[6rem]", + ctr: "w-[6rem]", + avgPosition: "w-[6rem]", + role: "w-[6rem]", + impressions: "w-[7rem]", + status: "w-[8rem]", + selected: "w-10 min-w-10 max-w-10", + primaryKeyword: "w-[12rem]", + strategy: "w-[14rem]", + title: "w-[14rem]", + }; + + const [searchInput, setSearchInput] = useState(""); + const [selectedIds, setSelectedIds] = useState>(new Set()); + + const filteredRows = useMemo(() => { + const normalizedSearch = searchInput.trim().toLowerCase(); + if (!normalizedSearch) return rows; + + return rows.filter((row) => + [ + row.title, + row.slug, + row.primaryKeyword, + row.status, + row.role ?? "", + row.strategyName ?? "", + ].some((value) => value?.toLowerCase().includes(normalizedSearch)), + ); + }, [rows, searchInput]); + + const sortedRows = useMemo(() => { + if (!sortBy) return filteredRows; + + const direction = sortOrder === "asc" ? 1 : -1; + return [...filteredRows].sort((a, b) => { + switch (sortBy) { + case "title": + return (a.title ?? "").localeCompare(b.title ?? "") * direction; + case "status": + return a.status.localeCompare(b.status) * direction; + case "strategy": + return ( + compareNullableString(a.strategyName, b.strategyName) * direction + ); + case "impressions": + return ( + compareNullableNumber( + a.aggregate?.impressions ?? null, + b.aggregate?.impressions ?? null, + ) * direction + ); + case "ctr": + return ( + compareNullableNumber( + a.aggregate?.ctr ?? null, + b.aggregate?.ctr ?? null, + ) * direction + ); + case "avgPosition": + return ( + compareNullableNumber( + a.aggregate?.avgPosition ?? null, + b.aggregate?.avgPosition ?? null, + ) * direction + ); + case "primaryKeyword": + return a.primaryKeyword.localeCompare(b.primaryKeyword) * direction; + default: + return ( + compareNullableNumber( + a.aggregate?.clicks ?? null, + b.aggregate?.clicks ?? null, + ) * direction + ); + } + }); + }, [filteredRows, sortBy, sortOrder]); + + const allVisibleSelected = + sortedRows.length > 0 && sortedRows.every((row) => selectedIds.has(row.id)); + + const hasAnyVisibleSelected = sortedRows.some((row) => + selectedIds.has(row.id), + ); + + const showStrategy = + showStrategyColumn ?? rows.some((row) => row.strategyName !== null); + + const columns = useMemo[]>(() => { + const draftColumns: ColumnDef[] = [ + { + cell: ({ row }) => ( + // biome-ignore lint/a11y/useKeyWithClickEvents: We just need onClick + + ), + enableSorting: false, + header: () => ( + + ), + id: "selected", + }, + { + accessorFn: (row) => row.title, + cell: ({ row }) => ( +
+

+ {row.original.title} +

+

+ {row.original.slug} +

+
+ ), + enableSorting: true, + header: "Title", + id: "title", + }, + { + accessorFn: (row) => row.status, + cell: ({ row }) => row.original.status, + enableSorting: true, + header: "Status", + id: "status", + }, + ]; + + if (showRoleColumn) { + draftColumns.push({ + accessorFn: (row) => row.role, + cell: ({ row }) => row.original.role ?? "", + enableSorting: false, + header: "Role", + id: "role", + }); + } + + if (showStrategy) { + draftColumns.push({ + accessorFn: (row) => row.strategyName, + cell: ({ row }) => ( + + {row.original.strategyName ?? ""} + + ), + enableSorting: true, + header: "Strategy", + id: "strategy", + }); + } + + draftColumns.push( + { + accessorFn: (row) => row.aggregate?.clicks ?? null, + cell: ({ row }) => + formatNullableNumber(row.original.aggregate?.clicks ?? null, { + fallback: "0", + }), + enableSorting: true, + header: "Clicks", + id: "clicks", + }, + { + accessorFn: (row) => row.aggregate?.impressions ?? null, + cell: ({ row }) => + formatNullableNumber(row.original.aggregate?.impressions ?? null, { + fallback: "0", + }), + enableSorting: true, + header: "Impressions", + id: "impressions", + }, + { + accessorFn: (row) => row.aggregate?.ctr ?? null, + cell: ({ row }) => + formatNullablePercent(row.original.aggregate?.ctr ?? null, { + fallback: "0.0%", + }), + enableSorting: true, + header: "CTR", + id: "ctr", + }, + { + accessorFn: (row) => row.aggregate?.avgPosition ?? null, + cell: ({ row }) => + formatNullableNumber(row.original.aggregate?.avgPosition ?? null, { + fallback: "0", + maximumFractionDigits: 1, + }), + enableSorting: true, + header: "Position", + id: "avgPosition", + }, + { + accessorFn: (row) => row.primaryKeyword, + cell: ({ row }) => ( + + {row.original.primaryKeyword} + + ), + enableSorting: true, + header: "Primary keyword", + id: "primaryKeyword", + }, + ); + + return draftColumns; + }, [ + allVisibleSelected, + hasAnyVisibleSelected, + selectedIds, + showRoleColumn, + showStrategy, + sortedRows, + ]); + + const table = useReactTable({ + columns, + data: sortedRows, + getCoreRowModel: getCoreRowModel(), + }); + + return ( +
+
+
+ + setSearchInput(event.target.value)} + placeholder="Search content..." + value={searchInput} + /> +
+ {onDownloadSelected && selectedIds.size > 0 && ( + + )} +
+ + {sortedRows.length === 0 ? ( +
+ No content found. +
+ ) : ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + if (header.isPlaceholder) { + return ; + } + + const canSort = header.column.getCanSort(); + const headerId = header.column.id; + const isSorted = sortBy === headerId; + + return ( + + {canSort ? ( + + ) : ( +
+ {flexRender( + header.column.columnDef.header, + header.getContext(), + )} +
+ )} +
+ ); + })} +
+ ))} +
+ + {table.getRowModel().rows.map((row) => ( + onOpenContentDetails(row.original.id)} + onKeyDown={(event) => { + if (event.key === "Enter" || event.key === " ") { + onOpenContentDetails(row.original.id); + } + }} + role="button" + tabIndex={0} + > + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + ))} + +
+
+ )} +
+ ); +} diff --git a/apps/seo/src/routes/_authed/$organizationSlug/$projectSlug/-components/manage-content-metadata-dialog.tsx b/apps/seo/src/routes/_authed/$organizationSlug/$projectSlug/-components/manage-content-metadata-dialog.tsx new file mode 100644 index 000000000..677229832 --- /dev/null +++ b/apps/seo/src/routes/_authed/$organizationSlug/$projectSlug/-components/manage-content-metadata-dialog.tsx @@ -0,0 +1,329 @@ +"use client"; + +import type { RouterOutputs } from "@rectangular-labs/api-seo/types"; +import { Button } from "@rectangular-labs/ui/components/ui/button"; +import { + DialogDrawer, + DialogDrawerDescription, + DialogDrawerFooter, + DialogDrawerHeader, + DialogDrawerTitle, +} from "@rectangular-labs/ui/components/ui/dialog-drawer"; +import { + arktypeResolver, + Controller, + Field, + FieldDescription, + FieldError, + FieldGroup, + FieldLabel, + useForm, +} from "@rectangular-labs/ui/components/ui/field"; +import { Input } from "@rectangular-labs/ui/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@rectangular-labs/ui/components/ui/select"; +import { toast } from "@rectangular-labs/ui/components/ui/sonner"; +import { Textarea } from "@rectangular-labs/ui/components/ui/textarea"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { type } from "arktype"; +import { useEffect, useMemo } from "react"; +import { getApiClientRq } from "~/lib/api"; + +const formSchema = type({ + title: "string", + description: "string", + slug: type("string") + .atLeastLength(1) + .configure({ message: () => "Slug is required." }), + primaryKeyword: type("string") + .atLeastLength(1) + .configure({ message: () => "Primary keyword is required." }), + heroImage: "string", + heroImageCaption: "string", + contentMarkdown: "string", + strategyId: "string", +}); + +export function ManageContentMetadataDialog({ + canEdit, + draftDetails, + draftId, + onOpenChange, + open, + organizationIdentifier, + projectId, +}: { + canEdit: boolean; + draftDetails: + | RouterOutputs["content"]["getDraftDetails"]["contentDraft"] + | null; + draftId: string | null; + onOpenChange: (open: boolean) => void; + open: boolean; + organizationIdentifier: string; + projectId: string; +}) { + const api = getApiClientRq(); + const queryClient = useQueryClient(); + const isCreate = !draftId || !draftDetails; + const strategyListQuery = useQuery( + api.strategy.list.queryOptions({ + input: { + organizationIdentifier, + projectId, + }, + enabled: open, + staleTime: 1000 * 60 * 5, + gcTime: 1000 * 60 * 30, + }), + ); + const strategies = strategyListQuery.data?.strategies ?? []; + + const defaultValues = useMemo( + () => ({ + title: draftDetails?.title ?? "", + description: draftDetails?.description ?? "", + slug: draftDetails?.slug ?? "", + primaryKeyword: draftDetails?.primaryKeyword ?? "", + heroImage: draftDetails?.heroImage ?? "", + heroImageCaption: draftDetails?.heroImageCaption ?? "", + contentMarkdown: draftDetails?.contentMarkdown ?? "", + strategyId: draftDetails?.strategyId ?? "", + }), + [draftDetails], + ); + + const form = useForm({ + resolver: arktypeResolver(formSchema), + defaultValues: defaultValues, + }); + + useEffect(() => { + if (open) { + form.reset(defaultValues); + } + }, [defaultValues, form, open]); + + const { mutate: updateDraft, isPending } = useMutation( + api.content.updateDraft.mutationOptions({ + onError: (error) => { + form.setError("root", { message: error.message }); + }, + onSuccess: async () => { + toast.success("Metadata updated"); + onOpenChange(false); + await Promise.all([ + queryClient.invalidateQueries({ + queryKey: api.content.list.queryKey({ + input: { + organizationIdentifier, + projectId, + }, + }), + }), + draftId + ? queryClient.invalidateQueries({ + queryKey: api.content.getDraftDetails.queryKey({ + input: { + organizationIdentifier, + projectId, + id: draftId, + months: 3, + }, + }), + }) + : Promise.resolve(), + ]); + }, + }), + ); + + const slug = form.watch("slug"); + const primaryKeyword = form.watch("primaryKeyword"); + const hasRequiredMetadata = + slug.trim().length > 0 && primaryKeyword.trim().length > 0; + + const submitForm = (values: typeof formSchema.infer) => { + if (!draftId || isCreate) { + toast.error("Draft creation from this dialog is not available yet."); + return; + } + + updateDraft({ + organizationIdentifier, + projectId, + id: draftId, + title: values.title.trim(), + description: values.description.trim(), + slug: values.slug.trim(), + primaryKeyword: values.primaryKeyword.trim(), + heroImage: values.heroImage.trim() || null, + heroImageCaption: values.heroImageCaption.trim() || null, + strategyId: values.strategyId || null, + }); + }; + + const title = isCreate ? "Create content draft" : "Edit draft metadata"; + const description = isCreate + ? "Set content metadata and create a new draft." + : "Update title, URL, keyword, and strategy assignment."; + const submitLabel = isCreate ? "Create content" : "Save metadata"; + + return ( + + + {title} + {description} + + +
+ + ( + + Assigned strategy + + + )} + /> + + ( + + Title + + Optional. Leave blank and we will generate an optimized title + from SERPs and the primary keyword. + + + + )} + /> + + ( + + Slug + + {fieldState.invalid && ( + + )} + + )} + /> + + ( + + Primary keyword + + {fieldState.invalid && ( + + )} + + )} + /> + + ( + + Hero image URL + + + )} + /> + + ( + + Hero caption + + + )} + /> + + ( + + Meta description + + Optional. Leave blank and we will generate an optimized + description from SERPs and the primary keyword. + +