From 7037b885e2ee17e71bf11f149e4a0d15bdad7d3b Mon Sep 17 00:00:00 2001 From: DocFast Bot Date: Tue, 17 Feb 2026 13:07:26 +0000 Subject: [PATCH] fix: BUG-055,058,060,061,067,069,053 - QA low/info fixes - BUG-055: Remove duplicate preconnect tags from homepage - BUG-058: Add twitter:image meta tag to homepage - BUG-060: Add og:title/description/url to sub-pages - BUG-061: Add /status to sitemap.xml - BUG-067: Add skip-to-content link on all pages - BUG-069: Add legal footer to /docs page - BUG-053: Minify app.js with terser --- package-lock.json | 103 +++++++++++++++++++++- package.json | 3 +- public/app.min.js | 1 + public/docs.html | 12 +++ public/impressum.html | 16 ++-- public/index.html | 152 ++++++++++++++++++--------------- public/privacy.html | 16 ++-- public/sitemap.xml | 1 + public/terms.html | 18 ++-- templates/pages/docs.html | 12 +++ templates/pages/impressum.html | 9 +- templates/pages/index.html | 11 ++- templates/pages/privacy.html | 9 +- templates/pages/terms.html | 9 +- 14 files changed, 278 insertions(+), 94 deletions(-) create mode 100644 public/app.min.js diff --git a/package-lock.json b/package-lock.json index 920aa6c..f9dad1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "@types/node": "^22.0.0", "@types/nodemailer": "^7.0.9", "@types/pg": "^8.11.0", + "terser": "^5.46.0", "tsx": "^4.19.0", "typescript": "^5.7.0", "vitest": "^3.0.0" @@ -497,6 +498,38 @@ "node": ">=18" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -504,6 +537,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@pinojs/redact": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", @@ -1207,6 +1251,19 @@ "node": ">= 0.6" } }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -1429,6 +1486,13 @@ "node": "*" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1558,6 +1622,13 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -3606,8 +3677,8 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "devOptional": true, "license": "BSD-3-Clause", - "optional": true, "engines": { "node": ">=0.10.0" } @@ -3622,6 +3693,17 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -3761,6 +3843,25 @@ "streamx": "^2.15.0" } }, + "node_modules/terser": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/text-decoder": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.6.tgz", diff --git a/package.json b/package.json index 410296e..0616cf2 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Markdown/HTML to PDF API with built-in invoice templates", "main": "dist/index.js", "scripts": { - "build:pages": "node scripts/build-pages.js", + "build:pages": "node scripts/build-pages.js && npx terser public/app.js -o public/app.min.js --compress --mangle", "build": "npm run build:pages && tsc", "start": "node dist/index.js", "dev": "tsx src/index.ts", @@ -30,6 +30,7 @@ "@types/node": "^22.0.0", "@types/nodemailer": "^7.0.9", "@types/pg": "^8.11.0", + "terser": "^5.46.0", "tsx": "^4.19.0", "typescript": "^5.7.0", "vitest": "^3.0.0" diff --git a/public/app.min.js b/public/app.min.js new file mode 100644 index 0000000..258c395 --- /dev/null +++ b/public/app.min.js @@ -0,0 +1 @@ +var signupEmail="",recoverEmail="";function showState(e){["signupInitial","signupLoading","signupVerify","signupResult"].forEach(function(e){var t=document.getElementById(e);t&&t.classList.remove("active")}),document.getElementById(e).classList.add("active")}function showRecoverState(e){["recoverInitial","recoverLoading","recoverVerify","recoverResult"].forEach(function(e){var t=document.getElementById(e);t&&t.classList.remove("active")}),document.getElementById(e).classList.add("active")}function openSignup(){document.getElementById("signupModal").classList.add("active"),showState("signupInitial"),document.getElementById("signupError").style.display="none",document.getElementById("verifyError").style.display="none",document.getElementById("signupEmail").value="",document.getElementById("verifyCode").value="",signupEmail=""}function closeSignup(){document.getElementById("signupModal").classList.remove("active")}function openRecover(){closeSignup(),document.getElementById("recoverModal").classList.add("active"),showRecoverState("recoverInitial");var e=document.getElementById("recoverError");e&&(e.style.display="none");var t=document.getElementById("recoverVerifyError");t&&(t.style.display="none"),document.getElementById("recoverEmailInput").value="",document.getElementById("recoverCode").value="",recoverEmail=""}function closeRecover(){document.getElementById("recoverModal").classList.remove("active")}async function submitSignup(){var e=document.getElementById("signupError"),t=document.getElementById("signupBtn"),n=document.getElementById("signupEmail").value.trim();if(!n||!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(n))return e.textContent="Please enter a valid email address.",void(e.style.display="block");e.style.display="none",t.disabled=!0,showState("signupLoading");try{var a=await fetch("/v1/signup/free",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:n})}),i=await a.json();if(!a.ok)return showState("signupInitial"),e.textContent=i.error||"Something went wrong. Please try again.",e.style.display="block",void(t.disabled=!1);signupEmail=n,document.getElementById("verifyEmailDisplay").textContent=n,showState("signupVerify"),document.getElementById("verifyCode").focus(),t.disabled=!1}catch(n){showState("signupInitial"),e.textContent="Network error. Please try again.",e.style.display="block",t.disabled=!1}}async function submitVerify(){var e=document.getElementById("verifyError"),t=document.getElementById("verifyBtn"),n=document.getElementById("verifyCode").value.trim();if(!n||!/^\d{6}$/.test(n))return e.textContent="Please enter a 6-digit code.",void(e.style.display="block");e.style.display="none",t.disabled=!0;try{var a=await fetch("/v1/signup/verify",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:signupEmail,code:n})}),i=await a.json();if(!a.ok)return e.textContent=i.error||"Verification failed.",e.style.display="block",void(t.disabled=!1);document.getElementById("apiKeyText").textContent=i.apiKey,showState("signupResult")}catch(n){e.textContent="Network error. Please try again.",e.style.display="block",t.disabled=!1}}async function submitRecover(){var e=document.getElementById("recoverError"),t=document.getElementById("recoverBtn"),n=document.getElementById("recoverEmailInput").value.trim();if(!n||!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(n))return e.textContent="Please enter a valid email address.",void(e.style.display="block");e.style.display="none",t.disabled=!0,showRecoverState("recoverLoading");try{var a=await fetch("/v1/recover",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:n})}),i=await a.json();if(!a.ok)return showRecoverState("recoverInitial"),e.textContent=i.error||"Something went wrong.",e.style.display="block",void(t.disabled=!1);recoverEmail=n,document.getElementById("recoverEmailDisplay").textContent=n,showRecoverState("recoverVerify"),document.getElementById("recoverCode").focus(),t.disabled=!1}catch(n){showRecoverState("recoverInitial"),e.textContent="Network error. Please try again.",e.style.display="block",t.disabled=!1}}async function submitRecoverVerify(){var e=document.getElementById("recoverVerifyError"),t=document.getElementById("recoverVerifyBtn"),n=document.getElementById("recoverCode").value.trim();if(!n||!/^\d{6}$/.test(n))return e.textContent="Please enter a 6-digit code.",void(e.style.display="block");e.style.display="none",t.disabled=!0;try{var a=await fetch("/v1/recover/verify",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:recoverEmail,code:n})}),i=await a.json();if(!a.ok)return e.textContent=i.error||"Verification failed.",e.style.display="block",void(t.disabled=!1);i.apiKey?(document.getElementById("recoveredKeyText").textContent=i.apiKey,showRecoverState("recoverResult")):(e.textContent=i.message||"No key found for this email.",e.style.display="block",t.disabled=!1)}catch(n){e.textContent="Network error. Please try again.",e.style.display="block",t.disabled=!1}}function copyKey(){doCopy(document.getElementById("apiKeyText").textContent,document.getElementById("copyBtn"))}function copyRecoveredKey(){doCopy(document.getElementById("recoveredKeyText").textContent,document.getElementById("copyRecoveredBtn"))}function doCopy(e,t){function n(){t.textContent="✓ Copied!",setTimeout(function(){t.textContent="Copy"},2e3)}function a(){t.textContent="Failed",setTimeout(function(){t.textContent="Copy"},2e3)}try{if(navigator.clipboard&&window.isSecureContext)navigator.clipboard.writeText(e).then(n).catch(function(){try{var t=document.createElement("textarea");t.value=e,t.style.position="fixed",t.style.opacity="0",t.style.top="-9999px",t.style.left="-9999px",document.body.appendChild(t),t.focus(),t.select();var i=document.execCommand("copy");document.body.removeChild(t),i?n():a()}catch(e){a()}});else{var i=document.createElement("textarea");i.value=e,i.style.position="fixed",i.style.opacity="0",i.style.top="-9999px",i.style.left="-9999px",document.body.appendChild(i),i.focus(),i.select();var o=document.execCommand("copy");document.body.removeChild(i),o?n():a()}}catch(e){a()}}async function checkout(){try{var e=await fetch("/v1/billing/checkout",{method:"POST"}),t=await e.json();t.url?window.location.href=t.url:alert("Checkout is not available yet. Please try again later.")}catch(e){alert("Something went wrong. Please try again.")}}document.addEventListener("DOMContentLoaded",function(){document.getElementById("btn-signup").addEventListener("click",openSignup),document.getElementById("btn-signup-2").addEventListener("click",openSignup),document.getElementById("btn-checkout").addEventListener("click",checkout),document.getElementById("btn-close-signup").addEventListener("click",closeSignup),document.getElementById("signupBtn").addEventListener("click",submitSignup),document.getElementById("verifyBtn").addEventListener("click",submitVerify),document.getElementById("copyBtn").addEventListener("click",copyKey),document.getElementById("btn-close-recover").addEventListener("click",closeRecover),document.getElementById("recoverBtn").addEventListener("click",submitRecover),document.getElementById("recoverVerifyBtn").addEventListener("click",submitRecoverVerify),document.getElementById("copyRecoveredBtn").addEventListener("click",copyRecoveredKey),document.getElementById("recoverModal").addEventListener("click",function(e){e.target===this&&closeRecover()}),document.querySelectorAll(".open-recover").forEach(function(e){e.addEventListener("click",function(e){e.preventDefault(),openRecover()})}),document.getElementById("signupModal").addEventListener("click",function(e){e.target===this&&closeSignup()}),document.querySelectorAll('a[href^="#"]').forEach(function(e){e.addEventListener("click",function(e){e.preventDefault();var t=document.querySelector(this.getAttribute("href"));t&&t.scrollIntoView({behavior:"smooth"})})})});var emailChangeApiKey="",emailChangeNewEmail="";function showEmailChangeState(e){["emailChangeInitial","emailChangeLoading","emailChangeVerify","emailChangeResult"].forEach(function(e){var t=document.getElementById(e);t&&t.classList.remove("active")}),document.getElementById(e).classList.add("active")}function openEmailChange(){closeSignup(),closeRecover(),document.getElementById("emailChangeModal").classList.add("active"),showEmailChangeState("emailChangeInitial");var e=document.getElementById("emailChangeError");e&&(e.style.display="none");var t=document.getElementById("emailChangeVerifyError");t&&(t.style.display="none"),document.getElementById("emailChangeApiKey").value="",document.getElementById("emailChangeNewEmail").value="",document.getElementById("emailChangeCode").value="",emailChangeApiKey="",emailChangeNewEmail=""}function closeEmailChange(){document.getElementById("emailChangeModal").classList.remove("active")}async function submitEmailChange(){var e=document.getElementById("emailChangeError"),t=document.getElementById("emailChangeBtn"),n=document.getElementById("emailChangeApiKey").value.trim(),a=document.getElementById("emailChangeNewEmail").value.trim();if(!n)return e.textContent="Please enter your API key.",void(e.style.display="block");if(!a||!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(a))return e.textContent="Please enter a valid email address.",void(e.style.display="block");e.style.display="none",t.disabled=!0,showEmailChangeState("emailChangeLoading");try{var i=await fetch("/v1/email-change",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({apiKey:n,newEmail:a})}),o=await i.json();if(!i.ok)return showEmailChangeState("emailChangeInitial"),e.textContent=o.error||"Something went wrong.",e.style.display="block",void(t.disabled=!1);emailChangeApiKey=n,emailChangeNewEmail=a,document.getElementById("emailChangeEmailDisplay").textContent=a,showEmailChangeState("emailChangeVerify"),document.getElementById("emailChangeCode").focus(),t.disabled=!1}catch(n){showEmailChangeState("emailChangeInitial"),e.textContent="Network error. Please try again.",e.style.display="block",t.disabled=!1}}async function submitEmailChangeVerify(){var e=document.getElementById("emailChangeVerifyError"),t=document.getElementById("emailChangeVerifyBtn"),n=document.getElementById("emailChangeCode").value.trim();if(!n||!/^\d{6}$/.test(n))return e.textContent="Please enter a 6-digit code.",void(e.style.display="block");e.style.display="none",t.disabled=!0;try{var a=await fetch("/v1/email-change/verify",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({apiKey:emailChangeApiKey,newEmail:emailChangeNewEmail,code:n})}),i=await a.json();if(!a.ok)return e.textContent=i.error||"Verification failed.",e.style.display="block",void(t.disabled=!1);document.getElementById("emailChangeNewDisplay").textContent=i.newEmail||emailChangeNewEmail,showEmailChangeState("emailChangeResult")}catch(n){e.textContent="Network error. Please try again.",e.style.display="block",t.disabled=!1}}document.addEventListener("DOMContentLoaded",function(){var e=document.getElementById("btn-close-email-change");e&&e.addEventListener("click",closeEmailChange);var t=document.getElementById("emailChangeBtn");t&&t.addEventListener("click",submitEmailChange);var n=document.getElementById("emailChangeVerifyBtn");n&&n.addEventListener("click",submitEmailChangeVerify);var a=document.getElementById("emailChangeModal");a&&a.addEventListener("click",function(e){e.target===this&&closeEmailChange()}),document.querySelectorAll(".open-email-change").forEach(function(e){e.addEventListener("click",function(e){e.preventDefault(),openEmailChange()})})}),function(){function e(){for(var e=["signupModal","recoverModal","emailChangeModal"],t=0;t + ← Back to docfast.dev +
+
+ diff --git a/public/impressum.html b/public/impressum.html index 201a558..5907197 100644 --- a/public/impressum.html +++ b/public/impressum.html @@ -5,6 +5,9 @@ Impressum — DocFast + + + @@ -20,7 +23,7 @@ body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Robo a { color: var(--accent); text-decoration: none; transition: color 0.2s; } a:hover { color: var(--accent-hover); } .container { max-width: 800px; margin: 0 auto; padding: 0 24px; } -nav { padding: 20px 0; border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--bg); z-index: 100; } +nav { padding: 20px 0; border-bottom: 1px solid var(--border); } nav .container { display: flex; align-items: center; justify-content: space-between; } .logo { font-size: 1.25rem; font-weight: 700; letter-spacing: -0.5px; color: var(--fg); display: flex; align-items: center; gap: 8px; text-decoration: none; } .logo span { color: var(--accent); } @@ -44,11 +47,13 @@ footer .container { display: flex; justify-content: space-between; align-items: footer .container { flex-direction: column; text-align: center; } .nav-links { gap: 16px; } } - -.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; } +/* Skip to content */ +.skip-link { position: absolute; top: -100%; left: 16px; background: var(--accent); color: #0b0d11; padding: 8px 16px; border-radius: 0 0 8px 8px; font-weight: 600; font-size: 0.9rem; z-index: 200; transition: top 0.2s; } +.skip-link:focus { top: 0; } + -
+

Impressum

Legal notice according to § 5 ECG and § 25 MedienG (Austrian law)

@@ -98,7 +103,8 @@ footer .container { display: flex; justify-content: space-between; align-items:
- +
@@ -356,7 +339,7 @@ html, body {
🇪🇺
-

Hosted in the EU

+

Hosted in the EU

Your data never leaves the EU • GDPR Compliant • Hetzner Germany (Nuremberg)

@@ -424,7 +407,7 @@ html, body {
€9 /mo
For production apps and businesses
    -
  • 2,500 PDFs per month
  • +
  • 5,000 PDFs per month
  • All conversion endpoints
  • All templates included
  • Priority support (support@docfast.dev)
  • @@ -435,15 +418,14 @@ html, body {
-
-