Add GET endpoint support, response caching, and update landing page
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 9m11s
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 9m11s
- Add GET /v1/screenshot endpoint with query parameter support - Support API key authentication via ?key= query parameter - Implement in-memory LRU cache with configurable TTL (5min) and size limits (100MB) - Add X-Cache headers (HIT/MISS) to indicate cache status - Add cache bypass option via ?cache=false parameter - Update OpenAPI documentation with GET endpoint and caching info - Add GET/Embed code examples to landing page hero section - Add Response Caching and GET Request Support feature cards - Update features grid layout to accommodate new features
This commit is contained in:
parent
609e7d0808
commit
44e31e355c
5 changed files with 433 additions and 18 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import { Router } from "express";
|
||||
import { takeScreenshot } from "../services/screenshot.js";
|
||||
import { screenshotCache } from "../services/cache.js";
|
||||
import logger from "../services/logger.js";
|
||||
|
||||
export const screenshotRouter = Router();
|
||||
|
|
@ -128,32 +129,217 @@ export const screenshotRouter = Router();
|
|||
* content:
|
||||
* application/json:
|
||||
* schema: { $ref: "#/components/schemas/Error" }
|
||||
* get:
|
||||
* tags: [Screenshots]
|
||||
* summary: Take a screenshot via GET request (authenticated)
|
||||
* description: >
|
||||
* Capture a pixel-perfect, unwatermarked screenshot using GET request.
|
||||
* All parameters are passed via query string. Perfect for image embeds:
|
||||
* `<img src="https://snapapi.eu/v1/screenshot?url=https://example.com&key=YOUR_KEY">`
|
||||
* operationId: takeScreenshotGet
|
||||
* security:
|
||||
* - BearerAuth: []
|
||||
* - ApiKeyAuth: []
|
||||
* - QueryKeyAuth: []
|
||||
* parameters:
|
||||
* - name: url
|
||||
* in: query
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* format: uri
|
||||
* description: URL to capture
|
||||
* example: https://example.com
|
||||
* - name: key
|
||||
* in: query
|
||||
* schema:
|
||||
* type: string
|
||||
* description: API key (alternative to header auth)
|
||||
* example: sk_test_1234567890abcdef
|
||||
* - name: format
|
||||
* in: query
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [png, jpeg, webp]
|
||||
* default: png
|
||||
* description: Output image format
|
||||
* - name: width
|
||||
* in: query
|
||||
* schema:
|
||||
* type: integer
|
||||
* minimum: 320
|
||||
* maximum: 3840
|
||||
* default: 1280
|
||||
* description: Viewport width in pixels
|
||||
* - name: height
|
||||
* in: query
|
||||
* schema:
|
||||
* type: integer
|
||||
* minimum: 200
|
||||
* maximum: 2160
|
||||
* default: 800
|
||||
* description: Viewport height in pixels
|
||||
* - name: fullPage
|
||||
* in: query
|
||||
* schema:
|
||||
* type: boolean
|
||||
* default: false
|
||||
* description: Capture full scrollable page instead of viewport only
|
||||
* - name: quality
|
||||
* in: query
|
||||
* schema:
|
||||
* type: integer
|
||||
* minimum: 1
|
||||
* maximum: 100
|
||||
* default: 80
|
||||
* description: JPEG/WebP quality (ignored for PNG)
|
||||
* - name: waitForSelector
|
||||
* in: query
|
||||
* schema:
|
||||
* type: string
|
||||
* description: CSS selector to wait for before capturing
|
||||
* - name: deviceScale
|
||||
* in: query
|
||||
* schema:
|
||||
* type: number
|
||||
* minimum: 1
|
||||
* maximum: 3
|
||||
* default: 1
|
||||
* description: Device scale factor (2 = Retina)
|
||||
* - name: delay
|
||||
* in: query
|
||||
* schema:
|
||||
* type: integer
|
||||
* minimum: 0
|
||||
* maximum: 5000
|
||||
* default: 0
|
||||
* description: Extra delay in ms after page load before capturing
|
||||
* - name: waitUntil
|
||||
* in: query
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [load, domcontentloaded, networkidle0, networkidle2]
|
||||
* default: domcontentloaded
|
||||
* description: Page load event to wait for before capturing
|
||||
* - name: cache
|
||||
* in: query
|
||||
* schema:
|
||||
* type: boolean
|
||||
* default: true
|
||||
* description: Enable response caching (5-minute TTL)
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Screenshot image binary
|
||||
* headers:
|
||||
* X-Cache:
|
||||
* description: Cache status
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [HIT, MISS]
|
||||
* content:
|
||||
* image/png:
|
||||
* schema: { type: string, format: binary }
|
||||
* image/jpeg:
|
||||
* schema: { type: string, format: binary }
|
||||
* image/webp:
|
||||
* schema: { type: string, format: binary }
|
||||
* 400:
|
||||
* description: Invalid request (bad URL, blocked domain, etc.)
|
||||
* content:
|
||||
* application/json:
|
||||
* schema: { $ref: "#/components/schemas/Error" }
|
||||
* 401:
|
||||
* description: Missing API key
|
||||
* content:
|
||||
* application/json:
|
||||
* schema: { $ref: "#/components/schemas/Error" }
|
||||
* 403:
|
||||
* description: Invalid API key
|
||||
* content:
|
||||
* application/json:
|
||||
* schema: { $ref: "#/components/schemas/Error" }
|
||||
* 429:
|
||||
* description: Rate or usage limit exceeded
|
||||
* content:
|
||||
* application/json:
|
||||
* schema: { $ref: "#/components/schemas/Error" }
|
||||
* 503:
|
||||
* description: Service busy (queue full)
|
||||
* content:
|
||||
* application/json:
|
||||
* schema: { $ref: "#/components/schemas/Error" }
|
||||
* 504:
|
||||
* description: Screenshot timed out
|
||||
* content:
|
||||
* application/json:
|
||||
* schema: { $ref: "#/components/schemas/Error" }
|
||||
*/
|
||||
screenshotRouter.post("/", async (req: any, res) => {
|
||||
const { url, format, width, height, fullPage, quality, waitForSelector, deviceScale, delay, waitUntil } = req.body;
|
||||
// Shared handler for both GET and POST requests
|
||||
async function handleScreenshotRequest(req: any, res: any) {
|
||||
// Extract parameters from both query (GET) and body (POST)
|
||||
const source = req.method === "GET" ? req.query : req.body;
|
||||
|
||||
const {
|
||||
url,
|
||||
format,
|
||||
width,
|
||||
height,
|
||||
fullPage,
|
||||
quality,
|
||||
waitForSelector,
|
||||
deviceScale,
|
||||
delay,
|
||||
waitUntil,
|
||||
cache,
|
||||
} = source;
|
||||
|
||||
if (!url || typeof url !== "string") {
|
||||
res.status(400).json({ error: "Missing required parameter: url" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Normalize parameters
|
||||
const params = {
|
||||
url,
|
||||
format: format || "png",
|
||||
width: width ? parseInt(width, 10) : undefined,
|
||||
height: height ? parseInt(height, 10) : undefined,
|
||||
fullPage: fullPage === true || fullPage === "true",
|
||||
quality: quality ? parseInt(quality, 10) : undefined,
|
||||
waitForSelector,
|
||||
deviceScale: deviceScale ? parseFloat(deviceScale) : undefined,
|
||||
delay: delay ? parseInt(delay, 10) : undefined,
|
||||
waitUntil: ["load", "domcontentloaded", "networkidle0", "networkidle2"].includes(waitUntil) ? waitUntil : undefined,
|
||||
cache,
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await takeScreenshot({
|
||||
url,
|
||||
format: format || "png",
|
||||
width: width ? parseInt(width, 10) : undefined,
|
||||
height: height ? parseInt(height, 10) : undefined,
|
||||
fullPage: fullPage === true || fullPage === "true",
|
||||
quality: quality ? parseInt(quality, 10) : undefined,
|
||||
waitForSelector,
|
||||
deviceScale: deviceScale ? parseFloat(deviceScale) : undefined,
|
||||
delay: delay ? parseInt(delay, 10) : undefined,
|
||||
waitUntil: ["load", "domcontentloaded", "networkidle0", "networkidle2"].includes(waitUntil) ? waitUntil : undefined,
|
||||
});
|
||||
// Check cache first (if not bypassed)
|
||||
let cacheHit = false;
|
||||
if (!screenshotCache.shouldBypass(params)) {
|
||||
const cached = screenshotCache.get(params);
|
||||
if (cached) {
|
||||
res.setHeader("Content-Type", cached.contentType);
|
||||
res.setHeader("Content-Length", cached.buffer.length);
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
res.setHeader("X-Cache", "HIT");
|
||||
res.send(cached.buffer);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Take new screenshot
|
||||
const result = await takeScreenshot(params);
|
||||
|
||||
// Cache the result (if not bypassed)
|
||||
if (!screenshotCache.shouldBypass(params)) {
|
||||
screenshotCache.put(params, result.buffer, result.contentType);
|
||||
}
|
||||
|
||||
res.setHeader("Content-Type", result.contentType);
|
||||
res.setHeader("Content-Length", result.buffer.length);
|
||||
res.setHeader("Cache-Control", "no-store");
|
||||
res.setHeader("X-Cache", "MISS");
|
||||
res.send(result.buffer);
|
||||
} catch (err: any) {
|
||||
logger.error({ err: err.message, url }, "Screenshot failed");
|
||||
|
|
@ -173,4 +359,8 @@ screenshotRouter.post("/", async (req: any, res) => {
|
|||
|
||||
res.status(500).json({ error: "Screenshot failed", details: err.message });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Register both GET and POST routes
|
||||
screenshotRouter.get("/", handleScreenshotRequest);
|
||||
screenshotRouter.post("/", handleScreenshotRequest);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue