fix: privacy 404 + enhanced playground controls
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 9m7s
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 9m7s
BUG-010: Add 301 redirects for clean URLs (/privacy → /privacy.html etc.) and fix inconsistent href links across legal pages. FEATURE: Enhanced playground with fullPage, quality, deviceScale, waitUntil, and waitForSelector controls for better API evaluation.
This commit is contained in:
parent
d20fbbfe2e
commit
db1fa8d506
6 changed files with 79 additions and 22 deletions
|
|
@ -162,9 +162,9 @@ footer{border-top:1px solid var(--border);padding:48px 24px 32px;background:var(
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-col">
|
<div class="footer-col">
|
||||||
<h5>Legal</h5>
|
<h5>Legal</h5>
|
||||||
<a href="/impressum">Impressum</a>
|
<a href="/impressum.html">Impressum</a>
|
||||||
<a href="/privacy">Privacy Policy</a>
|
<a href="/privacy.html">Privacy Policy</a>
|
||||||
<a href="/terms">Terms of Service</a>
|
<a href="/terms.html">Terms of Service</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-bottom">
|
<div class="footer-bottom">
|
||||||
|
|
|
||||||
|
|
@ -302,13 +302,19 @@ footer{border-top:1px solid var(--border);padding:48px 24px 32px;background:var(
|
||||||
<label>URL to capture</label>
|
<label>URL to capture</label>
|
||||||
<input type="url" id="pg-url" value="https://example.com" placeholder="https://example.com">
|
<input type="url" id="pg-url" value="https://example.com" placeholder="https://example.com">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
|
||||||
<label>Format</label>
|
<div>
|
||||||
<select id="pg-format">
|
<label>Format</label>
|
||||||
<option value="png">PNG</option>
|
<select id="pg-format">
|
||||||
<option value="jpeg">JPEG</option>
|
<option value="png">PNG</option>
|
||||||
<option value="webp">WebP</option>
|
<option value="jpeg">JPEG</option>
|
||||||
</select>
|
<option value="webp">WebP</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Quality</label>
|
||||||
|
<input type="number" id="pg-quality" value="80" min="1" max="100" title="JPEG/WebP quality (1-100). Ignored for PNG.">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -320,6 +326,34 @@ footer{border-top:1px solid var(--border);padding:48px 24px 32px;background:var(
|
||||||
<input type="number" id="pg-height" value="800" min="200" max="1080">
|
<input type="number" id="pg-height" value="800" min="200" max="1080">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
|
||||||
|
<div>
|
||||||
|
<label>Device Scale</label>
|
||||||
|
<select id="pg-scale">
|
||||||
|
<option value="1">1x</option>
|
||||||
|
<option value="2">2x (Retina)</option>
|
||||||
|
<option value="3">3x</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Wait Until</label>
|
||||||
|
<select id="pg-waituntil">
|
||||||
|
<option value="domcontentloaded">DOM Ready</option>
|
||||||
|
<option value="load">Page Load</option>
|
||||||
|
<option value="networkidle0">Network Idle</option>
|
||||||
|
<option value="networkidle2">Network Idle 2</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;text-transform:none;letter-spacing:0;font-size:.9rem">
|
||||||
|
<input type="checkbox" id="pg-fullpage" style="width:auto;padding:0;accent-color:var(--primary)"> Capture full page
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Wait for Selector <span style="font-weight:400;text-transform:none;letter-spacing:0">(optional)</span></label>
|
||||||
|
<input type="text" id="pg-selector" placeholder="#content, .loaded, img" title="CSS selector to wait for before capturing">
|
||||||
|
</div>
|
||||||
<button class="btn btn-primary" onclick="runPlayground()" id="pg-btn" style="margin-top:auto">
|
<button class="btn btn-primary" onclick="runPlayground()" id="pg-btn" style="margin-top:auto">
|
||||||
Take Screenshot →
|
Take Screenshot →
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -632,8 +666,13 @@ footer{border-top:1px solid var(--border);padding:48px 24px 32px;background:var(
|
||||||
async function runPlayground(){
|
async function runPlayground(){
|
||||||
var url=document.getElementById('pg-url').value;
|
var url=document.getElementById('pg-url').value;
|
||||||
var format=document.getElementById('pg-format').value;
|
var format=document.getElementById('pg-format').value;
|
||||||
|
var quality=parseInt(document.getElementById('pg-quality').value)||80;
|
||||||
var width=parseInt(document.getElementById('pg-width').value)||1280;
|
var width=parseInt(document.getElementById('pg-width').value)||1280;
|
||||||
var height=parseInt(document.getElementById('pg-height').value)||800;
|
var height=parseInt(document.getElementById('pg-height').value)||800;
|
||||||
|
var fullPage=document.getElementById('pg-fullpage').checked;
|
||||||
|
var deviceScale=parseInt(document.getElementById('pg-scale').value)||1;
|
||||||
|
var waitUntil=document.getElementById('pg-waituntil').value;
|
||||||
|
var waitForSelector=document.getElementById('pg-selector').value.trim()||undefined;
|
||||||
if(!url){alert('Please enter a URL');return}
|
if(!url){alert('Please enter a URL');return}
|
||||||
|
|
||||||
var btn=document.getElementById('pg-btn');
|
var btn=document.getElementById('pg-btn');
|
||||||
|
|
@ -646,11 +685,14 @@ async function runPlayground(){
|
||||||
placeholder.style.display='none';result.style.display='none';error.style.display='none';
|
placeholder.style.display='none';result.style.display='none';error.style.display='none';
|
||||||
loading.style.display='flex';
|
loading.style.display='flex';
|
||||||
|
|
||||||
|
var body={url:url,format:format,width:width,height:height,fullPage:fullPage,quality:quality,deviceScale:deviceScale,waitUntil:waitUntil};
|
||||||
|
if(waitForSelector)body.waitForSelector=waitForSelector;
|
||||||
|
|
||||||
try{
|
try{
|
||||||
var r=await fetch('/v1/playground',{
|
var r=await fetch('/v1/playground',{
|
||||||
method:'POST',
|
method:'POST',
|
||||||
headers:{'Content-Type':'application/json'},
|
headers:{'Content-Type':'application/json'},
|
||||||
body:JSON.stringify({url:url,format:format,width:width,height:height})
|
body:JSON.stringify(body)
|
||||||
});
|
});
|
||||||
if(!r.ok){var d=await r.json().catch(function(){return{}});throw new Error(d.error||'HTTP '+r.status)}
|
if(!r.ok){var d=await r.json().catch(function(){return{}});throw new Error(d.error||'HTTP '+r.status)}
|
||||||
var blob=await r.blob();
|
var blob=await r.blob();
|
||||||
|
|
|
||||||
|
|
@ -298,9 +298,9 @@ footer{border-top:1px solid var(--border);padding:48px 24px 32px;background:var(
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-col">
|
<div class="footer-col">
|
||||||
<h5>Legal</h5>
|
<h5>Legal</h5>
|
||||||
<a href="/impressum">Impressum</a>
|
<a href="/impressum.html">Impressum</a>
|
||||||
<a href="/privacy">Privacy Policy</a>
|
<a href="/privacy.html">Privacy Policy</a>
|
||||||
<a href="/terms">Terms of Service</a>
|
<a href="/terms.html">Terms of Service</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-bottom">
|
<div class="footer-bottom">
|
||||||
|
|
|
||||||
|
|
@ -236,7 +236,7 @@ footer{border-top:1px solid var(--border);padding:48px 24px 32px;background:var(
|
||||||
<h2>6. Data & Privacy</h2>
|
<h2>6. Data & Privacy</h2>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>Your privacy is governed by our <a href="/privacy">Privacy Policy</a></li>
|
<li>Your privacy is governed by our <a href="/privacy.html">Privacy Policy</a></li>
|
||||||
<li>All data processing occurs within the European Union</li>
|
<li>All data processing occurs within the European Union</li>
|
||||||
<li>Screenshots are generated and returned immediately (not stored)</li>
|
<li>Screenshots are generated and returned immediately (not stored)</li>
|
||||||
<li>API usage logs retained for billing and security purposes</li>
|
<li>API usage logs retained for billing and security purposes</li>
|
||||||
|
|
@ -379,9 +379,9 @@ footer{border-top:1px solid var(--border);padding:48px 24px 32px;background:var(
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-col">
|
<div class="footer-col">
|
||||||
<h5>Legal</h5>
|
<h5>Legal</h5>
|
||||||
<a href="/impressum">Impressum</a>
|
<a href="/impressum.html">Impressum</a>
|
||||||
<a href="/privacy">Privacy Policy</a>
|
<a href="/privacy.html">Privacy Policy</a>
|
||||||
<a href="/terms">Terms of Service</a>
|
<a href="/terms.html">Terms of Service</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-bottom">
|
<div class="footer-bottom">
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,11 @@ app.get("/openapi.json", (_req, res) => {
|
||||||
app.get("/docs", (_req, res) => {
|
app.get("/docs", (_req, res) => {
|
||||||
res.sendFile(path.join(__dirname, "../public/docs.html"));
|
res.sendFile(path.join(__dirname, "../public/docs.html"));
|
||||||
});
|
});
|
||||||
|
// Clean URLs for legal pages (redirect /privacy → /privacy.html, etc.)
|
||||||
|
for (const page of ["privacy", "terms", "impressum", "status"]) {
|
||||||
|
app.get(`/${page}`, (_req, res) => res.redirect(301, `/${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 }));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ const playgroundLimiter = rateLimit({
|
||||||
* schema: { $ref: "#/components/schemas/Error" }
|
* schema: { $ref: "#/components/schemas/Error" }
|
||||||
*/
|
*/
|
||||||
playgroundRouter.post("/", playgroundLimiter, async (req, res) => {
|
playgroundRouter.post("/", playgroundLimiter, async (req, res) => {
|
||||||
const { url, format, width, height } = req.body;
|
const { url, format, width, height, fullPage, quality, waitForSelector, deviceScale, waitUntil } = req.body;
|
||||||
|
|
||||||
if (!url || typeof url !== "string") {
|
if (!url || typeof url !== "string") {
|
||||||
res.status(400).json({ error: "Missing required parameter: url" });
|
res.status(400).json({ error: "Missing required parameter: url" });
|
||||||
|
|
@ -95,6 +95,14 @@ playgroundRouter.post("/", playgroundLimiter, async (req, res) => {
|
||||||
const safeWidth = Math.min(Math.max(parseInt(width, 10) || 1280, 320), 1920);
|
const safeWidth = Math.min(Math.max(parseInt(width, 10) || 1280, 320), 1920);
|
||||||
const safeHeight = Math.min(Math.max(parseInt(height, 10) || 800, 200), 1080);
|
const safeHeight = Math.min(Math.max(parseInt(height, 10) || 800, 200), 1080);
|
||||||
const safeFormat = ["png", "jpeg", "webp"].includes(format) ? format : "png";
|
const safeFormat = ["png", "jpeg", "webp"].includes(format) ? format : "png";
|
||||||
|
const safeFullPage = fullPage === true;
|
||||||
|
const safeQuality = safeFormat === "png" ? undefined : Math.min(Math.max(parseInt(quality, 10) || 80, 1), 100);
|
||||||
|
const safeDeviceScale = Math.min(Math.max(parseInt(deviceScale, 10) || 1, 1), 3);
|
||||||
|
const validWaitUntil = ["load", "domcontentloaded", "networkidle0", "networkidle2"];
|
||||||
|
const safeWaitUntil = validWaitUntil.includes(waitUntil) ? waitUntil : "domcontentloaded";
|
||||||
|
// Sanitize waitForSelector — allow simple CSS selectors only (no script injection)
|
||||||
|
const safeWaitForSelector = typeof waitForSelector === "string" && /^[a-zA-Z0-9\s\-_.#\[\]=:"'>,+~()]+$/.test(waitForSelector) && waitForSelector.length <= 200
|
||||||
|
? waitForSelector : undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await takeScreenshot({
|
const result = await takeScreenshot({
|
||||||
|
|
@ -102,9 +110,11 @@ playgroundRouter.post("/", playgroundLimiter, async (req, res) => {
|
||||||
format: safeFormat as "png" | "jpeg" | "webp",
|
format: safeFormat as "png" | "jpeg" | "webp",
|
||||||
width: safeWidth,
|
width: safeWidth,
|
||||||
height: safeHeight,
|
height: safeHeight,
|
||||||
fullPage: false,
|
fullPage: safeFullPage,
|
||||||
quality: safeFormat === "png" ? undefined : 70,
|
quality: safeQuality,
|
||||||
deviceScale: 1,
|
deviceScale: safeDeviceScale,
|
||||||
|
waitUntil: safeWaitUntil as any,
|
||||||
|
waitForSelector: safeWaitForSelector,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add watermark
|
// Add watermark
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue