From c874e7e9378a5ba895870e9680484fb4af5c6e93 Mon Sep 17 00:00:00 2001
From: Jacky Zhao <j.zhao2k19@gmail.com>
Date: Sat, 19 Aug 2023 15:52:25 -0700
Subject: [PATCH] base path refactor to better support subpath hosting

---
 content/advanced/paths.md                     |  32 ++--
 content/hosting.md                            |   2 +-
 index.d.ts                                    |   2 +-
 package.json                                  |   2 +-
 quartz/components/Backlinks.tsx               |   6 +-
 quartz/components/Head.tsx                    |   5 +-
 quartz/components/PageList.tsx                |   8 +-
 quartz/components/PageTitle.tsx               |   5 +-
 quartz/components/TagList.tsx                 |   5 +-
 quartz/components/pages/FolderContent.tsx     |   6 +-
 quartz/components/pages/TagContent.tsx        |   4 +-
 quartz/components/renderPage.tsx              |  19 +-
 quartz/components/scripts/graph.inline.ts     |  34 ++--
 quartz/components/scripts/search.inline.ts    |  10 +-
 quartz/components/scripts/spa.inline.ts       |   8 +-
 quartz/plugins/emitters/aliases.ts            |  17 +-
 quartz/plugins/emitters/componentResources.ts |   8 +-
 quartz/plugins/emitters/contentIndex.ts       |  30 ++-
 quartz/plugins/emitters/contentPage.tsx       |   6 +-
 quartz/plugins/emitters/folderPage.tsx        |  20 +-
 quartz/plugins/emitters/tagPage.tsx           |  12 +-
 quartz/plugins/index.ts                       |   4 +-
 quartz/plugins/transformers/links.ts          |  26 ++-
 quartz/plugins/transformers/ofm.ts            |   5 +-
 quartz/plugins/types.ts                       |   4 +-
 quartz/util/ctx.ts                            |   4 +-
 quartz/util/path.test.ts                      | 179 +++++++-----------
 quartz/util/path.ts                           | 179 ++++++------------
 quartz/worker.ts                              |   4 +-
 29 files changed, 257 insertions(+), 389 deletions(-)

diff --git a/content/advanced/paths.md b/content/advanced/paths.md
index 68fc181..9455b98 100644
--- a/content/advanced/paths.md
+++ b/content/advanced/paths.md
@@ -4,7 +4,7 @@ title: Paths in Quartz
 
 Paths are pretty complex to reason about because, especially for a static site generator, they can come from so many places.
 
-The current browser URL? Technically a path. A full file path to a piece of content? Also a path. What about a slug for a piece of content? Yet another path.
+A full file path to a piece of content? Also a path. What about a slug for a piece of content? Yet another path.
 
 It would be silly to type these all as `string` and call it a day as it's pretty common to accidentally mistake one type of path for another. Unfortunately, TypeScript does not have [nominal types](https://en.wikipedia.org/wiki/Nominal_type_system) for type aliases meaning even if you made custom types of a server-side slug or a client-slug slug, you can still accidentally assign one to another and TypeScript wouldn't catch it.
 
