From e0ebee5aa9b3646de722f139f1d8d15591df538e Mon Sep 17 00:00:00 2001
From: Jacky Zhao <j.zhao2k19@gmail.com>
Date: Sun, 02 Jul 2023 20:08:29 +0000
Subject: [PATCH] various polish

---
 quartz/components/Footer.tsx                  |    2 
 quartz/components/Header.tsx                  |    1 
 quartz/components/PageList.tsx                |    5 
 quartz/cfg.ts                                 |   11 +
 quartz/components/Head.tsx                    |   19 -
 quartz/path.ts                                |   13 +
 quartz/components/styles/graph.scss           |    5 
 quartz/components/styles/listPage.scss        |   29 ++-
 quartz/components/scripts/plausible.inline.ts |    3 
 quartz/bootstrap-cli.mjs                      |    2 
 quartz/components/scripts/graph.inline.ts     |   17 +
 quartz/plugins/transformers/links.ts          |    5 
 quartz/plugins/index.ts                       |   72 +++-----
 quartz/components/pages/Content.tsx           |    4 
 quartz/components/scripts/darkmode.inline.ts  |    5 
 quartz/plugins/emitters/contentIndex.ts       |    1 
 quartz/components/renderPage.tsx              |   43 +++-
 quartz/processors/emit.ts                     |   78 ++++++++
 quartz.config.ts                              |   36 ++-
 quartz/components/ArticleTitle.tsx            |    5 
 quartz/styles/base.scss                       |   86 ++++++--
 quartz/resources.tsx                          |    8 
 quartz/components/styles/search.scss          |    3 
 quartz/plugins/transformers/gfm.ts            |    5 
 quartz/components/scripts/popover.inline.ts   |   32 +++
 quartz/components/TagList.tsx                 |   23 +-
 quartz/components/styles/backlinks.scss       |    6 
 quartz/components/types.ts                    |    2 
 quartz/components/styles/popover.scss         |    2 
 quartz/components/styles/footer.scss          |    2 
 30 files changed, 337 insertions(+), 188 deletions(-)

diff --git a/quartz.config.ts b/quartz.config.ts
index f58bc32..58c1d9c 100644
--- a/quartz.config.ts
+++ b/quartz.config.ts
@@ -4,12 +4,7 @@
 
 const sharedPageComponents = {
   head: Component.Head(),
-  header: [
-    Component.PageTitle(),
-    Component.Spacer(),
-    Component.Search(),
-    Component.Darkmode()
-  ],
+  header: [],
   footer: Component.Footer({
     authorName: "Jacky",
     links: {
@@ -25,11 +20,15 @@
     Component.ReadingTime(),
     Component.TagList(),
   ],
-  left: [],
+  left: [
+    Component.PageTitle(),
+    Component.Search(),
+    Component.TableOfContents(),
+    Component.Darkmode()
+  ],
   right: [
     Component.Graph(),
-    Component.TableOfContents(),
-    Component.Backlinks()
+    Component.Backlinks(),
   ],
 }
 
@@ -37,7 +36,11 @@
   beforeBody: [
     Component.ArticleTitle()
   ],
-  left: [],
+  left: [
+    Component.PageTitle(),
+    Component.Search(),
+    Component.Darkmode()
+  ],
   right: [],
 }
 
@@ -46,6 +49,9 @@
     pageTitle: "🪴 Quartz 4.0",
     enableSPA: true,
     enablePopovers: true,
+    analytics: {
+      provider: 'plausible',
+    },
     canonicalUrl: "quartz.jzhao.xyz",
     ignorePatterns: ["private", "templates"],
     theme: {
@@ -102,16 +108,16 @@
         ...contentPageLayout,
         pageBody: Component.Content(),
       }),
-      Plugin.TagPage({
-        ...sharedPageComponents,
-        ...listPageLayout,
-        pageBody: Component.TagContent(),
-      }),
       Plugin.FolderPage({
         ...sharedPageComponents,
         ...listPageLayout,
         pageBody: Component.FolderContent(),
       }),
