Most AI-generated MDX templates start beautiful and become unreadable by article number 30 — too many one-off components, inconsistent spacing, layouts that fight the content. This article gives 10 layout patterns that hold up past 1000 articles, the prompt structure that gets them out of an LLM cleanly, and the Astro plus Shiki config that locks the vocabulary in at build time.
TL;DR
- Pick 5-7 layout patterns, define each as one component or one CSS class, and forbid every other ad-hoc styled block.
- Drive an LLM with three inputs: the fixed pattern list, one real article as a style anchor, and the new topic. Skip the anchor and you get generic SaaS-blog voice.
- Enforce the vocabulary in two places: a Zod schema on your Astro content collection (frontmatter) and a ~30-line lint script run as
prebuild(body structure). - For prose-heavy sites, build raw: 5 components plus ~200 lines of CSS beat a component library. Highlight code with Shiki at build time (zero client JS), not Prism in the browser.
Why a fixed vocabulary matters
MDX is Markdown plus components, so an article template is really two decisions: which Markdown elements you style globally, and which dedicated components you expose. AI handles both well, but only under constraint. Ask for “a beautiful article template” and you get gradient backgrounds and Tailwind soup. Give it a fixed component list plus a content sample and it produces something you can actually maintain. The 10 patterns below are the ones we run across a 1200-article bilingual site.
The cost of template drift is non-linear. At 50 articles, inconsistency feels like minor polish work. At 500 articles, each drift becomes a refactor that touches hundreds of files. Past 1000 articles you stop refactoring and start papering over the drift with style overrides, which is the worst of both worlds. Define the vocabulary early.
When this applies to you
- You are scaffolding a new content site, or refreshing the layout of an existing one.
- Your current template has 6+ ad-hoc styled divs per article and you keep re-tweaking them.
- New writers (or AI) keep inventing heading structures because the template has no clear vocabulary.
- You want one template to carry tutorials, troubleshooting, listicles, and reviews.
Quick verdict
Pick 5-7 core patterns from the list below. Define each as a single component, or a CSS class on a standard HTML element. Forbid all other ad-hoc styled blocks. Let AI generate articles inside this fixed vocabulary instead of inventing a new shape per article.
The 10 patterns
- Step list: numbered steps with a bold lead phrase. Use an ordered list with a
<strong>first sentence. No dedicated component needed. - Compare table: 3 columns max (option, when to use, when to skip). Render as a standard Markdown table; style globally for striping and small-caps headers.
- Decision tree: a bullet list with conditional indentation. Cap nesting at 2 levels; anything deeper becomes its own section.
- Verdict box: a single-paragraph
<aside>with a left border. One per article, near the top. - Code-with-caption: a code block immediately followed by a one-line italic description. No special component; rely on a CSS sibling selector.
- Diagnostic table: symptom, cause, fix. Three columns, rendered as a Markdown table. Used in every troubleshooting article.
- Inline checklist: a task list with
- [ ]syntax. Useful for “before you start” sections. - Quote block: for real screenshots of platform messages or rejection reasons. Style as a callout, not a generic blockquote.
- Comparison example: a Bad/Good code or text pair. Render with a
textcode block andBad:/Good:prefixes. Keep it dead simple. - Related links cluster: a bullet list of 3-6 links under
## Related, always at the end, the same shape every article.
Prompt structure that produces clean output
Send the LLM three things: the fixed pattern list, a single real article in MDX as a sample, and the new article topic. Anchor the prompt with these constraints:
- Use only the 10 patterns listed. Do not invent components or styled divs.
- Headings: ## level only, no ### unless content has 4+ subsections.
- No emoji, no exclamation marks, no marketing voice.
- First paragraph: 2-3 sentences, no heading.
- Last section is always "## Related" with 3-6 links.
- Tone matches the sample article.
Without the sample article as an anchor, the model averages over its training data and you get generic SaaS-blog voice. With the sample, it copies your house style, usually well enough that a 10-minute edit is all each article needs.
Both current frontier coding models do this reliably as of June 2026: Claude Opus 4.7 and Sonnet 4.6 hold the constraint list across long articles, and GPT-5.5 (Thinking) is comparable. Sonnet 4.6 is the cost-effective workhorse here — at $3 / $15 per 1M input/output tokens it is roughly half the cost of Opus 4.7 ($5 / $25) and fine for templated body generation. Save Opus for the few articles where structure is genuinely hard.
How to enforce the vocabulary
Enforcement lives in two layers, and you want both.
Frontmatter — enforce with a Zod schema. Astro content collections (Astro 6 ships Zod 4 schemas, as of June 2026) validate every article’s frontmatter at build time. Define the keys once, mark them required, and a missing or mistyped field fails the build before a single page renders:
const articles = defineCollection({
loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/articles" }),
schema: z.object({
title: z.string().max(70),
description: z.string().min(50).max(160),
tags: z.array(z.string()).min(1),
translationKey: z.string(),
}),
});
Body structure — enforce with a lint script. Zod cannot see inside the MDX body, so add a ~30-line script that scans every file: counts ## headings, checks the last section is ## Related, flags components outside the allowlist, and flags raw <div> tags in prose. Run it as a prebuild step so a failing file fails the build. Keep an examples/ folder with one MDX per pattern as living documentation the model can read when generating new content. For each new article PR, attach a one-line checklist: which patterns were used, which were not, and why.
Migrating a legacy template
- Inventory every component used across existing articles. Most legacy templates carry 30+ components, half of which appear exactly once.
- Map each existing component to one of the 10 patterns, or mark it for deletion. Target a 5:1 reduction.
- Run a codemod pass that swaps deprecated components for their standard equivalents. The model writes the codemod well once you hand it 3-5 before/after examples.
- Migrate in batches of 50-100 articles. Build after each batch; an unexpected break in one article usually means the codemod missed an edge case.
Build raw vs. component library
For an article-heavy content site, raw wins on weight and maintainability. The table below is the choice in practice.
| Approach | Client JS shipped | Maintenance at 1000+ articles | Best for |
|---|---|---|---|
| 5 components + ~200 lines CSS | Effectively none | Low — one vocabulary to police | Prose / content sites |
| shadcn/ui or similar | Per-component bundle | Higher — library churn + unused parts | App dashboards, interactive UI |
| Per-article styled divs | Varies | Worst — drift compounds | Nothing; avoid |
Syntax highlighting: Shiki, configured once
Highlight code at build time with Shiki, Astro’s default highlighter as of June 2026, so the page ships zero client JS for highlighting. One configuration gotcha bites everyone: markdown.shikiConfig in astro.config.mjs applies to .md and .mdx code fences, but the standalone <Code /> component does not inherit it. Configure dual light/dark themes through CSS variables so theme switching costs nothing at runtime:
// astro.config.mjs
markdown: {
shikiConfig: {
themes: { light: "github-light", dark: "github-dark" },
},
}
Astro emits the highlighted markup on the .astro-code element (not .shiki); target that class in your dark-mode CSS. The default single theme is github-dark if you set nothing.
Common mistakes
- Letting the model invent components for “visual variety”. Variety kills consistency; the pattern list is the variety.
- Asking for “a beautiful template” with no constraints. You get gradients, animation, and three font families.
- Shipping generated components without testing them in MDX first. Some libraries break on MDX braces; a single
{value}in prose will fail the build, so validate one article before rolling out. - Skipping the sample article. The model defaults to whatever blog template was popular in its training set, not your style.
- Dropping decorative dividers (
---) between every section. MDX renders them as<hr>— visual noise with no information.
FAQ
Should I let the model design the components directly? For the first pass, yes. For final styling, do it yourself or pair a designer with the model. AI proposes good shapes but is weaker at picking spacing and color systems.
How do I prevent template drift across 1000 articles?
Two layers. A Zod schema on the content collection catches frontmatter problems at build time; a ~30-line lint script run as prebuild checks body structure (heading levels, a closing ## Related, no components outside the allowlist). Together they catch the large majority of drift before deploy.
Can I use the same template for English and Chinese? Yes. Define spacing, font stack, and line height once. The Chinese version usually wants a slightly tighter line height and a different font fallback; everything else stays identical.
What about dark mode? Define semantic tokens (background, foreground, muted, accent) and have every component read them. Never hardcode a color in a component.
Should I use shadcn or build raw? For a prose-heavy content site, build raw. shadcn/ui is excellent for app UI but adds weight you do not need for articles. About 200 lines of CSS and 5 components beat a component library here.
Where does syntax highlighting belong?
Use Shiki at build time, not Prism in the browser. Shiki ships zero JS to the client and renders identically. Configure two themes and switch via CSS variables. Remember the standalone <Code /> component needs its own theme config — it ignores markdown.shikiConfig.
Do I need an MDX provider for global components?
Only if you actually share components like Aside or Callout across many files. For a 5-component vocabulary, importing each per file is fine and keeps the build simpler.
Should I version the template?
Yes, informally. Note major template changes in a CHANGELOG.md so you know which articles were authored under v1 vs v2. It helps when debugging style anomalies on older articles.
Related
- Markdown vs MDX for a content site
- Astro content collections intro
- AI bulk translation of an existing content site
- Build a content site end-to-end with Claude Code
- Astro best use cases
- Astro syntax highlighting docs
Tags: #Indie dev #ai-assisted #building #MDX