Guidelines for agentic coding agents operating in the Sink codebase.
Sink is a link shortener with analytics, running 100% on Cloudflare. Uses Nuxt 4 frontend and Cloudflare Workers backend.
All documentation and comments must be in English.
app/ # Nuxt 4 application
├── components/ # Vue components (PascalCase)
│ ├── ui/ # shadcn-vue components (DO NOT EDIT)
│ ├── dashboard/ # Dashboard components
│ └── home/ # Landing page components
├── composables/ # Vue composables (camelCase)
├── pages/ # File-based routing
├── stores/ # Pinia stores
├── types/ # TypeScript types
├── utils/ # Utility functions
└── lib/ # Shared helpers
server/ # Nitro server (Cloudflare Workers)
├── api/ # API endpoints
└── utils/ # Server utilities
schemas/ # Zod validation schemas
tests/ # Vitest tests
Use pnpm (v10+) with Node.js 22+.
pnpm dev # Start dev server (port 7465)
pnpm build # Production build
pnpm preview # Worker preview via wrangler
pnpm lint:fix # ESLint with auto-fix
pnpm types:check # TypeScript type check
# Testing (Vitest + Cloudflare Workers pool)
pnpm vitest # Watch mode
pnpm vitest run # CI mode (run once)
pnpm vitest tests/sink.spec.ts # Single file
pnpm vitest tests/api/link.spec.ts # Single API test
pnpm vitest -t "returns 200" # Pattern match
# Deployment
pnpm deploy:pages # Deploy to Cloudflare Pages
pnpm deploy:worker # Deploy to Cloudflare WorkersUses @antfu/eslint-config. Run pnpm lint:fix before committing.
- Indentation: 2 spaces | Quotes: Single | Semicolons: None | Trailing commas: Always
- Use TypeScript for all code; prefer
interfacefor objects,typefor unions - Avoid
any; use proper types orunknown - Use Zod for runtime validation (see
schemas/)
interface Link { id: string, url: string, slug: string }
export const LinkSchema = z.object({
id: z.string().trim().max(26),
url: z.string().trim().url().max(2048),
slug: z.string().trim().max(2048),
})Use <script setup lang="ts"> always. Files: PascalCase (LinkEditor.vue).
<script setup lang="ts">
import type { Link } from '@/types'
const props = defineProps<{ link: Link }>()
const emit = defineEmits<{ update: [link: Link] }>()
</script>
<template>
<div>{{ props.link.slug }}</div>
</template>- Prefer Nuxt auto-imports (
ref,computed,useFetch, etc.) - Explicit imports: external libs (
import { z } from 'zod'), types (import type { Link } from '@/types'), icons (import { Copy } from 'lucide-vue-next') - Path aliases:
@/(app),@@/(root)
| Item | Convention | Example |
|---|---|---|
| Components | PascalCase | LinkEditor.vue |
| Composables | use prefix |
useDashboardRoute() |
| Stores | use...Store |
useDashboardLinksStore |
| API routes | method suffix | create.post.ts |
| Directories | kebab-case | dashboard/links/ |
| Functions/vars | camelCase | getLink |
| Constants | UPPER_SNAKE_CASE | DASHBOARD_ROUTES |
// Server API - use createError for HTTP errors
export default eventHandler(async (event) => {
const link = await readValidatedBody(event, LinkSchema.parse)
if (existingLink) {
throw createError({ status: 409, statusText: 'Link already exists' })
}
})Access via event.context.cloudflare.env:
const { KV, ANALYTICS, AI, R2 } = event.context.cloudflare.env| Binding | Type | Purpose |
|---|---|---|
KV |
Workers KV | Link storage |
ANALYTICS |
Analytics Engine | Click tracking & analytics |
AI |
Workers AI | AI-powered slug generation |
R2 |
R2 Bucket | Data backup & file storage |
- Use shadcn-vue from
app/components/ui/— Never edit (auto-generated) - Use
ResponsiveModalfor mobile-optimized dialogs - Use Tailwind CSS v4 for styling
Use static English for aria-label (no $t() translations):
<button aria-label="Open menu">
...
</button> <!-- Good -->
<button :aria-label="$t('menu.open')">
...
</button> <!-- Bad -->Follow Conventional Commits: feat:, fix:, docs:, chore:, refactor:
feat: add link expiration
fix: correct analytics filter
simple-git-hooks runs lint-staged on commit, auto-runs eslint --fix on staged files.