feat: add 3 SEO use case pages with clean URLs, sitemap, and index section
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 10m32s

This commit is contained in:
OpenClaw 2026-03-02 09:07:57 +01:00
parent 195a656a7d
commit e9ee3a6c2c
8 changed files with 729 additions and 2 deletions

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "snapapi", "name": "snapapi",
"version": "0.1.0", "version": "0.6.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "snapapi", "name": "snapapi",
"version": "0.1.0", "version": "0.6.0",
"dependencies": { "dependencies": {
"compression": "^1.8.1", "compression": "^1.8.1",
"express": "^4.21.0", "express": "^4.21.0",

View file

@ -546,6 +546,31 @@ screenshot = snap.<span class="fn">capture</span>(
</div> </div>
</section> </section>
<section class="section" id="use-cases">
<div class="container text-center">
<div class="section-label">Use Cases</div>
<h2 class="section-title">Built for real-world workflows</h2>
<p class="section-subtitle">See how developers use SnapAPI to automate screenshots across their stack.</p>
<div class="features-grid" style="grid-template-columns:repeat(3,1fr);margin-top:48px">
<a href="/use-cases/social-media-previews" class="feature-card" style="text-decoration:none;color:inherit">
<div class="feature-icon blue">🖼️</div>
<h3>OG Images & Social Previews</h3>
<p>Generate dynamic Open Graph images and Twitter cards from HTML templates on-the-fly.</p>
</a>
<a href="/use-cases/website-monitoring" class="feature-card" style="text-decoration:none;color:inherit">
<div class="feature-icon green">👁️</div>
<h3>Visual Website Monitoring</h3>
<p>Schedule screenshots to detect layout regressions, broken pages, and visual bugs automatically.</p>
</a>
<a href="/use-cases/pdf-reports" class="feature-card" style="text-decoration:none;color:inherit">
<div class="feature-icon purple">📄</div>
<h3>Reports & Thumbnails</h3>
<p>Create website thumbnails for link previews, directories, dashboards, and email digests.</p>
</a>
</div>
</div>
</section>
<section class="section" id="pricing"> <section class="section" id="pricing">
<div class="container text-center"> <div class="container text-center">
<div class="section-label">Pricing</div> <div class="section-label">Pricing</div>

View file

@ -2,6 +2,9 @@
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url><loc>https://snapapi.eu/</loc><changefreq>weekly</changefreq><priority>1.0</priority></url> <url><loc>https://snapapi.eu/</loc><changefreq>weekly</changefreq><priority>1.0</priority></url>
<url><loc>https://snapapi.eu/docs</loc><changefreq>monthly</changefreq><priority>0.8</priority></url> <url><loc>https://snapapi.eu/docs</loc><changefreq>monthly</changefreq><priority>0.8</priority></url>
<url><loc>https://snapapi.eu/use-cases/social-media-previews</loc><changefreq>monthly</changefreq><priority>0.7</priority></url>
<url><loc>https://snapapi.eu/use-cases/website-monitoring</loc><changefreq>monthly</changefreq><priority>0.7</priority></url>
<url><loc>https://snapapi.eu/use-cases/pdf-reports</loc><changefreq>monthly</changefreq><priority>0.7</priority></url>
<url><loc>https://snapapi.eu/status</loc><changefreq>always</changefreq><priority>0.3</priority></url> <url><loc>https://snapapi.eu/status</loc><changefreq>always</changefreq><priority>0.3</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>

View file

@ -0,0 +1,205 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Generate Visual Reports & Thumbnails from Web Content | SnapAPI</title>
<meta name="description" content="Use SnapAPI to generate website thumbnails, link preview images, and visual reports from any URL. Create web page previews for dashboards, directories, and content platforms.">
<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">
<link rel="canonical" href="https://snapapi.eu/use-cases/pdf-reports">
<meta property="og:title" content="Generate Visual Reports & Thumbnails from Web Content">
<meta property="og:description" content="Create website thumbnails and visual reports from any URL with SnapAPI's screenshot API.">
<meta property="og:type" content="article">
<meta property="og:url" content="https://snapapi.eu/use-cases/pdf-reports">
<meta property="og:image" content="https://snapapi.eu/og-image.png">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Generate Visual Reports & Thumbnails from Web Content">
<meta name="twitter:description" content="Create website thumbnails and visual reports from any URL with SnapAPI's screenshot API.">
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"Article","headline":"Generate Visual Reports & Thumbnails from Web Content","description":"Use SnapAPI to generate website thumbnails, link preview images, and visual reports from any URL.","author":{"@type":"Organization","name":"Cloonar Technologies GmbH"},"publisher":{"@type":"Organization","name":"SnapAPI","url":"https://snapapi.eu"},"datePublished":"2026-03-02","url":"https://snapapi.eu/use-cases/pdf-reports"}
</script>
<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;--primary-glow:rgba(79,143,255,0.15);
--accent:#10b981;--accent-glow:rgba(16,185,129,0.15);
--purple:#a78bfa;--orange:#f59e0b;--pink:#ec4899;
--gradient:linear-gradient(135deg,#4f8fff 0%,#a78bfa 50%,#ec4899 100%);
--radius:12px;--radius-lg:16px;--radius-xl:24px;
}
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)}
::selection{background:var(--primary);color:#fff}
.container{max-width:800px;margin:0 auto;padding:0 24px}
.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);box-shadow:0 6px 28px rgba(79,143,255,0.4);color:#fff}
.btn-lg{padding:16px 36px;font-size:1.05rem;border-radius:12px}
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;transition:color .2s}
.nav-links a:hover{color:var(--text)}
.nav-cta{margin-left:8px}
.nav-mobile{display:none;background:none;border:none;color:var(--text);font-size:1.5rem;cursor:pointer}
.article{padding:80px 0 60px}
.article h1{font-size:2.5rem;font-weight:800;line-height:1.2;margin-bottom:24px}
.article h2{font-size:1.6rem;font-weight:700;margin:48px 0 16px}
.article h3{font-size:1.2rem;font-weight:600;margin:32px 0 12px}
.article p{color:var(--text-secondary);font-size:1rem;line-height:1.8;margin-bottom:16px}
.article ul,.article ol{color:var(--text-secondary);margin:0 0 16px 24px;line-height:1.8}
.article li{margin-bottom:8px}
.code-window{background:var(--card);border:1px solid var(--border);border-radius:var(--radius-lg);overflow:hidden;margin:24px 0;box-shadow:0 10px 40px rgba(0,0,0,0.2)}
.code-titlebar{display:flex;align-items:center;gap:8px;padding:14px 20px;background:rgba(0,0,0,0.2);border-bottom:1px solid var(--border)}
.code-dot{width:12px;height:12px;border-radius:50%}
.code-dot:nth-child(1){background:#ff5f57}.code-dot:nth-child(2){background:#ffbd2e}.code-dot:nth-child(3){background:#28c840}
.code-titlebar span{flex:1;text-align:center;font-size:.8rem;color:var(--muted);font-weight:500}
.code-body{padding:24px;font-family:'JetBrains Mono',monospace;font-size:.85rem;line-height:1.9;overflow-x:auto;color:var(--text-secondary)}
.code-body .kw{color:var(--purple)}.code-body .str{color:var(--accent)}.code-body .cmt{color:#475569;font-style:italic}.code-body .fn{color:var(--primary-light)}.code-body .prop{color:var(--orange)}
.cta-box{background:linear-gradient(135deg,rgba(79,143,255,0.1) 0%,rgba(167,139,250,0.1) 100%);border:1px solid rgba(79,143,255,0.2);border-radius:var(--radius-xl);padding:48px;text-align:center;margin:48px 0}
.cta-box h2{font-size:1.8rem;font-weight:800;margin-bottom:12px}
.cta-box p{color:var(--text-secondary);margin-bottom:24px}
.related{margin:48px 0;padding:32px;background:var(--card);border:1px solid var(--border);border-radius:var(--radius-lg)}
.related h3{font-size:1.1rem;font-weight:700;margin-bottom:16px}
.related a{display:block;padding:8px 0;font-size:.95rem}
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;transition:color .2s}
.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:640px){
.article{padding:40px 0 30px}
.article h1{font-size:1.8rem}
.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}
.footer-grid{grid-template-columns:1fr}
}
</style>
</head>
<body>
<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="/#playground">Try It Free</a>
<a href="/#pricing">Pricing</a>
<a href="/docs">API Docs</a>
<a href="/#pricing" class="btn btn-primary btn-sm nav-cta">Get API Key</a>
</div>
<button class="nav-mobile" onclick="document.querySelector('.nav-links').classList.toggle('show')" aria-label="Menu"></button>
</div>
</nav>
<article class="article">
<div class="container">
<h1>Generate Visual Reports & Thumbnails from Web Content</h1>
<p>Building a link directory, content aggregator, or dashboard? You need thumbnail previews of web pages. Rather than relying on unreliable meta images or building your own headless browser infrastructure, use SnapAPI to generate accurate website thumbnails on demand.</p>
<h2>Common Use Cases</h2>
<ul>
<li><strong>Link preview thumbnails</strong> — Show visual previews of URLs in chat apps, bookmarking tools, or CMS platforms.</li>
<li><strong>Report generation</strong> — Capture web dashboards (Grafana, analytics) as images for PDF reports or email digests.</li>
<li><strong>Content directories</strong> — Generate thumbnails for website listings, app stores, or portfolio showcases.</li>
<li><strong>Email newsletters</strong> — Embed live website previews in newsletters instead of stale static images.</li>
</ul>
<h2>Code Example</h2>
<h3>Generate a Website Thumbnail</h3>
<div class="code-window">
<div class="code-titlebar"><span class="code-dot"></span><span class="code-dot"></span><span class="code-dot"></span><span>Node.js</span></div>
<div class="code-body"><pre><span class="kw">async function</span> <span class="fn">getThumbnail</span>(url) {
<span class="kw">const</span> res = <span class="kw">await</span> <span class="fn">fetch</span>(<span class="str">'https://snapapi.eu/v1/screenshot'</span>, {
method: <span class="str">'POST'</span>,
headers: {
<span class="str">'Content-Type'</span>: <span class="str">'application/json'</span>,
<span class="str">'X-API-Key'</span>: process.env.<span class="prop">SNAPAPI_KEY</span>
},
body: JSON.stringify({
url,
width: <span class="prop">1280</span>,
height: <span class="prop">800</span>,
format: <span class="str">'webp'</span>, <span class="cmt">// Smaller file size</span>
quality: <span class="prop">80</span>
})
});
<span class="kw">return</span> Buffer.from(<span class="kw">await</span> res.<span class="fn">arrayBuffer</span>());
}
<span class="cmt">// Generate thumbnails for a list of URLs</span>
<span class="kw">const</span> urls = [<span class="str">'https://github.com'</span>, <span class="str">'https://news.ycombinator.com'</span>];
<span class="kw">for</span> (<span class="kw">const</span> url <span class="kw">of</span> urls) {
<span class="kw">const</span> img = <span class="kw">await</span> <span class="fn">getThumbnail</span>(url);
<span class="cmt">// Store in database, upload to CDN, etc.</span>
}</pre></div>
</div>
<h3>Batch Processing with Caching</h3>
<p>For high-volume use, cache thumbnails and refresh them periodically:</p>
<div class="code-window">
<div class="code-titlebar"><span class="code-dot"></span><span class="code-dot"></span><span class="code-dot"></span><span>Bash</span></div>
<div class="code-body"><pre><span class="cmt"># Quick thumbnail via cURL</span>
curl -X POST https://snapapi.eu/v1/screenshot \
-H <span class="str">"Content-Type: application/json"</span> \
-H <span class="str">"X-API-Key: $SNAPAPI_KEY"</span> \
-d <span class="str">'{"url":"https://example.com","width":1280,"height":800,"format":"webp"}'</span> \
--output thumbnail.webp
<span class="cmt"># Resize locally for smaller thumbnails</span>
convert thumbnail.webp -resize 400x250 thumbnail-sm.webp</pre></div>
</div>
<h2>Why SnapAPI for Thumbnails?</h2>
<ul>
<li><strong>Accurate rendering</strong> — Real Chromium browser captures the page exactly as users see it, including JavaScript-rendered content.</li>
<li><strong>Multiple formats</strong> — PNG for lossless quality, WebP for smaller file sizes, JPEG for maximum compatibility.</li>
<li><strong>Custom viewport</strong> — Set any width/height to capture desktop, tablet, or mobile views.</li>
<li><strong>EU-hosted</strong> — All screenshots rendered in Germany. GDPR compliant by default.</li>
</ul>
<div class="cta-box">
<h2>Start Generating Thumbnails</h2>
<p>Try it free in the playground — no signup needed.</p>
<a href="/#pricing" class="btn btn-primary btn-lg">Get Your API Key →</a>
</div>
<div class="related">
<h3>Related Use Cases</h3>
<a href="/use-cases/social-media-previews">Generate OG Images & Social Media Previews →</a>
<a href="/use-cases/website-monitoring">Visual Website Monitoring & Regression Testing →</a>
</div>
</div>
</article>
<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="/#playground">Playground</a><a href="/docs">API Docs</a></div>
<div class="footer-col"><h5>Developers</h5><a href="/docs">Swagger / OpenAPI</a><a href="/health">Status</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>Linzer Straße 192/1/2, 1140 Wien, Austria 🇦🇹</span>
<span>EU-hosted 🇪🇺 · All data stays in Europe</span>
</div>
</footer>
</body>
</html>

