DEV Community

Yuya Mukai
Yuya Mukai

Posted on

Progressive Adoption of Kiro for 90s Website Generator

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
Enter fullscreen mode Exit fullscreen mode

The architecture follows a clean separation:

┌─────────────────────────────────────┐
│     Pages (Next.js App Router)      │
└─────────────────┬───────────────────┘
                  │
┌─────────────────▼───────────────────┐
│     Components (React + shadcn)     │
└─────────────────┬───────────────────┘
                  │
┌─────────────────▼───────────────────┐
│     State (Zustand) + Generator     │
└─────────────────┬───────────────────┘
                  │
┌─────────────────▼───────────────────┐
│     Backend (Convex real-time DB)   │
└─────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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)

A screenshot at this point

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:

  1. Requirements (requirements.md): 24 user stories with EARS-formatted acceptance criteria
  2. Design (design.md): Architecture, data models, 16 correctness properties
  3. 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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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 }
  );
});
Enter fullscreen mode Exit fullscreen mode

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:

  1. Run Tests on Save - Auto-runs tests when any .ts/.tsx file is saved
  2. Run Property Tests on Test Change - Ensures property tests pass
  3. Check Convex Schema on Change - Reminds to update types when schema changes
  4. Security Check on User Input - Prompts security review when touching input handling
  5. Task Completion Reminder - Prompts to update task status
  6. Update Spec on Requirements Change - Keeps spec documents in sync
  7. 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.
Authentic 90s Visual Effects

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>`;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Marquee scrolling (yes, it still works):

<marquee behavior="scroll" direction="left">
  Welcome to my homepage! You are visitor #${visitorCount}!
</marquee>
</html>
Enter fullscreen mode Exit fullscreen mode

Blinking text:

@keyframes blink {
  0%, 50% { opacity: 1; }
  51%, 100% { opacity: 0; }
}
.blink { animation: blink 1s infinite; }
Enter fullscreen mode Exit fullscreen mode

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"]),
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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?';
  };
}
Enter fullscreen mode Exit fullscreen mode

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>
  `;
}
Enter fullscreen mode Exit fullscreen mode

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

  1. Start with vibe coding to explore the problem space
  2. Add specs when you know what you're building
  3. Add steering docs when you want consistency
  4. Add hooks when you keep forgetting things
  5. Add MCP when you hit capability walls
  6. 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)