From fac51527c881e59cac5a7265ec2b09e36b6ca0c0 Mon Sep 17 00:00:00 2001 From: alam00000 Date: Sat, 31 Oct 2026 13:44:04 +0530 Subject: [PATCH] feat(ads): collect ad details at Dodo checkout --- advertise.html | 399 +++++++++++++++++++++++++++++++++++ api/ad-checkout.js | 80 +++++++ index.html | 83 ++++++++ scripts/content-segments.mjs | 7 +- src/css/landing.css | 158 ++++++++++++++ src/js/advertise.ts | 12 ++ src/js/main.ts | 9 + vercel.json | 4 + vite.config.ts | 2 + 9 files changed, 753 insertions(+), 1 deletion(-) create mode 100644 advertise.html create mode 100644 api/ad-checkout.js create mode 100644 src/js/advertise.ts diff --git a/advertise.html b/advertise.html new file mode 100644 index 00000000..f1e91c03 --- /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 usually put it live within two + business days. 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..d4de19b1 --- /dev/null +++ b/api/ad-checkout.js @@ -0,0 +1,80 @@ +const DODO_API = 'https://test.dodopayments.com'; + +const 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(productId, page) { + const response = await fetch(`${DODO_API}/checkouts`, { + method: 'POST', + headers: { + Authorization: `Bearer ${process.env.DODO_PAYMENTS_API_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: ${body}`); + } + return checkout_url; +} + +export async function GET(request) { + const url = new URL(request.url); + const page = `${url.origin}/advertise`; + const productId = PRODUCTS.get(url.searchParams.get('row')); + if (!productId) return redirect(`${page}#pricing`); + + try { + return redirect(await createCheckout(productId, page)); + } catch (error) { + console.error(error); + return redirect(`${page}?checkout=failed`); + } +} diff --git a/index.html b/index.html index c6698d8d..34b4d73f 100644 --- a/index.html +++ b/index.html @@ -348,6 +348,89 @@ + +
{ await initI18n(); await loadRuntimeConfig(); 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..a39a8d9c 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', @@ -803,6 +804,7 @@ export default defineConfig(({ command }) => { licensing: resolve(__dirname, 'licensing.html'), kura: resolve(__dirname, 'kura.html'), 'hyper-compress': resolve(__dirname, 'hyper-compress.html'), + advertise: resolve(__dirname, 'advertise.html'), ...Object.fromEntries( fs .readdirSync(resolve(__dirname, 'blog'))