From 2f6747b1666316e579c6e7238092ac6a65d00925 Mon Sep 17 00:00:00 2001
From: Jacky Zhao <j.zhao2k19@gmail.com>
Date: Thu, 17 Aug 2023 05:04:15 +0000
Subject: [PATCH] fix relative path resolution in router and link crawling

---
 quartz/plugins/types.ts                       |    6 
 quartz/util/resources.tsx                     |    0 
 package-lock.json                             |   10 --
 quartz/util/perf.ts                           |    0 
 quartz/components/PageTitle.tsx               |    2 
 quartz/components/PageList.tsx                |    2 
 quartz/plugins/transformers/frontmatter.ts    |    2 
 quartz/components/Head.tsx                    |    4 
 quartz/util/trace.ts                          |    0 
 quartz/components/scripts/search.inline.ts    |    2 
 quartz/bootstrap-cli.mjs                      |   74 ++++++++++++--
 quartz/components/scripts/graph.inline.ts     |    2 
 quartz/plugins/transformers/links.ts          |    2 
 quartz/util/theme.ts                          |    0 
 quartz/build.ts                               |   12 +-
 quartz/plugins/emitters/componentResources.ts |    8 
 quartz/plugins/index.ts                       |    6 
 quartz/plugins/emitters/contentIndex.ts       |    8 +
 quartz/util/path.ts                           |   17 ++-
 quartz/plugins/emitters/tagPage.tsx           |    4 
 quartz/plugins/transformers/ofm.ts            |    6 
 quartz/components/renderPage.tsx              |    4 
 quartz/processors/emit.ts                     |   10 +-
 quartz/plugins/emitters/assets.ts             |    4 
 quartz/plugins/emitters/aliases.ts            |    2 
 quartz/processors/filter.ts                   |    4 
 quartz/util/glob.ts                           |    0 
 quartz/components/Backlinks.tsx               |    2 
 quartz/util/ctx.ts                            |    2 
 quartz/util/path.test.ts                      |   27 ++--
 quartz/util/sourcemap.ts                      |    0 
 quartz/plugins/emitters/static.ts             |    4 
 quartz/components/TagList.tsx                 |    2 
 quartz/util/log.ts                            |    0 
 quartz/worker.ts                              |    2 
 quartz/components/scripts/spa.inline.ts       |    2 
 quartz/processors/parse.ts                    |   10 +-
 content/features/upcoming features.md         |    2 
 package.json                                  |    3 
 quartz/plugins/emitters/folderPage.tsx        |    8 +
 content/hosting.md                            |    5 
 quartz/components/pages/TagContent.tsx        |    2 
 quartz/plugins/emitters/contentPage.tsx       |    2 
 quartz/components/pages/FolderContent.tsx     |    2 
 44 files changed, 160 insertions(+), 106 deletions(-)

diff --git a/content/features/upcoming features.md b/content/features/upcoming features.md
index 6c64f18..8d6c657 100644
--- a/content/features/upcoming features.md
+++ b/content/features/upcoming features.md
@@ -8,8 +8,6 @@
 - debounce cfg rebuild on large repos
   - investigate content rebuild triggering multiple times even when debounced, causing an esbuild deadlock
 - dereference symlink for npx quartz sync
-- test/fix with subpath
-- fix docs with deploy from github
 
 ## high priority backlog
 
