@kennethsolomon/shipkit 3.1.0 → 3.3.0
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 +8 -1
- package/commands/sk/security-check.md +16 -15
- package/package.json +1 -1
- package/skills/sk:accessibility/SKILL.md +13 -8
- package/skills/sk:e2e/SKILL.md +161 -10
- package/skills/sk:mvp/SKILL.md +266 -0
- package/skills/sk:mvp/references/design-system.md +136 -0
- package/skills/sk:mvp/references/landing-page.md +236 -0
- package/skills/sk:mvp/references/stacks/laravel.md +321 -0
- package/skills/sk:mvp/references/stacks/nextjs.md +189 -0
- package/skills/sk:mvp/references/stacks/nuxt.md +250 -0
- package/skills/sk:mvp/references/stacks/react-vite.md +287 -0
- package/skills/sk:perf/SKILL.md +12 -11
- package/skills/sk:seo-audit/SKILL.md +283 -0
- package/skills/sk:write-tests/SKILL.md +42 -3
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# React + Vite + Tailwind — Stack Reference
|
|
2
|
+
|
|
3
|
+
## Scaffold
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm create vite@latest {project-name} -- --template react-ts
|
|
7
|
+
cd {project-name}
|
|
8
|
+
npm install
|
|
9
|
+
npm install -D tailwindcss @tailwindcss/vite
|
|
10
|
+
npm install react-router-dom
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Directory Structure
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
{project-name}/
|
|
17
|
+
├── src/
|
|
18
|
+
│ ├── main.tsx ← entry point (router setup)
|
|
19
|
+
│ ├── App.tsx ← route definitions
|
|
20
|
+
│ ├── index.css ← Tailwind directives + custom CSS
|
|
21
|
+
│ ├── pages/
|
|
22
|
+
│ │ ├── Landing.tsx ← landing page
|
|
23
|
+
│ │ ├── Dashboard.tsx ← dashboard
|
|
24
|
+
│ │ ├── {Feature1}.tsx
|
|
25
|
+
│ │ ├── {Feature2}.tsx
|
|
26
|
+
│ │ └── Settings.tsx
|
|
27
|
+
│ ├── components/
|
|
28
|
+
│ │ ├── landing/
|
|
29
|
+
│ │ │ ├── Navbar.tsx
|
|
30
|
+
│ │ │ ├── Hero.tsx
|
|
31
|
+
│ │ │ ├── Features.tsx
|
|
32
|
+
│ │ │ ├── HowItWorks.tsx
|
|
33
|
+
│ │ │ ├── Pricing.tsx
|
|
34
|
+
│ │ │ ├── Testimonials.tsx
|
|
35
|
+
│ │ │ ├── WaitlistForm.tsx
|
|
36
|
+
│ │ │ └── Footer.tsx
|
|
37
|
+
│ │ ├── app/
|
|
38
|
+
│ │ │ ├── Sidebar.tsx
|
|
39
|
+
│ │ │ ├── AppLayout.tsx
|
|
40
|
+
│ │ │ ├── DashboardCards.tsx
|
|
41
|
+
│ │ │ └── {feature components}
|
|
42
|
+
│ │ └── ui/
|
|
43
|
+
│ │ ├── Button.tsx
|
|
44
|
+
│ │ ├── Input.tsx
|
|
45
|
+
│ │ ├── Card.tsx
|
|
46
|
+
│ │ ├── Modal.tsx
|
|
47
|
+
│ │ └── Toast.tsx
|
|
48
|
+
│ ├── data/
|
|
49
|
+
│ │ └── mock.ts ← all fake data centralized
|
|
50
|
+
│ └── lib/
|
|
51
|
+
│ └── utils.ts ← shared helpers (cn, etc.)
|
|
52
|
+
├── public/
|
|
53
|
+
│ └── {static assets}
|
|
54
|
+
├── index.html
|
|
55
|
+
├── vite.config.ts
|
|
56
|
+
├── tailwind.config.ts
|
|
57
|
+
└── package.json
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Vite Config
|
|
61
|
+
|
|
62
|
+
`vite.config.ts`:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { defineConfig } from 'vite'
|
|
66
|
+
import react from '@vitejs/plugin-react'
|
|
67
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
68
|
+
|
|
69
|
+
export default defineConfig({
|
|
70
|
+
plugins: [react(), tailwindcss()],
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Tailwind Config
|
|
75
|
+
|
|
76
|
+
`tailwind.config.ts`:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
export default {
|
|
80
|
+
content: ['./index.html', './src/**/*.{ts,tsx}'],
|
|
81
|
+
theme: {
|
|
82
|
+
extend: {
|
|
83
|
+
colors: {
|
|
84
|
+
bg: 'var(--color-bg)',
|
|
85
|
+
fg: 'var(--color-fg)',
|
|
86
|
+
accent: 'var(--color-accent)',
|
|
87
|
+
muted: 'var(--color-muted)',
|
|
88
|
+
},
|
|
89
|
+
fontFamily: {
|
|
90
|
+
display: ['var(--font-display)', 'serif'],
|
|
91
|
+
body: ['var(--font-body)', 'sans-serif'],
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
CSS variables and font imports in `src/index.css`:
|
|
99
|
+
|
|
100
|
+
```css
|
|
101
|
+
@import url('https://fonts.googleapis.com/css2?family={DisplayFont}:wght@400;600;700;800&family={BodyFont}:wght@400;500;600&display=swap');
|
|
102
|
+
|
|
103
|
+
@tailwind base;
|
|
104
|
+
@tailwind components;
|
|
105
|
+
@tailwind utilities;
|
|
106
|
+
|
|
107
|
+
:root {
|
|
108
|
+
--color-bg: #xxxxxx;
|
|
109
|
+
--color-fg: #xxxxxx;
|
|
110
|
+
--color-accent: #xxxxxx;
|
|
111
|
+
--color-muted: #xxxxxx;
|
|
112
|
+
--font-display: '{DisplayFont}', serif;
|
|
113
|
+
--font-body: '{BodyFont}', sans-serif;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
body {
|
|
117
|
+
font-family: var(--font-body);
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Router Setup
|
|
122
|
+
|
|
123
|
+
`src/App.tsx`:
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
|
|
127
|
+
import Landing from './pages/Landing'
|
|
128
|
+
import AppLayout from './components/app/AppLayout'
|
|
129
|
+
import Dashboard from './pages/Dashboard'
|
|
130
|
+
import Feature1 from './pages/{Feature1}'
|
|
131
|
+
import Feature2 from './pages/{Feature2}'
|
|
132
|
+
import Settings from './pages/Settings'
|
|
133
|
+
|
|
134
|
+
const router = createBrowserRouter([
|
|
135
|
+
{ path: '/', element: <Landing /> },
|
|
136
|
+
{
|
|
137
|
+
element: <AppLayout />,
|
|
138
|
+
children: [
|
|
139
|
+
{ path: '/dashboard', element: <Dashboard /> },
|
|
140
|
+
{ path: '/{feature-1}', element: <Feature1 /> },
|
|
141
|
+
{ path: '/{feature-2}', element: <Feature2 /> },
|
|
142
|
+
{ path: '/settings', element: <Settings /> },
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
])
|
|
146
|
+
|
|
147
|
+
export default function App() {
|
|
148
|
+
return <RouterProvider router={router} />
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## App Layout
|
|
153
|
+
|
|
154
|
+
`src/components/app/AppLayout.tsx`:
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
import { Outlet } from 'react-router-dom'
|
|
158
|
+
import Sidebar from './Sidebar'
|
|
159
|
+
|
|
160
|
+
export default function AppLayout() {
|
|
161
|
+
return (
|
|
162
|
+
<div className="flex min-h-screen bg-bg text-fg">
|
|
163
|
+
<Sidebar />
|
|
164
|
+
<main className="flex-1 p-6 lg:p-8">
|
|
165
|
+
<Outlet />
|
|
166
|
+
</main>
|
|
167
|
+
</div>
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Waitlist — Formspree Integration
|
|
173
|
+
|
|
174
|
+
Since React + Vite has no backend, use Formspree for email collection.
|
|
175
|
+
|
|
176
|
+
`src/components/landing/WaitlistForm.tsx`:
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
import { useState, FormEvent } from 'react'
|
|
180
|
+
|
|
181
|
+
// Replace YOUR_FORM_ID with your Formspree form ID
|
|
182
|
+
// Create one free at https://formspree.io
|
|
183
|
+
const FORMSPREE_URL = 'https://formspree.io/f/YOUR_FORM_ID'
|
|
184
|
+
|
|
185
|
+
export default function WaitlistForm() {
|
|
186
|
+
const [email, setEmail] = useState('')
|
|
187
|
+
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle')
|
|
188
|
+
const [message, setMessage] = useState('')
|
|
189
|
+
|
|
190
|
+
async function handleSubmit(e: FormEvent) {
|
|
191
|
+
e.preventDefault()
|
|
192
|
+
setStatus('loading')
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const res = await fetch(FORMSPREE_URL, {
|
|
196
|
+
method: 'POST',
|
|
197
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
198
|
+
body: JSON.stringify({ email }),
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
if (res.ok) {
|
|
202
|
+
setStatus('success')
|
|
203
|
+
setMessage("You're on the list! We'll notify you when we launch.")
|
|
204
|
+
} else {
|
|
205
|
+
throw new Error()
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
setStatus('error')
|
|
209
|
+
setMessage('Something went wrong. Please try again.')
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (status === 'success') {
|
|
214
|
+
return <p className="text-green-600 font-medium text-lg">{message}</p>
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<form onSubmit={handleSubmit} className="flex flex-col sm:flex-row gap-3 max-w-md">
|
|
219
|
+
<input
|
|
220
|
+
type="email"
|
|
221
|
+
value={email}
|
|
222
|
+
onChange={e => setEmail(e.target.value)}
|
|
223
|
+
placeholder="you@example.com"
|
|
224
|
+
required
|
|
225
|
+
className="flex-1 px-4 py-3 rounded-lg border border-muted/30 bg-bg focus:ring-2 focus:ring-accent focus:outline-none"
|
|
226
|
+
/>
|
|
227
|
+
<button
|
|
228
|
+
type="submit"
|
|
229
|
+
disabled={status === 'loading'}
|
|
230
|
+
className="px-6 py-3 bg-accent text-white rounded-xl font-medium hover:opacity-90 transition-all disabled:opacity-50"
|
|
231
|
+
>
|
|
232
|
+
{status === 'loading' ? 'Joining...' : 'Join Waitlist'}
|
|
233
|
+
</button>
|
|
234
|
+
{status === 'error' && <p className="text-red-500 text-sm">{message}</p>}
|
|
235
|
+
</form>
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Mock Data
|
|
241
|
+
|
|
242
|
+
Centralize all fake data in `src/data/mock.ts`:
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
export const features = [
|
|
246
|
+
{ icon: '🎯', title: 'Feature One', description: 'Short benefit-driven description.' },
|
|
247
|
+
{ icon: '⚡', title: 'Feature Two', description: 'Short benefit-driven description.' },
|
|
248
|
+
{ icon: '🔒', title: 'Feature Three', description: 'Short benefit-driven description.' },
|
|
249
|
+
]
|
|
250
|
+
|
|
251
|
+
export const testimonials = [
|
|
252
|
+
{ quote: 'Realistic testimonial here.', name: 'Jane Smith', role: 'CTO, TechCo' },
|
|
253
|
+
// ...
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
export const pricingPlans = [
|
|
257
|
+
{ name: 'Free', price: '$0', features: ['Feature A', 'Feature B'], cta: 'Get Started' },
|
|
258
|
+
{ name: 'Pro', price: '$29/mo', features: ['Everything in Free', 'Feature C'], cta: 'Get Pro', popular: true },
|
|
259
|
+
{ name: 'Enterprise', price: 'Custom', features: ['Everything in Pro', 'Priority support'], cta: 'Contact Us' },
|
|
260
|
+
]
|
|
261
|
+
|
|
262
|
+
// Dashboard mock data
|
|
263
|
+
export const dashboardStats = [
|
|
264
|
+
{ label: 'Total Users', value: '2,847', change: '+12%' },
|
|
265
|
+
// ...
|
|
266
|
+
]
|
|
267
|
+
|
|
268
|
+
export const recentActivity = [
|
|
269
|
+
{ user: 'Jane Smith', action: 'created a new project', time: '2 hours ago' },
|
|
270
|
+
// ...
|
|
271
|
+
]
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Component Patterns
|
|
275
|
+
|
|
276
|
+
- Use functional components with TypeScript.
|
|
277
|
+
- Use `useState` for local state, no state management library needed.
|
|
278
|
+
- Navigation: `<Link to="/dashboard">` from react-router-dom.
|
|
279
|
+
- Keep components focused — one file per component.
|
|
280
|
+
- Props typed with interfaces or inline.
|
|
281
|
+
|
|
282
|
+
## Dev Server
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
npm run dev
|
|
286
|
+
# Runs on http://localhost:5173
|
|
287
|
+
```
|
package/skills/sk:perf/SKILL.md
CHANGED
|
@@ -120,24 +120,25 @@ Write findings to `tasks/perf-findings.md`:
|
|
|
120
120
|
|
|
121
121
|
## Critical
|
|
122
122
|
|
|
123
|
-
- **[FILE:LINE]** Description
|
|
123
|
+
- [ ] **[FILE:LINE]** Description
|
|
124
124
|
**Impact:** What happens at scale
|
|
125
125
|
**Recommendation:** How to fix
|
|
126
|
+
- [x] **[FILE:LINE]** Description *(resolved)*
|
|
126
127
|
|
|
127
128
|
## High
|
|
128
129
|
|
|
129
|
-
- **[FILE:LINE]** Description
|
|
130
|
+
- [ ] **[FILE:LINE]** Description
|
|
130
131
|
**Impact:** ...
|
|
131
132
|
**Recommendation:** ...
|
|
132
133
|
|
|
133
134
|
## Medium
|
|
134
135
|
|
|
135
|
-
- **[FILE:LINE]** Description
|
|
136
|
+
- [ ] **[FILE:LINE]** Description
|
|
136
137
|
**Recommendation:** ...
|
|
137
138
|
|
|
138
139
|
## Low
|
|
139
140
|
|
|
140
|
-
- **[FILE:LINE]** Description
|
|
141
|
+
- [ ] **[FILE:LINE]** Description
|
|
141
142
|
**Recommendation:** ...
|
|
142
143
|
|
|
143
144
|
## Passed Checks
|
|
@@ -146,13 +147,13 @@ Write findings to `tasks/perf-findings.md`:
|
|
|
146
147
|
|
|
147
148
|
## Summary
|
|
148
149
|
|
|
149
|
-
| Severity |
|
|
150
|
-
|
|
151
|
-
| Critical | N |
|
|
152
|
-
| High | N |
|
|
153
|
-
| Medium | N |
|
|
154
|
-
| Low | N |
|
|
155
|
-
| **Total** | **N** |
|
|
150
|
+
| Severity | Open | Resolved this run |
|
|
151
|
+
|----------|------|-------------------|
|
|
152
|
+
| Critical | N | N |
|
|
153
|
+
| High | N | N |
|
|
154
|
+
| Medium | N | N |
|
|
155
|
+
| Low | N | N |
|
|
156
|
+
| **Total** | **N** | **N** |
|
|
156
157
|
```
|
|
157
158
|
|
|
158
159
|
**Never overwrite** `tasks/perf-findings.md` — append new audits with a date header.
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sk:seo-audit
|
|
3
|
+
description: "SEO audit for web projects. Dual-mode: scans source templates + optionally fetches from running dev server. Ask-before-fix for mechanical issues. Outputs checklist findings to tasks/seo-findings.md."
|
|
4
|
+
license: Complete terms in LICENSE.txt
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# /sk:seo-audit
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
Standalone optional command — audits any web project for SEO issues regardless of framework (Laravel, Next.js, Nuxt, plain HTML, etc.). Run at any point after implementation is complete. NOT a numbered workflow step — invoke it independently like `/sk:debug`.
|
|
12
|
+
|
|
13
|
+
Two modes:
|
|
14
|
+
- **Source mode** (always runs): scans template files directly for SEO signals
|
|
15
|
+
- **Server mode** (optional): fetches from a running dev server to validate rendered output
|
|
16
|
+
|
|
17
|
+
Run when: before shipping a client site, after adding new pages, or any time you want to check SEO health.
|
|
18
|
+
|
|
19
|
+
## Hard Rules
|
|
20
|
+
|
|
21
|
+
- **Never auto-apply fixes without explicit user confirmation.**
|
|
22
|
+
- **Every finding must cite a specific `file:line`.**
|
|
23
|
+
- **Every finding is a checkbox:** `- [ ]` (open) or `- [x]` (auto-fixed this run)
|
|
24
|
+
- **Append to `tasks/seo-findings.md`** — never overwrite (use date header per run)
|
|
25
|
+
- **Degrade gracefully** if no server is running — skip Phase 2, note it in report
|
|
26
|
+
- **Structured data validation requires external tools** (Google Rich Results Test) — flag it, don't skip silently
|
|
27
|
+
|
|
28
|
+
## Before You Start
|
|
29
|
+
|
|
30
|
+
1. Read `tasks/findings.md` if it exists — look for site context, target audience, business type (helps tailor content strategy recommendations)
|
|
31
|
+
2. Read `tasks/lessons.md` if it exists — apply any SEO-related lessons
|
|
32
|
+
3. Check if `tasks/seo-findings.md` exists — if yes, read the last dated section to identify previously flagged items (used to populate "Passed Checks" in the new report)
|
|
33
|
+
|
|
34
|
+
## Mode Detection
|
|
35
|
+
|
|
36
|
+
### Source Mode — Always Active
|
|
37
|
+
|
|
38
|
+
Scan the project for template files:
|
|
39
|
+
|
|
40
|
+
| Extension | Framework |
|
|
41
|
+
|-----------|-----------|
|
|
42
|
+
| `.blade.php` | Laravel |
|
|
43
|
+
| `.jsx`, `.tsx` | React / Next.js |
|
|
44
|
+
| `.vue` | Vue / Nuxt |
|
|
45
|
+
| `.html` | Plain HTML / static |
|
|
46
|
+
| `.ejs` | Express / Node |
|
|
47
|
+
| `.njk` | Nunjucks |
|
|
48
|
+
| `.twig` | Twig / Symfony |
|
|
49
|
+
| `.erb` | Ruby on Rails |
|
|
50
|
+
| `.astro` | Astro |
|
|
51
|
+
|
|
52
|
+
Print: `"Source mode: found N template files ([extensions detected])"`
|
|
53
|
+
|
|
54
|
+
### Server Mode — Optional
|
|
55
|
+
|
|
56
|
+
Probe ports in parallel (background curl processes) to avoid 14-second worst-case serial timeout:
|
|
57
|
+
- Ports: 3000, 5173, 8000, 8080, 4321, 4000, 8888
|
|
58
|
+
- Command: `curl -s -I --max-time 2 http://localhost:PORT` (HEAD request to capture both status code and headers)
|
|
59
|
+
- Use the first port that returns HTTP 200 **and** has a `Content-Type: text/html` response header
|
|
60
|
+
|
|
61
|
+
If a port returns 200 but no `Content-Type: text/html` header, skip it — it is likely a non-HTTP service (e.g., a database, gRPC server) and not a web app. Try the next port.
|
|
62
|
+
|
|
63
|
+
If any port qualifies: `"Server mode: detected running dev server at http://localhost:PORT"`
|
|
64
|
+
|
|
65
|
+
If none respond or qualify: `"Server mode: no dev server detected — skipping Phase 2. Start your dev server and re-run for full audit."`
|
|
66
|
+
|
|
67
|
+
> Note: confirm the detected URL looks correct before trusting Phase 2 results.
|
|
68
|
+
|
|
69
|
+
## Phase 1 — Source Audit
|
|
70
|
+
|
|
71
|
+
### Technical SEO
|
|
72
|
+
|
|
73
|
+
- `robots.txt` — exists in project root or `public/`; does NOT contain `Disallow: /` blocking all crawlers
|
|
74
|
+
- `sitemap.xml` — exists in project root or `public/`; referenced in `robots.txt` via `Sitemap:` directive
|
|
75
|
+
- `<html lang="">` — present on all layout/root templates (not empty)
|
|
76
|
+
- Canonical tags — `<link rel="canonical">` present on key page templates
|
|
77
|
+
- No accidental `<meta name="robots" content="noindex">` on public-facing pages
|
|
78
|
+
- No hardcoded `http://` asset URLs in templates (mixed content risk)
|
|
79
|
+
|
|
80
|
+
### On-Page SEO
|
|
81
|
+
|
|
82
|
+
- `<title>` — present in `<head>`, unique across pages, 50–60 characters
|
|
83
|
+
- `<meta name="description">` — present in `<head>`, unique across pages, 150–160 characters
|
|
84
|
+
- Exactly one `<h1>` per page template (not zero, not two+)
|
|
85
|
+
- Heading hierarchy not skipped (no jumping from `<h2>` to `<h4>`)
|
|
86
|
+
- All `<img>` tags have `alt` attribute (even if empty for decorative — but flag empty alt on non-decorative images)
|
|
87
|
+
- Internal `<a>` link text is descriptive — flag anchors with text: "click here", "here", "read more", "link", "this"
|
|
88
|
+
- Image filenames are descriptive — flag patterns like `img001`, `IMG_`, `photo`, `image`, `DSC_`, `screenshot` with no context
|
|
89
|
+
|
|
90
|
+
### Content Signals
|
|
91
|
+
|
|
92
|
+
- Open Graph tags: `og:title`, `og:description`, `og:url`, `og:image` all present in layout
|
|
93
|
+
- Twitter Card tags: `twitter:card` present
|
|
94
|
+
- JSON-LD structured data block: look for `<script type="application/ld+json">` — note presence/absence; do NOT validate schema (requires external tool)
|
|
95
|
+
- Page `<html lang="">` matches expected locale
|
|
96
|
+
|
|
97
|
+
## Phase 2 — Server Audit (Optional)
|
|
98
|
+
|
|
99
|
+
If server detected:
|
|
100
|
+
|
|
101
|
+
1. Fetch `/` and discover up to 4 additional pages (from `<a>` href values in homepage, or from sitemap.xml)
|
|
102
|
+
2. For each page fetched, extract and compare:
|
|
103
|
+
- Rendered `<title>` vs source template value
|
|
104
|
+
- Rendered `<meta name="description">` vs source template value
|
|
105
|
+
- Rendered `<h1>` vs source template value
|
|
106
|
+
- Rendered OG tags vs source template
|
|
107
|
+
3. Flag mismatches: `"/about — Source template declares <title>About Us</title> but rendered output shows <title>My App</title> — framework may be overriding"`
|
|
108
|
+
4. Check HTTP status codes — flag any key page returning non-200
|
|
109
|
+
5. Check for redirect chains on common pages (/ → /home → /index is a chain)
|
|
110
|
+
|
|
111
|
+
> Note in report: "Structured data detected but NOT validated — use Google Rich Results Test (https://search.google.com/test/rich-results) to verify schema markup."
|
|
112
|
+
|
|
113
|
+
## Phase 3 — Ask Before Fix
|
|
114
|
+
|
|
115
|
+
After completing Phase 1 (and Phase 2 if run):
|
|
116
|
+
|
|
117
|
+
1. Collect all auto-fixable findings (see Mechanical Fixes Reference below)
|
|
118
|
+
2. Display numbered list:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
Found N auto-fixable issues:
|
|
122
|
+
1. Missing <title> in resources/views/layouts/app.blade.php
|
|
123
|
+
2. Missing alt attribute on <img> in resources/views/home.blade.php:42
|
|
124
|
+
3. Missing robots.txt
|
|
125
|
+
... (all N items)
|
|
126
|
+
|
|
127
|
+
Apply mechanical fixes? [y/N]
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
3. Wait for user response
|
|
131
|
+
4. On `y`: apply each fix in order, log `"Fixed: [description] in [file:line]"`, mark as `- [x]` in report. On individual fix failure: log the error, mark that item `- [ ]`, and continue with remaining fixes.
|
|
132
|
+
5. On `n`: mark all as `- [ ]` in report with Fix instructions
|
|
133
|
+
|
|
134
|
+
## Mechanical Fixes Reference
|
|
135
|
+
|
|
136
|
+
What this skill CAN auto-apply when user confirms:
|
|
137
|
+
|
|
138
|
+
| Issue | Fix Applied |
|
|
139
|
+
|-------|------------|
|
|
140
|
+
| Missing `<title>` in `<head>` | Add `<title>TODO: Add page title (50-60 chars)</title>` |
|
|
141
|
+
| Missing `<meta name="description">` | Add `<meta name="description" content="TODO: Add description (150-160 chars)">` |
|
|
142
|
+
| `<img>` missing `alt` attribute | Add `alt="TODO: Describe this image for screen readers"` |
|
|
143
|
+
| Missing `<link rel="canonical">` | Add `<link rel="canonical" href="TODO: Add canonical URL">` |
|
|
144
|
+
| Missing `robots.txt` | Create `robots.txt`: `User-agent: *\nAllow: /\nSitemap: /sitemap.xml` |
|
|
145
|
+
| Missing `sitemap.xml` | Create `sitemap.xml` scaffold with homepage entry |
|
|
146
|
+
| Multiple `<h1>` on same page | Demote 2nd, 3rd... `<h1>` to `<h2>` |
|
|
147
|
+
| Missing OG tags | Add `og:title`, `og:description`, `og:url` block (with TODO placeholders) |
|
|
148
|
+
| Missing `<html lang="">` | Add `lang="en"` — **note in output: verify correct language code** |
|
|
149
|
+
|
|
150
|
+
Things this skill CANNOT auto-apply (report only):
|
|
151
|
+
- Content quality improvements
|
|
152
|
+
- Keyword targeting
|
|
153
|
+
- Title/description CONTENT (only adds TODOs)
|
|
154
|
+
- Schema markup content (only flags missing)
|
|
155
|
+
- Backlink strategy
|
|
156
|
+
- `<meta name="robots" content="noindex">` removal — only the developer can confirm whether a page is intentionally noindexed
|
|
157
|
+
|
|
158
|
+
## Generate Report
|
|
159
|
+
|
|
160
|
+
Write to `tasks/seo-findings.md` — append with date header, never overwrite.
|
|
161
|
+
|
|
162
|
+
```markdown
|
|
163
|
+
# SEO Audit — YYYY-MM-DD
|
|
164
|
+
|
|
165
|
+
**Mode:** Source only | Source + Server (`http://localhost:PORT`)
|
|
166
|
+
**Templates scanned:** N files ([detected extensions])
|
|
167
|
+
**Pages fetched:** N | none — server not detected
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Critical
|
|
172
|
+
|
|
173
|
+
- [x] `resources/views/layouts/app.blade.php` — Missing `<title>` tag *(auto-fixed — add real title)*
|
|
174
|
+
- [ ] `resources/views/about.blade.php:1` — Missing `<meta name="description">`
|
|
175
|
+
**Impact:** Google may auto-generate a description from page content, often poorly.
|
|
176
|
+
**Fix:** Add `<meta name="description" content="150-160 char description">` in `<head>`
|
|
177
|
+
|
|
178
|
+
## High
|
|
179
|
+
|
|
180
|
+
- [ ] `public/robots.txt` — File missing
|
|
181
|
+
**Impact:** Search engines have no crawl guidance — may index unwanted pages.
|
|
182
|
+
**Fix:** Create `robots.txt` with `User-agent: *`, `Allow: /`, `Sitemap:` directive
|
|
183
|
+
|
|
184
|
+
## Medium
|
|
185
|
+
|
|
186
|
+
- [ ] `resources/views/home.blade.php:42` — `<img src="hero.jpg">` missing alt attribute
|
|
187
|
+
**Impact:** Accessibility violation + missed keyword opportunity.
|
|
188
|
+
**Fix:** Add descriptive `alt="..."` text
|
|
189
|
+
|
|
190
|
+
## Low
|
|
191
|
+
|
|
192
|
+
- [ ] Image filename `IMG_4521.jpg` — not descriptive
|
|
193
|
+
**Impact:** Minor missed keyword signal.
|
|
194
|
+
**Fix:** Rename to describe the image content
|
|
195
|
+
|
|
196
|
+
## Content Strategy — Manual Action
|
|
197
|
+
|
|
198
|
+
- [ ] No JSON-LD structured data detected — consider adding schema markup (Article / Product / LocalBusiness / FAQPage) based on your content type. Validate at: https://search.google.com/test/rich-results
|
|
199
|
+
- [ ] `og:image` missing — social shares will have no preview image. Add a default OG image in your layout.
|
|
200
|
+
- [ ] Submit `sitemap.xml` to Google Search Console for faster indexing
|
|
201
|
+
- [ ] Title tags are present but content is generic ("TODO") — research target keywords for each page
|
|
202
|
+
|
|
203
|
+
## Passed Checks
|
|
204
|
+
|
|
205
|
+
- `robots.txt` exists and allows crawling *(was: missing — fixed in 2026-03-10 audit)*
|
|
206
|
+
- All `<img>` tags have alt attributes
|
|
207
|
+
- Single `<h1>` per page
|
|
208
|
+
|
|
209
|
+
(or "First run — no prior baseline to compare against")
|
|
210
|
+
|
|
211
|
+
## Applied Fixes
|
|
212
|
+
|
|
213
|
+
- Fixed: Added `<title>` placeholder to `resources/views/layouts/app.blade.php`
|
|
214
|
+
- Fixed: Created `public/robots.txt`
|
|
215
|
+
|
|
216
|
+
(or "No fixes applied this run")
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Summary
|
|
221
|
+
|
|
222
|
+
| Severity | Open | Fixed this run |
|
|
223
|
+
|----------|------|----------------|
|
|
224
|
+
| Critical | 1 | 1 |
|
|
225
|
+
| High | 1 | 0 |
|
|
226
|
+
| Medium | 3 | 0 |
|
|
227
|
+
| Low | 2 | 0 |
|
|
228
|
+
| Content Strategy | 4 | — |
|
|
229
|
+
| **Total** | **11** | **1** |
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
**Never overwrite** `tasks/seo-findings.md` — append new audits with a date header.
|
|
233
|
+
|
|
234
|
+
## When Done
|
|
235
|
+
|
|
236
|
+
If Critical or High items are open:
|
|
237
|
+
> "SEO audit complete. **N critical/high issues** need attention before this site will rank well. Findings and checklist in `tasks/seo-findings.md`."
|
|
238
|
+
|
|
239
|
+
If only Medium/Low/Content Strategy open:
|
|
240
|
+
> "Technical SEO is solid. **N medium/low polish items** and **N content strategy items** noted in `tasks/seo-findings.md`. Check off items as you address them."
|
|
241
|
+
|
|
242
|
+
If all clean:
|
|
243
|
+
> "SEO audit passed — no issues found. `tasks/seo-findings.md` updated with clean baseline."
|
|
244
|
+
|
|
245
|
+
If fixes were declined (`n`):
|
|
246
|
+
> "SEO audit complete. **N auto-fixable issues** left open (fixes declined). Checklist in `tasks/seo-findings.md` — check off items as you manually address them."
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Fix & Retest Protocol
|
|
251
|
+
|
|
252
|
+
When applying an SEO fix, classify it before committing:
|
|
253
|
+
|
|
254
|
+
**a. Template/config change** (adding a meta tag, fixing alt text, scaffolding robots.txt, adding lang attribute, creating sitemap.xml) → commit and re-run `/sk:seo-audit`. No test update needed.
|
|
255
|
+
|
|
256
|
+
**b. Logic change** (changing how a framework generates meta tags, modifying a layout component's data-fetching or rendering logic, changing routing that affects canonical URLs) → trigger protocol:
|
|
257
|
+
1. Update or add failing unit tests for the new behavior
|
|
258
|
+
2. Re-run `/sk:test` — must pass at 100% coverage
|
|
259
|
+
3. Commit (tests + fix together in one commit)
|
|
260
|
+
4. Re-run `/sk:seo-audit` to verify the fix resolved the finding
|
|
261
|
+
|
|
262
|
+
**Common logic-change SEO fixes:**
|
|
263
|
+
- Changing a Next.js `generateMetadata()` function → update tests asserting metadata output
|
|
264
|
+
- Modifying a Laravel controller that sets page title → update feature tests
|
|
265
|
+
- Changing a Vue component that injects `<head>` tags → update component tests
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Model Routing
|
|
270
|
+
|
|
271
|
+
Read `.shipkit/config.json` from the project root if it exists.
|
|
272
|
+
|
|
273
|
+
- If `model_overrides["sk:seo-audit"]` is set, use that model — it takes precedence.
|
|
274
|
+
- Otherwise use the `profile` field. Default: `balanced`.
|
|
275
|
+
|
|
276
|
+
| Profile | Model |
|
|
277
|
+
|---------|-------|
|
|
278
|
+
| `full-sail` | sonnet |
|
|
279
|
+
| `quality` | sonnet |
|
|
280
|
+
| `balanced` | sonnet |
|
|
281
|
+
| `budget` | haiku |
|
|
282
|
+
|
|
283
|
+
> `opus` = inherit (uses the current session model). When spawning sub-agents via the Agent tool, pass `model: "<resolved-model>"`.
|
|
@@ -145,16 +145,54 @@ If a frontend stack was detected, generate FE test files:
|
|
|
145
145
|
|
|
146
146
|
Skip this step if no FE stack was detected.
|
|
147
147
|
|
|
148
|
-
### 8b.
|
|
148
|
+
### 8b. Write E2E Spec Files (conditional)
|
|
149
|
+
|
|
150
|
+
**Only if `playwright.config.ts` or `playwright.config.js` is detected in the project root:**
|
|
151
|
+
|
|
152
|
+
Write `e2e/<feature>.spec.ts` files covering the acceptance criteria from `tasks/todo.md`. Follow these rules:
|
|
153
|
+
|
|
154
|
+
- Use `test.describe` / `test` blocks — not `describe`/`it`
|
|
155
|
+
- Use role-based locators: `getByRole`, `getByLabel`, `getByText`, `getByPlaceholder` — never CSS selectors
|
|
156
|
+
- Use `test.beforeEach` for shared setup (auth, navigation)
|
|
157
|
+
- Use `test.skip(!email, 'ENV_VAR not set — skipping')` guards for credential-dependent tests
|
|
158
|
+
- Auth credentials from env vars via `e2e/helpers/auth.ts` — never hardcode credentials
|
|
159
|
+
- Soft assertions (`expect.soft`) for non-critical checks; hard `expect` for gate conditions
|
|
160
|
+
|
|
161
|
+
E2E spec example structure:
|
|
162
|
+
```ts
|
|
163
|
+
import { test, expect } from '@playwright/test'
|
|
164
|
+
import { signIn, TEST_USERS } from './helpers/auth'
|
|
165
|
+
|
|
166
|
+
test.describe('[Feature] — [scenario]', () => {
|
|
167
|
+
test.beforeEach(async ({ page }) => {
|
|
168
|
+
const { email, password } = TEST_USERS.regular
|
|
169
|
+
test.skip(!email, 'E2E_USER_EMAIL not set — skipping')
|
|
170
|
+
await signIn(page, email, password)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test('[behavior description]', async ({ page }) => {
|
|
174
|
+
await page.goto('/dashboard/feature')
|
|
175
|
+
await expect(page.getByRole('heading', { name: /title/i })).toBeVisible()
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Create `e2e/helpers/auth.ts` if it doesn't exist (see `/sk:e2e` Playwright Setup Reference).
|
|
181
|
+
|
|
182
|
+
**Run the E2E spec to confirm tests fail or skip** (they should fail until implementation, or skip if env vars aren't set — both are acceptable for the RED phase):
|
|
183
|
+
```bash
|
|
184
|
+
npx playwright test e2e/<feature>.spec.ts --reporter=list
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### 8c. Playwright MCP Inspection (optional)
|
|
149
188
|
|
|
150
|
-
**Only if
|
|
189
|
+
**Only if the Playwright MCP plugin is active in the session AND live selectors are needed:**
|
|
151
190
|
|
|
152
191
|
Use the Playwright MCP plugin to inspect live page state for more accurate selectors:
|
|
153
192
|
|
|
154
193
|
1. Navigate to target URL
|
|
155
194
|
2. Capture accessibility snapshot for role-based selectors
|
|
156
195
|
3. Screenshot for visual reference
|
|
157
|
-
4. Optionally run inline assertions for complex interactions
|
|
158
196
|
|
|
159
197
|
### 9. Verify Tests Fail (Red Phase)
|
|
160
198
|
|
|
@@ -170,6 +208,7 @@ Output:
|
|
|
170
208
|
```
|
|
171
209
|
BE tests written: X tests in Y files ([framework])
|
|
172
210
|
FE tests written: X tests in Y files ([framework]) ← omit if no FE stack
|
|
211
|
+
E2E specs written: X tests in Y files (Playwright) ← omit if no playwright.config.ts
|
|
173
212
|
Existing tests updated: X files
|
|
174
213
|
Status: RED (tests fail as expected — ready for implementation)
|
|
175
214
|
```
|