diff --git a/advertise.html b/advertise.html new file mode 100644 index 00000000..433f63ad --- /dev/null +++ b/advertise.html @@ -0,0 +1,399 @@ + + + + + + Advertise on BentoPDF - Homepage Logo Ads | BentoPDF + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{> navbar }} + +
+ + + + + + +
+
+
+
+

+ Pick a + row +

+

+ Billed monthly. Cancel anytime and your ad stays up until the + end of the month you paid for. +

+
+
+ +
+
+

Top row

+

+ Four slots, first in line above the tools +

+
$199/ month
+
    +
  • Larger tile in the first row
  • +
  • Your logo, name and link
  • +
  • + On every language version of the + homepage +
  • +
  • Cancel anytime
  • +
+ Book the top row +
+
+

Second row

+

Four slots, right below the top row

+
$129/ month
+
    +
  • + Standard tile in the second row +
  • +
  • Your logo, name and link
  • +
  • + On every language version of the + homepage +
  • +
  • Cancel anytime
  • +
+ Book the second row +
+
+
+
+ +
+
+
+
+

+ How it + works +

+
+
+
+
+ +

Book a row

+

+ Checkout is handled by Dodo Payments, our merchant of record, so + you get a proper invoice with sales tax taken care of. +

+
+
+ +

Add your ad

+

+ Checkout asks for the name to show, your link, and a link to + your logo as a square SVG or PNG of at least 128 by 128 pixels. +

+
+
+ +

Go live

+

+ We review every ad by hand and put it live within 24 hours. Need + a new logo or link later? Just email us. +

+
+
+
+
+ +
+
+
+
+ +

+ We don't track our visitors, so we don't track your ad + either. + No pixels, no scripts, no impression or click counts. Your logo + is served from bentopdf.com and links straight to your site. To + measure visits, add UTM parameters to your link and read them in + your own analytics. +

+
+
+
+
+ +
+
+
+
+

+ Frequently asked + questions +

+
+
+
+
+
+ +
+

+ Software, services and products that are useful to people + who work with documents. We don't accept gambling, adult + content, malware, or anything misleading. If we turn your ad + down, we refund you in full. +

+
+
+
+ +
+

+ On the bentopdf.com homepage, in every language, directly + above the tool grid. Self-hosted copies of BentoPDF never + show ads. +

+
+
+
+ +
+

+ No. BentoPDF has no analytics, so there are no impression or + click numbers to share. Add UTM parameters to your link to + see the visits we send you in your own analytics. +

+
+
+
+ +
+

+ Email us and we'll cancel your subscription. Your ad stays + up until the end of the month you paid for. +

+
+
+
+
+
+
+ +
+
+
+

Questions before you book?

+

Email us about slots, pricing or anything else.