diff --git a/content/hosting.md b/content/hosting.md
index 5cab760..11dff18 100644
--- a/content/hosting.md
+++ b/content/hosting.md
@@ -80,7 +80,10 @@
         uses: actions/deploy-pages@v2
 ```
 
-Then, commit these changes by doing `npx quartz sync`. This should deploy your site to `<github-username>.github.io/<repository-name>`.
+Then:
+
+1. Head to "Settings" tab of your forked repository and in the sidebar, click "Pages". Under "Source", select "GitHub Actions".
+2. Commit these changes by doing `npx quartz sync`. This should deploy your site to `<github-username>.github.io/<repository-name>`.
 
 ### Custom Domain
 
diff --git a/package-lock.json b/package-lock.json
index f8eb5a4..4245d61 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -72,7 +72,6 @@
         "@types/js-yaml": "^4.0.5",
         "@types/node": "^20.1.2",
         "@types/pretty-time": "^1.1.2",
-        "@types/serve-handler": "^6.1.1",
         "@types/source-map-support": "^0.5.6",
         "@types/workerpool": "^6.4.0",
         "@types/ws": "^8.5.5",
@@ -1478,15 +1477,6 @@
       "integrity": "sha512-4i+Y+O5H80Rh01lY/3Z0hB/UWc4R64ReE83joEpVsIG3iQWpYx66k6pQh1amJNZquKtJQyu/RcfkTtvL0KwssA==",
       "dev": true
     },
-    "node_modules/@types/serve-handler": {
-      "version": "6.1.1",
-      "resolved": "https://registry.npmjs.org/@types/serve-handler/-/serve-handler-6.1.1.tgz",
-      "integrity": "sha512-bIwSmD+OV8w0t2e7EWsuQYlGoS1o5aEdVktgkXaa43Zm0qVWi21xaSRb3DQA1UXD+DJ5bRq1Rgu14ZczB+CjIQ==",
-      "dev": true,
-      "dependencies": {
-        "@types/node": "*"
-      }
-    },
     "node_modules/@types/source-map-support": {
       "version": "0.5.6",
       "resolved": "https://registry.npmjs.org/@types/source-map-support/-/source-map-support-0.5.6.tgz",
diff --git a/package.json b/package.json
index 29b46f2..17e46b0 100644
--- a/package.json
+++ b/package.json
@@ -14,7 +14,7 @@
   "scripts": {
     "check": "tsc --noEmit && npx prettier . --check",
     "format": "npx prettier . --write",
-    "test": "tsx ./quartz/path.test.ts",
+    "test": "tsx ./quartz/util/path.test.ts",
     "profile": "0x -D prof ./quartz/bootstrap-cli.mjs build --concurrency=1"
   },
   "keywords": [
@@ -89,7 +89,6 @@
     "@types/js-yaml": "^4.0.5",
     "@types/node": "^20.1.2",
     "@types/pretty-time": "^1.1.2",
-    "@types/serve-handler": "^6.1.1",
     "@types/source-map-support": "^0.5.6",
     "@types/workerpool": "^6.4.0",
     "@types/ws": "^8.5.5",
diff --git a/quartz/bootstrap-cli.mjs b/quartz/bootstrap-cli.mjs
index 0730f90..077e31b 100755
--- a/quartz/bootstrap-cli.mjs
+++ b/quartz/bootstrap-cli.mjs
@@ -74,6 +74,10 @@
     default: false,
     describe: "run a local server to live-preview your Quartz",
   },
+  baseDir: {
+    string: true,
+    describe: "base path to serve your local server on",
+  },
   port: {
     number: true,
     default: 8080,
@@ -384,19 +388,63 @@
 
       await build(clientRefresh)
       const server = http.createServer(async (req, res) => {
-        await serveHandler(req, res, {
-          public: argv.output,
-          directoryListing: false,
-          trailingSlash: true,
-        })
-        const status = res.statusCode
-        const statusString =
-          status >= 200 && status < 300
-            ? chalk.green(`[${status}]`)
-            : status >= 300 && status < 400
-            ? chalk.yellow(`[${status}]`)
-            : chalk.red(`[${status}]`)
-        console.log(statusString + chalk.grey(` ${req.url}`))
+        const serve = async (fp) => {
+          await serveHandler(req, res, {
+            public: argv.output,
+            directoryListing: false,
+          })
+          const status = res.statusCode
+          const statusString =
+            status >= 200 && status < 300 ? chalk.green(`[${status}]`) : chalk.red(`[${status}]`)
+          console.log(statusString + chalk.grey(` ${req.url}`))
+        }
+
+        const redirect = (newFp) => {
+          res.writeHead(301, {
+            Location: newFp,
+          })
+          console.log(chalk.yellow("[301]") + chalk.grey(` ${req.url} -> ${newFp}`))
+          return res.end()
+        }
+
+        let fp = req.url?.split("?")[0] ?? "/"
+
+        // handle redirects
+        if (fp.endsWith("/")) {
+          // /trailing/
+          // does /trailing/index.html exist? if so, serve it
+          const indexFp = path.posix.join(fp, "index.html")
+          if (fs.existsSync(path.posix.join(argv.output, indexFp))) {
+            return serve(indexFp)
+          }
+
+          // does /trailing.html exist? if so, redirect to /trailing
+          let base = fp.slice(0, -1)
+          if (path.extname(base) === "") {
+            base += ".html"
+          }
+          if (fs.existsSync(path.posix.join(argv.output, base))) {
+            return redirect(base)
+          }
+        } else {
+          // /regular
+          // does /regular.html exist? if so, serve it
+          let base = fp
+          if (path.extname(base) === "") {
+            base += ".html"
+          }
+          if (fs.existsSync(path.posix.join(argv.output, base))) {
+            return serve(base)
+          }
+
+          // does /regular/index.html exist? if so, redirect to /regular/
+          let indexFp = path.posix.join(fp, "index.html")
+          if (fs.existsSync(path.posix.join(argv.output, indexFp))) {
+            return redirect(fp + "/")
+          }
+        }
+
+        return serve(fp)
       })
       server.listen(argv.port)
       console.log(chalk.cyan(`Started a Quartz server listening at http://localhost:${argv.port}`))
