From ba9f243728cab171f86b40b9d50db485af272a39 Mon Sep 17 00:00:00 2001
From: Jacky Zhao <j.zhao2k19@gmail.com>
Date: Sat, 01 Jul 2023 07:03:01 +0000
Subject: [PATCH] tag and folder pages

---
 quartz/components/Footer.tsx              |   27 ++
 quartz/components/index.ts                |   10 
 quartz/components/PageList.tsx            |   53 ++++
 quartz/cfg.ts                             |   15 +
 quartz/components/styles/graph.scss       |    1 
 quartz/components/styles/listPage.scss    |   36 +++
 quartz/components/scripts/graph.inline.ts |   26 ++
 quartz/build.ts                           |   11 
 quartz/plugins/index.ts                   |    9 
 quartz/plugins/vfile.ts                   |   11 
 quartz/components/pages/Content.tsx       |   11 
 quartz/plugins/emitters/tagPage.tsx       |   74 ++++++
 quartz/components/renderPage.tsx          |   63 +++++
 quartz.config.ts                          |   79 ++++--
 quartz/styles/base.scss                   |    3 
 quartz/components/styles/search.scss      |    1 
 quartz/components/Date.tsx                |   12 +
 /dev/null                                 |   31 --
 quartz/plugins/emitters/folderPage.tsx    |   77 ++++++
 quartz/components/pages/TagContent.tsx    |   33 ++
 quartz/components/styles/popover.scss     |    1 
 quartz/plugins/emitters/index.ts          |    2 
 quartz/plugins/emitters/contentPage.tsx   |   73 +----
 quartz/components/pages/FolderContent.tsx |   37 +++
 quartz/components/styles/footer.scss      |   13 +
 25 files changed, 586 insertions(+), 123 deletions(-)

diff --git a/quartz.config.ts b/quartz.config.ts
index bd7a81d..0f2ca8d 100644
--- a/quartz.config.ts
+++ b/quartz.config.ts
@@ -1,7 +1,46 @@
-import { QuartzConfig } from "./quartz/cfg"
+import { PageLayout, QuartzConfig } from "./quartz/cfg"
 import * as Component from "./quartz/components"
 import * as Plugin from "./quartz/plugins"
 
+const sharedPageComponents = {
+  head: Component.Head(),
+  header: [
+    Component.PageTitle({ title: "🪴 Quartz 4.0" }),
+    Component.Spacer(),
+    Component.Search(),
+    Component.Darkmode()
+  ],
+  footer: Component.Footer({
+    authorName: "Jacky",
+    links: {
+      "GitHub": "https://github.com/jackyzha0",
+      "Twitter": "https://twitter.com/_jzhao"
+    }
+  })
+}
+
+const contentPageLayout: PageLayout = {
+  beforeBody: [
+    Component.ArticleTitle(),
+    Component.ReadingTime(),
+    Component.TagList(),
+  ],
+  left: [],
+  right: [
+    Component.Graph(),
+    Component.TableOfContents(),
+    Component.Backlinks()
+  ],
+}
+
+const listPageLayout: PageLayout = {
+  beforeBody: [
+    Component.ArticleTitle()
+  ],
+  left: [],
+  right: [],
+}
+
 const config: QuartzConfig = {
   configuration: {
     enableSPA: true,
@@ -56,30 +95,22 @@
     emitters: [
       Plugin.AliasRedirects(),
       Plugin.ContentPage({
-        head: Component.Head(),
-        header: [
-          Component.PageTitle({ title: "🪴 Quartz 4.0" }),
-          Component.Spacer(),
-          Component.Search(),
-          Component.Darkmode()
-        ],
-        beforeBody: [
-          Component.ArticleTitle(),
-          Component.ReadingTime(),
-          Component.TagList(),
-        ],
-        content: Component.Content(),
-        left: [
-        ],
-        right: [
-          Component.Graph(),
-          Component.TableOfContents(),
-          Component.Backlinks()
-        ],
-        footer: []
+        ...sharedPageComponents,
+        ...contentPageLayout,
+        pageBody: Component.Content(),
       }),
-      Plugin.ContentIndex(), // you can exclude this if you don't plan on using popovers, graph, or backlinks,
-      Plugin.CNAME({ domain: "yoursite.xyz" }) // set this to your final deployed domain
+      Plugin.TagPage({
+        ...sharedPageComponents,
+        ...listPageLayout,
+        pageBody: Component.TagContent(),
+      }),
+      Plugin.FolderPage({
+        ...sharedPageComponents,
+        ...listPageLayout,
+        pageBody: Component.FolderContent(),
+      }),
+      Plugin.ContentIndex(), // you can exclude this if you don't plan on using popovers, graph view, or backlinks
+      Plugin.CNAME({ domain: "quartz.jzhao.xyz" }) // set this to your final deployed domain
     ]
   },
 }
