From b6623de23a2cfead5b8c234dcf881cb3c072ad57 Mon Sep 17 00:00:00 2001 From: Adam French Date: Mon, 6 Apr 2026 13:27:10 +0100 Subject: [PATCH] Add Quartz service for serving Obsidian notes at /notes/ Replaces the custom Go/Vue notes system with Quartz v4, a polished static site generator for Obsidian vaults. Mounts OBSIDIAN_DIR as the Quartz content directory and serves it at /notes/ with hot-reload via `npx quartz build --serve`. Co-Authored-By: Claude Sonnet 4.6 --- docker-compose.yml | 14 ++++++ nginx/entrypoint.sh | 4 +- nginx/nginx.conf.template | 15 +++++++ nginx/nginx_dev.conf.template | 30 +++++++++++++ quartz/Dockerfile | 18 ++++++++ quartz/entrypoint.sh | 8 ++++ quartz/quartz.config.ts.template | 76 ++++++++++++++++++++++++++++++++ 7 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 quartz/Dockerfile create mode 100644 quartz/entrypoint.sh create mode 100644 quartz/quartz.config.ts.template diff --git a/docker-compose.yml b/docker-compose.yml index 30e80ae..79da695 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,6 +31,7 @@ services: - icecast2 - gitea - hasura + - quartz networks: - app-network ports: @@ -115,6 +116,19 @@ services: ports: - "${LIQUIDSOAP_HARBOR_PORT:-8001}:${LIQUIDSOAP_HARBOR_PORT:-8001}" + quartz: + build: + context: ./quartz + dockerfile: Dockerfile + container_name: "${QUARTZ_HOST}" + restart: always + networks: + - app-network + env_file: + - ./.env + volumes: + - ${OBSIDIAN_DIR}:/quartz/content:ro + gitea-runner: image: gitea/act_runner:latest container_name: "${GITEA_RUNNER_HOST}" diff --git a/nginx/entrypoint.sh b/nginx/entrypoint.sh index d568544..a54c3b6 100755 --- a/nginx/entrypoint.sh +++ b/nginx/entrypoint.sh @@ -12,12 +12,12 @@ if [ "$DEV_MODE" = "true" ]; then -out "$CERT_DIR/fullchain.pem" \ -subj "/CN=localhost" 2>/dev/null fi - envsubst '${DOMAIN} ${BACKEND_HOST} ${BACKEND_PORT} ${BACKEND_ENDPOINT} ${ICECAST_HOST} ${ICECAST_PORT} ${GITEA_HOST} ${GITEA_PORT} ${HASURA_HOST} ${HASURA_PORT}' \ + envsubst '${DOMAIN} ${BACKEND_HOST} ${BACKEND_PORT} ${BACKEND_ENDPOINT} ${ICECAST_HOST} ${ICECAST_PORT} ${GITEA_HOST} ${GITEA_PORT} ${HASURA_HOST} ${HASURA_PORT} ${QUARTZ_HOST} ${QUARTZ_PORT}' \ /etc/nginx/nginx.conf elif [ -f "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" ] && [ -f "/etc/letsencrypt/live/$DOMAIN/privkey.pem" ]; then echo "Certificates found. Using production nginx config." - envsubst '${DOMAIN} ${BACKEND_HOST} ${BACKEND_PORT} ${BACKEND_ENDPOINT} ${ICECAST_HOST} ${ICECAST_PORT} ${GITEA_HOST} ${GITEA_PORT} ${HASURA_HOST} ${HASURA_PORT}' \ + envsubst '${DOMAIN} ${BACKEND_HOST} ${BACKEND_PORT} ${BACKEND_ENDPOINT} ${ICECAST_HOST} ${ICECAST_PORT} ${GITEA_HOST} ${GITEA_PORT} ${HASURA_HOST} ${HASURA_PORT} ${QUARTZ_HOST} ${QUARTZ_PORT}' \ /etc/nginx/nginx.conf else diff --git a/nginx/nginx.conf.template b/nginx/nginx.conf.template index 5ab5332..1a3eabd 100644 --- a/nginx/nginx.conf.template +++ b/nginx/nginx.conf.template @@ -217,6 +217,21 @@ http { proxy_set_header Connection "upgrade"; } + location /notes { + return 301 /notes/; + } + + location /notes/ { + proxy_pass http://$QUARTZ_HOST:$QUARTZ_PORT/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } } diff --git a/nginx/nginx_dev.conf.template b/nginx/nginx_dev.conf.template index 61f4f3d..f275e6e 100644 --- a/nginx/nginx_dev.conf.template +++ b/nginx/nginx_dev.conf.template @@ -144,6 +144,21 @@ http { proxy_set_header Connection "upgrade"; } + location /notes { + return 301 /notes/; + } + + location /notes/ { + proxy_pass http://$QUARTZ_HOST:$QUARTZ_PORT/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } server { @@ -253,6 +268,21 @@ http { proxy_set_header Connection "upgrade"; } + location /notes { + return 301 /notes/; + } + + location /notes/ { + proxy_pass http://$QUARTZ_HOST:$QUARTZ_PORT/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } } diff --git a/quartz/Dockerfile b/quartz/Dockerfile new file mode 100644 index 0000000..0ba4b72 --- /dev/null +++ b/quartz/Dockerfile @@ -0,0 +1,18 @@ +FROM node:22-slim + +RUN apt-get update \ + && apt-get install --yes git gettext-base \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /quartz + +ARG QUARTZ_VERSION=v4.4.0 +RUN git clone --depth 1 --branch ${QUARTZ_VERSION} \ + https://github.com/jackyzha0/quartz.git . \ + && npm ci + +COPY quartz.config.ts.template /quartz/quartz.config.ts.template +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/quartz/entrypoint.sh b/quartz/entrypoint.sh new file mode 100644 index 0000000..0d83f7b --- /dev/null +++ b/quartz/entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/sh +set -e + +envsubst '${DOMAIN}' \ + < /quartz/quartz.config.ts.template \ + > /quartz/quartz.config.ts + +exec npx quartz build --serve --port "${QUARTZ_PORT:-8080}" diff --git a/quartz/quartz.config.ts.template b/quartz/quartz.config.ts.template new file mode 100644 index 0000000..c2e68a4 --- /dev/null +++ b/quartz/quartz.config.ts.template @@ -0,0 +1,76 @@ +import { QuartzConfig } from "./quartz/cfg" +import * as Plugin from "./quartz/plugins" + +const config: QuartzConfig = { + configuration: { + pageTitle: "Notes", + pageTitleSuffix: "", + enableSPA: true, + enablePopovers: true, + analytics: null, + locale: "en-GB", + baseUrl: "www.${DOMAIN}/notes", + ignorePatterns: ["private", "templates", ".obsidian"], + defaultDateType: "modified", + theme: { + fontOrigin: "googleFonts", + cdnCaching: true, + typography: { + header: "Schibsted Grotesk", + body: "Source Sans Pro", + code: "IBM Plex Mono", + }, + colors: { + lightMode: { + light: "#faf8f8", + lightgray: "#e5e5e5", + gray: "#b8b8b8", + darkgray: "#4e4e4e", + dark: "#2b2b2b", + secondary: "#284b63", + tertiary: "#84a98c", + highlight: "rgba(143, 159, 169, 0.15)", + textHighlight: "#fff23688", + }, + darkMode: { + light: "#161618", + lightgray: "#393639", + gray: "#646464", + darkgray: "#d4d4d4", + dark: "#ebebec", + secondary: "#7b97aa", + tertiary: "#84a98c", + highlight: "rgba(143, 159, 169, 0.15)", + textHighlight: "#b3aa0288", + }, + }, + }, + }, + plugins: { + transformers: [ + Plugin.FrontMatter(), + Plugin.CreatedModifiedDate({ priority: ["frontmatter", "filesystem"] }), + Plugin.SyntaxHighlighting(), + Plugin.ObsidianFlavoredMarkdown({ enableInHtmlEmbed: false }), + Plugin.GitHubFlavoredMarkdown(), + Plugin.TableOfContents(), + Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }), + Plugin.Description(), + Plugin.Latex({ renderEngine: "katex" }), + ], + filters: [Plugin.RemoveDrafts()], + emitters: [ + Plugin.AliasRedirects(), + Plugin.ComponentResources(), + Plugin.ContentPage(), + Plugin.FolderPage(), + Plugin.TagPage(), + Plugin.ContentIndex({ enableSiteMap: true, enableRSS: true }), + Plugin.Assets(), + Plugin.Static(), + Plugin.NotFoundPage(), + ], + }, +} + +export default config