─── ❖ ── ✦ ── ❖ ───

Associated notes

This note is part of a series about how this blog is set up. See here for an overview.

This post’s topic are the changes I made of my Quartz instance in order to differentiate its appearance from the default config.

The main drawback of changing core components of Quartz is, that upstream changes might break your installation (if you incorporate them without looking closely).

β†’ add callout with version info

Tip

The instructions provided here might be outdated, please check the newest official instructions first!

Inspirations from other Quartz instances

The inspirations for most changes come from the Quartz Showcases. See the commit history for recent additions.

It’s also useful to check out how features look on mobile before implementing them.

More helpful resources: - https://quartz.eilleeenz.com/Quartz-Snippets

Config Changes (little to none code changes necessary)

These β€˜features’ require almost no new code, only intended config changes. It is likely that these changes will keep working after merging newer Quartz commits from upstream.

β€˜Feature’SiteCode availableOfficial Doc
Custom blog title & base URL
Custom footeryxy.ninjaGitHub
Change layout of starting page
Increase ToC indentation

New Features (more code changes necessary)

These features require relatively much new code and might break when updating to a newer Quartz version (by merging newer commits).

FeatureSiteCode availableAdded to this siteDetails
LinksHeaderNotesko & Morrowind Modding WikiGitHub & GitHubnot anymoreused too much space on mobile
Animated image (on mouseover)Topo DamenteGitHubnovery complicated
Tags in right columnTopo DamenteGitHubyes
Update Date (next to Publish Date)yes
Site-specific git links below Publish Dateyxy.ninjaGitHubyes
Dark Mode toggle in right column
Navbar (and mobile hamburger menu)The PondGitHubno→ many good parts
Link to random page & scroll to top (footer)Morrowind Modding Wiki & eilleeenz.comGitHub & GitHub
Map icon next to search buttoneilleeenz.comGitHub→ many good parts
Custom image as blog titleGatekeeper Wiki & eledah.irGitHub & GitHub
Link blocks on start pageGatekeeper WikiGitHubmainly custom CSS
Links in left page columnEllie.wtfGitHub
Graph view & Backlinks below central columnbe-far.comGitHub
(Mobile) Tags, backlinks, most recent & explorer under main pageTopo DamenteGitHub
Search icon instead of text fieldTopo DamenteGitHub
’Most Recent’ only on start page
Icon (’#’) with link to all tags (next to search)airbyte.comGitHubold Quartz version

Current Issues

  • code block copy function doesn’t work
  • Dark/light mode toggle doesn’t function

missing:

  • graph- und explorer exclude
  • folder emoji (index.md)
  • browser tab image
  • footer links
  • folder structure
  • tag structure
  • note template

Individual Changes

Finished:

  • Required: Blog title, base URL
  • Analytics, fonts, colors
  • Footer links
  • Recent notes
  • Explorer customization
  • Graph customization
  • Desktop and mobile specific layout changes

Missing:

  • Last updated & History
  • Browser tab image
  • Compress search bar (or add all tags next to darkmode toggle)
  • enable RSS
  • move DarkModeToggle to right, like here: https://notes.camargomau.com/
  • add explorer and recent posts to mobile layout
  • remove the page itself from its own backlinks
  • define poetry code

Minimal required config changes

These are the only strictly required config changes: modify the pageTitle & baseURL in quartz.config.ts. Follow this instructions for the baseURL.

Emojis might render differently depending on the end device, for example πŸͺ΄ (from the Quartz Documentation Blog) renders as a potted plant in iOS, but the Brave browser under Windows shows just a square. Here can some renderings be compared.

Emoji examples: 🍺🍻🧠🌌🌊πŸͺπŸŒ βš›β˜£πŸ”–πŸ·πŸ—ΊπŸ³πŸ“šπŸ“–πŸ“‹πŸš§πŸ›‘βš πŸ’ΎπŸ“ŒπŸ—ƒπŸ“

RSS

The RSS feed is enabled by default, it’s reachable at https://zoylendt.github.io/index.xml. It uses the previously set pageTitle.

Analytics, fonts, colors

This are the other changes that happen entirely within quartz.config.ts.

The analytics can be switched off with

quartz.config.ts
  analytics: {
    provider: "null",
  },

For the general site theme the next code block is relevant, you can experiment with fonts from Google Fonts by referencing them by name.

quartz.config.ts
  theme: {
    fontOrigin: "googleFonts",
    cdnCaching: true,
    typography: {
      header: "Sedan SC",
      body: "Lexend",
      code: "JetBrains Mono",
    },

The white page color in the default lightMode was not to my liking, so I changed it like in this blog (there he also set lightgray: "#646464",, which was not to my liking since it made code snippets harder to read).

quartz.config.ts
    colors: {
      lightMode: {
        //light: "#faf8f8",
        light: "#d8cfc4",
        lightgray: "#e5e5e5",
        gray: "#b8b8b8",
        darkgray: "#4e4e4e",
        dark: "#2b2b2b",
        secondary: "#284b63",
        tertiary: "#84a59d",
        highlight: "rgba(143, 159, 169, 0.15)",
      },
      darkMode: {
        light: "#161618",
        lightgray: "#393639",
        gray: "#646464",
        darkgray: "#d4d4d4",
        dark: "#ebebec",
        secondary: "#7b97aa",
        tertiary: "#84a59d",
        highlight: "rgba(143, 159, 169, 0.15)",
      },
    },

In theory it’s possible to set a custom background image, like in jzhao.xyz/, but I couldn’t get that to work.

Now we need to edit the file quartz.layout.ts. The default footer links are defined at the start:

quartz.layout.ts
footer: Component.Footer({
  links: {
    GitHub: "https://github.com/jackyzha0/quartz",
    "Discord Community": "https://discord.gg/cRFFHYye7t",
  },
}),

Recent notes

To create a list of the latest four new posts above the Explorer in the left part of the page’s layout, we add this code block

quartz.layout.ts
  Component.DesktopOnly(Component.RecentNotes({
        title: "Recent Notes",
        limit: 4,
        filter: (f) =>
          !f.frontmatter?.noindex,
        linkToMore: "tags/note" as SimpleSlug,
      }),),

right after this:

quartz.layout.ts
left: [
  Component.PageTitle(),
  Component.MobileOnly(Component.Spacer()),
  Component.Search(),
  Component.Darkmode(),

Also, add import { SimpleSlug } from "./quartz/util/path" at the beginning of quartz.layout.ts. And take care where linkToMore points. Here are the config options for this plugin listed.

One remaining issue: this plugin does not show up when I open one of the folders, like Braindump or Brewing. The Explorer customization also don’t take effect here.

Explorer customization

The Explorer can also be configured in many ways. I used the example code to add emoji prefix to files, but without changing the folder icons (they can be individualized for each folder with a index.md inside that folder).

I changed Component.DesktopOnly(Component.Explorer()), to

quartz.layout.ts
Component.DesktopOnly(Component.Explorer({
  mapFn: (node) => {
    // dont change name of root node
    if (node.depth > 0) {
      // set emoji for file/folder
      if (node.file) {
        node.displayName = "πŸ“„ " + node.displayName
      } else {
        node.displayName = node.displayName
      }
    }
  },
}))

Graph customization

The graph-view on the right side is also highly customizable, I just removed the tags from the local and global graph:

quartz.layout.ts
right: [
  Component.Graph({
    localGraph: {
      showTags: false,
    },
    globalGraph: {
      showTags: false,
    },
  }),
  Component.DesktopOnly(Component.TableOfContents()),
  Component.Backlinks(),
],

Desktop and mobile layout

You can change the order of plugins, if they appear right or left and if they are only on the desktop (Component.DesktopOnly(...),) or the mobile view (Component.MobileOnly(...),), otherwise they appear in both.

LinksHeader

This change, together with Last updated & History, involves probably the most code modifications. Its target is to create a row with six links in the beforeBody-part of the layout (above the breadcrumbs). This can be viewed in action on the site of its (apparent) creator: https://notes.camargomau.com/

After some digging in his GitHub repo, I identified the corresponding changes. We need to create two new files, quartz/components/LinksHeader.tsx & quartz/components/styles/linksHeader.scss, and modify two other, quartz.layout.ts & quartz/components/index.ts.

The first new file, quartz/components/LinksHeader.tsx, contains the links and images, so we need to modify it to suit our needs.

One advantage of this β€œplugin” is, that it renders well on the mobile layout (as two rows with 3 elements each), where it can be used to browse the folders (if configured in this way), since the mobile layout per default no Explorer has.

quartz/components/LinksHeader.tsx
import { QuartzComponentConstructor } from "./types"
import style from "./styles/linksHeader.scss"
 
interface Options {
  links: Record<string, string>
}
 
export default (() => {
  function LinksHeader() {
    return (
      <div>
        <div id="links-header">
          <span>
            <img src="https://raw.githubusercontent.com/microsoft/fluentui-emoji/main/assets/Brain/Color/brain_color.svg"></img>
            <a href="/Braindump">Braindump</a>
          </span>
          <span>
            <img src="https://raw.githubusercontent.com/microsoft/fluentui-emoji/main/assets/Keyboard/Color/keyboard_color.svg"></img>
            <a href="/Coding">Coding</a>
          </span>
          <span>
            <img src="https://raw.githubusercontent.com/microsoft/fluentui-emoji/main/assets/Beer%20mug/Color/beer_mug_color.svg"></img>
            <a href="/Brewing">Brewing</a>
          </span>
          <span>
            <img src="https://raw.githubusercontent.com/microsoft/fluentui-emoji/main/assets/Atom%20symbol/Color/atom_symbol_color.svg"></img>
            <a href="/Physics">Physics</a>
          </span>
          <span>
            <img src="https://raw.githubusercontent.com/microsoft/fluentui-emoji/main/assets/Open%20book/Color/open_book_color.svg"></img>
            <a href="/Media">Media</a>
          </span>
          <span>
            <img src="https://raw.githubusercontent.com/microsoft/fluentui-emoji/main/assets/Spouting%20whale/Color/spouting_whale_color.svg"></img>
            <a href="/Selfhosted">Selfhosted</a>
          </span>
        </div>
      <hr style="background-color: var(--gray); border-top: 1px var(--gray) solid; margin-top: 1.3rem"></hr>
      </div>
    )
  }
 
  LinksHeader.css = style
  return LinksHeader
}) satisfies QuartzComponentConstructor
quartz/components/styles/linksHeader.scss
@use "../../styles/variables.scss" as *;
 
header {
  display: block;
  margin-top: 1rem;
}
 
#links-header {
  display: flex;
  flex-wrap: wrap;
  column-gap: 1em;
  font-size: 1.2em;
  justify-content: space-between;
  margin: 0 0.2em;
 
  span {
    margin-top: 0.75em;
  }
 
  img {
    height: 1em;
    margin: 0 0.3em 0 0;
    vertical-align: sub;
  }
 
  a {
    color: var(--dark);
  }
}
 
@media (max-width: $mobileBreakpoint) {
  #links-header > * {
    width: calc(33.33% - 0.67em); /* 33.33% width with some gap */
  }
 
  #links-header > *:nth-child(3n-1) {
    text-align: center;
  }
 
  #links-header > *:nth-child(3n) {
    text-align: right;
  }
}

