@kennethsolomon/shipkit 3.13.2 → 3.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +7 -6
  2. package/commands/sk/brainstorm.md +13 -0
  3. package/commands/sk/execute-plan.md +1 -0
  4. package/commands/sk/security-check.md +4 -0
  5. package/commands/sk/website.md +93 -0
  6. package/commands/sk/write-plan.md +38 -0
  7. package/package.json +1 -1
  8. package/skills/sk:autopilot/SKILL.md +0 -1
  9. package/skills/sk:fast-track/SKILL.md +0 -1
  10. package/skills/sk:gates/SKILL.md +4 -1
  11. package/skills/sk:retro/SKILL.md +0 -1
  12. package/skills/sk:reverse-doc/SKILL.md +0 -1
  13. package/skills/sk:review/SKILL.md +24 -6
  14. package/skills/sk:scope-check/SKILL.md +0 -1
  15. package/skills/sk:setup-claude/templates/commands/brainstorm.md.template +13 -0
  16. package/skills/sk:setup-claude/templates/commands/execute-plan.md.template +1 -0
  17. package/skills/sk:setup-claude/templates/commands/security-check.md.template +3 -0
  18. package/skills/sk:setup-claude/templates/commands/write-plan.md.template +37 -0
  19. package/skills/sk:start/SKILL.md +0 -1
  20. package/skills/sk:team/SKILL.md +0 -1
  21. package/skills/sk:website/SKILL.md +471 -0
  22. package/skills/sk:website/references/art-direction.md +210 -0
  23. package/skills/sk:website/references/brief-template.md +121 -0
  24. package/skills/sk:website/references/content-seo.md +143 -0
  25. package/skills/sk:website/references/handoff-template.md +261 -0
  26. package/skills/sk:website/references/launch-checklist.md +99 -0
  27. package/skills/sk:website/references/niche/accountant.md +75 -0
  28. package/skills/sk:website/references/niche/agency.md +75 -0
  29. package/skills/sk:website/references/niche/cafe.md +79 -0
  30. package/skills/sk:website/references/niche/dentist.md +78 -0
  31. package/skills/sk:website/references/niche/ecommerce.md +76 -0
  32. package/skills/sk:website/references/niche/gym.md +75 -0
  33. package/skills/sk:website/references/niche/home-services.md +76 -0
  34. package/skills/sk:website/references/niche/law-firm.md +75 -0
  35. package/skills/sk:website/references/niche/local-business.md +78 -0
  36. package/skills/sk:website/references/niche/med-spa.md +78 -0
  37. package/skills/sk:website/references/niche/portfolio.md +77 -0
  38. package/skills/sk:website/references/niche/real-estate.md +72 -0
  39. package/skills/sk:website/references/niche/restaurant.md +80 -0
  40. package/skills/sk:website/references/niche/saas.md +80 -0
  41. package/skills/sk:website/references/niche/wedding.md +80 -0
  42. package/skills/sk:website/references/stacks/laravel.md +425 -0
  43. package/skills/sk:website/references/stacks/nextjs.md +345 -0
  44. package/skills/sk:website/references/stacks/nuxt.md +374 -0
  45. package/skills/sk:website/references/whatsapp-cta.md +160 -0
