diff --git a/index.html b/index.html index 21a65c5..28de876 100644 --- a/index.html +++ b/index.html @@ -10,11 +10,6 @@ - OpenThorn — The BYOK AI Website Builder diff --git a/public/ads.txt b/public/ads.txt index 07e259e..41f79f1 100644 --- a/public/ads.txt +++ b/public/ads.txt @@ -1 +1,4 @@ -google.com, pub-7071904662816669, DIRECT, f08c47fec0942fa0 +# Authorized digital sellers for openthorn.app inventory. +# Replace the Adsterra line below with the exact record shown in the Adsterra +# dashboard if it includes a certification (4th) field. +adsterra.com, 31110698, DIRECT diff --git a/src/App.tsx b/src/App.tsx index 9d52914..3fb5842 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,6 +10,7 @@ import Header from './components/Header/Header' import HeroSection from './components/HeroSection/HeroSection' import MeetOpenThornSection from './components/MeetOpenThornSection/MeetOpenThornSection' import BYOKSection from './components/BYOKSection/BYOKSection' +import AdsterraNativeBanner from './components/AdsterraNativeBanner/AdsterraNativeBanner' import BottomCTA from './components/BottomCTA/BottomCTA' import Footer from './components/Footer/Footer' import AuthModal from './components/AuthModal/AuthModal' @@ -75,6 +76,7 @@ function HomePage() { + ) diff --git a/src/components/AdsterraNativeBanner/AdsterraNativeBanner.module.css b/src/components/AdsterraNativeBanner/AdsterraNativeBanner.module.css new file mode 100644 index 0000000..1794c07 --- /dev/null +++ b/src/components/AdsterraNativeBanner/AdsterraNativeBanner.module.css @@ -0,0 +1,39 @@ +.wrapper { + max-width: var(--max-width); + margin: 0 auto; + padding: var(--space-2xl) var(--space-lg); + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-sm); + text-align: center; +} + +.label { + font-family: var(--font-body); + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.14em; + color: var(--color-text-muted); +} + +.container { + width: 100%; + min-height: 90px; + display: flex; + justify-content: center; + overflow: hidden; + border-radius: var(--radius-lg); +} + +/* Nothing rendered (blocked tag, empty response) — collapse the slot. */ +.wrapper:has(.container:empty) { + display: none; +} + +@media (max-width: 640px) { + .wrapper { + padding: var(--space-xl) var(--space-md); + } +} diff --git a/src/components/AdsterraNativeBanner/AdsterraNativeBanner.tsx b/src/components/AdsterraNativeBanner/AdsterraNativeBanner.tsx new file mode 100644 index 0000000..cf4b5a8 --- /dev/null +++ b/src/components/AdsterraNativeBanner/AdsterraNativeBanner.tsx @@ -0,0 +1,52 @@ +import { useEffect, useRef, useState } from 'react' +import styles from './AdsterraNativeBanner.module.css' + +/** Adsterra native-banner tag (zone acd496412d230553ca4f4a243557f98c). */ +const SCRIPT_SRC = + 'https://pl31110698.profitableratecpmnetwork.com/acd496412d230553ca4f4a243557f98c/invoke.js' +const CONTAINER_ID = 'container-acd496412d230553ca4f4a243557f98c' + +/** + * Only one Adsterra tag may be active at a time. Tracked module-wide so a + * remount always replaces the previous tag instead of stacking duplicates. + */ +let activeScript: HTMLScriptElement | null = null + +export default function AdsterraNativeBanner() { + const containerRef = useRef(null) + const [failed, setFailed] = useState(false) + + useEffect(() => { + const container = containerRef.current + if (!container) return + + // Adsterra's invoke.js fills whichever element matches CONTAINER_ID when it + // executes, so re-append the tag on mount to refill a freshly mounted + // container (route re-entry, Strict Mode double-invoke). + activeScript?.remove() + + const script = document.createElement('script') + script.async = true + script.setAttribute('data-cfasync', 'false') + script.src = SCRIPT_SRC + script.onerror = () => setFailed(true) + document.head.appendChild(script) + activeScript = script + + return () => { + script.remove() + if (activeScript === script) activeScript = null + container.innerHTML = '' + } + }, []) + + // Blocked or unreachable tags (ad blockers, offline) leave no empty gap. + if (failed) return null + + return ( + + ) +} diff --git a/vercel.json b/vercel.json index 77aa222..8e05996 100644 --- a/vercel.json +++ b/vercel.json @@ -24,7 +24,7 @@ { "key": "Strict-Transport-Security", "value": "max-age=31536000; includeSubDomains" }, { "key": "Content-Security-Policy", - "value": "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' blob: data: https://esm.sh https://pagead2.googlesyndication.com https://*.googlesyndication.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; worker-src 'self' blob:; child-src 'self' blob:; frame-src 'self' blob: data: https://www.youtube-nocookie.com https://googleads.g.doubleclick.net https://*.googlesyndication.com; connect-src 'self' https: wss: blob: data:; manifest-src 'self'" + "value": "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' blob: data: https://esm.sh https://pl31110698.profitableratecpmnetwork.com https://*.profitableratecpmnetwork.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; worker-src 'self' blob:; child-src 'self' blob:; frame-src 'self' blob: data: https://www.youtube-nocookie.com https://*.profitableratecpmnetwork.com; connect-src 'self' https: wss: blob: data:; manifest-src 'self'" } ] }