diff --git a/quartz/build.ts b/quartz/build.ts
index c25efbf..779ab35 100644
--- a/quartz/build.ts
+++ b/quartz/build.ts
@@ -1,7 +1,7 @@
 import sourceMapSupport from "source-map-support"
 sourceMapSupport.install(options)
 import path from "path"
-import { PerfTimer } from "./perf"
+import { PerfTimer } from "./util/perf"
 import { rimraf } from "rimraf"
 import { isGitIgnored } from "globby"
 import chalk from "chalk"
@@ -9,13 +9,13 @@
 import { filterContent } from "./processors/filter"
 import { emitContent } from "./processors/emit"
 import cfg from "../quartz.config"
-import { FilePath, joinSegments, slugifyFilePath } from "./path"
+import { FilePath, joinSegments, slugifyFilePath } from "./util/path"
 import chokidar from "chokidar"
 import { ProcessedContent } from "./plugins/vfile"
-import { Argv, BuildCtx } from "./ctx"
-import { glob, toPosixPath } from "./glob"
-import { trace } from "./trace"
-import { options } from "./sourcemap"
+import { Argv, BuildCtx } from "./util/ctx"
+import { glob, toPosixPath } from "./util/glob"
+import { trace } from "./util/trace"
+import { options } from "./util/sourcemap"
 
 async function buildQuartz(argv: Argv, clientRefresh: () => void) {
   const ctx: BuildCtx = {
diff --git a/quartz/components/Backlinks.tsx b/quartz/components/Backlinks.tsx
index 575e613..8cf3afa 100644
--- a/quartz/components/Backlinks.tsx
+++ b/quartz/components/Backlinks.tsx
@@ -1,6 +1,6 @@
 import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
 import style from "./styles/backlinks.scss"
-import { canonicalizeServer, resolveRelative } from "../path"
+import { canonicalizeServer, resolveRelative } from "../util/path"
 
 function Backlinks({ fileData, allFiles }: QuartzComponentProps) {
   const slug = canonicalizeServer(fileData.slug!)
diff --git a/quartz/components/Head.tsx b/quartz/components/Head.tsx
index 0651b95..bfeb3f1 100644
--- a/quartz/components/Head.tsx
+++ b/quartz/components/Head.tsx
@@ -1,5 +1,5 @@
-import { canonicalizeServer, pathToRoot } from "../path"
-import { JSResourceToScriptElement } from "../resources"
+import { canonicalizeServer, pathToRoot } from "../util/path"
+import { JSResourceToScriptElement } from "../util/resources"
 import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
 
 export default (() => {
diff --git a/quartz/components/PageList.tsx b/quartz/components/PageList.tsx
index 7183acb..83b1b07 100644
--- a/quartz/components/PageList.tsx
+++ b/quartz/components/PageList.tsx
@@ -1,4 +1,4 @@
-import { CanonicalSlug, canonicalizeServer, resolveRelative } from "../path"
+import { CanonicalSlug, canonicalizeServer, resolveRelative } from "../util/path"
 import { QuartzPluginData } from "../plugins/vfile"
 import { Date } from "./Date"
 import { QuartzComponentProps } from "./types"
diff --git a/quartz/components/PageTitle.tsx b/quartz/components/PageTitle.tsx
index f6319ef..c1e74f3 100644
--- a/quartz/components/PageTitle.tsx
+++ b/quartz/components/PageTitle.tsx
@@ -1,4 +1,4 @@
-import { canonicalizeServer, pathToRoot } from "../path"
+import { canonicalizeServer, pathToRoot } from "../util/path"
 import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
 
 function PageTitle({ fileData, cfg }: QuartzComponentProps) {
diff --git a/quartz/components/TagList.tsx b/quartz/components/TagList.tsx
index 6560035..bf5badd 100644
--- a/quartz/components/TagList.tsx
+++ b/quartz/components/TagList.tsx
@@ -1,4 +1,4 @@
-import { canonicalizeServer, pathToRoot, slugTag } from "../path"
+import { canonicalizeServer, pathToRoot, slugTag } from "../util/path"
 import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
 
 function TagList({ fileData }: QuartzComponentProps) {
diff --git a/quartz/components/pages/FolderContent.tsx b/quartz/components/pages/FolderContent.tsx
index ea7ab5e..6037930 100644
--- a/quartz/components/pages/FolderContent.tsx
+++ b/quartz/components/pages/FolderContent.tsx
@@ -5,7 +5,7 @@
 
 import style from "../styles/listPage.scss"
 import { PageList } from "../PageList"
-import { canonicalizeServer } from "../../path"
+import { canonicalizeServer } from "../../util/path"
 
 function FolderContent(props: QuartzComponentProps) {
   const { tree, fileData, allFiles } = props
diff --git a/quartz/components/pages/TagContent.tsx b/quartz/components/pages/TagContent.tsx
index 5d30240..2cae40d 100644
--- a/quartz/components/pages/TagContent.tsx
+++ b/quartz/components/pages/TagContent.tsx
@@ -3,7 +3,7 @@
 import { toJsxRuntime } from "hast-util-to-jsx-runtime"
 import style from "../styles/listPage.scss"
 import { PageList } from "../PageList"
-import { ServerSlug, canonicalizeServer, getAllSegmentPrefixes } from "../../path"
+import { ServerSlug, canonicalizeServer, getAllSegmentPrefixes } from "../../util/path"
 import { QuartzPluginData } from "../../plugins/vfile"
 
 const numPages = 10
diff --git a/quartz/components/renderPage.tsx b/quartz/components/renderPage.tsx
index ab93709..171c414 100644
--- a/quartz/components/renderPage.tsx
+++ b/quartz/components/renderPage.tsx
@@ -2,8 +2,8 @@
 import { QuartzComponent, QuartzComponentProps } from "./types"
 import HeaderConstructor from "./Header"
 import BodyConstructor from "./Body"
-import { JSResourceToScriptElement, StaticResources } from "../resources"
-import { CanonicalSlug, pathToRoot } from "../path"
+import { JSResourceToScriptElement, StaticResources } from "../util/resources"
+import { CanonicalSlug, pathToRoot } from "../util/path"
 
 interface RenderComponents {
   head: QuartzComponent
diff --git a/quartz/components/scripts/graph.inline.ts b/quartz/components/scripts/graph.inline.ts
index f3f5cbd..0a7c19e 100644
--- a/quartz/components/scripts/graph.inline.ts
+++ b/quartz/components/scripts/graph.inline.ts
@@ -1,7 +1,7 @@
 import type { ContentDetails } from "../../plugins/emitters/contentIndex"
 import * as d3 from "d3"
 import { registerEscapeHandler, removeAllChildren } from "./util"
-import { CanonicalSlug, getCanonicalSlug, getClientSlug, resolveRelative } from "../../path"
+import { CanonicalSlug, getCanonicalSlug, getClientSlug, resolveRelative } from "../../util/path"
 
 type NodeData = {
   id: CanonicalSlug
diff --git a/quartz/components/scripts/search.inline.ts b/quartz/components/scripts/search.inline.ts
index a607fe2..5c7dae0 100644
--- a/quartz/components/scripts/search.inline.ts
+++ b/quartz/components/scripts/search.inline.ts
@@ -1,7 +1,7 @@
 import { Document } from "flexsearch"
 import { ContentDetails } from "../../plugins/emitters/contentIndex"
 import { registerEscapeHandler, removeAllChildren } from "./util"
-import { CanonicalSlug, getClientSlug, resolveRelative } from "../../path"
+import { CanonicalSlug, getClientSlug, resolveRelative } from "../../util/path"
 
 interface Item {
   id: number
diff --git a/quartz/components/scripts/spa.inline.ts b/quartz/components/scripts/spa.inline.ts
index 4d31b5e..7e450b7 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 "../../path"
+import { CanonicalSlug, RelativeURL, getCanonicalSlug } from "../../util/path"
 
 // adapted from `micromorph`
 // https://github.com/natemoo-re/micromorph
diff --git a/quartz/plugins/emitters/aliases.ts b/quartz/plugins/emitters/aliases.ts
index cf99d29..1fbea76 100644
--- a/quartz/plugins/emitters/aliases.ts
+++ b/quartz/plugins/emitters/aliases.ts
@@ -4,7 +4,7 @@
   ServerSlug,
   canonicalizeServer,
   resolveRelative,
-} from "../../path"
+} from "../../util/path"
 import { QuartzEmitterPlugin } from "../types"
 import path from "path"
 
diff --git a/quartz/plugins/emitters/assets.ts b/quartz/plugins/emitters/assets.ts
index 0f7f3e4..44bb718 100644
--- a/quartz/plugins/emitters/assets.ts
+++ b/quartz/plugins/emitters/assets.ts
@@ -1,8 +1,8 @@
-import { FilePath, joinSegments, slugifyFilePath } from "../../path"
+import { FilePath, joinSegments, slugifyFilePath } from "../../util/path"
 import { QuartzEmitterPlugin } from "../types"
 import path from "path"
 import fs from "fs"
-import { glob } from "../../glob"
+import { glob } from "../../util/glob"
 
 export const Assets: QuartzEmitterPlugin = () => {
   return {
diff --git a/quartz/plugins/emitters/componentResources.ts b/quartz/plugins/emitters/componentResources.ts
index 859109f..19eddc6 100644
--- a/quartz/plugins/emitters/componentResources.ts
+++ b/quartz/plugins/emitters/componentResources.ts
@@ -1,4 +1,4 @@
-import { FilePath, ServerSlug } from "../../path"
+import { FilePath, ServerSlug } from "../../util/path"
 import { QuartzEmitterPlugin } from "../types"
 
 // @ts-ignore
@@ -9,10 +9,10 @@
 import popoverScript from "../../components/scripts/popover.inline"
 import styles from "../../styles/base.scss"
 import popoverStyle from "../../components/styles/popover.scss"
-import { BuildCtx } from "../../ctx"
-import { StaticResources } from "../../resources"
+import { BuildCtx } from "../../util/ctx"
+import { StaticResources } from "../../util/resources"
 import { QuartzComponent } from "../../components/types"
-import { googleFontHref, joinStyles } from "../../theme"
+import { googleFontHref, joinStyles } from "../../util/theme"
 import { Features, transform } from "lightningcss"
 
 type ComponentResources = {
diff --git a/quartz/plugins/emitters/contentIndex.ts b/quartz/plugins/emitters/contentIndex.ts
index f3a0281..85cdfe7 100644
--- a/quartz/plugins/emitters/contentIndex.ts
+++ b/quartz/plugins/emitters/contentIndex.ts
@@ -1,5 +1,11 @@
 import { GlobalConfiguration } from "../../cfg"
-import { CanonicalSlug, ClientSlug, FilePath, ServerSlug, canonicalizeServer } from "../../path"
+import {
+  CanonicalSlug,
+  ClientSlug,
+  FilePath,
+  ServerSlug,
+  canonicalizeServer,
+} from "../../util/path"
 import { QuartzEmitterPlugin } from "../types"
 import path from "path"
 
diff --git a/quartz/plugins/emitters/contentPage.tsx b/quartz/plugins/emitters/contentPage.tsx
index dcf2829..b7e347a 100644
--- a/quartz/plugins/emitters/contentPage.tsx
+++ b/quartz/plugins/emitters/contentPage.tsx
@@ -4,7 +4,7 @@
 import BodyConstructor from "../../components/Body"
 import { pageResources, renderPage } from "../../components/renderPage"
 import { FullPageLayout } from "../../cfg"
-import { FilePath, canonicalizeServer } from "../../path"
+import { FilePath, canonicalizeServer } from "../../util/path"
 import { defaultContentPageLayout, sharedPageComponents } from "../../../quartz.layout"
 import { Content } from "../../components"
 
diff --git a/quartz/plugins/emitters/folderPage.tsx b/quartz/plugins/emitters/folderPage.tsx
index f5bc7a6..724717a 100644
--- a/quartz/plugins/emitters/folderPage.tsx
+++ b/quartz/plugins/emitters/folderPage.tsx
@@ -6,7 +6,13 @@
 import { ProcessedContent, defaultProcessedContent } from "../vfile"
 import { FullPageLayout } from "../../cfg"
 import path from "path"
-import { CanonicalSlug, FilePath, ServerSlug, canonicalizeServer, joinSegments } from "../../path"
+import {
+  CanonicalSlug,
+  FilePath,
+  ServerSlug,
+  canonicalizeServer,
+  joinSegments,
+} from "../../util/path"
 import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout"
 import { FolderContent } from "../../components"
 
diff --git a/quartz/plugins/emitters/static.ts b/quartz/plugins/emitters/static.ts
index 794cbac..6f5d19d 100644
--- a/quartz/plugins/emitters/static.ts
+++ b/quartz/plugins/emitters/static.ts
@@ -1,7 +1,7 @@
-import { FilePath, QUARTZ, joinSegments } from "../../path"
+import { FilePath, QUARTZ, joinSegments } from "../../util/path"
 import { QuartzEmitterPlugin } from "../types"
 import fs from "fs"
-import { glob } from "../../glob"
+import { glob } from "../../util/glob"
 
 export const Static: QuartzEmitterPlugin = () => ({
   name: "Static",
diff --git a/quartz/plugins/emitters/tagPage.tsx b/quartz/plugins/emitters/tagPage.tsx
index fd00d9c..de07623 100644
--- a/quartz/plugins/emitters/tagPage.tsx
+++ b/quartz/plugins/emitters/tagPage.tsx
@@ -11,7 +11,7 @@
   ServerSlug,
   getAllSegmentPrefixes,
   joinSegments,
-} from "../../path"
+} from "../../util/path"
 import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout"
 import { TagContent } from "../../components"
 
@@ -41,7 +41,7 @@
         allFiles.flatMap((data) => data.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes),
       )
       // add base tag
-      tags.add("")
+      tags.add("index")
 
       const tagDescriptions: Record<string, ProcessedContent> = Object.fromEntries(
         [...tags].map((tag) => {
diff --git a/quartz/plugins/index.ts b/quartz/plugins/index.ts
index 23440fb..c83455e 100644
--- a/quartz/plugins/index.ts
+++ b/quartz/plugins/index.ts
@@ -1,6 +1,6 @@
-import { StaticResources } from "../resources"
-import { FilePath, ServerSlug } from "../path"
-import { BuildCtx } from "../ctx"
+import { StaticResources } from "../util/resources"
+import { FilePath, ServerSlug } from "../util/path"
+import { BuildCtx } from "../util/ctx"
 
 export function getStaticResourcesFromPlugins(ctx: BuildCtx) {
   const staticResources: StaticResources = {
diff --git a/quartz/plugins/transformers/frontmatter.ts b/quartz/plugins/transformers/frontmatter.ts
index e09d20f..5b067f6 100644
--- a/quartz/plugins/transformers/frontmatter.ts
+++ b/quartz/plugins/transformers/frontmatter.ts
@@ -2,7 +2,7 @@
 import remarkFrontmatter from "remark-frontmatter"
 import { QuartzTransformerPlugin } from "../types"
 import yaml from "js-yaml"
-import { slugTag } from "../../path"
+import { slugTag } from "../../util/path"
 
 export interface Options {
   delims: string | string[]
diff --git a/quartz/plugins/transformers/links.ts b/quartz/plugins/transformers/links.ts
index 10b527c..a260719 100644
--- a/quartz/plugins/transformers/links.ts
+++ b/quartz/plugins/transformers/links.ts
@@ -8,7 +8,7 @@
   joinSegments,
   splitAnchor,
   transformLink,
-} from "../../path"
+} from "../../util/path"
 import path from "path"
 import { visit } from "unist-util-visit"
 import isAbsoluteUrl from "is-absolute-url"
diff --git a/quartz/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts
index ba3fb9e..97054b1 100644
--- a/quartz/plugins/transformers/ofm.ts
+++ b/quartz/plugins/transformers/ofm.ts
@@ -6,10 +6,10 @@
 import rehypeRaw from "rehype-raw"
 import { visit } from "unist-util-visit"
 import path from "path"
-import { JSResource } from "../../resources"
+import { JSResource } from "../../util/resources"
 // @ts-ignore
 import calloutScript from "../../components/scripts/callout.inline.ts"
-import { FilePath, canonicalizeServer, pathToRoot, slugTag, slugifyFilePath } from "../../path"
+import { FilePath, canonicalizeServer, 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"
@@ -294,7 +294,7 @@
               }
 
               const text = firstChild.children[0].value
-              const restChildren = firstChild.children.splice(1)
+              const restChildren = firstChild.children.slice(1)
               const [firstLine, ...remainingLines] = text.split("\n")
               const remainingText = remainingLines.join("\n")
 
diff --git a/quartz/plugins/types.ts b/quartz/plugins/types.ts
index 2662aed..ad1881b 100644
--- a/quartz/plugins/types.ts
+++ b/quartz/plugins/types.ts
@@ -1,9 +1,9 @@
 import { PluggableList } from "unified"
-import { StaticResources } from "../resources"
+import { StaticResources } from "../util/resources"
 import { ProcessedContent } from "./vfile"
 import { QuartzComponent } from "../components/types"
-import { FilePath, ServerSlug } from "../path"
-import { BuildCtx } from "../ctx"
+import { FilePath, ServerSlug } from "../util/path"
+import { BuildCtx } from "../util/ctx"
 
 export interface PluginTypes {
   transformers: QuartzTransformerPluginInstance[]
diff --git a/quartz/processors/emit.ts b/quartz/processors/emit.ts
index fd32685..3b357aa 100644
--- a/quartz/processors/emit.ts
+++ b/quartz/processors/emit.ts
@@ -1,13 +1,13 @@
 import path from "path"
 import fs from "fs"
-import { PerfTimer } from "../perf"
+import { PerfTimer } from "../util/perf"
 import { getStaticResourcesFromPlugins } from "../plugins"
 import { EmitCallback } from "../plugins/types"
 import { ProcessedContent } from "../plugins/vfile"
-import { FilePath, joinSegments } from "../path"
-import { QuartzLogger } from "../log"
-import { trace } from "../trace"
-import { BuildCtx } from "../ctx"
+import { FilePath, joinSegments } from "../util/path"
+import { QuartzLogger } from "../util/log"
+import { trace } from "../util/trace"
+import { BuildCtx } from "../util/ctx"
 
 export async function emitContent(ctx: BuildCtx, content: ProcessedContent[]) {
   const { argv, cfg } = ctx
diff --git a/quartz/processors/filter.ts b/quartz/processors/filter.ts
index dae6a3d..b269fb3 100644
--- a/quartz/processors/filter.ts
+++ b/quartz/processors/filter.ts
@@ -1,5 +1,5 @@
-import { BuildCtx } from "../ctx"
-import { PerfTimer } from "../perf"
+import { BuildCtx } from "../util/ctx"
+import { PerfTimer } from "../util/perf"
 import { ProcessedContent } from "../plugins/vfile"
 
 export function filterContent(ctx: BuildCtx, content: ProcessedContent[]): ProcessedContent[] {
diff --git a/quartz/processors/parse.ts b/quartz/processors/parse.ts
index 0015527..29f92fc 100644
--- a/quartz/processors/parse.ts
+++ b/quartz/processors/parse.ts
@@ -5,14 +5,14 @@
 import { Root as MDRoot } from "remark-parse/lib"
 import { Root as HTMLRoot } from "hast"
 import { ProcessedContent } from "../plugins/vfile"
-import { PerfTimer } from "../perf"
+import { PerfTimer } from "../util/perf"
 import { read } from "to-vfile"
-import { FilePath, QUARTZ, slugifyFilePath } from "../path"
+import { FilePath, QUARTZ, slugifyFilePath } from "../util/path"
 import path from "path"
 import workerpool, { Promise as WorkerPromise } from "workerpool"
-import { QuartzLogger } from "../log"
-import { trace } from "../trace"
-import { BuildCtx } from "../ctx"
+import { QuartzLogger } from "../util/log"
+import { trace } from "../util/trace"
+import { BuildCtx } from "../util/ctx"
 
 export type QuartzProcessor = Processor<MDRoot, HTMLRoot, void>
 export function createProcessor(ctx: BuildCtx): QuartzProcessor {
diff --git a/quartz/ctx.ts b/quartz/util/ctx.ts
similarity index 87%
rename from quartz/ctx.ts
rename to quartz/util/ctx.ts
index dad5cef..b127839 100644
--- a/quartz/ctx.ts
+++ b/quartz/util/ctx.ts
@@ -1,4 +1,4 @@
-import { QuartzConfig } from "./cfg"
+import { QuartzConfig } from "../cfg"
 import { ServerSlug } from "./path"
 
 export interface Argv {
diff --git a/quartz/glob.ts b/quartz/util/glob.ts
similarity index 100%
rename from quartz/glob.ts
rename to quartz/util/glob.ts
diff --git a/quartz/log.ts b/quartz/util/log.ts
similarity index 100%
rename from quartz/log.ts
rename to quartz/util/log.ts
diff --git a/quartz/path.test.ts b/quartz/util/path.test.ts
similarity index 96%
rename from quartz/path.test.ts
rename to quartz/util/path.test.ts
index d86bca5..5655585 100644
--- a/quartz/path.test.ts
+++ b/quartz/util/path.test.ts
@@ -53,8 +53,6 @@
     assert(!path.isRelativeURL("abc"))
     assert(!path.isRelativeURL("/abc/def"))
     assert(!path.isRelativeURL(""))
-    assert(!path.isRelativeURL("../"))
-    assert(!path.isRelativeURL("./"))
     assert(!path.isRelativeURL("./abc/def.html"))
     assert(!path.isRelativeURL("./abc/def.md"))
   })
@@ -160,17 +158,18 @@
       [
         ["", "."],
         [".", "."],
-        ["./", "."],
-        ["./index", "."],
-        ["./index.html", "."],
-        ["./index.md", "."],
+        ["./", "./"],
+        ["./index", "./"],
+        ["./index.html", "./"],
+        ["./index.md", "./"],
         ["content", "./content"],
         ["content/test.md", "./content/test"],
         ["./content/test.md", "./content/test"],
         ["../content/test.md", "../content/test"],
-        ["tags/", "./tags"],
-        ["/tags/", "./tags"],
+        ["tags/", "./tags/"],
+        ["/tags/", "./tags/"],
         ["content/with spaces", "./content/with-spaces"],
+        ["content/with spaces/index", "./content/with-spaces/"],
         ["content/with spaces#and Anchor!", "./content/with-spaces#and-anchor"],
       ],
       path.transformInternalLink,
@@ -269,16 +268,16 @@
     test("from a/b/c", () => {
       const cur = "a/b/c" as CanonicalSlug
       assert.strictEqual(path.transformLink(cur, "d", opts), "./d")
-      assert.strictEqual(path.transformLink(cur, "index", opts), ".")
-      assert.strictEqual(path.transformLink(cur, "../../index", opts), "../..")
-      assert.strictEqual(path.transformLink(cur, "../../", opts), "../..")
+      assert.strictEqual(path.transformLink(cur, "index", opts), "./")
+      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")
     })
 
     test("from a/b/index", () => {
       const cur = "a/b" as CanonicalSlug
-      assert.strictEqual(path.transformLink(cur, "../../index", opts), "../..")
-      assert.strictEqual(path.transformLink(cur, "../../", opts), "../..")
+      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")
       assert.strictEqual(path.transformLink(cur, "c", opts), "./c")
     })
@@ -286,7 +285,7 @@
     test("from index", () => {
       const cur = "" as CanonicalSlug
       assert.strictEqual(path.transformLink(cur, "e/g/h", opts), "./e/g/h")
-      assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "./a/b")
+      assert.strictEqual(path.transformLink(cur, "a/b/index", opts), "./a/b/")
     })
   })
 })
diff --git a/quartz/path.ts b/quartz/util/path.ts
similarity index 96%
rename from quartz/path.ts
rename to quartz/util/path.ts
index 8588c21..c1016af 100644
--- a/quartz/path.ts
+++ b/quartz/util/path.ts
@@ -71,7 +71,7 @@
 export type RelativeURL = SlugLike<"relative">
 export function isRelativeURL(s: string): s is RelativeURL {
   const validStart = /^\.{1,2}/.test(s)
-  const validEnding = !(s.endsWith("/") || s.endsWith("/index") || s === "index")
+  const validEnding = !(s.endsWith("/index") || s === "index")
   return validStart && validEnding && !_hasFileExtension(s)
 }
 
@@ -133,6 +133,12 @@
 
 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("/")
   let segments = fplike.split("/").filter((x) => x.length > 0)
   let prefix = segments.filter(_isRelativeSegment).join("/")
   let fp = segments.filter((seg) => !_isRelativeSegment(seg)).join("/")
@@ -143,14 +149,13 @@
   }
 
   fp = canonicalizeServer(slugifyFilePath(fp as FilePath))
-  fp = _trimSuffix(fp, "index")
-
-  let joined = joinSegments(_stripSlashes(prefix), _stripSlashes(fp))
-  const res = (_addRelativeToStart(joined) + anchor) as RelativeURL
+  const joined = joinSegments(_stripSlashes(prefix), _stripSlashes(fp))
+  const trail = folderPath ? "/" : ""
+  const res = (_addRelativeToStart(joined) + anchor + trail) as RelativeURL
   return res
 }
 
-// resolve /a/b/c to ../../
+// resolve /a/b/c to ../../..
 export function pathToRoot(slug: CanonicalSlug): RelativeURL {
   let rootPath = slug
     .split("/")
diff --git a/quartz/perf.ts b/quartz/util/perf.ts
similarity index 100%
rename from quartz/perf.ts
rename to quartz/util/perf.ts
diff --git a/quartz/resources.tsx b/quartz/util/resources.tsx
similarity index 100%
rename from quartz/resources.tsx
rename to quartz/util/resources.tsx
diff --git a/quartz/sourcemap.ts b/quartz/util/sourcemap.ts
similarity index 100%
rename from quartz/sourcemap.ts
rename to quartz/util/sourcemap.ts
diff --git a/quartz/theme.ts b/quartz/util/theme.ts
similarity index 100%
rename from quartz/theme.ts
rename to quartz/util/theme.ts
diff --git a/quartz/trace.ts b/quartz/util/trace.ts
similarity index 100%
rename from quartz/trace.ts
rename to quartz/util/trace.ts
diff --git a/quartz/worker.ts b/quartz/worker.ts
index db11cbb..d42bc25 100644
--- a/quartz/worker.ts
+++ b/quartz/worker.ts
@@ -1,7 +1,7 @@
 import sourceMapSupport from "source-map-support"
 sourceMapSupport.install(options)
 import cfg from "../quartz.config"
-import { Argv, BuildCtx } from "./ctx"
+import { Argv, BuildCtx } from "./util/ctx"
 import { FilePath, ServerSlug } from "./path"
 import { createFileParser, createProcessor } from "./processors/parse"
 import { options } from "./sourcemap"

--
Gitblit v1.10.0