Why AI-Generated Landing Pages All Look the Same and the Solution
For solo developers who handle everything from planning to deployment on their own, tools like Cursor and Claude Code feel like relief pitchers. Type a few prompts and a plausible-looking webpage pops up in just 10 minutes.
The problem comes next. You hit the deploy button and look at the screen with a sense of déjà vu. Purple gradient buttons, the Inter font, a 3-column card layout smack in the middle. It reeks of a template you've seen everywhere. It looks clean on the surface, but it doesn't lead to user payments or sign-ups. People instantly spot pages that look like they were churned out of a factory and close the tab.
LLMs operate on statistical averages. Without rules, they regress to the most commonly used Tailwind default presets and Shadcn UI defaults on the web. The moment you toss out a prompt like "Create a clean and sophisticated landing page," the AI spews out unidentifiable inline pixel values like w-[320px] and top-[117px], messing up the layout.
Trying to fix this issue just by tweaking prompts gets exhausting. You need to build a pipeline that enforces brand design rules every time code is generated, and physically blocks violating code at the commit stage.
Writing Rule Files to Tie AI Agents' Hands and Feet
Cursor reads .cursor/rules/*.mdc, Claude Code reads CLAUDE.md, and open-source tools read AGENTS.md. If you define your brand design tokens in these files, the AI is prevented from using arbitrary styles right from the code generation stage.
If a rule file exceeds 500 lines, the context cost for reading it on every session increases, and the model's instruction-following rate drops. It needs to be written clearly within around 200 to 300 lines.
Create .cursor/rules/design-system.mdc in your project root and add the following content.
`yaml
description: Design System Rules and Custom Token Enforcement
globs: ["src/app//*.tsx", "src/components//.tsx", "src/styles/**/.css"]
alwaysApply: false
Brand Design System Constraints
Universal Rules
- MUST NOT use arbitrary Tailwind utility classes such as
bg-[#123456] or h-[117px].
- MUST use predefined semantic Design Tokens for colors, spacing, and typography.
- MUST run
pnpm lint:style to verify token compliance before completing tasks.
Design Token Reference Map
Color Tokens
- Surface Background:
var(--color-bg-primary) (Tailwind: bg-brand-primary)
- Surface Secondary:
var(--color-bg-secondary) (Tailwind: bg-brand-secondary)
- Text Main:
var(--color-text-main) (Tailwind: text-brand-main)
- Text Muted:
var(--color-text-muted) (Tailwind: text-brand-muted)
- Accent Primary:
var(--color-accent-default) (Tailwind: bg-brand-accent)
Spacing Scale (8pt Grid Standard)
var(--space-1): 0.25rem (4px)
var(--space-2): 0.5rem (8px)
var(--space-4): 1.0rem (16px)
var(--space-6): 1.5rem (24px)
var(--space-8): 2.0rem (32px)
Typography Rules
- Main Heading (H1): Class
text-brand-h1 -> Font: Inter, Weight: 700, Size: 2.5rem, Tracking: -0.02em
- Body Text: Class
text-brand-body -> Font: Inter, Weight: 400, Size: 1.0rem, Leading: 1.5
`
Adding these rules significantly reduces the frequency with which the AI directly injects bracketed arbitrary values or hex codes.
Blocking Hardcoded Style Commits with Stylelint and Husky
Prompts alone are not enough. AI often ignores rules and secretly mixes in inline pixel values. By combining Stylelint and Git pre-commit hooks, you can block hardcoded styles from entering the repository.
Install the tools and initialize Husky.
`bash
pnpm add -D husky lint-staged stylelint stylelint-declaration-strict-value
npx husky init
`
Create stylelint.config.mjs in the project root. This configuration throws a build error if any arbitrary color codes or pixel values sneak in.
`javascript
import type { Config } from "stylelint";
export default {
plugins: ["stylelint-declaration-strict-value"],
rules: {
"scale-unlimited/declaration-strict-value": [
["/color/", "font-size", "/margin/", "/padding/"],
{
ignoreVariables: false,
ignoreFunctions: false,
ignoreKeywords: {
"": ["transparent", "inherit", "currentColor", "auto", "0"]
},
message: "Design System Violation: Hardcoded value for '{property}' is forbidden. Use CSS Design Tokens instead."
}
]
}
} satisfies Config;
`
Add lint-staged configuration to your package.json.
`json
{
"lint-staged": {
"*.{css,scss,tsx,jsx}": [
"stylelint --fix",
"eslint --max-warnings=0"
]
}
}
`
Register the following single line in the .husky/pre-commit file.
`bash
npx lint-staged
`
Now, every time you run git commit, it statically analyzes the staged code. If there is code like margin: 17px that the AI snuck in arbitrarily, the commit itself fails. You can save about 5 hours a week previously spent on style fixes.
Automating Visual Regression Testing with Playwright
Static analysis only checks text rules. Issues where elements overlap or navigation menus break on mobile screens require spinning up an actual browser to check. Since you can't manually shrink and stretch the viewport every time, you can automate snapshot comparisons with Playwright.
Create the playwright.config.ts file.
`typescript
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests/visual',
snapshotPixelRatioTemplate: '{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{ext}',
expect: {
toHaveScreenshot: {
maxDiffPixelRatio: 0.01,
threshold: 0.2,
animations: 'disabled',
},
},
webServer: {
command: 'pnpm dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120 * 1000,
},
projects: [
{ name: 'Desktop Chrome', use: { ...devices['Desktop Chrome'] } },
{ name: 'Mobile Safari', use: { ...devices['iPhone 13'] } },
],
});
`
Write a test script (tests/visual/landing-page.spec.ts) that validates 5 core UI locations.
`typescript
import { test, expect } from '@playwright/test';
test.describe('Visual Regression Guardrails', () => {
test.beforeEach(async ({ page }) => {
await page.goto('http://localhost:3000');
await page.evaluate(() => document.fonts.ready);
});
test('TC1: Desktop Hero Section Rendering', async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 900 });
const heroSection = page.locator('section#hero');
await expect(heroSection).toBeVisible();
await expect(heroSection).toHaveScreenshot('hero-desktop.png', { maxDiffPixelRatio: 0.01 });
});
test('TC2: Mobile Navigation Menu Viewport Rendering', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 });
const navBar = page.locator('header#main-nav');
await expect(navBar).toHaveScreenshot('nav-mobile.png');
});
test('TC3: Pricing Card Grid Alignment', async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 });
const pricingGrid = page.locator('div#pricing-cards');
await expect(pricingGrid).toHaveScreenshot('pricing-grid.png', {
mask: [page.locator('.dynamic-price-timestamp')]
});
});
test('TC4: Main CTA Button Hover State', async ({ page }) => {
const ctaButton = page.locator('button#primary-cta');
await ctaButton.hover();
await expect(ctaButton).toHaveScreenshot('cta-button-hover.png');
});
test('TC5: Login Modal Layout', async ({ page }) => {
await page.click('button#open-login-modal');
const modalDialog = page.locator('div[role="dialog"]');
await expect(modalDialog).toBeVisible();
await expect(modalDialog).toHaveScreenshot('login-modal.png');
});
});
`
Register the commands in package.json.
`json
{
"scripts": {
"test:visual": "playwright test",
"test:visual:update": "playwright test --update-snapshots"
}
}
`
By typing a single line pnpm test:visual in the terminal, you can catch broken layouts across desktop and mobile viewports in just one minute. You can eliminate the tedious task of manually adjusting screen sizes and straining your eyes during reviews.
By establishing a structure where rule files control AI input, Git hooks block the inflow of incorrect styles, and Playwright checks for broken screens, you can maintain design consistency even in solo development projects.