@@ -12,13 +12,13 @@ Luckily, we can mimic nominal typing using [brands](https://www.typescriptlang.o
 
 ```typescript
 // instead of
-type ClientSlug = string
+type FullSlug = string
 
 // we do
-type ClientSlug = string & { __brand: "client" }
+type FullSlug = string & { __brand: "full" }
 
 // that way, the following will fail typechecking
-const slug: ClientSlug = "some random slug"
+const slug: FullSlug = "some random string"
 ```
 
 While this prevents most typing mistakes _within_ our nominal typing system (e.g. mistaking a server slug for a client slug), it doesn't prevent us from _accidentally_ mistaking a string for a client slug when we forcibly cast it.
@@ -29,27 +29,23 @@ The following diagram draws the relationships between all the path sources, nomi
 
 ```mermaid
 graph LR
-    Browser{{Browser}} --> Window{{Window}} & LinkElement{{Link Element}}
-    Window --"getCanonicalSlug()"--> Canonical[Canonical Slug]
-    Window --"getClientSlug()"--> Client[Client Slug]
+    Browser{{Browser}} --> Window{{Body}} & LinkElement{{Link Element}}
+    Window --"getFullSlug()"--> FullSlug[Full Slug]
     LinkElement --".href"--> Relative[Relative URL]
-    Client --"canonicalizeClient()"--> Canonical
-    Canonical --"pathToRoot()"--> Relative
-    Canonical --"resolveRelative()" --> Relative
+    FullSlug --"simplifySlug()" --> SimpleSlug[Simple Slug]
+    SimpleSlug --"pathToRoot()"--> Relative
+    SimpleSlug --"resolveRelative()" --> Relative
     MD{{Markdown File}} --> FilePath{{File Path}} & Links[Markdown links]
     Links --"transformLink()"--> Relative
-    FilePath --"slugifyFilePath()"--> Server[Server Slug]
-    Server --> HTML["HTML File"]
-    Server --"canonicalizeServer()"--> Canonical
-    style Canonical stroke-width:4px
+    FilePath --"slugifyFilePath()"--> FullSlug[Full Slug]
+    style FullSlug stroke-width:4px
 ```
 
 Here are the main types of slugs with a rough description of each type of path:
 
-- `ClientSlug`: client-side slug, usually obtained through `window.location`. Contains the protocol (i.e. starts with `https://`)
-- `CanonicalSlug`: should be used whenever you need to refer to the location of a file/note. Shouldn't be a relative path and shouldn't have leading or trailing slashes `/` either. Also shouldn't have `/index` as an ending or a file extension.
-- `RelativeURL`: must start with `.` or `..` to indicate it's a relative URL. Shouldn't have `/index` as an ending or a file extension.
-- `ServerSlug`: cannot be relative and may not have leading or trailing slashes.
 - `FilePath`: a real file path to a file on disk. Cannot be relative and must have a file extension.
+- `FullSlug`: cannot be relative and may not have leading or trailing slashes. It can have `index` as it's last segment. Use this wherever possible is it's the most 'general' interpretation of a slug.
+- `SimpleSlug`: cannot be relative and shouldn't have `/index` as an ending or a file extension. It _can_ however have a trailing slash to indicate a folder path.
+- `RelativeURL`: must start with `.` or `..` to indicate it's a relative URL. Shouldn't have `/index` as an ending or a file extension but can contain a trailing slash.
 
 To get a clearer picture of how these relate to each other, take a look at the path tests in `quartz/path.test.ts`.
diff --git a/content/hosting.md b/content/hosting.md
index b247627..d6ccd0b 100644
--- a/content/hosting.md
+++ b/content/hosting.md
@@ -17,7 +17,7 @@ However, if you'd like to publish your site to the world, you need a way to host
 
 | Configuration option   | Value              |
 | ---------------------- | ------------------ |
-| Production branch      | `v4`         |
+| Production branch      | `v4`               |
 | Framework preset       | `None`             |
 | Build command          | `npx quartz build` |
 | Build output directory | `public`           |
diff --git a/index.d.ts b/index.d.ts
index 4a93f16..aec536d 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -5,7 +5,7 @@ declare module "*.scss" {
 
 // dom custom event
 interface CustomEventMap {
-  nav: CustomEvent<{ url: CanonicalSlug }>
+  nav: CustomEvent<{ url: FullSlug }>
 }
 
 declare const fetchData: Promise<ContentIndex>
diff --git a/package.json b/package.json
index 17e46b0..08ab2d6 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
   "name": "@jackyzha0/quartz",
   "description": "🌱 publish your digital garden and notes as a website",
   "private": true,
-  "version": "4.0.7",
+  "version": "4.0.8",
   "type": "module",
   "author": "jackyzha0 <j.zhao2k19@gmail.com>",
   "license": "MIT",
diff --git a/quartz/components/Backlinks.tsx b/quartz/components/Backlinks.tsx
index 8cf3afa..e88966b 100644
--- a/quartz/components/Backlinks.tsx
+++ b/quartz/components/Backlinks.tsx
@@ -1,9 +1,9 @@
 import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
 import style from "./styles/backlinks.scss"
-import { canonicalizeServer, resolveRelative } from "../util/path"
+import { resolveRelative, simplifySlug } from "../util/path"
 
 function Backlinks({ fileData, allFiles }: QuartzComponentProps) {
-  const slug = canonicalizeServer(fileData.slug!)
+  const slug = simplifySlug(fileData.slug!)
   const backlinkFiles = allFiles.filter((file) => file.links?.includes(slug))
   return (
     <div class="backlinks">
@@ -12,7 +12,7 @@ function Backlinks({ fileData, allFiles }: QuartzComponentProps) {
         {backlinkFiles.length > 0 ? (
           backlinkFiles.map((f) => (
             <li>
-              <a href={resolveRelative(slug, canonicalizeServer(f.slug!))} class="internal">
+              <a href={resolveRelative(fileData.slug!, f.slug!)} class="internal">
                 {f.frontmatter?.title}
               </a>
             </li>
diff --git a/quartz/components/Head.tsx b/quartz/components/Head.tsx
index bfeb3f1..5221c29 100644
--- a/quartz/components/Head.tsx
+++ b/quartz/components/Head.tsx
@@ -1,14 +1,13 @@
-import { canonicalizeServer, pathToRoot } from "../util/path"
+import { pathToRoot } from "../util/path"
 import { JSResourceToScriptElement } from "../util/resources"
 import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
 
 export default (() => {
   function Head({ cfg, fileData, externalResources }: QuartzComponentProps) {
-    const slug = canonicalizeServer(fileData.slug!)
     const title = fileData.frontmatter?.title ?? "Untitled"
     const description = fileData.description?.trim() ?? "No description provided"
     const { css, js } = externalResources
-    const baseDir = pathToRoot(slug)
+    const baseDir = pathToRoot(fileData.slug!)
     const iconPath = baseDir + "/static/icon.png"
     const ogImagePath = `https://${cfg.baseUrl}/static/og-image.png`
 
diff --git a/quartz/components/PageList.tsx b/quartz/components/PageList.tsx
index 83b1b07..63513f5 100644
--- a/quartz/components/PageList.tsx
+++ b/quartz/components/PageList.tsx
@@ -1,4 +1,4 @@
-import { CanonicalSlug, canonicalizeServer, resolveRelative } from "../util/path"
+import { FullSlug, resolveRelative } from "../util/path"
 import { QuartzPluginData } from "../plugins/vfile"
 import { Date } from "./Date"
 import { QuartzComponentProps } from "./types"
@@ -25,7 +25,6 @@ type Props = {
 } & QuartzComponentProps
 
 export function PageList({ fileData, allFiles, limit }: Props) {
-  const slug = canonicalizeServer(fileData.slug!)
   let list = allFiles.sort(byDateAndAlphabetical)
   if (limit) {
     list = list.slice(0, limit)
@@ -35,7 +34,6 @@ export function PageList({ fileData, allFiles, limit }: Props) {
     <ul class="section-ul">
       {list.map((page) => {
         const title = page.frontmatter?.title
-        const pageSlug = canonicalizeServer(page.slug!)
         const tags = page.frontmatter?.tags ?? []
 
         return (
@@ -48,7 +46,7 @@ export function PageList({ fileData, allFiles, limit }: Props) {
               )}
               <div class="desc">
                 <h3>
-                  <a href={resolveRelative(slug, pageSlug)} class="internal">
+                  <a href={resolveRelative(fileData.slug!, page.slug!)} class="internal">
                     {title}
                   </a>
                 </h3>
@@ -58,7 +56,7 @@ export function PageList({ fileData, allFiles, limit }: Props) {
                   <li>
                     <a
                       class="internal tag-link"
-                      href={resolveRelative(slug, `tags/${tag}` as CanonicalSlug)}
+                      href={resolveRelative(fileData.slug!, `tags/${tag}/index` as FullSlug)}
                     >
                       #{tag}
                     </a>
diff --git a/quartz/components/PageTitle.tsx b/quartz/components/PageTitle.tsx
index c1e74f3..c327547 100644
--- a/quartz/components/PageTitle.tsx
+++ b/quartz/components/PageTitle.tsx
@@ -1,10 +1,9 @@
-import { canonicalizeServer, pathToRoot } from "../util/path"
+import { pathToRoot } from "../util/path"
 import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
 
 function PageTitle({ fileData, cfg }: QuartzComponentProps) {
   const title = cfg?.pageTitle ?? "Untitled Quartz"
-  const slug = canonicalizeServer(fileData.slug!)
-  const baseDir = pathToRoot(slug)
+  const baseDir = pathToRoot(fileData.slug!)
   return (
     <h1 class="page-title">
       <a href={baseDir}>{title}</a>
diff --git a/quartz/components/TagList.tsx b/quartz/components/TagList.tsx
index bf5badd..639f68c 100644
--- a/quartz/components/TagList.tsx
+++ b/quartz/components/TagList.tsx
@@ -1,10 +1,9 @@
-import { canonicalizeServer, pathToRoot, slugTag } from "../util/path"
+import { pathToRoot, slugTag } from "../util/path"
 import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
 
 function TagList({ fileData }: QuartzComponentProps) {
   const tags = fileData.frontmatter?.tags
-  const slug = canonicalizeServer(fileData.slug!)
-  const baseDir = pathToRoot(slug)
+  const baseDir = pathToRoot(fileData.slug!)
   if (tags && tags.length > 0) {
     return (
       <ul class="tags">
diff --git a/quartz/components/pages/FolderContent.tsx b/quartz/components/pages/FolderContent.tsx
index 6037930..478eaa7 100644
--- a/quartz/components/pages/FolderContent.tsx
+++ b/quartz/components/pages/FolderContent.tsx
@@ -5,13 +5,13 @@ import path from "path"
 
 import style from "../styles/listPage.scss"
 import { PageList } from "../PageList"
-import { canonicalizeServer } from "../../util/path"
+import { simplifySlug } from "../../util/path"
 
 function FolderContent(props: QuartzComponentProps) {
   const { tree, fileData, allFiles } = props
-  const folderSlug = canonicalizeServer(fileData.slug!)
+  const folderSlug = simplifySlug(fileData.slug!)
   const allPagesInFolder = allFiles.filter((file) => {
-    const fileSlug = canonicalizeServer(file.slug!)
+    const fileSlug = simplifySlug(file.slug!)
     const prefixed = fileSlug.startsWith(folderSlug) && fileSlug !== folderSlug
     const folderParts = folderSlug.split(path.posix.sep)
     const fileParts = fileSlug.split(path.posix.sep)
diff --git a/quartz/components/pages/TagContent.tsx b/quartz/components/pages/TagContent.tsx
index 2cae40d..7b3a582 100644
--- a/quartz/components/pages/TagContent.tsx
+++ b/quartz/components/pages/TagContent.tsx
@@ -3,7 +3,7 @@ import { Fragment, jsx, jsxs } from "preact/jsx-runtime"
 import { toJsxRuntime } from "hast-util-to-jsx-runtime"
 import style from "../styles/listPage.scss"
 import { PageList } from "../PageList"
-import { ServerSlug, canonicalizeServer, getAllSegmentPrefixes } from "../../util/path"
+import { FullSlug, getAllSegmentPrefixes, simplifySlug } from "../../util/path"
 import { QuartzPluginData } from "../../plugins/vfile"
 
 const numPages = 10
@@ -15,7 +15,7 @@ function TagContent(props: QuartzComponentProps) {
     throw new Error(`Component "TagContent" tried to render a non-tag page: ${slug}`)
   }
 
-  const tag = canonicalizeServer(slug.slice("tags/".length) as ServerSlug)
+  const tag = simplifySlug(slug.slice("tags/".length) as FullSlug)
   const allPagesWithTag = (tag: string) =>
     allFiles.filter((file) =>
       (file.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes).includes(tag),
diff --git a/quartz/components/renderPage.tsx b/quartz/components/renderPage.tsx
index 171c414..a1d5fee 100644
--- a/quartz/components/renderPage.tsx
+++ b/quartz/components/renderPage.tsx
@@ -3,7 +3,7 @@ import { QuartzComponent, QuartzComponentProps } from "./types"
 import HeaderConstructor from "./Header"
 import BodyConstructor from "./Body"
 import { JSResourceToScriptElement, StaticResources } from "../util/resources"
-import { CanonicalSlug, pathToRoot } from "../util/path"
+import { FullSlug, joinSegments, pathToRoot } from "../util/path"
 
 interface RenderComponents {
   head: QuartzComponent
@@ -15,19 +15,20 @@ interface RenderComponents {
   footer: QuartzComponent
 }
 
-export function pageResources(
-  slug: CanonicalSlug,
-  staticResources: StaticResources,
-): StaticResources {
+export function pageResources(slug: FullSlug, staticResources: StaticResources): StaticResources {
   const baseDir = pathToRoot(slug)
 
-  const contentIndexPath = baseDir + "/static/contentIndex.json"
+  const contentIndexPath = joinSegments(baseDir, "static/contentIndex.json")
   const contentIndexScript = `const fetchData = fetch(\`${contentIndexPath}\`).then(data => data.json())`
 
   return {
-    css: [baseDir + "/index.css", ...staticResources.css],
+    css: [joinSegments(baseDir, "index.css"), ...staticResources.css],
     js: [
-      { src: baseDir + "/prescript.js", loadTime: "beforeDOMReady", contentType: "external" },
+      {
+        src: joinSegments(baseDir, "/prescript.js"),
+        loadTime: "beforeDOMReady",
+        contentType: "external",
+      },
       {
         loadTime: "beforeDOMReady",
         contentType: "inline",
@@ -46,7 +47,7 @@ export function pageResources(
 }
 
 export function renderPage(
-  slug: CanonicalSlug,
+  slug: FullSlug,
   componentData: QuartzComponentProps,
   components: RenderComponents,
   pageResources: StaticResources,
diff --git a/quartz/components/scripts/graph.inline.ts b/quartz/components/scripts/graph.inline.ts
index bea32eb..e84480e 100644
--- a/quartz/components/scripts/graph.inline.ts
+++ b/quartz/components/scripts/graph.inline.ts
@@ -1,31 +1,32 @@
 import type { ContentDetails } from "../../plugins/emitters/contentIndex"
 import * as d3 from "d3"
 import { registerEscapeHandler, removeAllChildren } from "./util"
-import { CanonicalSlug, getCanonicalSlug, getClientSlug, resolveRelative } from "../../util/path"
+import { FullSlug, SimpleSlug, getFullSlug, resolveRelative, simplifySlug } from "../../util/path"
 
 type NodeData = {
-  id: CanonicalSlug
+  id: SimpleSlug
   text: string
   tags: string[]
 } & d3.SimulationNodeDatum
 
 type LinkData = {
-  source: CanonicalSlug
-  target: CanonicalSlug
+  source: SimpleSlug
+  target: SimpleSlug
 }
 
 const localStorageKey = "graph-visited"
-function getVisited(): Set<CanonicalSlug> {
+function getVisited(): Set<SimpleSlug> {
   return new Set(JSON.parse(localStorage.getItem(localStorageKey) ?? "[]"))
 }
 
-function addToVisited(slug: CanonicalSlug) {
+function addToVisited(slug: SimpleSlug) {
   const visited = getVisited()
   visited.add(slug)
   localStorage.setItem(localStorageKey, JSON.stringify([...visited]))
 }
 
-async function renderGraph(container: string, slug: CanonicalSlug) {
+async function renderGraph(container: string, fullSlug: FullSlug) {
+  const slug = simplifySlug(fullSlug)
   const visited = getVisited()
   const graph = document.getElementById(container)
   if (!graph) return
@@ -47,16 +48,17 @@ async function renderGraph(container: string, slug: CanonicalSlug) {
 
   const links: LinkData[] = []
   for (const [src, details] of Object.entries<ContentDetails>(data)) {
+    const source = simplifySlug(src as FullSlug)
     const outgoing = details.links ?? []
     for (const dest of outgoing) {
-      if (src in data && dest in data) {
-        links.push({ source: src as CanonicalSlug, target: dest })
+      if (dest in data) {
+        links.push({ source, target: dest })
       }
     }
   }
 
-  const neighbourhood = new Set<CanonicalSlug>()
-  const wl: (CanonicalSlug | "__SENTINEL")[] = [slug, "__SENTINEL"]
+  const neighbourhood = new Set<SimpleSlug>()
+  const wl: (SimpleSlug | "__SENTINEL")[] = [slug, "__SENTINEL"]
   if (depth >= 0) {
     while (depth >= 0 && wl.length > 0) {
       // compute neighbours
@@ -72,7 +74,7 @@ async function renderGraph(container: string, slug: CanonicalSlug) {
       }
     }
   } else {
-    Object.keys(data).forEach((id) => neighbourhood.add(id as CanonicalSlug))
+    Object.keys(data).forEach((id) => neighbourhood.add(simplifySlug(id as FullSlug)))
   }
 
   const graphData: { nodes: NodeData[]; links: LinkData[] } = {
@@ -171,11 +173,11 @@ async function renderGraph(container: string, slug: CanonicalSlug) {
     .attr("fill", color)
     .style("cursor", "pointer")
     .on("click", (_, d) => {
-      const targ = resolveRelative(slug, d.id)
-      window.spaNavigate(new URL(targ, getClientSlug(window)))
+      const targ = resolveRelative(fullSlug, d.id)
+      window.spaNavigate(new URL(targ, window.location.toString()))
     })
     .on("mouseover", function (_, d) {
-      const neighbours: CanonicalSlug[] = data[slug].links ?? []
+      const neighbours: SimpleSlug[] = data[slug].links ?? []
       const neighbourNodes = d3
         .selectAll<HTMLElement, NodeData>(".node")
         .filter((d) => neighbours.includes(d.id))
@@ -271,7 +273,7 @@ async function renderGraph(container: string, slug: CanonicalSlug) {
 }
 
 function renderGlobalGraph() {
-  const slug = getCanonicalSlug(window)
+  const slug = getFullSlug(window)
   const container = document.getElementById("global-graph-outer")
   const sidebar = container?.closest(".sidebar") as HTMLElement
   container?.classList.add("active")
diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts
index b94cdfb..038566c 100644
--- a/quartz/components/scripts/search.inline.ts
+++ b/quartz/components/scripts/search.inline.ts
@@ -1,11 +1,11 @@
 import { Document } from "flexsearch"
 import { ContentDetails } from "../../plugins/emitters/contentIndex"
 import { registerEscapeHandler, removeAllChildren } from "./util"
-import { CanonicalSlug, getClientSlug, resolveRelative } from "../../util/path"
+import { FullSlug, getFullSlug, resolveRelative, simplifySlug } from "../../util/path"
 
 interface Item {
   id: number
-  slug: CanonicalSlug
+  slug: FullSlug
   title: string
   content: string
 }
@@ -73,7 +73,7 @@ document.addEventListener("nav", async (e: unknown) => {
   const searchIcon = document.getElementById("search-icon")
   const searchBar = document.getElementById("search-bar") as HTMLInputElement | null
   const results = document.getElementById("results-container")
-  const idDataMap = Object.keys(data) as CanonicalSlug[]
+  const idDataMap = Object.keys(data) as FullSlug[]
 
   function hideSearch() {
     container?.classList.remove("active")
@@ -126,7 +126,7 @@ document.addEventListener("nav", async (e: unknown) => {
     button.innerHTML = `<h3>${title}</h3><p>${content}</p>`
     button.addEventListener("click", () => {
       const targ = resolveRelative(currentSlug, slug)
-      window.spaNavigate(new URL(targ, getClientSlug(window)))
+      window.spaNavigate(new URL(targ, window.location.toString()))
     })
     return button
   }
@@ -192,7 +192,7 @@ document.addEventListener("nav", async (e: unknown) => {
     for (const [slug, fileData] of Object.entries<ContentDetails>(data)) {
       await index.addAsync(id, {
         id,
-        slug: slug as CanonicalSlug,
+        slug: slug as FullSlug,
         title: fileData.title,
         content: fileData.content,
       })
diff --git a/quartz/components/scripts/spa.inline.ts b/quartz/components/scripts/spa.inline.ts
index e0a8373..0a57a9d 100644
--- a/quartz/components/scripts/spa.inline.ts
+++ b/quartz/components/scripts/spa.inline.ts
@@ -1,5 +1,5 @@
 import micromorph from "micromorph"
-import { CanonicalSlug, RelativeURL, getCanonicalSlug } from "../../util/path"
+import { FullSlug, RelativeURL, getFullSlug } from "../../util/path"
 
 // adapted from `micromorph`
 // https://github.com/natemoo-re/micromorph
@@ -31,7 +31,7 @@ const getOpts = ({ target }: Event): { url: URL; scroll?: boolean } | undefined
   return { url: new URL(href), scroll: "routerNoscroll" in a.dataset ? false : undefined }
 }
 
-function notifyNav(url: CanonicalSlug) {
+function notifyNav(url: FullSlug) {
   const event: CustomEventMap["nav"] = new CustomEvent("nav", { detail: { url } })
   document.dispatchEvent(event)
 }
@@ -81,7 +81,7 @@ async function navigate(url: URL, isBack: boolean = false) {
   const elementsToAdd = html.head.querySelectorAll(":not([spa-preserve])")
   elementsToAdd.forEach((el) => document.head.appendChild(el))
 
-  notifyNav(getCanonicalSlug(window))
+  notifyNav(getFullSlug(window))
   delete announcer.dataset.persist
 }
 
@@ -129,7 +129,7 @@ function createRouter() {
 }
 
 createRouter()
-notifyNav(getCanonicalSlug(window))
+notifyNav(getFullSlug(window))
 
 if (!customElements.get("route-announcer")) {
   const attrs = {
diff --git a/quartz/plugins/emitters/aliases.ts b/quartz/plugins/emitters/aliases.ts
index 1fbea76..62879f7 100644
--- a/quartz/plugins/emitters/aliases.ts
+++ b/quartz/plugins/emitters/aliases.ts
@@ -1,10 +1,4 @@
-import {
-  CanonicalSlug,
-  FilePath,
-  ServerSlug,
-  canonicalizeServer,
-  resolveRelative,
-} from "../../util/path"
+import { FilePath, FullSlug, resolveRelative, simplifySlug } from "../../util/path"
 import { QuartzEmitterPlugin } from "../types"
 import path from "path"
 
@@ -17,10 +11,10 @@ export const AliasRedirects: QuartzEmitterPlugin = () => ({
     const fps: FilePath[] = []
 
     for (const [_tree, file] of content) {
-      const ogSlug = canonicalizeServer(file.data.slug!)
+      const ogSlug = simplifySlug(file.data.slug!)
       const dir = path.posix.relative(argv.directory, file.dirname ?? argv.directory)
 
-      let aliases: CanonicalSlug[] = []
+      let aliases: FullSlug[] = []
       if (file.data.frontmatter?.aliases) {
         aliases = file.data.frontmatter?.aliases
       } else if (file.data.frontmatter?.alias) {
@@ -28,9 +22,8 @@ export const AliasRedirects: QuartzEmitterPlugin = () => ({
       }
 
       for (const alias of aliases) {
-        const slug = path.posix.join(dir, alias) as ServerSlug
-
-        const redirUrl = resolveRelative(canonicalizeServer(slug), ogSlug)
+        const slug = path.posix.join(dir, alias) as FullSlug
+        const redirUrl = resolveRelative(slug, file.data.slug!)
         const fp = await emit({
           content: `
             <!DOCTYPE html>
diff --git a/quartz/plugins/emitters/componentResources.ts b/quartz/plugins/emitters/componentResources.ts
index 19eddc6..e0bc83d 100644
--- a/quartz/plugins/emitters/componentResources.ts
+++ b/quartz/plugins/emitters/componentResources.ts
@@ -1,4 +1,4 @@
-import { FilePath, ServerSlug } from "../../util/path"
+import { FilePath, FullSlug } from "../../util/path"
 import { QuartzEmitterPlugin } from "../types"
 
 // @ts-ignore
@@ -154,7 +154,7 @@ export const ComponentResources: QuartzEmitterPlugin<Options> = (opts?: Partial<
       const postscript = joinScripts(componentResources.afterDOMLoaded)
       const fps = await Promise.all([
         emit({
-          slug: "index" as ServerSlug,
+          slug: "index" as FullSlug,
           ext: ".css",
           content: transform({
             filename: "index.css",
@@ -171,12 +171,12 @@ export const ComponentResources: QuartzEmitterPlugin<Options> = (opts?: Partial<
           }).code.toString(),
         }),
         emit({
-          slug: "prescript" as ServerSlug,
+          slug: "prescript" as FullSlug,
           ext: ".js",
           content: prescript,
         }),
         emit({
-          slug: "postscript" as ServerSlug,
+          slug: "postscript" as FullSlug,
           ext: ".js",
           content: postscript,
         }),
diff --git a/quartz/plugins/emitters/contentIndex.ts b/quartz/plugins/emitters/contentIndex.ts
index 85cdfe7..f4bf6db 100644
--- a/quartz/plugins/emitters/contentIndex.ts
+++ b/quartz/plugins/emitters/contentIndex.ts
@@ -1,18 +1,12 @@
 import { GlobalConfiguration } from "../../cfg"
-import {
-  CanonicalSlug,
-  ClientSlug,
-  FilePath,
-  ServerSlug,
-  canonicalizeServer,
-} from "../../util/path"
+import { FilePath, FullSlug, SimpleSlug, simplifySlug } from "../../util/path"
 import { QuartzEmitterPlugin } from "../types"
 import path from "path"
 
-export type ContentIndex = Map<CanonicalSlug, ContentDetails>
+export type ContentIndex = Map<FullSlug, ContentDetails>
 export type ContentDetails = {
   title: string
-  links: CanonicalSlug[]
+  links: SimpleSlug[]
   tags: string[]
   content: string
   date?: Date
@@ -33,21 +27,21 @@ const defaultOptions: Options = {
 
 function generateSiteMap(cfg: GlobalConfiguration, idx: ContentIndex): string {
   const base = cfg.baseUrl ?? ""
-  const createURLEntry = (slug: CanonicalSlug, content: ContentDetails): string => `<url>
+  const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => `<url>
     <loc>https://${base}/${slug}</loc>
     <lastmod>${content.date?.toISOString()}</lastmod>
   </url>`
   const urls = Array.from(idx)
-    .map(([slug, content]) => createURLEntry(slug, content))
+    .map(([slug, content]) => createURLEntry(simplifySlug(slug), content))
     .join("")
   return `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">${urls}</urlset>`
 }
 
 function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex): string {
   const base = cfg.baseUrl ?? ""
-  const root = `https://${base}` as ClientSlug
+  const root = `https://${base}`
 
-  const createURLEntry = (slug: CanonicalSlug, content: ContentDetails): string => `<items>
+  const createURLEntry = (slug: SimpleSlug, content: ContentDetails): string => `<items>
     <title>${content.title}</title>
     <link>${root}/${slug}</link>
     <guid>${root}/${slug}</guid>
@@ -56,7 +50,7 @@ function generateRSSFeed(cfg: GlobalConfiguration, idx: ContentIndex): string {
   </items>`
 
   const items = Array.from(idx)
-    .map(([slug, content]) => createURLEntry(slug, content))
+    .map(([slug, content]) => createURLEntry(simplifySlug(slug), content))
     .join("")
   return `<rss xmlns:atom="http://www.w3.org/2005/atom" version="2.0">
     <channel>
@@ -79,7 +73,7 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
       const emitted: FilePath[] = []
       const linkIndex: ContentIndex = new Map()
       for (const [_tree, file] of content) {
-        const slug = canonicalizeServer(file.data.slug!)
+        const slug = file.data.slug!
         const date = file.data.dates?.modified ?? new Date()
         if (opts?.includeEmptyFiles || (file.data.text && file.data.text !== "")) {
           linkIndex.set(slug, {
@@ -97,7 +91,7 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
         emitted.push(
           await emit({
             content: generateSiteMap(cfg, linkIndex),
-            slug: "sitemap" as ServerSlug,
+            slug: "sitemap" as FullSlug,
             ext: ".xml",
           }),
         )
@@ -107,13 +101,13 @@ export const ContentIndex: QuartzEmitterPlugin<Partial<Options>> = (opts) => {
         emitted.push(
           await emit({
             content: generateRSSFeed(cfg, linkIndex),
-            slug: "index" as ServerSlug,
+            slug: "index" as FullSlug,
             ext: ".xml",
           }),
         )
       }
 
-      const fp = path.join("static", "contentIndex") as ServerSlug
+      const fp = path.join("static", "contentIndex") as FullSlug
       const simplifiedIndex = Object.fromEntries(
         Array.from(linkIndex).map(([slug, content]) => {
           // remove description and from content index as nothing downstream
diff --git a/quartz/plugins/emitters/contentPage.tsx b/quartz/plugins/emitters/contentPage.tsx
index b7e347a..0e510db 100644
--- a/quartz/plugins/emitters/contentPage.tsx
+++ b/quartz/plugins/emitters/contentPage.tsx
@@ -4,7 +4,7 @@ import HeaderConstructor from "../../components/Header"
 import BodyConstructor from "../../components/Body"
 import { pageResources, renderPage } from "../../components/renderPage"
 import { FullPageLayout } from "../../cfg"
-import { FilePath, canonicalizeServer } from "../../util/path"
+import { FilePath } from "../../util/path"
 import { defaultContentPageLayout, sharedPageComponents } from "../../../quartz.layout"
 import { Content } from "../../components"
 
@@ -30,7 +30,7 @@ export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOp
       const fps: FilePath[] = []
       const allFiles = content.map((c) => c[1].data)
       for (const [tree, file] of content) {
-        const slug = canonicalizeServer(file.data.slug!)
+        const slug = file.data.slug!
         const externalResources = pageResources(slug, resources)
         const componentData: QuartzComponentProps = {
           fileData: file.data,
@@ -44,7 +44,7 @@ export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOp
         const content = renderPage(slug, componentData, opts, externalResources)
         const fp = await emit({
           content,
-          slug: file.data.slug!,
+          slug,
           ext: ".html",
         })
 
diff --git a/quartz/plugins/emitters/folderPage.tsx b/quartz/plugins/emitters/folderPage.tsx
index 724717a..4d22556 100644
--- a/quartz/plugins/emitters/folderPage.tsx
+++ b/quartz/plugins/emitters/folderPage.tsx
@@ -6,13 +6,7 @@ import { pageResources, renderPage } from "../../components/renderPage"
 import { ProcessedContent, defaultProcessedContent } from "../vfile"
 import { FullPageLayout } from "../../cfg"
 import path from "path"
-import {
-  CanonicalSlug,
-  FilePath,
-  ServerSlug,
-  canonicalizeServer,
-  joinSegments,
-} from "../../util/path"
+import { FilePath, FullSlug, SimpleSlug, joinSegments, simplifySlug } from "../../util/path"
 import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout"
 import { FolderContent } from "../../components"
 
@@ -38,10 +32,10 @@ export const FolderPage: QuartzEmitterPlugin<FullPageLayout> = (userOpts) => {
       const allFiles = content.map((c) => c[1].data)
       const cfg = ctx.cfg.configuration
 
-      const folders: Set<CanonicalSlug> = new Set(
+      const folders: Set<SimpleSlug> = new Set(
         allFiles.flatMap((data) => {
           const slug = data.slug
-          const folderName = path.dirname(slug ?? "") as CanonicalSlug
+          const folderName = path.dirname(slug ?? "") as SimpleSlug
           if (slug && folderName !== "." && folderName !== "tags") {
             return [folderName]
           }
@@ -53,21 +47,21 @@ export const FolderPage: QuartzEmitterPlugin<FullPageLayout> = (userOpts) => {
         [...folders].map((folder) => [
           folder,
           defaultProcessedContent({
-            slug: joinSegments(folder, "index") as ServerSlug,
+            slug: joinSegments(folder, "index") as FullSlug,
             frontmatter: { title: `Folder: ${folder}`, tags: [] },
           }),
         ]),
       )
 
       for (const [tree, file] of content) {
-        const slug = canonicalizeServer(file.data.slug!)
+        const slug = simplifySlug(file.data.slug!)
         if (folders.has(slug)) {
           folderDescriptions[slug] = [tree, file]
         }
       }
 
       for (const folder of folders) {
-        const slug = folder
+        const slug = joinSegments(folder, "index") as FullSlug
         const externalResources = pageResources(slug, resources)
         const [tree, file] = folderDescriptions[folder]
         const componentData: QuartzComponentProps = {
@@ -82,7 +76,7 @@ export const FolderPage: QuartzEmitterPlugin<FullPageLayout> = (userOpts) => {
         const content = renderPage(slug, componentData, opts, externalResources)
         const fp = await emit({
           content,
-          slug: file.data.slug!,
+          slug,
           ext: ".html",
         })
 
diff --git a/quartz/plugins/emitters/tagPage.tsx b/quartz/plugins/emitters/tagPage.tsx
index de07623..54ad934 100644
--- a/quartz/plugins/emitters/tagPage.tsx
+++ b/quartz/plugins/emitters/tagPage.tsx
@@ -5,13 +5,7 @@ import BodyConstructor from "../../components/Body"
 import { pageResources, renderPage } from "../../components/renderPage"
 import { ProcessedContent, defaultProcessedContent } from "../vfile"
 import { FullPageLayout } from "../../cfg"
-import {
-  CanonicalSlug,
-  FilePath,
-  ServerSlug,
-  getAllSegmentPrefixes,
-  joinSegments,
-} from "../../util/path"
+import { FilePath, FullSlug, getAllSegmentPrefixes, joinSegments } from "../../util/path"
 import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout"
 import { TagContent } from "../../components"
 
@@ -49,7 +43,7 @@ export const TagPage: QuartzEmitterPlugin<FullPageLayout> = (userOpts) => {
           return [
             tag,
             defaultProcessedContent({
-              slug: joinSegments("tags", tag) as ServerSlug,
+              slug: joinSegments("tags", tag) as FullSlug,
               frontmatter: { title, tags: [] },
             }),
           ]
@@ -67,7 +61,7 @@ export const TagPage: QuartzEmitterPlugin<FullPageLayout> = (userOpts) => {
       }
 
       for (const tag of tags) {
-        const slug = joinSegments("tags", tag) as CanonicalSlug
+        const slug = joinSegments("tags", tag) as FullSlug
         const externalResources = pageResources(slug, resources)
         const [tree, file] = tagDescriptions[tag]
         const componentData: QuartzComponentProps = {
diff --git a/quartz/plugins/index.ts b/quartz/plugins/index.ts
index c83455e..9753d2e 100644
--- a/quartz/plugins/index.ts
+++ b/quartz/plugins/index.ts
@@ -1,5 +1,5 @@
 import { StaticResources } from "../util/resources"
-import { FilePath, ServerSlug } from "../util/path"
+import { FilePath, FullSlug } from "../util/path"
 import { BuildCtx } from "../util/ctx"
 
 export function getStaticResourcesFromPlugins(ctx: BuildCtx) {
@@ -28,7 +28,7 @@ export * from "./emitters"
 declare module "vfile" {
   // inserted in processors.ts
   interface DataMap {
-    slug: ServerSlug
+    slug: FullSlug
     filePath: FilePath
   }
 }
diff --git a/quartz/plugins/transformers/links.ts b/quartz/plugins/transformers/links.ts
index d867039..5d34fa3 100644
--- a/quartz/plugins/transformers/links.ts
+++ b/quartz/plugins/transformers/links.ts
@@ -1,11 +1,12 @@
 import { QuartzTransformerPlugin } from "../types"
 import {
-  CanonicalSlug,
+  FullSlug,
   RelativeURL,
+  SimpleSlug,
   TransformOptions,
   _stripSlashes,
-  canonicalizeServer,
   joinSegments,
+  simplifySlug,
   splitAnchor,
   transformLink,
 } from "../../util/path"
@@ -33,8 +34,8 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
       return [
         () => {
           return (tree, file) => {
-            const curSlug = canonicalizeServer(file.data.slug!)
-            const outgoing: Set<CanonicalSlug> = new Set()
+            const curSlug = simplifySlug(file.data.slug!)
+            const outgoing: Set<SimpleSlug> = new Set()
 
             const transformOptions: TransformOptions = {
               strategy: opts.markdownLinkResolution,
@@ -54,10 +55,15 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
 
                 // don't process external links or intra-document anchors
                 if (!(isAbsoluteUrl(dest) || dest.startsWith("#"))) {
-                  dest = node.properties.href = transformLink(curSlug, dest, transformOptions)
+                  dest = node.properties.href = transformLink(
+                    file.data.slug!,
+                    dest,
+                    transformOptions,
+                  )
                   const canonicalDest = path.posix.normalize(joinSegments(curSlug, dest))
                   const [destCanonical, _destAnchor] = splitAnchor(canonicalDest)
-                  outgoing.add(destCanonical as CanonicalSlug)
+                  const simple = simplifySlug(destCanonical as FullSlug)
+                  outgoing.add(simple)
                 }
 
                 // rewrite link internals if prettylinks is on
@@ -79,7 +85,11 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
               ) {
                 if (!isAbsoluteUrl(node.properties.src)) {
                   let dest = node.properties.src as RelativeURL
-                  dest = node.properties.src = transformLink(curSlug, dest, transformOptions)
+                  dest = node.properties.src = transformLink(
+                    file.data.slug!,
+                    dest,
+                    transformOptions,
+                  )
                   node.properties.src = dest
                 }
               }
@@ -95,6 +105,6 @@ export const CrawlLinks: QuartzTransformerPlugin<Partial<Options> | undefined> =
 
 declare module "vfile" {
   interface DataMap {
-    links: CanonicalSlug[]
+    links: SimpleSlug[]
   }
 }
diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts
index b324dac..b66ba85 100644
--- a/quartz/plugins/transformers/ofm.ts
+++ b/quartz/plugins/transformers/ofm.ts
@@ -9,7 +9,7 @@ import path from "path"
 import { JSResource } from "../../util/resources"
 // @ts-ignore
 import calloutScript from "../../components/scripts/callout.inline.ts"
-import { FilePath, canonicalizeServer, pathToRoot, slugTag, slugifyFilePath } from "../../util/path"
+import { FilePath, pathToRoot, slugTag, slugifyFilePath } from "../../util/path"
 import { toHast } from "mdast-util-to-hast"
 import { toHtml } from "hast-util-to-html"
 import { PhrasingContent } from "mdast-util-find-and-replace/lib"
@@ -381,8 +381,7 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin<Partial<Options>
       if (opts.parseTags) {
         plugins.push(() => {
           return (tree: Root, file) => {
-            const slug = canonicalizeServer(file.data.slug!)
-            const base = pathToRoot(slug)
+            const base = pathToRoot(file.data.slug!)
             findAndReplace(tree, tagRegex, (value: string, tag: string) => {
               if (file.data.frontmatter) {
                 file.data.frontmatter.tags.push(tag)
diff --git a/quartz/plugins/types.ts b/quartz/plugins/types.ts
index ad1881b..eaeb12a 100644
--- a/quartz/plugins/types.ts
+++ b/quartz/plugins/types.ts
@@ -2,7 +2,7 @@ import { PluggableList } from "unified"
 import { StaticResources } from "../util/resources"
 import { ProcessedContent } from "./vfile"
 import { QuartzComponent } from "../components/types"
-import { FilePath, ServerSlug } from "../util/path"
+import { FilePath, FullSlug } from "../util/path"
 import { BuildCtx } from "../util/ctx"
 
 export interface PluginTypes {
@@ -46,7 +46,7 @@ export type QuartzEmitterPluginInstance = {
 }
 
 export interface EmitOptions {
-  slug: ServerSlug
+  slug: FullSlug
   ext: `.${string}` | ""
   content: string
 }
diff --git a/quartz/util/ctx.ts b/quartz/util/ctx.ts
index b127839..d303391 100644
--- a/quartz/util/ctx.ts
+++ b/quartz/util/ctx.ts
@@ -1,5 +1,5 @@
 import { QuartzConfig } from "../cfg"
-import { ServerSlug } from "./path"
+import { FullSlug } from "./path"
 
 export interface Argv {
   directory: string
@@ -13,5 +13,5 @@ export interface Argv {
 export interface BuildCtx {
   argv: Argv
   cfg: QuartzConfig
-  allSlugs: ServerSlug[]
+  allSlugs: FullSlug[]
 }
diff --git a/quartz/util/path.test.ts b/quartz/util/path.test.ts
index cbfd86b..8bbb58d 100644
--- a/quartz/util/path.test.ts
+++ b/quartz/util/path.test.ts
@@ -1,45 +1,25 @@
 import test, { describe } from "node:test"
 import * as path from "./path"
 import assert from "node:assert"
-import { CanonicalSlug, ServerSlug, TransformOptions } from "./path"
+import { FullSlug, TransformOptions } from "./path"
 
 describe("typeguards", () => {
-  test("isClientSlug", () => {
-    assert(path.isClientSlug("http://example.com"))
-    assert(path.isClientSlug("http://example.com/index"))
-    assert(path.isClientSlug("http://example.com/index.html"))
-    assert(path.isClientSlug("http://example.com/"))
-    assert(path.isClientSlug("https://example.com"))
-    assert(path.isClientSlug("https://example.com/abc/def"))
-    assert(path.isClientSlug("https://example.com/abc/def/"))
-    assert(path.isClientSlug("https://example.com/abc/def#cool"))
-    assert(path.isClientSlug("https://example.com/abc/def?field=1&another=2"))
-    assert(path.isClientSlug("https://example.com/abc/def?field=1&another=2#cool"))
-    assert(path.isClientSlug("https://example.com/abc/def.html?field=1&another=2#cool"))
+  test("isSimpleSlug", () => {
+    assert(path.isSimpleSlug(""))
+    assert(path.isSimpleSlug("abc"))
+    assert(path.isSimpleSlug("abc/"))
+    assert(path.isSimpleSlug("notindex"))
+    assert(path.isSimpleSlug("notindex/def"))
 
-    assert(!path.isClientSlug("./"))
-    assert(!path.isClientSlug(""))
-    assert(!path.isClientSlug("ipfs://example.com"))
-    assert(!path.isClientSlug("http"))
-    assert(!path.isClientSlug("https"))
-  })
-
-  test("isCanonicalSlug", () => {
-    assert(path.isCanonicalSlug(""))
-    assert(path.isCanonicalSlug("abc"))
-    assert(path.isCanonicalSlug("notindex"))
-    assert(path.isCanonicalSlug("notindex/def"))
-
-    assert(!path.isCanonicalSlug("//"))
-    assert(!path.isCanonicalSlug("index"))
-    assert(!path.isCanonicalSlug("https://example.com"))
-    assert(!path.isCanonicalSlug("/abc"))
-    assert(!path.isCanonicalSlug("abc/"))
-    assert(!path.isCanonicalSlug("abc/index"))
-    assert(!path.isCanonicalSlug("abc#anchor"))
-    assert(!path.isCanonicalSlug("abc?query=1"))
-    assert(!path.isCanonicalSlug("index.md"))
-    assert(!path.isCanonicalSlug("index.html"))
+    assert(!path.isSimpleSlug("//"))
+    assert(!path.isSimpleSlug("index"))
+    assert(!path.isSimpleSlug("https://example.com"))
+    assert(!path.isSimpleSlug("/abc"))
+    assert(!path.isSimpleSlug("abc/index"))
+    assert(!path.isSimpleSlug("abc#anchor"))
+    assert(!path.isSimpleSlug("abc?query=1"))
+    assert(!path.isSimpleSlug("index.md"))
+    assert(!path.isSimpleSlug("index.html"))
   })
 
   test("isRelativeURL", () => {
@@ -58,18 +38,18 @@ describe("typeguards", () => {
     assert(!path.isRelativeURL("./abc/def.md"))
   })
 
-  test("isServerSlug", () => {
-    assert(path.isServerSlug("index"))
-    assert(path.isServerSlug("abc/def"))
-    assert(path.isServerSlug("html.energy"))
-    assert(path.isServerSlug("test.pdf"))
+  test("isFullSlug", () => {
+    assert(path.isFullSlug("index"))
+    assert(path.isFullSlug("abc/def"))
+    assert(path.isFullSlug("html.energy"))
+    assert(path.isFullSlug("test.pdf"))
 
-    assert(!path.isServerSlug("."))
-    assert(!path.isServerSlug("./abc/def"))
-    assert(!path.isServerSlug("../abc/def"))
-    assert(!path.isServerSlug("abc/def#anchor"))
-    assert(!path.isServerSlug("abc/def?query=1"))
-    assert(!path.isServerSlug("note with spaces"))
+    assert(!path.isFullSlug("."))
+    assert(!path.isFullSlug("./abc/def"))
+    assert(!path.isFullSlug("../abc/def"))
+    assert(!path.isFullSlug("abc/def#anchor"))
+    assert(!path.isFullSlug("abc/def?query=1"))
+    assert(!path.isFullSlug("note with spaces"))
   })
 
   test("isFilePath", () => {
@@ -100,40 +80,17 @@ describe("transforms", () => {
     }
   }
 
-  test("canonicalizeServer", () => {
+  test("simplifySlug", () => {
     asserts(
       [
         ["index", ""],
-        ["abc/index", "abc"],
+        ["abc", "abc"],
+        ["abc/index", "abc/"],
         ["abc/def", "abc/def"],
       ],
-      path.canonicalizeServer,
-      path.isServerSlug,
-      path.isCanonicalSlug,
-    )
-  })
-
-  test("canonicalizeClient", () => {
-    asserts(
-      [
-        ["http://localhost:3000", ""],
-        ["http://localhost:3000/index", ""],
-        ["http://localhost:3000/test", "test"],
-        ["http://example.com", ""],
-        ["http://example.com/index", ""],
-        ["http://example.com/index.html", ""],
-        ["http://example.com/", ""],
-        ["https://example.com", ""],
-        ["https://example.com/abc/def", "abc/def"],
-        ["https://example.com/abc/def/", "abc/def"],
-        ["https://example.com/abc/def#cool", "abc/def"],
-        ["https://example.com/abc/def?field=1&another=2", "abc/def"],
-        ["https://example.com/abc/def?field=1&another=2#cool", "abc/def"],
-        ["https://example.com/abc/def.html?field=1&another=2#cool", "abc/def"],
-      ],
-      path.canonicalizeClient,
-      path.isClientSlug,
-      path.isCanonicalSlug,
+      path.simplifySlug,
+      path.isFullSlug,
+      path.isSimpleSlug,
     )
   })
 
@@ -151,7 +108,7 @@ describe("transforms", () => {
       ],
       path.slugifyFilePath,
       path.isFilePath,
-      path.isServerSlug,
+      path.isFullSlug,
     )
   })
 
@@ -186,12 +143,14 @@ describe("transforms", () => {
   test("pathToRoot", () => {
     asserts(
       [
-        ["", "."],
-        ["abc", ".."],
-        ["abc/def", "../.."],
+        ["index", "."],
+        ["abc", "."],
+        ["abc/def", ".."],
+        ["abc/def/ghi", "../.."],
+        ["abc/def/index", "../.."],
       ],
       path.pathToRoot,
-      path.isCanonicalSlug,
+      path.isFullSlug,
       path.isRelativeURL,
     )
   })
@@ -206,7 +165,7 @@ describe("link strategies", () => {
     "e/g/h",
     "index",
     "a/test.png",
-  ] as ServerSlug[]
+  ] as FullSlug[]
 
   describe("absolute", () => {
     const opts: TransformOptions = {
@@ -215,28 +174,28 @@ describe("link strategies", () => {
     }
 
     test("from a/b/c", () => {
-      const cur = "a/b/c" as CanonicalSlug
-      assert.strictEqual(path.transformLink(cur, "a/b/d", opts), "../../../a/b/d")
-      assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "../../../a/b/")
-      assert.strictEqual(path.transformLink(cur, "e/f", opts), "../../../e/f")
-      assert.strictEqual(path.transformLink(cur, "e/g/h", opts), "../../../e/g/h")
-      assert.strictEqual(path.transformLink(cur, "index", opts), "../../../")
-      assert.strictEqual(path.transformLink(cur, "index.png", opts), "../../../index.png")
-      assert.strictEqual(path.transformLink(cur, "index#abc", opts), "../../../#abc")
-      assert.strictEqual(path.transformLink(cur, "tag/test", opts), "../../../tag/test")
-      assert.strictEqual(path.transformLink(cur, "a/b/c#test", opts), "../../../a/b/c#test")
-      assert.strictEqual(path.transformLink(cur, "a/test.png", opts), "../../../a/test.png")
+      const cur = "a/b/c" as FullSlug
+      assert.strictEqual(path.transformLink(cur, "a/b/d", opts), "../../a/b/d")
+      assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "../../a/b/")
+      assert.strictEqual(path.transformLink(cur, "e/f", opts), "../../e/f")
+      assert.strictEqual(path.transformLink(cur, "e/g/h", opts), "../../e/g/h")
+      assert.strictEqual(path.transformLink(cur, "index", opts), "../../")
+      assert.strictEqual(path.transformLink(cur, "index.png", opts), "../../index.png")
+      assert.strictEqual(path.transformLink(cur, "index#abc", opts), "../../#abc")
+      assert.strictEqual(path.transformLink(cur, "tag/test", opts), "../../tag/test")
+      assert.strictEqual(path.transformLink(cur, "a/b/c#test", opts), "../../a/b/c#test")
+      assert.strictEqual(path.transformLink(cur, "a/test.png", opts), "../../a/test.png")
     })
 
     test("from a/b/index", () => {
-      const cur = "a/b" as CanonicalSlug
+      const cur = "a/b/index" as FullSlug
       assert.strictEqual(path.transformLink(cur, "a/b/d", opts), "../../a/b/d")
       assert.strictEqual(path.transformLink(cur, "a/b", opts), "../../a/b")
       assert.strictEqual(path.transformLink(cur, "index", opts), "../../")
     })
 
     test("from index", () => {
-      const cur = "" as CanonicalSlug
+      const cur = "index" as FullSlug
       assert.strictEqual(path.transformLink(cur, "index", opts), "./")
       assert.strictEqual(path.transformLink(cur, "a/b/c", opts), "./a/b/c")
       assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "./a/b/")
@@ -250,20 +209,20 @@ describe("link strategies", () => {
     }
 
     test("from a/b/c", () => {
-      const cur = "a/b/c" as CanonicalSlug
-      assert.strictEqual(path.transformLink(cur, "d", opts), "../../../a/b/d")
-      assert.strictEqual(path.transformLink(cur, "h", opts), "../../../e/g/h")
-      assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "../../../a/b/")
-      assert.strictEqual(path.transformLink(cur, "a/b/index.png", opts), "../../../a/b/index.png")
-      assert.strictEqual(path.transformLink(cur, "a/b/index#abc", opts), "../../../a/b/#abc")
-      assert.strictEqual(path.transformLink(cur, "index", opts), "../../../")
-      assert.strictEqual(path.transformLink(cur, "index.png", opts), "../../../index.png")
-      assert.strictEqual(path.transformLink(cur, "test.png", opts), "../../../a/test.png")
-      assert.strictEqual(path.transformLink(cur, "index#abc", opts), "../../../#abc")
+      const cur = "a/b/c" as FullSlug
+      assert.strictEqual(path.transformLink(cur, "d", opts), "../../a/b/d")
+      assert.strictEqual(path.transformLink(cur, "h", opts), "../../e/g/h")
+      assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "../../a/b/")
+      assert.strictEqual(path.transformLink(cur, "a/b/index.png", opts), "../../a/b/index.png")
+      assert.strictEqual(path.transformLink(cur, "a/b/index#abc", opts), "../../a/b/#abc")
+      assert.strictEqual(path.transformLink(cur, "index", opts), "../../")
+      assert.strictEqual(path.transformLink(cur, "index.png", opts), "../../index.png")
+      assert.strictEqual(path.transformLink(cur, "test.png", opts), "../../a/test.png")
+      assert.strictEqual(path.transformLink(cur, "index#abc", opts), "../../#abc")
     })
 
     test("from a/b/index", () => {
-      const cur = "a/b" as CanonicalSlug
+      const cur = "a/b/index" as FullSlug
       assert.strictEqual(path.transformLink(cur, "d", opts), "../../a/b/d")
       assert.strictEqual(path.transformLink(cur, "h", opts), "../../e/g/h")
       assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "../../a/b/")
@@ -271,7 +230,7 @@ describe("link strategies", () => {
     })
 
     test("from index", () => {
-      const cur = "" as CanonicalSlug
+      const cur = "index" as FullSlug
       assert.strictEqual(path.transformLink(cur, "d", opts), "./a/b/d")
       assert.strictEqual(path.transformLink(cur, "h", opts), "./e/g/h")
       assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "./a/b/")
@@ -286,7 +245,7 @@ describe("link strategies", () => {
     }
 
     test("from a/b/c", () => {
-      const cur = "a/b/c" as CanonicalSlug
+      const cur = "a/b/c" as FullSlug
       assert.strictEqual(path.transformLink(cur, "d", opts), "./d")
       assert.strictEqual(path.transformLink(cur, "index", opts), "./")
       assert.strictEqual(path.transformLink(cur, "../../../index", opts), "../../../")
@@ -303,7 +262,7 @@ describe("link strategies", () => {
     })
 
     test("from a/b/index", () => {
-      const cur = "a/b" as CanonicalSlug
+      const cur = "a/b/index" as FullSlug
       assert.strictEqual(path.transformLink(cur, "../../index", opts), "../../")
       assert.strictEqual(path.transformLink(cur, "../../", opts), "../../")
       assert.strictEqual(path.transformLink(cur, "../../e/g/h", opts), "../../e/g/h")
@@ -311,7 +270,7 @@ describe("link strategies", () => {
     })
 
     test("from index", () => {
-      const cur = "" as CanonicalSlug
+      const cur = "index" as FullSlug
       assert.strictEqual(path.transformLink(cur, "e/g/h", opts), "./e/g/h")
       assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "./a/b/")
     })
diff --git a/quartz/util/path.ts b/quartz/util/path.ts
index 7394a43..d14b827 100644
--- a/quartz/util/path.ts
+++ b/quartz/util/path.ts
@@ -1,73 +1,35 @@
 import { slug } from "github-slugger"
 // this file must be isomorphic so it can't use node libs (e.g. path)
 
-// Quartz Paths
-// Things in boxes are not actual types but rather sources which these types can be acquired from
-//
-//                                                         ┌────────────┐
-//                                             ┌───────────┤  Browser   ├────────────┐
-//                                             │           └────────────┘            │
-//                                             │                                     │
-//                                             ▼                                     ▼
-//                                        ┌────────┐                          ┌─────────────┐
-//                    ┌───────────────────┤ Window │                          │ LinkElement │
-//                    │                   └────┬───┘                          └──────┬──────┘
-//                    │                        │                                     │
-//                    │        getClientSlug() │                               .href │
-//                    │                        ▼                                     ▼
-//                    │
-//                    │                  Client Slug                    ┌───►  Relative URL
-// getCanonicalSlug() │     https://test.ca/note/abc#anchor?query=123   │      ../note/def#anchor
-//                    │                                                 │
-//                    │   canonicalizeClient() │                        │      ▲     ▲
-//                    │                        ▼                        │      │     │
-//                    │                                  pathToRoot()   │      │     │
-//                    └───────────────►  Canonical Slug ────────────────┘      │     │
-//                                          note/abc                           │     │
-//                                                   ──────────────────────────┘     │
-//                                             ▲             resolveRelative()       │
-//                        canonicalizeServer() │                                     │
-//                                                                                   │
-// HTML File                               Server Slug                               │
-//  note/abc/index.html  ◄─────────────   note/abc/index                             │
-//                                                                                   │
-//                                             ▲                            ┌────────┴────────┐
-//                           slugifyFilePath() │            transformLink() │                 │
-//                                             │                            │                 │
-//                                   ┌─────────┴──────────┐           ┌─────┴─────┐  ┌────────┴──────┐
-//                                   │     File Path      │           │ Wikilinks │  │ Markdown Link │
-//                                   │  note/abc/index.md │           └───────────┘  └───────────────┘
-//                                   └────────────────────┘                 ▲                 ▲
-//                                             ▲                            │                 │
-//                                             │            ┌─────────┐     │                 │
-//                                             └────────────┤ MD File ├─────┴─────────────────┘
-//                                                          └─────────┘
-
 export const QUARTZ = "quartz"
 
 /// Utility type to simulate nominal types in TypeScript
 type SlugLike<T> = string & { __brand: T }
 
-/** Client-side slug, usually obtained through `window.location` */
-export type ClientSlug = SlugLike<"client">
-export function isClientSlug(s: string): s is ClientSlug {
-  const res = /^https?:\/\/.+/.test(s)
-  return res
+/** Cannot be relative and must have a file extension. */
+export type FilePath = SlugLike<"filepath">
+export function isFilePath(s: string): s is FilePath {
+  const validStart = !s.startsWith(".")
+  return validStart && _hasFileExtension(s)
 }
 
-/** Canonical slug, should be used whenever you need to refer to the location of a file/note.
- * On the client, this is normally stored in `document.body.dataset.slug`
- */
-export type CanonicalSlug = SlugLike<"canonical">
-export function isCanonicalSlug(s: string): s is CanonicalSlug {
+/** Cannot be relative and may not have leading or trailing slashes. It can have `index` as it's last segment. Use this wherever possible is it's the most 'general' interpretation of a slug. */
+export type FullSlug = SlugLike<"full">
+export function isFullSlug(s: string): s is FullSlug {
   const validStart = !(s.startsWith(".") || s.startsWith("/"))
-  const validEnding = !(s.endsWith("/") || s.endsWith("/index") || s === "index")
+  const validEnding = !s.endsWith("/")
+  return validStart && validEnding && !_containsForbiddenCharacters(s)
+}
+
+/** Shouldn't be a relative path and shouldn't have `/index` as an ending or a file extension. It _can_ however have a trailing slash to indicate a folder path. */
+export type SimpleSlug = SlugLike<"simple">
+export function isSimpleSlug(s: string): s is SimpleSlug {
+  const validStart = !(s.startsWith(".") || s.startsWith("/"))
+  const validEnding = !(s.endsWith("/index") || s === "index")
   return validStart && !_containsForbiddenCharacters(s) && validEnding && !_hasFileExtension(s)
 }
 
-/** A relative link, can be found on `href`s but can also be constructed for
- * client-side navigation (e.g. search and graph)
- */
+/** Can be found on `href`s but can also be constructed for client-side navigation (e.g. search and graph) */
 export type RelativeURL = SlugLike<"relative">
 export function isRelativeURL(s: string): s is RelativeURL {
   const validStart = /^\.{1,2}/.test(s)
@@ -75,46 +37,12 @@ export function isRelativeURL(s: string): s is RelativeURL {
   return validStart && validEnding && ![".md", ".html"].includes(_getFileExtension(s) ?? "")
 }
 
-/** A server side slug. This is what Quartz uses to emit files so uses index suffixes */
-export type ServerSlug = SlugLike<"server">
-export function isServerSlug(s: string): s is ServerSlug {
-  const validStart = !(s.startsWith(".") || s.startsWith("/"))
-  const validEnding = !s.endsWith("/")
-  return validStart && validEnding && !_containsForbiddenCharacters(s)
-}
-
-/** The real file path to a file on disk */
-export type FilePath = SlugLike<"filepath">
-export function isFilePath(s: string): s is FilePath {
-  const validStart = !s.startsWith(".")
-  return validStart && _hasFileExtension(s)
-}
-
-export function getClientSlug(window: Window): ClientSlug {
-  const res = window.location.href as ClientSlug
+export function getFullSlug(window: Window): FullSlug {
+  const res = window.document.body.dataset.slug! as FullSlug
   return res
 }
 
-export function getCanonicalSlug(window: Window): CanonicalSlug {
-  const res = window.document.body.dataset.slug! as CanonicalSlug
-  return res
-}
-
-export function canonicalizeClient(slug: ClientSlug): CanonicalSlug {
-  const { pathname } = new URL(slug)
-  let fp = pathname.slice(1)
-  fp = fp.replace(new RegExp(_getFileExtension(fp) + "$"), "")
-  const res = _canonicalize(fp) as CanonicalSlug
-  return res
-}
-
-export function canonicalizeServer(slug: ServerSlug): CanonicalSlug {
-  let fp = slug as string
-  const res = _canonicalize(fp) as CanonicalSlug
-  return res
-}
-
-export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): ServerSlug {
+export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): FullSlug {
   fp = _stripSlashes(fp) as FilePath
   let ext = _getFileExtension(fp)
   const withoutFileExt = fp.replace(new RegExp(ext + "$"), "")
@@ -133,44 +61,47 @@ export function slugifyFilePath(fp: FilePath, excludeExt?: boolean): ServerSlug
     slug = slug.replace(/_index$/, "index")
   }
 
-  return (slug + ext) as ServerSlug
+  return (slug + ext) as FullSlug
+}
+
+export function simplifySlug(fp: FullSlug): SimpleSlug {
+  return _stripSlashes(_trimSuffix(fp, "index"), true) as SimpleSlug
 }
 
 export function transformInternalLink(link: string): RelativeURL {
   let [fplike, anchor] = splitAnchor(decodeURI(link))
 
-  const folderPath =
-    fplike.endsWith("index") ||
-    fplike.endsWith("index.md") ||
-    fplike.endsWith("index.html") ||
-    fplike.endsWith("/")
-
+  const folderPath = _isFolderPath(fplike)
   let segments = fplike.split("/").filter((x) => x.length > 0)
   let prefix = segments.filter(_isRelativeSegment).join("/")
-  let fp = segments.filter((seg) => !_isRelativeSegment(seg)).join("/")
+  let fp = segments.filter((seg) => !_isRelativeSegment(seg) && seg !== "").join("/")
 
   // manually add ext here as we want to not strip 'index' if it has an extension
-  fp = canonicalizeServer(slugifyFilePath(fp as FilePath) as ServerSlug)
-  const joined = joinSegments(_stripSlashes(prefix), _stripSlashes(fp))
+  const simpleSlug = simplifySlug(slugifyFilePath(fp as FilePath))
+  const joined = joinSegments(_stripSlashes(prefix), _stripSlashes(simpleSlug))
   const trail = folderPath ? "/" : ""
   const res = (_addRelativeToStart(joined) + trail + anchor) as RelativeURL
   return res
 }
 
-// resolve /a/b/c to ../../..
-export function pathToRoot(slug: CanonicalSlug): RelativeURL {
+// resolve /a/b/c to ../..
+export function pathToRoot(slug: FullSlug): RelativeURL {
   let rootPath = slug
     .split("/")
     .filter((x) => x !== "")
+    .slice(0, -1)
     .map((_) => "..")
     .join("/")
 
-  const res = _addRelativeToStart(rootPath) as RelativeURL
-  return res
+  if (rootPath.length === 0) {
+    rootPath = "."
+  }
+
+  return rootPath as RelativeURL
 }
 
-export function resolveRelative(current: CanonicalSlug, target: CanonicalSlug): RelativeURL {
-  const res = joinSegments(pathToRoot(current), target) as RelativeURL
+export function resolveRelative(current: FullSlug, target: FullSlug | SimpleSlug): RelativeURL {
+  const res = joinSegments(pathToRoot(current), simplifySlug(target as FullSlug)) as RelativeURL
   return res
 }
 
@@ -206,20 +137,16 @@ export function getAllSegmentPrefixes(tags: string): string[] {
 
 export interface TransformOptions {
   strategy: "absolute" | "relative" | "shortest"
-  allSlugs: ServerSlug[]
+  allSlugs: FullSlug[]
 }
 
-export function transformLink(
-  src: CanonicalSlug,
-  target: string,
-  opts: TransformOptions,
-): RelativeURL {
-  let targetSlug: string = transformInternalLink(target)
+export function transformLink(src: FullSlug, target: string, opts: TransformOptions): RelativeURL {
+  let targetSlug = transformInternalLink(target)
 
   if (opts.strategy === "relative") {
-    return _addRelativeToStart(targetSlug) as RelativeURL
+    return targetSlug as RelativeURL
   } else {
-    const folderTail = targetSlug.endsWith("/") ? "/" : ""
+    const folderTail = _isFolderPath(targetSlug) ? "/" : ""
     const canonicalSlug = _stripSlashes(targetSlug.slice(".".length))
     let [targetCanonical, targetAnchor] = splitAnchor(canonicalSlug)
 
@@ -233,7 +160,7 @@ export function transformLink(
 
       // only match, just use it
       if (matchingFileNames.length === 1) {
-        const targetSlug = canonicalizeServer(matchingFileNames[0])
+        const targetSlug = matchingFileNames[0]
         return (resolveRelative(src, targetSlug) + targetAnchor) as RelativeURL
       }
     }
@@ -243,9 +170,13 @@ export function transformLink(
   }
 }
 
-function _canonicalize(fp: string): string {
-  fp = _trimSuffix(fp, "index")
-  return _stripSlashes(fp)
+function _isFolderPath(fplike: string): boolean {
+  return (
+    fplike.endsWith("/") ||
+    _endsWith(fplike, "index") ||
+    _endsWith(fplike, "index.md") ||
+    _endsWith(fplike, "index.html")
+  )
 }
 
 function _endsWith(s: string, suffix: string): boolean {
@@ -275,12 +206,12 @@ function _isRelativeSegment(s: string): boolean {
   return /^\.{0,2}$/.test(s)
 }
 
-export function _stripSlashes(s: string): string {
+export function _stripSlashes(s: string, onlyStripPrefix?: boolean): string {
   if (s.startsWith("/")) {
     s = s.substring(1)
   }
 
-  if (s.endsWith("/")) {
+  if (!onlyStripPrefix && s.endsWith("/")) {
     s = s.slice(0, -1)
   }
 
diff --git a/quartz/worker.ts b/quartz/worker.ts
index 3bd7ea7..b92bdac 100644
--- a/quartz/worker.ts
+++ b/quartz/worker.ts
@@ -2,12 +2,12 @@ import sourceMapSupport from "source-map-support"
 sourceMapSupport.install(options)
 import cfg from "../quartz.config"
 import { Argv, BuildCtx } from "./util/ctx"
-import { FilePath, ServerSlug } from "./util/path"
+import { FilePath, FullSlug } from "./util/path"
 import { createFileParser, createProcessor } from "./processors/parse"
 import { options } from "./util/sourcemap"
 
 // only called from worker thread
-export async function parseFiles(argv: Argv, fps: FilePath[], allSlugs: ServerSlug[]) {
+export async function parseFiles(argv: Argv, fps: FilePath[], allSlugs: FullSlug[]) {
   const ctx: BuildCtx = {
     cfg,
     argv,