I run a one-person web agency called the vibe company that ships real client sites in 7 days. Live: zamhwa-studio.netlify.app. This post is the stack and the actual code that makes the 7-day promise not a lie.
TL;DR stack
- 1 HTML file per client (yes, still in 2026)
- Netlify for hosting, HTTPS, forms, analytics
- Python 3 script that generates topical SEO cluster pages
- Playwright for cross-viewport QA screenshots
- Claude for first drafts (I rewrite ~60% of the output)
Zero frameworks. Zero build step beyond netlify deploy. Zero CMS.
Why no framework
I'm not anti-framework. I'm anti-framework-for-brochure-sites. Ninety percent of small business sites are:
- Hero
- Services
- Portfolio
- Pricing
- Contact form
- Footer
A single HTML file with Tailwind utility classes compiled in via CDN ships in 3 days. The same site in Next.js ships in 6 and costs me Vercel time I can't bill for.
The rule I use: if the client updates content less than once a month, they don't need a CMS.
The Python SEO generator (the actual code)
This is the piece that took me from "pretty site" to "site that Google can place in context." The idea: generate a small cluster of topically related static pages so each page's internal links reinforce the main site's authority.
# seo_gen.py — minimal version, ~80 lines
from pathlib import Path
import json, html
TEMPLATE = """<!doctype html>
<html lang="{lang}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>{title}</title>
<meta name="description" content="{description}">
<link rel="canonical" href="{canonical}">
<script type="application/ld+json">{schema}</script>
<link rel="stylesheet" href="/style.css">
</head>
<body>
{header}
<main>
<h1>{h1}</h1>
{body}
<aside class="related">
<h2>Related</h2>
<ul>{related_links}</ul>
</aside>
</main>
{footer}
</body>
</html>
"""
def render_page(page, all_pages, header, footer):
related = [p for p in all_pages if p["slug"] in page["internal_links"]]
related_links = "".join(
f'<li><a href="/{p["slug"]}">{html.escape(p["title"])}</a></li>'
for p in related
)
schema = json.dumps({
"@context": "https://schema.org",
"@type": "Article",
"headline": page["h1"],
"description": page["description"],
})
return TEMPLATE.format(
lang=page.get("lang", "ko"),
title=page["title"],
description=page["description"],
canonical=f'https://zamhwa-studio.netlify.app/{page["slug"]}',
schema=schema,
header=header,
h1=page["h1"],
body=page["body_html"],
related_links=related_links,
footer=footer,
)
def build(pages_json="pages.json", out_dir="dist"):
pages = json.loads(Path(pages_json).read_text(encoding="utf-8"))
header = Path("partials/header.html").read_text(encoding="utf-8")
footer = Path("partials/footer.html").read_text(encoding="utf-8")
out = Path(out_dir)
out.mkdir(exist_ok=True)
sitemap = ['<?xml version="1.0" encoding="UTF-8"?>',
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">']
for page in pages:
(out / f'{page["slug"]}.html').write_text(
render_page(page, pages, header, footer), encoding="utf-8"
)
sitemap.append(
f'<url><loc>https://zamhwa-studio.netlify.app/{page["slug"]}</loc></url>'
)
sitemap.append("</urlset>")
(out / "sitemap.xml").write_text("\n".join(sitemap), encoding="utf-8")
if __name__ == "__main__":
build()
pages.json looks like this:
[
{
"slug": "guide/cost-2026",
"title": "홈페이지 제작 비용 2026 | the vibe company",
"h1": "2026 홈페이지 제작 비용 완전 가이드",
"description": "실제 견적서와 패키지별 단가.",
"body_html": "<p>...</p>",
"internal_links": ["guide/domain", "guide/seo-checklist"]
}
]
Run python seo_gen.py → dist/ has 8 HTML files plus sitemap.xml → netlify deploy --prod --dir=dist. Done.
The live cluster is here if you want to see the output:
- /guide/cost-2026 — cost breakdown
- /guide/domain — domain decision tree
- /guide/seo-checklist — the checklist I actually use
The Playwright QA runner
I never deploy without running this:
// qa.js
const { chromium } = require('playwright');
const viewports = [
{ name: 'mobile', width: 375, height: 812 },
{ name: 'tablet', width: 768, height: 1024 },
{ name: 'desktop', width: 1440, height: 900 },
];
const urls = [
'https://zamhwa-studio.netlify.app/',
'https://zamhwa-studio.netlify.app/guide/cost-2026',
'https://zamhwa-studio.netlify.app/industries/cafe',
];
(async () => {
const browser = await chromium.launch();
for (const vp of viewports) {
const ctx = await browser.newContext({ viewport: vp });
for (const url of urls) {
const page = await ctx.newPage();
await page.goto(url, { waitUntil: 'networkidle' });
const slug = url.replace(/[^a-z0-9]/gi, '_');
await page.screenshot({ path: `shots/${vp.name}_${slug}.png`, fullPage: true });
await page.close();
}
await ctx.close();
}
await browser.close();
})();
Before every client call I flip through the shots. In the last month this has caught iOS Safari button cutoff, a mobile-only stacked nav regression, and one CDN image cache that was serving a blurry variant.
Where Claude actually helps (and doesn't)
Helps a lot:
- First draft of copy in both EN and KR
- Writing Python glue scripts like the one above
- Debugging CSS weirdness at 2am
- Explaining error messages from Netlify build logs
Does not help:
- Deciding the pricing tiers
- Choosing what goes above the fold for a specific client
- Anything involving the client's actual business
The honest version: it's a human-run shop where the human got faster. That's enough.
What this site ranks for (after 4 weeks)
- "홈페이지 제작 비용" (home-page production cost, Korean) — page 2 on Naver
- "카페 홈페이지 제작" — page 1 on Naver, position 6
- "netlify korean agency" — position 2 on Google
Not massive, but the trajectory is up, and every cluster page adds link equity to the root.
If you want to poke at the live site
Main: zamhwa-studio.netlify.app
Pricing guide with real numbers: cost-2026
Happy to answer stack questions in the comments. Especially interested in any Naver SEO specifics that aren't obvious from the Google side — that's still the part I'm learning in public.
Top comments (0)