Remember GeoCities? Angelfire? Tripod? The 1990s web was a magical chaos of Comic Sans, animated GIFs, MIDI music, visitor counters, and guestbooks. For the Kiroween hackathon, I decided to resurrect that era—and learned a lot about progressively adopting AI development tools along the way.
Live Demo: kiroween-mu.vercel.app
GitHub: github.com/TheIllusionOfLife/kiroween
The Project: 90s Website Generator
The app lets anyone create authentic 1990s-style personal homepages:
- 6 themes (Neon, Space, Rainbow, Matrix, GeoCities, Angelfire)
- 6 template presets ("90s Gamer Kid", "Elite Hacker", etc.)
- Real-time live preview
- Background music and sound effects
- Working guestbook with real-time updates
- Visitor tracking
- Download as standalone HTML
- Guest mode (no sign-in required)
Tech Stack & Architecture
Frontend: Next.js 16, React 19, TypeScript, Tailwind CSS, shadcn/ui, Zustand
Backend: Convex (real-time database), Clerk (authentication)
Testing: Vitest, fast-check (property-based testing)
Deployment: Vercel, Convex Cloud
The architecture follows a clean separation:
┌─────────────────────────────────────┐
│ Pages (Next.js App Router) │
└─────────────────┬───────────────────┘
│
┌─────────────────▼───────────────────┐
│ Components (React + shadcn) │
└─────────────────┬───────────────────┘
│
┌─────────────────▼───────────────────┐
│ State (Zustand) + Generator │
└─────────────────┬───────────────────┘
│
┌─────────────────▼───────────────────┐
│ Backend (Convex real-time DB) │
└─────────────────────────────────────┘
Progressive Adoption: My Journey with Kiro
Here's the key insight: Adopt Kiro's featuress progressively as you encounter specific problems.
Phase 1: Vibe Coding (Exploration)
I started with pure vibe coding—just chatting with Kiro to explore the problem space. This produced two experimental versions in vibe_coding/version1 (vanilla JS) and vibe_coding/version2 (Next.js).
These versions were messy but valuable. They helped me understand:
- What features a 90s site generator actually needs
- How to generate inline HTML with embedded CSS/JS
- The tricky parts (iframe popups, audio autoplay policies)
Lesson: Vibe coding is great for exploration. Don't skip it.
Phase 2: Spec-Driven Development (Structure)
Once I understood what I was building, I formalized it with Kiro's spec-driven workflow:
-
Requirements (
requirements.md): 24 user stories with EARS-formatted acceptance criteria -
Design (
design.md): Architecture, data models, 16 correctness properties -
Tasks (
tasks.md): 22 implementation tasks with requirement references
Example EARS requirement:
WHEN a user provides a name, hobby, and optional email
THEN the System SHALL generate a complete HTML website incorporating these details
Example correctness property:
Property 1: Site generation incorporates all configuration
*For any* valid site configuration, the generated HTML should contain
all specified values and include/exclude features according to the toggles.
Lesson: Specs eliminate ambiguity. You know exactly what "done" means.
Phase 3: Steering Docs (Consistency)
After a few commits, I noticed Kiro's suggestions weren't always consistent with my coding style. I added steering docs:
-
tech.md- Tech stack, commands, code conventions -
structure.md- Directory layout, naming conventions -
coding-standards.md- TypeScript patterns, React conventions -
testing-guide.md- Property-based testing patterns with fast-check
The testing-guide.md was especially valuable. It taught Kiro my property testing patterns:
// **Feature: 90s-website-generator, Property 1: Site generation**
it('Property 1: Site generation incorporates all configuration', () => {
fc.assert(
fc.property(
fc.record({
name: fc.string({ minLength: 1, maxLength: 50 }),
hobby: fc.string({ minLength: 1, maxLength: 100 }),
theme: fc.constantFrom('neon', 'space', 'rainbow'),
}),
(config) => {
const html = generateSiteHTML(config);
expect(html).toContain(config.name);
}
),
{ numRuns: 100 }
);
});
Lesson: Steering docs compound over time. Document patterns as you discover them.
Phase 4: Agent Hooks (Automation)
After forgetting to run tests a few times (and pushing broken code), I added 7 agent hooks:
- Run Tests on Save - Auto-runs tests when any .ts/.tsx file is saved
- Run Property Tests on Test Change - Ensures property tests pass
- Check Convex Schema on Change - Reminds to update types when schema changes
- Security Check on User Input - Prompts security review when touching input handling
- Task Completion Reminder - Prompts to update task status
- Update Spec on Requirements Change - Keeps spec documents in sync
- Validate Before Commit - Manual hook for pre-commit validation
The security hook was a lifesaver—it reminded me to add HTML escaping to the site generator when I might have shipped an XSS vulnerability.
Lesson: Hooks are "set and forget" safety nets. Add them when you keep forgetting something.
Phase 5: MCP (Extended Capabilities)
Finally, I added the Playwright MCP for end-to-end testing. This let Kiro:
- Navigate to the deployed site
- Test complete user flows
- Verify the guestbook actually works
- Test authentication flows
Lesson: Add MCP when you hit a wall that unit tests can't solve.
Implementing 90s Web Features
Now let's talk about the fun part—recreating authentic 90s web features with modern tech.

