@kennethsolomon/shipkit 3.14.0 → 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 +1 -0
- package/commands/sk/website.md +93 -0
- package/package.json +1 -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,80 @@
|
|
|
1
|
+
# Restaurant
|
|
2
|
+
|
|
3
|
+
Use for full-service restaurants, casual dining, bistros, brasseries, and food-first hospitality brands where reservations, menus, and atmosphere all drive decisions.
|
|
4
|
+
|
|
5
|
+
## Priorities
|
|
6
|
+
|
|
7
|
+
1. Menu and reservation path must be the top two accessible things on the site.
|
|
8
|
+
2. Atmosphere through photography — people decide before they taste.
|
|
9
|
+
3. Location, hours, and parking are always important.
|
|
10
|
+
4. Reduce friction to booking — no multi-step flows for a table inquiry.
|
|
11
|
+
|
|
12
|
+
## Default page set
|
|
13
|
+
|
|
14
|
+
- Home (hero + ambiance + signature dishes + reservation CTA)
|
|
15
|
+
- Menu (full menu by category — starters, mains, desserts, drinks)
|
|
16
|
+
- About (story, chef, kitchen philosophy)
|
|
17
|
+
- Reservations (booking form or third-party reservation link)
|
|
18
|
+
- Find Us (address, hours, parking, map embed)
|
|
19
|
+
|
|
20
|
+
Optional: Events / Private Dining, Gallery, Takeout / Delivery, Press
|
|
21
|
+
|
|
22
|
+
## Section guidance
|
|
23
|
+
|
|
24
|
+
**Hero:**
|
|
25
|
+
- Strong food or interior photography above the fold
|
|
26
|
+
- Headline should set the dining experience: cuisine type + atmosphere + location
|
|
27
|
+
- Two CTAs: "Make a Reservation" (primary) + "View Menu" (secondary)
|
|
28
|
+
|
|
29
|
+
**Signature dishes:**
|
|
30
|
+
- 3–6 featured dishes with short descriptions — texture, flavor, ingredient-forward
|
|
31
|
+
- Real names from the actual menu only
|
|
32
|
+
|
|
33
|
+
**About / story:**
|
|
34
|
+
- Chef background if notable, kitchen philosophy, sourcing if meaningful
|
|
35
|
+
- Warm and specific — not generic "passion for food" copy
|
|
36
|
+
|
|
37
|
+
**Reservation section:**
|
|
38
|
+
- Keep it simple: Name, Date, Time, Number of guests, Phone/Email
|
|
39
|
+
- Link to OpenTable / Resy / Quandoo if integrated
|
|
40
|
+
- WhatsApp for direct reservation inquiries
|
|
41
|
+
|
|
42
|
+
**Location + hours:**
|
|
43
|
+
- Prominently placed — in nav, on homepage, and on dedicated page
|
|
44
|
+
- Include parking, valet, or transit notes
|
|
45
|
+
- Map embed (Google Maps iframe)
|
|
46
|
+
|
|
47
|
+
**Reviews:**
|
|
48
|
+
- Real rating + review count from Google, Zomato, or TripAdvisor if available
|
|
49
|
+
- Specific quotes if real reviews exist — never invent
|
|
50
|
+
|
|
51
|
+
## Design guidance
|
|
52
|
+
|
|
53
|
+
- **Art direction:** Warm Hospitality almost always. Bold Brand-Forward for trendy concept restaurants.
|
|
54
|
+
- Photography is the design system's backbone — image quality matters more than any other element.
|
|
55
|
+
- Typography: editorial serif for headlines (Playfair Display, Cormorant, Fraunces) + clean sans for menus.
|
|
56
|
+
- Palette: warm, food-first colors — deep red, terracotta, cream, charcoal, dark green.
|
|
57
|
+
- Menu page: well-structured table/list layout — legible at a glance, not overly designed.
|
|
58
|
+
- Motion: minimal — slow hero crossfade, gentle parallax on food photography.
|
|
59
|
+
|
|
60
|
+
## SEO guidance
|
|
61
|
+
|
|
62
|
+
- Homepage title: "[Restaurant Name] — [Cuisine] Restaurant in [Neighborhood], [City]"
|
|
63
|
+
- H1: "[Cuisine type] in [Location]" e.g., "Modern Filipino Restaurant in Poblacion, Makati"
|
|
64
|
+
- Target: "restaurant in [city]", "[cuisine] restaurant near me", "best [cuisine] [city]"
|
|
65
|
+
- Include structured data: `Restaurant` schema with address, hours, telephone, menu URL, accepts reservations
|
|
66
|
+
- Separate menu page helps SEO — ensure it's crawlable HTML, not just a PDF
|
|
67
|
+
|
|
68
|
+
## WhatsApp / contact
|
|
69
|
+
|
|
70
|
+
- WhatsApp for reservation inquiries — high conversion for SEA restaurants
|
|
71
|
+
- Pre-filled message: "Hi! I'd like to make a reservation. I found you on your website."
|
|
72
|
+
- Combine with a simple form for advance/large-group bookings
|
|
73
|
+
|
|
74
|
+
## Avoid
|
|
75
|
+
|
|
76
|
+
- Menu as a PDF (not crawlable, terrible UX on mobile)
|
|
77
|
+
- Reservations buried below the fold
|
|
78
|
+
- Using SaaS/tech visual aesthetics for a warm dining brand
|
|
79
|
+
- Generic stock photography of food
|
|
80
|
+
- Hiding the address — locals google restaurants on the go
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# SaaS / Software Product
|
|
2
|
+
|
|
3
|
+
Use for SaaS products, B2B tools, AI products, developer platforms, and software-first brands.
|
|
4
|
+
|
|
5
|
+
## Priorities
|
|
6
|
+
|
|
7
|
+
1. Clarify what the product does in the first 5 seconds — no mystery.
|
|
8
|
+
2. Show who it's for — visitors self-qualify before trialing.
|
|
9
|
+
3. Reduce perceived complexity — demos, how-it-works, and proof over feature lists.
|
|
10
|
+
4. Move visitors toward signup, trial, or demo request.
|
|
11
|
+
|
|
12
|
+
## Default page set
|
|
13
|
+
|
|
14
|
+
- Home (outcome-driven hero + proof + social proof + pricing CTA)
|
|
15
|
+
- Features (or use cases, if that's the better frame)
|
|
16
|
+
- Pricing (3-tier with clear feature comparison)
|
|
17
|
+
- About (team + mission + why this product)
|
|
18
|
+
- Contact / Demo (simple lead capture or Calendly)
|
|
19
|
+
|
|
20
|
+
Optional: Docs, Blog, Changelog, Integrations, Customers/Case Studies
|
|
21
|
+
|
|
22
|
+
## Section guidance
|
|
23
|
+
|
|
24
|
+
**Hero:**
|
|
25
|
+
- One clear benefit statement — what the user can do with this product
|
|
26
|
+
- Sub-headline: who it's for + how it works in one line
|
|
27
|
+
- CTA: "Start Free Trial", "Request a Demo", or "Get Started Free"
|
|
28
|
+
- Supporting visual: screenshot, short video loop, or product mockup (real only)
|
|
29
|
+
|
|
30
|
+
**Social proof bar:**
|
|
31
|
+
- Company logos or "X+ teams" or "Y+ users" — real numbers only
|
|
32
|
+
- Even if small: "Trusted by 50+ early teams" is credible; invented logos are not
|
|
33
|
+
|
|
34
|
+
**Features / value pillars:**
|
|
35
|
+
- 3–6 cards: one benefit per card, not feature specs
|
|
36
|
+
- Frame around outcomes: "Deploy in minutes, not days" not "One-click deployment button"
|
|
37
|
+
|
|
38
|
+
**How it works:**
|
|
39
|
+
- 3-step visual flow — numbered, no jargon
|
|
40
|
+
- If there's a product tour or demo video, this is where it goes
|
|
41
|
+
|
|
42
|
+
**Pricing:**
|
|
43
|
+
- 3 tiers standard (Free/Starter/Pro or Starter/Growth/Enterprise)
|
|
44
|
+
- Highlight the recommended tier
|
|
45
|
+
- Annual toggle if applicable
|
|
46
|
+
- Use real pricing if known; if not, use "Contact for pricing" not invented numbers
|
|
47
|
+
|
|
48
|
+
**Testimonials:**
|
|
49
|
+
- Real quotes from real customers, name + role + company
|
|
50
|
+
- If none available, use social proof bar instead — never invent
|
|
51
|
+
|
|
52
|
+
## Design guidance
|
|
53
|
+
|
|
54
|
+
- **Art direction:** Premium Product-Led or Sharp Technical — depending on whether the product is design-forward or developer-facing.
|
|
55
|
+
- Avoid generic SaaS visual tropes: purple gradients, glowing UI previews, "scale" metaphors.
|
|
56
|
+
- Typography: clean, modern sans — DM Sans, Inter, Plus Jakarta Sans.
|
|
57
|
+
- Palette: controlled — neutral background, one primary brand color, strategic accent.
|
|
58
|
+
- Dark mode optional — great for developer tools, not necessary for all SaaS.
|
|
59
|
+
- Screenshots of real product UI carry more weight than illustrations.
|
|
60
|
+
|
|
61
|
+
## SEO guidance
|
|
62
|
+
|
|
63
|
+
- Homepage title: "[Product Name] — [Primary Benefit] for [Audience]"
|
|
64
|
+
- H1: specific value statement, not just a tagline
|
|
65
|
+
- Target: "[problem the product solves]", "[product category] software", "[use case] tool"
|
|
66
|
+
- Structured data: `SoftwareApplication` or `WebApplication`
|
|
67
|
+
- Blog/docs pages help long-tail SEO significantly
|
|
68
|
+
|
|
69
|
+
## WhatsApp / contact
|
|
70
|
+
|
|
71
|
+
- Not applicable for SaaS — use demo form, trial signup, or Calendly
|
|
72
|
+
- If B2B sales motion: contact form with company size, use case fields
|
|
73
|
+
|
|
74
|
+
## Avoid
|
|
75
|
+
|
|
76
|
+
- Fake product dashboards when no real screenshots exist
|
|
77
|
+
- Purple-to-blue gradients as the primary design element
|
|
78
|
+
- Feature lists without benefit framing
|
|
79
|
+
- "Innovative" and "cutting-edge" — show, don't claim
|
|
80
|
+
- Pricing page with no real tiers
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Wedding / Event / Bridal
|
|
2
|
+
|
|
3
|
+
Use for wedding planners, bridal brands, venues, photographers, florists, and wedding-focused event businesses.
|
|
4
|
+
|
|
5
|
+
## Priorities
|
|
6
|
+
|
|
7
|
+
1. Create emotional atmosphere immediately — couples are making an emotional, high-stakes purchase.
|
|
8
|
+
2. Make inquiry or booking paths obvious — couples often browse multiple vendors before contacting.
|
|
9
|
+
3. Show taste, portfolio, and experience.
|
|
10
|
+
4. Balance romance with practical planning information.
|
|
11
|
+
|
|
12
|
+
## Default page set
|
|
13
|
+
|
|
14
|
+
- Home (emotional positioning + portfolio preview + inquiry CTA)
|
|
15
|
+
- Services / Packages (what you offer, how it works, pricing approach)
|
|
16
|
+
- Portfolio / Gallery (real work — weddings photographed, events planned, flowers arranged)
|
|
17
|
+
- About (your story, why you do this, your team)
|
|
18
|
+
- Contact / Inquire (inquiry form with date, venue, guest count)
|
|
19
|
+
|
|
20
|
+
Optional: FAQ, Testimonials (real), Blog, Availability Calendar
|
|
21
|
+
|
|
22
|
+
## Section guidance
|
|
23
|
+
|
|
24
|
+
**Hero:**
|
|
25
|
+
- Set the emotional tone before the practical details
|
|
26
|
+
- Example: "Wedding Photographer in Manila — Candid, Intimate, Timeless"
|
|
27
|
+
- CTA: "Check Availability" or "Inquire Now"
|
|
28
|
+
- Strong hero photography (real work only)
|
|
29
|
+
|
|
30
|
+
**Portfolio / gallery:**
|
|
31
|
+
- Real weddings/events only — no stock wedding imagery
|
|
32
|
+
- If portfolio is limited (new business), use 3–5 strong examples or focus on mood/process
|
|
33
|
+
- Quality over quantity — 6 great photos beat 30 mediocre ones
|
|
34
|
+
|
|
35
|
+
**Services / packages:**
|
|
36
|
+
- Clear list of offerings: what's included, duration, deliverables
|
|
37
|
+
- Pricing approach: "Packages starting from [X]" or "Contact for custom quote" — avoid total opacity
|
|
38
|
+
- Separate wedding day vs. engagement vs. elopement if applicable
|
|
39
|
+
|
|
40
|
+
**About / story:**
|
|
41
|
+
- Personal and specific — why you do this work, your approach
|
|
42
|
+
- Team photos if working with a crew
|
|
43
|
+
|
|
44
|
+
**Testimonials:**
|
|
45
|
+
- Real couple quotes, real names, optional wedding date/venue
|
|
46
|
+
- Google Review count if available
|
|
47
|
+
|
|
48
|
+
**Contact / inquiry:**
|
|
49
|
+
- Form: Name, Email, Phone, Date, Venue (or location), Type of event, Guest count, Message
|
|
50
|
+
- Response time expectation — couples often contact multiple vendors simultaneously
|
|
51
|
+
|
|
52
|
+
## Design guidance
|
|
53
|
+
|
|
54
|
+
- **Art direction:** Quiet Luxury or Restrained Editorial for premium brands; Warm Hospitality for approachable/bohemian styles.
|
|
55
|
+
- Visual pacing and typography carry more weight than heavy ornament.
|
|
56
|
+
- Typography: romantic but readable — fine serif (Cormorant, Playfair Display) + clean sans
|
|
57
|
+
- Palette: soft and curated — blush, ivory, sage, champagne, dusty rose, eucalyptus green
|
|
58
|
+
- Photography is the entire design system — never use stock wedding imagery
|
|
59
|
+
- Motion: slow and graceful — crossfades, gentle scroll reveals
|
|
60
|
+
|
|
61
|
+
## SEO guidance
|
|
62
|
+
|
|
63
|
+
- Title: "[Business Name] — Wedding [Photographer/Planner/Florist] in [City]"
|
|
64
|
+
- H1: "Wedding [Service Type] in [City]" — e.g., "Wedding Photographer in Manila"
|
|
65
|
+
- Target: "wedding photographer [city]", "wedding planner [city]", "wedding florist near me"
|
|
66
|
+
- Portfolio pages optimized per wedding style/venue help for style-specific searches
|
|
67
|
+
- Structured data: `LocalBusiness` or `EventPlanner`
|
|
68
|
+
|
|
69
|
+
## WhatsApp / contact
|
|
70
|
+
|
|
71
|
+
- WhatsApp for initial availability inquiries in PH/SEA
|
|
72
|
+
- Pre-filled: "Hi! I'm inquiring about availability for my wedding. I found you on your website."
|
|
73
|
+
- Full inquiry form for packages/quotes
|
|
74
|
+
|
|
75
|
+
## Avoid
|
|
76
|
+
|
|
77
|
+
- Stock wedding photography — real work or no work
|
|
78
|
+
- Generic "Love, Laughter, and Happily Ever After" copy
|
|
79
|
+
- Hiding pricing entirely (couples will bounce if they have no price signal)
|
|
80
|
+
- Burying the inquiry form below excessive intro copy
|
|
@@ -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.
|