Jekyll Secret Posts is a lightweight, open-source Jekyll plugin designed to publish "share-only" posts. It easily integrates with any Jekyll project and is compatible with other Jekyll plugins.
At build time, the plugin hashes the Markdown file path with SHA-256 to generate unique URLs. Since these hashed URLs are excluded from sitemaps and search engine indexing, your posts remain accessible only to those with the direct link.
By enabling link-only access, the plugin allows for exclusive content sharing and gives more privacy to your website.
Add jekyll-secret-posts gem to your Gemfile:
gem "jekyll-secret-posts"or use GitHub repository link:
gem "jekyll-secret-posts", git: "https://github.com/developerlee79/jekyll-secret-posts.git"Or install the gem manually and specify the plugin in your _config.yml:
gem install jekyll-secret-postsplugins:
- jekyll-secret-postsCreate a _secret directory in your Jekyll project root and add markdown file(same as regular posts).
my-jekyll-site/
├── _config.yml
├── _layouts/
└── _secret/
└── article.md # Any .md files in this directory or its subfolders are supported
Since the URL is hashed, you cannot know it without inspecting the built output. The easiest way to see it is in the build log when you build; this plugin only exposes those URLs in the log via the list_urls option.
However, enabling this in production or CI can expose hashed URLs on external servers or in pipeline logs. For that reason, list_urls defaults to false so you can turn it on manually only in safe environments.
For this guide, we enable it so you can verify that secret URLs are generated. Add the list_urls option to _config.yml:
secret_posts:
list_urls: trueURLs are hashed with SHA-256 using the JEKYLL_SECRET_SALT environment variable as salt. The plugin still builds without it, but what gets hashed is only the collection name and the file path — both of which are usually public in your repository — so without a salt the URLs are guessable. The build warns when the salt is missing or shorter than 16 characters.
Use a long random value and keep it out of the repository:
openssl rand -hex 32Changing the salt changes every secret URL, so previously shared links stop working. Treat it as a long-lived secret.
Set it before build and build:
export JEKYLL_SECRET_SALT="my-secret"
bundle exec jekyll buildor run build with the environment variable:
JEKYLL_SECRET_SALT="my-secret" bundle exec jekyll buildThen you will be able to find the hashed URL in the build log like this:
Secret post URL: /s/eac1bc3d5e2cb1881215f42c7926d462/
AutoPages: Disabled/Not configured in site.config.
Pagination: Complete, processed 1 pagination page(s)
done in 1.825 seconds.
All done! Share the URL only with people who should see the post.
You can add custom settings to your _config.yml as follows:
secret_posts:
source_dir: "_secret"
collection_name: "secret"
url_prefix: "/s/"
index_layout: "default"
redirect_url: "/"
list_urls: false| Key | Default | Description |
|---|---|---|
source_dir |
"_secret" |
Directory containing target Markdown files. Derived from collection_name, not freely configurable — see below |
collection_name |
"secret" |
Internal Jekyll collection name, and therefore the source directory (_<collection_name>) |
url_prefix |
"/s/" |
URL prefix for hashed URLs |
index_layout |
"default" |
Layout used for the redirect page at the url_prefix |
redirect_url |
baseurl |
URL to which the url_prefix index page redirects (If unset, the site baseurl is used; if that is also unset, / is used) |
list_urls |
false |
When true, prints hashed URLs in Jekyll build log (Use only in safe environment) |
Jekyll resolves a collection's directory as _<collection_name> and ignores any other setting, so the source directory follows collection_name. To keep secret posts in _private/, set collection_name: "private" — setting source_dir: "_private" alone does nothing. A source_dir that disagrees with the derived directory is ignored with a build warning.
redirect_url must be either an http(s):// URL or a root-relative path such as /landing. Anything else — javascript:, data:, a protocol-relative //host, or a bare hostname — is rejected with a build warning and falls back to /.
list_urls must be a real YAML boolean. A quoted "true" is a string, not true, and leaves URL logging off; this fails closed on purpose, because the flag prints secret URLs into the build log.
Secret posts are unlisted, not access-controlled. The files are published as ordinary static pages, so anyone holding the URL can read them, and anyone who can read your repository can recompute the URL if they also know the salt.
The plugin does the following on every secret page:
- sets
sitemap: falseso the URL stays out ofjekyll-sitemapoutput - injects
<meta name="robots" content="noindex, nofollow"> - injects
<meta name="referrer" content="no-referrer">, so the secret URL is not handed to third parties in theRefererheader of outbound links, images, or scripts
Only files with YAML front matter become secret posts. Jekyll reads a file without front matter as a static file, which never goes through the hashing, so such files would be published unhashed at /<collection_name>/<path>. The plugin drops them from the build instead and names them in a build warning — so attachments do not belong in the secret directory. Put images and downloads somewhere else and link to them; note that they are then public to anyone who guesses their URL.
If your site already declares a collection under the same name, the plugin takes over that collection's URLs and removes it from the sitemap, and warns while doing so. Set collection_name to something else to keep the existing collection public.
It cannot protect against the following, which are up to your site:
- Templates that iterate over all documents. Search-index generators and "all content" listings usually loop over
site.documentsorsite.collections, which include secret posts. A template like{% for d in site.documents %}will publish every secret URL and title intosearch.json. Exclude the secret collection in any such template. - Server-side indexing.
noindexis a request; crawlers that ignore it, and anyone with the link, still get the content. - Anything requiring real authentication. If the content must not be readable by a URL holder, this plugin is the wrong tool.
Secret posts live in a collection, not in site.posts, so they are not included in pagination.
Secret documents are set to sitemap: false, so they do not appear in the sitemap.
If you use Polyglot for localization and do not plan to support multiple languages for secret posts, add the secret source directory to exclude_from_localization in your _config.yml so that secret posts are not processed for multiple languages:
exclude_from_localization: ["images", "css", "scss", "js", "_secret"]Contributions are welcome. If you have an improvement or idea, feel free to open a pull request.