@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,425 @@
|
|
|
1
|
+
# Laravel 11 + Blade + Tailwind — Client Website Stack Reference
|
|
2
|
+
|
|
3
|
+
Stack for building multi-page client marketing sites with PHP/Laravel. NOT a prototype — real copy, real SEO, no fake data.
|
|
4
|
+
|
|
5
|
+
## Scaffold
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
composer create-project laravel/laravel {project-name}
|
|
9
|
+
cd {project-name}
|
|
10
|
+
npm install
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Laravel 11 ships with Tailwind CSS configured via Vite out of the box.
|
|
14
|
+
|
|
15
|
+
## Directory Structure
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
{project-name}/
|
|
19
|
+
├── resources/
|
|
20
|
+
│ ├── views/
|
|
21
|
+
│ │ ├── layouts/
|
|
22
|
+
│ │ │ └── site.blade.php ← site layout (head, nav, footer, WhatsApp)
|
|
23
|
+
│ │ ├── components/
|
|
24
|
+
│ │ │ ├── layout/
|
|
25
|
+
│ │ │ │ ├── navbar.blade.php
|
|
26
|
+
│ │ │ │ └── footer.blade.php
|
|
27
|
+
│ │ │ ├── home/
|
|
28
|
+
│ │ │ │ ├── hero.blade.php
|
|
29
|
+
│ │ │ │ ├── services.blade.php
|
|
30
|
+
│ │ │ │ └── testimonials.blade.php
|
|
31
|
+
│ │ │ ├── contact/
|
|
32
|
+
│ │ │ │ └── form.blade.php
|
|
33
|
+
│ │ │ └── whatsapp-button.blade.php ← floating CTA partial
|
|
34
|
+
│ │ ├── home.blade.php ← Home page
|
|
35
|
+
│ │ ├── about.blade.php ← About page
|
|
36
|
+
│ │ ├── services.blade.php ← Services / Menu page
|
|
37
|
+
│ │ └── contact.blade.php ← Contact page
|
|
38
|
+
│ ├── css/
|
|
39
|
+
│ │ └── app.css ← Tailwind directives + CSS custom properties
|
|
40
|
+
│ └── js/
|
|
41
|
+
│ └── app.js ← Vite entry + Alpine.js for interactivity
|
|
42
|
+
├── routes/
|
|
43
|
+
│ └── web.php ← page routes + contact POST route
|
|
44
|
+
├── app/
|
|
45
|
+
│ ├── Http/
|
|
46
|
+
│ │ └── Controllers/
|
|
47
|
+
│ │ └── ContactController.php ← contact form handler
|
|
48
|
+
│ └── Data/
|
|
49
|
+
│ └── SiteData.php ← typed site config: copy, pages, metadata
|
|
50
|
+
├── config/
|
|
51
|
+
│ └── site.php ← site-wide config values
|
|
52
|
+
├── public/
|
|
53
|
+
│ ├── images/
|
|
54
|
+
│ └── favicon.ico
|
|
55
|
+
├── tailwind.config.js
|
|
56
|
+
└── vite.config.js
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Site Config
|
|
60
|
+
|
|
61
|
+
`config/site.php` — single source of truth for all copy and metadata:
|
|
62
|
+
|
|
63
|
+
```php
|
|
64
|
+
<?php
|
|
65
|
+
|
|
66
|
+
return [
|
|
67
|
+
'name' => '{Business Name}',
|
|
68
|
+
'tagline' => '{Tagline}',
|
|
69
|
+
'description' => '{Meta description — used for SEO}',
|
|
70
|
+
'url' => env('APP_URL', 'https://{domain}'),
|
|
71
|
+
'phone' => '{639171234567}', // E.164 without +
|
|
72
|
+
'email' => '{contact@example.com}',
|
|
73
|
+
'address' => '{Full address}',
|
|
74
|
+
'hours' => '{Mon–Fri 9am–6pm}',
|
|
75
|
+
'social' => [
|
|
76
|
+
'facebook' => '{https://facebook.com/page}',
|
|
77
|
+
'instagram' => '{https://instagram.com/handle}',
|
|
78
|
+
],
|
|
79
|
+
'pages' => [
|
|
80
|
+
'home' => [
|
|
81
|
+
'title' => '{Business Name} — {Primary benefit}',
|
|
82
|
+
'description' => '{Page-specific meta description}',
|
|
83
|
+
'hero' => [
|
|
84
|
+
'headline' => '{Real headline — no Lorem ipsum}',
|
|
85
|
+
'subheadline' => '{Supporting line}',
|
|
86
|
+
'cta' => '{Primary CTA text}',
|
|
87
|
+
'cta_href' => '/contact',
|
|
88
|
+
],
|
|
89
|
+
],
|
|
90
|
+
'about' => [
|
|
91
|
+
'title' => 'About — {Business Name}',
|
|
92
|
+
'description' => '{About page meta description}',
|
|
93
|
+
],
|
|
94
|
+
'services' => [
|
|
95
|
+
'title' => 'Services — {Business Name}',
|
|
96
|
+
'description' => '{Services page meta description}',
|
|
97
|
+
'items' => [
|
|
98
|
+
['name' => '{Service 1}', 'description' => '{Real description}', 'price' => '{optional}'],
|
|
99
|
+
],
|
|
100
|
+
],
|
|
101
|
+
'contact' => [
|
|
102
|
+
'title' => 'Contact — {Business Name}',
|
|
103
|
+
'description' => '{Contact page meta description}',
|
|
104
|
+
],
|
|
105
|
+
],
|
|
106
|
+
];
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Site Layout
|
|
110
|
+
|
|
111
|
+
`resources/views/layouts/site.blade.php`:
|
|
112
|
+
|
|
113
|
+
```blade
|
|
114
|
+
<!DOCTYPE html>
|
|
115
|
+
<html lang="en">
|
|
116
|
+
<head>
|
|
117
|
+
<meta charset="UTF-8">
|
|
118
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
119
|
+
<title>{{ $title ?? config('site.name') }}</title>
|
|
120
|
+
<meta name="description" content="{{ $description ?? config('site.description') }}">
|
|
121
|
+
|
|
122
|
+
{{-- Open Graph --}}
|
|
123
|
+
<meta property="og:title" content="{{ $title ?? config('site.name') }}">
|
|
124
|
+
<meta property="og:description" content="{{ $description ?? config('site.description') }}">
|
|
125
|
+
<meta property="og:url" content="{{ $canonical ?? url()->current() }}">
|
|
126
|
+
<meta property="og:site_name" content="{{ config('site.name') }}">
|
|
127
|
+
<meta property="og:type" content="website">
|
|
128
|
+
|
|
129
|
+
{{-- Canonical --}}
|
|
130
|
+
<link rel="canonical" href="{{ $canonical ?? url()->current() }}">
|
|
131
|
+
<link rel="icon" href="/favicon.ico">
|
|
132
|
+
|
|
133
|
+
{{-- Fonts: replace with art-direction spec fonts --}}
|
|
134
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
135
|
+
<link href="https://fonts.googleapis.com/css2?family={DisplayFont}:wght@400;600;700;800&family={BodyFont}:wght@400;500;600&display=swap" rel="stylesheet">
|
|
136
|
+
|
|
137
|
+
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
|
138
|
+
</head>
|
|
139
|
+
<body class="bg-bg text-fg font-body antialiased">
|
|
140
|
+
<x-layout.navbar />
|
|
141
|
+
<main>
|
|
142
|
+
{{ $slot }}
|
|
143
|
+
</main>
|
|
144
|
+
<x-layout.footer />
|
|
145
|
+
|
|
146
|
+
{{-- Remove if not a local PH/SEA business --}}
|
|
147
|
+
<x-whatsapp-button :phone="config('site.phone')" message="Hi! I found you on your website." />
|
|
148
|
+
|
|
149
|
+
{{-- LocalBusiness structured data --}}
|
|
150
|
+
<script type="application/ld+json">
|
|
151
|
+
{
|
|
152
|
+
"@context": "https://schema.org",
|
|
153
|
+
"@type": "LocalBusiness",
|
|
154
|
+
"name": "{{ config('site.name') }}",
|
|
155
|
+
"description": "{{ config('site.description') }}",
|
|
156
|
+
"url": "{{ config('site.url') }}",
|
|
157
|
+
"telephone": "+{{ config('site.phone') }}",
|
|
158
|
+
"address": {
|
|
159
|
+
"@type": "PostalAddress",
|
|
160
|
+
"streetAddress": "{{ config('site.address') }}"
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
</script>
|
|
164
|
+
</body>
|
|
165
|
+
</html>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Page Views
|
|
169
|
+
|
|
170
|
+
`resources/views/home.blade.php`:
|
|
171
|
+
|
|
172
|
+
```blade
|
|
173
|
+
<x-layouts.site
|
|
174
|
+
title="{{ config('site.pages.home.title') }}"
|
|
175
|
+
description="{{ config('site.pages.home.description') }}"
|
|
176
|
+
>
|
|
177
|
+
<x-home.hero />
|
|
178
|
+
<x-home.services />
|
|
179
|
+
<x-home.testimonials />
|
|
180
|
+
</x-layouts.site>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`resources/views/contact.blade.php`:
|
|
184
|
+
|
|
185
|
+
```blade
|
|
186
|
+
<x-layouts.site
|
|
187
|
+
title="{{ config('site.pages.contact.title') }}"
|
|
188
|
+
description="{{ config('site.pages.contact.description') }}"
|
|
189
|
+
>
|
|
190
|
+
<section class="py-24 px-4 max-w-2xl mx-auto">
|
|
191
|
+
<h1 class="font-display text-4xl font-bold mb-8">Contact Us</h1>
|
|
192
|
+
<x-contact.form />
|
|
193
|
+
</section>
|
|
194
|
+
</x-layouts.site>
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Routes
|
|
198
|
+
|
|
199
|
+
`routes/web.php`:
|
|
200
|
+
|
|
201
|
+
```php
|
|
202
|
+
<?php
|
|
203
|
+
|
|
204
|
+
use App\Http\Controllers\ContactController;
|
|
205
|
+
|
|
206
|
+
Route::view('/', 'home')->name('home');
|
|
207
|
+
Route::view('/about', 'about')->name('about');
|
|
208
|
+
Route::view('/services', 'services')->name('services');
|
|
209
|
+
Route::view('/contact', 'contact')->name('contact');
|
|
210
|
+
|
|
211
|
+
Route::post('/contact', [ContactController::class, 'store'])->name('contact.store');
|
|
212
|
+
|
|
213
|
+
// Sitemap
|
|
214
|
+
Route::get('/sitemap.xml', function () {
|
|
215
|
+
$sitemap = simplexml_load_string('<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"></urlset>');
|
|
216
|
+
foreach (['/', '/about', '/services', '/contact'] as $path) {
|
|
217
|
+
$url = $sitemap->addChild('url');
|
|
218
|
+
$url->addChild('loc', config('site.url') . $path);
|
|
219
|
+
$url->addChild('changefreq', 'monthly');
|
|
220
|
+
$url->addChild('priority', $path === '/' ? '1.0' : '0.8');
|
|
221
|
+
}
|
|
222
|
+
return response($sitemap->asXML(), 200)->header('Content-Type', 'application/xml');
|
|
223
|
+
})->name('sitemap');
|
|
224
|
+
|
|
225
|
+
// Robots.txt
|
|
226
|
+
Route::get('/robots.txt', function () {
|
|
227
|
+
return response("User-agent: *\nAllow: /\nSitemap: " . config('site.url') . "/sitemap.xml", 200)
|
|
228
|
+
->header('Content-Type', 'text/plain');
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Contact Controller
|
|
233
|
+
|
|
234
|
+
`app/Http/Controllers/ContactController.php`:
|
|
235
|
+
|
|
236
|
+
```php
|
|
237
|
+
<?php
|
|
238
|
+
|
|
239
|
+
namespace App\Http\Controllers;
|
|
240
|
+
|
|
241
|
+
use Illuminate\Http\Request;
|
|
242
|
+
|
|
243
|
+
class ContactController extends Controller
|
|
244
|
+
{
|
|
245
|
+
public function store(Request $request)
|
|
246
|
+
{
|
|
247
|
+
$validated = $request->validate([
|
|
248
|
+
'name' => 'required|string|max:255',
|
|
249
|
+
'email' => 'required|email',
|
|
250
|
+
'phone' => 'nullable|string|max:50',
|
|
251
|
+
'message' => 'required|string|max:5000',
|
|
252
|
+
]);
|
|
253
|
+
|
|
254
|
+
// Honeypot check (add a hidden "website" field to the form)
|
|
255
|
+
if ($request->filled('website')) {
|
|
256
|
+
return back()->with('success', "Message received. We'll be in touch soon.");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// TODO: wire to mail (Mail::to(...)->send(new ContactMail($validated)))
|
|
260
|
+
// For now: log submission
|
|
261
|
+
logger()->info('Contact form submission', $validated);
|
|
262
|
+
|
|
263
|
+
return back()->with('success', "Message received. We'll be in touch soon.");
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Contact Form Component
|
|
269
|
+
|
|
270
|
+
`resources/views/components/contact/form.blade.php`:
|
|
271
|
+
|
|
272
|
+
```blade
|
|
273
|
+
<form
|
|
274
|
+
action="{{ route('contact.store') }}"
|
|
275
|
+
method="POST"
|
|
276
|
+
x-data="{ loading: false }"
|
|
277
|
+
@submit="loading = true"
|
|
278
|
+
class="space-y-4"
|
|
279
|
+
>
|
|
280
|
+
@csrf
|
|
281
|
+
|
|
282
|
+
{{-- Honeypot --}}
|
|
283
|
+
<input type="text" name="website" class="hidden" autocomplete="off" tabindex="-1">
|
|
284
|
+
|
|
285
|
+
@if (session('success'))
|
|
286
|
+
<p class="text-green-600 font-medium">{{ session('success') }}</p>
|
|
287
|
+
@endif
|
|
288
|
+
|
|
289
|
+
<input type="text" name="name" value="{{ old('name') }}" placeholder="Your name" required
|
|
290
|
+
class="w-full px-4 py-3 border rounded-lg @error('name') border-red-500 @enderror">
|
|
291
|
+
@error('name') <p class="text-red-500 text-sm">{{ $message }}</p> @enderror
|
|
292
|
+
|
|
293
|
+
<input type="email" name="email" value="{{ old('email') }}" placeholder="Email address" required
|
|
294
|
+
class="w-full px-4 py-3 border rounded-lg @error('email') border-red-500 @enderror">
|
|
295
|
+
@error('email') <p class="text-red-500 text-sm">{{ $message }}</p> @enderror
|
|
296
|
+
|
|
297
|
+
<input type="tel" name="phone" value="{{ old('phone') }}" placeholder="Phone (optional)"
|
|
298
|
+
class="w-full px-4 py-3 border rounded-lg">
|
|
299
|
+
|
|
300
|
+
<textarea name="message" placeholder="Your message" required rows="4"
|
|
301
|
+
class="w-full px-4 py-3 border rounded-lg resize-none @error('message') border-red-500 @enderror">{{ old('message') }}</textarea>
|
|
302
|
+
@error('message') <p class="text-red-500 text-sm">{{ $message }}</p> @enderror
|
|
303
|
+
|
|
304
|
+
<button type="submit" :disabled="loading"
|
|
305
|
+
class="w-full px-6 py-3 bg-accent text-white rounded-lg font-medium transition hover:opacity-90 disabled:opacity-60">
|
|
306
|
+
<span x-show="!loading">Send Message</span>
|
|
307
|
+
<span x-show="loading">Sending...</span>
|
|
308
|
+
</button>
|
|
309
|
+
</form>
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## WhatsApp Blade Component
|
|
313
|
+
|
|
314
|
+
`resources/views/components/whatsapp-button.blade.php`:
|
|
315
|
+
|
|
316
|
+
```blade
|
|
317
|
+
@props([
|
|
318
|
+
'phone', // E.164 without +: e.g., "639171234567"
|
|
319
|
+
'message' => null,
|
|
320
|
+
])
|
|
321
|
+
|
|
322
|
+
@php
|
|
323
|
+
$url = $message
|
|
324
|
+
? 'https://wa.me/' . $phone . '?text=' . urlencode($message)
|
|
325
|
+
: 'https://wa.me/' . $phone;
|
|
326
|
+
@endphp
|
|
327
|
+
|
|
328
|
+
<a
|
|
329
|
+
href="{{ $url }}"
|
|
330
|
+
target="_blank"
|
|
331
|
+
rel="noopener noreferrer"
|
|
332
|
+
aria-label="Chat on WhatsApp"
|
|
333
|
+
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"
|
|
334
|
+
>
|
|
335
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" class="h-7 w-7" aria-hidden="true">
|
|
336
|
+
<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"/>
|
|
337
|
+
</svg>
|
|
338
|
+
</a>
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Tailwind Config
|
|
342
|
+
|
|
343
|
+
`tailwind.config.js`:
|
|
344
|
+
|
|
345
|
+
```js
|
|
346
|
+
export default {
|
|
347
|
+
content: [
|
|
348
|
+
'./resources/**/*.blade.php',
|
|
349
|
+
'./resources/**/*.js',
|
|
350
|
+
],
|
|
351
|
+
theme: {
|
|
352
|
+
extend: {
|
|
353
|
+
colors: {
|
|
354
|
+
bg: 'var(--color-bg)',
|
|
355
|
+
fg: 'var(--color-fg)',
|
|
356
|
+
accent: 'var(--color-accent)',
|
|
357
|
+
muted: 'var(--color-muted)',
|
|
358
|
+
surface: 'var(--color-surface)',
|
|
359
|
+
},
|
|
360
|
+
fontFamily: {
|
|
361
|
+
display: ['{DisplayFont}', 'serif'],
|
|
362
|
+
body: ['{BodyFont}', 'sans-serif'],
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
`resources/css/app.css`:
|
|
370
|
+
|
|
371
|
+
```css
|
|
372
|
+
@tailwind base;
|
|
373
|
+
@tailwind components;
|
|
374
|
+
@tailwind utilities;
|
|
375
|
+
|
|
376
|
+
:root {
|
|
377
|
+
--color-bg: #xxxxxx; /* from art direction spec */
|
|
378
|
+
--color-fg: #xxxxxx;
|
|
379
|
+
--color-accent: #xxxxxx;
|
|
380
|
+
--color-muted: #xxxxxx;
|
|
381
|
+
--color-surface: #xxxxxx;
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
## Alpine.js for Interactivity
|
|
386
|
+
|
|
387
|
+
`resources/js/app.js`:
|
|
388
|
+
|
|
389
|
+
```js
|
|
390
|
+
import Alpine from 'alpinejs'
|
|
391
|
+
window.Alpine = Alpine
|
|
392
|
+
Alpine.start()
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
Install:
|
|
396
|
+
|
|
397
|
+
```bash
|
|
398
|
+
npm install alpinejs
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
## Dev + Build Commands
|
|
402
|
+
|
|
403
|
+
```bash
|
|
404
|
+
# Run both in separate terminals
|
|
405
|
+
php artisan serve # http://localhost:8000
|
|
406
|
+
npm run dev # Vite HMR for assets
|
|
407
|
+
|
|
408
|
+
# Or use Laravel Herd (auto-serves at {project-name}.test)
|
|
409
|
+
|
|
410
|
+
npm run build # compile assets for production
|
|
411
|
+
php artisan optimize # cache config, routes, views for production
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
## Vercel / Netlify Deploy
|
|
415
|
+
|
|
416
|
+
Laravel requires a PHP host — Vercel and Netlify do not support PHP natively. Options:
|
|
417
|
+
|
|
418
|
+
| Host | Notes |
|
|
419
|
+
|---|---|
|
|
420
|
+
| **Laravel Cloud** | First-party — simplest, scalable |
|
|
421
|
+
| **Forge + DigitalOcean** | Full control, $6–12/mo droplet |
|
|
422
|
+
| **Railway** | Docker-based, easy setup |
|
|
423
|
+
| **Render** | Free tier available for small sites |
|
|
424
|
+
|
|
425
|
+
Add deploy steps to `DEPLOY.md` based on chosen host. The default guide should recommend Laravel Cloud or Forge.
|