Password-protected encrypted pages. Encrypts page content at build time using AES-256-GCM and decrypts client-side with the Web Crypto API. Passwords are set per-page via frontmatter. A companion emitter writes an encrypted shadow content index so unlisted encrypted pages can be dynamically revealed in graph, explorer, and search after a successful decryption — without ever leaking their metadata to visitors who do not hold the password.
### Hiding encrypted pages from discovery surfaces
By default, encrypted pages still appear in the graph, explorer, search, RSS, sitemap, and backlinks — visitors can see the page exists and its title, but cannot read the content without the password.
To hide an encrypted page entirely until a visitor successfully decrypts it, set `unlisted: true` in its frontmatter:
```yaml
---
title:My Secret Page
password:mysecretpassword
unlisted:true
---
```
An unlisted page:
- Is **absent** from `contentIndex.json`, `sitemap.xml`, the RSS feed, backlinks, recent notes, folder listings, tag listings, bases views, graph, explorer, and search.
- Is still emitted as HTML, so it remains accessible by direct URL.
- Has its metadata (slug, title, links, tags) written to a separate `static/encryptedContentIndex.json` file, encrypted with the page's own password.
- Is dynamically re-added to the in-memory content index when a visitor successfully decrypts it, so graph, explorer, and search reflect it for the rest of the browser session. Server-rendered listings — backlinks, recent notes, tag pages, folder listings, and [[BasesPage|bases views]] — remain statically hidden even after decryption because they are baked as HTML at build time.
To make this the default for every encrypted page on your site, set `unlistWhenEncrypted: true` in the plugin options. Individual pages can then opt back in with `unlisted: false`.
> [!note]
> The `unlisted: true` frontmatter field above only takes effect for encrypted pages when this plugin is installed. If you also want `unlisted: true` to work on **non-encrypted** pages across your site, install [[UnlistedPages]] alongside this one. The two plugins compose cleanly — when both are enabled, `unlisted: true` hides any page, encrypted or not, from every discovery surface that respects the `file.data.unlisted` convention.
By default, an `unlisted: true` encrypted page is _revealed_ in graph, explorer, and search after a visitor successfully decrypts it. This is usually what you want: the user just proved they know the password, so showing them the page in the sidebar makes sense for the rest of their session.
If you instead want a page that stays permanently invisible — accessible only by direct URL, even to users who have successfully decrypted other pages on the same site — set `stealth: true` in its frontmatter:
```yaml
---
title:Deep Secret
password:mysecretpassword
stealth:true
---
```
A stealth page:
- Is **absent** from every discovery surface, same as any `unlisted` page.
- Has **no entry** in the shadow content index (`encryptedContentIndex.json`). The plugin deliberately skips stealth pages when building the shadow index.
- Stays hidden even after the visitor enters the correct password. Since there is no shadow-index entry to decrypt, there is nothing to patch into the in-memory content index — graph, explorer, and search never learn the page exists. Only the decrypted HTML is visible to the user on the page itself.
- The password is still cached in session storage, so re-visiting the same stealth page will auto-unlock it.
`stealth: true` implies `unlisted: true` — you do not need to set both, and if you write `stealth: true, unlisted: false` the stealth flag wins. On non-encrypted pages `stealth: true` has no effect (there is no shadow index to skip).
Use stealth pages for "secret door" content that should only reach users who already know the exact URL: private notes linked from an external wiki, personal pages you send to specific people, or anything you never want to show up in a site-internal search even to authenticated readers.
This plugin provides a transformer, an emitter, and a component. All options are set on a single config entry and shared between the transformer and the emitter — Quartz instantiates both automatically.
> The `EncryptedPages` transformer replaces the entire HAST tree of an encrypted page with an opaque ciphertext container. Any transformer that needs to read the real HTML — in particular [[CrawlLinks]], which populates the links used by backlinks and the shadow content index — must run **before** `EncryptedPages`. Use the `order` field in `quartz.config.yaml` to control this.
- Plaintext is stripped from search indices, RSS feeds, and the shadow content index regardless of visibility setting.
- The shadow content index is a flat array of opaque encrypted blobs. An attacker who downloads it learns only the number of unlisted encrypted pages and the PBKDF2 iteration count — no slugs, titles, or link relationships leak.
- Passwords are set per-page in frontmatter. Avoid committing passwords to public repositories.
- This is client-side encryption of a static site. It protects against casual browsing but not against determined attackers with access to the page source.