A theme-agnostic tool that makes your Obsidian articles Jekyll-ready.
Intaglio scans your Obsidian vault, converts the notes you've marked as shared into Jekyll-compatible posts, and writes them to your site — so your vault stays clean Markdown while Jekyll gets the flavour it expects.
It runs as a GitHub Action that opens a pull request against your site repository on every push, or as a local CLI if you'd rather keep your vault off GitHub.
- Auto-generates all the essentials for the frontmatter.
- Converts your
h1header to your post title. - Copies used images to Jekyll assets folder and updates
![[img]]links along with alt texts and width settings. - Converts
[[Wikilinks]]to standard Markdown links and links posts properly, including URLs and internal links. $Math$/Code/> [!Callout]/[[#^Block Link]]support, and more.- Syncs to your vault; removed posts and stale images get removed from your Jekyll site too.
- Your original Obsidian articles remain intact, the way you want them to be.
| Original Obsidian Article | Processed Jekyll Site |
|---|---|
![]() |
![]() |
- For the GitHub Action: nothing to install.
- For the local CLI: Python 3.10+ and uv.
Add share: true and date: YYYY-MM-DD to your post's frontmatter (Obsidian Properties). You can use a checkbox or plain text for share. date will be the publish date displayed on your Jekyll site. Anything else you add (slug, permalink, and so on) is optional and will be left alone.
The tool adds title, layout, and math (based on settings) to the frontmatter for you, so you don't have to configure those unless you want to override them.
---
share: true
date: 2026-01-01
---
# My Post TitleOnly posts with share: true will be processed.
Important
Set date yourself. The creation date the tool falls back to is not stable and may change on copy, clone, or sync. When it changes, the generated filename changes with it — and so does the post's permalink. The old URL starts returning 404, and every link anyone has already shared to that post breaks. This is also why the GitHub Action refuses to run at all when a shared note has no date.
Before setting anything up on GitHub, run the conversion on your own machine and have a look at the results:
uv tool install git+https://github.com/kckhchen/intaglio@v1This installs an intaglio command onto your PATH in its own isolated environment. If your shell can't find the command afterwards, run uv tool update-shell and open a new terminal.
@v1 follows the latest 1.x release; use a full tag such as @v1.5.0 to pin an exact version. To upgrade later:
uv tool upgrade --reinstall intaglio
# or if you prefer pipx:
# pipx install git+https://github.com/kckhchen/intaglio@v1From the root of your Jekyll site, create a config file:
cd /path/to/jekyll/site
intaglio initThis writes a commented .intagliorc. The only setting you need is the path to your vault:
# .intagliorc
VAULT_DIR="/path/to/obsidian/vault"
# or exported as environment variable with
# export VAULT_DIR="/path/to/obsidian/vault"JEKYLL_DIR defaults to wherever .intagliorc lives, so you don't need to set it. Run intaglio from that directory or any subdirectory of it.
Note
.intagliorc contains the absolute path to your vault and exposes your username. Consider adding .intagliorc to your .gitignore, or, if you wish to carry the config file around, you can export VAULT_DIR as environment variable.
Important
Earlier versions used a .env file. It is still read, but is deprecated and will warn on every run. Rename it to .intagliorc.
You can override the settings in .intagliorc either with environment variables or temporarily with CLI flags. CLI flags takes precedence over everything else. The tool automatically resolves the config. To see which config source is at work for each variable, use the command:
intaglio configStart with a dry run. It prints every action it would take without changing a single file:
intaglio run --dryLooks good? Drop the flag:
intaglio runNote
A Note on Styling: The first time you run the tool, it will create _includes/obsidian-callouts.html in your Jekyll repository. This file handles the icons and colors for your callouts. Feel free to customize it.
Important
If you set up a baseurl for your Jekyll site and found links dead due to duplicate baseurls e.g. blog/blog/my-post, flip PREVENT_DOUBLE_BASEURL to True in .intagliorc. This could happen for Jekyll 4.x or some special themes.
Once you like what the tool produces, this is the way to keep it running hassle-free. After setting the things up ,your publishing workflow becomes as simple as "write, commit, push". The Action converts the notes and sends a pull request to your Jekyll site with the formatted posts.
Three things have to be true before you start:
- Your vault is a GitHub repository, and the workflow file lives in it. The Action checks out the calling repository as the vault.
- Your Jekyll site is a separate repository. A single checkout cannot serve as both source and destination.
- Every shared note has an explicit
date. Modification time refreshes on every push, so the Action cannot fall back to it. A shared note without adatefails the run on purpose, rather than silently producing an unstable permalink.
If any of those don't suit you, skip ahead to running the CLI instead.
Generate a fine-grained token for your Jekyll site and set repository permissions to Contents: Read and write and Pull Requests: Read and write. Copy the token and paste it to your repository secrets in your vault repo, naming it BLOG_PUSH_TOKEN.
Create a publish.yml file under .github/workflows and paste the following snippet:
name: Publish
on:
push:
branches: [main] # or master based on your repo
workflow_dispatch: # for manual run
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: kckhchen/intaglio@v1
with:
jekyll-repo: username/jekyll-repo # your own repo name
token: ${{ secrets.BLOG_PUSH_TOKEN }}By default, this publishes new and updated posts only. Posts you delete from your vault will stay on your site (we don't want to delete anything without your explicit consent). To remove stale posts and images and sync your vault status, see the sync example below:
name: Publish
on:
push:
branches: [main]
workflow_dispatch:
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: kckhchen/intaglio@v1
with:
jekyll-repo: username/jekyll-repo # your own repo name
token: ${{ secrets.BLOG_PUSH_TOKEN }}
args: update --force --yesThis will update the posts and remove stale posts and images from your Jekyll site.
Important
If you set up a baseurl for your Jekyll site and found links dead due to duplicate baseurls e.g. blog/blog/my-post, set prevent-double-baseurl: true under the with: section in your .yml file. This could happen for Jekyll 4.x or some special themes. For more information about flags, check out GUIDE.md.
A few more things worth knowing:
- Unlike the CLI tool, the Action does not implement incremental builds. Incremental builds rely on file modification time, which refreshes on push. In other words, it process every post regardless it's been modified or not, just like the
--forceflag. This would not make any practical difference to your posts though. - Cleanup automatically proceeds. With no terminal to prompt at,
updateandcleanneed the--yesflag or they abort without deleting anything. To keep that safe, you can set amax-deletions(default to 10) limit that when reached, the process will send a warning. Regardless, you can always have a look before merging the PR. - For full configuration, check out action_config.md
Prefer to keep your vault off GitHub? The tool got ya. The CLI does the same conversion locally, and nothing in your vault ever leaves your machine. Just cd to your Jekyll directory and run any of the commands:
# Process new posts
intaglio run
# Process posts and clean up posts deleted from the vault
intaglio update
# Process only one post (use only the post name, not the relative path)
intaglio run --only "My Post.md"Check your _posts folder. Like how they look? Push it and publish to the world!
intaglio --help lists every command, and intaglio <command> --help lists the flags for one. The full list is in GUIDE.md.
Neither your original Obsidian notes nor hand-authored posts on your Jekyll site are ever touched when you run intaglio update: cleanup only removes files carrying the tool's own generator: intaglio frontmatter marker.
Note
Running intaglio with no command still means intaglio run. At a terminal it first shows which vault and site it is about to touch and asks you to confirm; in a script or CI it just runs.
Use this if you want to modify the tool or run the test suite:
git clone https://github.com/kckhchen/intaglio.git
cd intaglio
python3 -m venv .venv
source .venv/bin/activate
# or .venv\Scripts\activate.bat for Windows
pip install -e .You can find the full User Guide and Advanced Settings in GUIDE.md.
This tool converts most Obsidian Markdown syntax elements into Jekyll-compatible liquid tags or html tags, but these feature supports are not included (yet):
- Note embed (frame for another note inside current note)
- PDF embed
- Mermaid graphs
- Backlinks
If you would like any of these feature supports to be added to the tool, please open an issue or contact me. I will add these features ASAP.
Other things this tool doesn't support are non-note and non-text based elements, such as bases, canvas, and graph views.
This project is actively maintained and frequently updated. If you'd like to contribute, you can submit issues or fork this repository.
This project uses pytest for testing. The tests are in the tests folder. To run the tests locally, follow these steps:
- Install Dependencies
pip install -r requirements-dev.txt- Run the Test Suite To run all tests:
pytest
# or pytest --spec for spec reviewsTo run a specific test file:
pytest tests/test_process_images.pyThe test suite covers the conversion pipeline end to end across macOS, Linux and Windows on Python 3.10 and 3.13, and every post on my personal blog (in Mandarin Chinese) is generated by this tool. That said, Jekyll themes vary widely — if something renders oddly on your site, please open an issue.

