|
|
@ -52,6 +52,14 @@ NUXT_PUBLIC_STATINATOR_COOKIE_DOMAIN=.doxa.life
|
|||
DEEPL_API_KEY=your-deepl-api-key
|
||||
DEEPL_API_URL=https://api.deepl.com
|
||||
|
||||
# Cloudflare cache purge on deploy (server/plugins/cloudflare-purge.ts).
|
||||
# On container boot the edge cache is purged so stale prerendered HTML can't
|
||||
# reference content-hashed assets the new deploy replaced. Leave both blank to
|
||||
# disable (e.g. in dev). Token: My Profile → API Tokens → Custom Token with
|
||||
# Zone → Cache Purge for the doxa.life zone. Zone ID: zone Overview page.
|
||||
CF_API_TOKEN=
|
||||
CF_ZONE_ID=
|
||||
|
||||
# OAuth 2.1 server
|
||||
# Issuer is derived from NUXT_PUBLIC_SITE_URL; resource is `${siteUrl}/mcp`.
|
||||
# Generate with: openssl rand -hex 32
|
||||
|
|
|
|||
|
|
@ -84,6 +84,12 @@ img {
|
|||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* <picture> wraps <img> for AVIF/WebP fallback; display:contents removes its
|
||||
box so the inner <img> stays the effective flex/layout child. */
|
||||
picture {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.bg-image {
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
|
|
|
|||
|
|
@ -16,4 +16,26 @@ const profileConfig = computed(() => JSON.stringify({
|
|||
instanceId: 'fb-doxa-life-parent',
|
||||
projectId: pub.feedbackProjectId
|
||||
}))
|
||||
|
||||
// The feedback widget is a ~95 KB third-party script that is not needed for
|
||||
// first paint. Load it once the browser is idle so it stays off the initial
|
||||
// load critical path. The <feedback-web-component> below upgrades in place
|
||||
// when the definition arrives.
|
||||
const FEEDBACK_SCRIPT = 'https://support.gospelambition.org/js/feedback-web-component.iife.js'
|
||||
|
||||
function loadFeedbackScript() {
|
||||
if (document.querySelector(`script[src="${FEEDBACK_SCRIPT}"]`)) return
|
||||
const script = document.createElement('script')
|
||||
script.src = FEEDBACK_SCRIPT
|
||||
script.async = true
|
||||
document.head.appendChild(script)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if ('requestIdleCallback' in window) {
|
||||
requestIdleCallback(loadFeedbackScript, { timeout: 3000 })
|
||||
} else {
|
||||
setTimeout(loadFeedbackScript, 2000)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,23 @@
|
|||
// routes — admin pages declare their own typography in admin.css.
|
||||
|
||||
const { locale, locales } = useI18n()
|
||||
const { public: pub } = useRuntimeConfig()
|
||||
|
||||
// Warm DNS+TCP+TLS for the cross-origin services these pages hit: the prayer
|
||||
// data API (people-group components) and the idle-loaded analytics + feedback
|
||||
// widget scripts. Deriving from runtime config keeps these correct per env.
|
||||
const preconnectOrigins = computed(() => {
|
||||
const origins = new Set<string>()
|
||||
for (const url of [pub.prayBaseUrl, pub.statinatorUrl, pub.feedbackApiBase]) {
|
||||
if (!url) continue
|
||||
try {
|
||||
origins.add(new URL(String(url)).origin)
|
||||
} catch {
|
||||
// ignore malformed/empty url
|
||||
}
|
||||
}
|
||||
return [...origins]
|
||||
})
|
||||
|
||||
// Direction attribute per locale (Arabic = rtl)
|
||||
const currentLocale = computed(() =>
|
||||
|
|
@ -21,6 +38,10 @@ useHead(() => ({
|
|||
dir: dir.value
|
||||
},
|
||||
link: [
|
||||
...preconnectOrigins.value.map(origin => ({ rel: 'preconnect', href: origin })),
|
||||
// Preload the heading font (Bebas Neue) — it renders the above-the-fold
|
||||
// h1/h2 (the LCP text), so fetching it early avoids a swap flash.
|
||||
{ rel: 'preload', as: 'font', type: 'font/woff2', href: '/assets/fonts/BebasNeue/BebasNeue-Regular.woff2', crossorigin: '' },
|
||||
{ rel: 'stylesheet', href: '/assets/fonts/BebasNeue/stylesheet.css' },
|
||||
{ rel: 'stylesheet', href: '/assets/fonts/Brandon_Grotesque/stylesheet.css' },
|
||||
{ rel: 'stylesheet', href: '/assets/fonts/Poppins/stylesheet.css' }
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ useTextHighlight()
|
|||
</div>
|
||||
<div
|
||||
class="grow-2 bg-image rounded-md"
|
||||
style="background-image: url('/assets/images/adopt-01-africa-4women.jpg');"
|
||||
style="background-image: url('/assets/images/adopt-01-africa-4women.jpg'); background-image: image-set(url('/assets/images/adopt-01-africa-4women.avif') type('image/avif'), url('/assets/images/adopt-01-africa-4women.webp') type('image/webp'), url('/assets/images/adopt-01-africa-4women.jpg') type('image/jpeg'));"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -106,7 +106,24 @@ useTextHighlight()
|
|||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<img class="center" src="/assets/images/adopt-02-ipad-mockup.png" :alt="t('Your daily prayer guide')">
|
||||
<picture>
|
||||
<source
|
||||
srcset="/assets/images/adopt-02-ipad-mockup.avif"
|
||||
type="image/avif"
|
||||
>
|
||||
<source
|
||||
srcset="/assets/images/adopt-02-ipad-mockup.webp"
|
||||
type="image/webp"
|
||||
>
|
||||
<img
|
||||
class="center"
|
||||
src="/assets/images/adopt-02-ipad-mockup.png"
|
||||
:alt="t('Your daily prayer guide')"
|
||||
width="500"
|
||||
height="672"
|
||||
loading="lazy"
|
||||
>
|
||||
</picture>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -118,7 +135,23 @@ useTextHighlight()
|
|||
<h2>{{ t('Why adoption matters') }}</h2>
|
||||
<div class="switcher | gap-md" data-width="xl">
|
||||
<div class="switcher-item center grow-none">
|
||||
<img src="/assets/images/Adopt-03-DurbetInMongolia.jpg" :alt="t('Adopt an unengaged people group')">
|
||||
<picture>
|
||||
<source
|
||||
srcset="/assets/images/Adopt-03-DurbetInMongolia.avif"
|
||||
type="image/avif"
|
||||
>
|
||||
<source
|
||||
srcset="/assets/images/Adopt-03-DurbetInMongolia.webp"
|
||||
type="image/webp"
|
||||
>
|
||||
<img
|
||||
src="/assets/images/Adopt-03-DurbetInMongolia.jpg"
|
||||
:alt="t('Adopt an unengaged people group')"
|
||||
width="150"
|
||||
height="188"
|
||||
loading="lazy"
|
||||
>
|
||||
</picture>
|
||||
</div>
|
||||
<div class="stack stack--lg | text-card | surface-brand-lightest justify-center">
|
||||
<h4 class="font-heading font-size-2xl">{{ t('They have no gospel access') }}</h4>
|
||||
|
|
@ -127,7 +160,23 @@ useTextHighlight()
|
|||
</div>
|
||||
<div class="switcher | gap-md" data-width="xl">
|
||||
<div class="switcher-item center grow-none">
|
||||
<img src="/assets/images/Adopt-04-Maha-Brahmin-in-India.jpg" :alt="t('Adopt an unengaged people group')">
|
||||
<picture>
|
||||
<source
|
||||
srcset="/assets/images/Adopt-04-Maha-Brahmin-in-India.avif"
|
||||
type="image/avif"
|
||||
>
|
||||
<source
|
||||
srcset="/assets/images/Adopt-04-Maha-Brahmin-in-India.webp"
|
||||
type="image/webp"
|
||||
>
|
||||
<img
|
||||
src="/assets/images/Adopt-04-Maha-Brahmin-in-India.jpg"
|
||||
:alt="t('Adopt an unengaged people group')"
|
||||
width="150"
|
||||
height="188"
|
||||
loading="lazy"
|
||||
>
|
||||
</picture>
|
||||
</div>
|
||||
<div class="stack stack--lg | text-card | surface-brand-lightest justify-center">
|
||||
<h4 class="font-heading font-size-2xl overflow-wrap-anywhere">{{ t('Prayer opens the door for engagement') }}</h4>
|
||||
|
|
@ -136,7 +185,23 @@ useTextHighlight()
|
|||
</div>
|
||||
<div class="switcher | gap-md" data-width="xl">
|
||||
<div class="switcher-item center grow-none">
|
||||
<img src="/assets/images/Adopt-05-KamaraInGhana.jpg" :alt="t('Adopt an unengaged people group')">
|
||||
<picture>
|
||||
<source
|
||||
srcset="/assets/images/Adopt-05-KamaraInGhana.avif"
|
||||
type="image/avif"
|
||||
>
|
||||
<source
|
||||
srcset="/assets/images/Adopt-05-KamaraInGhana.webp"
|
||||
type="image/webp"
|
||||
>
|
||||
<img
|
||||
src="/assets/images/Adopt-05-KamaraInGhana.jpg"
|
||||
:alt="t('Adopt an unengaged people group')"
|
||||
width="150"
|
||||
height="188"
|
||||
loading="lazy"
|
||||
>
|
||||
</picture>
|
||||
</div>
|
||||
<div class="stack stack--lg | text-card | surface-brand-lightest justify-center">
|
||||
<h4 class="font-heading font-size-2xl">{{ t("Churches become active partners in God's mission") }}</h4>
|
||||
|
|
@ -174,7 +239,23 @@ useTextHighlight()
|
|||
<figcaption>- {{ t('Jesus') }}</figcaption>
|
||||
</figure>
|
||||
<div>
|
||||
<img src="/assets/images/adopt-bottom-banner.jpg" :alt="t('Jesus')">
|
||||
<picture>
|
||||
<source
|
||||
srcset="/assets/images/adopt-bottom-banner.avif"
|
||||
type="image/avif"
|
||||
>
|
||||
<source
|
||||
srcset="/assets/images/adopt-bottom-banner.webp"
|
||||
type="image/webp"
|
||||
>
|
||||
<img
|
||||
src="/assets/images/adopt-bottom-banner.jpg"
|
||||
:alt="t('Jesus')"
|
||||
width="1200"
|
||||
height="341"
|
||||
loading="lazy"
|
||||
>
|
||||
</picture>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -36,11 +36,24 @@ useTextHighlight()
|
|||
<svg class="icon">
|
||||
<use href="/assets/icons/play-button.svg#play-button" />
|
||||
</svg>
|
||||
<img
|
||||
class="rounded-xlg"
|
||||
src="/assets/images/home-01-hero.jpg"
|
||||
:alt="t('Engage every people by 2033')"
|
||||
>
|
||||
<picture>
|
||||
<source
|
||||
srcset="/assets/images/home-01-hero.avif"
|
||||
type="image/avif"
|
||||
>
|
||||
<source
|
||||
srcset="/assets/images/home-01-hero.webp"
|
||||
type="image/webp"
|
||||
>
|
||||
<img
|
||||
class="rounded-xlg"
|
||||
src="/assets/images/home-01-hero.jpg"
|
||||
:alt="t('Engage every people by 2033')"
|
||||
width="1200"
|
||||
height="723"
|
||||
fetchpriority="high"
|
||||
>
|
||||
</picture>
|
||||
</div>
|
||||
<p class="text-center color-primary uppercase font-button font-weight-medium">
|
||||
{{ t('The DOXA Vision: Click image to watch the video') }}
|
||||
|
|
@ -95,7 +108,7 @@ useTextHighlight()
|
|||
|
||||
<section
|
||||
class="bg-image"
|
||||
style="background-image: url('/assets/images/home-02-WhoAreTheUnreached-new.jpg');"
|
||||
style="background-image: url('/assets/images/home-02-WhoAreTheUnreached-new.jpg'); background-image: image-set(url('/assets/images/home-02-WhoAreTheUnreached-new.avif') type('image/avif'), url('/assets/images/home-02-WhoAreTheUnreached-new.webp') type('image/webp'), url('/assets/images/home-02-WhoAreTheUnreached-new.jpg') type('image/jpeg'));"
|
||||
>
|
||||
<h2 class="text-center banner-title invisible-placeholder">{{ t('Who are the unengaged?') }}</h2>
|
||||
</section>
|
||||
|
|
@ -156,12 +169,25 @@ useTextHighlight()
|
|||
<div class="">
|
||||
<div class="switcher" data-width="xl">
|
||||
<div class="switcher-item center grow-none">
|
||||
<img
|
||||
class="center"
|
||||
src="/assets/images/home-03-Vision-2033.jpg"
|
||||
:alt="t('Vision 2033')"
|
||||
style="width: clamp(150px, 25vw, 350px);"
|
||||
>
|
||||
<picture>
|
||||
<source
|
||||
srcset="/assets/images/home-03-Vision-2033.avif"
|
||||
type="image/avif"
|
||||
>
|
||||
<source
|
||||
srcset="/assets/images/home-03-Vision-2033.webp"
|
||||
type="image/webp"
|
||||
>
|
||||
<img
|
||||
class="center"
|
||||
src="/assets/images/home-03-Vision-2033.jpg"
|
||||
:alt="t('Vision 2033')"
|
||||
width="366"
|
||||
height="679"
|
||||
loading="lazy"
|
||||
style="width: clamp(150px, 25vw, 350px);"
|
||||
>
|
||||
</picture>
|
||||
</div>
|
||||
<div class="switcher-item align-center justify-center">
|
||||
<div class="stack stack--xl">
|
||||
|
|
@ -191,10 +217,23 @@ useTextHighlight()
|
|||
<h2 class="highlight" data-highlight-last>{{ t('Engagement starts with prayer') }}</h2>
|
||||
<div class="switcher | align-center">
|
||||
<div class="switcher-item center grow-none">
|
||||
<img
|
||||
src="/assets/images/home-04-EngagementStartsWithPrayer.jpg"
|
||||
:alt="t('Engagement starts with prayer')"
|
||||
>
|
||||
<picture>
|
||||
<source
|
||||
srcset="/assets/images/home-04-EngagementStartsWithPrayer.avif"
|
||||
type="image/avif"
|
||||
>
|
||||
<source
|
||||
srcset="/assets/images/home-04-EngagementStartsWithPrayer.webp"
|
||||
type="image/webp"
|
||||
>
|
||||
<img
|
||||
src="/assets/images/home-04-EngagementStartsWithPrayer.jpg"
|
||||
:alt="t('Engagement starts with prayer')"
|
||||
width="350"
|
||||
height="344"
|
||||
loading="lazy"
|
||||
>
|
||||
</picture>
|
||||
</div>
|
||||
<div>
|
||||
<div class="stack stack--3xl | align-center">
|
||||
|
|
@ -210,7 +249,7 @@ useTextHighlight()
|
|||
<div class="switcher container | gap-md" data-width="xl">
|
||||
<div
|
||||
class="switcher-item card | padding-clamp-2xl bg-image align-center"
|
||||
style="background-image: url('/assets/images/home-doxa-background.jpg');"
|
||||
style="background-image: url('/assets/images/home-doxa-background.jpg'); background-image: image-set(url('/assets/images/home-doxa-background.avif') type('image/avif'), url('/assets/images/home-doxa-background.webp') type('image/webp'), url('/assets/images/home-doxa-background.jpg') type('image/jpeg'));"
|
||||
>
|
||||
<div class="stack stack--md | text-center text-secondary">
|
||||
<h2>{{ t('What does "DOXA" mean?') }}</h2>
|
||||
|
|
@ -219,11 +258,24 @@ useTextHighlight()
|
|||
</div>
|
||||
</div>
|
||||
<div class="switcher-item center grow-none">
|
||||
<img
|
||||
class="rounded-xlg"
|
||||
src="/assets/images/home-05-WhatDoesDoxaMean.jpg"
|
||||
:alt="t('Engagement starts with prayer')"
|
||||
>
|
||||
<picture>
|
||||
<source
|
||||
srcset="/assets/images/home-05-WhatDoesDoxaMean.avif"
|
||||
type="image/avif"
|
||||
>
|
||||
<source
|
||||
srcset="/assets/images/home-05-WhatDoesDoxaMean.webp"
|
||||
type="image/webp"
|
||||
>
|
||||
<img
|
||||
class="rounded-xlg"
|
||||
src="/assets/images/home-05-WhatDoesDoxaMean.jpg"
|
||||
:alt="t('Engagement starts with prayer')"
|
||||
width="300"
|
||||
height="520"
|
||||
loading="lazy"
|
||||
>
|
||||
</picture>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ useTextHighlight()
|
|||
</div>
|
||||
<div
|
||||
class="grow-2 bg-image rounded-md"
|
||||
style="background-image: url('/assets/images/pray-01-hero.jpg');"
|
||||
style="background-image: url('/assets/images/pray-01-hero.jpg'); background-image: image-set(url('/assets/images/pray-01-hero.avif') type('image/avif'), url('/assets/images/pray-01-hero.webp') type('image/webp'), url('/assets/images/pray-01-hero.jpg') type('image/jpeg'));"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -109,11 +109,24 @@ useTextHighlight()
|
|||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<img
|
||||
class="center"
|
||||
src="/assets/images/pray-02-PrayerFUEL-Phone-graphic-2.png"
|
||||
:alt="t('Your daily prayer guide')"
|
||||
>
|
||||
<picture>
|
||||
<source
|
||||
srcset="/assets/images/pray-02-PrayerFUEL-Phone-graphic-2.avif"
|
||||
type="image/avif"
|
||||
>
|
||||
<source
|
||||
srcset="/assets/images/pray-02-PrayerFUEL-Phone-graphic-2.webp"
|
||||
type="image/webp"
|
||||
>
|
||||
<img
|
||||
class="center"
|
||||
src="/assets/images/pray-02-PrayerFUEL-Phone-graphic-2.png"
|
||||
:alt="t('Your daily prayer guide')"
|
||||
width="500"
|
||||
height="872"
|
||||
loading="lazy"
|
||||
>
|
||||
</picture>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -125,7 +138,23 @@ useTextHighlight()
|
|||
<h2>{{ t('Why prayer matters') }}</h2>
|
||||
<div class="switcher | gap-md" data-width="xl">
|
||||
<div class="switcher-item center grow-none">
|
||||
<img src="/assets/images/Pray-04-Doxa.jpg" :alt="t('Adopt an unengaged people group')">
|
||||
<picture>
|
||||
<source
|
||||
srcset="/assets/images/Pray-04-Doxa.avif"
|
||||
type="image/avif"
|
||||
>
|
||||
<source
|
||||
srcset="/assets/images/Pray-04-Doxa.webp"
|
||||
type="image/webp"
|
||||
>
|
||||
<img
|
||||
src="/assets/images/Pray-04-Doxa.jpg"
|
||||
:alt="t('Adopt an unengaged people group')"
|
||||
width="150"
|
||||
height="179"
|
||||
loading="lazy"
|
||||
>
|
||||
</picture>
|
||||
</div>
|
||||
<div class="stack stack--lg | text-card | surface-brand-lightest justify-center">
|
||||
<h4 class="font-heading font-size-2xl">{{ t('They have no one praying for them') }}</h4>
|
||||
|
|
@ -134,7 +163,23 @@ useTextHighlight()
|
|||
</div>
|
||||
<div class="switcher | gap-md" data-width="xl">
|
||||
<div class="switcher-item center grow-none">
|
||||
<img src="/assets/images/Pray-05-Doxa.jpg" :alt="t('Adopt an unengaged people group')">
|
||||
<picture>
|
||||
<source
|
||||
srcset="/assets/images/Pray-05-Doxa.avif"
|
||||
type="image/avif"
|
||||
>
|
||||
<source
|
||||
srcset="/assets/images/Pray-05-Doxa.webp"
|
||||
type="image/webp"
|
||||
>
|
||||
<img
|
||||
src="/assets/images/Pray-05-Doxa.jpg"
|
||||
:alt="t('Adopt an unengaged people group')"
|
||||
width="150"
|
||||
height="193"
|
||||
loading="lazy"
|
||||
>
|
||||
</picture>
|
||||
</div>
|
||||
<div class="stack stack--lg | text-card | surface-brand-lightest justify-center">
|
||||
<h4 class="font-heading font-size-2xl">{{ t('Prayer prepares the way for the gospel') }}</h4>
|
||||
|
|
@ -143,7 +188,23 @@ useTextHighlight()
|
|||
</div>
|
||||
<div class="switcher | gap-md" data-width="xl">
|
||||
<div class="switcher-item center grow-none">
|
||||
<img src="/assets/images/Pray-06-Doxa.jpg" :alt="t('Adopt an unengaged people group')">
|
||||
<picture>
|
||||
<source
|
||||
srcset="/assets/images/Pray-06-Doxa.avif"
|
||||
type="image/avif"
|
||||
>
|
||||
<source
|
||||
srcset="/assets/images/Pray-06-Doxa.webp"
|
||||
type="image/webp"
|
||||
>
|
||||
<img
|
||||
src="/assets/images/Pray-06-Doxa.jpg"
|
||||
:alt="t('Adopt an unengaged people group')"
|
||||
width="150"
|
||||
height="191"
|
||||
loading="lazy"
|
||||
>
|
||||
</picture>
|
||||
</div>
|
||||
<div class="stack stack--lg | text-card | surface-brand-lightest justify-center">
|
||||
<h4 class="font-heading font-size-2xl">{{ t('Prayer unites the global church') }}</h4>
|
||||
|
|
@ -193,7 +254,23 @@ useTextHighlight()
|
|||
<figcaption>- {{ t('Jesus') }}</figcaption>
|
||||
</figure>
|
||||
<div>
|
||||
<img src="/assets/images/pray-03-bottom-unsplash.jpg" :alt="t('Jesus')">
|
||||
<picture>
|
||||
<source
|
||||
srcset="/assets/images/pray-03-bottom-unsplash.avif"
|
||||
type="image/avif"
|
||||
>
|
||||
<source
|
||||
srcset="/assets/images/pray-03-bottom-unsplash.webp"
|
||||
type="image/webp"
|
||||
>
|
||||
<img
|
||||
src="/assets/images/pray-03-bottom-unsplash.jpg"
|
||||
:alt="t('Jesus')"
|
||||
width="1200"
|
||||
height="328"
|
||||
loading="lazy"
|
||||
>
|
||||
</picture>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -66,7 +66,16 @@ export default defineNuxtPlugin(() => {
|
|||
injectScript().then(() => window.goStats?.pageview()).catch(() => {})
|
||||
}
|
||||
|
||||
if (!isExcluded(useRoute().path)) firePageview()
|
||||
// Defer the initial pageview's script injection until the browser is idle so
|
||||
// the analytics bundle stays off the initial load critical path. SPA route
|
||||
// changes (below) still fire immediately — they happen after load.
|
||||
if (!isExcluded(useRoute().path)) {
|
||||
if ('requestIdleCallback' in window) {
|
||||
requestIdleCallback(() => firePageview(), { timeout: 3000 })
|
||||
} else {
|
||||
setTimeout(firePageview, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
router.afterEach((to, from) => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
import { existsSync, readdirSync, rmSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { generateI18nLocales } from './config/languages'
|
||||
import { generateI18nLocales, ENABLED_LANGUAGE_CODES } from './config/languages'
|
||||
|
||||
const LAYERS_DIR = '.layers'
|
||||
|
||||
|
|
@ -20,6 +20,24 @@ function stripLayerTsconfigs() {
|
|||
|
||||
stripLayerTsconfigs()
|
||||
|
||||
// Prerendered marketing pages are served as edge-cacheable HTML so first paint
|
||||
// comes from Cloudflare rather than the origin. `max-age=0` keeps browsers
|
||||
// revalidating (a deploy is visible immediately); `s-maxage` is the edge TTL;
|
||||
// `stale-while-revalidate` lets the edge serve instantly while it refreshes. The
|
||||
// deploy purge (server/plugins/cloudflare-purge.ts) evicts stale HTML so it can
|
||||
// never reference content-hashed /_nuxt assets the new build replaced.
|
||||
const MARKETING_HTML_CACHE = 'public, max-age=0, s-maxage=86400, stale-while-revalidate=86400'
|
||||
const MARKETING_PAGES = ['', '/adopt', '/pray', '/research', '/contact-us']
|
||||
const MARKETING_LOCALE_PREFIXES = ['', ...ENABLED_LANGUAGE_CODES.filter(code => code !== 'en').map(code => `/${code}`)]
|
||||
const marketingRouteRules = Object.fromEntries(
|
||||
MARKETING_LOCALE_PREFIXES.flatMap(prefix =>
|
||||
MARKETING_PAGES.map(page => [
|
||||
`${prefix}${page}` || '/',
|
||||
{ prerender: true, headers: { 'cache-control': MARKETING_HTML_CACHE } }
|
||||
])
|
||||
)
|
||||
)
|
||||
|
||||
export default defineNuxtConfig({
|
||||
extends: [
|
||||
process.env.OAUTH_LAYER_PATH || ['github:corsacca/nuxt-blueprints/layers/oauth#master', {
|
||||
|
|
@ -102,9 +120,6 @@ export default defineNuxtConfig({
|
|||
{ rel: 'icon', type: 'image/png', sizes: '192x192', href: '/favicon/cropped-Favicon-light-doxa-01-192x192.png' },
|
||||
{ rel: 'apple-touch-icon', href: '/favicon/cropped-Favicon-light-doxa-01-180x180.png' }
|
||||
],
|
||||
script: [
|
||||
{ src: 'https://support.gospelambition.org/js/feedback-web-component.iife.js', defer: true }
|
||||
],
|
||||
meta: [
|
||||
{ name: 'msapplication-TileImage', content: '/favicon/cropped-Favicon-light-doxa-01-270x270.png' }
|
||||
]
|
||||
|
|
@ -112,11 +127,13 @@ export default defineNuxtConfig({
|
|||
},
|
||||
|
||||
routeRules: {
|
||||
'/': { prerender: true },
|
||||
'/adopt': { prerender: true },
|
||||
'/pray': { prerender: true },
|
||||
'/research': { prerender: true },
|
||||
'/contact-us': { prerender: true },
|
||||
// Content-hashed build assets never change under the same URL — cache forever.
|
||||
'/_nuxt/**': { headers: { 'cache-control': 'public, max-age=31536000, immutable' } },
|
||||
// Fonts/images use stable filenames, so avoid `immutable`: a 7-day cache
|
||||
// still lets an overwritten asset propagate within a week.
|
||||
'/assets/**': { headers: { 'cache-control': 'public, max-age=604800' } },
|
||||
// Prerendered marketing pages (all enabled locales) — edge-cacheable HTML.
|
||||
...marketingRouteRules,
|
||||
'/login': { ssr: false, prerender: false },
|
||||
'/register': { ssr: false, prerender: false },
|
||||
'/reset-password': { ssr: false, prerender: false },
|
||||
|
|
@ -135,6 +152,9 @@ export default defineNuxtConfig({
|
|||
// server crashes on boot. The bun preset builds the output for Bun's own
|
||||
// resolver. Run it with `bun run ./.output/server/index.mjs`.
|
||||
preset: 'bun',
|
||||
// Precompress public assets (JS/CSS/HTML/SVG/JSON) to .br and .gz at build
|
||||
// time; the server serves the compressed variant when the client supports it.
|
||||
compressPublicAssets: { brotli: true, gzip: true },
|
||||
// Dev-only: use in-memory cache for the payload mount. The default fs
|
||||
// driver can't represent both a leaf route ("/fr") and children of that
|
||||
// route ("/fr/adopt") simultaneously — the first visit writes a file at
|
||||
|
|
@ -211,6 +231,10 @@ export default defineNuxtConfig({
|
|||
prayBaseUrl: process.env.NUXT_PRAY_BASE_URL || 'https://pray.doxa.life',
|
||||
deeplApiKey: process.env.DEEPL_API_KEY || '',
|
||||
deeplApiUrl: process.env.DEEPL_API_URL || 'https://api.deepl.com',
|
||||
// Cloudflare cache purge on deploy (server/plugins/cloudflare-purge.ts).
|
||||
// Token needs only Zone → Cache Purge for the doxa.life zone.
|
||||
cfApiToken: process.env.CF_API_TOKEN || '',
|
||||
cfZoneId: process.env.CF_ZONE_ID || '',
|
||||
public: {
|
||||
appName: process.env.APP_TITLE || 'My App',
|
||||
nodeEnv: process.env.NODE_ENV || '',
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 166 KiB |
|
|
@ -1,192 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex, noarchive">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<title>Transfonter demo</title>
|
||||
<link href="stylesheet.css" rel="stylesheet">
|
||||
<style>
|
||||
/*
|
||||
http://meyerweb.com/eric/tools/css/reset/
|
||||
v2.0 | 20110126
|
||||
License: none (public domain)
|
||||
*/
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
a, abbr, acronym, address, big, cite, code,
|
||||
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||
small, strike, strong, sub, sup, tt, var,
|
||||
b, u, i, center,
|
||||
dl, dt, dd, ol, ul, li,
|
||||
fieldset, form, label, legend,
|
||||
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||
article, aside, canvas, details, embed,
|
||||
figure, figcaption, footer, header, hgroup,
|
||||
menu, nav, output, ruby, section, summary,
|
||||
time, mark, audio, video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section {
|
||||
display: block;
|
||||
}
|
||||
body {
|
||||
line-height: 1;
|
||||
}
|
||||
ol, ul {
|
||||
list-style: none;
|
||||
}
|
||||
blockquote, q {
|
||||
quotes: none;
|
||||
}
|
||||
blockquote:before, blockquote:after,
|
||||
q:before, q:after {
|
||||
content: '';
|
||||
content: none;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
/* demo styles */
|
||||
body {
|
||||
background: #f0f0f0;
|
||||
color: #000;
|
||||
}
|
||||
.page {
|
||||
background: #fff;
|
||||
width: 920px;
|
||||
margin: 0 auto;
|
||||
padding: 20px 20px 0 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.font-container {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
margin-bottom: 40px;
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
h1 {
|
||||
position: relative;
|
||||
background: #444;
|
||||
font-size: 32px;
|
||||
color: #fff;
|
||||
padding: 10px 20px;
|
||||
margin: 0 -20px 12px -20px;
|
||||
}
|
||||
.letters {
|
||||
font-size: 25px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.s10:before {
|
||||
content: '10px';
|
||||
}
|
||||
.s11:before {
|
||||
content: '11px';
|
||||
}
|
||||
.s12:before {
|
||||
content: '12px';
|
||||
}
|
||||
.s14:before {
|
||||
content: '14px';
|
||||
}
|
||||
.s18:before {
|
||||
content: '18px';
|
||||
}
|
||||
.s24:before {
|
||||
content: '24px';
|
||||
}
|
||||
.s30:before {
|
||||
content: '30px';
|
||||
}
|
||||
.s36:before {
|
||||
content: '36px';
|
||||
}
|
||||
.s48:before {
|
||||
content: '48px';
|
||||
}
|
||||
.s60:before {
|
||||
content: '60px';
|
||||
}
|
||||
.s72:before {
|
||||
content: '72px';
|
||||
}
|
||||
.s10:before, .s11:before, .s12:before, .s14:before,
|
||||
.s18:before, .s24:before, .s30:before, .s36:before,
|
||||
.s48:before, .s60:before, .s72:before {
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 10px;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
color: #999;
|
||||
padding-right: 6px;
|
||||
}
|
||||
pre {
|
||||
display: block;
|
||||
padding: 9px;
|
||||
margin: 0 0 12px;
|
||||
font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.428571429;
|
||||
color: #333;
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
background-color: #f5f5f5;
|
||||
border: 1px solid #ccc;
|
||||
overflow-x: auto;
|
||||
border-radius: 4px;
|
||||
}
|
||||
/* responsive */
|
||||
@media (max-width: 959px) {
|
||||
.page {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<div class="demo">
|
||||
<h1 style="font-family: 'Bebas Kai'; font-weight: normal; font-style: normal;">Bebas Kai</h1>
|
||||
<pre title="Usage">.your-style {
|
||||
font-family: 'Bebas Kai';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}</pre>
|
||||
<pre title="Preload (optional)">
|
||||
<link rel="preload" href="BebasKai.woff2" as="font" type="font/woff2" crossorigin></pre>
|
||||
<div class="font-container" style="font-family: 'Bebas Kai'; font-weight: normal; font-style: normal;">
|
||||
<p class="letters">
|
||||
abcdefghijklmnopqrstuvwxyz<br>
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ<br>
|
||||
0123456789.:,;()*!?'@#<>$%&^+-=~
|
||||
</p>
|
||||
<p class="s10" style="font-size: 10px;">The quick brown fox jumps over the lazy dog.</p>
|
||||
<p class="s11" style="font-size: 11px;">The quick brown fox jumps over the lazy dog.</p>
|
||||
<p class="s12" style="font-size: 12px;">The quick brown fox jumps over the lazy dog.</p>
|
||||
<p class="s14" style="font-size: 14px;">The quick brown fox jumps over the lazy dog.</p>
|
||||
<p class="s18" style="font-size: 18px;">The quick brown fox jumps over the lazy dog.</p>
|
||||
<p class="s24" style="font-size: 24px;">The quick brown fox jumps over the lazy dog.</p>
|
||||
<p class="s30" style="font-size: 30px;">The quick brown fox jumps over the lazy dog.</p>
|
||||
<p class="s36" style="font-size: 36px;">The quick brown fox jumps over the lazy dog.</p>
|
||||
<p class="s48" style="font-size: 48px;">The quick brown fox jumps over the lazy dog.</p>
|
||||
<p class="s60" style="font-size: 60px;">The quick brown fox jumps over the lazy dog.</p>
|
||||
<p class="s72" style="font-size: 72px;">The quick brown fox jumps over the lazy dog.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
@font-face {
|
||||
font-family: 'Bebas Kai';
|
||||
src: url('BebasKai.eot');
|
||||
src: url('BebasKai.eot?#iefix') format('embedded-opentype'),
|
||||
url('BebasKai.woff2') format('woff2'),
|
||||
url('BebasKai.woff') format('woff'),
|
||||
url('BebasKai.ttf') format('truetype'),
|
||||
url('BebasKai.svg#BebasKai') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
BIN
public/assets/fonts/BebasNeue/BebasNeue-Regular.woff2
Normal file
|
|
@ -1,6 +1,7 @@
|
|||
@font-face {
|
||||
font-family: 'Bebas Neue';
|
||||
src: url('BebasNeue-Regular.ttf') format('truetype');
|
||||
src: url('BebasNeue-Regular.woff2') format('woff2'),
|
||||
url('BebasNeue-Regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
@font-face {
|
||||
font-family: 'Brandon Grotesque';
|
||||
src: url('Brandon_Grotesque_medium.otf') format('opentype');
|
||||
src: url('Brandon_Grotesque_medium.woff2') format('woff2'),
|
||||
url('Brandon_Grotesque_medium.otf') format('opentype');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
|
|
|
|||
BIN
public/assets/fonts/Poppins/Poppins-Bold.woff2
Normal file
BIN
public/assets/fonts/Poppins/Poppins-BoldItalic.woff2
Normal file
BIN
public/assets/fonts/Poppins/Poppins-Italic.woff2
Normal file
BIN
public/assets/fonts/Poppins/Poppins-Medium.woff2
Normal file
BIN
public/assets/fonts/Poppins/Poppins-MediumItalic.woff2
Normal file
BIN
public/assets/fonts/Poppins/Poppins-Regular.woff2
Normal file
|
|
@ -1,6 +1,7 @@
|
|||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('Poppins-Regular.ttf') format('truetype');
|
||||
src: url('Poppins-Regular.woff2') format('woff2'),
|
||||
url('Poppins-Regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
|
|
@ -8,7 +9,8 @@
|
|||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('Poppins-Italic.ttf') format('truetype');
|
||||
src: url('Poppins-Italic.woff2') format('woff2'),
|
||||
url('Poppins-Italic.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
|
|
@ -16,7 +18,8 @@
|
|||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('Poppins-Medium.ttf') format('truetype');
|
||||
src: url('Poppins-Medium.woff2') format('woff2'),
|
||||
url('Poppins-Medium.ttf') format('truetype');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
|
|
@ -24,7 +27,8 @@
|
|||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('Poppins-MediumItalic.ttf') format('truetype');
|
||||
src: url('Poppins-MediumItalic.woff2') format('woff2'),
|
||||
url('Poppins-MediumItalic.ttf') format('truetype');
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
|
|
@ -32,7 +36,8 @@
|
|||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('Poppins-Bold.ttf') format('truetype');
|
||||
src: url('Poppins-Bold.woff2') format('woff2'),
|
||||
url('Poppins-Bold.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
|
|
@ -40,7 +45,8 @@
|
|||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url('Poppins-BoldItalic.ttf') format('truetype');
|
||||
src: url('Poppins-BoldItalic.woff2') format('woff2'),
|
||||
url('Poppins-BoldItalic.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
|
|
|
|||
BIN
public/assets/images/Adopt-03-DurbetInMongolia.avif
Normal file
BIN
public/assets/images/Adopt-03-DurbetInMongolia.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
public/assets/images/Adopt-04-Maha-Brahmin-in-India.avif
Normal file
BIN
public/assets/images/Adopt-04-Maha-Brahmin-in-India.webp
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
public/assets/images/Adopt-05-KamaraInGhana.avif
Normal file
BIN
public/assets/images/Adopt-05-KamaraInGhana.webp
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
public/assets/images/Pray-04-Doxa.avif
Normal file
BIN
public/assets/images/Pray-04-Doxa.webp
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
public/assets/images/Pray-05-Doxa.avif
Normal file
BIN
public/assets/images/Pray-05-Doxa.webp
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
public/assets/images/Pray-06-Doxa.avif
Normal file
BIN
public/assets/images/Pray-06-Doxa.webp
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
public/assets/images/adopt-01-africa-4women.avif
Normal file
BIN
public/assets/images/adopt-01-africa-4women.webp
Normal file
|
After Width: | Height: | Size: 165 KiB |
BIN
public/assets/images/adopt-02-ipad-mockup.avif
Normal file
BIN
public/assets/images/adopt-02-ipad-mockup.webp
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
public/assets/images/adopt-bottom-banner.avif
Normal file
BIN
public/assets/images/adopt-bottom-banner.webp
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
public/assets/images/home-01-hero.avif
Normal file
BIN
public/assets/images/home-01-hero.webp
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
public/assets/images/home-02-WhoAreTheUnreached-new.avif
Normal file
BIN
public/assets/images/home-02-WhoAreTheUnreached-new.webp
Normal file
|
After Width: | Height: | Size: 218 KiB |
BIN
public/assets/images/home-03-Vision-2033.avif
Normal file
BIN
public/assets/images/home-03-Vision-2033.webp
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
public/assets/images/home-04-EngagementStartsWithPrayer.avif
Normal file
BIN
public/assets/images/home-04-EngagementStartsWithPrayer.webp
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
public/assets/images/home-05-WhatDoesDoxaMean.avif
Normal file
BIN
public/assets/images/home-05-WhatDoesDoxaMean.webp
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
public/assets/images/home-doxa-background.avif
Normal file
BIN
public/assets/images/home-doxa-background.webp
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
public/assets/images/pray-01-hero.avif
Normal file
BIN
public/assets/images/pray-01-hero.webp
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
public/assets/images/pray-02-PrayerFUEL-Phone-graphic-2.avif
Normal file
BIN
public/assets/images/pray-02-PrayerFUEL-Phone-graphic-2.webp
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
public/assets/images/pray-03-bottom-unsplash.avif
Normal file
BIN
public/assets/images/pray-03-bottom-unsplash.webp
Normal file
|
After Width: | Height: | Size: 42 KiB |
27
server/plugins/cloudflare-purge.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// Purge the Cloudflare edge cache when a fresh container boots — which is what
|
||||
// every deploy triggers on this platform. The prerendered marketing HTML is
|
||||
// edge-cached (Cache-Control s-maxage), but it references content-hashed
|
||||
// /_nuxt assets that the new deploy just replaced. If stale HTML survived in
|
||||
// the edge it would point at files that no longer exist, so it must be evicted.
|
||||
//
|
||||
// Fire-and-forget: a purge failure must never block the server from starting.
|
||||
//
|
||||
// Caveat: this also runs on plain restarts / scale-ups, which is harmless (it
|
||||
// just re-warms the cache). If the app ever enters a crash loop, move the purge
|
||||
// to a POST_DEPLOY job so the API can't be hammered.
|
||||
export default defineNitroPlugin(() => {
|
||||
// Don't fire during local dev or the build-time prerender pass — only when a
|
||||
// real deployed container comes up.
|
||||
if (import.meta.dev || import.meta.prerender) return
|
||||
|
||||
const { cfApiToken, cfZoneId } = useRuntimeConfig()
|
||||
if (!cfApiToken || !cfZoneId) return
|
||||
|
||||
$fetch(`https://api.cloudflare.com/client/v4/zones/${cfZoneId}/purge_cache`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${cfApiToken}` },
|
||||
body: { purge_everything: true }
|
||||
})
|
||||
.then(() => console.log('[cf-purge] Cloudflare edge cache purged on boot'))
|
||||
.catch((err: unknown) => console.error('[cf-purge] purge failed:', err))
|
||||
})
|
||||