Making My Website Curl-Friendly

I've always loved the idea of websites that respond differently to curl requests versus browser visits. You know, those sites where running curl example.com gives you a beautiful ASCII art terminal output instead of just HTML. It's a nice touch that shows attention to detail and respect for terminal users.

So I decided to add this to my own site. Here's how I built a system that detects curl requests and serves up a colorful terminal-style business card.

The Goal

When someone runs curl https://zdrenka.com, instead of getting raw HTML, they should see:

  • My ASCII art portrait (in color!)
  • Professional info laid out like a screenfetch/neofetch output
  • ANSI colors that match my website's terminal theme
  • Clickable URLs for GitHub, LinkedIn, and company
  • A teaser for my latest blog post

Step 1: User-Agent Detection

The key is detecting when a request comes from curl versus a browser. Different hosting platforms handle this differently:

For Vercel (vercel.json):

{
  "rewrites": [
    {
      "source": "/",
      "destination": "/curl.txt",
      "has": [
        {
          "type": "header",
          "key": "user-agent",
          "value": "(?i).*(curl|wget|httpie|lynx|links|elinks).*"
        }
      ]
    },
    {
      "source": "/",
      "destination": "/index.html"
    }
  ]
}

For Netlify (_redirects):

/  /curl.txt  200  User-Agent: *curl*
/  /curl.txt  200  User-Agent: *wget*  
/  /curl.txt  200  User-Agent: *httpie*
/  /curl.txt  200  User-Agent: *lynx*

# Default to index.html for browsers
/  /index.html  200

For Apache (.htaccess):

RewriteEngine On

RewriteCond %{HTTP_USER_AGENT} ^(curl|wget|httpie|lynx|links|elinks).*$ [NC]
RewriteRule ^/?$ /curl.txt [L]

RewriteCond %{HTTP_USER_AGENT} !^(curl|wget|httpie|lynx|links|elinks).*$ [NC]
RewriteRule ^/?$ /index.html [L]

The logic is simple: if the User-Agent header contains "curl", "wget", "httpie", or similar tools, serve the plain text version. Otherwise, serve the normal HTML.

As a last resort, the build also generates a curl-detector.html — a JavaScript fallback that sniffs navigator.userAgent client-side and redirects accordingly, for cases where server-side routing isn't available.

Step 2: Generating the Content

I created a Node.js script (lib/generateCurlPage.js) that generates the curl-friendly output with ANSI colors:

// ANSI color codes matching website theme
const colors = {
  reset: '\x1b[0m',
  green: '\x1b[32m',
  brightGreen: '\x1b[92m',
  red: '\x1b[31m',
  brightRed: '\x1b[91m',      // ASCII art
  cyan: '\x1b[36m',
  brightCyan: '\x1b[96m',     // Field labels
  yellow: '\x1b[33m',
  brightYellow: '\x1b[93m',   // Highlighted fields
  white: '\x1b[37m',
  brightWhite: '\x1b[97m',    // Main text
  blue: '\x1b[34m',           // URLs
  brightBlue: '\x1b[94m',
  gray: '\x1b[90m'
};

The script combines ASCII art with info in a side-by-side layout, just like the screenfetch display on my homepage:

for (let i = 0; i < maxLines; i++) {
  const artLine = i < asciiArt.length ? asciiArt[i] : " ".repeat(35);
  const infoLine = i < info.length ? info[i] : "";
  contentLines.push(`${artLine}  ${infoLine}`);
}

Step 3: Centralised Profile Data

One thing I wanted to avoid was having to update my role, company, or other info in multiple places. So all profile data lives in a single file — lib/profileData.js — which is shared between the curl generator and the browser homepage:

const profileData = {
  roleStartDate: '2018-07-30',
  industryStartDate: '2011-07-01',
  name: "Karl Lankester-Carthy",
  role: "Lead Software Engineer",
  company: { name: "Amplience.com", url: "https://amplience.com" },
  // ...
};

The build uses this to generate both curl.txt and a profile-data.js file that the browser loads. Change one file, both outputs stay in sync.

Step 4: Build Integration

Everything is wired into the build script, so every time I run npm run build, it generates curl.txt alongside the HTML:

const { generateCurlPage } = require('./lib/generateCurlPage');

// In build function
generateCurlPage(outputDir);

Step 5: Dynamic Content

The "Time In Role" and "Time In Industry" fields update automatically — no manual edits needed:

function calculateTimeDifference(startDate) {
  const from = new Date(startDate);
  const now = new Date();

  let years = now.getFullYear() - from.getFullYear();
  let months = now.getMonth() - from.getMonth();
  let days = now.getDate() - from.getDate();

  if (days < 0) {
    months--;
    const prevMonth = new Date(now.getFullYear(), now.getMonth(), 0);
    days += prevMonth.getDate();
  }

  if (months < 0) {
    years--;
    months += 12;
  }

  return { years, months, days };
}

And at the bottom of the curl output, it automatically appends a link to the latest blog post — pulled from the same posts the build already processes:

contentLines.push(`${colors.brightWhite}${profileData.getLatestBlogMessagePlain()}${colors.reset}`);

So if I publish a new post, the curl output advertises it automatically.

The Result

Now when someone runs curl https://zdrenka.com, they get a colorful terminal output that looks like a proper business card:

  • ✅ Bright red ASCII art portrait
  • ✅ Cyan field labels with white values
  • ✅ Clickable OSC 8 hyperlinks for URLs (supported in iTerm2, GNOME Terminal, Windows Terminal, Kitty, etc.)
  • ✅ Yellow highlighted time in role and industry
  • ✅ Proper padding and alignment
  • ✅ Latest blog post teaser at the bottom

Why Bother?

Sure, 99% of visitors will use a browser and never see this. But for the 1% who curl my site (usually other developers), it shows attention to detail and respect for the terminal. Plus, it was a fun little project that taught me about User-Agent detection and ANSI color handling.

It's these small touches that make personal sites interesting. Anyone can throw up a boring HTML page, but adding personality and Easter eggs makes it memorable.

Try it yourself: curl https://zdrenka.com 🎨