@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.
- package/README.md +7 -6
- package/commands/sk/brainstorm.md +13 -0
- package/commands/sk/execute-plan.md +1 -0
- package/commands/sk/security-check.md +4 -0
- package/commands/sk/website.md +93 -0
- package/commands/sk/write-plan.md +38 -0
- package/package.json +1 -1
- package/skills/sk:autopilot/SKILL.md +0 -1
- package/skills/sk:fast-track/SKILL.md +0 -1
- package/skills/sk:gates/SKILL.md +4 -1
- package/skills/sk:retro/SKILL.md +0 -1
- package/skills/sk:reverse-doc/SKILL.md +0 -1
- package/skills/sk:review/SKILL.md +24 -6
- package/skills/sk:scope-check/SKILL.md +0 -1
- package/skills/sk:setup-claude/templates/commands/brainstorm.md.template +13 -0
- package/skills/sk:setup-claude/templates/commands/execute-plan.md.template +1 -0
- package/skills/sk:setup-claude/templates/commands/security-check.md.template +3 -0
- package/skills/sk:setup-claude/templates/commands/write-plan.md.template +37 -0
- package/skills/sk:start/SKILL.md +0 -1
- package/skills/sk:team/SKILL.md +0 -1
- package/skills/sk:website/SKILL.md +471 -0
- package/skills/sk:website/references/art-direction.md +210 -0
- package/skills/sk:website/references/brief-template.md +121 -0
- package/skills/sk:website/references/content-seo.md +143 -0
- package/skills/sk:website/references/handoff-template.md +261 -0
- package/skills/sk:website/references/launch-checklist.md +99 -0
- package/skills/sk:website/references/niche/accountant.md +75 -0
- package/skills/sk:website/references/niche/agency.md +75 -0
- package/skills/sk:website/references/niche/cafe.md +79 -0
- package/skills/sk:website/references/niche/dentist.md +78 -0
- package/skills/sk:website/references/niche/ecommerce.md +76 -0
- package/skills/sk:website/references/niche/gym.md +75 -0
- package/skills/sk:website/references/niche/home-services.md +76 -0
- package/skills/sk:website/references/niche/law-firm.md +75 -0
- package/skills/sk:website/references/niche/local-business.md +78 -0
- package/skills/sk:website/references/niche/med-spa.md +78 -0
- package/skills/sk:website/references/niche/portfolio.md +77 -0
- package/skills/sk:website/references/niche/real-estate.md +72 -0
- package/skills/sk:website/references/niche/restaurant.md +80 -0
- package/skills/sk:website/references/niche/saas.md +80 -0
- package/skills/sk:website/references/niche/wedding.md +80 -0
- package/skills/sk:website/references/stacks/laravel.md +425 -0
- package/skills/sk:website/references/stacks/nextjs.md +345 -0
- package/skills/sk:website/references/stacks/nuxt.md +374 -0
- 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.
|