View file

@ -0,0 +1,194 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Generate OG Images & Social Media Previews with a Screenshot API | SnapAPI</title>
<meta name="description" content="Use SnapAPI to generate dynamic OG images and social media preview cards from HTML templates. Render custom Open Graph images on-the-fly with a simple API call.">
<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">
<link rel="canonical" href="https://snapapi.eu/use-cases/social-media-previews">
<meta property="og:title" content="Generate OG Images & Social Media Previews with a Screenshot API">
<meta property="og:description" content="Render dynamic Open Graph images and Twitter cards from HTML templates using SnapAPI.">
<meta property="og:type" content="article">
<meta property="og:url" content="https://snapapi.eu/use-cases/social-media-previews">
<meta property="og:image" content="https://snapapi.eu/og-image.png">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Generate OG Images & Social Media Previews with a Screenshot API">
<meta name="twitter:description" content="Render dynamic Open Graph images and Twitter cards from HTML templates using SnapAPI.">
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"Article","headline":"Generate OG Images & Social Media Previews with a Screenshot API","description":"Use SnapAPI to generate dynamic OG images and social media preview cards from HTML templates.","author":{"@type":"Organization","name":"Cloonar Technologies GmbH"},"publisher":{"@type":"Organization","name":"SnapAPI","url":"https://snapapi.eu"},"datePublished":"2026-03-02","url":"https://snapapi.eu/use-cases/social-media-previews"}
</script>
<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;--primary-glow:rgba(79,143,255,0.15);
--accent:#10b981;--accent-glow:rgba(16,185,129,0.15);
--purple:#a78bfa;--orange:#f59e0b;--pink:#ec4899;
--gradient:linear-gradient(135deg,#4f8fff 0%,#a78bfa 50%,#ec4899 100%);
--radius:12px;--radius-lg:16px;--radius-xl:24px;
}
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)}
::selection{background:var(--primary);color:#fff}
.container{max-width:800px;margin:0 auto;padding:0 24px}
.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);box-shadow:0 6px 28px rgba(79,143,255,0.4);color:#fff}
.btn-lg{padding:16px 36px;font-size:1.05rem;border-radius:12px}
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;transition:color .2s}
.nav-links a:hover{color:var(--text)}
.nav-cta{margin-left:8px}
.nav-mobile{display:none;background:none;border:none;color:var(--text);font-size:1.5rem;cursor:pointer}
.article{padding:80px 0 60px}
.article h1{font-size:2.5rem;font-weight:800;line-height:1.2;margin-bottom:24px}
.article h2{font-size:1.6rem;font-weight:700;margin:48px 0 16px}
.article h3{font-size:1.2rem;font-weight:600;margin:32px 0 12px}
.article p{color:var(--text-secondary);font-size:1rem;line-height:1.8;margin-bottom:16px}
.article ul,.article ol{color:var(--text-secondary);margin:0 0 16px 24px;line-height:1.8}
.article li{margin-bottom:8px}
.code-window{background:var(--card);border:1px solid var(--border);border-radius:var(--radius-lg);overflow:hidden;margin:24px 0;box-shadow:0 10px 40px rgba(0,0,0,0.2)}
.code-titlebar{display:flex;align-items:center;gap:8px;padding:14px 20px;background:rgba(0,0,0,0.2);border-bottom:1px solid var(--border)}
.code-dot{width:12px;height:12px;border-radius:50%}
.code-dot:nth-child(1){background:#ff5f57}.code-dot:nth-child(2){background:#ffbd2e}.code-dot:nth-child(3){background:#28c840}
.code-titlebar span{flex:1;text-align:center;font-size:.8rem;color:var(--muted);font-weight:500}
.code-body{padding:24px;font-family:'JetBrains Mono',monospace;font-size:.85rem;line-height:1.9;overflow-x:auto;color:var(--text-secondary)}
.code-body .kw{color:var(--purple)}.code-body .str{color:var(--accent)}.code-body .cmt{color:#475569;font-style:italic}.code-body .fn{color:var(--primary-light)}.code-body .prop{color:var(--orange)}
.cta-box{background:linear-gradient(135deg,rgba(79,143,255,0.1) 0%,rgba(167,139,250,0.1) 100%);border:1px solid rgba(79,143,255,0.2);border-radius:var(--radius-xl);padding:48px;text-align:center;margin:48px 0}
.cta-box h2{font-size:1.8rem;font-weight:800;margin-bottom:12px}
.cta-box p{color:var(--text-secondary);margin-bottom:24px}
.related{margin:48px 0;padding:32px;background:var(--card);border:1px solid var(--border);border-radius:var(--radius-lg)}
.related h3{font-size:1.1rem;font-weight:700;margin-bottom:16px}
.related a{display:block;padding:8px 0;font-size:.95rem}
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;transition:color .2s}
.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:640px){
.article{padding:40px 0 30px}
.article h1{font-size:1.8rem}
.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}
.footer-grid{grid-template-columns:1fr}
}
</style>
</head>
<body>
<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="/#playground">Try It Free</a>
<a href="/#pricing">Pricing</a>
<a href="/docs">API Docs</a>
<a href="/#pricing" class="btn btn-primary btn-sm nav-cta">Get API Key</a>
</div>
<button class="nav-mobile" onclick="document.querySelector('.nav-links').classList.toggle('show')" aria-label="Menu"></button>
</div>
</nav>
<article class="article">
<div class="container">
<h1>Generate OG Images & Social Media Previews with a Screenshot API</h1>
<p>When you share a link on Twitter, LinkedIn, or Slack, the platform fetches an Open Graph image to display as a preview card. Static OG images are fine for homepages — but what about blog posts, user profiles, or product pages that need <strong>unique, dynamic preview images</strong>?</p>
<p>SnapAPI lets you render any HTML page as a PNG image via a simple API call. Build an HTML template with your title, author, and branding, host it on a URL, and let SnapAPI screenshot it into a pixel-perfect OG image.</p>
<h2>How It Works</h2>
<p>The workflow is straightforward:</p>
<ol>
<li><strong>Create an HTML template</strong> — Design your OG card layout (1200×630px) with CSS. Use query parameters for dynamic content.</li>
<li><strong>Call SnapAPI</strong> — Pass the template URL to the screenshot endpoint. SnapAPI renders it in a real Chromium browser.</li>
<li><strong>Serve the image</strong> — Use the returned PNG as your <code>og:image</code> meta tag, or cache it on your CDN.</li>
</ol>
<h2>Code Example</h2>
<h3>Screenshot an OG Template</h3>
<div class="code-window">
<div class="code-titlebar"><span class="code-dot"></span><span class="code-dot"></span><span class="code-dot"></span><span>Node.js</span></div>
<div class="code-body"><pre><span class="kw">const</span> response = <span class="kw">await</span> <span class="fn">fetch</span>(<span class="str">'https://snapapi.eu/v1/screenshot'</span>, {
method: <span class="str">'POST'</span>,
headers: {
<span class="str">'Content-Type'</span>: <span class="str">'application/json'</span>,
<span class="str">'X-API-Key'</span>: process.env.<span class="prop">SNAPAPI_KEY</span>
},
body: JSON.stringify({
url: <span class="str">'https://yoursite.com/og-template?title=My+Post&amp;author=Jane'</span>,
width: <span class="prop">1200</span>,
height: <span class="prop">630</span>,
format: <span class="str">'png'</span>
})
});
<span class="kw">const</span> imageBuffer = <span class="kw">await</span> response.<span class="fn">arrayBuffer</span>();
<span class="cmt">// Upload to S3, serve from CDN, or return directly</span></pre></div>
</div>
<h3>Dynamic Meta Tags</h3>
<p>Point your <code>og:image</code> to a serverless function that calls SnapAPI on-the-fly:</p>
<div class="code-window">
<div class="code-titlebar"><span class="code-dot"></span><span class="code-dot"></span><span class="code-dot"></span><span>HTML</span></div>
<div class="code-body"><pre><span class="cmt">&lt;!-- In your page's &lt;head&gt; --&gt;</span>
&lt;meta property=<span class="str">"og:image"</span>
content=<span class="str">"https://yoursite.com/api/og?title=My+Blog+Post"</span> /&gt;
&lt;meta property=<span class="str">"og:image:width"</span> content=<span class="str">"1200"</span> /&gt;
&lt;meta property=<span class="str">"og:image:height"</span> content=<span class="str">"630"</span> /&gt;</pre></div>
</div>
<h2>Why Use SnapAPI for OG Images?</h2>
<ul>
<li><strong>Real browser rendering</strong> — Full CSS support, custom fonts, gradients, SVGs. No template language limitations.</li>
<li><strong>EU-hosted, GDPR compliant</strong> — All rendering happens on servers in Germany. No data leaves the EU.</li>
<li><strong>Fast</strong> — Typical render times under 2 seconds. Cache the result and serve instantly.</li>
<li><strong>Simple API</strong> — One POST request, one image back. No SDKs required.</li>
</ul>
<div class="cta-box">
<h2>Start Generating OG Images</h2>
<p>Try SnapAPI free in the playground — no signup needed.</p>
<a href="/#pricing" class="btn btn-primary btn-lg">Get Your API Key →</a>
</div>
<div class="related">
<h3>Related Use Cases</h3>
<a href="/use-cases/website-monitoring">Visual Website Monitoring & Regression Testing →</a>
<a href="/use-cases/pdf-reports">Generate Visual Reports & Thumbnails from Web Content →</a>
</div>
</div>
</article>
<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="/#playground">Playground</a><a href="/docs">API Docs</a></div>
<div class="footer-col"><h5>Developers</h5><a href="/docs">Swagger / OpenAPI</a><a href="/health">Status</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>Linzer Straße 192/1/2, 1140 Wien, Austria 🇦🇹</span>
<span>EU-hosted 🇪🇺 · All data stays in Europe</span>
</div>
</footer>
</body>
</html>

