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 fromFileNode
inquartz/components/ExplorerNode.tsx
, toFileTrieNode
inquartz/util/fileTrie.ts
.The notes filtered out at the indexing stage in
linkIndex.set()
atquartz/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.
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.
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.
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.
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.
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 withsortFn
.
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.
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.
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
.