feat: add developer blog with two posts
Some checks failed
Build & Deploy to Staging / Build & Deploy to Staging (push) Has been cancelled
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:
parent
9609501d7b
commit
56c7a87f3c
7 changed files with 729 additions and 0 deletions
|
|
@ -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 }));
|
||||
|
||||
|
|
|
|||
168
src/routes/__tests__/blog.test.ts
Normal file
168
src/routes/__tests__/blog.test.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue