diff --git a/docs/configuration.md b/docs/configuration.md index 047f6ca6b..35e0b9d95 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -21,10 +21,12 @@ 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/offline access.md b/docs/features/offline access.md new file mode 100644 index 000000000..885bbd501 --- /dev/null +++ b/docs/features/offline access.md @@ -0,0 +1,31 @@ +--- +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/features/table of contents.md b/docs/features/table of contents.md index f05857368..a66c85017 100644 --- a/docs/features/table of contents.md +++ b/docs/features/table of contents.md @@ -8,6 +8,7 @@ tags: Quartz can automatically generate a table of contents from a list of headings on each page. It will also show you your current scroll position on the site by marking headings you've scrolled through with a different colour. By default, it will show all headers from H1 (`# Title`) all the way to H3 (`### Title`) and will only show the table of contents if there is more than 1 header on the page. +You can also hide the table of contents on a page by adding `showToc: false` to the frontmatter for that page. > [!info] > This feature requires both `Plugin.TableOfContents` in your `quartz.config.ts` and `Component.TableOfContents` in your `quartz.layout.ts` to function correctly. diff --git a/docs/index.md b/docs/index.md index 05de2bae9..570d5b364 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]], 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]], [[offline access]] 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 a87907897..8ff94245d 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.1.2", + "@types/node": "^20.6.2", "@types/pretty-time": "^1.1.2", "@types/source-map-support": "^0.5.6", "@types/workerpool": "^6.4.0", @@ -113,6 +113,7 @@ }, "node_modules/@clack/prompts/node_modules/is-unicode-supported": { "version": "1.3.0", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -1463,9 +1464,9 @@ } }, "node_modules/@types/node": { - "version": "20.3.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.3.tgz", - "integrity": "sha512-wheIYdr4NYML61AjC8MKj/2jrR/kDQri/CIpVoZwldwhnIrD/j9jIU5bJ8yBKuB2VhpFV7Ab6G2XkBjv9r9Zzw==", + "version": "20.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.2.tgz", + "integrity": "sha512-Y+/1vGBHV/cYk6OI1Na/LHzwnlNCAfU3ZNGrc1LdRe/LAIbdDPTTv/HU3M7yXN448aTVDq3eKRm2cg7iKLb8gw==", "dev": true }, "node_modules/@types/parse5": { diff --git a/package.json b/package.json index 0a2085cef..e514edfbd 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "@types/flexsearch": "^0.7.3", "@types/hast": "^2.3.4", "@types/js-yaml": "^4.0.5", - "@types/node": "^20.1.2", + "@types/node": "^20.6.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 8371b5e2b..73e959fb7 100644 --- a/quartz/cfg.ts +++ b/quartz/cfg.ts @@ -19,6 +19,7 @@ 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 8597075d2..c33d37542 100644 --- a/quartz/components/Explorer.tsx +++ b/quartz/components/Explorer.tsx @@ -22,6 +22,7 @@ const defaultOptions = { return -1 } }, + filterFn: (node) => node.name !== "tags", order: ["filter", "map", "sort"], } satisfies Options diff --git a/quartz/components/Head.tsx b/quartz/components/Head.tsx index 2bf263817..972f7497e 100644 --- a/quartz/components/Head.tsx +++ b/quartz/components/Head.tsx @@ -14,6 +14,8 @@ 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 ( @@ -25,7 +27,9 @@ export default (() => { {cfg.baseUrl && } + + diff --git a/quartz/components/pages/OfflineFallbackPage.tsx b/quartz/components/pages/OfflineFallbackPage.tsx new file mode 100644 index 000000000..d2fede3ce --- /dev/null +++ b/quartz/components/pages/OfflineFallbackPage.tsx @@ -0,0 +1,12 @@ +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/plugins/emitters/componentResources.ts b/quartz/plugins/emitters/componentResources.ts index 1290a3548..a82e7c12a 100644 --- a/quartz/plugins/emitters/componentResources.ts +++ b/quartz/plugins/emitters/componentResources.ts @@ -116,6 +116,11 @@ 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 99a2c54d5..6de824d5f 100644 --- a/quartz/plugins/emitters/index.ts +++ b/quartz/plugins/emitters/index.ts @@ -7,3 +7,4 @@ 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 new file mode 100644 index 000000000..b17771a22 --- /dev/null +++ b/quartz/plugins/emitters/offline.ts @@ -0,0 +1,97 @@ +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/plugins/transformers/ofm.ts b/quartz/plugins/transformers/ofm.ts index 8306f40d8..4d55edad8 100644 --- a/quartz/plugins/transformers/ofm.ts +++ b/quartz/plugins/transformers/ofm.ts @@ -400,6 +400,10 @@ export const ObsidianFlavoredMarkdown: QuartzTransformerPlugin return (tree: Root, file) => { const base = pathToRoot(file.data.slug!) findAndReplace(tree, tagRegex, (_value: string, tag: string) => { + // Check if the tag only includes numbers + if (/^\d+$/.test(tag)) { + return false + } tag = slugTag(tag) if (file.data.frontmatter && !file.data.frontmatter.tags.includes(tag)) { file.data.frontmatter.tags.push(tag) diff --git a/quartz/static/icon.svg b/quartz/static/icon.svg new file mode 100644 index 000000000..c6ecfa2db --- /dev/null +++ b/quartz/static/icon.svg @@ -0,0 +1,74 @@ + + + + + + + +