View file

@ -0,0 +1,211 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Visual Website Monitoring & Regression Testing with a Screenshot API | SnapAPI</title>
<meta name="description" content="Use SnapAPI for automated visual website monitoring and regression testing. Take scheduled screenshots to detect layout changes, broken pages, and visual bugs.">
<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">
<link rel="canonical" href="https://snapapi.eu/use-cases/website-monitoring">
<meta property="og:title" content="Visual Website Monitoring & Regression Testing with a Screenshot API">
<meta property="og:description" content="Automate visual regression testing with scheduled screenshots via SnapAPI.">
<meta property="og:type" content="article">
<meta property="og:url" content="https://snapapi.eu/use-cases/website-monitoring">
<meta property="og:image" content="https://snapapi.eu/og-image.png">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Visual Website Monitoring & Regression Testing with a Screenshot API">
<meta name="twitter:description" content="Automate visual regression testing with scheduled screenshots via SnapAPI.">
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"Article","headline":"Visual Website Monitoring & Regression Testing with a Screenshot API","description":"Use SnapAPI for automated visual website monitoring and regression testing.","author":{"@type":"Organization","name":"Cloonar Technologies GmbH"},"publisher":{"@type":"Organization","name":"SnapAPI","url":"https://snapapi.eu"},"datePublished":"2026-03-02","url":"https://snapapi.eu/use-cases/website-monitoring"}
</script>
<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;--primary-glow:rgba(79,143,255,0.15);
--accent:#10b981;--accent-glow:rgba(16,185,129,0.15);
--purple:#a78bfa;--orange:#f59e0b;--pink:#ec4899;
--gradient:linear-gradient(135deg,#4f8fff 0%,#a78bfa 50%,#ec4899 100%);
--radius:12px;--radius-lg:16px;--radius-xl:24px;
}
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)}
::selection{background:var(--primary);color:#fff}
.container{max-width:800px;margin:0 auto;padding:0 24px}
.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);box-shadow:0 6px 28px rgba(79,143,255,0.4);color:#fff}
.btn-lg{padding:16px 36px;font-size:1.05rem;border-radius:12px}
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;transition:color .2s}
.nav-links a:hover{color:var(--text)}
.nav-cta{margin-left:8px}
.nav-mobile{display:none;background:none;border:none;color:var(--text);font-size:1.5rem;cursor:pointer}
.article{padding:80px 0 60px}
.article h1{font-size:2.5rem;font-weight:800;line-height:1.2;margin-bottom:24px}
.article h2{font-size:1.6rem;font-weight:700;margin:48px 0 16px}
.article h3{font-size:1.2rem;font-weight:600;margin:32px 0 12px}
.article p{color:var(--text-secondary);font-size:1rem;line-height:1.8;margin-bottom:16px}
.article ul,.article ol{color:var(--text-secondary);margin:0 0 16px 24px;line-height:1.8}
.article li{margin-bottom:8px}
.code-window{background:var(--card);border:1px solid var(--border);border-radius:var(--radius-lg);overflow:hidden;margin:24px 0;box-shadow:0 10px 40px rgba(0,0,0,0.2)}
.code-titlebar{display:flex;align-items:center;gap:8px;padding:14px 20px;background:rgba(0,0,0,0.2);border-bottom:1px solid var(--border)}
.code-dot{width:12px;height:12px;border-radius:50%}
.code-dot:nth-child(1){background:#ff5f57}.code-dot:nth-child(2){background:#ffbd2e}.code-dot:nth-child(3){background:#28c840}
.code-titlebar span{flex:1;text-align:center;font-size:.8rem;color:var(--muted);font-weight:500}
.code-body{padding:24px;font-family:'JetBrains Mono',monospace;font-size:.85rem;line-height:1.9;overflow-x:auto;color:var(--text-secondary)}
.code-body .kw{color:var(--purple)}.code-body .str{color:var(--accent)}.code-body .cmt{color:#475569;font-style:italic}.code-body .fn{color:var(--primary-light)}.code-body .prop{color:var(--orange)}
.cta-box{background:linear-gradient(135deg,rgba(79,143,255,0.1) 0%,rgba(167,139,250,0.1) 100%);border:1px solid rgba(79,143,255,0.2);border-radius:var(--radius-xl);padding:48px;text-align:center;margin:48px 0}
.cta-box h2{font-size:1.8rem;font-weight:800;margin-bottom:12px}
.cta-box p{color:var(--text-secondary);margin-bottom:24px}
.related{margin:48px 0;padding:32px;background:var(--card);border:1px solid var(--border);border-radius:var(--radius-lg)}
.related h3{font-size:1.1rem;font-weight:700;margin-bottom:16px}
.related a{display:block;padding:8px 0;font-size:.95rem}
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;transition:color .2s}
.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:640px){
.article{padding:40px 0 30px}
.article h1{font-size:1.8rem}
.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}
.footer-grid{grid-template-columns:1fr}
}
</style>
</head>
<body>
<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="/#playground">Try It Free</a>
<a href="/#pricing">Pricing</a>
<a href="/docs">API Docs</a>
<a href="/#pricing" class="btn btn-primary btn-sm nav-cta">Get API Key</a>
</div>
<button class="nav-mobile" onclick="document.querySelector('.nav-links').classList.toggle('show')" aria-label="Menu"></button>
</div>
</nav>
<article class="article">
<div class="container">
<h1>Visual Website Monitoring & Regression Testing</h1>
<p>CSS changes, dependency updates, or CMS edits can silently break your site's layout. Traditional uptime monitoring checks if a page returns 200 — but it won't tell you if your hero section is now overlapping your navigation bar.</p>
<p><strong>Visual monitoring</strong> solves this by taking periodic screenshots and comparing them against a known-good baseline. SnapAPI provides the screenshot capture part — you bring the comparison logic or just review the images manually.</p>
<h2>How It Works</h2>
<ol>
<li><strong>Schedule screenshots</strong> — Use a cron job or CI pipeline to call SnapAPI at regular intervals (daily, hourly, per-deploy).</li>
<li><strong>Store the results</strong> — Save screenshots to S3, a database, or your local filesystem with timestamps.</li>
<li><strong>Compare</strong> — Use pixel-diff tools like <code>pixelmatch</code> or <code>resemble.js</code> to detect visual changes. Alert when the diff exceeds a threshold.</li>
</ol>
<h2>Code Example</h2>
<h3>Daily Screenshot Cron Job</h3>
<div class="code-window">
<div class="code-titlebar"><span class="code-dot"></span><span class="code-dot"></span><span class="code-dot"></span><span>Node.js</span></div>
<div class="code-body"><pre><span class="kw">import</span> fs <span class="kw">from</span> <span class="str">'fs'</span>;
<span class="kw">const</span> PAGES = [
<span class="str">'https://yoursite.com'</span>,
<span class="str">'https://yoursite.com/pricing'</span>,
<span class="str">'https://yoursite.com/docs'</span>,
];
<span class="kw">for</span> (<span class="kw">const</span> url <span class="kw">of</span> PAGES) {
<span class="kw">const</span> res = <span class="kw">await</span> <span class="fn">fetch</span>(<span class="str">'https://snapapi.eu/v1/screenshot'</span>, {
method: <span class="str">'POST'</span>,
headers: {
<span class="str">'Content-Type'</span>: <span class="str">'application/json'</span>,
<span class="str">'X-API-Key'</span>: process.env.<span class="prop">SNAPAPI_KEY</span>
},
body: JSON.stringify({
url,
width: <span class="prop">1440</span>,
height: <span class="prop">900</span>,
format: <span class="str">'png'</span>,
fullPage: <span class="prop">true</span>
})
});
<span class="kw">const</span> slug = <span class="kw">new</span> URL(url).pathname.replace(<span class="str">/\//g</span>, <span class="str">'_'</span>) || <span class="str">'home'</span>;
<span class="kw">const</span> date = <span class="kw">new</span> Date().toISOString().slice(<span class="prop">0</span>, <span class="prop">10</span>);
fs.<span class="fn">writeFileSync</span>(<span class="str">`screenshots/${slug}_${date}.png`</span>,
Buffer.from(<span class="kw">await</span> res.<span class="fn">arrayBuffer</span>()));
}
<span class="cmt">// Run via: node monitor.mjs (cron: 0 6 * * *)</span></pre></div>
</div>
<h3>CI Pipeline Integration</h3>
<p>Add visual regression checks to your deployment pipeline. Take a screenshot after each deploy and compare it to the previous version:</p>
<div class="code-window">
<div class="code-titlebar"><span class="code-dot"></span><span class="code-dot"></span><span class="code-dot"></span><span>Bash</span></div>
<div class="code-body"><pre><span class="cmt"># In your CI pipeline (GitHub Actions, GitLab CI, etc.)</span>
curl -X POST https://snapapi.eu/v1/screenshot \
-H <span class="str">"Content-Type: application/json"</span> \
-H <span class="str">"X-API-Key: $SNAPAPI_KEY"</span> \
-d <span class="str">'{"url":"https://staging.yoursite.com","width":1440,"height":900,"format":"png"}'</span> \
--output screenshot-after-deploy.png
<span class="cmt"># Compare with baseline using pixelmatch, ImageMagick, etc.</span></pre></div>
</div>
<h2>Use Cases for Visual Monitoring</h2>
<ul>
<li><strong>Pre/post deploy checks</strong> — Catch CSS regressions before users see them.</li>
<li><strong>Third-party widget monitoring</strong> — Detect when embedded widgets (chat, analytics banners) break your layout.</li>
<li><strong>Competitor tracking</strong> — Screenshot competitor pages periodically to track pricing or feature changes.</li>
<li><strong>Compliance archiving</strong> — Keep dated visual records of your pages for regulatory requirements.</li>
</ul>
<div class="cta-box">
<h2>Start Monitoring Visually</h2>
<p>Try SnapAPI free in the playground. Set up your first visual monitor in minutes.</p>
<a href="/#pricing" class="btn btn-primary btn-lg">Get Your API Key →</a>
</div>
<div class="related">
<h3>Related Use Cases</h3>
<a href="/use-cases/social-media-previews">Generate OG Images & Social Media Previews →</a>
<a href="/use-cases/pdf-reports">Generate Visual Reports & Thumbnails from Web Content →</a>
</div>
</div>
</article>
<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="/#playground">Playground</a><a href="/docs">API Docs</a></div>
<div class="footer-col"><h5>Developers</h5><a href="/docs">Swagger / OpenAPI</a><a href="/health">Status</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>Linzer Straße 192/1/2, 1140 Wien, Austria 🇦🇹</span>
<span>EU-hosted 🇪🇺 · All data stays in Europe</span>
</div>
</footer>
</body>
</html>

View file

@ -130,6 +130,11 @@ for (const page of ["privacy", "terms", "impressum", "status", "usage"]) {
app.get(`/${page}`, (_req, res) => res.redirect(301, `/${page}.html`)); app.get(`/${page}`, (_req, res) => res.redirect(301, `/${page}.html`));
} }
// Clean URLs for use case pages
for (const page of ["social-media-previews", "website-monitoring", "pdf-reports"]) {
app.get(`/use-cases/${page}`, (_req, res) => res.redirect(301, `/use-cases/${page}.html`));
}
// Static files (landing page) // Static files (landing page)
app.use(express.static(path.join(__dirname, "../public"), { etag: true })); app.use(express.static(path.join(__dirname, "../public"), { etag: true }));