The HTML Generator
The core of the app is lib/site-generator.ts—a function that takes a config object and returns a complete HTML string with inline CSS and JavaScript.
export function generateSiteHTML(config: SiteConfig): string {
const theme = themes[config.theme];
return `<!DOCTYPE html>
<html>
<head>
<title>${escapeHtml(config.name)}'s Homepage</title>
<style>
body {
background: ${theme.background};
color: ${theme.textColor};
font-family: 'Comic Sans MS', cursive;
}
/* ... 200+ lines of inline CSS ... */
</style>
</head>
<body>
${generateHeader(config)}
${generateAboutSection(config)}
${generateLinksSection(config)}
${generateGuestbookSection(config)}
${generateFooter(config)}
<script>
${generateJavaScript(config)}
</script>
</body>
</html>`;
}
Key challenges:
- Everything inline: No external files, so the downloaded HTML works standalone
-
XSS prevention: User inputs must be escaped with
escapeHtml() - Iframe detection: Popups must be suppressed in preview mode
Authentic 90s Visual Effects
Rainbow text with CSS animations:
@keyframes rainbow {
0% { color: red; }
17% { color: orange; }
33% { color: yellow; }
50% { color: green; }
67% { color: blue; }
83% { color: indigo; }
100% { color: violet; }
}
.rainbow-text {
animation: rainbow 3s infinite;
}
Marquee scrolling (yes, it still works):
<marquee behavior="scroll" direction="left">
Welcome to my homepage! You are visitor #${visitorCount}!
</marquee>
</html>
Blinking text:
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
.blink { animation: blink 1s infinite; }
The Guestbook System
The guestbook was surprisingly complex. It needed to:
- Store entries in a real database (Convex)
- Update in real-time when new entries are added
- Validate input lengths (name 1-50 chars, message 1-500 chars)
- Display entries chronologically
Convex schema:
guestbookEntries: defineTable({
siteId: v.id("sites"),
name: v.string(),
message: v.string(),
email: v.optional(v.string()),
website: v.optional(v.string()),
createdAt: v.number(),
}).index("by_site", ["siteId"]),
The real-time updates come for free with Convex's reactive queries:
const entries = useQuery(api.guestbook.getEntries, { siteId });
// Automatically updates when new entries are added!
Iframe Popup Suppression
Generated sites can have alert() and confirm() dialogs—authentic 90s behavior! But these break the live preview iframe.
Solution: Detect iframe context and suppress popups:
const isInIframe = window.self !== window.top;
if (!isInIframe && config.addPopups) {
alert('Welcome to ' + config.name + '\\'s homepage!');
window.onbeforeunload = function() {
return 'Are you sure you want to leave this awesome page?';
};
}
Audio Support
90s sites had MIDI music! We support background music and sound effects:
if (config.bgmTrack) {
html += `
<audio id="bgm" autoplay loop>
<source src="${config.bgmTrack}" type="audio/mpeg">
</audio>
<div class="audio-controls">
<button onclick="document.getElementById('bgm').paused ?
document.getElementById('bgm').play() :
document.getElementById('bgm').pause()">
🎵 Toggle Music
</button>
</div>
`;
}
Results
The progressive adoption approach worked:
- 35 tests passing (13 property-based, 100+ iterations each)
- 24 requirements formally specified
- 16 correctness properties validated
- 7 agent hooks automating the workflow
- Production deployment at kiroween-mu.vercel.app
Key Takeaways
- Start with vibe coding to explore the problem space
- Add specs when you know what you're building
- Add steering docs when you want consistency
- Add hooks when you keep forgetting things
- Add MCP when you hit capability walls
- Don't adopt everything at once—each layer solves a specific problem
The 90s web is back. And it's tested.
Built for Kiroween hackathon. Try it at kiroween-mu.vercel.app

Top comments (0)