In quartz.layout.ts change header: [], to header: [Component.LinksHeader()], like this:

quartz.layout.ts
  head: Component.Head(),
  header: [Component.LinksHeader()],
  footer: Component.Footer({

In quartz/components/index.ts add import LinksHeader from "./LinksHeader" to the start and LinksHeader, to the export list, like this:

quartz/components/index.ts
import Content from "./pages/Content"
import TagContent from "./pages/TagContent"
import FolderContent from "./pages/FolderContent"
import NotFound from "./pages/404"
import ArticleTitle from "./ArticleTitle"
import Darkmode from "./Darkmode"
import Head from "./Head"
import PageTitle from "./PageTitle"
import ContentMeta from "./ContentMeta"
import Spacer from "./Spacer"
import TableOfContents from "./TableOfContents"
import Explorer from "./Explorer"
import TagList from "./TagList"
import Graph from "./Graph"
import Backlinks from "./Backlinks"
import Search from "./Search"
import Footer from "./Footer"
import DesktopOnly from "./DesktopOnly"
import MobileOnly from "./MobileOnly"
import RecentNotes from "./RecentNotes"
import Breadcrumbs from "./Breadcrumbs"
import LinksHeader from "./LinksHeader"
 
export {
  ArticleTitle,
  Content,
  TagContent,
  FolderContent,
  Darkmode,
  Head,
  PageTitle,
  ContentMeta,
  Spacer,
  TableOfContents,
  Explorer,
  TagList,
  Graph,
  Backlinks,
  Search,
  Footer,
  DesktopOnly,
  MobileOnly,
  RecentNotes,
  NotFound,
  Breadcrumbs,
  LinksHeader,
}

Last updated & History

This feature was implemented differently by two blogs, https://www.chadly.net/ & https://notes.yxy.ninja/. I replaced the default quartz/components/ContentMeta.tsx with a modified version of this file, so it looks like this:

/quartz/components/ContentMeta.tsx
import { formatDate, getDate } from "./Date"
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
import readingTime from "reading-time"
import { classNames } from "../util/lang"
import { i18n } from "../i18n"
import { JSX } from "preact"
import style from "./styles/contentMeta.scss"
 
interface ContentMetaOptions {
  /**
   * Whether to display reading time
   */
  showReadingTime: boolean
  showComma: boolean
}
 
const defaultOptions: ContentMetaOptions = {
  showReadingTime: true,
  showComma: true,
}
 
export default ((opts?: Partial<ContentMetaOptions>) => {
  // Merge options with defaults
  const options: ContentMetaOptions = { ...defaultOptions, ...opts }
 
  function ContentMetadata({ cfg, fileData, displayClass }: QuartzComponentProps) {
    const text = fileData.text
 
    if (text) {
      var modifiedSegment: string = ""
      var createdSegment: string = ""
      const fileRelativePath = fileData.filePath
      //const segments: (string | JSX.Element)[] = []
 
      if (fileData.dates) {
        const cfgDefaultDataType = cfg.defaultDateType // For backward compatibility, just in case this is used somewhere else by the original author
 
        if (fileData.dates.created) {
          cfg.defaultDateType = "created"
          createdSegment = formatDate(getDate(cfg, fileData)!)
        }
 
        if (fileData.dates.modified) {
          cfg.defaultDateType = "modified"
          modifiedSegment = formatDate(getDate(cfg, fileData)!)
        }
 
        cfg.defaultDateType = cfgDefaultDataType
      }
 
 
      // Display reading time if enabled
      var readingTimeStr: string = ""
      if (options.showReadingTime) {
        const { minutes, words: _words } = readingTime(text)
        const displayedTime = i18n(cfg.locale).components.contentMeta.readingTime({
          minutes: Math.ceil(minutes),
        })
        // segments.push(displayedTime)
        readingTimeStr = `${_words} words, ${displayedTime}`
      }
 
      //Created: &nbsp;{createdSegment} <br /> 
      return (
        <p class={classNames(displayClass, "content-meta")}>
          {readingTimeStr} <br />
          Created {createdSegment} & updated {modifiedSegment} <br /> 
          🌟 <a href={`https://github.com/zoylendt/zoylendt.github.io/blame/v4/${fileRelativePath}`} class={classNames(displayClass, "external")} target={"_blank"} style={"font-weight:400"}>
            View Source<svg class="external-icon" viewBox="0 0 512 512"><path d="M320 0H288V64h32 82.7L201.4 265.4 178.7 288 224 333.3l22.6-22.6L448 109.3V192v32h64V192 32 0H480 320zM32 32H0V64 480v32H32 456h32V480 352 320H424v32 96H64V96h96 32V32H160 32z"></path></svg>
          </a> &nbsp;
          πŸ—“οΈ <a href={`https://github.com/zoylendt/zoylendt.github.io/commits/v4/${fileRelativePath}`} class={classNames(displayClass, "external")} target={"_blank"} style={"font-weight:400"}>
            Commit history<svg class="external-icon" viewBox="0 0 512 512"><path d="M320 0H288V64h32 82.7L201.4 265.4 178.7 288 224 333.3l22.6-22.6L448 109.3V192v32h64V192 32 0H480 320zM32 32H0V64 480v32H32 456h32V480 352 320H424v32 96H64V96h96 32V32H160 32z"></path></svg>
          </a>
        </p>
      )
 
      /*const segmentsElements = segments.map((segment) => <span>{segment}</span>)
      return (
        <p show-comma={options.showComma} class={classNames(displayClass, "content-meta")}>
          {segmentsElements}
        </p>
      )*/
    } else {
      return null
    }
  }
 
  ContentMetadata.css = style
 
  return ContentMetadata
}) satisfies QuartzComponentConstructor

My own config

The original files and my custom changed files have I documented here.

They are also included in this blog’s GitHub repo.

In total, this files were modified or newly created:

  • quartz.config.ts
  • quartz.layout.ts
  • /quartz/components/ContentMeta.tsx
  • /quartz/components/index.ts
  • /quartz/components/LinksHeader.tsx
  • /quartz/components/styles/linksHeader.scss
  • /quartz/static/icon.png (not yet changed)

─── ❖ ── ✦ ── ❖ ───