Below you will find a piece of python code which copies items from your published obsidian vault folder, over to your quartz content folder.
I made this because I installed quartz empty and want to semi-manually add my files if I want to publish them to quartz, but in an automated way.
This script can be used for any file copy intentions, however, my use case is the Obsidian and Quartz 4 integration.
The full script is at the bottom of the page
Please note
This script is intended to use with the empty folder Quartz install, not with copy or symlink.
Functional design of the code
Step 1: Deleting the source contents of the Quartzcontent folder.
Before anything can be copied from your Obsidian vault, the script first wipes the Quartz content folder clean. This ensures Quartz receives a perfect one-to-one mirror of your Published folder, without leftover files from previous runs.
This is intentional. Quartz does not introspect or clean itself — if old files remain, they may continue to appear on your published site even after you remove them from Obsidian. Clearing the folder guarantees a predictable, consistent state.
Here’s the function:
def clean_dest_contents():
"""
Ensure DEST_DIR exists, then remove everything *inside* it
without deleting the DEST_DIR folder itself.
"""
if not os.path.exists(DEST_DIR):
print(f"[INFO] Destination does not exist, creating: {DEST_DIR}")
os.makedirs(DEST_DIR, exist_ok=True)
return
print(f"[INFO] Clearing existing contents of: {DEST_DIR}")
for entry in os.listdir(DEST_DIR):
path = os.path.join(DEST_DIR, entry)
if os.path.isdir(path) and not os.path.islink(path):
shutil.rmtree(path)
else:
os.remove(path)
What this step does
-
Checks if the Quartz
contentfolder exists
If it doesn’t, the script creates it. This makes the script safe to run on a freshly cloned Quartz repository. -
Deletes all files and folders inside
content
Important: the folder itself is not deleted — only its contents.
Quartz expects this folder to exist. -
Removes everything, regardless of type
Regular files, images, Markdown notes, subfolders, and old assets all get removed.
Symlinks get removed as files.
Why this step matters
-
It prevents “ghost notes” from older versions of your site.
-
It ensures your website always reflects only the notes you intended to publish.
-
It removes the need to manually clean your Quartz repo when reorganizing notes.
-
It keeps the sync behaviour simple and transparent:
ObsidianPublished→ Quartzcontentwith no questions asked.
When this step is dangerous
If you store anything manually inside your Quartz content folder that is not generated by this script, you will lose it.
The folder is meant to be treated as disposable and fully regenerated on each run.
If you need special files (custom images, scripts, or assets), either:
-
store them in Obsidian so they get copied automatically, or
-
place them outside the
contentfolder (for example in Quartz’sstatic/folder)
Step 2: Copying files from your Obsidian folder into Quartz
After the destination content folder has been cleared, the script walks through your Obsidian Published folder and copies everything into Quartz:
def copy_tree(src_root, dst_root):
for root, dirs, files in os.walk(src_root):
# Filter ignored dirs in-place so os.walk won't descend into them
dirs[:] = [d for d in dirs if d not in IGNORED_DIRS]
# Compute relative path from src_root and corresponding dst dir
rel_path = os.path.relpath(root, src_root)
dst_dir = os.path.join(dst_root, rel_path) if rel_path != "." else dst_root
os.makedirs(dst_dir, exist_ok=True)
for name in files:
if name in IGNORED_FILES:
continue
src_file = os.path.join(root, name)
dst_file = os.path.join(dst_dir, name)
shutil.copy2(src_file, dst_file)
print(f"[COPY] {src_file} -> {dst_file}")
A few important details here:
-
os.walk(src_root)iterates over all subfolders in your ObsidianPublishedfolder. -
dirs[:] = [...]removes ignored directories in-place, which meansos.walkwill not even descend into those folders. -
rel_pathkeeps your folder structure intact. If you havePublished/coffee/recipes, you’ll getcontent/coffee/recipes. -
shutil.copy2is used instead ofshutil.copyso file metadata (timestamps, etc.) is preserved as much as possible.
This is the part that gives you the “semi-manual but automated” behaviour: you decide what lives in your Obsidian Published folder, and the script mirrors that into Quartz exactly.
Step 3: Ignore lists (keeping junk and internal stuff out)
The script has two ignore lists at the top:
# Directories to ignore by name
IGNORED_DIRS = {
".obsidian",
".trash",
".git",
".github",
"node_modules",
"__pycache__",
".SynologyWorkingDirectory",
}
# Files to ignore by name
IGNORED_FILES = {
"Thumbs.db",
"desktop.ini",
}
These are there to prevent:
-
Obsidian’s own workspace/config files from ending up in your Quartz site.
-
Synology and Windows junk (like
Thumbs.db) from polluting your repo. -
Development folders like
node_modulesor__pycache__from being copied.
You can freely add or remove entries depending on what lives in your vault. For example, if you have a local “scratch” or “archive” folder you never want to publish, just add its folder name to IGNORED_DIRS.
Step 4: Configuration – what you must change
At the top of the script you’ll find:
# Obsidian "Published" folder (source)
SOURCE_DIR = r"C:\path/to/your/obsidian/vault/published/folder"
# Quartz "content" folder (destination)
DEST_DIR = r"C:\Users\<youruserhere>\github\quartz\content"
You need to:
-
Point
SOURCE_DIRto the folder in your Obsidian vault that contains only the notes you want to publish (for example aPublishedorPublicfolder). -
Point
DEST_DIRto your Quartz 4contentfolder.
Important: DEST_DIR itself is not deleted, but everything inside it is cleared on each run. So make sure that folder is dedicated to Quartz content you’re happy to regenerate.
Step 5: How to run the script
-
Save the script to a file, for example:
C:\Users\<youruser>\scripts\sync_obsidian_to_quartz.py -
Make sure Python is installed and available in your
PATH. -
Open a terminal (PowerShell or Command Prompt) and run:
python C:\Users\<youruser>\scripts\sync_obsidian_to_quartz.py- You’ll see log lines like:
=== Sync Obsidian -> Quartz content ===
Source: C:\...\Published
Destination: C:\...\quartz\content
[INFO] Clearing existing contents of: C:\...\quartz\content
[COPY] C:\...\Published\index.md -> C:\...\quartz\content\index.md
[COPY] C:\...\Published\coffee\recipe.md -> C:\...\quartz\content\coffee\recipe.md
[DONE] Quartz content is now in sync with Obsidian.
From there, you can run your normal Quartz build/deploy command (for example via npx quartz build or your GitHub Actions workflow).
Safety considerations
Be very clear about one thing:
The script deletes everything inside
DEST_DIRbefore copying.
That’s intentional, because Quartz should mirror your Obsidian Published folder exactly. But it also means:
-
Don’t point
DEST_DIRat any folder that contains files you manually manage and don’t want to lose. -
Keep
DEST_DIRexclusively for Quartz content that’s generated or synced. -
If you want a safer first run, you can temporarily comment out the
clean_dest_contents()call and just inspect what would be copied.
If you want to make it extra safe, you could create a Git commit in your Quartz repo before running the script. That way, if something goes wrong, you can just revert in Git.
Optional extensions (ideas)
If you want to push this further, some possible upgrades:
-
Dry-run mode: add a flag that prints what would be deleted/copied without actually changing files.
-
Selective sync: instead of clearing the whole
contentfolder, only overwrite files that changed or only sync a subfolder (e.g.Published/coffee→content/coffee). -
Logging to file: write all
[COPY]lines to a log file so you can see what changed during each run. -
Pre-/post-hooks: run
git status,git addornpx quartz buildautomatically after syncing.
For my use case, this “dumb but predictable” behaviour is exactly what I want: I curate which notes go into my Obsidian Published folder, and the script takes care of mirroring that state into Quartz in one command.
The full script:
#!/usr/bin/env python3
"""
Sync Obsidian Published folder -> Quartz content folder.
- Clears all files/subfolders inside the Quartz content folder
- Copies everything from source to destination
- Skips some known junk folders/files
"""
import os
import shutil
# === CONFIG – ADJUST THESE ===
# Obsidian "Published" folder (source)
SOURCE_DIR = r"C:\path/to/your/obsidian/vault/published/folder"
# Quartz "content" folder (destination)
DEST_DIR = r"C:\Users\<youruserhere>\github\quartz\content"
# Directories to ignore by name
IGNORED_DIRS = {
".obsidian",
".trash",
".git",
".github",
"node_modules",
"__pycache__",
".SynologyWorkingDirectory",
}
# Files to ignore by name
IGNORED_FILES = {
"Thumbs.db",
"desktop.ini",
}
# === IMPLEMENTATION – NO NEED TO TOUCH BELOW THIS LINE ===
def ensure_source_exists():
if not os.path.isdir(SOURCE_DIR):
raise SystemExit(f"[ERROR] Source directory does not exist:\n {SOURCE_DIR}")
def clean_dest_contents():
"""
Ensure DEST_DIR exists, then remove everything *inside* it
without deleting the DEST_DIR folder itself.
"""
if not os.path.exists(DEST_DIR):
print(f"[INFO] Destination does not exist, creating: {DEST_DIR}")
os.makedirs(DEST_DIR, exist_ok=True)
return
print(f"[INFO] Clearing existing contents of: {DEST_DIR}")
for entry in os.listdir(DEST_DIR):
path = os.path.join(DEST_DIR, entry)
if os.path.isdir(path) and not os.path.islink(path):
shutil.rmtree(path)
else:
os.remove(path)
def copy_tree(src_root, dst_root):
for root, dirs, files in os.walk(src_root):
# Filter ignored dirs in-place so os.walk won't descend into them
dirs[:] = [d for d in dirs if d not in IGNORED_DIRS]
# Compute relative path from src_root and corresponding dst dir
rel_path = os.path.relpath(root, src_root)
dst_dir = os.path.join(dst_root, rel_path) if rel_path != "." else dst_root
os.makedirs(dst_dir, exist_ok=True)
for name in files:
if name in IGNORED_FILES:
continue
src_file = os.path.join(root, name)
dst_file = os.path.join(dst_dir, name)
shutil.copy2(src_file, dst_file)
print(f"[COPY] {src_file} -> {dst_file}")
def main():
print("=== Sync Obsidian -> Quartz content ===")
print(f"Source: {SOURCE_DIR}")
print(f"Destination: {DEST_DIR}")
print()
ensure_source_exists()
clean_dest_contents()
copy_tree(SOURCE_DIR, DEST_DIR)
print()
print("[DONE] Quartz content is now in sync with Obsidian.")
if __name__ == "__main__":
main()