@@ -0,0 +1,345 @@
1
+ # Next.js + Tailwind — Client Website Stack Reference
2
+
3
+ Stack for building multi-page client marketing sites. NOT a prototype — real copy, real SEO, no fake data.
4
+
5
+ ## Scaffold
6
+
7
+ ```bash
8
+ npx create-next-app@latest {project-name} --typescript --tailwind --eslint --app --src-dir --no-import-alias
9
+ cd {project-name} && npm install
10
+ ```
11
+
12
+ ## Directory Structure
13
+
14
+ ```
15
+ {project-name}/
16
+ ├── src/
17
+ │ ├── app/
18
+ │ │ ├── layout.tsx ← root layout (fonts, WhatsApp CTA, global nav)
19
+ │ │ ├── page.tsx ← Home page
20
+ │ │ ├── globals.css ← Tailwind directives + CSS custom properties
21
+ │ │ ├── sitemap.ts ← auto-generated sitemap.xml
22
+ │ │ ├── robots.ts ← robots.txt
23
+ │ │ ├── about/
24
+ │ │ │ └── page.tsx ← About page
25
+ │ │ ├── services/
26
+ │ │ │ └── page.tsx ← Services / Menu page
27
+ │ │ ├── contact/
28
+ │ │ │ └── page.tsx ← Contact page
29
+ │ │ ├── [additional-pages]/
30
+ │ │ │ └── page.tsx
31
+ │ │ └── api/
32
+ │ │ └── contact/
33
+ │ │ └── route.ts ← contact form handler
34
+ │ └── components/
35
+ │ ├── layout/
36
+ │ │ ├── Navbar.tsx
37
+ │ │ └── Footer.tsx
38
+ │ ├── home/
39
+ │ │ ├── Hero.tsx
40
+ │ │ ├── Services.tsx
41
+ │ │ ├── About.tsx ← brief About preview on Home
42
+ │ │ └── Testimonials.tsx
43
+ │ ├── contact/
44
+ │ │ └── ContactForm.tsx ← "use client" — form with validation
45
+ │ ├── WhatsAppButton.tsx ← floating CTA
46
+ │ └── MessengerButton.tsx ← alternative CTA (Philippines)
47
+ ├── content/
48
+ │ └── site.ts ← typed site config: copy, pages, metadata
49
+ ├── public/
50
+ │ ├── images/
51
+ │ └── favicon.ico
52
+ ├── tailwind.config.ts
53
+ ├── next.config.ts
54
+ └── package.json
55
+ ```
56
+
57
+ ## Site Config Pattern
58
+
59
+ `content/site.ts` — single source of truth for all copy and metadata:
60
+
61
+ ```ts
62
+ export const site = {
63
+ name: '{Business Name}',
64
+ tagline: '{Tagline}',
65
+ description: '{Meta description — used for SEO}',
66
+ url: 'https://{domain}',
67
+ phone: '{639171234567}', // E.164 without +
68
+ email: '{contact@example.com}',
69
+ address: '{Full address}',
70
+ hours: '{Mon–Fri 9am–6pm}',
71
+ social: {
72
+ facebook: '{https://facebook.com/page}',
73
+ instagram: '{https://instagram.com/handle}',
74
+ },
75
+ pages: {
76
+ home: {
77
+ title: '{Business Name} — {Primary benefit}',
78
+ description: '{Page-specific meta description}',
79
+ hero: {
80
+ headline: '{Real headline — no Lorem ipsum}',
81
+ subheadline: '{Supporting line}',
82
+ cta: '{Primary CTA text}',
83
+ ctaHref: '/contact',
84
+ },
85
+ },
86
+ about: {
87
+ title: 'About — {Business Name}',
88
+ description: '{About page meta description}',
89
+ },
90
+ services: {
91
+ title: 'Services — {Business Name}',
92
+ description: '{Services page meta description}',
93
+ items: [
94
+ { name: '{Service 1}', description: '{Real description}', price: '{optional}' },
95
+ ],
96
+ },
97
+ contact: {
98
+ title: 'Contact — {Business Name}',
99
+ description: '{Contact page meta description}',
100
+ },
101
+ },
102
+ }
103
+ ```
104
+
105
+ ## Root Layout
106
+
107
+ `src/app/layout.tsx`:
108
+
109
+ ```tsx
110
+ import type { Metadata } from 'next'
111
+ import { {DisplayFont}, {BodyFont} } from 'next/font/google'
112
+ import './globals.css'
113
+ import { Navbar } from '@/components/layout/Navbar'
114
+ import { Footer } from '@/components/layout/Footer'
115
+ import { WhatsAppButton } from '@/components/WhatsAppButton'
116
+ import { site } from '@/content/site'
117
+
118
+ const display = {DisplayFont}({ subsets: ['latin'], variable: '--font-display' })
119
+ const body = {BodyFont}({ subsets: ['latin'], variable: '--font-body' })
120
+
121
+ export const metadata: Metadata = {
122
+ metadataBase: new URL(site.url),
123
+ title: { default: site.name, template: `%s — ${site.name}` },
124
+ description: site.description,
125
+ }
126
+
127
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
128
+ return (
129
+ <html lang="en" className={`${display.variable} ${body.variable}`}>
130
+ <body className="font-body antialiased bg-bg text-fg">
131
+ <Navbar />
132
+ <main>{children}</main>
133
+ <Footer />
134
+ {/* Remove WhatsAppButton if not a local PH/SEA business */}
135
+ <WhatsAppButton phone={site.phone} message="Hi! I found you on your website." />
136
+ </body>
137
+ </html>
138
+ )
139
+ }
140
+ ```
141
+
142
+ ## Per-Page SEO Metadata
143
+
144
+ Each page exports a `generateMetadata` function or a `metadata` object:
145
+
146
+ ```tsx
147
+ // src/app/about/page.tsx
148
+ import type { Metadata } from 'next'
149
+ import { site } from '@/content/site'
150
+
151
+ export const metadata: Metadata = {
152
+ title: site.pages.about.title,
153
+ description: site.pages.about.description,
154
+ openGraph: {
155
+ title: site.pages.about.title,
156
+ description: site.pages.about.description,
157
+ url: `${site.url}/about`,
158
+ siteName: site.name,
159
+ type: 'website',
160
+ },
161
+ }
162
+
163
+ export default function AboutPage() {
164
+ return (
165
+ <div>
166
+ <h1>About {site.name}</h1>
167
+ {/* real content */}
168
+ </div>
169
+ )
170
+ }
171
+ ```
172
+
173
+ ## Tailwind Config
174
+
175
+ `tailwind.config.ts`:
176
+
177
+ ```ts
178
+ export default {
179
+ theme: {
180
+ extend: {
181
+ colors: {
182
+ bg: 'var(--color-bg)',
183
+ fg: 'var(--color-fg)',
184
+ accent: 'var(--color-accent)',
185
+ muted: 'var(--color-muted)',
186
+ surface: 'var(--color-surface)',
187
+ },
188
+ fontFamily: {
189
+ display: ['var(--font-display)', 'serif'],
190
+ body: ['var(--font-body)', 'sans-serif'],
191
+ },
192
+ },
193
+ },
194
+ }
195
+ ```
196
+
197
+ `src/app/globals.css`:
198
+
199
+ ```css
200
+ @tailwind base;
201
+ @tailwind components;
202
+ @tailwind utilities;
203
+
204
+ :root {
205
+ --color-bg: #xxxxxx; /* from art direction spec */
206
+ --color-fg: #xxxxxx;
207
+ --color-accent: #xxxxxx;
208
+ --color-muted: #xxxxxx;
209
+ --color-surface: #xxxxxx;
210
+ }
211
+ ```
212
+
213
+ ## Contact Form API Route
214
+
215
+ `src/app/api/contact/route.ts`:
216
+
217
+ ```ts
218
+ import { NextResponse } from 'next/server'
219
+
220
+ export async function POST(request: Request) {
221
+ const body = await request.json()
222
+ const { name, email, phone, message } = body
223
+
224
+ if (!name || !email || !message) {
225
+ return NextResponse.json({ success: false, message: 'Name, email, and message are required.' }, { status: 400 })
226
+ }
227
+
228
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
229
+ return NextResponse.json({ success: false, message: 'Invalid email address.' }, { status: 400 })
230
+ }
231
+
232
+ // TODO: wire to email service (Resend, Nodemailer, SendGrid)
233
+ // For now: log to console and return success
234
+ console.log('Contact form submission:', { name, email, phone, message, timestamp: new Date().toISOString() })
235
+
236
+ return NextResponse.json({ success: true, message: "Message received. We'll be in touch soon." })
237
+ }
238
+ ```
239
+
240
+ ## Sitemap + Robots
241
+
242
+ `src/app/sitemap.ts`:
243
+
244
+ ```ts
245
+ import { MetadataRoute } from 'next'
246
+ import { site } from '@/content/site'
247
+
248
+ export default function sitemap(): MetadataRoute.Sitemap {
249
+ return [
250
+ { url: site.url, lastModified: new Date(), changeFrequency: 'monthly', priority: 1 },
251
+ { url: `${site.url}/about`, lastModified: new Date(), changeFrequency: 'monthly', priority: 0.8 },
252
+ { url: `${site.url}/services`, lastModified: new Date(), changeFrequency: 'monthly', priority: 0.8 },
253
+ { url: `${site.url}/contact`, lastModified: new Date(), changeFrequency: 'yearly', priority: 0.5 },
254
+ ]
255
+ }
256
+ ```
257
+
258
+ `src/app/robots.ts`:
259
+
260
+ ```ts
261
+ import { MetadataRoute } from 'next'
262
+ import { site } from '@/content/site'
263
+
264
+ export default function robots(): MetadataRoute.Robots {
265
+ return {
266
+ rules: { userAgent: '*', allow: '/' },
267
+ sitemap: `${site.url}/sitemap.xml`,
268
+ }
269
+ }
270
+ ```
271
+
272
+ ## WhatsApp Component
273
+
274
+ `src/components/WhatsAppButton.tsx` — see `references/whatsapp-cta.md` for full implementation.
275
+
276
+ ```tsx
277
+ 'use client'
278
+
279
+ interface WhatsAppButtonProps {
280
+ phone: string // E.164 without +: e.g., "639171234567"
281
+ message?: string
282
+ }
283
+
284
+ export function WhatsAppButton({ phone, message }: WhatsAppButtonProps) {
285
+ const url = message
286
+ ? `https://wa.me/${phone}?text=${encodeURIComponent(message)}`
287
+ : `https://wa.me/${phone}`
288
+
289
+ return (
290
+ <a
291
+ href={url}
292
+ target="_blank"
293
+ rel="noopener noreferrer"
294
+ aria-label="Chat on WhatsApp"
295
+ className="fixed bottom-6 right-6 z-50 flex h-14 w-14 items-center justify-center rounded-full bg-[#25D366] shadow-lg transition-transform hover:scale-110"
296
+ >
297
+ {/* SVG icon — see whatsapp-cta.md */}
298
+ </a>
299
+ )
300
+ }
301
+ ```
302
+
303
+ ## Structured Data
304
+
305
+ Add to each page's `<head>` via Next.js metadata or a script tag:
306
+
307
+ ```tsx
308
+ // For local businesses — in layout.tsx or per-page
309
+ const structuredData = {
310
+ '@context': 'https://schema.org',
311
+ '@type': 'LocalBusiness', // or Restaurant, Dentist, LawFirm, etc.
312
+ name: site.name,
313
+ description: site.description,
314
+ url: site.url,
315
+ telephone: `+${site.phone}`,
316
+ address: {
317
+ '@type': 'PostalAddress',
318
+ streetAddress: site.address,
319
+ },
320
+ }
321
+
322
+ // In page component:
323
+ <script
324
+ type="application/ld+json"
325
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
326
+ />
327
+ ```
328
+
329
+ ## Dev + Build Commands
330
+
331
+ ```bash
332
+ npm run dev # http://localhost:3000
333
+ npm run build # production build — must pass before handoff
334
+ npm run start # preview production build locally
335
+ npm run lint # ESLint check
336
+ ```
337
+
338
+ ## Vercel Deploy
339
+
340
+ ```bash
341
+ npm install -g vercel
342
+ vercel --prod # deploy to production
343
+ ```
344
+
345
+ Or: connect GitHub repo to Vercel dashboard for automatic deployments on push.
@@ -0,0 +1,374 @@
1
+ # Nuxt 3 + Tailwind — Client Website Stack Reference
2
+
3
+ Stack for building multi-page client marketing sites with Vue 3. NOT a prototype — real copy, real SEO, no fake data.
4
+
5
+ ## Scaffold
6
+
7
+ ```bash
8
+ npx nuxi@latest init {project-name}
9
+ cd {project-name}
10
+ npm install
11
+ npx nuxi module add @nuxtjs/tailwindcss
12
+ npx nuxi module add @nuxtjs/google-fonts
13
+ npx nuxi module add @nuxtjs/sitemap
14
+ ```
15
+
16
+ ## Directory Structure
17
+
18
+ ```
19
+ {project-name}/
20
+ ├── pages/
21
+ │ ├── index.vue ← Home page
22
+ │ ├── about.vue ← About page
23
+ │ ├── services.vue ← Services / Menu page
24
+ │ ├── contact.vue ← Contact page
25
+ │ └── [additional].vue ← niche-specific pages
26
+ ├── components/
27
+ │ ├── layout/
28
+ │ │ ├── TheNavbar.vue
29
+ │ │ └── TheFooter.vue
30
+ │ ├── home/
31
+ │ │ ├── HomeHero.vue
32
+ │ │ ├── HomeServices.vue
33
+ │ │ └── HomeTestimonials.vue
34
+ │ ├── contact/
35
+ │ │ └── ContactForm.vue ← reactive form with validation
36
+ │ ├── WhatsAppButton.vue ← floating CTA
37
+ │ └── MessengerButton.vue ← alternative CTA (Philippines)
38
+ ├── layouts/
39
+ │ └── default.vue ← site layout (Navbar + Footer + WhatsApp CTA)
40
+ ├── server/
41
+ │ └── api/
42
+ │ └── contact.post.ts ← contact form handler
43
+ ├── content/
44
+ │ └── site.ts ← typed site config: copy, pages, metadata
45
+ ├── assets/
46
+ │ └── css/
47
+ │ └── main.css ← Tailwind directives + CSS custom properties
48
+ ├── public/
49
+ │ ├── images/
50
+ │ └── favicon.ico
51
+ ├── tailwind.config.ts
52
+ └── nuxt.config.ts
53
+ ```
54
+
55
+ ## Site Config
56
+
57
+ `content/site.ts`:
58
+
59
+ ```ts
60
+ export const site = {
61
+ name: '{Business Name}',
62
+ tagline: '{Tagline}',
63
+ description: '{Meta description — used for SEO}',
64
+ url: 'https://{domain}',
65
+ phone: '{639171234567}', // E.164 without +
66
+ email: '{contact@example.com}',
67
+ address: '{Full address}',
68
+ hours: '{Mon–Fri 9am–6pm}',
69
+ social: {
70
+ facebook: '{https://facebook.com/page}',
71
+ instagram: '{https://instagram.com/handle}',
72
+ },
73
+ pages: {
74
+ home: {
75
+ title: '{Business Name} — {Primary benefit}',
76
+ description: '{Page-specific meta description}',
77
+ hero: {
78
+ headline: '{Real headline — no Lorem ipsum}',
79
+ subheadline: '{Supporting line}',
80
+ cta: '{Primary CTA text}',
81
+ ctaHref: '/contact',
82
+ },
83
+ },
84
+ about: {
85
+ title: 'About — {Business Name}',
86
+ description: '{About page meta description}',
87
+ },
88
+ services: {
89
+ title: 'Services — {Business Name}',
90
+ description: '{Services page meta description}',
91
+ items: [
92
+ { name: '{Service 1}', description: '{Real description}', price: '{optional}' },
93
+ ],
94
+ },
95
+ contact: {
96
+ title: 'Contact — {Business Name}',
97
+ description: '{Contact page meta description}',
98
+ },
99
+ },
100
+ }
101
+ ```
102
+
103
+ ## Nuxt Config
104
+
105
+ `nuxt.config.ts`:
106
+
107
+ ```ts
108
+ import { site } from './content/site'
109
+
110
+ export default defineNuxtConfig({
111
+ modules: [
112
+ '@nuxtjs/tailwindcss',
113
+ '@nuxtjs/google-fonts',
114
+ '@nuxtjs/sitemap',
115
+ ],
116
+ googleFonts: {
117
+ families: {
118
+ '{DisplayFont}': [400, 600, 700, 800],
119
+ '{BodyFont}': [400, 500, 600],
120
+ },
121
+ },
122
+ css: ['~/assets/css/main.css'],
123
+ app: {
124
+ head: {
125
+ htmlAttrs: { lang: 'en' },
126
+ meta: [
127
+ { name: 'description', content: site.description },
128
+ { property: 'og:site_name', content: site.name },
129
+ ],
130
+ link: [
131
+ { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
132
+ ],
133
+ },
134
+ },
135
+ sitemap: {
136
+ hostname: site.url,
137
+ },
138
+ nitro: {
139
+ preset: 'vercel', // or 'netlify', 'node-server'
140
+ },
141
+ })
142
+ ```
143
+
144
+ ## Tailwind Config
145
+
146
+ `tailwind.config.ts`:
147
+
148
+ ```ts
149
+ export default {
150
+ theme: {
151
+ extend: {
152
+ colors: {
153
+ bg: 'var(--color-bg)',
154
+ fg: 'var(--color-fg)',
155
+ accent: 'var(--color-accent)',
156
+ muted: 'var(--color-muted)',
157
+ surface: 'var(--color-surface)',
158
+ },
159
+ fontFamily: {
160
+ display: ['{DisplayFont}', 'serif'],
161
+ body: ['{BodyFont}', 'sans-serif'],
162
+ },
163
+ },
164
+ },
165
+ }
166
+ ```
167
+
168
+ `assets/css/main.css`:
169
+
170
+ ```css
171
+ @tailwind base;
172
+ @tailwind components;
173
+ @tailwind utilities;
174
+
175
+ :root {
176
+ --color-bg: #xxxxxx; /* from art direction spec */
177
+ --color-fg: #xxxxxx;
178
+ --color-accent: #xxxxxx;
179
+ --color-muted: #xxxxxx;
180
+ --color-surface: #xxxxxx;
181
+ }
182
+
183
+ body {
184
+ font-family: '{BodyFont}', sans-serif;
185
+ }
186
+ ```
187
+
188
+ ## Default Layout
189
+
190
+ `layouts/default.vue`:
191
+
192
+ ```vue
193
+ <template>
194
+ <div class="min-h-screen bg-bg text-fg font-body antialiased">
195
+ <LayoutTheNavbar />
196
+ <main>
197
+ <slot />
198
+ </main>
199
+ <LayoutTheFooter />
200
+ <!-- Remove if not a local PH/SEA business -->
201
+ <WhatsAppButton :phone="site.phone" message="Hi! I found you on your website." />
202
+ </div>
203
+ </template>
204
+
205
+ <script setup lang="ts">
206
+ import { site } from '~/content/site'
207
+ </script>
208
+ ```
209
+
210
+ ## Per-Page SEO
211
+
212
+ Use `useSeoMeta` (recommended) or `useHead` in each page:
213
+
214
+ ```vue
215
+ <!-- pages/about.vue -->
216
+ <script setup lang="ts">
217
+ import { site } from '~/content/site'
218
+
219
+ useSeoMeta({
220
+ title: site.pages.about.title,
221
+ description: site.pages.about.description,
222
+ ogTitle: site.pages.about.title,
223
+ ogDescription: site.pages.about.description,
224
+ ogUrl: `${site.url}/about`,
225
+ ogSiteName: site.name,
226
+ ogType: 'website',
227
+ })
228
+ </script>
229
+
230
+ <template>
231
+ <div>
232
+ <h1>About {{ site.name }}</h1>
233
+ <!-- real content -->
234
+ </div>
235
+ </template>
236
+ ```
237
+
238
+ ## Contact Form API
239
+
240
+ `server/api/contact.post.ts`:
241
+
242
+ ```ts
243
+ export default defineEventHandler(async (event) => {
244
+ const { name, email, phone, message } = await readBody(event)
245
+
246
+ if (!name || !email || !message) {
247
+ throw createError({ statusCode: 400, message: 'Name, email, and message are required.' })
248
+ }
249
+
250
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
251
+ throw createError({ statusCode: 400, message: 'Invalid email address.' })
252
+ }
253
+
254
+ // TODO: wire to email service (Resend, Nodemailer, etc.)
255
+ console.log('Contact submission:', { name, email, phone, message, timestamp: new Date().toISOString() })
256
+
257
+ return { success: true, message: "Message received. We'll be in touch soon." }
258
+ })
259
+ ```
260
+
261
+ ## Contact Form Component
262
+
263
+ `components/contact/ContactForm.vue`:
264
+
265
+ ```vue
266
+ <script setup lang="ts">
267
+ const form = reactive({ name: '', email: '', phone: '', message: '' })
268
+ const status = ref<'idle' | 'loading' | 'success' | 'error'>('idle')
269
+ const feedback = ref('')
270
+
271
+ async function submit() {
272
+ status.value = 'loading'
273
+ try {
274
+ const res = await $fetch('/api/contact', { method: 'POST', body: form })
275
+ status.value = 'success'
276
+ feedback.value = (res as any).message
277
+ } catch (err: any) {
278
+ status.value = 'error'
279
+ feedback.value = err.data?.message || 'Something went wrong. Please try again.'
280
+ }
281
+ }
282
+ </script>
283
+
284
+ <template>
285
+ <form @submit.prevent="submit" class="space-y-4">
286
+ <input v-model="form.name" type="text" placeholder="Your name" required class="w-full px-4 py-3 border rounded-lg" />
287
+ <input v-model="form.email" type="email" placeholder="Email address" required class="w-full px-4 py-3 border rounded-lg" />
288
+ <input v-model="form.phone" type="tel" placeholder="Phone (optional)" class="w-full px-4 py-3 border rounded-lg" />
289
+ <textarea v-model="form.message" placeholder="Your message" required rows="4" class="w-full px-4 py-3 border rounded-lg resize-none"></textarea>
290
+ <button type="submit" :disabled="status === 'loading'" class="w-full px-6 py-3 bg-accent text-white rounded-lg font-medium transition hover:opacity-90">
291
+ {{ status === 'loading' ? 'Sending...' : 'Send Message' }}
292
+ </button>
293
+ <p v-if="status === 'success'" class="text-green-600 font-medium">{{ feedback }}</p>
294
+ <p v-if="status === 'error'" class="text-red-500 text-sm">{{ feedback }}</p>
295
+ </form>
296
+ </template>
297
+ ```
298
+
299
+ ## WhatsApp Component
300
+
301
+ `components/WhatsAppButton.vue`:
302
+
303
+ ```vue
304
+ <script setup lang="ts">
305
+ const props = defineProps<{
306
+ phone: string // E.164 without +: e.g., "639171234567"
307
+ message?: string
308
+ }>()
309
+
310
+ const url = computed(() =>
311
+ props.message
312
+ ? `https://wa.me/${props.phone}?text=${encodeURIComponent(props.message)}`
313
+ : `https://wa.me/${props.phone}`
314
+ )
315
+ </script>
316
+
317
+ <template>
318
+ <a
319
+ :href="url"
320
+ target="_blank"
321
+ rel="noopener noreferrer"
322
+ aria-label="Chat on WhatsApp"
323
+ class="fixed bottom-6 right-6 z-50 flex h-14 w-14 items-center justify-center rounded-full bg-[#25D366] shadow-lg transition-transform hover:scale-110 focus:outline-none focus:ring-2 focus:ring-[#25D366] focus:ring-offset-2"
324
+ >
325
+ <!-- WhatsApp SVG icon — see whatsapp-cta.md for full SVG path -->
326
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" class="h-7 w-7" aria-hidden="true">
327
+ <path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z"/>
328
+ </svg>
329
+ </a>
330
+ </template>
331
+ ```
332
+
333
+ ## Structured Data
334
+
335
+ In each page or layout, add LocalBusiness structured data:
336
+
337
+ ```vue
338
+ <script setup lang="ts">
339
+ import { site } from '~/content/site'
340
+
341
+ useHead({
342
+ script: [{
343
+ type: 'application/ld+json',
344
+ children: JSON.stringify({
345
+ '@context': 'https://schema.org',
346
+ '@type': 'LocalBusiness',
347
+ name: site.name,
348
+ description: site.description,
349
+ url: site.url,
350
+ telephone: `+${site.phone}`,
351
+ address: { '@type': 'PostalAddress', streetAddress: site.address },
352
+ }),
353
+ }],
354
+ })
355
+ </script>
356
+ ```
357
+
358
+ ## Dev + Build Commands
359
+
360
+ ```bash
361
+ npm run dev # http://localhost:3000
362
+ npm run build # production build (outputs .output/)
363
+ npm run preview # preview production build locally
364
+ npm run generate # static site generation (SSG)
365
+ ```
366
+
367
+ ## Vercel Deploy
368
+
369
+ ```bash
370
+ npm install -g vercel
371
+ vercel --prod
372
+ ```
373
+
374
+ Or: set `nitro.preset = 'vercel'` in `nuxt.config.ts` and connect GitHub to Vercel dashboard.