Comparison

Exporting Notes to PDF from Notion, Joplin, Logseq, Bear and Six More Apps

Published July 30, 2026

Every note app claims Markdown support and almost none of them mean the same thing by it. Some store standard Markdown files on disk. Some store a proprietary format and export something Markdown-shaped. Some add syntax that exists nowhere else. When you export to PDF, those differences surface as broken images, literal syntax in your output, or a document that looks nothing like the note.

This guide compares nine apps on the two things that matter for conversion — export fidelity and built-in PDF quality — then covers a workflow that produces a clean document from any of them.

The comparison

AppStorageMarkdown exportBuilt-in PDFMain export problem
NotionProprietary cloudGood, with caveatsFairSigned image URLs expire; databases flatten
ObsidianPlain .md filesNative — it is MarkdownFairWikilinks dead outside the vault
JoplinSQLite + MarkdownVery goodGoodResource IDs need rewriting
LogseqPlain .md filesGoodFairOutline structure becomes deep nesting
Roam ResearchProprietary cloudFairPoorBlock refs and attributes have no equivalent
BearProprietary SQLiteGoodGoodCustom tag syntax; no standard frontmatter
CraftProprietary cloudGoodVery goodBlocks and cards lose structure
iA WriterPlain .md filesNativeVery goodContent blocks are non-standard
UlyssesProprietary libraryGoodExcellentSheet structure lost; own markup dialect
EvernoteProprietary (ENEX)None directlyFairNot Markdown at all — needs conversion

Two patterns in that table. Apps storing plain .md files (Obsidian, Logseq, iA Writer) have no export step to go wrong — the files are already there. Apps with excellent built-in PDF export (Ulysses, Craft, iA Writer) tend to be the ones with the most opinionated, least portable Markdown.

Notion

The most widely used and among the more troublesome.

Exporting

Settings on a page → ExportMarkdown & CSV. For a whole workspace, Settings & members → Settings → Export all workspace content.

Options that matter: Include subpages (nests them as separate files) and Create folders for subpages (affects the directory structure and therefore the relative image paths).

You get a .zip containing .md files, a folder of images per page, and .csv files for any databases.

The expiring URL problem

This is the significant one. Notion’s exported Markdown sometimes references images by signed S3 URL with a short expiry rather than by local path — particularly for content that was recently added or for certain block types. Those URLs stop working within about an hour.

The consequence: an export that converts fine immediately produces missing images if you convert it a week later. If you are archiving Notion content, download and rewrite the assets at export time:

#!/usr/bin/env bash
# Rewrite remote image URLs in exported Markdown to local files.
set -euo pipefail

mkdir -p assets
i=0

