Merge pull request #5 from doxa-life/perf

Perf
This commit is contained in:
corsacca 2026-05-20 11:19:56 -05:00 committed by GitHub
commit aceec6e85b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
76 changed files with 391 additions and 2492 deletions

View file

@ -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

View file

@ -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;

View file

@ -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>

View file

@ -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' }

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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) => {

View file

@ -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 || '',

File diff suppressed because it is too large Load diff

Before

Width:  |  Height:  |  Size: 166 KiB

View file

@ -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)">
&lt;link rel=&quot;preload&quot; href=&quot;BebasKai.woff2&quot; as=&quot;font&quot; type=&quot;font/woff2&quot; crossorigin&gt;</pre>
<div class="font-container" style="font-family: 'Bebas Kai'; font-weight: normal; font-style: normal;">
<p class="letters">
abcdefghijklmnopqrstuvwxyz<br>
ABCDEFGHIJKLMNOPQRSTUVWXYZ<br>
0123456789.:,;()*!?'@#&lt;&gt;$%&^+-=~
</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>

View file

@ -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;
}

Binary file not shown.

View 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;

View file

@ -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;

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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;

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View 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))
})