Introduction
I found that lots of excellent DESIGN.md examples are already available in this awesome repo:
Light introduction: this repository is a curated collection of ready-to-use DESIGN.md files extracted from real websites, designed to help AI agents generate UI with better visual consistency.
Big credit to the maintainers and contributors of awesome-design-md for pushing this direction forward and making design-system knowledge easier to reuse.
That said, sometimes I want to build something not included in the repository. Especially when I am in an old-school mood and want a retro flavor that is uniquely mine. ;)
So in this tutorial, we build a fun experiment: a custom DESIGN.md generator app in C#.
The app will:
- Crawl a target page.
- Extract visual tokens (colors, fonts, semantic classes).
- Ask LLM to synthesize a full
DESIGN.md. - Ask LLM to generate a verification
index.htmlfrom thatDESIGN.md. - Save both files locally.
We will use:
.NET 8OllamaSharphttp://localhost:11434nemotron-3-super:cloud
What We Are Building
Target URL
-> DesignMdGenerator.CrawlAndExtractMetadataAsync
-> tokenized design metadata (title/colors/fonts/classes)
-> Ollama (nemotron-3-super:cloud)
-> DESIGN.md
-> Ollama verification round
-> index.html
-> save to Output/DesignMdGenerator
Prerequisites
- Visual Studio or VS Code
- .NET 8 SDK
- Ollama running locally
- Model available in your Ollama environment
Run Ollama with the model used in this demo:
ollama run nemotron-3-super:cloud
If your Ollama service is already running at http://localhost:11434, you are good.
Step 1: Create a New Console App
dotnet new console -n DesignMdGeneratorDemo
cd DesignMdGeneratorDemo
dotnet add package OllamaSharp
Step 2: Project File (Reference)
Your .csproj should include at least:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OllamaSharp" Version="5.4.25" />
</ItemGroup>
</Project>
Step 3: Add Program.cs
This version is focused only on the DESIGN.md demo flow.
using DesignMdGeneratorDemo.Services;
Console.OutputEncoding = System.Text.Encoding.UTF8;
string targetUrl = args.Length > 0 ? args[0] : "https://www.yahoo.com";
string modelName = args.Length > 1 ? args[1] : "nemotron-3-super:cloud";
string ollamaApiUrl = args.Length > 2 ? args[2] : "http://localhost:11434";
var generator = new DesignMdGenerator(ollamaApiUrl, modelName);
var outputDirectory = Path.Combine(Directory.GetCurrentDirectory(), "Output", "DesignMdGenerator");
Console.WriteLine("Custom DESIGN.md Generator Demo");
Console.WriteLine("-------------------------------");
Console.WriteLine($"Target URL : {targetUrl}");
Console.WriteLine($"Model : {modelName}");
Console.WriteLine($"Ollama URL : {ollamaApiUrl}");
Console.WriteLine();
var result = await generator.GenerateArtifactsAsync(targetUrl, outputDirectory);
Console.WriteLine("Generation complete.");
Console.WriteLine($"DESIGN.md : {result.DesignMarkdownPath}");
Console.WriteLine($"index.html: {result.VerificationHtmlPath}");
if (!string.IsNullOrWhiteSpace(result.WarningMessage))
{
Console.WriteLine();
Console.WriteLine("Warning:");
Console.WriteLine(result.WarningMessage);
}
Step 4: Add Services/DesignMdGenerator.cs
Create Services folder, then add this complete service class:
using OllamaSharp;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
namespace DesignMdGeneratorDemo.Services;
public sealed class DesignMdGenerationResult
{
public required string TargetUrl { get; init; }
public required string DesignMarkdown { get; init; }
public required string DesignMarkdownPath { get; init; }
public required string VerificationHtml { get; init; }
public required string VerificationHtmlPath { get; init; }
public required DesignSiteMetadata Metadata { get; init; }
public string? WarningMessage { get; init; }
}
public sealed class DesignSiteMetadata
{
public string Title { get; init; } = string.Empty;
public IReadOnlyList<string> Colors { get; init; } = [];
public IReadOnlyList<string> Fonts { get; init; } = [];
public IReadOnlyList<string> StructuralClasses { get; init; } = [];
public IReadOnlyList<string> StylesheetUrls { get; init; } = [];
}
public sealed class DesignMdGenerator
{
private static readonly Regex TitleRegex = new("<title[^>]*>(?<title>.*?)</title>", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled);
private static readonly Regex StyleBlockRegex = new("<style[^>]*>(?<style>.*?)</style>", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled);
private static readonly Regex InlineStyleRegex = new("style\\s*=\\s*[\"'](?<value>.*?)[\"']", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled);
private static readonly Regex StylesheetHrefRegex = new("<link[^>]*rel=[\"'][^\"']*stylesheet[^\"']*[\"'][^>]*href=[\"'](?<href>[^\"']+)[\"'][^>]*>", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled);
private static readonly Regex ClassRegex = new("class\\s*=\\s*[\"'](?<value>[^\"']+)[\"']", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled);
private static readonly Regex HexColorRegex = new("#([0-9a-fA-F]{3,8})\\b", RegexOptions.Compiled);
private static readonly Regex RgbColorRegex = new("rgba?\\(\\s*\\d+\\s*,\\s*\\d+\\s*,\\s*\\d+\\s*(?:,\\s*[0-9.]+\\s*)?\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex FontFamilyRegex = new("font-family\\s*:\\s*(?<value>[^;}]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly string[] StructuralClassKeywords =
[
"btn", "button", "nav", "header", "footer", "card", "menu", "hero", "logo", "input"
];
private readonly OllamaApiClient _ollamaClient;
private readonly HttpClient _httpClient;
private readonly string _ollamaApiUrl;
private readonly string _modelName;
public DesignMdGenerator(
string ollamaApiUrl = "http://localhost:11434",
string modelName = "nemotron-3-super:cloud",
HttpClient? httpClient = null)
{
_ollamaApiUrl = ollamaApiUrl;
_modelName = modelName;
_ollamaClient = new OllamaApiClient(new Uri(ollamaApiUrl))
{
SelectedModel = modelName
};
_httpClient = httpClient ?? new HttpClient();
if (_httpClient.DefaultRequestHeaders.UserAgent.Count == 0)
{
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36");
}
}
public async Task<DesignMdGenerationResult> GenerateArtifactsAsync(
string targetUrl,
string? outputDirectory = null,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(targetUrl))
{
throw new ArgumentException("Target URL is required.", nameof(targetUrl));
}
targetUrl = NormalizeTargetUrl(targetUrl);
outputDirectory = string.IsNullOrWhiteSpace(outputDirectory)
? Directory.GetCurrentDirectory()
: outputDirectory;
Directory.CreateDirectory(outputDirectory);
var metadata = await CrawlAndExtractMetadataAsync(targetUrl, cancellationToken);
var warnings = new List<string>();
var designMarkdown = await GenerateDesignMarkdownAsync(metadata, targetUrl, warnings, cancellationToken);
var designMarkdownPath = Path.Combine(outputDirectory, "DESIGN.md");
await File.WriteAllTextAsync(designMarkdownPath, designMarkdown, cancellationToken);
var verificationHtml = await GenerateVerificationHtmlAsync(designMarkdown, targetUrl, warnings, cancellationToken);
var verificationHtmlPath = Path.Combine(outputDirectory, "index.html");
await File.WriteAllTextAsync(verificationHtmlPath, verificationHtml, cancellationToken);
return new DesignMdGenerationResult
{
TargetUrl = targetUrl,
DesignMarkdown = designMarkdown,
DesignMarkdownPath = designMarkdownPath,
VerificationHtml = verificationHtml,
VerificationHtmlPath = verificationHtmlPath,
Metadata = metadata,
WarningMessage = warnings.Count > 0 ? string.Join(Environment.NewLine, warnings) : null
};
}
public async Task<string> GenerateDesignMarkdownAsync(
DesignSiteMetadata metadata,
string targetUrl,
List<string>? warnings = null,
CancellationToken cancellationToken = default)
{
var prompt = BuildDesignMarkdownPrompt(metadata, targetUrl);
try
{
var markdown = await GenerateTextAsync(prompt, cancellationToken);
return markdown.Trim();
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Forbidden)
{
warnings?.Add($"Ollama generation was forbidden (403) for model '{_modelName}' at '{_ollamaApiUrl}'. Generated heuristic DESIGN.md fallback instead.");
return BuildFallbackDesignMarkdown(metadata, targetUrl);
}
}
public async Task<string> GenerateVerificationHtmlAsync(
string designMarkdown,
string targetUrl,
List<string>? warnings = null,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(designMarkdown))
{
throw new ArgumentException("Design markdown is required.", nameof(designMarkdown));
}
var prompt = BuildVerificationPrompt(designMarkdown, targetUrl);
try
{
var rawHtml = await GenerateTextAsync(prompt, cancellationToken);
return CleanModelCodeFence(rawHtml);
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Forbidden)
{
warnings?.Add($"Ollama HTML verification was forbidden (403) for model '{_modelName}'. Generated basic HTML fallback instead.");
return BuildFallbackVerificationHtml(targetUrl);
}
}
private async Task<string> GenerateTextAsync(string prompt, CancellationToken cancellationToken)
{
var builder = new StringBuilder();
await foreach (var chunk in _ollamaClient.GenerateAsync(prompt, context: null, cancellationToken: cancellationToken))
{
if (!string.IsNullOrWhiteSpace(chunk?.Response))
{
builder.Append(chunk.Response);
}
}
var response = builder.ToString();
if (string.IsNullOrWhiteSpace(response))
{
throw new InvalidOperationException("The model response was empty.");
}
return response;
}
private static string BuildDesignMarkdownPrompt(DesignSiteMetadata metadata, string targetUrl)
{
return $$"""
You are a senior design-system analyst.
Analyze the extracted website design metadata and return a complete DESIGN.md document.
Output markdown only. No preface. No code fences.
Target URL: {{targetUrl}}
Page title: {{metadata.Title}}
Extracted colors:
- {{string.Join("\n- ", metadata.Colors.DefaultIfEmpty("n/a"))}}
Extracted fonts:
- {{string.Join("\n- ", metadata.Fonts.DefaultIfEmpty("n/a"))}}
Structural classes:
- {{string.Join("\n- ", metadata.StructuralClasses.DefaultIfEmpty("n/a"))}}
External stylesheets sampled:
- {{string.Join("\n- ", metadata.StylesheetUrls.DefaultIfEmpty("n/a"))}}
Required sections and order:
1. Overview
2. Colors (Brand & Accent, Surface, Text, Semantic)
3. Typography (Font Family, Hierarchy table, Principles)
4. Layout (Spacing System, Grid & Container, Whitespace Philosophy)
5. Elevation & Depth (table)
6. Shapes (Border Radius Scale table, Photography & Illustrations)
7. Components (Top Navigation, Buttons, Cards, Inputs, Badges, Tabs, Footer)
8. Do and Don't guidance
9. Responsive Behavior (Breakpoints table, Touch Targets, Collapsing Strategy)
10. Iteration Guide
11. Known Gaps
Keep it implementation-focused and concrete.
""";
}
private static string BuildVerificationPrompt(string designMarkdown, string targetUrl)
{
return $$"""
You are a senior front-end engineer validating a DESIGN.md spec.
Build a single-file index.html from DESIGN.md.
Return HTML only.
Target brand reference: {{targetUrl}}
DESIGN.md:
---
{{designMarkdown}}
---
Constraints:
- Use Tailwind CSS CDN.
- Include navigation, hero, content/feature section, and footer.
- Configure theme values to reflect DESIGN.md tokens.
- Keep layout responsive.
- Keep all code in one HTML file.
""";
}
private static string CleanModelCodeFence(string modelOutput)
{
if (string.IsNullOrWhiteSpace(modelOutput))
{
return string.Empty;
}
var cleaned = modelOutput.Trim();
cleaned = Regex.Replace(cleaned, "^```
{% endraw %}
(?:html)?\\s*", string.Empty, RegexOptions.IgnoreCase);
cleaned = Regex.Replace(cleaned, "\\s*
{% raw %}
```$", string.Empty, RegexOptions.IgnoreCase);
return cleaned.Trim();
}
private async Task<DesignSiteMetadata> CrawlAndExtractMetadataAsync(string targetUrl, CancellationToken cancellationToken)
{
targetUrl = NormalizeTargetUrl(targetUrl);
var pageUri = new Uri(targetUrl, UriKind.Absolute);
var pageHtml = await _httpClient.GetStringAsync(pageUri, cancellationToken);
var cssBuilder = new StringBuilder();
foreach (Match styleMatch in StyleBlockRegex.Matches(pageHtml))
{
var css = styleMatch.Groups["style"].Value;
if (!string.IsNullOrWhiteSpace(css))
{
cssBuilder.AppendLine(css);
}
}
foreach (Match inlineStyleMatch in InlineStyleRegex.Matches(pageHtml))
{
var inlineStyle = WebUtility.HtmlDecode(inlineStyleMatch.Groups["value"].Value);
if (!string.IsNullOrWhiteSpace(inlineStyle))
{
cssBuilder.AppendLine(inlineStyle);
}
}
var stylesheetUrls = ExtractStylesheetUrls(pageHtml, pageUri, 3);
foreach (var stylesheetUrl in stylesheetUrls)
{
try
{
var stylesheetContent = await _httpClient.GetStringAsync(stylesheetUrl, cancellationToken);
if (!string.IsNullOrWhiteSpace(stylesheetContent))
{
cssBuilder.AppendLine(stylesheetContent);
}
}
catch
{
// Keep going on stylesheet fetch failures
}
}
var rawCss = cssBuilder.ToString();
return new DesignSiteMetadata
{
Title = ExtractTitle(pageHtml),
Colors = ExtractUniqueColors(rawCss, 30),
Fonts = ExtractUniqueFonts(rawCss, 12),
StructuralClasses = ExtractStructuralClasses(pageHtml, 25),
StylesheetUrls = stylesheetUrls
};
}
private static string ExtractTitle(string html)
{
var titleMatch = TitleRegex.Match(html);
if (!titleMatch.Success)
{
return "Unknown Title";
}
return WebUtility.HtmlDecode(titleMatch.Groups["title"].Value).Trim();
}
private static IReadOnlyList<string> ExtractStylesheetUrls(string html, Uri pageUri, int maxStylesheets)
{
var urls = new List<string>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (Match match in StylesheetHrefRegex.Matches(html))
{
if (urls.Count >= maxStylesheets)
{
break;
}
var href = WebUtility.HtmlDecode(match.Groups["href"].Value).Trim();
if (string.IsNullOrWhiteSpace(href))
{
continue;
}
if (!Uri.TryCreate(pageUri, href, out var stylesheetUri))
{
continue;
}
var absoluteUrl = stylesheetUri.ToString();
if (seen.Add(absoluteUrl))
{
urls.Add(absoluteUrl);
}
}
return urls;
}
private static IReadOnlyList<string> ExtractUniqueColors(string css, int maxCount)
{
var colors = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (Match hexMatch in HexColorRegex.Matches(css))
{
colors.Add(hexMatch.Value.ToLowerInvariant());
}
foreach (Match rgbMatch in RgbColorRegex.Matches(css))
{
colors.Add(rgbMatch.Value.ToLowerInvariant());
}
return colors.Take(maxCount).ToList();
}
private static IReadOnlyList<string> ExtractUniqueFonts(string css, int maxCount)
{
var fonts = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (Match fontMatch in FontFamilyRegex.Matches(css))
{
var value = fontMatch.Groups["value"].Value
.Replace("\"", string.Empty, StringComparison.Ordinal)
.Replace("'", string.Empty, StringComparison.Ordinal)
.Trim();
if (!string.IsNullOrWhiteSpace(value))
{
fonts.Add(value);
}
}
return fonts.Take(maxCount).ToList();
}
private static IReadOnlyList<string> ExtractStructuralClasses(string html, int maxCount)
{
var classes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (Match classMatch in ClassRegex.Matches(html))
{
var classValue = classMatch.Groups["value"].Value;
if (string.IsNullOrWhiteSpace(classValue))
{
continue;
}
foreach (var className in classValue.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
if (StructuralClassKeywords.Any(k => className.Contains(k, StringComparison.OrdinalIgnoreCase)))
{
classes.Add(className);
}
}
}
return classes.Take(maxCount).ToList();
}
private static string NormalizeTargetUrl(string targetUrl)
{
var trimmed = targetUrl.Trim();
if (Uri.TryCreate(trimmed, UriKind.Absolute, out var absoluteUri) &&
(absoluteUri.Scheme == Uri.UriSchemeHttp || absoluteUri.Scheme == Uri.UriSchemeHttps))
{
return absoluteUri.ToString();
}
if (Uri.TryCreate($"https://{trimmed}", UriKind.Absolute, out var httpsUri))
{
return httpsUri.ToString();
}
throw new ArgumentException("Target URL must be a valid HTTP/HTTPS URL.", nameof(targetUrl));
}
private static string BuildFallbackDesignMarkdown(DesignSiteMetadata metadata, string targetUrl)
{
return $"""
# Overview
Heuristic fallback for {targetUrl}. LLM generation was not available.
## Colors
### Brand & Accent
- {string.Join("\n- ", metadata.Colors.Take(6).DefaultIfEmpty("n/a"))}
### Surface
- {metadata.Colors.Skip(6).FirstOrDefault() ?? "n/a"}
### Text
- {metadata.Colors.Skip(7).FirstOrDefault() ?? "n/a"}
### Semantic
- Success: #16a34a
- Warning: #f59e0b
- Error: #dc2626
## Typography
### Font Family
- {string.Join("\n- ", metadata.Fonts.DefaultIfEmpty("system-ui, sans-serif"))}
### Hierarchy
| Token | Size | Weight | Line Height | Letter Spacing | Use |
|---|---:|---:|---:|---:|---|
| h1 | 40px | 700 | 1.2 | -0.02em | Hero |
| h2 | 32px | 700 | 1.25 | -0.01em | Section heading |
| body | 16px | 400 | 1.6 | 0 | Paragraph |
### Principles
- Keep typography readable and consistent.
## Layout
### Spacing System
- 4, 8, 12, 16, 24, 32, 48
### Grid & Container
- Max-width: 1200px
### Whitespace Philosophy
- Favor breathable sections and clear grouping.
## Elevation & Depth
| Level | Treatment | Use |
|---|---|---|
| 0 | none | background |
| 1 | subtle shadow | cards |
| 2 | medium shadow | overlays |
## Shapes
### Border Radius Scale
| Token | Value | Use |
|---|---|---|
| sm | 6px | inputs |
| md | 10px | buttons |
| lg | 16px | cards |
### Photography & Illustrations
- Match page tone and avoid style drift.
## Components
- Top Navigation, Buttons, Cards, Inputs, Badges, Tabs, Footer with consistent spacing and state contrast.
## Do and Don't guidance
### Do
- Reuse tokens consistently.
### Don't
- Introduce random colors and font mixes.
## Responsive Behavior
### Breakpoints
| Name | Width | Key Changes |
|---|---|---|
| sm | 640px | stack compact groups |
| md | 768px | 2-column sections |
| lg | 1024px | wider content and nav |
### Touch Targets
- Minimum 44x44px.
### Collapsing Strategy
- Collapse nav and dense rows on small screens.
## Iteration Guide
1. Freeze token map.
2. Validate components.
3. Test breakpoints.
## Known Gaps
- Dynamic runtime interactions are not captured.
""";
}
private static string BuildFallbackVerificationHtml(string targetUrl)
{
var encodedTargetUrl = WebUtility.HtmlEncode(targetUrl);
return $$"""
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Design Preview</title>
<style>
:root { --bg:#0b1020; --card:#141a2e; --text:#e8ecff; --muted:#9aa3c7; --accent:#6ea8fe; }
* { box-sizing: border-box; }
body { margin:0; font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; background:var(--bg); color:var(--text); }
.container { max-width:1100px; margin:0 auto; padding:24px; }
.nav, .card { background:var(--card); border:1px solid #202948; border-radius:14px; }
.nav { padding:14px 18px; display:flex; justify-content:space-between; align-items:center; }
.hero { padding:56px 0 24px; }
.btn { background:var(--accent); color:#071126; border:0; border-radius:10px; padding:10px 16px; font-weight:700; }
.grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(220px, 1fr)); gap:16px; margin-top:20px; }
.card { padding:16px; }
.muted { color:var(--muted); }
footer { margin-top:28px; padding-top:18px; border-top:1px solid #202948; color:var(--muted); }
</style>
</head>
<body>
<div class="container">
<div class="nav">
<strong>Design Preview</strong>
<span class="muted">{{encodedTargetUrl}}</span>
</div>
<section class="hero">
<h1>Fallback verification page</h1>
<p class="muted">Generated because LLM verification was unavailable.</p>
<button class="btn">Primary Action</button>
</section>
<section class="grid">
<article class="card"><h3>Card One</h3><p class="muted">Sample content block.</p></article>
<article class="card"><h3>Card Two</h3><p class="muted">Sample content block.</p></article>
<article class="card"><h3>Card Three</h3><p class="muted">Sample content block.</p></article>
</section>
<footer>Generated by DesignMdGenerator fallback renderer.</footer>
</div>
</body>
</html>
""";
}
}
Step 5: Run the Demo
dotnet run -- https://www.yahoo.com nemotron-3-super:cloud http://localhost:11434
If you want to test missing scheme URL normalization:
dotnet run -- yahoo.com nemotron-3-super:cloud http://localhost:11434
Step 6: Check Output
You should get:
Output/DesignMdGenerator/DESIGN.mdOutput/DesignMdGenerator/index.html
Open index.html in browser to inspect if it aligns with the design spec generated from your target page.
Why This Is a Fun Experiment
- You can quickly build custom design references for niche aesthetics.
- You can compare generated
DESIGN.mdwith existing public ones. - You can tune prompts to bias toward old-school, minimal, brutalist, or neon-retro tastes.
- You can pair this workflow with your coding agent for faster UI iteration.
Notes and Limitations
- Websites with heavy JavaScript rendering may require a headless browser pass.
- Regex extraction is intentionally lightweight, not a full CSS parser.
- Some cloud models may return 403 in specific environments; fallback mode in this service keeps the demo usable.
Credit
Again, credit to:
This demo is inspired by the idea of making design intent explicit in markdown and applying it in AI-assisted UI workflows.
Closing
If you love unusual aesthetics like I do, this is a fun playground:
- start from a URL,
- generate a custom
DESIGN.md, - let AI build the UI around your style.
Love C# & AI!




Top comments (0)