View file

@ -0,0 +1,84 @@
import { describe, it, expect, vi } from 'vitest'
import request from 'supertest'
import express from 'express'
import path from 'path'
import { fileURLToPath } from 'url'
import fs from 'fs'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const publicDir = path.join(__dirname, '../../../public')
// Build a minimal app that mirrors index.ts routing
function createApp() {
const app = express()
// Clean URLs for use-case pages
const useCasePages = ['social-media-previews', 'website-monitoring', 'pdf-reports']
for (const page of useCasePages) {
app.get(`/use-cases/${page}`, (_req, res) => res.redirect(301, `/use-cases/${page}.html`))
}
app.use(express.static(publicDir, { etag: true }))
return app
}
const useCases = [
{ slug: 'social-media-previews', title: 'OG Images', keywords: ['og image', 'social media preview'] },
{ slug: 'website-monitoring', title: 'Website Monitoring', keywords: ['website screenshot monitoring', 'visual regression'] },
{ slug: 'pdf-reports', title: 'Reports', keywords: ['thumbnail', 'web page preview'] },
]
describe('Use Case Pages', () => {
const app = createApp()
for (const uc of useCases) {
describe(uc.slug, () => {
it(`GET /use-cases/${uc.slug}.html returns 200`, async () => {
const res = await request(app).get(`/use-cases/${uc.slug}.html`)
expect(res.status).toBe(200)
expect(res.headers['content-type']).toContain('text/html')
})
it(`GET /use-cases/${uc.slug} redirects 301 to .html`, async () => {
const res = await request(app).get(`/use-cases/${uc.slug}`)
expect(res.status).toBe(301)
expect(res.headers.location).toBe(`/use-cases/${uc.slug}.html`)
})
it('contains required SEO elements', async () => {
const res = await request(app).get(`/use-cases/${uc.slug}.html`)
const html = res.text
// Title tag
expect(html).toMatch(/<title>.+<\/title>/)
// Meta description
expect(html).toMatch(/<meta name="description" content=".+"/)
// OG tags
expect(html).toMatch(/<meta property="og:title"/)
expect(html).toMatch(/<meta property="og:description"/)
expect(html).toMatch(/<meta property="og:type" content="article"/)
// JSON-LD
expect(html).toContain('"@type":"Article"')
// H1
expect(html).toMatch(/<h1[^>]*>.+<\/h1>/)
// Canonical
expect(html).toMatch(/<link rel="canonical"/)
})
})
}
it('sitemap contains all use case URLs', () => {
const sitemap = fs.readFileSync(path.join(publicDir, 'sitemap.xml'), 'utf-8')
for (const uc of useCases) {
expect(sitemap).toContain(`https://snapapi.eu/use-cases/${uc.slug}`)
}
})
it('index.html contains use cases section', () => {
const index = fs.readFileSync(path.join(publicDir, 'index.html'), 'utf-8')
expect(index).toContain('id="use-cases"')
for (const uc of useCases) {
expect(index).toContain(`/use-cases/${uc.slug}`)
}
})
})