+      Plugin.TagPage({
+        ...sharedPageComponents,
+        ...listPageLayout,
+        pageBody: Component.TagContent(),
+      }),
       Plugin.ContentIndex({
         enableSiteMap: true,
         enableRSS: true,
diff --git a/quartz/bootstrap-cli.mjs b/quartz/bootstrap-cli.mjs
index 2824f8e..3f71b17 100755
--- a/quartz/bootstrap-cli.mjs
+++ b/quartz/bootstrap-cli.mjs
@@ -64,7 +64,7 @@
       packages: "external",
       plugins: [
         sassPlugin({
-          type: 'css-text'
+          type: 'css-text',
         }),
         {
           name: 'inline-script-loader',
diff --git a/quartz/cfg.ts b/quartz/cfg.ts
index 1c9ece8..49698ab 100644
--- a/quartz/cfg.ts
+++ b/quartz/cfg.ts
@@ -2,12 +2,23 @@
 import { PluginTypes } from "./plugins/types"
 import { Theme } from "./theme"
 
+export type Analytics = null
+  | {
+    provider: 'plausible'
+  }
+  | {
+    provider: 'google',
+    tagId: string
+  }
+
 export interface GlobalConfiguration {
   pageTitle: string,
   /** Whether to enable single-page-app style rendering. this prevents flashes of unstyled content and improves smoothness of Quartz */
   enableSPA: boolean,
   /** Whether to display Wikipedia-style popovers when hovering over links */
   enablePopovers: boolean,
+  /** Analytics mode */
+  analytics: Analytics 
   /** Glob patterns to not search */
   ignorePatterns: string[],
   /** Base URL to use for CNAME files, sitemaps, and RSS feeds that require an absolute URL.
diff --git a/quartz/components/ArticleTitle.tsx b/quartz/components/ArticleTitle.tsx
index c25769e..b8d58c6 100644
--- a/quartz/components/ArticleTitle.tsx
+++ b/quartz/components/ArticleTitle.tsx
@@ -2,9 +2,8 @@
 
 function ArticleTitle({ fileData }: QuartzComponentProps) {
   const title = fileData.frontmatter?.title
-  const displayTitle = fileData.slug === "index" ? undefined : title
-  if (displayTitle) {
-    return <h1 class="article-title">{displayTitle}</h1>
+  if (title) {
+    return <h1 class="article-title">{title}</h1>
   } else {
     return null
   }
diff --git a/quartz/components/Footer.tsx b/quartz/components/Footer.tsx
index 4229f9d..5fc6d64 100644
--- a/quartz/components/Footer.tsx
+++ b/quartz/components/Footer.tsx
@@ -14,7 +14,7 @@
     return <>
       <hr />
       <footer>
-        <p>Made by {name} using <a>Quartz</a>, © {year}</p>
+        <p>Made by {name} using <a href="https://quartz.jzhao.xyz/">Quartz</a>, © {year}</p>
         <ul>{Object.entries(links).map(([text, link]) => <li>
           <a href={link}>{text}</a>
         </li>)}</ul>
diff --git a/quartz/components/Head.tsx b/quartz/components/Head.tsx
index a0b62b7..f8439a0 100644
--- a/quartz/components/Head.tsx
+++ b/quartz/components/Head.tsx
@@ -2,15 +2,7 @@
 import { JSResourceToScriptElement } from "../resources"
 import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
 
-interface Options {
-  prefetchContentIndex: boolean
-}
-
-const defaultOptions: Options = {
-  prefetchContentIndex: true
-}
-
-export default ((opts?: Options) => {
+export default (() => {
   function Head({ fileData, externalResources }: QuartzComponentProps) {
     const slug = fileData.slug!
     const title = fileData.frontmatter?.title ?? "Untitled"
@@ -20,10 +12,6 @@
     const iconPath = baseDir + "/static/icon.png"
     const ogImagePath = baseDir + "/static/og-image.png"
 
-    const prefetchContentIndex = opts?.prefetchContentIndex ?? defaultOptions.prefetchContentIndex
-    const contentIndexPath = baseDir + "/static/contentIndex.json"
-    const contentIndexScript = `const fetchData = fetch(\`${contentIndexPath}\`).then(data => data.json())`
-
     return <head>
       <title>{title}</title>
       <meta charSet="utf-8" />
@@ -36,9 +24,8 @@
       <link rel="icon" href={iconPath} />
       <meta name="description" content={description} />
       <meta name="generator" content="Quartz" />
-      <link rel="preconnect" href="https://fonts.googleapis.com" />
-      <link rel="preconnect" href="https://fonts.gstatic.com" />
-      {prefetchContentIndex && <script spa-preserve>{contentIndexScript}</script>}
+      <link rel="preconnect" href="https://fonts.googleapis.com"/>
+      <link rel="preconnect" href="https://fonts.gstatic.com"/>
       {css.map(href => <link key={href} href={href} rel="stylesheet" type="text/css" spa-preserve />)}
       {js.filter(resource => resource.loadTime === "beforeDOMReady").map(res => JSResourceToScriptElement(res, true))}
     </head>
diff --git a/quartz/components/Header.tsx b/quartz/components/Header.tsx
index 06ae88b..0f13ca2 100644
--- a/quartz/components/Header.tsx
+++ b/quartz/components/Header.tsx
@@ -12,6 +12,7 @@
   flex-direction: row;
   align-items: center;
   margin: 2em 0;
+  gap: 1.5rem;
 }
 
 header h1 {
diff --git a/quartz/components/PageList.tsx b/quartz/components/PageList.tsx
index e5d8dfb..3c39bee 100644
--- a/quartz/components/PageList.tsx
+++ b/quartz/components/PageList.tsx
@@ -23,7 +23,7 @@
 
 export function PageList({ fileData, allFiles }: QuartzComponentProps) {
   const slug = fileData.slug!
-  return <ul class="section-ul">
+  return <ul class="section-ul popover-hint">
     {allFiles.sort(byDateAndAlphabetical).map(page => {
       const title = page.frontmatter?.title
       const pageSlug = page.slug!
@@ -36,9 +36,8 @@
           <div class="desc">
             <h3><a href={stripIndex(relativeToRoot(slug, pageSlug))} class="internal">{title}</a></h3>
           </div>
-          <div class="spacer"></div>
           <ul class="tags">
-            {tags.map(tag => <li><a href={relativeToRoot(slug, `tags/${tag}`)}>#{tag}</a></li>)}
+            {tags.map(tag => <li><a class="internal" href={relativeToRoot(slug, `tags/${tag}`)}>#{tag}</a></li>)}
           </ul>
         </div>
       </li>
diff --git a/quartz/components/TagList.tsx b/quartz/components/TagList.tsx
index 65286a5..366889b 100644
--- a/quartz/components/TagList.tsx
+++ b/quartz/components/TagList.tsx
@@ -11,7 +11,7 @@
       const display = `#${tag}`
       const linkDest = baseDir + `/tags/${slugAnchor(tag)}`
       return <li>
-        <a href={linkDest}>{display}</a>
+        <a href={linkDest} class="internal">{display}</a>
       </li>
     })}</ul>
   } else {
@@ -25,17 +25,18 @@
   display: flex;
   padding-left: 0;
   gap: 0.4rem;
+}
+  
+.tags > li {
+  display: inline-block;
+  margin: 0;
+  overflow-wrap: normal;
+}
 
-  & > li {
-    display: inline-block;
-    margin: 0;
-
-    & > a {
-      border-radius: 8px;
-      border: var(--lightgray) 1px solid;
-      padding: 0.2rem 0.5rem;
-    }
-  }
+.tags > li > a {
+  border-radius: 8px;
+  background-color: var(--highlight);
+  padding: 0.2rem 0.5rem;
 }
 `
 
diff --git a/quartz/components/pages/Content.tsx b/quartz/components/pages/Content.tsx
index 7856d6e..d233845 100644
--- a/quartz/components/pages/Content.tsx
+++ b/quartz/components/pages/Content.tsx
@@ -5,7 +5,7 @@
 function Content({ tree }: QuartzComponentProps) {
   // @ts-ignore (preact makes it angry)
   const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' })
-  return <article>{content}</article>
+  return <article class="popover-hint">{content}</article>
 }
 
-export default (() => Content) satisfies QuartzComponentConstructor
\ No newline at end of file
+export default (() => Content) satisfies QuartzComponentConstructor
diff --git a/quartz/components/renderPage.tsx b/quartz/components/renderPage.tsx
index 0e0f4c0..c70f092 100644
--- a/quartz/components/renderPage.tsx
+++ b/quartz/components/renderPage.tsx
@@ -17,10 +17,15 @@
 
 export function pageResources(slug: string, staticResources: StaticResources): StaticResources {
   const baseDir = resolveToRoot(slug)
+
+  const contentIndexPath = baseDir + "/static/contentIndex.json"
+  const contentIndexScript = `const fetchData = fetch(\`${contentIndexPath}\`).then(data => data.json())`
+
   return {
     css: [baseDir + "/index.css", ...staticResources.css],
     js: [
       { src: baseDir + "/prescript.js", loadTime: "beforeDOMReady", contentType: "external" },
+      { loadTime: "afterDOMReady", contentType: "inline", spaPreserve: true, script: contentIndexScript },
       ...staticResources.js,
       { src: baseDir + "/postscript.js", loadTime: "afterDOMReady", moduleType: 'module', contentType: "external" }
     ]
@@ -32,28 +37,40 @@
   const Header = HeaderConstructor()
   const Body = BodyConstructor()
 
+  const LeftComponent =
+    <div class="left">
+      <div class="left-inner">
+        {left.map(BodyComponent => <BodyComponent {...componentData} />)}
+      </div>
+    </div>
+
+  const RightComponent =
+    <div class="right">
+      <div class="right-inner">
+        {right.map(BodyComponent => <BodyComponent {...componentData} />)}
+      </div>
+    </div>
+
   const doc = <html>
     <Head {...componentData} />
     <body data-slug={slug}>
       <div id="quartz-root" class="page">
-        <Header {...componentData} >
-          {header.map(HeaderComponent => <HeaderComponent {...componentData} />)}
-        </Header>
-        <div class="popover-hint">
-          {beforeBody.map(BodyComponent => <BodyComponent {...componentData} />)}
+        <div class="page-header">
+          <Header {...componentData} >
+            {header.map(HeaderComponent => <HeaderComponent {...componentData} />)}
+          </Header>
+          <div class="popover-hint">
+            {beforeBody.map(BodyComponent => <BodyComponent {...componentData} />)}
+          </div>
         </div>
         <Body {...componentData}>
-          <div class="left">
-            {left.map(BodyComponent => <BodyComponent {...componentData} />)}
-          </div>
-          <div class="center popover-hint">
+          {LeftComponent}
+          <div class="center">
             <Content {...componentData} />
+            <Footer {...componentData} />
           </div>
-          <div class="right">
-            {right.map(BodyComponent => <BodyComponent {...componentData} />)}
-          </div>
+          {RightComponent}
         </Body>
-        <Footer {...componentData} />
       </div>
     </body>
     {pageResources.js.filter(resource => resource.loadTime === "afterDOMReady").map(res => JSResourceToScriptElement(res))}
diff --git a/quartz/components/scripts/darkmode.inline.ts b/quartz/components/scripts/darkmode.inline.ts
index f00a873..594bd3a 100644
--- a/quartz/components/scripts/darkmode.inline.ts
+++ b/quartz/components/scripts/darkmode.inline.ts
@@ -2,7 +2,7 @@
 const currentTheme = localStorage.getItem('theme') ?? userPref
 document.documentElement.setAttribute('saved-theme', currentTheme)
 
-window.addEventListener('DOMContentLoaded', () => {
+document.addEventListener("nav", () => {
   const switchTheme = (e: any) => {
     if (e.target.checked) {
       document.documentElement.setAttribute('saved-theme', 'dark')
@@ -16,7 +16,8 @@
 
   // Darkmode toggle
   const toggleSwitch = document.querySelector('#darkmode-toggle') as HTMLInputElement
-  toggleSwitch.addEventListener('change', switchTheme, false)
+  toggleSwitch.removeEventListener('change', switchTheme)
+  toggleSwitch.addEventListener('change', switchTheme)
   if (currentTheme === 'dark') {
     toggleSwitch.checked = true
   }
diff --git a/quartz/components/scripts/graph.inline.ts b/quartz/components/scripts/graph.inline.ts
index 27e9a81..169b8c4 100644
--- a/quartz/components/scripts/graph.inline.ts
+++ b/quartz/components/scripts/graph.inline.ts
@@ -266,9 +266,9 @@
   })
 }
 
-function renderGlobalGraph() {
+async function renderGlobalGraph() {
   const slug = document.body.dataset["slug"]!
-  renderGraph("global-graph-container", slug)
+  await renderGraph("global-graph-container", slug)
   const container = document.getElementById("global-graph-outer")
   container?.classList.add("active")
 
@@ -293,7 +293,14 @@
   containerIcon?.addEventListener("click", renderGlobalGraph)
 })
 
-window.addEventListener('resize', async () => {
-  const slug = document.body.dataset["slug"]!
-  await renderGraph("graph-container", slug)
+let resizeEventDebounce: number | undefined = undefined
+window.addEventListener('resize', () => {
+  if (resizeEventDebounce) {
+    clearTimeout(resizeEventDebounce)
+  }
+
+  resizeEventDebounce = window.setTimeout(async () => {
+    const slug = document.body.dataset["slug"]!
+    await renderGraph("graph-container", slug)
+  }, 50)
 })
diff --git a/quartz/components/scripts/plausible.inline.ts b/quartz/components/scripts/plausible.inline.ts
new file mode 100644
index 0000000..60817c2
--- /dev/null
+++ b/quartz/components/scripts/plausible.inline.ts
@@ -0,0 +1,3 @@
+import Plausible from 'plausible-tracker'
+const { trackPageview } = Plausible()
+document.addEventListener("nav", () => trackPageview())
diff --git a/quartz/components/scripts/popover.inline.ts b/quartz/components/scripts/popover.inline.ts
index 655831d..b388995 100644
--- a/quartz/components/scripts/popover.inline.ts
+++ b/quartz/components/scripts/popover.inline.ts
@@ -1,5 +1,24 @@
 import { computePosition, flip, inline, shift } from "@floating-ui/dom"
 
+// from micromorph/src/utils.ts
+// https://github.com/natemoo-re/micromorph/blob/main/src/utils.ts#L5
+export function normalizeRelativeURLs(
+  el: Element | Document,
+  base: string | URL
+) {
+  const update = (el: Element, attr: string, base: string | URL) => {
+    el.setAttribute(attr, new URL(el.getAttribute(attr)!, base).pathname)
+  }
+
+  el.querySelectorAll('[href^="./"], [href^="../"]').forEach((item) =>
+    update(item, 'href', base)
+  )
+
+  el.querySelectorAll('[src^="./"], [src^="../"]').forEach((item) =>
+    update(item, 'src', base)
+  )
+}
+
 document.addEventListener("nav", () => {
   const links = [...document.getElementsByClassName("internal")] as HTMLLinkElement[]
   const p = new DOMParser()
@@ -41,6 +60,7 @@
 
       if (!contents) return
       const html = p.parseFromString(contents, "text/html")
+      normalizeRelativeURLs(html, targetUrl)
       const elts = [...html.getElementsByClassName("popover-hint")]
       if (elts.length === 0) return
 
@@ -54,11 +74,13 @@
       setPosition(popoverElement)
       link.appendChild(popoverElement)
       link.dataset.fetchedPopover = "true"
-      
-      const heading = popoverInner.querySelector(hash) as HTMLElement | null
-      if (heading) {
-        // leave ~12px of buffer when scrolling to a heading
-        popoverInner.scroll({ top: heading.offsetTop - 12, behavior: 'instant' })
+
+      if (hash !== "") {
+        const heading = popoverInner.querySelector(hash) as HTMLElement | null
+        if (heading) {
+          // leave ~12px of buffer when scrolling to a heading
+          popoverInner.scroll({ top: heading.offsetTop - 12, behavior: 'instant' })
+        }
       }
     })
   }
diff --git a/quartz/components/styles/backlinks.scss b/quartz/components/styles/backlinks.scss
index 3344a7b..80baefc 100644
--- a/quartz/components/styles/backlinks.scss
+++ b/quartz/components/styles/backlinks.scss
@@ -7,13 +7,9 @@
   & > ul {
     list-style: none;
     padding: 0;
-    margin: 0;
+    margin: 0.5rem 0;
 
     & > li {
-      margin: 0.5rem 0;
-      padding: 0.25rem 1rem;
-      border: var(--lightgray) 1px solid;
-      border-radius: 5px;
       & > a {
         background-color: transparent;
       }
diff --git a/quartz/components/styles/footer.scss b/quartz/components/styles/footer.scss
index d104e50..16df545 100644
--- a/quartz/components/styles/footer.scss
+++ b/quartz/components/styles/footer.scss
@@ -1,6 +1,8 @@
 footer {
   text-align: left;
   opacity: 0.8;
+  margin-bottom: 4rem;
+
   & ul {
     list-style: none;
     margin: 0;
diff --git a/quartz/components/styles/graph.scss b/quartz/components/styles/graph.scss
index 244f2e4..4533a84 100644
--- a/quartz/components/styles/graph.scss
+++ b/quartz/components/styles/graph.scss
@@ -11,6 +11,7 @@
     height: 250px;
     margin: 0.5em 0;
     position: relative;
+    overflow: hidden;
 
     & > #global-graph-icon {
       color: var(--dark);
@@ -30,10 +31,6 @@
         background-color: var(--lightgray);
       }
     }
-
-    & > #graph-container > svg {
-      margin-bottom: -5px;
-    }
   }
 
   & > #global-graph-outer {
diff --git a/quartz/components/styles/listPage.scss b/quartz/components/styles/listPage.scss
index a5d0a91..1823815 100644
--- a/quartz/components/styles/listPage.scss
+++ b/quartz/components/styles/listPage.scss
@@ -8,29 +8,36 @@
   margin-bottom: 1em;
 
   & > .section {
-    display: flex;
-    align-items: center;
+    display: grid;
+    grid-template-columns: 6em 3fr 1fr;
 
     @media all and (max-width: 600px) {
-      & .tags {
+      & > .tags {
         display: none;
       }
     }
 
-    & h3 > a {
-      font-weight: 700;
-      margin: 0;
-      background-color: transparent;
+    & > .tags {
+      justify-self: end;
+      margin-left: 1rem;
     }
 
-    & p {
+    & > .desc a {
+      background-color: transparent; 
+    }
+
+    & > .meta {
       margin: 0;
-      padding-right: 1em;
       flex-basis: 6em;
+      opacity: 0.6;
     }
   }
+}
 
-  & .meta {
-    opacity: 0.6;
+// modifications in popover context
+.popover .section {
+  grid-template-columns: 6em 1fr !important;
+  & > .tags {
+    display: none;
   }
 }
diff --git a/quartz/components/styles/popover.scss b/quartz/components/styles/popover.scss
index 5ae09fe..80bdfad 100644
--- a/quartz/components/styles/popover.scss
+++ b/quartz/components/styles/popover.scss
@@ -24,7 +24,7 @@
     height: 20rem;
     padding: 0 1rem 1rem 1rem;
     font-weight: initial;
-    line-height: initial;
+    line-height: normal;
     font-size: initial;
     font-family: var(--bodyFont);
     border: 1px solid var(--gray);
diff --git a/quartz/components/styles/search.scss b/quartz/components/styles/search.scss
index 32d5744..cbf982a 100644
--- a/quartz/components/styles/search.scss
+++ b/quartz/components/styles/search.scss
@@ -1,8 +1,7 @@
 .search {
   min-width: 5rem;
-  max-width: 12rem;
+  max-width: 14rem;
   flex-grow: 0.3;
-  margin: 0 1.5rem;
 
   & > #search-icon {
     background-color: var(--lightgray);
diff --git a/quartz/components/types.ts b/quartz/components/types.ts
index c7584b6..d1c153d 100644
--- a/quartz/components/types.ts
+++ b/quartz/components/types.ts
@@ -8,7 +8,7 @@
   externalResources: StaticResources
   fileData: QuartzPluginData
   cfg: GlobalConfiguration
-  children: QuartzComponent[] | JSX.Element[]
+  children: (QuartzComponent | JSX.Element)[]
   tree: Node<QuartzPluginData>
   allFiles: QuartzPluginData[]
 }
diff --git a/quartz/path.ts b/quartz/path.ts
index 4755687..81cdb3a 100644
--- a/quartz/path.ts
+++ b/quartz/path.ts
@@ -5,7 +5,17 @@
   return s.replace(/\s/g, '-')
 }
 
+// on the client, 'index' isn't ever rendered so we should clean it up
+export function clientSideSlug(fp: string): string {
+  if (fp.endsWith("index")) {
+    fp = fp.slice(0, -"index".length)
+  }
+
+  return fp
+}
+
 export function trimPathSuffix(fp: string): string {
+  fp = clientSideSlug(fp)
   let [cleanPath, anchor] = fp.split("#", 2)
   anchor = anchor === undefined ? "" : "#" + anchor
 
@@ -27,9 +37,6 @@
 // resolve /a/b/c to ../../
 export function resolveToRoot(slug: string): string {
   let fp = trimPathSuffix(slug)
-  if (fp.endsWith("index")) {
-    fp = fp.slice(0, -"index".length)
-  }
 
   if (fp === "") {
     return "."
diff --git a/quartz/plugins/emitters/contentIndex.ts b/quartz/plugins/emitters/contentIndex.ts
index f75334a..cf42756 100644
--- a/quartz/plugins/emitters/contentIndex.ts
+++ b/quartz/plugins/emitters/contentIndex.ts
@@ -36,7 +36,6 @@
   const base = cfg.canonicalUrl ?? ""
   const root = `https://${base}`
 
-  // TODO: ogimage
   const createURLEntry = (slug: string, content: ContentDetails): string => `<items>
     <title>${content.title}</title>
     <link>${root}/${slug}</link>
diff --git a/quartz/plugins/index.ts b/quartz/plugins/index.ts
index 8815811..c55e4dd 100644
--- a/quartz/plugins/index.ts
+++ b/quartz/plugins/index.ts
@@ -1,29 +1,17 @@
 import { GlobalConfiguration } from '../cfg'
 import { QuartzComponent } from '../components/types'
 import { StaticResources } from '../resources'
-import { googleFontHref, joinStyles } from '../theme'
+import { joinStyles } from '../theme'
 import { EmitCallback, PluginTypes } from './types'
 import styles from '../styles/base.scss'
 
-// @ts-ignore
-import spaRouterScript from '../components/scripts/spa.inline'
-// @ts-ignore
-import popoverScript from '../components/scripts/popover.inline'
-import popoverStyle from '../components/styles/popover.scss'
-
 export type ComponentResources = {
   css: string[],
   beforeDOMLoaded: string[],
   afterDOMLoaded: string[]
 }
 
-function joinScripts(scripts: string[]): string {
-  // wrap with iife to prevent scope collision
-  return scripts.map(script => `(function () {${script}})();`).join("\n")
-}
-
-export function emitComponentResources(cfg: GlobalConfiguration, resources: StaticResources, plugins: PluginTypes, emit: EmitCallback) {
-  const fps: string[] = []
+export function getComponentResources(plugins: PluginTypes): ComponentResources {
   const allComponents: Set<QuartzComponent> = new Set()
   for (const emitter of plugins.emitters) {
     const components = emitter.getQuartzComponents()
@@ -50,41 +38,35 @@
       componentResources.afterDOMLoaded.push(afterDOMLoaded)
     }
   }
-  
-  if (cfg.enablePopovers) {
-    componentResources.afterDOMLoaded.push(popoverScript)
-    componentResources.css.push(popoverStyle)
-  }
 
-  if (cfg.enableSPA) {
-    componentResources.afterDOMLoaded.push(spaRouterScript)
-  } else {
-    componentResources.afterDOMLoaded.push(`
-      window.spaNavigate = (url, _) => window.location.assign(url)
-      const event = new CustomEvent("nav", { detail: { slug: document.body.dataset.slug } })
-      document.dispatchEvent(event)`
-    )
-  }
+  return componentResources
+}
 
-  emit({
-    slug: "index",
-    ext: ".css",
-    content: joinStyles(cfg.theme, styles, ...componentResources.css)
-  })
-  emit({
-    slug: "prescript",
-    ext: ".js",
-    content: joinScripts(componentResources.beforeDOMLoaded)
-  })
-  emit({
-    slug: "postscript",
-    ext: ".js",
-    content: joinScripts(componentResources.afterDOMLoaded)
-  })
+function joinScripts(scripts: string[]): string {
+  // wrap with iife to prevent scope collision
+  return scripts.map(script => `(function () {${script}})();`).join("\n")
+}
 
-  fps.push("index.css", "prescript.js", "postscript.js")
-  resources.css.push(googleFontHref(cfg.theme))
+export async function emitComponentResources(cfg: GlobalConfiguration, res: ComponentResources, emit: EmitCallback): Promise<string[]> {
+  const fps = await Promise.all([
+    emit({
+      slug: "index",
+      ext: ".css",
+      content: joinStyles(cfg.theme, styles, ...res.css)
+    }),
+    emit({
+      slug: "prescript",
+      ext: ".js",
+      content: joinScripts(res.beforeDOMLoaded)
+    }),
+    emit({
+      slug: "postscript",
+      ext: ".js",
+      content: joinScripts(res.afterDOMLoaded)
+    })
+  ])
   return fps
+
 }
 
 export function getStaticResourcesFromPlugins(plugins: PluginTypes) {
diff --git a/quartz/plugins/transformers/gfm.ts b/quartz/plugins/transformers/gfm.ts
index 54f8ca6..f966e58 100644
--- a/quartz/plugins/transformers/gfm.ts
+++ b/quartz/plugins/transformers/gfm.ts
@@ -1,4 +1,3 @@
-import { PluggableList } from "unified"
 import remarkGfm from "remark-gfm"
 import smartypants from 'remark-smartypants'
 import { QuartzTransformerPlugin } from "../types"
@@ -20,14 +19,14 @@
   return {
     name: "GitHubFlavoredMarkdown",
     markdownPlugins() {
-      return opts.enableSmartyPants ? [remarkGfm] : [remarkGfm, smartypants]
+      return opts.enableSmartyPants ? [remarkGfm, smartypants] : [remarkGfm]
     },
     htmlPlugins() {
       if (opts.linkHeadings) {
         return [rehypeSlug, [rehypeAutolinkHeadings, {
           behavior: 'append', content: {
             type: 'text',
-            value: ' §'
+            value: ' §',
           }
         }]]
       } else {
diff --git a/quartz/plugins/transformers/links.ts b/quartz/plugins/transformers/links.ts
index 1391452..b8a800a 100644
--- a/quartz/plugins/transformers/links.ts
+++ b/quartz/plugins/transformers/links.ts
@@ -1,5 +1,5 @@
 import { QuartzTransformerPlugin } from "../types"
-import { relativeToRoot, slugify, trimPathSuffix } from "../../path"
+import { clientSideSlug, relativeToRoot, slugify, trimPathSuffix } from "../../path"
 import path from "path"
 import { visit } from 'unist-util-visit'
 import isAbsoluteUrl from "is-absolute-url"
@@ -27,7 +27,7 @@
     htmlPlugins() {
       return [() => {
         return (tree, file) => {
-          const curSlug = file.data.slug!
+          const curSlug = clientSideSlug(file.data.slug!)
           const transformLink = (target: string) => {
             const targetSlug = slugify(decodeURI(target).trim())
             if (opts.markdownLinkResolution === 'relative' && !path.isAbsolute(targetSlug)) {
@@ -49,7 +49,6 @@
               let dest = node.properties.href
               node.properties.className = isAbsoluteUrl(dest) ? "external" : "internal"
 
-
               // don't process external links or intra-document anchors
               if (!(isAbsoluteUrl(dest) || dest.startsWith("#"))) {
                 node.properties.href = transformLink(dest)
diff --git a/quartz/processors/emit.ts b/quartz/processors/emit.ts
index e1438fa..7150f5e 100644
--- a/quartz/processors/emit.ts
+++ b/quartz/processors/emit.ts
@@ -1,13 +1,69 @@
 import path from "path"
 import fs from "fs"
-import { QuartzConfig } from "../cfg"
+import { GlobalConfiguration, QuartzConfig } from "../cfg"
 import { PerfTimer } from "../perf"
-import { emitComponentResources, getStaticResourcesFromPlugins } from "../plugins"
+import { ComponentResources, emitComponentResources, getComponentResources, getStaticResourcesFromPlugins } from "../plugins"
 import { EmitCallback } from "../plugins/types"
 import { ProcessedContent } from "../plugins/vfile"
 import { QUARTZ, slugify } from "../path"
 import { globbyStream } from "globby"
 import chalk from "chalk"
+import { googleFontHref } from '../theme'
+
+// @ts-ignore
+import spaRouterScript from '../components/scripts/spa.inline'
+// @ts-ignore
+import plausibleScript from '../components/scripts/plausible.inline'
+// @ts-ignore
+import popoverScript from '../components/scripts/popover.inline'
+import popoverStyle from '../components/styles/popover.scss'
+import { StaticResources } from "../resources"
+
+function addGlobalPageResources(cfg: GlobalConfiguration, staticResources: StaticResources, componentResources: ComponentResources) {
+  // font and other resources
+  staticResources.css.push(googleFontHref(cfg.theme))
+
+  // popovers
+  if (cfg.enablePopovers) {
+    componentResources.afterDOMLoaded.push(popoverScript)
+    componentResources.css.push(popoverStyle)
+  }
+
+  if (cfg.analytics?.provider === "google") {
+    const tagId = cfg.analytics.tagId
+    staticResources.js.push({
+      src: `https://www.googletagmanager.com/gtag/js?id=${tagId}`,
+      contentType: 'external',
+      loadTime: 'afterDOMReady',
+    })
+    componentResources.afterDOMLoaded.push(`
+    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") {
+    componentResources.afterDOMLoaded.push(plausibleScript)
+  }
+
+  // spa
+  if (cfg.enableSPA) {
+    componentResources.afterDOMLoaded.push(spaRouterScript)
+  } else {
+    componentResources.afterDOMLoaded.push(`
+      window.spaNavigate = (url, _) => window.location.assign(url)
+      const event = new CustomEvent("nav", { detail: { slug: document.body.dataset.slug } })
+      document.dispatchEvent(event)`
+    )
+  }
+}
 
 export async function emitContent(contentFolder: string, output: string, cfg: QuartzConfig, content: ProcessedContent[], verbose: boolean) {
   const perf = new PerfTimer()
@@ -19,9 +75,25 @@
     return pathToPage
   }
 
+  // initialize from plugins
   const staticResources = getStaticResourcesFromPlugins(cfg.plugins)
-  emitComponentResources(cfg.configuration, staticResources, cfg.plugins, emit)
 
+  // component specific scripts and styles
+  const componentResources = getComponentResources(cfg.plugins)
+  // 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(cfg.configuration, staticResources, componentResources)
+
+  // emit in one go
+  const emittedResources = await emitComponentResources(cfg.configuration, componentResources, emit)
+  if (verbose) {
+    for (const file of emittedResources) {
+      console.log(`[emit:Resources] ${file}`)
+    }
+  }
+
+  // emitter plugins
   let emittedFiles = 0
   for (const emitter of cfg.plugins.emitters) {
     try {
diff --git a/quartz/resources.tsx b/quartz/resources.tsx
index 78ae10b..3780751 100644
--- a/quartz/resources.tsx
+++ b/quartz/resources.tsx
@@ -3,7 +3,8 @@
 
 export type JSResource = {
   loadTime: 'beforeDOMReady' | 'afterDOMReady'
-  moduleType?: 'module'
+  moduleType?: 'module',
+  spaPreserve?: boolean
 } & ({
   src: string
   contentType: 'external'
@@ -14,11 +15,12 @@
 
 export function JSResourceToScriptElement(resource: JSResource, preserve?: boolean): JSX.Element {
   const scriptType = resource.moduleType ?? 'application/javascript'
+  const spaPreserve = preserve ?? resource.spaPreserve
   if (resource.contentType === 'external') {
-    return <script key={resource.src} src={resource.src} type={scriptType} spa-preserve={preserve} />
+    return <script key={resource.src} src={resource.src} type={scriptType} spa-preserve={spaPreserve} />
   } else {
     const content = resource.script
-    return <script key={randomUUID()} type={scriptType} spa-preserve={preserve}>{content}</script>
+    return <script key={randomUUID()} type={scriptType} spa-preserve={spaPreserve}>{content}</script>
   }
 }
 
diff --git a/quartz/styles/base.scss b/quartz/styles/base.scss
index 45e6370..db0299b 100644
--- a/quartz/styles/base.scss
+++ b/quartz/styles/base.scss
@@ -11,6 +11,9 @@
   box-sizing: border-box;
   background-color: var(--light);
   font-family: var(--bodyFont);
+  --pageWidth: 800px;
+  --sidePanelWidth: 400px;
+  --topSpacing: 6rem;
 }
 
 .text-highlight {
@@ -27,7 +30,7 @@
 a {
   font-weight: 600;
   text-decoration: none;
-  transition: all 0.2s ease;
+  transition: color 0.2s ease;
   color: var(--secondary);
 
   &:hover {
@@ -43,34 +46,48 @@
 }
 
 .page {
-  margin: 6rem 35vw 6rem 20vw;
-  max-width: 1000px;
-  position: relative;
+  & > .page-header {
+    max-width: var(--pageWidth);
+    margin: var(--topSpacing) auto 0 auto;
+  }
 
-  & .left, & .right {
-    position: fixed;
-    height: 100vh;
-    overflow-y: scroll;
-    box-sizing: border-box;
+  & > #quartz-body {
+    width: 100%;
     display: flex;
-    flex-direction: column;
-    top: 0;
-    gap: 2rem;
-    padding: 6rem;
-  }
-  
-  & .left {
-    left: 0;
-    padding-left: 10vw;
-    width: 20vw;
-  }
 
-  & .right {
-    right: 0;
-    padding-right: 10vw;
-    width: 35vw;
-  }
+    & .left, & .right {
+      flex: 1;
+      width: calc(calc(100vw - var(--pageWidth)) / 2);
+    }
 
+    & .left-inner, & .right-inner {
+      display: flex;
+      flex-direction: column;
+      gap: 2rem;
+      top: 0;
+      width: var(--sidePanelWidth);
+      margin-top: calc(var(--topSpacing));
+      box-sizing: border-box;
+      padding: 0 4rem;
+      position: fixed;
+    }
+
+    & .left-inner {
+      left: calc(calc(100vw - var(--pageWidth)) / 2 - var(--sidePanelWidth));
+    }
+
+    & .right-inner {
+      right: calc(calc(100vw - var(--pageWidth)) / 2 - var(--sidePanelWidth));
+    }
+
+    & .center {
+      width: var(--pageWidth);
+      margin: 0 auto;
+    }
+  }
+}
+
+.page {
   @media all and (max-width: 1200px) {
     margin: 25px 5vw;
     & .left, & .right {
@@ -89,9 +106,26 @@
     & > h1 {
       font-size: 2rem;
     }
+
+    // darkmode diagrams
+    & svg {
+      stroke: var(--dark);
+    }
+
+    & ul:has(input[type='checkbox']) {
+      list-style-type: none;
+      padding-left: 0;
+    }
   }
 }
 
+input[type="checkbox"] {
+  transform: translateY(2px);
+  color: var(--secondary);
+  border-color: var(--lightgray);
+  background-color: var(--light);
+}
+
 blockquote {
   margin: 1rem 0;
   border-left: 3px solid var(--secondary);
@@ -120,7 +154,7 @@
 }
 
 h1, h2, h3, h4, h5, h6 {
-  &[id] > a {
+  &[id] > a[href^="#"] {
     margin: 0 0.5rem;
     opacity: 0;
     transition: opacity 0.2s ease;

--
Gitblit v1.10.0