Skip to content

Commit

Permalink
base path refactor to better support subpath hosting
Browse files Browse the repository at this point in the history
  • Loading branch information
jackyzha0 committed Aug 19, 2023
1 parent 3201f83 commit c874e7e
Show file tree
Hide file tree
Showing 29 changed files with 257 additions and 389 deletions.
32 changes: 14 additions & 18 deletions content/advanced/paths.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@ 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.

Luckily, we can mimic nominal typing using [brands](https://www.typescriptlang.org/play#example/nominal-typing).

```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.
Expand All @@ -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`.
2 changes: 1 addition & 1 deletion content/hosting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down
2 changes: 1 addition & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 <[email protected]>",
"license": "MIT",
Expand Down
6 changes: 3 additions & 3 deletions quartz/components/Backlinks.tsx
Original file line number Diff line number Diff line change
@@ -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">
Expand All @@ -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>
Expand Down
5 changes: 2 additions & 3 deletions quartz/components/Head.tsx
Original file line number Diff line number Diff line change
@@ -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`

Expand Down
8 changes: 3 additions & 5 deletions quartz/components/PageList.tsx
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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)
Expand All @@ -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 (
Expand All @@ -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>
Expand All @@ -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>
Expand Down
5 changes: 2 additions & 3 deletions quartz/components/PageTitle.tsx
Original file line number Diff line number Diff line change
@@ -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>
Expand Down
5 changes: 2 additions & 3 deletions quartz/components/TagList.tsx
Original file line number Diff line number Diff line change
@@ -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">
Expand Down
6 changes: 3 additions & 3 deletions quartz/components/pages/FolderContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions quartz/components/pages/TagContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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),
Expand Down
19 changes: 10 additions & 9 deletions quartz/components/renderPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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",
Expand All @@ -46,7 +47,7 @@ export function pageResources(
}

export function renderPage(
slug: CanonicalSlug,
slug: FullSlug,
componentData: QuartzComponentProps,
components: RenderComponents,
pageResources: StaticResources,
Expand Down
34 changes: 18 additions & 16 deletions quartz/components/scripts/graph.inline.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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[] } = {
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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")
Expand Down
Loading

0 comments on commit c874e7e

Please sign in to comment.