while IFS= read -r url; do
  i=$((i+1))
  ext="${url##*.}"; ext="${ext%%\?*}"          # strip query string
  [ ${#ext} -gt 5 ] && ext="png"                # fall back if odd
  file="assets/img-$i.$ext"
  curl -sL "$url" -o "$file"
  # Escape the URL for use in sed
  esc=$(printf '%s' "$url" | sed 's/[&/\]/\\&/g')
  sed -i.bak "s|$esc|$file|g" note.md
done < <(grep -oE 'https://[^)]*amazonaws[^)]*' note.md | sort -u)

rm -f note.md.bak
echo "Localised $i images"

Run it immediately after exporting, before the URLs expire.

Notion-specific artefacts

Page titles become # Heading plus the filename gets a hash suffix (Meeting Notes 4f8a9c2b.md). Strip the hash before it appears in output.

Databases export as CSV, not as Markdown tables. Converting them:

# Requires csvkit
csvlook --no-inference tasks.csv

Or a small script to emit a GFM table:

awk -F',' 'NR==1{
    printf "|"; for(i=1;i<=NF;i++) printf " %s |", $i; print "";
    printf "|"; for(i=1;i<=NF;i++) printf " --- |";     print "";
    next
  }
  { printf "|"; for(i=1;i<=NF;i++) printf " %s |", $i; print "" }' tasks.csv

That breaks on quoted fields containing commas — use a real CSV parser for anything non-trivial.

Toggles, callouts and columns flatten. A toggle becomes its content with no indication it was collapsible; column layouts become sequential blocks. Usually acceptable, occasionally confusing.

Synced blocks export as duplicated content in each location.

Comments and page properties are not exported at all.

Evernote

Not a Markdown app. Evernote stores ENML, an XHTML dialect, and exports .enex files. Getting to Markdown is a real conversion.

The most reliable route is via Joplin, which has a mature ENEX importer:

  1. Export from Evernote: select notebook → File → Export Notes.enex.
  2. In Joplin: File → Import → ENEX (as Markdown).
  3. Then export from Joplin as Markdown.

This preserves attachments, note titles, creation dates and tags better than direct converters, because Joplin’s importer has had years of edge-case fixes.

Direct alternatives exist (yarle is the most capable) and are worth it if you need control over the output structure — Yarle has extensive options for frontmatter, tag handling and attachment naming.

Things that will not survive regardless of route: Evernote’s handwriting recognition data, note encryption (decrypt first), reminders, and the Web Clipper’s original page styling. Tables convert with variable success; complex nested tables usually need manual repair.

Joplin

Among the best-behaved apps here, because it stores Markdown natively and was designed for portability.

Exporting

File → Export offers JEX (Joplin’s own archive), Markdown, PDF, and HTML. Choose Markdown for a directory of .md files with a _resources/ folder.

The resource ID problem

Joplin references attachments by internal ID:

![diagram](:/8f3a9c2b1d4e5f6a7b8c9d0e1f2a3b4c)

Those :/id references mean nothing outside Joplin. Markdown export usually rewrites them to _resources/ paths, but if you are working from a JEX archive or the raw database you will need to map them yourself. The resource files are named by ID with their original extension, so the mapping is mechanical:

# Rewrite :/id references to _resources/ paths
perl -pi -e 's{\(:/([0-9a-f]{32})\)}{(_resources/$1.png)}g' *.md

That assumes .png; a robust version reads the actual extension from the resources directory.

Built-in PDF

Joplin’s PDF export is Electron/Chromium, so the same constraints as Obsidian: no page numbers or margin boxes. It respects custom CSS via Settings → Appearance → Custom stylesheet for rendered Markdown, and it honours @media print, so the Obsidian CSS approach in the Obsidian guide transfers with different selectors — target #rendered-md.

Logseq

Stores plain .md (or .org) files, so no export needed for the raw content — the files are in your graph directory under pages/ and journals/.

The outline problem

Logseq is outline-first: everything is a bullet, and nesting is structural. Exported Markdown reflects that:

- Project kickoff
	- Attendees
		- Jordan
		- Katrin
	- Decisions
		- Ship in June
			- Blocked on the migration

Valid Markdown, and not how a document should read. Five levels of nesting for what is conceptually a heading with a list under it. Converting to prose structure means promoting the top levels to headings, which is a manual editorial pass — there is no automatic transformation that gets it right, because the mapping from outline depth to heading level is a judgement about your content.

For notes you intend to export as documents, writing them with less nesting from the start is easier than fixing them afterwards.

Logseq-specific syntax

SyntaxMeaningPortable?
[[Page]]Page referenceNo — same as Obsidian wikilinks
((block-uuid))Block referenceNo — becomes literal text
#tagTagRenders as text; usually harmless
key:: valueBlock propertyNo — appears as literal key:: value
{{query ...}}Dynamic queryNo — literal text
{{embed ...}}EmbedNo — literal text
TODO / DOING / DONETask markersLiteral text
LOGBOOK blocksTime trackingLiteral text — strip these

Properties and LOGBOOK blocks are the ones that most visibly pollute output:

# Strip Logseq properties and logbook blocks
perl -ne 'next if /^\s*[a-zA-Z-]+:: /;
          next if /^\s*:LOGBOOK:/ .. /^\s*:END:/;
          print' page.md > page.clean.md

Roam Research

The hardest of these to get a clean document from, because Roam’s model is the least document-like.

Export via the ... menu → Export AllMarkdown. You get one .md per page.

Roam-specific syntax that has no Markdown equivalent:

  • ((block-ref)) — block references become opaque UUIDs
  • {{[[TODO]]}} — task markers become literal text
  • {{[[query]]: ...}} — queries become literal text
  • {{[[table]]}} and {{[[kanban]]}} — render as literal text
  • #[[Tag With Spaces]] — becomes literal
  • ^^highlight^^ — Roam’s own highlight syntax, not standard
  • Attributes (Key:: value) — literal text

A cleanup pass handles the mechanical ones:

perl -pe '
  s/\{\{\[\[TODO\]\]\}\}/[ ]/g;
  s/\{\{\[\[DONE\]\]\}\}/[x]/g;
  s/\^\^([^^]+)\^\^/**$1**/g;
  s/#\[\[([^\]]+)\]\]/$1/g;
  s/\[\[([^\]]+)\]\]/$1/g;