diff --git a/quartz/build.ts b/quartz/build.ts
index b96bf01..45595e7 100644
--- a/quartz/build.ts
+++ b/quartz/build.ts
@@ -57,11 +57,18 @@
 
   if (argv.serve) {
     const server = http.createServer(async (req, res) => {
-      console.log(chalk.grey(`[req] ${req.url}`))
-      return serveHandler(req, res, {
+      let status = 200
+      const result = await serveHandler(req, res, {
         public: output,
         directoryListing: false,
+      }, {
+        async sendError() {
+          status = 404
+        },
       })
+      const statusString = status === 200 ? chalk.green(`[${status}]`) : chalk.red(`[${status}]`)
+      console.log(statusString + chalk.grey(` ${req.url}`))
+      return result
     })
     server.listen(argv.port)
     console.log(`Started a Quartz server listening at http://localhost:${argv.port}`)
diff --git a/quartz/cfg.ts b/quartz/cfg.ts
index e1cf3af..bb097c9 100644
--- a/quartz/cfg.ts
+++ b/quartz/cfg.ts
@@ -1,9 +1,12 @@
+import { QuartzComponent } from "./components/types"
 import { PluginTypes } from "./plugins/types"
 import { Theme } from "./theme"
 
 export interface GlobalConfiguration {
   /** Whether to enable single-page-app style rendering. this prevents flashes of unstyled content and improves smoothness of Quartz */
   enableSPA: boolean,
+  /** Whether to display Wikipedia-style popovers when hovering over links */
+  enablePopovers: boolean,
   /** Glob patterns to not search */
   ignorePatterns: string[],
   theme: Theme
@@ -13,3 +16,15 @@
   configuration: GlobalConfiguration,
   plugins: PluginTypes,
 }
+
+export interface FullPageLayout {
+  head: QuartzComponent
+  header: QuartzComponent[],
+  beforeBody: QuartzComponent[],
+  pageBody: QuartzComponent,
+  left: QuartzComponent[],
+  right: QuartzComponent[],
+  footer: QuartzComponent,
+}
+
+export type PageLayout = Pick<FullPageLayout, "beforeBody" | "left" | "right">
diff --git a/quartz/components/Content.tsx b/quartz/components/Content.tsx
deleted file mode 100644
index 0bcab1e..0000000
--- a/quartz/components/Content.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
-import { Fragment, jsx, jsxs } from 'preact/jsx-runtime'
-import { toJsxRuntime } from "hast-util-to-jsx-runtime"
-
-// @ts-ignore
-import popoverScript from './scripts/popover.inline'
-import popoverStyle from './styles/popover.scss'
-
-interface Options {
-  enablePopover: boolean
-}
-
-const defaultOptions: Options = {
-  enablePopover: true
-}
-
-export default ((opts?: Partial<Options>) => {
-  function Content({ tree }: QuartzComponentProps) {
-    // @ts-ignore (preact makes it angry)
-    const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' })
-    return <article>{content}</article>
-  }
-
-  const enablePopover = opts?.enablePopover ?? defaultOptions.enablePopover
-  if (enablePopover) {
-    Content.afterDOMLoaded = popoverScript
-    Content.css = popoverStyle
-  }
-
-  return Content
-}) satisfies QuartzComponentConstructor
diff --git a/quartz/components/Date.tsx b/quartz/components/Date.tsx
new file mode 100644
index 0000000..7ea6ad4
--- /dev/null
+++ b/quartz/components/Date.tsx
@@ -0,0 +1,12 @@
+interface Props {
+  date: Date
+}
+
+export function Date({ date }: Props) {
+  const formattedDate = date.toLocaleDateString('en-US', {
+    year: "numeric",
+    month: "short",
+    day: '2-digit'
+  })
+  return <>{formattedDate}</>
+}
diff --git a/quartz/components/Footer.tsx b/quartz/components/Footer.tsx
new file mode 100644
index 0000000..4229f9d
--- /dev/null
+++ b/quartz/components/Footer.tsx
@@ -0,0 +1,27 @@
+import { QuartzComponentConstructor } from "./types"
+import style from "./styles/footer.scss"
+
+interface Options {
+  authorName: string,
+  links: Record<string, string>
+}
+
+export default ((opts?: Options) => {
+  function Footer() {
+    const year = new Date().getFullYear()
+    const name = opts?.authorName ?? "someone"
+    const links = opts?.links ?? []
+    return <>
+      <hr />
+      <footer>
+        <p>Made by {name} using <a>Quartz</a>, © {year}</p>
+        <ul>{Object.entries(links).map(([text, link]) => <li>
+          <a href={link}>{text}</a>
+        </li>)}</ul>
+      </footer>
+    </>
+  }
+
+  Footer.css = style
+  return Footer
+}) satisfies QuartzComponentConstructor
diff --git a/quartz/components/PageList.tsx b/quartz/components/PageList.tsx
new file mode 100644
index 0000000..e5d8dfb
--- /dev/null
+++ b/quartz/components/PageList.tsx
@@ -0,0 +1,53 @@
+import { relativeToRoot } from "../path"
+import { QuartzPluginData } from "../plugins/vfile"
+import { Date } from "./Date"
+import { stripIndex } from "./scripts/util"
+import { QuartzComponentProps } from "./types"
+
+function byDateAndAlphabetical(f1: QuartzPluginData, f2: QuartzPluginData): number {
+  if (f1.dates && f2.dates) {
+    // sort descending by last modified
+    return f2.dates.modified.getTime() - f1.dates.modified.getTime()
+  } else if (f1.dates && !f2.dates) {
+    // prioritize files with dates
+    return -1
+  } else if (!f1.dates && f2.dates) {
+    return 1
+  }
+
+  // otherwise, sort lexographically by title
+  const f1Title = f1.frontmatter?.title.toLowerCase() ?? ""
+  const f2Title = f2.frontmatter?.title.toLowerCase() ?? ""
+  return f1Title.localeCompare(f2Title) 
+}
+
+export function PageList({ fileData, allFiles }: QuartzComponentProps) {
+  const slug = fileData.slug!
+  return <ul class="section-ul">
+    {allFiles.sort(byDateAndAlphabetical).map(page => {
+      const title = page.frontmatter?.title
+      const pageSlug = page.slug!
+      const tags = page.frontmatter?.tags ?? []
+      return <li class="section-li">
+        <div class="section">
+          {page.dates && <p class="meta">
+            <Date date={page.dates.modified} />
+          </p>}
+          <div class="desc">
+            <h3><a href={stripIndex(relativeToRoot(slug, pageSlug))} class="internal">{title}</a></h3>
+          </div>
+          <div class="spacer"></div>
+          <ul class="tags">
+            {tags.map(tag => <li><a href={relativeToRoot(slug, `tags/${tag}`)}>#{tag}</a></li>)}
+          </ul>
+        </div>
+      </li>
+    })}
+  </ul>
+}
+
+PageList.css = `
+.section h3 {
+  margin: 0;
+}
+`
diff --git a/quartz/components/index.ts b/quartz/components/index.ts
index 61df101..ed0c668 100644
--- a/quartz/components/index.ts
+++ b/quartz/components/index.ts
@@ -1,5 +1,7 @@
 import ArticleTitle from "./ArticleTitle"
-import Content from "./Content"
+import Content from "./pages/Content"
+import TagContent from "./pages/TagContent"
+import FolderContent from "./pages/FolderContent"
 import Darkmode from "./Darkmode"
 import Head from "./Head"
 import PageTitle from "./PageTitle"
@@ -10,10 +12,13 @@
 import Graph from "./Graph" 
 import Backlinks from "./Backlinks"
 import Search from "./Search"
+import Footer from "./Footer"
 
 export {
   ArticleTitle,
   Content,
+  TagContent,
+  FolderContent,
   Darkmode,
   Head,
   PageTitle,
@@ -23,5 +28,6 @@
   TagList,
   Graph,
   Backlinks,
-  Search
+  Search,
+  Footer
 } 
diff --git a/quartz/components/pages/Content.tsx b/quartz/components/pages/Content.tsx
new file mode 100644
index 0000000..7856d6e
--- /dev/null
+++ b/quartz/components/pages/Content.tsx
@@ -0,0 +1,11 @@
+import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
+import { Fragment, jsx, jsxs } from 'preact/jsx-runtime'
+import { toJsxRuntime } from "hast-util-to-jsx-runtime"
+
+function Content({ tree }: QuartzComponentProps) {
+  // @ts-ignore (preact makes it angry)
+  const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' })
+  return <article>{content}</article>
+}
+
+export default (() => Content) satisfies QuartzComponentConstructor
\ No newline at end of file
diff --git a/quartz/components/pages/FolderContent.tsx b/quartz/components/pages/FolderContent.tsx
new file mode 100644
index 0000000..4806843
--- /dev/null
+++ b/quartz/components/pages/FolderContent.tsx
@@ -0,0 +1,37 @@
+import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
+import { Fragment, jsx, jsxs } from 'preact/jsx-runtime'
+import { toJsxRuntime } from "hast-util-to-jsx-runtime"
+import path from "path"
+
+import style from '../styles/listPage.scss'
+import { PageList } from "../PageList"
+
+function TagContent(props: QuartzComponentProps) {
+  const { tree, fileData, allFiles } = props
+  const folderSlug = fileData.slug!
+  const allPagesInFolder = allFiles.filter(file => {
+    const fileSlug = file.slug ?? ""
+    const prefixed = fileSlug.startsWith(folderSlug)
+    const folderParts = folderSlug.split(path.posix.sep)
+    const fileParts = fileSlug.split(path.posix.sep)
+    const isDirectChild = fileParts.length === folderParts.length + 1
+    return prefixed && isDirectChild
+  })
+
+  const listProps = {
+    ...props,
+    allFiles: allPagesInFolder
+  }
+
+  // @ts-ignore
+  const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' })
+  return <div>
+    <article>{content}</article>
+    <div>
+      <PageList {...listProps} /> 
+    </div>
+  </div>
+}
+
+TagContent.css = style + PageList.css
+export default (() => TagContent) satisfies QuartzComponentConstructor
diff --git a/quartz/components/pages/TagContent.tsx b/quartz/components/pages/TagContent.tsx
new file mode 100644
index 0000000..e7e5f6d
--- /dev/null
+++ b/quartz/components/pages/TagContent.tsx
@@ -0,0 +1,33 @@
+import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
+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"
+
+function TagContent(props: QuartzComponentProps) {
+  const { tree, fileData, allFiles } = props
+  const slug = fileData.slug
+  if (slug?.startsWith("tags/")) {
+    const tag = slug.slice("tags/".length)
+
+    const allPagesWithTag = allFiles.filter(file => (file.frontmatter?.tags ?? []).includes(tag))
+    const listProps = {
+      ...props,
+      allFiles: allPagesWithTag
+    }
+
+    // @ts-ignore
+    const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' })
+    return <div>
+      <article>{content}</article>
+      <div>
+        <PageList {...listProps} />
+      </div>
+    </div>
+  } else {
+    throw `Component "TagContent" tried to render a non-tag page: ${slug}`
+  }
+}
+
+TagContent.css = style + PageList.css
+export default (() => TagContent) satisfies QuartzComponentConstructor
diff --git a/quartz/components/renderPage.tsx b/quartz/components/renderPage.tsx
new file mode 100644
index 0000000..0e0f4c0
--- /dev/null
+++ b/quartz/components/renderPage.tsx
@@ -0,0 +1,63 @@
+import { render } from "preact-render-to-string";
+import { QuartzComponent, QuartzComponentProps } from "./types";
+import HeaderConstructor from "./Header"
+import BodyConstructor from "./Body"
+import { JSResourceToScriptElement, StaticResources } from "../resources";
+import { resolveToRoot } from "../path";
+
+interface RenderComponents {
+  head: QuartzComponent
+  header: QuartzComponent[],
+  beforeBody: QuartzComponent[],
+  pageBody: QuartzComponent,
+  left: QuartzComponent[],
+  right: QuartzComponent[],
+  footer: QuartzComponent,
+}
+
+export function pageResources(slug: string, staticResources: StaticResources): StaticResources {
+  const baseDir = resolveToRoot(slug)
+  return {
+    css: [baseDir + "/index.css", ...staticResources.css],
+    js: [
+      { src: baseDir + "/prescript.js", loadTime: "beforeDOMReady", contentType: "external" },
+      ...staticResources.js,
+      { src: baseDir + "/postscript.js", loadTime: "afterDOMReady", moduleType: 'module', contentType: "external" }
+    ]
+  }
+}
+
+export function renderPage(slug: string, componentData: QuartzComponentProps, components: RenderComponents, pageResources: StaticResources): string {
+  const { head: Head, header, beforeBody, pageBody: Content, left, right, footer: Footer } = components
+  const Header = HeaderConstructor()
+  const Body = BodyConstructor()
+
+  const doc = <html>
+    <Head {...componentData} />
+    <body data-slug={slug}>
+      <div id="quartz-root" class="page">
+        <Header {...componentData} >
+          {header.map(HeaderComponent => <HeaderComponent {...componentData} />)}
+        </Header>
+        <div class="popover-hint">
+          {beforeBody.map(BodyComponent => <BodyComponent {...componentData} />)}
+        </div>
+        <Body {...componentData}>
+          <div class="left">
+            {left.map(BodyComponent => <BodyComponent {...componentData} />)}
+          </div>
+          <div class="center popover-hint">
+            <Content {...componentData} />
+          </div>
+          <div class="right">
+            {right.map(BodyComponent => <BodyComponent {...componentData} />)}
+          </div>
+        </Body>
+        <Footer {...componentData} />
+      </div>
+    </body>
+    {pageResources.js.filter(resource => resource.loadTime === "afterDOMReady").map(res => JSResourceToScriptElement(res))}
+  </html>
+
+  return "<!DOCTYPE html>\n" + render(doc)
+}
diff --git a/quartz/components/scripts/graph.inline.ts b/quartz/components/scripts/graph.inline.ts
index 4ff2dfe..27e9a81 100644
--- a/quartz/components/scripts/graph.inline.ts
+++ b/quartz/components/scripts/graph.inline.ts
@@ -13,7 +13,19 @@
   target: string
 }
 
+const localStorageKey = "graph-visited"
+function getVisited(): Set<string> {
+  return new Set(JSON.parse(localStorage.getItem(localStorageKey) ?? "[]"))
+}
+
+function addToVisited(slug: string) {
+  const visited = getVisited()
+  visited.add(slug)
+  localStorage.setItem(localStorageKey, JSON.stringify([...visited]))
+}
+
 async function renderGraph(container: string, slug: string) {
+  const visited = getVisited()
   const graph = document.getElementById(container)
   if (!graph) return
   removeAllChildren(graph)
@@ -106,7 +118,13 @@
   // calculate radius
   const color = (d: NodeData) => {
     const isCurrent = d.id === slug
-    return isCurrent ? "var(--secondary)" : "var(--gray)"
+    if (isCurrent) {
+      return "var(--secondary)"
+    } else if (visited.has(d.id)) {
+      return "var(--tertiary)"
+    } else {
+      return "var(--gray)"
+    }
   }
 
   const drag = (simulation: d3.Simulation<NodeData, LinkData>) => {
@@ -267,9 +285,15 @@
 
 document.addEventListener("nav", async (e: unknown) => {
   const slug = (e as CustomEventMap["nav"]).detail.url
+  addToVisited(slug)
   await renderGraph("graph-container", slug)
 
   const containerIcon = document.getElementById("global-graph-icon")
   containerIcon?.removeEventListener("click", renderGlobalGraph)
   containerIcon?.addEventListener("click", renderGlobalGraph)
 })
+
+window.addEventListener('resize', async () => {
+  const slug = document.body.dataset["slug"]!
+  await renderGraph("graph-container", slug)
+})
diff --git a/quartz/components/styles/footer.scss b/quartz/components/styles/footer.scss
new file mode 100644
index 0000000..d104e50
--- /dev/null
+++ b/quartz/components/styles/footer.scss
@@ -0,0 +1,13 @@
+footer {
+  text-align: left;
+  opacity: 0.8;
+  & ul {
+    list-style: none;
+    margin: 0;
+    padding: 0;
+    display: flex;
+    flex-direction: row;
+    gap: 1rem;
+    margin-top: -1rem;
+  }
+}
diff --git a/quartz/components/styles/graph.scss b/quartz/components/styles/graph.scss
index 76323bb..244f2e4 100644
--- a/quartz/components/styles/graph.scss
+++ b/quartz/components/styles/graph.scss
@@ -9,7 +9,6 @@
     border: 1px solid var(--lightgray);
     box-sizing: border-box;
     height: 250px;
-    width: 300px;
     margin: 0.5em 0;
     position: relative;
 
diff --git a/quartz/components/styles/listPage.scss b/quartz/components/styles/listPage.scss
new file mode 100644
index 0000000..a5d0a91
--- /dev/null
+++ b/quartz/components/styles/listPage.scss
@@ -0,0 +1,36 @@
+ul.section-ul {
+  list-style: none;
+  margin-top: 2em;
+  padding-left: 0;
+}
+
+li.section-li {
+  margin-bottom: 1em;
+
+  & > .section {
+    display: flex;
+    align-items: center;
+
+    @media all and (max-width: 600px) {
+      & .tags {
+        display: none;
+      }
+    }
+
+    & h3 > a {
+      font-weight: 700;
+      margin: 0;
+      background-color: transparent;
+    }
+
+    & p {
+      margin: 0;
+      padding-right: 1em;
+      flex-basis: 6em;
+    }
+  }
+
+  & .meta {
+    opacity: 0.6;
+  }
+}
diff --git a/quartz/components/styles/popover.scss b/quartz/components/styles/popover.scss
index f95dc7b..5ae09fe 100644
--- a/quartz/components/styles/popover.scss
+++ b/quartz/components/styles/popover.scss
@@ -26,6 +26,7 @@
     font-weight: initial;
     line-height: initial;
     font-size: initial;
+    font-family: var(--bodyFont);
     border: 1px solid var(--gray);
     background-color: var(--light);
     border-radius: 5px;
diff --git a/quartz/components/styles/search.scss b/quartz/components/styles/search.scss
index bac584b..32d5744 100644
--- a/quartz/components/styles/search.scss
+++ b/quartz/components/styles/search.scss
@@ -102,6 +102,7 @@
 
           & .highlight {
             color: var(--secondary);
+            font-weight: 700;
           }
 
           &:hover, &:focus {
diff --git a/quartz/plugins/emitters/contentPage.tsx b/quartz/plugins/emitters/contentPage.tsx
index 03bc0ff..576821a 100644
--- a/quartz/plugins/emitters/contentPage.tsx
+++ b/quartz/plugins/emitters/contentPage.tsx
@@ -1,90 +1,49 @@
-import { JSResourceToScriptElement, StaticResources } from "../../resources"
 import { QuartzEmitterPlugin } from "../types"
-import { render } from "preact-render-to-string"
-import { QuartzComponent } from "../../components/types"
-import { resolveToRoot, trimPathSuffix } from "../../path"
-import HeaderConstructor from "../../components/Header"
 import { QuartzComponentProps } from "../../components/types"
+import HeaderConstructor from "../../components/Header"
 import BodyConstructor from "../../components/Body"
+import { pageResources, renderPage } from "../../components/renderPage"
+import { FullPageLayout } from "../../cfg"
 
-interface Options {
-  head: QuartzComponent
-  header: QuartzComponent[],
-  beforeBody: QuartzComponent[],
-  content: QuartzComponent,
-  left: QuartzComponent[],
-  right: QuartzComponent[],
-  footer: QuartzComponent[],
-}
-
-export const ContentPage: QuartzEmitterPlugin<Options> = (opts) => {
+export const ContentPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => {
   if (!opts) {
     throw new Error("ContentPage must be initialized with options specifiying the components to use")
   }
 
-  const { head: Head, header, beforeBody, left, right, footer } = opts
+  const { head: Head, header, beforeBody, pageBody: Content, left, right, footer: Footer } = opts
   const Header = HeaderConstructor()
   const Body = BodyConstructor()
 
   return {
     name: "ContentPage",
     getQuartzComponents() {
-      return [opts.head, Header, Body, ...opts.header, ...opts.beforeBody, opts.content, ...opts.left, ...opts.right, ...opts.footer]
+      return [Head, Header, Body, ...header, ...beforeBody, Content, ...left, ...right, Footer]
     },
     async emit(_contentDir, cfg, content, resources, emit): Promise<string[]> {
       const fps: string[] = []
       const allFiles = content.map(c => c[1].data)
       for (const [tree, file] of content) {
-        const baseDir = resolveToRoot(file.data.slug!)
-        const pageResources: StaticResources = {
-          css: [baseDir + "/index.css", ...resources.css],
-          js: [
-            { src: baseDir + "/prescript.js", loadTime: "beforeDOMReady", contentType: "external" },
-            ...resources.js,
-            { src: baseDir + "/postscript.js", loadTime: "afterDOMReady", moduleType: 'module', contentType: "external" }
-          ]
-        }
-
+        const slug = file.data.slug!
+        const externalResources = pageResources(slug, resources)
         const componentData: QuartzComponentProps = {
           fileData: file.data,
-          externalResources: pageResources,
+          externalResources,
           cfg,
           children: [],
           tree,
           allFiles
         }
 
-        const Content = opts.content
-        const doc = <html>
-          <Head {...componentData} />
-          <body data-slug={file.data.slug ?? ""}>
-            <div id="quartz-root" class="page">
-              <Header {...componentData} >
-                {header.map(HeaderComponent => <HeaderComponent {...componentData} />)}
-              </Header>
-              <div class="popover-hint">
-                {beforeBody.map(BodyComponent => <BodyComponent {...componentData} />)}
-              </div>
-              <Body {...componentData}>
-                <div class="left">
-                  {left.map(BodyComponent => <BodyComponent {...componentData} />)}
-                </div>
-                <div class="center popover-hint">
-                  <Content {...componentData} />
-                </div>
-                <div class="right">
-                  {right.map(BodyComponent => <BodyComponent {...componentData} />)}
-                </div>
-              </Body>
-
-            </div>
-          </body>
-          {pageResources.js.filter(resource => resource.loadTime === "afterDOMReady").map(res => JSResourceToScriptElement(res))}
-        </html>
+        const content = renderPage(
+          slug,
+          componentData,
+          opts,
+          externalResources
+        )
 
         const fp = file.data.slug + ".html"
         await emit({
-          content: "<!DOCTYPE html>\n" + render(doc),
+          content,
           slug: file.data.slug!,
           ext: ".html",
         })
diff --git a/quartz/plugins/emitters/folderPage.tsx b/quartz/plugins/emitters/folderPage.tsx
new file mode 100644
index 0000000..ee8f0b9
--- /dev/null
+++ b/quartz/plugins/emitters/folderPage.tsx
@@ -0,0 +1,77 @@
+import { QuartzEmitterPlugin } from "../types"
+import { QuartzComponentProps } from "../../components/types"
+import HeaderConstructor from "../../components/Header"
+import BodyConstructor from "../../components/Body"
+import { pageResources, renderPage } from "../../components/renderPage"
+import { ProcessedContent, defaultProcessedContent } from "../vfile"
+import { FullPageLayout } from "../../cfg"
+import path from "path"
+
+export const FolderPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => {
+  if (!opts) {
+    throw new Error("ErrorPage must be initialized with options specifiying the components to use")
+  }
+
+  const { head: Head, header, beforeBody, pageBody: Content, left, right, footer: Footer } = opts
+  const Header = HeaderConstructor()
+  const Body = BodyConstructor()
+
+  return {
+    name: "FolderPage",
+    getQuartzComponents() {
+      return [Head, Header, Body, ...header, ...beforeBody, Content, ...left, ...right, Footer]
+    },
+    async emit(_contentDir, cfg, content, resources, emit): Promise<string[]> {
+      const fps: string[] = []
+      const allFiles = content.map(c => c[1].data)
+
+      const folders: Set<string> = new Set(allFiles.flatMap(data => data.slug ? [path.dirname(data.slug)] : []))
+
+      // remove special prefixes
+      folders.delete(".")
+      folders.delete("tags")
+
+      const folderDescriptions: Record<string, ProcessedContent> = Object.fromEntries([...folders].map(folder => ([
+        folder, defaultProcessedContent({ slug: folder, frontmatter: { title: `Folder: ${folder}`, tags: [] } })
+      ])))
+
+      for (const [tree, file] of content) {
+        const slug = file.data.slug!
+        if (folders.has(slug)) {
+          folderDescriptions[slug] = [tree, file]
+        }
+      }
+
+      for (const folder of folders) {
+        const slug = folder 
+        const externalResources = pageResources(slug, resources)
+        const [tree, file] = folderDescriptions[folder]
+        const componentData: QuartzComponentProps = {
+          fileData: file.data,
+          externalResources,
+          cfg,
+          children: [],
+          tree,
+          allFiles
+        }
+
+        const content = renderPage(
+          slug,
+          componentData,
+          opts,
+          externalResources
+        )
+
+        const fp = file.data.slug + ".html"
+        await emit({
+          content,
+          slug: file.data.slug!,
+          ext: ".html",
+        })
+
+        fps.push(fp)
+      }
+      return fps
+    }
+  }
+}
diff --git a/quartz/plugins/emitters/index.ts b/quartz/plugins/emitters/index.ts
index 971bf19..ff684d5 100644
--- a/quartz/plugins/emitters/index.ts
+++ b/quartz/plugins/emitters/index.ts
@@ -1,4 +1,6 @@
 export { ContentPage } from './contentPage'
+export { TagPage } from './tagPage'
+export { FolderPage } from './folderPage'
 export { ContentIndex } from './contentIndex'
 export { AliasRedirects } from './aliases'
 export { CNAME } from './cname'
diff --git a/quartz/plugins/emitters/tagPage.tsx b/quartz/plugins/emitters/tagPage.tsx
new file mode 100644
index 0000000..1f69715
--- /dev/null
+++ b/quartz/plugins/emitters/tagPage.tsx
@@ -0,0 +1,74 @@
+import { QuartzEmitterPlugin } from "../types"
+import { QuartzComponentProps } from "../../components/types"
+import HeaderConstructor from "../../components/Header"
+import BodyConstructor from "../../components/Body"
+import { pageResources, renderPage } from "../../components/renderPage"
+import { ProcessedContent, defaultProcessedContent } from "../vfile"
+import { FullPageLayout } from "../../cfg"
+
+export const TagPage: QuartzEmitterPlugin<FullPageLayout> = (opts) => {
+  if (!opts) {
+    throw new Error("TagPage must be initialized with options specifiying the components to use")
+  }
+
+  const { head: Head, header, beforeBody, pageBody: Content, left, right, footer: Footer } = opts
+  const Header = HeaderConstructor()
+  const Body = BodyConstructor()
+
+  return {
+    name: "TagPage",
+    getQuartzComponents() {
+      return [Head, Header, Body, ...header, ...beforeBody, Content, ...left, ...right, Footer]
+    },
+    async emit(_contentDir, cfg, content, resources, emit): Promise<string[]> {
+      const fps: string[] = []
+      const allFiles = content.map(c => c[1].data)
+
+      const tags: Set<string> = new Set(allFiles.flatMap(data => data.frontmatter?.tags ?? []))
+      const tagDescriptions: Record<string, ProcessedContent> = Object.fromEntries([...tags].map(tag => ([
+        tag, defaultProcessedContent({ slug: `tags/${tag}`, frontmatter: { title: `Tag: ${tag}`, tags: [] } })
+      ])))
+
+      for (const [tree, file] of content) {
+        const slug = file.data.slug!
+        if (slug.startsWith("tags/")) {
+          const tag = slug.slice("tags/".length)
+          if (tags.has(tag)) {
+            tagDescriptions[tag] = [tree, file]
+          }
+        }
+      }
+
+      for (const tag of tags) {
+        const slug = `tags/${tag}`
+        const externalResources = pageResources(slug, resources)
+        const [tree, file] = tagDescriptions[tag]
+        const componentData: QuartzComponentProps = {
+          fileData: file.data,
+          externalResources,
+          cfg,
+          children: [],
+          tree,
+          allFiles
+        }
+
+        const content = renderPage(
+          slug,
+          componentData,
+          opts,
+          externalResources
+        )
+
+        const fp = file.data.slug + ".html"
+        await emit({
+          content,
+          slug: file.data.slug!,
+          ext: ".html",
+        })
+
+        fps.push(fp)
+      }
+      return fps
+    }
+  }
+}
diff --git a/quartz/plugins/index.ts b/quartz/plugins/index.ts
index 0378b1b..ae4593f 100644
--- a/quartz/plugins/index.ts
+++ b/quartz/plugins/index.ts
@@ -4,8 +4,12 @@
 import { googleFontHref, joinStyles } from '../theme'
 import { EmitCallback, PluginTypes } from './types'
 import styles from '../styles/base.scss'
+
 // @ts-ignore
 import spaRouterScript from '../components/scripts/spa.inline'
+// @ts-ignore
+import popoverScript from '../components/scripts/popover.inline'
+import popoverStyle from '../components/styles/popover.scss'
 
 export type ComponentResources = {
   css: string[],
@@ -57,6 +61,11 @@
     )
   }
 
+  if (cfg.enablePopovers) {
+    componentResources.afterDOMLoaded.push(popoverScript)
+    componentResources.css.push(popoverStyle)
+  }
+
   emit({
     slug: "index",
     ext: ".css",
diff --git a/quartz/plugins/vfile.ts b/quartz/plugins/vfile.ts
index 9df3192..d3d24d3 100644
--- a/quartz/plugins/vfile.ts
+++ b/quartz/plugins/vfile.ts
@@ -1,5 +1,12 @@
-import { Node } from 'hast'
-import { Data, VFile } from 'vfile/lib'
+import { Node, Parent } from 'hast'
+import { Data, VFile } from 'vfile'
 
 export type QuartzPluginData = Data
 export type ProcessedContent = [Node<QuartzPluginData>, VFile]
+
+export function defaultProcessedContent(vfileData: Partial<QuartzPluginData>): ProcessedContent {
+  const root: Parent = { type: 'root', children: [] }
+  const vfile = new VFile("")
+  vfile.data = vfileData
+  return [root, vfile]
+}
diff --git a/quartz/styles/base.scss b/quartz/styles/base.scss
index fe62601..45e6370 100644
--- a/quartz/styles/base.scss
+++ b/quartz/styles/base.scss
@@ -3,9 +3,6 @@
 
 html {
   scroll-behavior: smooth;
-  & footer > p {
-    text-align: center !important;
-  }
 }
 
 body {

--
Gitblit v1.10.0