+ +
+
+
+
+ + {{> footer }} + + + + + + + + diff --git a/api/ad-checkout.js b/api/ad-checkout.js new file mode 100644 index 00000000..8f28b823 --- /dev/null +++ b/api/ad-checkout.js @@ -0,0 +1,92 @@ +const MODES = { + live: { + api: 'https://live.dodopayments.com', + products: new Map([ + ['top', 'pdt_0Nqv9McKI1iCBXb7z4DB7'], + ['second', 'pdt_0Nqv9OYDLwWZsu1hzpZ0c'], + ]), + }, + test: { + api: 'https://test.dodopayments.com', + products: new Map([ + ['top', 'pdt_0Nqt0672sYJEtrkIb2K6m'], + ['second', 'pdt_0Nqt069LgdQFPo1tFS1yF'], + ]), + }, +}; + +const FIELDS = [ + { + key: 'ad_name', + label: 'Name on the ad', + field_type: 'text', + placeholder: 'Acme Invoices', + required: true, + }, + { + key: 'ad_link', + label: 'Link', + field_type: 'url', + placeholder: 'https://example.com', + required: true, + }, + { + key: 'ad_logo', + label: 'Logo link (square SVG or PNG)', + field_type: 'url', + placeholder: 'https://example.com/logo.svg', + required: true, + }, +]; + +function redirect(location) { + return new Response(null, { + status: 303, + headers: { Location: location, 'Cache-Control': 'no-store' }, + }); +} + +async function createCheckout(key, api, productId, page) { + const response = await fetch(`${api}/checkouts`, { + method: 'POST', + headers: { + Authorization: `Bearer ${key}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + product_cart: [{ product_id: productId, quantity: 1 }], + custom_fields: FIELDS, + return_url: `${page}?paid=1`, + cancel_url: `${page}#pricing`, + }), + signal: AbortSignal.timeout(10000), + }); + const body = await response.text(); + if (!response.ok) { + throw new Error(`Dodo returned ${response.status}: ${body}`); + } + const { checkout_url } = JSON.parse(body); + if ( + typeof checkout_url !== 'string' || + !checkout_url.startsWith('https://') + ) { + throw new Error('Dodo returned no checkout URL'); + } + return checkout_url; +} + +export async function GET(request) { + const url = new URL(request.url); + const page = `${url.origin}/advertise`; + const key = process.env.DODO_PAYMENTS_API_KEY ?? ''; + const mode = key.startsWith('dodo_live_') ? MODES.live : MODES.test; + const productId = mode.products.get(url.searchParams.get('row')); + if (!productId) return redirect(`${page}#pricing`); + + try { + return redirect(await createCheckout(key, mode.api, productId, page)); + } catch (error) { + console.error(error); + return redirect(`${page}?checkout=failed`); + } +} diff --git a/index.html b/index.html index c6698d8d..cb4a5b92 100644 --- a/index.html +++ b/index.html @@ -348,6 +348,112 @@ + {{#if showAds}} + + {{/if}} +
{ await initI18n(); await loadRuntimeConfig(); diff --git a/src/partials/footer.html b/src/partials/footer.html index 5cb72f56..4695c367 100644 --- a/src/partials/footer.html +++ b/src/partials/footer.html @@ -19,19 +19,6 @@ Version {{appVersion}}

- - DigitalOcean Referral Badge -
diff --git a/vercel.json b/vercel.json index 63db79d0..06669794 100644 --- a/vercel.json +++ b/vercel.json @@ -217,6 +217,10 @@ "source": "/(.*)\\.html", "headers": [{ "key": "Cache-Control", "value": "public, max-age=3600" }] }, + { + "source": "/api/(.*)", + "headers": [{ "key": "Cache-Control", "value": "no-store" }] + }, { "source": "/(ar|be|da|de|es|fr|id|it|ja|ko|nl|pl|pt|ru|sk|sv|tr|uk|vi|zh|zh-TW|ca|fa)/(.*)", "headers": [{ "key": "Cache-Control", "value": "public, max-age=300" }] diff --git a/vite.config.ts b/vite.config.ts index 7d1fa1a7..ea9c9dc0 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -76,6 +76,7 @@ function loadPages(): Set { 'licensing', 'kura', 'hyper-compress', + 'advertise', '404', 'pdf-converter', 'pdf-editor', @@ -684,6 +685,11 @@ export default defineConfig(({ command }) => { (command === 'serve' || process.env.INCLUDE_DOCS === 'true') ? '/docs/' : 'https://www.bentopdf.com/docs'; + const showAds = + command === 'serve' || + ['bentopdf.com', 'www.bentopdf.com'].includes( + process.env.VERCEL_PROJECT_PRODUCTION_URL ?? '' + ); if (USE_CDN) { console.log('[Vite] Using CDN for WASM files (with local fallback)'); @@ -708,6 +714,7 @@ export default defineConfig(({ command }) => { brandLogo: process.env.VITE_BRAND_LOGO || '', footerText: process.env.VITE_FOOTER_TEXT || '', appVersion: process.env.npm_package_version || 'Unknown', + showAds, }, }), languageRouterPlugin(), @@ -803,6 +810,9 @@ export default defineConfig(({ command }) => { licensing: resolve(__dirname, 'licensing.html'), kura: resolve(__dirname, 'kura.html'), 'hyper-compress': resolve(__dirname, 'hyper-compress.html'), + ...(showAds + ? { advertise: resolve(__dirname, 'advertise.html') } + : {}), ...Object.fromEntries( fs .readdirSync(resolve(__dirname, 'blog'))