diff --git a/docs/advanced/index.md b/docs/advanced/index.md new file mode 100644 index 000000000..482258900 --- /dev/null +++ b/docs/advanced/index.md @@ -0,0 +1,3 @@ +--- +title: "Advanced" +--- diff --git a/docs/configuration.md b/docs/configuration.md index 35e0b9d95..047f6ca6b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -21,12 +21,10 @@ const config: QuartzConfig = { This part of the configuration concerns anything that can affect the whole site. The following is a list breaking down all the things you can configure: - `pageTitle`: title of the site. This is also used when generating the [[RSS Feed]] for your site. -- `description`: description of the site. This will be used when someone installs your site as an App. - `enableSPA`: whether to enable [[SPA Routing]] on your site. - `enablePopovers`: whether to enable [[popover previews]] on your site. - `analytics`: what to use for analytics on your site. Values can be - `null`: don't use analytics; - - `{ provider: "umami", websiteId: }`: easy, privacy-friendly, open source, GDPR Compliant analytics; - `{ provider: 'plausible' }`: use [Plausible](https://plausible.io/), a privacy-friendly alternative to Google Analytics; or - `{ provider: 'google', tagId: }`: use Google Analytics - `baseUrl`: this is used for sitemaps and RSS feeds that require an absolute URL to know where the canonical 'home' of your site lives. This is normally the deployed URL of your site (e.g. `quartz.jzhao.xyz` for this site). Do not include the protocol (i.e. `https://`) or any leading or trailing slashes. diff --git a/docs/features/explorer.md b/docs/features/explorer.md index 6f941b871..91766a999 100644 --- a/docs/features/explorer.md +++ b/docs/features/explorer.md @@ -8,6 +8,8 @@ Quartz features an explorer that allows you to navigate all files and folders on By default, it shows all folders and files on your page. To display the explorer in a different spot, you can edit the [[layout]]. +Display names for folders get determined by the `title` frontmatter field in `folder/index.md` (more detail in [[authoring content | Authoring Content]]). If this file does not exist or does not contain frontmatter, the local folder name will be used instead. + > [!info] > The explorer uses local storage by default to save the state of your explorer. This is done to ensure a smooth experience when navigating to different pages. > @@ -29,7 +31,7 @@ Component.Explorer({ sortFn: (a, b) => { ... // default implementation shown later }, - filterFn: undefined, + filterFn: filterFn: (node) => node.name !== "tags", // filters out 'tags' folder mapFn: undefined, // what order to apply functions in order: ["filter", "map", "sort"], @@ -57,7 +59,8 @@ All functions you can pass work with the `FileNode` class, which has the followi ```ts title="quartz/components/ExplorerNode.tsx" {2-5} export class FileNode { children: FileNode[] // children of current node - name: string // name of node (only useful for folders) + name: string // last part of slug + displayName: string // what actually should be displayed in the explorer file: QuartzPluginData | null // set if node is a file, see `QuartzPluginData` for more detail depth: number // depth of current node @@ -72,7 +75,7 @@ Every function you can pass is optional. By default, only a `sort` function will Component.Explorer({ sortFn: (a, b) => { if ((!a.file && !b.file) || (a.file && b.file)) { - return a.name.localeCompare(b.name) + return a.displayName.localeCompare(b.displayName) } if (a.file && !b.file) { return 1 @@ -120,7 +123,7 @@ Using this example, the explorer will alphabetically sort everything, but put al Component.Explorer({ sortFn: (a, b) => { if ((!a.file && !b.file) || (a.file && b.file)) { - return a.name.localeCompare(b.name) + return a.displayName.localeCompare(b.displayName) } if (a.file && !b.file) { return -1 @@ -138,7 +141,7 @@ Using this example, the display names of all `FileNodes` (folders + files) will ```ts title="quartz.layout.ts" Component.Explorer({ mapFn: (node) => { - node.name = node.name.toUpperCase() + node.displayName = node.displayName.toUpperCase() }, }) ``` @@ -159,6 +162,16 @@ Component.Explorer({ You can customize this by changing the entries of the `omit` set. Simply add all folder or file names you want to remove. +### Show every element in explorer + +To override the default filter function that removes the `tags` folder from the explorer, you can set the filter function to `undefined`. + +```ts title="quartz.layout.ts" +Component.Explorer({ + filterFn: undefined, // apply no filter function, every file and folder will visible +}) +``` + ## Advanced examples ### Add emoji prefix @@ -172,9 +185,9 @@ Component.Explorer({ if (node.depth > 0) { // set emoji for file/folder if (node.file) { - node.name = "📄 " + node.name + node.displayName = "📄 " + node.displayName } else { - node.name = "📁 " + node.name + node.displayName = "📁 " + node.displayName } } }, diff --git a/docs/features/offline access.md b/docs/features/offline access.md deleted file mode 100644 index 885bbd501..000000000 --- a/docs/features/offline access.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: "Offline Access (PWA)" -tags: - - plugin/emitter ---- - -This plugin allows your website to be accessible offline and be installed as an app. You can enable it by adding `Plugin.Offline(),` to the `emitters` in `quartz.config.ts` - -## Offline Capability - -Whenever you visit a page it gets cached for offline use. Depending on the kind of content, the process for caching is diffent: - -- **Pages** (HTML, your converted Markdown files): Quartz first tries to get them over the Network. If that fails, your browser attempts to fetch it from the cache. -- **Static Resources** (Fonts, CSS Styling, JavaScript): Quartz uses cached resources by default and updates the cache over the network in the background. -- **Images**: Images are saved once and then served from cache. Quartz uses a limited cache of 60 images and images remain in the cache for 30 days - -You can edit the fallback page by changing the `offline.md` file in the root of your `content` directory - -## Progressive Web App (PWA) - -Progressive Web Apps can have [many properties](https://developer.mozilla.org/en-US/docs/Web/Manifest). We're only going to mention the ones Quartz supports by default, however you can edit the offline plugins file to add more in case required. - -- **icons**: the `icon.svg` file in the `quartz/static` directory is used for all the icons. This makes it easier to scale the image since you don't need to provide an png for every size -- **name**, **short_name**: Uses the `pageTitle` configured in `quartz.config.ts` -- **description**: Uses the `description` configured in `quartz.config.ts` -- **background_color**, **theme_color**: Uses the `lightMode.light` color configured in `quartz.config.ts`. -- **start_url**: Uses the `baseUrl` configured in `quartz.config.ts` - -### Default values - -- **display**: this is set to `minimal-ui` diff --git a/docs/index.md b/docs/index.md index 570d5b364..05de2bae9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -30,7 +30,7 @@ This will guide you through initializing your Quartz with content. Once you've d ## 🔧 Features -- [[Obsidian compatibility]], [[full-text search]], [[graph view]], note transclusion, [[wikilinks]], [[backlinks]], [[Latex]], [[syntax highlighting]], [[popover previews]], [[offline access]] and [many more](./features) right out of the box +- [[Obsidian compatibility]], [[full-text search]], [[graph view]], note transclusion, [[wikilinks]], [[backlinks]], [[Latex]], [[syntax highlighting]], [[popover previews]], and [many more](./features) right out of the box - Hot-reload for both configuration and content - Simple JSX layouts and [[creating components|page components]] - [[SPA Routing|Ridiculously fast page loads]] and tiny bundle sizes diff --git a/package-lock.json b/package-lock.json index ae88ff7d6..35e93bd01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,7 +73,7 @@ "@types/flexsearch": "^0.7.3", "@types/hast": "^2.3.4", "@types/js-yaml": "^4.0.5", - "@types/node": "^20.6.2", + "@types/node": "^20.1.2", "@types/pretty-time": "^1.1.2", "@types/source-map-support": "^0.5.6", "@types/workerpool": "^6.4.0", @@ -113,7 +113,6 @@ }, "node_modules/@clack/prompts/node_modules/is-unicode-supported": { "version": "1.3.0", - "extraneous": true, "inBundle": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index e514edfbd..11a68d3ad 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.11", + "version": "4.1.0", "type": "module", "author": "jackyzha0 ", "license": "MIT", @@ -94,7 +94,7 @@ "@types/flexsearch": "^0.7.3", "@types/hast": "^2.3.4", "@types/js-yaml": "^4.0.5", - "@types/node": "^20.6.2", + "@types/node": "^20.1.2", "@types/pretty-time": "^1.1.2", "@types/source-map-support": "^0.5.6", "@types/workerpool": "^6.4.0", diff --git a/quartz/cfg.ts b/quartz/cfg.ts index 73e959fb7..8371b5e2b 100644 --- a/quartz/cfg.ts +++ b/quartz/cfg.ts @@ -19,7 +19,6 @@ export type Analytics = export interface GlobalConfiguration { pageTitle: string - description: string /** Whether to enable single-page-app style rendering. this prevents flashes of unstyled content and improves smoothness of Quartz */ enableSPA: boolean /** Whether to display Wikipedia-style popovers when hovering over links */ diff --git a/quartz/components/Explorer.tsx b/quartz/components/Explorer.tsx index c33d37542..de6b5e0ae 100644 --- a/quartz/components/Explorer.tsx +++ b/quartz/components/Explorer.tsx @@ -4,6 +4,7 @@ import explorerStyle from "./styles/explorer.scss" // @ts-ignore import script from "./scripts/explorer.inline" import { ExplorerNode, FileNode, Options } from "./ExplorerNode" +import { QuartzPluginData } from "../plugins/vfile" // Options interface defined in `ExplorerNode` to avoid circular dependency const defaultOptions = { @@ -11,10 +12,10 @@ const defaultOptions = { folderClickBehavior: "collapse", folderDefaultState: "collapsed", useSavedState: true, - // Sort order: folders first, then files. Sort folders and files alphabetically sortFn: (a, b) => { + // Sort order: folders first, then files. Sort folders and files alphabetically if ((!a.file && !b.file) || (a.file && b.file)) { - return a.name.localeCompare(b.name) + return a.displayName.localeCompare(b.displayName) } if (a.file && !b.file) { return 1 @@ -27,49 +28,58 @@ const defaultOptions = { } satisfies Options export default ((userOpts?: Partial) => { - function Explorer({ allFiles, displayClass, fileData }: QuartzComponentProps) { - // Parse config - const opts: Options = { ...defaultOptions, ...userOpts } + // Parse config + const opts: Options = { ...defaultOptions, ...userOpts } - // Construct tree from allFiles - const fileTree = new FileNode("") - allFiles.forEach((file) => fileTree.add(file, 1)) + // memoized + let fileTree: FileNode + let jsonTree: string - /** - * Keys of this object must match corresponding function name of `FileNode`, - * while values must be the argument that will be passed to the function. - * - * e.g. entry for FileNode.sort: `sort: opts.sortFn` (value is sort function from options) - */ - const functions = { - map: opts.mapFn, - sort: opts.sortFn, - filter: opts.filterFn, - } + function constructFileTree(allFiles: QuartzPluginData[]) { + if (!fileTree) { + // Construct tree from allFiles + fileTree = new FileNode("") + allFiles.forEach((file) => fileTree.add(file, 1)) - // Execute all functions (sort, filter, map) that were provided (if none were provided, only default "sort" is applied) - if (opts.order) { - // Order is important, use loop with index instead of order.map() - for (let i = 0; i < opts.order.length; i++) { - const functionName = opts.order[i] - if (functions[functionName]) { - // for every entry in order, call matching function in FileNode and pass matching argument - // e.g. i = 0; functionName = "filter" - // converted to: (if opts.filterFn) => fileTree.filter(opts.filterFn) + /** + * Keys of this object must match corresponding function name of `FileNode`, + * while values must be the argument that will be passed to the function. + * + * e.g. entry for FileNode.sort: `sort: opts.sortFn` (value is sort function from options) + */ + const functions = { + map: opts.mapFn, + sort: opts.sortFn, + filter: opts.filterFn, + } - // @ts-ignore - // typescript cant statically check these dynamic references, so manually make sure reference is valid and ignore warning - fileTree[functionName].call(fileTree, functions[functionName]) + // Execute all functions (sort, filter, map) that were provided (if none were provided, only default "sort" is applied) + if (opts.order) { + // Order is important, use loop with index instead of order.map() + for (let i = 0; i < opts.order.length; i++) { + const functionName = opts.order[i] + if (functions[functionName]) { + // for every entry in order, call matching function in FileNode and pass matching argument + // e.g. i = 0; functionName = "filter" + // converted to: (if opts.filterFn) => fileTree.filter(opts.filterFn) + + // @ts-ignore + // typescript cant statically check these dynamic references, so manually make sure reference is valid and ignore warning + fileTree[functionName].call(fileTree, functions[functionName]) + } } } + + // Get all folders of tree. Initialize with collapsed state + const folders = fileTree.getFolderPaths(opts.folderDefaultState === "collapsed") + + // Stringify to pass json tree as data attribute ([data-tree]) + jsonTree = JSON.stringify(folders) } + } - // Get all folders of tree. Initialize with collapsed state - const folders = fileTree.getFolderPaths(opts.folderDefaultState === "collapsed") - - // Stringify to pass json tree as data attribute ([data-tree]) - const jsonTree = JSON.stringify(folders) - + function Explorer({ allFiles, displayClass, fileData }: QuartzComponentProps) { + constructFileTree(allFiles) return (
diff --git a/quartz/components/ExplorerNode.tsx b/quartz/components/ExplorerNode.tsx index fd0c0823d..e5ceb0bf3 100644 --- a/quartz/components/ExplorerNode.tsx +++ b/quartz/components/ExplorerNode.tsx @@ -29,19 +29,28 @@ export type FolderState = { export class FileNode { children: FileNode[] name: string + displayName: string file: QuartzPluginData | null depth: number constructor(name: string, file?: QuartzPluginData, depth?: number) { this.children = [] this.name = name + this.displayName = name this.file = file ? structuredClone(file) : null this.depth = depth ?? 0 } private insert(file: DataWrapper) { if (file.path.length === 1) { - this.children.push(new FileNode(file.file.frontmatter!.title, file.file, this.depth + 1)) + if (file.path[0] !== "index.md") { + this.children.push(new FileNode(file.file.frontmatter!.title, file.file, this.depth + 1)) + } else { + const title = file.file.frontmatter?.title + if (title && title !== "index" && file.path[0] === "index.md") { + this.displayName = title + } + } } else { const next = file.path[0] file.path = file.path.splice(1) @@ -145,12 +154,12 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro } return ( -
+
  • {node.file ? ( // Single file node
  • - {node.name} + {node.displayName}
  • ) : ( @@ -174,17 +183,17 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro {/* render tag if folderBehavior is "link", otherwise render )} - +
    )} {/* Recursively render children of folder */} @@ -210,6 +219,6 @@ export function ExplorerNode({ node, opts, fullPath, fileData }: ExplorerNodePro )} - + ) } diff --git a/quartz/components/Head.tsx b/quartz/components/Head.tsx index 972f7497e..2bf263817 100644 --- a/quartz/components/Head.tsx +++ b/quartz/components/Head.tsx @@ -14,8 +14,6 @@ export default (() => { const iconPath = joinSegments(baseDir, "static/icon.png") const ogImagePath = `https://${cfg.baseUrl}/static/og-image.png` - const manifest = - cfg.baseUrl == undefined ? "/manifest.json" : `https://${cfg.baseUrl}/manifest.json` return ( @@ -27,9 +25,7 @@ export default (() => { {cfg.baseUrl && } - - diff --git a/quartz/components/pages/OfflineFallbackPage.tsx b/quartz/components/pages/OfflineFallbackPage.tsx deleted file mode 100644 index d2fede3ce..000000000 --- a/quartz/components/pages/OfflineFallbackPage.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { QuartzComponentConstructor } from "../types" - -function OfflineFallbackPage() { - return ( -
    -

    Offline

    -

    You're offline and this page hasn't been cached yet.

    -
    - ) -} - -export default (() => OfflineFallbackPage) satisfies QuartzComponentConstructor diff --git a/quartz/components/scripts/explorer.inline.ts b/quartz/components/scripts/explorer.inline.ts index 2b7df7d35..9fe18654f 100644 --- a/quartz/components/scripts/explorer.inline.ts +++ b/quartz/components/scripts/explorer.inline.ts @@ -113,9 +113,11 @@ function setupExplorer() { ) as HTMLElement // Get corresponding content
      tag and set state - const folderUL = folderLi.parentElement?.nextElementSibling - if (folderUL) { - setFolderState(folderUL as HTMLElement, folderUl.collapsed) + if (folderLi) { + const folderUL = folderLi.parentElement?.nextElementSibling + if (folderUL) { + setFolderState(folderUL as HTMLElement, folderUl.collapsed) + } } }) } else { diff --git a/quartz/components/styles/explorer.scss b/quartz/components/styles/explorer.scss index 776a5ae6e..28e9f9bb2 100644 --- a/quartz/components/styles/explorer.scss +++ b/quartz/components/styles/explorer.scss @@ -1,4 +1,5 @@ button#explorer { + all: unset; background-color: transparent; border: none; text-align: left; @@ -8,7 +9,7 @@ button#explorer { display: flex; align-items: center; - & h3 { + & h1 { font-size: 1rem; display: inline-block; margin: 0; @@ -58,7 +59,7 @@ button#explorer { max-height 0.35s ease, transform 0.35s ease, opacity 0.2s ease; - & div > li > a { + & li > a { color: var(--dark); opacity: 0.75; pointer-events: all; @@ -80,19 +81,20 @@ svg { align-items: center; user-select: none; - & li > a { - // other selector is more specific, needs important - color: var(--secondary) !important; - opacity: 1 !important; - font-size: 1.05rem !important; + & div > a { + color: var(--secondary); + font-family: var(--headerFont); + font-size: 0.95rem; + font-weight: 600; + line-height: 1.5rem; + display: inline-block; } - & li > a:hover { - // other selector is more specific, needs important - color: var(--tertiary) !important; + & div > a:hover { + color: var(--tertiary); } - & li > button { + & div > button { color: var(--dark); background-color: transparent; border: none; @@ -102,15 +104,15 @@ svg { padding-right: 0; display: flex; align-items: center; + font-family: var(--headerFont); - & h3 { + & p { font-size: 0.95rem; display: inline-block; color: var(--secondary); font-weight: 600; margin: 0; line-height: 1.5rem; - font-weight: bold; pointer-events: none; } } @@ -138,5 +140,7 @@ div:has(> .folder-outer:not(.open)) > .folder-container > svg { #explorer-end { // needs height so IntersectionObserver gets triggered - height: 1px; + height: 4px; + // remove default margin from li + margin: 0; } diff --git a/quartz/plugins/emitters/componentResources.ts b/quartz/plugins/emitters/componentResources.ts index a82e7c12a..1290a3548 100644 --- a/quartz/plugins/emitters/componentResources.ts +++ b/quartz/plugins/emitters/componentResources.ts @@ -116,11 +116,6 @@ function addGlobalPageResources( document.dispatchEvent(event)`) } - componentResources.afterDOMLoaded.push(` - if ('serviceWorker' in navigator) { - navigator.serviceWorker.register('/sw.js'); - }`) - let wsUrl = `ws://localhost:${ctx.argv.wsPort}` if (ctx.argv.remoteDevHost) { diff --git a/quartz/plugins/emitters/index.ts b/quartz/plugins/emitters/index.ts index 6de824d5f..99a2c54d5 100644 --- a/quartz/plugins/emitters/index.ts +++ b/quartz/plugins/emitters/index.ts @@ -7,4 +7,3 @@ export { Assets } from "./assets" export { Static } from "./static" export { ComponentResources } from "./componentResources" export { NotFoundPage } from "./404" -export { Offline } from "./offline" diff --git a/quartz/plugins/emitters/offline.ts b/quartz/plugins/emitters/offline.ts deleted file mode 100644 index b17771a22..000000000 --- a/quartz/plugins/emitters/offline.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { QuartzEmitterPlugin } from "../types" -import { FilePath, FullSlug } from "../../util/path" -import { FullPageLayout } from "../../cfg" -import { sharedPageComponents } from "../../../quartz.layout" -import OfflineFallbackPage from "../../components/pages/OfflineFallbackPage" -import BodyConstructor from "../../components/Body" -import { pageResources, renderPage } from "../../components/renderPage" -import { defaultProcessedContent } from "../vfile" -import { QuartzComponentProps } from "../../components/types" - -export const Offline: QuartzEmitterPlugin = () => { - const opts: FullPageLayout = { - ...sharedPageComponents, - pageBody: OfflineFallbackPage(), - beforeBody: [], - left: [], - right: [], - } - - const { head: Head, pageBody, footer: Footer } = opts - const Body = BodyConstructor() - - return { - name: "OfflineSupport", - getQuartzComponents() { - return [Head, Body, pageBody, Footer] - }, - async emit({ cfg }, _content, resources, emit): Promise { - const manifest = { - short_name: cfg.configuration.pageTitle, - name: cfg.configuration.pageTitle, - description: cfg.configuration.description, - background_color: cfg.configuration.theme.colors.lightMode.light, - theme_color: cfg.configuration.theme.colors.lightMode.light, - display: "minimal-ui", - icons: [ - { - src: "static/icon.svg", - sizes: "any", - purpose: "maskable", - }, - { - src: "static/icon.svg", - sizes: "any", - purpose: "any", - }, - ], - start_url: - cfg.configuration.baseUrl == undefined ? "/" : `https://${cfg.configuration.baseUrl}/`, - } - - const serviceWorker = - "importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js');" + - "const {pageCache, imageCache, staticResourceCache, googleFontsCache, offlineFallback} = workbox.recipes;" + - "pageCache(); googleFontsCache(); staticResourceCache(); imageCache(); offlineFallback();" - - const slug = "offline" as FullSlug - - const url = new URL(`https://${cfg.configuration.baseUrl ?? "example.com"}`) - const path = url.pathname as FullSlug - const externalResources = pageResources(path, resources) - const [tree, vfile] = defaultProcessedContent({ - slug, - text: "Offline", - description: "You're offline and this page hasn't been cached yet.", - frontmatter: { title: "Offline", tags: [] }, - }) - - const componentData: QuartzComponentProps = { - fileData: vfile.data, - externalResources, - cfg: cfg.configuration, - children: [], - tree, - allFiles: [], - } - - return Promise.all([ - emit({ - content: JSON.stringify(manifest), - slug: "manifest" as FullSlug, - ext: ".json", - }), - emit({ - content: serviceWorker, - slug: "sw" as FullSlug, - ext: ".js", - }), - emit({ - content: renderPage(slug, componentData, opts, externalResources), - slug, - ext: ".html", - }), - ]) - }, - } -} diff --git a/quartz/static/icon.svg b/quartz/static/icon.svg deleted file mode 100644 index c6ecfa2db..000000000 --- a/quartz/static/icon.svg +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - diff --git a/quartz/styles/base.scss b/quartz/styles/base.scss index c6925fbe5..9d553622d 100644 --- a/quartz/styles/base.scss +++ b/quartz/styles/base.scss @@ -446,7 +446,7 @@ video { ul.overflow, ol.overflow { - max-height: 300; + max-height: 400; overflow-y: auto; // clearfix