Note

This is part of the Advanced Customizations.

Note

Quartz has released its own Private Pages on Apr 23, 2025. This tweak still works with today’s Quartz unless further noticed.

The post A Privacy-First Setup Guide focuses on external ways of hiding the source content and limiting access to specific URLs. Here I’ll list some tweaks to make Quartz more robust when it comes to creating a secret garden.

Hide from Explorer

Deprecated

After my upgrade on Mar 15, 2025, from my last upgrade on Jan 1, the type of node has been changed from FileNode in quartz/components/ExplorerNode.tsx, to FileTrieNode in quartz/util/fileTrie.ts.

The notes filtered out at the indexing stage in linkIndex.set() at quartz/plugins/emitters/contentIndex.tsx will not appear anyway. Sorting Objects in Explorer also indicates this change.

That means, in order to hide notes in Explorer, applying the changes in Hide from Search is enough for now.

The following example hides notes whose names are ‘Clippings’ and ‘Being Mortal’ from the Explorer. Do apply the change to both the ‘single page layout’ and the ‘list of pages layout’ settings.

quartz/quartz.layout.ts
Component.Explorer({
  // mod: omit pages
  filterFn: (node) => {
    const omit = new Set(["tags", "clippings", "being-mortal"])
    return !omit.has(node.name.toLowerCase())
  },
}), 

Hide from Search

It’s still possible to access the full content of your hidden pages using the preview section of the Search function. Thus, we need to take out the hidden pages from the search index.

quartz/plugins/emitters/contentIndex.ts
async emit(ctx, content, _resources) {
  const cfg = ctx.cfg.configuration
  const emitted: FilePath[] = []
  const linkIndex: ContentIndex = new Map()
  for (const [tree, file] of content) {
    // mod: skip files with specific tag while building the search index
    if (file.data.frontmatter?.tags?.includes("exculsive")) {
      continue
    }
    ...
  }
  ...
},

Hide from Graph

The following example takes out the hidden pages from the graph view. Credits to the Discord Community.

quartz/components/scripts/graph.inline.ts
async function renderGraph(container: string, fullSlug: FullSlug) {
  const slug = simplifySlug(fullSlug)
  const visited = getVisited()
  const graph = document.getElementById(container)
  if (!graph) return
  removeAllChildren(graph)
 
  let {
    ...
  } = JSON.parse(graph.dataset["cfg"]!) as D3Config
 
  // const data: Map<SimpleSlug, ContentDetails> = new Map(
  //   Object.entries<ContentDetails>(await fetchData).map(([k, v]) => [
  //     simplifySlug(k as FullSlug),
  //     v,
  //   ]),
  // )
 
  // mod: take out files that have the tag exclusive
  const originalData: Map<SimpleSlug, ContentDetails> = new Map(
    Object.entries<ContentDetails>(await fetchData).map(([k, v]) => [
      simplifySlug(k as FullSlug),
      v,
    ]),
  )
  const data: Map<SimpleSlug, ContentDetails> = new Map(
    [...originalData.entries()].filter(([key, value]) => {
    return !value.tags?.includes("exclusive")
    })
  )
  ...
}

Hide from Tag pages

The Tag pages will list all the URLs that have specific tags. Simply take out the hidden pages from a Tag page with its filter.

quartz/components/pages/TagContent.tsx
const tag = simplifySlug(slug.slice("tags/".length) as FullSlug)
const allPagesWithTag = (tag: string) =>
  allFiles.filter((file) =>
    // mod: skip files with specific tag
    !file.frontmatter?.tags?.includes("exclusive") &&
    (file.frontmatter?.tags ?? []).flatMap(getAllSegmentPrefixes).includes(tag),
  )

Hide from Folder pages

By clicking the foldable titles, we will enter the Folder view which contains all the pages and subfolders within. To avoid leaking the notes, skip the pages upon scanning.

Besides, the subfolders containing your exclusive pages will still be shown in the list. If you are lazy like me, put a blockage to simply stop any folder from showing.

quartz/components/pages/FolderContent.tsx
const allPagesInFolder: QuartzPluginData[] =
  folder.children
    .map((node) => {
      // regular file, proceed
      if (node.data) {
        // mod: skip files with tag 'exclusive' in Folder pages
        if (node.data.frontmatter?.tags?.includes("exclusive")) {
          return undefined
        }
        return node.data
      }
 
      if (node.isFolder && options.showSubfolders) {
        // mod: stop showing any folder
        return undefined
      }
      ...
    })

Note

You might also want to change the behavior while clicking a header, triggering collapse instead of jumping to the already visible table of contents of the folder.

If so, simply pass folderClickBehavior: "collapse" together with sortFn.

Hide from popover

To prevent the secret pages from showing up in the link popover, a straightforward solution is to assign noPopover attribute to the link whose content has the tag ‘exclusive’. Not the most elegant solution, but it works.

quartz/components/scripts/popover.inline.ts
document.addEventListener("nav", () => {
  const links = [...document.getElementsByClassName("internal")] as HTMLAnchorElement[]
  for (const link of links) {
    // mod: skip links with specific tag
    const targetUrl = new URL(link.href)
    // fetch content and check frontmatter
    fetch(targetUrl.toString())
      .then(res => res.text())
      .then(content => {
        const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/)
        if (frontmatterMatch) {
          const frontmatter = frontmatterMatch[1]
          if (frontmatter.includes('tags:') && frontmatter.includes('exclusive')) {
            link.dataset.noPopover = "true"
          }
        }
      })
      .catch(err => console.error(err))
    link.addEventListener("mouseenter", mouseEnterHandler)
    window.addCleanup(() => link.removeEventListener("mouseenter", mouseEnterHandler))
  }
})

Hide from Backlinks

The Backlinks component will show all the URLs that link to a specific note. Again, we can filter out the hidden pages.

quartz/components/Backlinks.tsx
export default ((opts?: Partial<BacklinksOptions>) => {
  const options: BacklinksOptions = { ...defaultOptions, ...opts }
 
  const Backlinks: QuartzComponent = ({
    fileData,
    allFiles,
    displayClass,
    cfg,
  }: QuartzComponentProps) => {
    const slug = simplifySlug(fileData.slug!)
    const backlinkFiles = allFiles.filter((file) => 
      file.links?.includes(slug) && 
      // mod: skip files with specific tag
      !file.frontmatter?.tags?.includes("exclusive")
    )
    ...
  }
 
  Backlinks.css = style
 
  return Backlinks
}) satisfies QuartzComponentConstructor

Cloudflare access control

To use Cloudflare Access, your domain should have its DNS records configured on Cloudflare, or that your application has been successfully deployed on pages.dev.

Go to your Cloudflare account’s dashboard, Zero Trust → Access → Applications, then create an application. Configure the path you’d like to block in Basic information tab. For a demo, a rule of Path: Digital-Basement/Being-Mortal/* indicates that any access that falls under this address will trigger the blockage.

For basic protection, create an access policy under Policy tab. To customize the Block page and Login page, go to Zero Trust → Settings.

Protecting pages.dev

In Cloudflare Pages, your site will automatically be hosted on your-site.pages.dev. It’ll also keep the deploy snapshots accessible, for example, on b6dbf918.your-site.pages.dev. To block the access to the snapshots, add the rule of Subdomain: *, Domain: your-site.pages.dev.