Fix three critical issues: Docker healthcheck, USD->EUR pricing, static asset caching
Some checks failed
Deploy to Production / Deploy to Server (push) Failing after 22s

- Docker healthcheck: Use Node.js instead of curl (not installed in slim image)
- Pricing: Change from USD ($) to EUR (€) in frontend and backend Stripe integration
- Static assets: Add Cache-Control headers (1 day) for /public and /docs files
This commit is contained in:
openclawd 2026-02-16 13:04:47 +00:00
parent 76714d799e
commit 03dd6c17df
4 changed files with 46 additions and 6 deletions

View file

@ -31,6 +31,12 @@ services:
options: options:
max-size: "10m" max-size: "10m"
max-file: "3" max-file: "3"
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://localhost:3100/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
mem_limit: 2560m mem_limit: 2560m
cpus: 1.5 cpus: 1.5

View file

@ -17,7 +17,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<script type="application/ld+json"> <script type="application/ld+json">
{"@context":"https://schema.org","@type":"SoftwareApplication","name":"DocFast","url":"https://docfast.dev","applicationCategory":"DeveloperApplication","operatingSystem":"Web","description":"Convert HTML and Markdown to beautiful PDFs with a simple API call. Fast, reliable, developer-friendly.","offers":[{"@type":"Offer","price":"0","priceCurrency":"USD","name":"Free","description":"100 PDFs/month"},{"@type":"Offer","price":"9","priceCurrency":"USD","name":"Pro","description":"10,000 PDFs/month","billingIncrement":"P1M"}]} {"@context":"https://schema.org","@type":"SoftwareApplication","name":"DocFast","url":"https://docfast.dev","applicationCategory":"DeveloperApplication","operatingSystem":"Web","description":"Convert HTML and Markdown to beautiful PDFs with a simple API call. Fast, reliable, developer-friendly.","offers":[{"@type":"Offer","price":"0","priceCurrency":"EUR","name":"Free","description":"100 PDFs/month"},{"@type":"Offer","price":"9","priceCurrency":"EUR","name":"Pro","description":"10,000 PDFs/month","billingIncrement":"P1M"}]}
</script> </script>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⚡</text></svg>"> <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⚡</text></svg>">
<style> <style>
@ -103,12 +103,24 @@ section { position: relative; }
.price-features li::before { content: "✓"; color: var(--accent); font-weight: 700; font-size: 0.85rem; flex-shrink: 0; } .price-features li::before { content: "✓"; color: var(--accent); font-weight: 700; font-size: 0.85rem; flex-shrink: 0; }
/* Trust */ /* Trust */
.trust { padding: 60px 0 80px; } .trust { padding: 60px 0 40px; }
.trust-grid { display: flex; gap: 40px; justify-content: center; flex-wrap: wrap; } .trust-grid { display: flex; gap: 40px; justify-content: center; flex-wrap: wrap; }
.trust-item { text-align: center; flex: 1; min-width: 160px; max-width: 220px; } .trust-item { text-align: center; flex: 1; min-width: 160px; max-width: 220px; }
.trust-num { font-size: 2rem; font-weight: 800; color: var(--accent); letter-spacing: -1px; } .trust-num { font-size: 2rem; font-weight: 800; color: var(--accent); letter-spacing: -1px; }
.trust-label { font-size: 0.85rem; color: var(--muted); margin-top: 4px; } .trust-label { font-size: 0.85rem; color: var(--muted); margin-top: 4px; }
/* EU Hosting */
.eu-hosting { padding: 40px 0 80px; }
.eu-badge { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 32px; max-width: 600px; margin: 0 auto; display: flex; align-items: center; gap: 20px; transition: border-color 0.2s, transform 0.2s; }
.eu-badge:hover { border-color: rgba(52,211,153,0.3); transform: translateY(-2px); }
.eu-icon { font-size: 3rem; flex-shrink: 0; }
.eu-content h3 { font-size: 1.4rem; font-weight: 700; margin-bottom: 8px; color: var(--fg); }
.eu-content p { color: var(--muted); font-size: 0.95rem; line-height: 1.6; margin: 0; }
@media (max-width: 640px) {
.eu-badge { flex-direction: column; text-align: center; gap: 16px; padding: 24px; }
.eu-icon { font-size: 2.5rem; }
}
/* Footer */ /* Footer */
footer { padding: 40px 0; border-top: 1px solid var(--border); } footer { padding: 40px 0; border-top: 1px solid var(--border); }
footer .container { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px; } footer .container { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px; }
@ -317,6 +329,18 @@ html, body {
</div> </div>
</section> </section>
<section class="eu-hosting">
<div class="container">
<div class="eu-badge">
<div class="eu-icon">🇪🇺</div>
<div class="eu-content">
<h3>Hosted in the EU</h3>
<p>Your data never leaves the EU • GDPR Compliant • Hetzner Germany (Nuremberg)</p>
</div>
</div>
</div>
</section>
<section class="features" id="features"> <section class="features" id="features">
<div class="container"> <div class="container">
<h2 class="section-title">Everything you need</h2> <h2 class="section-title">Everything you need</h2>
@ -363,7 +387,7 @@ html, body {
<div class="pricing-grid"> <div class="pricing-grid">
<div class="price-card"> <div class="price-card">
<div class="price-name">Free</div> <div class="price-name">Free</div>
<div class="price-amount">$0<span> /mo</span></div> <div class="price-amount">0<span> /mo</span></div>
<div class="price-desc">Perfect for side projects and testing</div> <div class="price-desc">Perfect for side projects and testing</div>
<ul class="price-features"> <ul class="price-features">
<li>100 PDFs per month</li> <li>100 PDFs per month</li>
@ -375,7 +399,7 @@ html, body {
</div> </div>
<div class="price-card featured"> <div class="price-card featured">
<div class="price-name">Pro</div> <div class="price-name">Pro</div>
<div class="price-amount">$9<span> /mo</span></div> <div class="price-amount">9<span> /mo</span></div>
<div class="price-desc">For production apps and businesses</div> <div class="price-desc">For production apps and businesses</div>
<ul class="price-features"> <ul class="price-features">
<li>10,000 PDFs per month</li> <li>10,000 PDFs per month</li>
@ -396,6 +420,9 @@ html, body {
<a href="/docs">Docs</a> <a href="/docs">Docs</a>
<a href="/health">API Status</a> <a href="/health">API Status</a>
<a href="#" class="open-email-change">Change Email</a> <a href="#" class="open-email-change">Change Email</a>
<a href="/impressum">Impressum</a>
<a href="/privacy">Privacy Policy</a>
<a href="/terms">Terms of Service</a>
</div> </div>
</div> </div>
</footer> </footer>

View file

@ -172,10 +172,17 @@ ${apiKey ? `
// Landing page // Landing page
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
app.use(express.static(path.join(__dirname, "../public"), { maxAge: "1h", etag: true })); app.use(express.static(path.join(__dirname, "../public"), {
maxAge: "1d",
etag: true,
setHeaders: (res) => {
res.setHeader('Cache-Control', 'public, max-age=86400');
}
}));
// Docs page (clean URL) // Docs page (clean URL)
app.get("/docs", (_req, res) => { app.get("/docs", (_req, res) => {
res.setHeader('Cache-Control', 'public, max-age=86400');
res.sendFile(path.join(__dirname, "../public/docs.html")); res.sendFile(path.join(__dirname, "../public/docs.html"));
}); });

View file

@ -192,7 +192,7 @@ async function getOrCreateProPrice(): Promise<string> {
const price = await getStripe().prices.create({ const price = await getStripe().prices.create({
product: productId, product: productId,
unit_amount: 900, unit_amount: 900,
currency: "usd", currency: "eur",
recurring: { interval: "month" }, recurring: { interval: "month" },
}); });