Install
openclaw skills install publish-wiki-jsPublish markdown content to Wiki.js with proper formatting and metadata. Use when user wants to create or update wiki pages, convert notes/articles to wiki format, or publish content to a Wiki.js instance. Handles API authentication, content formatting (removing YAML frontmatter), automatic tagging, and path suggestions.
openclaw skills install publish-wiki-jsPublish markdown content to Wiki.js with proper formatting and metadata handling.
Use this skill when:
WIKI_KEY environment variable must be set (Wiki.js API key)CRITICAL: Wiki.js stores title/description in API parameters, NOT in content.
❌ Don't include in content:
---
title: Page Title
description: Page description
---
# Page Title
✅ Correct format:
# Page Title
Content starts here...
# Title)[text](url)The #1 cause of failures is incorrect string escaping.
# DON'T DO THIS - will fail with complex content
query = f'''
mutation {{
pages {{
create(content: "{content}") {{ # Content with quotes/newlines will break
page {{ id }}
}}
}}
}}
'''
Why it fails:
" (quotes) that terminate the GraphQL stringimport json
import requests
query = '''
mutation CreatePage($content: String!, $title: String!, $path: String!) {
pages {
create(
content: $content
title: $title
path: $path
editor: "markdown"
isPublished: true
isPrivate: false
locale: "zh"
) {
page {
id
path
title
}
responseResult {
succeeded
errorCode
message
}
}
}
}
'''
variables = {
"content": raw_content, # Pass raw content, no preprocessing
"title": "Page Title",
"path": "category/page-name",
"tags": [] # Required - can be empty list
}
# Let json.dumps handle all escaping automatically
payload = json.dumps({
"query": query,
"variables": variables
})
response = requests.post(
WIKI_URL,
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {WIKI_KEY}"
},
data=payload
)
Why this works:
json.dumps() properly escapes all special charactersFor simple updates when Variables cause issues:
# Replace any """ in content first
safe_content = content.replace('"""', '\x00TRIPLE\x00')
mutation = f'''
mutation {{
pages {{
update(
id: {page_id}
content: """{safe_content}"""
description: "Updated description"
editor: "markdown"
tags: []
) {{
page {{ id path title }}
responseResult {{ succeeded errorCode message }}
}}
}}
}}
'''
mutation CreatePage($content: String!, $title: String!, $path: String!, $description: String!, $tags: [String]!) {
pages {
create(
content: $content
description: $description
editor: "markdown"
isPublished: true
isPrivate: false
locale: "zh"
path: $path
tags: $tags
title: $title
) {
page {
id
path
title
}
responseResult {
succeeded
errorCode
message
}
}
}
}
Variables:
{
"content": "# Title\n\nContent...",
"title": "Page Title",
"path": "topic/category/page-name",
"description": "Page description",
"tags": ["tag1", "tag2"]
}
Important: tags must be provided even if empty ([]). Some Wiki.js versions require this field.
Type Requirements:
$content: String! - Required, raw markdown$title: String! - Required$path: String! - Required, URL path$description: String! - Required (can be empty string)$tags: [String]! - Required array of strings (can be empty [])Two-step process:
query GetPage($path: String!) {
pages {
single(path: $path) {
id
title
path
description
content
}
}
}
Variables:
{"path": "tech/api/rpc"}
Or query all pages to find by path:
query {
pages {
list {
id
path
title
}
}
}
mutation UpdatePage($id: Int!, $content: String!, $description: String!) {
pages {
update(
id: $id
content: $content
description: $description
editor: "markdown"
tags: []
) {
page {
id
path
title
}
responseResult {
succeeded
errorCode
message
}
}
}
}
⚠️ CRITICAL for Update:
tags is REQUIRED even if empty []description is REQUIRED even if empty string ""Cannot read properties of undefined (reading 'map')Variables:
{
"id": 25,
"content": "# Updated Content\n\n...",
"description": "Updated description"
}
GraphQL supports two string formats:
description: "Single line text"
Escape sequences required:
| Character | Escape | Example |
|---|---|---|
" | \" | "Say \"hello\"" |
\ | \\ | "C:\\path" |
| Newline | \n | "Line1\nLine2" |
| Tab | \t | "Col1\tCol2" |
content: """
Multi-line
content here
"""
Notes:
""" within contentMarkdown contains triple backticks that conflict:
```python
def hello():
pass
When inserted into GraphQL block string:
```graphql
content: """
```python # ❌ Conflicts with GraphQL """
def hello()
"""
**Solution:** Use GraphQL Variables (recommended) or escape each `` ` `` as `\`.
#### Pitfall 2: JSON Double-Escaping
```python
# ❌ WRONG - manual escape then JSON serialize
content_escaped = content.replace('"', '\\"')
payload = json.dumps({"query": f'..."{content_escaped}"...'})
# Results in: \" (double escaped)
# ✅ CORRECT - pass raw content to variables
payload = json.dumps({
"query": "mutation($c: String!) { create(content: $c) }",
"variables": {"c": raw_content}
})
GraphQL Strings are UTF-8 encoded. Ensure:
Content-Type: application/json; charset=utf-8Suggest paths based on content type:
| Content Type | Suggested Path |
|---|---|
| Technical docs | tech/{category}/{topic} |
| Thinking models | topic/thinking-models/{name} |
| Financial concepts | financial/{category}/{name} |
| Personal notes | notes/{category}/{name} |
| Project docs | projects/{name}/{doc} |
pages.create mutation with all required fieldspages.single(path: "...") or pages.listpages.update mutation with id + all required fields| Error | Cause | Solution |
|---|---|---|
ValidationError: Variable $content... invalid value | String escaping issue | Use Variables + json.dumps |
Field "create" argument "tags" of type "[String]!" is required | Missing required tags parameter | Always provide tags: [] even if empty |
GraphQLError: Variable $tags of type [String] | Tags type mismatch | Use [String]! in mutation signature |
Cannot read properties of undefined (reading 'map') | Missing tags or description in update | Add tags: [] and description: "..." |
Unauthorized | Invalid API key | Check WIKI_KEY env var |
Page already exists | Path conflict | Use update mutation or different path |
Always check both errors array and responseResult:
result = response.json()
# Check GraphQL-level errors
if 'errors' in result:
for err in result['errors']:
print(f"GraphQL Error: {err.get('message')}")
# Check Wiki.js response result
resp_result = result.get('data', {}).get('pages', {}).get('create', {}).get('responseResult', {})
if not resp_result.get('succeeded'):
print(f"Wiki.js Error {resp_result.get('errorCode')}: {resp_result.get('message')}")
import os
import json
import urllib.request
WIKI_URL = os.environ.get("WIKI_URL") # e.g. https://your-wiki.example.com/graphql
WIKI_KEY = os.environ.get("WIKI_KEY")
def create_wiki_page(content: str, title: str, path: str,
description: str = "", tags: list = None) -> dict:
"""
Create a Wiki.js page using GraphQL API.
"""
query = '''
mutation CreatePage($content: String!, $title: String!,
$path: String!, $description: String!,
$tags: [String]!) {
pages {
create(
content: $content
title: $title
path: $path
description: $description
tags: $tags
editor: "markdown"
isPublished: true
isPrivate: false
locale: "zh"
) {
page {
id
path
title
}
responseResult {
succeeded
errorCode
message
}
}
}
}
'''
variables = {
"content": content,
"title": title,
"path": path,
"description": description or title,
"tags": tags if tags is not None else []
}
payload = json.dumps({
"query": query,
"variables": variables
}).encode('utf-8')
req = urllib.request.Request(
WIKI_URL,
data=payload,
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {WIKI_KEY}"
},
method='POST'
)
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read().decode())
# Usage
with open('article.md', 'r', encoding='utf-8') as f:
content = f.read()
result = create_wiki_page(
content=content,
title="软件反模式(Anti-Patterns)",
path="tech/patterns/anti_patterns",
description="软件工程中常见反模式的识别与规避指南",
tags=["软件设计", "反模式", "最佳实践"]
)
import os
import json
import urllib.request
WIKI_URL = os.environ.get("WIKI_URL") # e.g. https://your-wiki.example.com/graphql
WIKI_KEY = os.environ.get("WIKI_KEY")
def update_wiki_page(page_id: int, content: str, description: str = "") -> dict:
"""
Update a Wiki.js page using GraphQL API.
⚠️ IMPORTANT: tags and description are REQUIRED even if empty!
"""
query = '''
mutation UpdatePage($id: Int!, $content: String!, $description: String!) {
pages {
update(
id: $id
content: $content
description: $description
editor: "markdown"
tags: []
) {
page {
id
path
title
}
responseResult {
succeeded
errorCode
message
}
}
}
}
'''
variables = {
"id": page_id,
"content": content,
"description": description
}
payload = json.dumps({
"query": query,
"variables": variables
}).encode('utf-8')
req = urllib.request.Request(
WIKI_URL,
data=payload,
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {WIKI_KEY}"
},
method='POST'
)
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read().decode())
def get_page_id_by_path(path: str) -> int:
"""Find page ID by path."""
query = json.dumps({
"query": "{ pages { list { id path title } } }"
}).encode()
req = urllib.request.Request(
WIKI_URL,
data=query,
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {WIKI_KEY}"
},
method='POST'
)
with urllib.request.urlopen(req) as resp:
data = json.loads(resp.read().decode())
pages = data.get('data', {}).get('pages', {}).get('list', [])
for p in pages:
if p.get('path') == path:
return p.get('id')
return None
# Usage: Update existing page
with open('wiki_rpc_article.md', 'r', encoding='utf-8') as f:
content = f.read()
page_id = get_page_id_by_path("tech/api/rpc")
if page_id:
result = update_wiki_page(
page_id=page_id,
content=content,
description="RPC协议全面指南,涵盖架构、框架对比、协议设计、性能优化等"
)
print(f"Updated: https://<wiki-url>/tech/api/rpc")
Remember:
pages.create with all fields including tags: [] and descriptionpages.update with id, content, description, and tags: []pages.single(path: "...") to find page ID, or pages.list to list allresponseResult for detailed error messages