| name | wulfskil |
| description | Clone a React/Vite template from GitHub and productionize it using a reference site for design inspiration. Sets up tooling (pnpm, Prettier, Husky, Volta), externalizes content into a typed data model, extracts brand tokens from the reference site (typography + palette only — never logos/photos/copy), adds a sticky mobile phone CTA, runs axe-core a11y fixes, scaffolds Vitest + Playwright tests, emits JSON-LD LocalBusiness schema + OG meta + sitemap, compresses images in place, replaces Lorem Ipsum with coherent placeholder copy, and lands a Vercel deploy config. Invoke as `/wulfskil <owner/repo> <reference-site-url>`. |
Wulfskil — React/Vite template productionization
You are running the Wulfskil template-to-production pipeline. Your job is to take a stock React/Vite template and transform it into a polished, lead-gen-ready marketing site that borrows its visual feel from a reference site.
Parsing the invocation
The user invokes you as /wulfskil <gh_repo> <reference_site>. Parse two arguments from the message:
gh_repo — GitHub repo in owner/name form (e.g. Wet-wr-Labs/automec) or a full https://github.com/owner/name URL. Normalize to owner/name.
reference_site — a full URL to a live site (e.g. https://www.grandcollision.com) whose design language you will emulate (typography, color restraint, button shape, spacing rhythm).
If either is missing, ask once and wait. Do not guess. If the repo URL can't be parsed, ask.
Ground rules
- Never copy logos, photos, or copy from the reference site. That's trademark/copyright territory. Extract design tokens (fonts, colors, button radii, spacing) only.
- Commit after every logical phase. One commit per phase, small and reviewable. Each commit message follows the
type: short imperative convention (refactor:, feat:, chore:, style:, fix:, test:, docs:, perf:).
- Never invent the business. If you don't know the real phone / address / services, use obvious placeholders (reserved
(555) 123-xxxx phone range, hello@placeholder.example.com, etc.) and call them out. Never imagine real-looking contact info.
- Type-check and test after every meaningful change.
pnpm exec tsc --noEmit and pnpm test:run must stay green across every commit.
- Stop and confirm at Phase-end checkpoints marked ⏸ CHECK-IN below. Autonomous plowing is great between check-ins, not across them.
Pre-flight
Before touching anything, verify the toolchain is present:
gh --version && git --version && node --version && pnpm --version
If pnpm is missing, install it: npm install -g pnpm. If gh is missing, suggest winget install GitHub.cli and stop.
Pick the clone location. Prefer a Clients/ or projects/ subdirectory if one exists in the user's workspace. Ask if ambiguous. Do not clone into a path with a space in it if you can avoid it (Windows + pnpm + spaces in paths can hit intermittent EPERM issues; prefer C:\dev\<name> over C:\...\OneDrive\Desktop\...).
Announce the full plan (this doc's phase list) before starting and confirm the user wants to proceed.
Phase 0 — Clone & install
gh repo clone <gh_repo> <local-path> (or git clone https://github.com/<gh_repo>.git <local-path>)
cd in, rm -rf node_modules package-lock.json
pnpm install
- Read
package.json; if no volta block, run volta pin node@22 (requires Volta installed)
pnpm run dev in the background; curl -sf http://localhost:5173/ to confirm it's up
- Take a full-page screenshot of the unmodified site as a
before-full-page.png baseline, stored alongside the site for later comparison
Commit: don't commit Phase 0 — it's orientation. Phase 1 is the first commit.
Phase 1 — Tooling baseline
Goal: lock in a lint/format/pre-commit setup so every subsequent commit is clean.
pnpm add -D prettier husky lint-staged
Create .prettierrc.json:
{
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"arrowParens": "always",
"endOfLine": "lf"
}
Create .prettierignore:
node_modules
dist
build
public
src/assets/css/*.css
src/assets/css/*.css.bak
src/assets/css/fontawesome.min.css
*.min.js
*.min.css
pnpm-lock.yaml
package-lock.json
One-time format pass:
pnpm exec eslint . --fix 2>&1 | tail -15
pnpm exec prettier --write "src/**/*.{ts,tsx,js,jsx,json,md}" "*.{json,md}" --log-level=warn
Husky setup:
pnpm exec husky init
echo "pnpm exec lint-staged" > .husky/pre-commit
Add lint-staged config to package.json via a Node one-liner (not a manual edit — less chance of malforming JSON):
node -e "const fs=require('fs');const p=JSON.parse(fs.readFileSync('package.json','utf8'));p['lint-staged']={'*.{ts,tsx,js,jsx}':['eslint --fix','prettier --write'],'*.{json,md,scss}':['prettier --write']};fs.writeFileSync('package.json',JSON.stringify(p,null,2)+'\n');"
Commit: chore: pnpm migration + lint/format baseline + husky pre-commit
Phase 2 — Content architecture
Goal: make the whole site edit-from-one-file by typing and externalizing content.
2a. Inventory hardcoded vs data-driven components.
ls src/jsonData/ 2>&1
find src/jsonData -type f | sort
grep -rl "jsonData" src/components/ | head -20
For each homepage component that doesn't import from jsonData/, its content is hardcoded — extract it.
2b. Install the canonical type model. Copy templates/content.template.ts to src/types/content.ts in the target repo.
2c. Install the canonical site settings file. Copy templates/SiteSettings.template.json to src/jsonData/site/SiteSettings.json. This is the single source of truth for business info (name, phone, email, address, hours, services, socials).
2d. Externalize each hardcoded component. For each one:
- Read the
.tsx file to identify every piece of copy and every image path
- Create a matching data file under
src/jsonData/<section>/<Section>Data.json
- Rewrite the
.tsx to import the data and map over it
- Reference
SiteSettings.json for anything that's canonical business info (phone, email, address, social links) — don't duplicate across multiple section files
2e. Add <script type="application/ld+json"> AutoRepair schema and OG meta. Copy templates/generateSchema.template.ts to src/utils/generateSchema.ts. Wire it into App.tsx's <Helmet>:
import SiteSettings from './jsonData/site/SiteSettings.json';
import { generateAutoRepairSchema } from './utils/generateSchema';
<Helmet defaultTitle={SiteSettings.seo.defaultTitle} titleTemplate={SiteSettings.seo.titleTemplate}>
<html lang="en" />
<meta name="description" content={SiteSettings.seo.defaultDescription} />
<link rel="canonical" href={SiteSettings.url} />
<meta property="og:type" content="website" />
<meta property="og:site_name" content={SiteSettings.businessName} />
<meta property="og:title" content={SiteSettings.seo.defaultTitle} />
<meta property="og:description" content={SiteSettings.seo.defaultDescription} />
<meta property="og:url" content={SiteSettings.url} />
<meta property="og:image" content={`${SiteSettings.url}${SiteSettings.seo.ogImage}`} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={SiteSettings.seo.defaultTitle} />
<meta name="twitter:description" content={SiteSettings.seo.defaultDescription} />
<meta name="twitter:image" content={`${SiteSettings.url}${SiteSettings.seo.ogImage}`} />
<script type="application/ld+json">{JSON.stringify(generateAutoRepairSchema())}</script>
</Helmet>
Type-check and test after every externalization. Your commits must each stay green.
⏸ CHECK-IN: after Phase 2 completes, surface the list of sections externalized and confirm the user wants to continue. Their content decisions (what kind of business this is) start to matter from here.
Commits (typically 2-4, one per logical batch):
refactor: externalize Hero/About/CTA content + add content types
refactor: externalize HeaderV1 + MainMenu + FooterV1 content
refactor: externalize remaining sections + consolidate business info under SiteSettings
Phase 3 — Brand extraction
Goal: learn the reference site's visual vocabulary without copying.
Run scripts/extract-brand.mjs against the reference URL. It spins up headless Chromium, loads the page, and dumps design tokens to <repo>/brand-reference/gc-tokens.json + a full-page screenshot <repo>/brand-reference/reference-full.png.
node <skill-root>/scripts/extract-brand.mjs <reference_site> <repo>/brand-reference
Parse the tokens: headings font, body font, top 5 colors by frequency, primary button bg/radius, body background, heading color. Surface the tokens to the user as a table. They decide whether to proceed with those tokens, swap some, or keep the template's existing palette.
Phase 4 — Brand application
Goal: apply the reference site's restraint while preserving any brand color the user wants to keep.
Do NOT do a global color swap unless the user explicitly opts in. Users usually want to keep their own brand color and borrow the reference site's typography + layout restraint.
Default approach (most common user intent):
- Swap body font to match the reference site's body font (e.g. Source Sans Pro)
- Swap heading font to match (e.g. Lato 700)
- Swap body background to the reference's off-white / restrained base color
- Pill-shape primary CTAs (50px radius) and uppercase them
- Keep the template's primary accent color untouched
Use append-to-style.css (or equivalent) rather than editing thousands of compiled CSS rules:
body {
background-color: <reference body-bg>;
font-family: '<reference body font>', sans-serif;
color: <reference body text color>;
}
h1, h2, h3, h4, h5, h6 {
font-family: '<reference heading font>', sans-serif;
font-weight: 700;
color: <reference heading color>;
}
.btn-primary, .te-btn, [class*="primary-btn"] {
border-radius: 50px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
Update the Google Fonts @import at the top of custom.css to load the new fonts.
Commit: style: <reference-domain>-inspired typography + layout
Phase 5 — UX + A11y
5a. Sticky mobile Call-Now CTA. Create src/components/others/StickyPhoneCta.tsx:
import React from 'react';
import SiteSettings from '../../jsonData/site/SiteSettings.json';
const StickyPhoneCta = () => {
const phone = SiteSettings.phone;
const telHref = `tel:${phone.replace(/[^0-9+]/g, '')}`;
return (
<a href={telHref} className="sticky-phone-cta d-lg-none" aria-label={`Call us at ${phone}`}>
<i className="fa-solid fa-phone" aria-hidden="true" />
<span className="sticky-phone-cta__label">Call Now</span>
<span className="sticky-phone-cta__number">{phone}</span>
</a>
);
};
export default StickyPhoneCta;
Import it in App.tsx alongside other global elements. Append CSS to custom.css:
.sticky-phone-cta {
position: fixed; left: 16px; right: 16px; bottom: 16px; z-index: 1200;
display: flex; align-items: center; justify-content: center; gap: 10px;
padding: 14px 18px; border-radius: 50px;
background: <primary-accent>; color: #fff !important;
font-family: 'Lato', sans-serif; font-weight: 700; font-size: 15px;
text-decoration: none; box-shadow: 0 10px 24px rgba(0,0,0,0.28);
transition: transform 0.15s, box-shadow 0.15s;
}
.sticky-phone-cta:hover,
.sticky-phone-cta:focus-visible {
color: #fff; transform: translateY(-2px);
box-shadow: 0 14px 28px rgba(0,0,0,0.34); outline: none;
}
.sticky-phone-cta__label { text-transform: uppercase; }
.sticky-phone-cta__number { font-weight: 900; }
@media (min-width: 992px) { .sticky-phone-cta { display: none !important; } }
5b. axe-core audit. Install and run:
pnpm add -D @axe-core/playwright
node <skill-root>/scripts/axe-audit.mjs http://localhost:5173/
Report violations to the user. Fix the easy wins automatically:
button-name — add aria-label + type="button" to buttons whose only content is an <i> icon; mark the icon aria-hidden="true"
link-name — add aria-label="Follow us on <platform>" to social-icon links; add rel="noopener noreferrer" when target="_blank"
Don't auto-fix these without surfacing them:
color-contrast — brand-color decision, let the user choose to darken or accept
aria-hidden-focus on react-slick clones — library-level, not a quick fix
Commits:
feat: sticky mobile Call-Now CTA
fix: a11y pass via axe-core audit
Phase 6 — SEO
Create public/robots.txt:
User-agent: *
Allow: /
Sitemap: <SiteSettings.url>/sitemap.xml
Create public/sitemap.xml with the site's top routes (detect them from Routers.tsx). A minimal template:
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url><loc>SITE_URL/</loc><changefreq>weekly</changefreq><priority>1.0</priority></url>
<url><loc>SITE_URL/about</loc><changefreq>monthly</changefreq><priority>0.8</priority></url>
<url><loc>SITE_URL/services</loc><changefreq>monthly</changefreq><priority>0.9</priority></url>
<url><loc>SITE_URL/contact</loc><changefreq>yearly</changefreq><priority>0.9</priority></url>
</urlset>
Replace SITE_URL with SiteSettings.url. If the repo has no /about, /services, etc., only include routes that actually exist in Routers.tsx.
Commit: feat: canonical site settings + JSON-LD schema + SEO meta + sitemap/robots (fold this in with Phase 2's Helmet wiring if you committed them together)
Phase 7 — Tests
pnpm add -D vitest @vitest/ui jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event @playwright/test
Create vite.config.ts additions (Vitest block):
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./vitest.setup.ts'],
css: false,
include: ['src/**/*.{test,spec}.{ts,tsx}'],
exclude: ['e2e/**', 'node_modules/**'],
},
});
Create vitest.setup.ts:
import '@testing-library/jest-dom/vitest';
Create playwright.config.ts:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'list',
use: { baseURL: 'http://localhost:5173', trace: 'on-first-retry' },
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'mobile-safari', use: { ...devices['iPhone 14'] } },
],
webServer: {
command: 'pnpm run dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI,
timeout: 60_000,
},
});
Write at least 2 unit specs (e.g. hero + sticky CTA) and 2 e2e specs (homepage loads, JSON-LD schema valid). Patterns in the Automec example repo.
Add scripts to package.json via the Node one-liner pattern:
node -e "const fs=require('fs');const p=JSON.parse(fs.readFileSync('package.json','utf8'));p.scripts.test='vitest';p.scripts['test:run']='vitest run';p.scripts['test:e2e']='playwright test';p.scripts['test:e2e:ui']='playwright test --ui';fs.writeFileSync('package.json',JSON.stringify(p,null,2)+'\n');"
Run pnpm test:run and confirm green before committing.
Commit: test: Vitest unit tests + Playwright E2E scaffolding
Phase 8 — Imagery
pnpm add -D sharp
Copy scripts/compress-images.mjs from this skill into the target repo as scripts/compress-images.mjs and register scripts:
node -e "const fs=require('fs');const p=JSON.parse(fs.readFileSync('package.json','utf8'));p.scripts['images:compress']='node scripts/compress-images.mjs public/images';p.scripts['images:compress:dry']='node scripts/compress-images.mjs public/images --dry-run';fs.writeFileSync('package.json',JSON.stringify(p,null,2)+'\n');"
Run pnpm images:compress. Expect 30-50% size reduction on a template with untouched PNG backgrounds.
Alt-text audit:
find src/components -type f -name '*.tsx' -exec sed -i \
-e 's/alt="image"/alt=""/g' \
-e 's/alt="icon"/alt=""/g' \
-e 's/alt="logo"/alt="<BusinessName> logo"/g' \
-e 's/alt="Footer Logo"/alt="<BusinessName> logo"/g' \
{} +
Substitute <BusinessName> with SiteSettings.businessName. Decorative images (image, icon) become alt=""; logos get a meaningful alt.
Commit: perf+a11y: compress images + fix generic alt text
Phase 9 — Copy cleanup
Go through every data file under src/jsonData/ and replace Lorem Ipsum with coherent placeholder English that matches each section's intent. Never invent real business facts — use placeholders like "[Business name]" or reserved-range (555) 123-xxxx phones.
Common fixes:
- Hero stats: replace implausible big numbers (
20K Satisfied Clients, 400 Team Member) with defensible placeholders (15+ Years in Business, 100% Warranty Backed)
- Grammar:
More Service → Our Services, Our All Services → Services, Team Member → Team Members, Ckients Review → Client Reviews, Terms & Condition → Terms & Conditions
- Template-exposed nav: kill
Home Page 1/2, all <section> Details Page dropdown items that leak scaffolding
- CTA verbs:
CONTACT NOW → Request a Free Estimate, GET A QUOTE → Request Estimate, SEND NOW → Send Message
Commit: copy: replace Lorem, flatten nav, fix grammar, sharpen CTAs
Phase 10 — Deploy config
Copy templates/vercel.template.json to vercel.json at the repo root.
Rewrite README.md: stack, quick start, script reference table, project layout, editing-content instructions, testing instructions, lint/format workflow, deployment instructions, accessibility/SEO summary, brand notes.
Commit: chore: Vercel deploy config + project README
Final summary
Report back to the user with:
- Every commit made, one-line each
- A
before/after comparison of total image weight
- The list of still-blocked items (real business info, positioning, photography, Phase 4 backend decision if applicable)
- The two commands to finish the job:
vercel login && vercel --prod
Escape hatches
- If the target repo isn't Vite/React/TS, stop at Phase 0 and explain. This skill targets that stack specifically.
- If the reference site is behind login / aggressive bot protection, extract-brand will fail. Ask the user for a different reference or manually-provided tokens.
- If pnpm install hits OneDrive-sync
EPERM errors on Windows (the path has OneDrive in it), retry the install once — OneDrive briefly locks files during sync.
- If Husky's
prepare script fails because the target repo isn't a git repo, skip Husky setup and note it for the user.
File manifest
Files this skill carries (relative to its root):
SKILL.md # this file
templates/
├── SiteSettings.template.json # canonical business-info shape
├── content.template.ts # TypeScript content model (types for all section data)
├── generateSchema.template.ts # JSON-LD AutoRepair schema generator
└── vercel.template.json # Vercel SPA deploy config
scripts/
├── compress-images.mjs # sharp-based in-place image compressor
├── extract-brand.mjs # Playwright script: reference-site design-token extractor
└── axe-audit.mjs # Playwright + axe-core accessibility auditor
Copy templates into the target repo where each phase says to. Scripts can be run directly from this skill's root without copying.