Use Logseq as your blog editor. Write posts in your graph and mark them with type:: blog property. Logseq-Blogger discovers them and when you set the property status:: online invokes syndicator.
logseq-blog syndicate
[Logseq Graph]───────○ )───────[Logseq-Blogger]───────( ○───────[Syndicator]
Logseq-Blogger uses the logseq-blog interface to retrieve blog posts from a Logseq Graph and forwards these blog posts to Syndicator using the syndicate interface.
- The logseq-blog interface is specified below.
- The
syndicatorinterface is specified in syndicator.
uv run logseq-blogger syndicate # full syndication of all ::online blog posts that have no ::syndicated-at
uv run logseq-blogger list # list slug of all blog posts with there ::syndicated-at status
uv run logseq-blogger syndicate --post <slug> # full syndication of the blog post with the given slug
uv run logseq-blogger redeploy --post <slug> # regenerate the static hugo page for the blog post with the given slug
uv run logseq-blogger version # print versionThe logseq-blog interface is the contract between a Logseq Graph and Logseq-Blogger. A blog post is a Logseq markdown fragment marked with type:: blog.
Under the Logseq graph root (logseq_dir in config.local.yaml):
| Path | Role |
|---|---|
journals/ |
Journal files (nested-list format) |
pages/ |
Page files (top-level metadata format) |
Every *.md file in those directories is scanned. A post is found wherever a property block contains the substring type:: blog.
| Rule | Behaviour |
|---|---|
type:: blog |
Required discovery marker |
status:: online |
Only online posts are considered for hand-off |
syndicated-at:: present |
Skipped by syndicate (already handed off) |
Logseq properties use key:: value syntax.
| Property | Required | Notes |
|---|---|---|
type |
yes | Must be blog |
status |
yes (for publish) | Only online is selected |
date |
yes | YYYY-MM-DD; used in the slug |
title |
yes | Used in the slug (spaces → _) |
author |
yes | Passed through to syndicate meta.author |
header |
yes for hand-off | Markdown image, e.g. . File must exist on disk |
summary |
no | Explicit summary; otherwise first body text block |
language |
no | Word form: german, english, spanish, french, italian → de/en/es/fr/it (default de) |
position |
no | Free text or coordinates; passed through to syndicate meta.position |
syndicated-at |
written by tool | ISO-8601 timestamp set after successful syndicate |
Metadata lives in the first list item under a parent (commonly [[Blog]]). Sibling list items are the body.
- [[Blog]]
-
type:: blog
status:: online
language:: german
date:: 2026-05-19
position:: 38.83388, 20.69757
title:: Charly Superstar
author:: Benno
header:: 
syndicated-at:: 2026-08-03T11:27:19+02:00
- Wie die meisten Hündeler glauben auch wir: unser Hund ist der schönste und liebste der Welt.
- 
- Charly scheint aber wirklich eine besondere Wirkung auf die Menschen zu haben.
- {{video https://youtube.com/shorts/_yNn9MUXBaw}}- Properties are indented under the
type:: blogbullet - Each subsequent list item becomes a content block
- Multiple blog posts may appear in one journal file
- Non-blog journal content is ignored
Properties at the top of the file; body as list items below.
type:: blog
status:: online
language:: german
date:: 2026-05-19
position:: 38.83388, 20.69757
title:: Charly Superstar
author:: Benno
header:: 
syndicated-at:: 2026-08-03T11:27:19+02:00
- Wie die meisten Hündeler glauben auch wir: unser Hund ist der schönste und liebste der Welt.
- 
- Charly scheint aber wirklich eine besondere Wirkung auf die Menschen zu haben.
- {{video https://youtube.com/shorts/_yNn9MUXBaw}}- One blog post per page file
Body list items are classified as:
| Kind | How it is recognised |
|---|---|
title |
Line starts with # … ###### |
text |
Ordinary prose (nested bullets become * … lines) |
media |
First line is  — image or local video by extension |
youtube |
First line is {{video https://…}} with a YouTube URL |
git clone git@github.com:bbaumgartner/logseq-blogger.git
cd logseq-blogger
curl -LsSf https://astral.sh/uv/install.sh | sh # if uv is missing
uv sync
cp config.local.yaml.example config.local.yaml # paths, SFTP host, webhook URLsuv sync --group dev
uv run pytestThis is a cli built with python. Simplicity and Maintainability are most important NFRs. It should just wire together various programs that do the complex things and hence, act as an orchestrator. The most complex thing this project does is parse Logseq Markdown files. That's at the very limit of acceptable complexity in this project. It was considered to pull that out into a dedicated project, however, with LLM support this complexity should be manageable, if not we can still pull it out or use a library. The runtime environment for this is Linux, Mac support is nice to have, Windows is out-of-scope.
classDiagram
direction TB
class cli {
entry point
parse commands, dispatch
}
class config {
load YAML → Config
}
class extract {
scan journals/pages → BlogPost
}
class marker {
read/write syndicated-at::
}
class trigger {
stage, upload, webhooks, mark done
}
class sftp {
resumable SFTP upload
}
class webhook {
HTTP POST /publish, /reel
}
cli --> config : load_config
cli --> trigger : syndicate / redeploy / list
trigger --> config
trigger --> extract : scan_blog_posts
trigger --> marker : is_syndicated / set_syndicated_at
trigger --> sftp : sftp_session / upload
trigger --> webhook : post_webhook
marker --> extract : BlogPost
sftp --> config : host, key
| Module | Role |
|---|---|
cli |
Entry point: parse commands, load config, dispatch |
config |
YAML → Config dataclasses |
extract |
Scan journals/pages → BlogPost domain model |
marker |
Read/write syndicated-at:: on source files |
trigger |
Stage, upload, fire webhooks, mark done |
sftp |
Resumable SFTP upload to syndicator staging |
webhook |
HTTP POST to n8n /publish and /reel |
