1
0
mirror of https://gitlab.com/MisterBiggs/brain-quartz.git synced 2025-07-23 06:41:30 +00:00
Files
brain-quartz/quartz/plugins/emitters/componentResources.ts
Wen Taichi 7253557d2c fix(goatcounter): properly count SPA page hits (#2035)
1. Should not create new instance after count.js, as it already setup an
instance.
2. Script block would be removed when navigating with SPA, and it cause
count.js can not find endpoint by query. The solution is to set
endpoint manually.
2025-07-07 22:52:49 -04:00

324 lines
13 KiB
TypeScript

import { FullSlug, joinSegments } from "../../util/path"
import { QuartzEmitterPlugin } from "../types"
// @ts-ignore
import spaRouterScript from "../../components/scripts/spa.inline"
// @ts-ignore
import popoverScript from "../../components/scripts/popover.inline"
import styles from "../../styles/custom.scss"
import popoverStyle from "../../components/styles/popover.scss"
import { BuildCtx } from "../../util/ctx"
import { QuartzComponent } from "../../components/types"
import {
googleFontHref,
googleFontSubsetHref,
joinStyles,
processGoogleFonts,
} from "../../util/theme"
import { Features, transform } from "lightningcss"
import { transform as transpile } from "esbuild"
import { write } from "./helpers"
type ComponentResources = {
css: string[]
beforeDOMLoaded: string[]
afterDOMLoaded: string[]
}
function getComponentResources(ctx: BuildCtx): ComponentResources {
const allComponents: Set<QuartzComponent> = new Set()
for (const emitter of ctx.cfg.plugins.emitters) {
const components = emitter.getQuartzComponents?.(ctx) ?? []
for (const component of components) {
allComponents.add(component)
}
}
const componentResources = {
css: new Set<string>(),
beforeDOMLoaded: new Set<string>(),
afterDOMLoaded: new Set<string>(),
}
function normalizeResource(resource: string | string[] | undefined): string[] {
if (!resource) return []
if (Array.isArray(resource)) return resource
return [resource]
}
for (const component of allComponents) {
const { css, beforeDOMLoaded, afterDOMLoaded } = component
const normalizedCss = normalizeResource(css)
const normalizedBeforeDOMLoaded = normalizeResource(beforeDOMLoaded)
const normalizedAfterDOMLoaded = normalizeResource(afterDOMLoaded)
normalizedCss.forEach((c) => componentResources.css.add(c))
normalizedBeforeDOMLoaded.forEach((b) => componentResources.beforeDOMLoaded.add(b))
normalizedAfterDOMLoaded.forEach((a) => componentResources.afterDOMLoaded.add(a))
}
return {
css: [...componentResources.css],
beforeDOMLoaded: [...componentResources.beforeDOMLoaded],
afterDOMLoaded: [...componentResources.afterDOMLoaded],
}
}
async function joinScripts(scripts: string[]): Promise<string> {
// wrap with iife to prevent scope collision
const script = scripts.map((script) => `(function () {${script}})();`).join("\n")
// minify with esbuild
const res = await transpile(script, {
minify: true,
})
return res.code
}
function addGlobalPageResources(ctx: BuildCtx, componentResources: ComponentResources) {
const cfg = ctx.cfg.configuration
// popovers
if (cfg.enablePopovers) {
componentResources.afterDOMLoaded.push(popoverScript)
componentResources.css.push(popoverStyle)
}
if (cfg.analytics?.provider === "google") {
const tagId = cfg.analytics.tagId
componentResources.afterDOMLoaded.push(`
const gtagScript = document.createElement('script');
gtagScript.src = 'https://www.googletagmanager.com/gtag/js?id=${tagId}';
gtagScript.defer = true;
gtagScript.onload = () => {
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', '${tagId}', { send_page_view: false });
gtag('event', 'page_view', { page_title: document.title, page_location: location.href });
document.addEventListener('nav', () => {
gtag('event', 'page_view', { page_title: document.title, page_location: location.href });
});
};
document.head.appendChild(gtagScript);
`)
} else if (cfg.analytics?.provider === "plausible") {
const plausibleHost = cfg.analytics.host ?? "https://plausible.io"
componentResources.afterDOMLoaded.push(`
const plausibleScript = document.createElement('script');
plausibleScript.src = '${plausibleHost}/js/script.manual.js';
plausibleScript.setAttribute('data-domain', location.hostname);
plausibleScript.defer = true;
plausibleScript.onload = () => {
window.plausible = window.plausible || function () { (window.plausible.q = window.plausible.q || []).push(arguments); };
plausible('pageview');
document.addEventListener('nav', () => {
plausible('pageview');
});
};
document.head.appendChild(plausibleScript);
`)
} else if (cfg.analytics?.provider === "umami") {
componentResources.afterDOMLoaded.push(`
const umamiScript = document.createElement("script");
umamiScript.src = "${cfg.analytics.host ?? "https://analytics.umami.is"}/script.js";
umamiScript.setAttribute("data-website-id", "${cfg.analytics.websiteId}");
umamiScript.setAttribute("data-auto-track", "true");
umamiScript.defer = true;
document.head.appendChild(umamiScript);
`)
} else if (cfg.analytics?.provider === "goatcounter") {
componentResources.afterDOMLoaded.push(`
const goatcounterScriptPre = document.createElement('script');
goatcounterScriptPre.textContent = \`
window.goatcounter = { no_onload: true };
\`;
document.head.appendChild(goatcounterScriptPre);
const endpoint = "https://${cfg.analytics.websiteId}.${cfg.analytics.host ?? "goatcounter.com"}/count";
const goatcounterScript = document.createElement('script');
goatcounterScript.src = "${cfg.analytics.scriptSrc ?? "https://gc.zgo.at/count.js"}";
goatcounterScript.defer = true;
goatcounterScript.setAttribute('data-goatcounter', endpoint);
goatcounterScript.onload = () => {
window.goatcounter.endpoint = endpoint;
goatcounter.count({ path: location.pathname });
document.addEventListener('nav', () => {
goatcounter.count({ path: location.pathname });
});
};
document.head.appendChild(goatcounterScript);
`)
} else if (cfg.analytics?.provider === "posthog") {
componentResources.afterDOMLoaded.push(`
const posthogScript = document.createElement("script");
posthogScript.innerHTML= \`!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys onSessionId".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('${cfg.analytics.apiKey}', {
api_host: '${cfg.analytics.host ?? "https://app.posthog.com"}',
capture_pageview: false,
});
document.addEventListener('nav', () => {
posthog.capture('$pageview', { path: location.pathname });
})\`
document.head.appendChild(posthogScript);
`)
} else if (cfg.analytics?.provider === "tinylytics") {
const siteId = cfg.analytics.siteId
componentResources.afterDOMLoaded.push(`
const tinylyticsScript = document.createElement('script');
tinylyticsScript.src = 'https://tinylytics.app/embed/${siteId}.js?spa';
tinylyticsScript.defer = true;
tinylyticsScript.onload = () => {
window.tinylytics.triggerUpdate();
document.addEventListener('nav', () => {
window.tinylytics.triggerUpdate();
});
};
document.head.appendChild(tinylyticsScript);
`)
} else if (cfg.analytics?.provider === "cabin") {
componentResources.afterDOMLoaded.push(`
const cabinScript = document.createElement("script")
cabinScript.src = "${cfg.analytics.host ?? "https://scripts.withcabin.com"}/hello.js"
cabinScript.defer = true
document.head.appendChild(cabinScript)
`)
} else if (cfg.analytics?.provider === "clarity") {
componentResources.afterDOMLoaded.push(`
const clarityScript = document.createElement("script")
clarityScript.innerHTML= \`(function(c,l,a,r,i,t,y){c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
t=l.createElement(r);t.defer=1;t.src="https://www.clarity.ms/tag/"+i;
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
})(window, document, "clarity", "script", "${cfg.analytics.projectId}");\`
document.head.appendChild(clarityScript)
`)
}
if (cfg.enableSPA) {
componentResources.afterDOMLoaded.push(spaRouterScript)
} else {
componentResources.afterDOMLoaded.push(`
window.spaNavigate = (url, _) => window.location.assign(url)
window.addCleanup = () => {}
const event = new CustomEvent("nav", { detail: { url: document.body.dataset.slug } })
document.dispatchEvent(event)
`)
}
}
// This emitter should not update the `resources` parameter. If it does, partial
// rebuilds may not work as expected.
export const ComponentResources: QuartzEmitterPlugin = () => {
return {
name: "ComponentResources",
async *emit(ctx, _content, _resources) {
const cfg = ctx.cfg.configuration
// component specific scripts and styles
const componentResources = getComponentResources(ctx)
let googleFontsStyleSheet = ""
if (cfg.theme.fontOrigin === "local") {
// let the user do it themselves in css
} else if (cfg.theme.fontOrigin === "googleFonts" && !cfg.theme.cdnCaching) {
// when cdnCaching is true, we link to google fonts in Head.tsx
const theme = ctx.cfg.configuration.theme
const response = await fetch(googleFontHref(theme))
googleFontsStyleSheet = await response.text()
if (theme.typography.title) {
const title = ctx.cfg.configuration.pageTitle
const response = await fetch(googleFontSubsetHref(theme, title))
googleFontsStyleSheet += `\n${await response.text()}`
}
if (!cfg.baseUrl) {
throw new Error(
"baseUrl must be defined when using Google Fonts without cfg.theme.cdnCaching",
)
}
const { processedStylesheet, fontFiles } = await processGoogleFonts(
googleFontsStyleSheet,
cfg.baseUrl,
)
googleFontsStyleSheet = processedStylesheet
// Download and save font files
for (const fontFile of fontFiles) {
const res = await fetch(fontFile.url)
if (!res.ok) {
throw new Error(`Failed to fetch font ${fontFile.filename}`)
}
const buf = await res.arrayBuffer()
yield write({
ctx,
slug: joinSegments("static", "fonts", fontFile.filename) as FullSlug,
ext: `.${fontFile.extension}`,
content: Buffer.from(buf),
})
}
}
// important that this goes *after* component scripts
// as the "nav" event gets triggered here and we should make sure
// that everyone else had the chance to register a listener for it
addGlobalPageResources(ctx, componentResources)
const stylesheet = joinStyles(
ctx.cfg.configuration.theme,
googleFontsStyleSheet,
...componentResources.css,
styles,
)
const [prescript, postscript] = await Promise.all([
joinScripts(componentResources.beforeDOMLoaded),
joinScripts(componentResources.afterDOMLoaded),
])
yield write({
ctx,
slug: "index" as FullSlug,
ext: ".css",
content: transform({
filename: "index.css",
code: Buffer.from(stylesheet),
minify: true,
targets: {
safari: (15 << 16) | (6 << 8), // 15.6
ios_saf: (15 << 16) | (6 << 8), // 15.6
edge: 115 << 16,
firefox: 102 << 16,
chrome: 109 << 16,
},
include: Features.MediaQueries,
}).code.toString(),
})
yield write({
ctx,
slug: "prescript" as FullSlug,
ext: ".js",
content: prescript,
})
yield write({
ctx,
slug: "postscript" as FullSlug,
ext: ".js",
content: postscript,
})
},
async *partialEmit() {},
}
}