' page.md > page.clean.md

Block references cannot be fixed mechanically — the referenced content lives in another page and resolving it requires reading the whole graph. If your notes lean heavily on block references, expect substantial manual work, and consider whether a document is the right output at all.

Bear

Stores notes in a proprietary SQLite database but exports clean Markdown.

Note → Export NotesMarkdown. For everything, select all notes first. Bear also exports a .textbundle, which packages the Markdown with its assets — a better choice when notes have images, since paths stay correct.

Bear-specific things:

Tags are inline (#project/active) and appear as literal text in output. They are part of the note body, not metadata, so they export as written. Strip them if they are noise:

perl -pe 's/(^|\s)#[\w\/-]+//g' note.md > note.clean.md

That will also remove legitimate # characters preceded by whitespace, so check the result.

No YAML frontmatter. Bear has no metadata concept beyond tags, so there is nothing to strip — and nothing to carry a title or date into the export.

The first line becomes the title. Bear treats line one as the note title, so exports start with a # Heading derived from it.

Bear’s built-in PDF export is genuinely good — clean typography, sensible margins, respects the app’s themes. For a single note where you like Bear’s aesthetic, it is a reasonable end point.

Craft

Craft’s export is good and its PDF output is among the best here — it was designed with document output as a first-class concern rather than an afterthought.

Share → Export → Markdown, or Export → PDF for direct output.

The structural caveat: Craft is block-based, with cards, sub-pages and nested documents. A Craft document with cards exports as sequential Markdown, losing the visual grouping that made it readable. Sub-pages export as separate files or inline depending on the option chosen.

Craft’s PDF export handles page breaks, has real margin control, and produces well-typeset output. If you are already in Craft and want a PDF, using its own export is usually better than routing through Markdown — the round trip loses more than it gains.

iA Writer

Plain .md files on disk, so nothing to export. iA Writer’s PDF output is excellent: careful typography, good default margins, and its templates are genuinely well designed.

The non-standard element is content blocks — a way of transcluding another file:

/Users/jordan/Documents/chapter-two.md

A bare file path on its own line becomes an embedded file. iA Writer resolves it; nothing else does, and in another tool it renders as a literal path. If you use content blocks to assemble long documents, flatten them before converting elsewhere.

iA Writer’s templates are HTML and CSS, and you can write your own. File → Export → PDF uses the selected template, and custom templates are .iatemplate bundles containing a document.html and stylesheets. This is the most flexible route available inside the app, and if you invest in a template you get output as controlled as anything print CSS gives you elsewhere.

Ulysses

The best built-in PDF export of the nine, and the least portable Markdown.

Ulysses uses its own markup dialect — “Markdown XL” — which extends Markdown with annotations, inline notes, footnote syntax variants, and its own image handling. Its library is proprietary; notes are not files on disk you can point another tool at.

Export via File → Export → PDF for output, or File → Export → Text → Markdown to get portable text.

Ulysses-specific things that do not travel:

  • Annotations and inline notes — editorial comments, excluded from export by design
  • Goals and writing statistics — app state, not content
  • Sheet structure — a Ulysses project is many sheets; export merges or separates depending on options, and the organisation is lost either way
  • Materials (attached notes and images per sheet) — handled separately from body text

Its PDF export offers real control: page size, margins, headers and footers with page numbers, and styled output through its Styles system. If PDF is your output and you write in Ulysses, its own export is the right choice. Exporting Markdown to convert elsewhere makes sense only if you need something Ulysses cannot do — citations, for instance.

Non-standard syntax, summarised

The portability problem across all these apps reduces to a handful of patterns:

PatternAppsStandard?
[[wikilink]]Obsidian, Logseq, RoamNo
((block-ref))Logseq, RoamNo
key:: valueLogseq, RoamNo
{{query}} / {{embed}}Logseq, RoamNo
:/resource-idJoplinNo
Bare file path transclusioniA WriterNo
^^highlight^^RoamNo
==highlight==Obsidian, othersGFM-adjacent; widely supported
#inline-tagBear, Obsidian, LogseqRenders as text
- [ ] task listsMostYes — GFM standard
$math$MostExtension, widely supported
```mermaidMostExtension, widely supported

The last three are safe. Everything above them needs handling.

A universal clean-export workflow

Regardless of source app, this sequence produces a document:

1. Export Markdown, with assets. Prefer whatever option bundles images (textbundle, “include attachments”, a zip with a resources folder) over one that leaves remote URLs.

2. Localise remote images immediately. Especially for Notion. Signed URLs expire; do this before anything else.

3. Strip app-specific syntax. A single pass covering the patterns above:

#!/usr/bin/env bash
# clean-export.sh input.md > output.md
set -euo pipefail

perl -pe '
  # Wikilinks: keep the display text
  s/\[\[([^\]|]+)\|([^\]]+)\]\]/$2/g;
  s/\[\[([^\]]+)\]\]/$1/g;
  # Roam highlight to bold
  s/\^\^([^^]+)\^\^/**$1**/g;
  # Roam/Logseq task markers
  s/\{\{\[\[TODO\]\]\}\}/- [ ]/g;
  s/\{\{\[\[DONE\]\]\}\}/- [x]/g;
' "$1" \
| perl -ne '
  # Drop Logseq properties and logbook blocks
  next if /^\s*[a-zA-Z][a-zA-Z-]*:: /;
  next if /^\s*:LOGBOOK:/ .. /^\s*:END:/;
  # Drop leftover Roam/Logseq curly directives on their own line
  next if /^\s*\{\{[^}]*\}\}\s*$/;
  print
'

4. Fix the heading hierarchy. Especially from outliners. Promote the top two or three outline levels to ## and ###, and let the rest stay as lists. This is the manual step and it is where the document actually becomes readable.

5. Read it end to end. Every one of these apps leaves something behind. Reading the cleaned Markdown once catches literal {{query}} blocks, orphaned block references and dangling image links before they reach the PDF.

6. Apply your own print CSS and convert. At this point you have standard Markdown and full control — see the print CSS guide.

Steps 3 through 5 are the ones people skip, and they are the difference between a PDF that looks exported and one that looks written.

Try it yourself

Once your notes are cleaned to standard Markdown, the editor here renders GFM with math, diagrams and task lists, and gives you a paginated preview with your own CSS — which is the fastest way to see whether the cleanup actually worked before committing to a layout. Nothing is uploaded, so personal notes stay on your machine.

There is also a notes to PDF page with an example loaded. Related: the Obsidian guide for that app in depth, and print CSS for layout.

About this guide

Written and maintained by the developer of MarkdownToFile — the browser-based converter this site runs on. Techniques described here are tested against the engines named in the text (Chromium's print pipeline, Paged.js, WeasyPrint), and engine limitations are stated rather than glossed over. Corrections are welcome via the contact page; more about the project on the about page.

Try it yourself — free, no signup

Convert your Markdown to a polished PDF right in your browser.

Open the editor