feat: add developer blog with two posts
Some checks failed
Build & Deploy to Staging / Build & Deploy to Staging (push) Has been cancelled

- Blog index page (public/blog.html) with dark theme
- Post 1: Why You Need a Screenshot API (~800 words)
- Post 2: Screenshot API Performance & Caching (~600 words)
- Express routes: /blog → /blog.html, /blog/:slug → /blog/:slug.html
- Blog link added to nav and footer on index.html
- Sitemap updated with blog URLs
- Full test coverage (19 new tests, 190 total passing)
This commit is contained in:
Hoid 2026-03-02 21:10:29 +01:00
parent 9609501d7b
commit 56c7a87f3c
7 changed files with 729 additions and 0 deletions

View file

@ -141,6 +141,10 @@ app.get("/guides/quick-start", (_req, res) => res.redirect(301, "/guides/quick-s
app.get("/pricing", (_req, res) => res.redirect(301, "/pricing.html"));
app.get("/changelog", (_req, res) => res.redirect(301, "/changelog.html"));
// Blog routes
app.get("/blog", (_req, res) => res.redirect(301, "/blog.html"));
app.get("/blog/:slug([^.]+)", (req, res) => res.redirect(301, `/blog/${req.params.slug}.html`));
// Static files (landing page)
app.use(express.static(path.join(__dirname, "../public"), { etag: true }));

View file

@ -0,0 +1,168 @@
import { describe, it, expect } 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')
function createApp() {
const app = express()
// Blog routes matching index.ts
app.get('/blog', (_req, res) => res.redirect(301, '/blog.html'))
app.get('/blog/:slug([^.]+)', (_req, res) => res.redirect(301, `/blog/${_req.params.slug}.html`))
app.use(express.static(publicDir, { etag: true }))
return app
}
describe('Blog Index Page', () => {
const app = createApp()
it('GET /blog.html returns 200', async () => {
const res = await request(app).get('/blog.html')
expect(res.status).toBe(200)
expect(res.headers['content-type']).toContain('text/html')
})
it('GET /blog redirects 301 to /blog.html', async () => {
const res = await request(app).get('/blog')
expect(res.status).toBe(301)
expect(res.headers.location).toBe('/blog.html')
})
it('contains required SEO elements', async () => {
const res = await request(app).get('/blog.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"/)
expect(html).toMatch(/<meta name="twitter:card"/)
})
it('contains JSON-LD Blog schema', async () => {
const res = await request(app).get('/blog.html')
const html = res.text
expect(html).toContain('"@type":"Blog"')
})
it('links to both blog posts', async () => {
const res = await request(app).get('/blog.html')
const html = res.text
expect(html).toContain('/blog/why-screenshot-api')
expect(html).toContain('/blog/screenshot-api-performance')
})
})
describe('Blog Post: Why Screenshot API', () => {
const app = createApp()
it('GET /blog/why-screenshot-api.html returns 200', async () => {
const res = await request(app).get('/blog/why-screenshot-api.html')
expect(res.status).toBe(200)
expect(res.headers['content-type']).toContain('text/html')
})
it('GET /blog/why-screenshot-api redirects 301 to .html', async () => {
const res = await request(app).get('/blog/why-screenshot-api')
expect(res.status).toBe(301)
expect(res.headers.location).toBe('/blog/why-screenshot-api.html')
})
it('contains required SEO elements', async () => {
const res = await request(app).get('/blog/why-screenshot-api.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\/why-screenshot-api"/)
expect(html).toMatch(/<meta name="twitter:card"/)
})
it('contains JSON-LD BlogPosting schema', async () => {
const res = await request(app).get('/blog/why-screenshot-api.html')
const html = res.text
expect(html).toContain('"@type":"BlogPosting"')
})
it('has substantial content (~800 words)', async () => {
const res = await request(app).get('/blog/why-screenshot-api.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/why-screenshot-api.html')
const html = res.text
expect(html).toContain('screenshot API')
expect(html).toContain('Puppeteer')
})
})
describe('Blog Post: Screenshot API Performance', () => {
const app = createApp()
it('GET /blog/screenshot-api-performance.html returns 200', async () => {
const res = await request(app).get('/blog/screenshot-api-performance.html')
expect(res.status).toBe(200)
expect(res.headers['content-type']).toContain('text/html')
})
it('GET /blog/screenshot-api-performance redirects 301 to .html', async () => {
const res = await request(app).get('/blog/screenshot-api-performance')
expect(res.status).toBe(301)
expect(res.headers.location).toBe('/blog/screenshot-api-performance.html')
})
it('contains required SEO elements', async () => {
const res = await request(app).get('/blog/screenshot-api-performance.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\/screenshot-api-performance"/)
expect(html).toMatch(/<meta name="twitter:card"/)
})
it('contains JSON-LD BlogPosting schema', async () => {
const res = await request(app).get('/blog/screenshot-api-performance.html')
const html = res.text
expect(html).toContain('"@type":"BlogPosting"')
})
it('has substantial content (~600 words)', async () => {
const res = await request(app).get('/blog/screenshot-api-performance.html')
const text = res.text.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ')
const wordCount = text.split(' ').filter(w => w.length > 0).length
expect(wordCount).toBeGreaterThan(400)
})
it('contains relevant keywords', async () => {
const res = await request(app).get('/blog/screenshot-api-performance.html')
const html = res.text
expect(html).toContain('caching')
expect(html).toContain('performance')
})
})
describe('Blog Sitemap & Navigation', () => {
it('sitemap contains blog URLs', () => {
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/why-screenshot-api')
expect(sitemap).toContain('https://snapapi.eu/blog/screenshot-api-performance')
})
it('index.html has Blog link in nav or footer', () => {
const index = fs.readFileSync(path.join(publicDir, 'index.html'), 'utf-8')
expect(index).toContain('/blog')
})
})