| name | joplin-publisher |
| description | Import markdown documents and mermaid diagrams into Joplin using the Joplin CLI. |
Joplin Publisher Skill
Purpose
Import markdown documents and mermaid diagrams into Joplin using the Joplin CLI.
When to Use
- Exporting research to Joplin notebooks
- Creating documentation in Joplin
- Generating diagrams for Joplin knowledge base
- Syncing analysis results to Joplin
Overview
Joplin uses a database-backed system. Content is imported via the Joplin CLI or API. This skill focuses on CLI-based imports.
Prerequisites
Install Joplin CLI
# Using npm
npm install -g joplin
# Verify installation
joplin version
Configure Joplin CLI
# Set sync target (if needed)
joplin config sync.target 2 # Filesystem
joplin config sync.2.path /path/to/sync/folder
# Or connect to Joplin Server/Cloud
joplin config sync.target 9
joplin config sync.9.path https://your-joplin-server.com
joplin config sync.9.username your-username
Joplin CLI Commands
List Notebooks
joplin ls / # List top-level notebooks
joplin ls /Notebook # List notes in notebook
Create Notebook
joplin mkbook "Notebook Name"
joplin mkbook "Parent/Child" # Nested notebook
Import Markdown
# Import single file to notebook
joplin import /path/to/file.md --notebook "Notebook Name"
# Import directory
joplin import /path/to/folder --notebook "Notebook Name"
# Import with format specification
joplin import /path/to/file.md --format md --notebook "Notebook Name"
Create Note Directly
# Create note from stdin
echo "# Title
Content" | joplin mknote "Note Title" --notebook "Notebook Name"
# Create from file content
cat file.md | joplin mknote "Note Title" --notebook "Notebook Name"
Publishing Workflow
Step 1: Validate Notebook Exists
import subprocess
def notebook_exists(notebook: str) -> bool:
"""Check if a Joplin notebook exists."""
result = subprocess.run(
["joplin", "ls", "/"],
capture_output=True,
text=True
)
notebooks = result.stdout.strip().split('
')
return notebook in notebooks
def create_notebook_if_missing(notebook: str):
"""Create notebook if it doesn't exist."""
if not notebook_exists(notebook):
subprocess.run(
["joplin", "mkbook", notebook],
check=True
)
Step 2: Prepare Content
def prepare_markdown(
title: str,
content: str,
tags: list = None
) -> str:
"""
Prepare markdown content for Joplin import.
Joplin supports YAML frontmatter for metadata.
"""
lines = [f"# {title}", ""]
if tags:
lines.extend([
"---",
f"tags: {', '.join(tags)}",
"---",
""
])
lines.append(content)
return '
'.join(lines)
Step 3: Write Temporary File
import tempfile
from pathlib import Path
def write_temp_markdown(content: str, filename: str) -> Path:
"""Write content to a temporary markdown file."""
temp_dir = Path(tempfile.mkdtemp())
file_path = temp_dir / f"{filename}.md"
file_path.write_text(content, encoding='utf-8')
return file_path
Step 4: Import to Joplin
def import_to_joplin(
file_path: Path,
notebook: str
) -> bool:
"""Import markdown file to Joplin notebook."""
result = subprocess.run(
[
"joplin", "import",
str(file_path),
"--notebook", notebook
],
capture_output=True,
text=True
)
if result.returncode != 0:
raise JoplinImportError(f"Import failed: {result.stderr}")
return True
Complete Publishing Function
def publish_to_joplin(
notebook: str,
title: str,
content: str,
tags: list = None
) -> bool:
"""
Publish markdown content to a Joplin notebook.
Args:
notebook: Target notebook name
title: Note title
content: Markdown content (can include mermaid)
tags: Optional list of tags
Returns:
True if successful
"""
# Ensure notebook exists
create_notebook_if_missing(notebook)
# Prepare content
full_content = prepare_markdown(title, content, tags)
# Write to temp file
temp_file = write_temp_markdown(full_content, title)
try:
# Import to Joplin
import_to_joplin(temp_file, notebook)
return True
finally:
# Cleanup
temp_file.unlink()
temp_file.parent.rmdir()
Document Formats
Basic Note
# Note Title
Content goes here.
## Section
More content.
Note with Tags
# Note Title
Content here.
Note with Mermaid
Joplin supports mermaid diagrams natively in markdown:
# System Architecture
```mermaid
flowchart TD
A[Client] --> B[Server]
B --> C[(Database)]
Description
The system consists of...
## Notebook Organization
### Flat Structure
Notebooks/ ├── Research ├── Projects ├── Meetings └── Archive
### Nested Structure
Notebooks/ ├── Work/ │ ├── Project Alpha │ └── Project Beta ├── Personal/ │ ├── Notes │ └── Ideas └── Archive/
Create nested notebooks:
```bash
joplin mkbook "Work"
joplin mkbook "Work/Project Alpha"
Usage Examples
Publish Research Note
publish_to_joplin(
notebook="Research",
title="API Design Patterns",
content="""
## Overview
Key findings from API design research.
## REST Best Practices
1. Use nouns for resources
2. Use HTTP methods correctly
3. Version your API
## GraphQL Considerations
- Schema-first design
- Query optimization
""",
tags=["api", "research", "design"]
)
Publish Diagram
publish_to_joplin(
notebook="Architecture",
title="System Overview Diagram",
content="""
## Architecture Diagram
```mermaid
flowchart TB
subgraph Frontend
A[Web App]
B[Mobile App]
end
subgraph Backend
C[API Server]
D[Worker]
end
subgraph Data
E[(PostgreSQL)]
F[(Redis)]
end
A --> C
B --> C
C --> E
C --> F
D --> E
Components
| Component | Technology | Purpose |
|---|---|---|
| Web App | React | User interface |
| API Server | FastAPI | REST API |
| Worker | Celery | Background jobs |
| """, |
tags=["architecture", "diagram"]
)
### Publish Meeting Notes
```python
publish_to_joplin(
notebook="Meetings/2024",
title="Project Sync - Jan 15",
content="""
## Attendees
- Alice
- Bob
- Charlie
## Agenda
1. Sprint review
2. Blockers
3. Next steps
## Notes
### Sprint Review
- Feature X completed
- Bug Y in progress
### Blockers
- Waiting on API access
### Action Items
- [ ] Alice: Follow up on API access
- [ ] Bob: Complete bug fix
- [ ] Charlie: Update documentation
""",
tags=["meeting", "project-alpha"]
)
Batch Import
For importing multiple documents:
def batch_import(
notebook: str,
documents: list[dict]
) -> dict:
"""
Import multiple documents to Joplin.
Args:
notebook: Target notebook
documents: List of {title, content, tags} dicts
Returns:
{success: int, failed: int, errors: list}
"""
results = {"success": 0, "failed": 0, "errors": []}
for doc in documents:
try:
publish_to_joplin(
notebook=notebook,
title=doc["title"],
content=doc["content"],
tags=doc.get("tags", [])
)
results["success"] += 1
except Exception as e:
results["failed"] += 1
results["errors"].append({
"title": doc["title"],
"error": str(e)
})
return results
Error Handling
class JoplinError(Exception):
"""Base exception for Joplin operations."""
pass
class JoplinNotInstalledError(JoplinError):
"""Joplin CLI not found."""
pass
class JoplinImportError(JoplinError):
"""Failed to import content."""
pass
class NotebookNotFoundError(JoplinError):
"""Notebook does not exist."""
pass
def check_joplin_installed():
"""Verify Joplin CLI is available."""
result = subprocess.run(
["joplin", "version"],
capture_output=True
)
if result.returncode != 0:
raise JoplinNotInstalledError(
"Joplin CLI not found. Install with: npm install -g joplin"
)
Joplin API Alternative
For more control, use the Joplin Data API:
import requests
class JoplinAPI:
def __init__(self, token: str, port: int = 41184):
self.base_url = f"http://localhost:{port}"
self.token = token
def create_note(
self,
title: str,
body: str,
parent_id: str = None
) -> dict:
"""Create a note via Joplin API."""
response = requests.post(
f"{self.base_url}/notes",
params={"token": self.token},
json={
"title": title,
"body": body,
"parent_id": parent_id
}
)
response.raise_for_status()
return response.json()
Enable the API in Joplin Desktop: Options → Web Clipper → Enable
Checklist
Before publishing:
- Joplin CLI is installed and configured
- Target notebook exists or will be created
- Content is valid markdown
- Mermaid diagrams use correct syntax
- Tags are properly formatted
- Sync is configured (if using cloud/server)