On the surface it seems that only google and plausible scripts handle the SPA correctly - but I don't know if maybe others handle window.history API themselves somehow or something like that. However, I am trying out goatcounter and in it's docs I see that it does no special SPA handling, so this has to be fixed. Just doing the dynamic script thing on every nav seems to do the trick. The script is not "spa-preserve" so they wouldn't accumulate - and when I tried the "spa-preserve" + call goatcounter api route it didn't quite work, they actually did accumulate
292 lines
11 KiB
TypeScript
292 lines
11 KiB
TypeScript
import { FilePath, 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, joinStyles } from "../../util/theme"
|
|
import { Features, transform } from "lightningcss"
|
|
import { transform as transpile } from "esbuild"
|
|
import { write } from "./helpers"
|
|
import DepGraph from "../../depgraph"
|
|
|
|
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>(),
|
|
}
|
|
|
|
for (const component of allComponents) {
|
|
const { css, beforeDOMLoaded, afterDOMLoaded } = component
|
|
if (css) {
|
|
componentResources.css.add(css)
|
|
}
|
|
if (beforeDOMLoaded) {
|
|
componentResources.beforeDOMLoaded.add(beforeDOMLoaded)
|
|
}
|
|
if (afterDOMLoaded) {
|
|
componentResources.afterDOMLoaded.add(afterDOMLoaded)
|
|
}
|
|
}
|
|
|
|
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.async = true
|
|
document.head.appendChild(gtagScript)
|
|
|
|
window.dataLayer = window.dataLayer || [];
|
|
function gtag() { dataLayer.push(arguments); }
|
|
gtag("js", new Date());
|
|
gtag("config", "${tagId}", { send_page_view: false });
|
|
|
|
document.addEventListener("nav", () => {
|
|
gtag("event", "page_view", {
|
|
page_title: document.title,
|
|
page_location: location.href,
|
|
});
|
|
});`)
|
|
} 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
|
|
document.head.appendChild(plausibleScript)
|
|
|
|
window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }
|
|
|
|
document.addEventListener("nav", () => {
|
|
plausible("pageview")
|
|
})
|
|
`)
|
|
} 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.async = true
|
|
|
|
document.head.appendChild(umamiScript)
|
|
`)
|
|
} else if (cfg.analytics?.provider === "goatcounter") {
|
|
componentResources.afterDOMLoaded.push(`
|
|
document.addEventListener("nav", () => {
|
|
const goatcounterScript = document.createElement("script")
|
|
goatcounterScript.src = "${cfg.analytics.scriptSrc ?? "https://gc.zgo.at/count.js"}"
|
|
goatcounterScript.async = true
|
|
goatcounterScript.setAttribute("data-goatcounter",
|
|
"https://${cfg.analytics.websiteId}.${cfg.analytics.host ?? "goatcounter.com"}/count")
|
|
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"}'})\`
|
|
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"
|
|
tinylyticsScript.defer = true
|
|
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
|
|
cabinScript.async = 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.async=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",
|
|
getQuartzComponents() {
|
|
return []
|
|
},
|
|
async getDependencyGraph(_ctx, _content, _resources) {
|
|
return new DepGraph<FilePath>()
|
|
},
|
|
async emit(ctx, _content, _resources): Promise<FilePath[]> {
|
|
const promises: Promise<FilePath>[] = []
|
|
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
|
|
let match
|
|
|
|
const fontSourceRegex = /url\((https:\/\/fonts.gstatic.com\/s\/[^)]+\.(woff2|ttf))\)/g
|
|
|
|
googleFontsStyleSheet = await (
|
|
await fetch(googleFontHref(ctx.cfg.configuration.theme))
|
|
).text()
|
|
|
|
while ((match = fontSourceRegex.exec(googleFontsStyleSheet)) !== null) {
|
|
// match[0] is the `url(path)`, match[1] is the `path`
|
|
const url = match[1]
|
|
// the static name of this file.
|
|
const [filename, ext] = url.split("/").pop()!.split(".")
|
|
|
|
googleFontsStyleSheet = googleFontsStyleSheet.replace(
|
|
url,
|
|
`https://${cfg.baseUrl}/static/fonts/${filename}.ttf`,
|
|
)
|
|
|
|
promises.push(
|
|
fetch(url)
|
|
.then((res) => {
|
|
if (!res.ok) {
|
|
throw new Error(`Failed to fetch font`)
|
|
}
|
|
return res.arrayBuffer()
|
|
})
|
|
.then((buf) =>
|
|
write({
|
|
ctx,
|
|
slug: joinSegments("static", "fonts", filename) as FullSlug,
|
|
ext: `.${ext}`,
|
|
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),
|
|
])
|
|
|
|
promises.push(
|
|
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(),
|
|
}),
|
|
write({
|
|
ctx,
|
|
slug: "prescript" as FullSlug,
|
|
ext: ".js",
|
|
content: prescript,
|
|
}),
|
|
write({
|
|
ctx,
|
|
slug: "postscript" as FullSlug,
|
|
ext: ".js",
|
|
content: postscript,
|
|
}),
|
|
)
|
|
|
|
return await Promise.all(promises)
|
|
},
|
|
}
|
|
}
|