Add status route tests, OG images blog post, and blog tests
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 9m27s

- Create src/routes/__tests__/status.test.ts (GET /status and /status.html)
- Add blog post: public/blog/automating-og-images.html (~1000 words)
- Update public/blog.html with new post entry
- Update public/sitemap.xml with new URL
- Add blog tests for automating-og-images post
- Update existing blog tests for new post references

Tests: 332 passed, 1 skipped
This commit is contained in:
OpenClaw 2026-03-03 18:06:56 +01:00
parent 05c91e6747
commit 740c70f905
5 changed files with 378 additions and 1 deletions

View file

@ -93,6 +93,13 @@ footer{border-top:1px solid var(--border);padding:48px 24px 32px;background:var(
</div> </div>
<div class="blog-grid"> <div class="blog-grid">
<article class="blog-card">
<div class="meta"><span>March 3, 2026</span><span>7 min read</span></div>
<h2><a href="/blog/automating-og-images">Automating OG Image Generation with Screenshot APIs</a></h2>
<p>Stop designing Open Graph images by hand. Learn how to use screenshot APIs to automatically generate beautiful, dynamic OG images for every page on your site.</p>
<a href="/blog/automating-og-images" class="read-more">Read article →</a>
</article>
<article class="blog-card"> <article class="blog-card">
<div class="meta"><span>March 2, 2026</span><span>8 min read</span></div> <div class="meta"><span>March 2, 2026</span><span>8 min read</span></div>
<h2><a href="/blog/why-screenshot-api">Why You Need a Screenshot API (And Why Building Your Own Is Harder Than You Think)</a></h2> <h2><a href="/blog/why-screenshot-api">Why You Need a Screenshot API (And Why Building Your Own Is Harder Than You Think)</a></h2>

View file

@ -0,0 +1,284 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Automating OG Image Generation with Screenshot APIs — SnapAPI Blog</title>
<meta name="description" content="Stop designing Open Graph images by hand. Learn how to use screenshot APIs to automatically generate beautiful, dynamic OG images for every page on your site.">
<link rel="canonical" href="https://snapapi.eu/blog/automating-og-images">
<meta property="og:title" content="Automating OG Image Generation with Screenshot APIs">
<meta property="og:description" content="Stop designing OG images by hand. Use screenshot APIs to generate dynamic social preview images automatically.">
<meta property="og:type" content="article">
<meta property="og:url" content="https://snapapi.eu/blog/automating-og-images">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="Automating OG Image Generation with Screenshot APIs">
<meta name="twitter:description" content="How to use screenshot APIs to automatically generate beautiful OG images for every page on your site.">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📸</text></svg>">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
*{box-sizing:border-box;margin:0;padding:0}
:root{--bg:#0a0e17;--bg2:#0f1420;--card:#141a28;--card-hover:#1a2235;--border:#1e2a3f;--border-light:#2a3752;--text:#f0f2f7;--text-secondary:#94a3c0;--muted:#6b7a96;--primary:#4f8fff;--primary-light:#6da3ff;--primary-dark:#3a6fd8;--accent:#10b981;--purple:#a78bfa;--orange:#f59e0b;--gradient:linear-gradient(135deg,#4f8fff 0%,#a78bfa 50%,#ec4899 100%);--radius:12px;--radius-lg:16px}
html{scroll-behavior:smooth}
body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.6;-webkit-font-smoothing:antialiased}
a{color:var(--primary-light);text-decoration:none;transition:color .2s}
a:hover{color:var(--primary)}
nav{position:sticky;top:0;z-index:100;background:rgba(10,14,23,0.85);backdrop-filter:blur(20px);border-bottom:1px solid rgba(30,42,63,0.5);padding:0 24px}
.nav-inner{max-width:1180px;margin:0 auto;display:flex;align-items:center;justify-content:space-between;height:64px}
.nav-logo{font-size:1.15rem;font-weight:800;display:flex;align-items:center;gap:8px;color:var(--text)}
.nav-logo span{background:var(--gradient);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
.nav-links{display:flex;gap:32px;align-items:center}
.nav-links a{color:var(--muted);font-size:.9rem;font-weight:500}
.nav-links a:hover{color:var(--text)}
.btn{display:inline-flex;align-items:center;gap:8px;padding:12px 28px;border-radius:10px;font-weight:600;font-size:.95rem;border:none;cursor:pointer;transition:all .2s;font-family:inherit}
.btn-primary{background:var(--primary);color:#fff;box-shadow:0 4px 20px rgba(79,143,255,0.3)}
.btn-primary:hover{background:var(--primary-dark);transform:translateY(-1px);color:#fff}
.btn-sm{padding:8px 18px;font-size:.85rem}
.nav-mobile{display:none;background:none;border:none;color:var(--text);font-size:1.5rem;cursor:pointer}
@media(max-width:768px){.nav-links{display:none;flex-direction:column;position:absolute;top:64px;left:0;right:0;background:rgba(10,14,23,0.97);backdrop-filter:blur(20px);border-bottom:1px solid var(--border);padding:16px 24px;gap:16px;z-index:99}.nav-links.show{display:flex}.nav-mobile{display:block}}
.article{max-width:740px;margin:0 auto;padding:80px 24px 100px}
.article .breadcrumb{font-size:.85rem;color:var(--muted);margin-bottom:32px}
.article .breadcrumb a{color:var(--muted)}
.article .breadcrumb a:hover{color:var(--text)}
.article h1{font-size:2.5rem;font-weight:900;line-height:1.2;margin-bottom:16px}
.article .meta{font-size:.85rem;color:var(--muted);margin-bottom:48px;display:flex;gap:16px}
.article h2{font-size:1.5rem;font-weight:700;margin:48px 0 16px;color:var(--text)}
.article h3{font-size:1.2rem;font-weight:600;margin:32px 0 12px;color:var(--text)}
.article p{color:var(--text-secondary);line-height:1.8;margin-bottom:20px;font-size:1.05rem}
.article ul,.article ol{color:var(--text-secondary);margin:0 0 20px 24px;line-height:1.8}
.article li{margin-bottom:8px}
.article code{font-family:'JetBrains Mono',monospace;background:var(--card);padding:2px 8px;border-radius:4px;font-size:.9rem;color:var(--primary-light)}
.article pre{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:20px;margin:0 0 24px;overflow-x:auto}
.article pre code{background:none;padding:0;font-size:.85rem;line-height:1.7;color:var(--text-secondary)}
.article blockquote{border-left:3px solid var(--primary);padding:16px 24px;margin:0 0 24px;background:var(--card);border-radius:0 var(--radius) var(--radius) 0}
.article blockquote p{margin:0;color:var(--text-secondary);font-style:italic}
.article .cta-box{background:var(--card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:32px;margin:48px 0;text-align:center}
.article .cta-box h3{margin-top:0;font-size:1.3rem}
.article .cta-box p{margin-bottom:20px}
footer{border-top:1px solid var(--border);padding:48px 24px 32px;background:var(--bg2)}
.footer-grid{max-width:1180px;margin:0 auto;display:grid;grid-template-columns:2fr 1fr 1fr 1fr;gap:40px;margin-bottom:40px}
.footer-brand h4{font-size:1.1rem;font-weight:800;margin-bottom:12px;display:flex;align-items:center;gap:8px}
.footer-brand p{color:var(--muted);font-size:.85rem;line-height:1.6;max-width:280px}
.footer-col h5{font-size:.8rem;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:var(--muted);margin-bottom:16px}
.footer-col a{display:block;color:var(--text-secondary);font-size:.88rem;padding:4px 0}
.footer-col a:hover{color:var(--text)}
.footer-bottom{max-width:1180px;margin:0 auto;padding-top:24px;border-top:1px solid var(--border);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:12px;font-size:.8rem;color:var(--muted)}
@media(max-width:768px){.footer-grid{grid-template-columns:1fr}}
.skip-link{position:absolute;top:-100%;left:50%;transform:translateX(-50%);background:var(--primary);color:#fff;padding:12px 24px;border-radius:0 0 8px 8px;font-weight:600;font-size:.9rem;z-index:1000;transition:top .2s}
.skip-link:focus{top:0}
</style>
<script type="application/ld+json">{"@context":"https://schema.org","@type":"BlogPosting","headline":"Automating OG Image Generation with Screenshot APIs","description":"Stop designing Open Graph images by hand. Learn how to use screenshot APIs to automatically generate beautiful, dynamic OG images for every page on your site.","datePublished":"2026-03-03","author":{"@type":"Organization","name":"SnapAPI"},"publisher":{"@type":"Organization","name":"SnapAPI","url":"https://snapapi.eu"},"url":"https://snapapi.eu/blog/automating-og-images","mainEntityOfPage":"https://snapapi.eu/blog/automating-og-images"}</script>
</head>
<body>
<a href="#main-content" class="skip-link">Skip to content</a>
<header>
<nav>
<div class="nav-inner">
<a href="/" class="nav-logo">📸 <span>SnapAPI</span></a>
<div class="nav-links">
<a href="/#features">Features</a>
<a href="/pricing">Pricing</a>
<a href="/docs">API Docs</a>
<a href="/blog">Blog</a>
<a href="/#pricing" class="btn btn-primary btn-sm">Get API Key</a>
</div>
<button class="nav-mobile" onclick="document.querySelector('.nav-links').classList.toggle('show')" aria-label="Menu"></button>
</div>
</nav>
</header>
<main id="main-content">
<article class="article">
<div class="breadcrumb"><a href="/">Home</a> / <a href="/blog">Blog</a> / Automating OG Images</div>
<h1>Automating OG Image Generation with Screenshot APIs</h1>
<div class="meta"><span>March 3, 2026</span><span>7 min read</span></div>
<p>You've just published a new blog post. You share it on Twitter, LinkedIn, and Slack. But instead of a rich preview with a beautiful image, your link shows up as a sad little text snippet — or worse, a broken image placeholder. Sound familiar?</p>
<p>Open Graph (OG) images are the social preview cards that appear when someone shares your URL. They're one of those things that seem trivial until you realize they directly impact click-through rates. Posts with compelling preview images get <strong>2-3x more engagement</strong> than plain text links. Yet most developers either skip them entirely or spend hours manually designing them in Figma for each page.</p>
<p>There's a better way: use a screenshot API to generate OG images automatically, on the fly, for every single page on your site.</p>
<h2>The OG Image Problem</h2>
<p>Let's start with why this is harder than it should be. The Open Graph protocol is simple — you add a few meta tags to your HTML, including <code>og:image</code>, and social platforms use that image as a preview. The challenge isn't the protocol. It's producing the images themselves.</p>
<p>The traditional approaches all have significant drawbacks:</p>
<ul>
<li><strong>Manual design:</strong> Create each image by hand in Figma or Canva. Looks great, doesn't scale. You'll stop doing it after the fifth blog post.</li>
<li><strong>Static templates:</strong> Use the same generic image for everything. Low effort, but also low engagement — every link looks identical.</li>
<li><strong>Canvas/SVG generation:</strong> Write code to programmatically compose images using node-canvas, Sharp, or SVG rendering. Works, but you're essentially building a graphics engine. Text wrapping, font rendering, and layout calculations become your problem.</li>
<li><strong>Self-hosted Puppeteer:</strong> Spin up a headless browser to screenshot an HTML template. Powerful, but now you're managing Chrome instances in production — the exact problem we discussed in our <a href="/blog/why-screenshot-api">previous article</a>.</li>
</ul>
<p>Each approach trades off between quality, scalability, and engineering effort. What if you could get all three?</p>
<h2>The Screenshot API Approach</h2>
<p>The idea is elegant in its simplicity: design your OG image as an HTML page, then use a screenshot API to render it as a PNG. You get the full power of CSS for layout and styling, web fonts for typography, and zero infrastructure to maintain.</p>
<p>Here's the workflow:</p>
<ol>
<li>Create an HTML template for your OG images (a simple page with your branding, title, and any dynamic content)</li>
<li>Host that template at a URL, passing page-specific data via query parameters</li>
<li>Call a screenshot API to capture it at 1200×630 pixels (the standard OG image size)</li>
<li>Set the resulting image URL as your <code>og:image</code> meta tag</li>
</ol>
<h3>The HTML Template</h3>
<p>Your OG image template is just HTML and CSS. This means you can use flexbox for layout, Google Fonts for typography, gradients, shadows — anything the browser can render. Here's a minimal example:</p>
<pre><code>&lt;div style="width:1200px;height:630px;display:flex;
align-items:center;justify-content:center;
background:linear-gradient(135deg,#667eea,#764ba2);
font-family:'Inter',sans-serif;padding:60px"&gt;
&lt;h1 style="color:#fff;font-size:56px;
text-align:center;line-height:1.2"&gt;
{{title}}
&lt;/h1&gt;
&lt;/div&gt;</code></pre>
<p>Replace <code>{{title}}</code> with your page title dynamically — either server-side or via query parameters — and you have a unique OG image for every page.</p>
<h3>Calling the API</h3>
<p>With SnapAPI, generating the OG image is a single API call:</p>
<pre><code>curl "https://api.snapapi.eu/v1/screenshot?url=https://yoursite.com/og-template?title=My+Blog+Post&amp;width=1200&amp;height=630&amp;format=png" \
-H "X-API-Key: your-api-key" \
--output og-image.png</code></pre>
<p>That's it. The API launches a browser, renders your template, captures it at exactly 1200×630, and returns a pixel-perfect PNG. No Chrome processes to manage, no memory leaks to debug, no zombie browsers consuming your server's RAM.</p>
<h2>Static vs. Dynamic Generation</h2>
<p>There are two strategies for integrating OG images into your site, and the right choice depends on your content velocity and caching requirements.</p>
<h3>Build-Time Generation (Static)</h3>
<p>Generate all OG images during your build step and serve them as static files. This works well for blogs and documentation sites where content changes infrequently. Your CI pipeline calls the screenshot API for each page, saves the PNGs to your public directory, and deploys them alongside your HTML.</p>
<p>The advantage is zero runtime dependencies — your OG images are just static files on a CDN. The downside is that adding or updating content requires a rebuild.</p>
<h3>On-Demand Generation (Dynamic)</h3>
<p>Generate OG images on the fly when they're first requested, then cache them aggressively. This is ideal for sites with user-generated content, e-commerce product pages, or any scenario where you can't enumerate all pages at build time.</p>
<p>A typical implementation uses a serverless function or edge worker as a proxy:</p>
<pre><code>// /api/og-image.ts (Edge Function)
export default async function handler(req) {
const { title, subtitle } = new URL(req.url).searchParams;
const templateUrl = `https://yoursite.com/og?title=${title}`;
const response = await fetch(
`https://api.snapapi.eu/v1/screenshot?` +
`url=${encodeURIComponent(templateUrl)}` +
`&amp;width=1200&amp;height=630&amp;format=png`,
{ headers: { "X-API-Key": process.env.SNAPAPI_KEY } }
);
return new Response(response.body, {
headers: {
"Content-Type": "image/png",
"Cache-Control": "public, max-age=86400, s-maxage=604800",
},
});
}</code></pre>
<p>With proper cache headers, each OG image is generated exactly once and then served from the CDN for subsequent requests. Your <code>og:image</code> tag simply points to <code>/api/og-image?title=My+Page+Title</code>.</p>
<h2>Design Tips for Better OG Images</h2>
<p>Since you're designing with HTML and CSS, you have enormous creative freedom. But social preview images have specific constraints worth respecting:</p>
<ul>
<li><strong>Size matters:</strong> Always render at 1200×630 pixels. This is the standard that works across Twitter, LinkedIn, Facebook, and Slack. Smaller images get upscaled and look blurry.</li>
<li><strong>Keep text large:</strong> Your title should be at least 48px. Remember, these images are often displayed at small sizes on mobile feeds. If you can't read the text at 300px wide, it's too small.</li>
<li><strong>Brand consistency:</strong> Include your logo and use your brand colors. OG images are branding real estate — make every share reinforce your visual identity.</li>
<li><strong>Contrast is king:</strong> Social feeds are visually noisy. High contrast between text and background ensures your preview stands out. Dark backgrounds with light text tend to perform well.</li>
<li><strong>Avoid text near edges:</strong> Some platforms crop OG images slightly. Keep all important content within 90% of the image area.</li>
<li><strong>Test everywhere:</strong> Use tools like <a href="https://www.opengraph.xyz/" rel="noopener">opengraph.xyz</a> to preview how your OG images render across different platforms before shipping.</li>
</ul>
<h2>Performance Considerations</h2>
<p>OG images don't need to be fast for end users — they're fetched by social platform crawlers, not by browsers loading your page. That said, there are performance aspects worth considering:</p>
<ul>
<li><strong>Cache aggressively:</strong> Set long <code>Cache-Control</code> headers. Social platforms cache OG images themselves, but your CDN should too. A week or more is typical.</li>
<li><strong>Use PNG for text-heavy images:</strong> PNG preserves sharp text better than JPEG. The file sizes are larger, but since these images are fetched by crawlers (not loaded on every page view), it's an acceptable tradeoff.</li>
<li><strong>Pre-generate when possible:</strong> For content you know about at build time, generate OG images during the build. Save the runtime generation for truly dynamic content.</li>
<li><strong>Monitor your API usage:</strong> Each unique OG image is one API call. If you have 1,000 blog posts and rebuild nightly, that's 1,000 calls per day. Most screenshot APIs (including SnapAPI) offer generous free tiers and predictable per-screenshot pricing, but it's worth tracking.</li>
</ul>
<h2>Beyond Social Previews</h2>
<p>Once you've set up the infrastructure for OG images, you'll find other uses for dynamically generated images. The same template approach works for:</p>
<ul>
<li><strong>Email headers:</strong> Personalized banner images for marketing emails</li>
<li><strong>Certificate generation:</strong> Course completion certificates with the student's name rendered beautifully</li>
<li><strong>Invoice previews:</strong> Thumbnail previews of invoices in dashboard UIs</li>
<li><strong>Documentation screenshots:</strong> Auto-generated screenshots of your own UI for docs that stay current</li>
</ul>
<p>The pattern is always the same: design it in HTML, screenshot it via API, serve the result. HTML becomes your universal image template language.</p>
<h2>Conclusion</h2>
<p>OG images shouldn't be an afterthought, and they shouldn't require a design team to maintain. By combining HTML templates with a screenshot API, you get the visual quality of hand-designed images with the scalability of automation. Every page gets a unique, branded social preview — automatically.</p>
<p>The best part? Your OG image templates are just code. They live in version control, they're easy to iterate on, and they update everywhere when you change your branding. No more stale Figma exports, no more missing preview images, no more sad text-only link shares.</p>
<p>Start with a simple template, wire it up to a screenshot API, add caching, and you're done. Your links will never look naked again.</p>
<div class="cta-box">
<h3>Generate OG Images with SnapAPI</h3>
<p>100 free screenshots to get started. Pixel-perfect rendering, EU-hosted, and ready in under a minute. Perfect for automated OG image generation.</p>
<a href="/#playground" class="btn btn-primary">Try the Playground →</a>
</div>
</article>
</main>
<footer>
<div class="footer-grid">
<div class="footer-brand">
<h4>📸 SnapAPI</h4>
<p>The EU-hosted screenshot API for developers. Convert any URL to a pixel-perfect image with a simple API call.</p>
</div>
<div class="footer-col">
<h5>Product</h5>
<a href="/#features">Features</a>
<a href="/pricing">Pricing</a>
<a href="/docs">API Docs</a>
<a href="/blog">Blog</a>
</div>
<div class="footer-col">
<h5>Developers</h5>
<a href="/docs">Swagger / OpenAPI</a>
<a href="/guides/quick-start">Quick Start</a>
<a href="/changelog">Changelog</a>
</div>
<div class="footer-col">
<h5>Legal</h5>
<a href="/impressum.html">Impressum</a>
<a href="/privacy.html">Privacy Policy</a>
<a href="/terms.html">Terms of Service</a>
</div>
</div>
<div class="footer-bottom">
<span>© 2026 Cloonar Technologies GmbH · FN 631089y · ATU81280034</span>
<span>EU-hosted 🇪🇺 · All data stays in Europe</span>
</div>
</footer>
</body>
</html>

View file

@ -13,6 +13,7 @@
<url><loc>https://snapapi.eu/blog</loc><changefreq>weekly</changefreq><priority>0.8</priority></url> <url><loc>https://snapapi.eu/blog</loc><changefreq>weekly</changefreq><priority>0.8</priority></url>
<url><loc>https://snapapi.eu/blog/why-screenshot-api</loc><changefreq>monthly</changefreq><priority>0.7</priority></url> <url><loc>https://snapapi.eu/blog/why-screenshot-api</loc><changefreq>monthly</changefreq><priority>0.7</priority></url>
<url><loc>https://snapapi.eu/blog/screenshot-api-performance</loc><changefreq>monthly</changefreq><priority>0.7</priority></url> <url><loc>https://snapapi.eu/blog/screenshot-api-performance</loc><changefreq>monthly</changefreq><priority>0.7</priority></url>
<url><loc>https://snapapi.eu/blog/automating-og-images</loc><changefreq>monthly</changefreq><priority>0.7</priority></url>
<url><loc>https://snapapi.eu/impressum.html</loc><changefreq>yearly</changefreq><priority>0.2</priority></url> <url><loc>https://snapapi.eu/impressum.html</loc><changefreq>yearly</changefreq><priority>0.2</priority></url>
<url><loc>https://snapapi.eu/privacy.html</loc><changefreq>yearly</changefreq><priority>0.2</priority></url> <url><loc>https://snapapi.eu/privacy.html</loc><changefreq>yearly</changefreq><priority>0.2</priority></url>
<url><loc>https://snapapi.eu/terms.html</loc><changefreq>yearly</changefreq><priority>0.2</priority></url> <url><loc>https://snapapi.eu/terms.html</loc><changefreq>yearly</changefreq><priority>0.2</priority></url>

View file

@ -51,11 +51,12 @@ describe('Blog Index Page', () => {
expect(html).toContain('"@type":"Blog"') expect(html).toContain('"@type":"Blog"')
}) })
it('links to both blog posts', async () => { it('links to all blog posts', async () => {
const res = await request(app).get('/blog.html') const res = await request(app).get('/blog.html')
const html = res.text const html = res.text
expect(html).toContain('/blog/why-screenshot-api') expect(html).toContain('/blog/why-screenshot-api')
expect(html).toContain('/blog/screenshot-api-performance') expect(html).toContain('/blog/screenshot-api-performance')
expect(html).toContain('/blog/automating-og-images')
}) })
}) })
@ -153,12 +154,60 @@ describe('Blog Post: Screenshot API Performance', () => {
}) })
}) })
describe('Blog Post: Automating OG Images', () => {
const app = createApp()
it('GET /blog/automating-og-images.html returns 200', async () => {
const res = await request(app).get('/blog/automating-og-images.html')
expect(res.status).toBe(200)
expect(res.headers['content-type']).toContain('text/html')
})
it('GET /blog/automating-og-images redirects 301 to .html', async () => {
const res = await request(app).get('/blog/automating-og-images')
expect(res.status).toBe(301)
expect(res.headers.location).toBe('/blog/automating-og-images.html')
})
it('contains required SEO elements', async () => {
const res = await request(app).get('/blog/automating-og-images.html')
const html = res.text
expect(html).toMatch(/<title>.+<\/title>/)
expect(html).toMatch(/<meta name="description" content=".+"/)
expect(html).toMatch(/<meta property="og:title"/)
expect(html).toMatch(/<meta property="og:description"/)
expect(html).toMatch(/<link rel="canonical" href="https:\/\/snapapi\.eu\/blog\/automating-og-images"/)
expect(html).toMatch(/<meta name="twitter:card"/)
})
it('contains JSON-LD BlogPosting schema', async () => {
const res = await request(app).get('/blog/automating-og-images.html')
const html = res.text
expect(html).toContain('"@type":"BlogPosting"')
})
it('has substantial content (~800 words)', async () => {
const res = await request(app).get('/blog/automating-og-images.html')
const text = res.text.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ')
const wordCount = text.split(' ').filter(w => w.length > 0).length
expect(wordCount).toBeGreaterThan(600)
})
it('contains relevant keywords', async () => {
const res = await request(app).get('/blog/automating-og-images.html')
const html = res.text
expect(html).toContain('OG image')
expect(html).toContain('screenshot API')
})
})
describe('Blog Sitemap & Navigation', () => { describe('Blog Sitemap & Navigation', () => {
it('sitemap contains blog URLs', () => { it('sitemap contains blog URLs', () => {
const sitemap = fs.readFileSync(path.join(publicDir, 'sitemap.xml'), 'utf-8') const sitemap = fs.readFileSync(path.join(publicDir, 'sitemap.xml'), 'utf-8')
expect(sitemap).toContain('https://snapapi.eu/blog') expect(sitemap).toContain('https://snapapi.eu/blog')
expect(sitemap).toContain('https://snapapi.eu/blog/why-screenshot-api') expect(sitemap).toContain('https://snapapi.eu/blog/why-screenshot-api')
expect(sitemap).toContain('https://snapapi.eu/blog/screenshot-api-performance') expect(sitemap).toContain('https://snapapi.eu/blog/screenshot-api-performance')
expect(sitemap).toContain('https://snapapi.eu/blog/automating-og-images')
}) })
it('index.html has Blog link in nav or footer', () => { it('index.html has Blog link in nav or footer', () => {

View file

@ -0,0 +1,36 @@
import { describe, it, expect } from 'vitest'
import request from 'supertest'
import express from 'express'
import path from 'path'
import { fileURLToPath } from 'url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const publicDir = path.join(__dirname, '../../../public')
function createApp() {
const app = express()
app.use(express.static(publicDir, { etag: true }))
// Mirror the status route from index.ts
app.get('/status', (_req, res) => {
res.sendFile(path.join(publicDir, 'status.html'))
})
return app
}
describe('Status Route', () => {
const app = createApp()
it('GET /status returns 200', async () => {
const res = await request(app).get('/status')
expect(res.status).toBe(200)
expect(res.headers['content-type']).toContain('text/html')
})
it('GET /status.html returns 200', async () => {
const res = await request(app).get('/status.html')
expect(res.status).toBe(200)
expect(res.headers['content-type']).toContain('text/html')
})
})