BeClaude

obsidian-serial-publish

New
GitHubDocumentationby Lknechtli

Pandoc filter, scripts, ai skill, etc for converting obsidian md files to royalroad compatible html, epub, and more

First seen 5/24/2026

Overview

Royal Road Converter — Pandoc Lua Filter

Recommended approach: Use the bundled rr-convert.sh script for deterministic, testable conversion:

./rr-convert.sh input.md -o output.html # Royal Road (default) ./rr-convert.sh input.md -o output.html --mode ghost # Ghost CMS

This handles \[\[...\]\] escape preprocessing and runs the appropriate Lua filter with pandoc. The script swaps escaped brackets to control characters before pandoc parses them, so the filter can distinguish literal brackets from wiki links.

Output Modes

ModeFlagHeadingsInline stylesUse case
Royal Road (default)--mode rr<div> with inline CSSYes — required for RR parserPublishing on Royal Road
Ghost CMS--mode ghostSemantic <h1><h6>No — theme CSS handles stylingGhost CMS posts

Ghost mode produces clean semantic HTML wrapped in <div class="rr-theme">. The Ghost theme's CSS custom properties (--read-font-size, --read-line-height, etc.) control all typography. Callout tables retain their inline styles since Ghost has no native callout support.

Note: The SKILL.md below documents the conversion rules that the Lua filter implements. For new conversions, always prefer running the lua filter over instructing an LLM to manually apply these rules.

Conversion Order (apply sequentially)

  1. Strip forbidden HTML tags and their contents
  2. Remove ID/class/event attributes from remaining tags
  3. Convert heading tags (<hN> to <div>)
  4. Fix CSS properties that RR would break or strip
  5. Remove absolute positioning declarations

1. STRIP — Forbidden Elements (delete entirely)

Royal Road's parser deletes these. Do not include them in output.

Tags and Contents to Delete

Delete the opening tag, all inner HTML, and closing tag for: <script>, <style>, <iframe>, <object>, <embed>, <form>, <input>, <button>, <textarea>, <svg>, <canvas>, <video>, <audio>

Attributes to Remove from Any Tag

  • id="..." — remove the entire attribute
  • class="..." — remove the entire attribute
  • Any on[a-z]+= event handler (onclick=, onload=, etc.) — remove the entire attribute

Note: When removing class attributes, preserve semantic meaning by adding inline styles (see heading conversion below).


2. CONVERT — Safe Replacements

Royal Road's parser breaks these properties unless you pre-convert them. Apply these transformations during markdown-to-HTML conversion.

Background Colors — RR Color Inverter

RR randomly inverts pure black (#000, #000000, "black") and pure white (#fff, #ffffff, "white") on publish/preview only (not edit mode). RR recommends #212529 for black and #f8f9fa or #fafafa for white.

Rule: When converting background-color or color values:

  • #000, #000000, black to #212529
  • #fff, #ffffff, white to #fafafa (or #f8f9fa)

To prevent RR from stripping solid background colors, wrap them in a gradient:

css
background-color: #000;
/* Convert to */
background-image: linear-gradient(135deg, #212529, #212529);
background-color: #212529 !important;

Border-Radius — Must Have !important

RR strips border-radius unless !important is present.

Rule: Append !important to any border-radius: declaration.

css
/* Before */
border-radius: 8px;
/* After */
border-radius: 8px !important;

Font-Size / Line-Height — px to em Conversion

RR strips pixel-based font sizes and line heights. Convert to em units.

Rule: font-size: Npx to (N divided by 10)em, rounded to one decimal place, clamped between 0.3em and 2.4em. Same for line-height.

css
/* Before */
font-size: 16px; line-height: 24px;
/* After */
font-size: 1.6em; line-height: 2.4em;

Heading Tags — <hN> to <div> with Inline Styling

RR mangles <h1> through <h6> into <p> tags during parsing. Convert to <div> elements with inline styles that replicate heading semantics.

Rule: Replace every <hN and </hN> (N is 1 through 6) with <div / </div>. Inline styles are sourced from rr-convert.settings.luaheadings (default values shown):

LevelDefault inline style
h1font-size:2.0em;font-weight:bold;margin:1.5em 0 0.8em;
h2font-size:1.6em;font-weight:bold;margin:1.3em 0 0.7em;
h3font-size:1.3em;font-weight:bold;margin:1.1em 0 0.5em;
h4font-size:1.1em;font-weight:bold;margin:0.9em 0 0.4em;
h5font-size:1.0em;font-weight:bold;margin:0.7em 0 0.3em;
h6font-size:0.9em;font-weight:bold;text-transform:uppercase;margin:0.6em 0 0.2em;

Preserve the original element's text content and any inline style attributes (merge if needed). Do not preserve class or id attributes — they get stripped by RR anyway.

Example:

html
<!-- Markdown: ## Chapter Title -->
<h2>Chapter Title</h2>
<!-- Becomes -->
<div style="font-size:1.6em;font-weight:bold;margin:1.3em 0 0.7em;">Chapter Title</div>

Background Shorthand — Convert to background-image:

RR strips background: shorthand declarations that contain gradients but preserves background-image: declarations.

Rule: When encountering background: with a gradient value, convert to background-image: only:

css
/* Before */
background: linear-gradient(135deg, #1a1a2e, #16213e) center/cover no-repeat;
/* After */
background-image: linear-gradient(135deg, #1a1a2e, #16213e);

Strip any shorthand sub-properties (color, position, size, repeat). Keep only the gradient.

Absolute Positioning — Remove Entirely

RR strips position: absolute and will break your layout. There is no safe equivalent value RR accepts.

Rule: Delete all position: absolute; declarations along with their positioning properties (top, left, right, bottom). For overlapping elements, rebuild using CSS Grid or structural margins/borders instead.

css
/* Before */
position: absolute; top: 10px; left: 20px; z-index: 5;
/* After — delete entirely */

Pixel Dimensions (width/height) — Convert to em or %

RR strips width: and height: values expressed in pixels.

Rule:

  • Convert width: Npx to (N divided by 10)em (same clamp as font-size: min 0.3em, max 2.4em)
  • Or better: remove fixed px dimensions entirely and use flexbox/grid for layout instead
  • Percentages are preserved by RR but pixels are not

3. REMOVE — Properties to Delete (RR strips these; no safe equivalent)

Do not include these in output — they waste bytes and serve zero purpose since RR deletes them regardless.

PropertyPatternAction
Clip-path: polygonclip-path: polygon(...)Delete from CSS block
Position absoluteposition: absoluteDelete entire declaration + related positioning props (top, left, right, bottom)

4. MARKDOWN TO HTML SPECIFIC RULES

When the input is Obsidian markdown, apply these conversions before applying converter rules above.

Headings

Styles are configured in rr-convert.settings.luaheadings. Default output:

markdown
# H1        -> <div style="font-size:2.0em;font-weight:bold;margin:1.5em 0 0.8em;">Heading</div>
## H2       -> <div style="font-size:1.6em;font-weight:bold;margin:1.3em 0 0.7em;">Heading</div>
### H3      -> <div style="font-size:1.3em;font-weight:bold;margin:1.1em 0 0.5em;">Heading</div>
#### H4     -> <div style="font-size:1.1em;font-weight:bold;margin:0.9em 0 0.4em;">Heading</div>
##### H5    -> <div style="font-size:1.0em;font-weight:bold;margin:0.7em 0 0.3em;">Heading</div>
###### H6   -> <div style="font-size:0.9em;font-weight:bold;text-transform:uppercase;margin:0.6em 0 0.2em;">Heading</div>

Paragraphs — Wrap in <p> Tags with Spacing

Each block of text between blank lines (or after a heading) is a paragraph. Wrap each one:

markdown
Some paragraph text   ->   <p style="margin-bottom:1em;">Some paragraph text</p>

Do not collapse consecutive paragraphs into a single <p> — RR may strip content if not separated properly.

Bold & Emphasis (inside <p>)

MarkdownHTML OutputNotes
**bold** or __bold__<b>text</b>RR-safe
_italic_ or *italic*<i>text</i>RR-safe
~~strikethrough~~<strike>text</strike>RR-safe

Code Blocks

markdown
Inline: `code`        -> <code>code</code> (safe)
Fenced block:          -> <div class="sourceCode"><code>...content...</code></div>
                        -> No <pre> wrapper (RR strips <pre> tags)

Links

  • [text](url) -> preserved as <a href="url">text</a> (Royal Road supports links natively)
  • [[Page Name]] -> stripped entirely (Obsidian wiki-links have no RR equivalent)
  • \[\[Text\]\] -> unescaped to literal [[Text]] in output (requires rr-convert.sh preprocessing)

Escaped Brackets — Unescape in Output

Backslash-escaped brackets are literal text in Obsidian and should output with brackets intact:

  • \[\[Text\]\] -> [[Text]] (requires rr-convert.sh which preprocesses escapes before pandoc)

Rule: Use ./rr-convert.sh input.md -o output.html rather than raw pandoc, so that \[\[...\]\] sequences are distinguished from wiki links during preprocessing.

Lists

MarkdownHTML OutputNotes
- item / * item<li>item</li> inside <ul>RR-safe
1. item<li>item</li> inside <ol>RR-safe

Horizontal Rules & Images

MarkdownHTML OutputNotes
---styled <hr> from rr-convert.settings.luahorizontal_rule (default: centered, max-width 80%, chromatic aberration)RR-safe
![alt](src)<img src="src" alt="alt">Ensure trusted domain; use em or % for width/height — never px

Callouts — Styled Tables (RR-safe, no <pre> tags)

Obsidian callout syntax (> [!type]) becomes styled <table> elements wrapped in a max-width container. Royal Road strips <pre> tags, so tables are used instead.

All callout styling is configured in rr-convert.settings.lua under callouts (per-type) and callout_table (shared). Each callout type defines:

  • color — hex color for backgrounds, borders, shadows
  • symbol — unicode symbol prepended to the title
  • heading_style — inline CSS for the title <td> (%s → color)
  • body_style — inline CSS for body <td> cells
  • border_between — CSS for the border between body sections (%s → color)
  • border_heading — CSS for the bottom border of the heading cell (%s → color)
  • error_override — optional override for shadow/border (used by error type)

Default callout types:

Callout TypeColor (hex)SymbolNotes
info#1e90ffBlue
tip#4caf50Green
warning#ff5722Deep orange/red
error#f44336Red + chromatic aberration
note#8bc34aLight green
task#9c27b0Purple
quote#607d8bSlate blue
example#ba68c8Lavender

Structure: Each callout renders as:

  • Outer wrapper div (callout_table.wrapper_style) — caps width, centers on page
  • Table with dark background and monospace font (callout_table.table_style)
  • Traffic light dots prefix (callout_table.title_prefix) before the title
  • Title row: colored bold text with symbol prefix; bottom border separator
  • Body row: content with <br /> preserving line breaks
  • Outer table border and shadow (callout_table.border / callout_table.shadow)

All critical CSS properties use `!important` to override Royal Road defaults.

Special handling for `[!error]`:

  • Chromatic aberration via multi-layer box-shadow (red/cyan offsets in multiple directions)
  • All body text is bold — wrapped in <b> tags

Note: Type variants with numeric suffixes (e.g., task-1) are normalized to their base type (task). Unknown types default to info. ---

5. SAFE ELEMENTS & PROPERTIES (no conversion needed)

HTML Elements That Survive Unchanged

<div>, <span>, <p>, <br>, <hr>, <ul>, <ol>, <li>, <a href="">, <b>, <i>, <u>, <strike>, <sub>, <sup>, <code>, <figure>, <figcaption>, <blockquote>

HTML Elements That Are Stripped

<pre> — RR strips <pre> tags. Use <code> without <pre> wrapper. <font> — RR converts <font color="..."> to <span style="color:...">. Use <span> directly.

CSS Properties That Survive (with correct values)

  • color: — safe as long as value is not pure #000/#fff (use conversions above)
  • background-image: — gradients via this property survive; shorthand does not
  • border: / border-top: etc. — safe
  • padding:, margin: — use em or %, avoid px
  • display: flex | grid | block | inline-block — all safe
  • text-shadow: — safe
  • font-weight: bold — safe

EXAMPLE: Full Conversion Walkthrough

Input markdown:

markdown
# My Chapter

## Introduction

This is **bold** and _italic_ text with a black background.

![Image](https://example.com/image.png)

> [!info] Note
> This is an informational note about the chapter.

> [!error] Warning
> The system failed to initialize.

[Read more](https://example.com)

Output HTML (RR-safe):

html
<div style="font-size:2.0em;font-weight:bold;margin:1.5em 0 0.8em;">My Chapter</div>

<div style="font-size:1.6em;font-weight:bold;margin:1.3em 0 0.7em;">Introduction</div>

<p>This is <b>bold</b> and <i>italic</i> text with a black background.</p>

<img src="https://example.com/image.png" alt="Image">

<div style="max-width:90ch!important;margin:auto;">
<table style="background:#1a1a2e!important;color:#ddd!important;width:100%!important;font-family:monospace!important;font-size:0.9em!important;white-space:pre-wrap!important;border-radius:8px !important;box-shadow:-4px 4px 0 #1e90ff66!important;border:4px solid #1e90ff!important;">
<tr><td colspan="2" style="color:#1e90ff!important;font-weight:bold!important;padding:0.5em 1em!important;border-top:0!important;border-right:0!important;border-left:0!important;border-bottom:2px solid #1e90ff!important;">ℹ Note</td></tr>
<tr><td colspan="2" style="padding:0.5em 1em!important;border-top:0!important;border-right:0!important;border-left:0!important;border-bottom:0!important;"><span style="display:block!important;padding-left:1em!important;text-indent:-1em!important;">This is an informational note about the chapter.</span></td></tr>
</tbody></table>
</div>

<div style="max-width:90ch!important;margin:auto;">
<table style="background:#1a1a2e!important;color:#ddd!important;width:100%!important;font-family:monospace!important;font-size:0.9em!important;white-space:pre-wrap!important;border-radius:8px !important;box-shadow:-4px 4px 0 #f4433666!important,-2px -1px 0 #ff0000!important,2px 1px 0 #00ffff!important,-3px 2px 0 rgba(255,0,0,0.4)!important,3px -2px 0 rgba(0,255,255,0.4)!important;border:4px solid #f44336!important;">
<tr><td colspan="2" style="color:#f44336!important;font-weight:bold!important;padding:0.5em 1em!important;border-top:0!important;border-right:0!important;border-left:0!important;border-bottom:2px solid #f44336!important;">✖ Warning</td></tr>
<tr><td colspan="2" style="padding:0.5em 1em!important;border-top:0!important;border-right:0!important;border-left:0!important;border-bottom:0!important;"><span style="display:block!important;padding-left:1em!important;text-indent:-1em!important;"><b>The system failed to initialize.</b></span></td></tr>
</tbody></table>
</div>

(Links [text](url) are preserved as <a> tags. Wiki links [[wiki]] are removed entirely, leaving surrounding text intact.)


SUMMARY CHECKLIST (Lua Filter Output)

For every markdown-to-RR-HTML conversion via the Lua filter, verify:

  • [ ] No <script>, <style>, <iframe>, <svg>, etc. tags remain
  • [ ] All headings are <div> with inline styles (not <hN>)
  • [ ] Links [text](url) preserved as <a href=""> tags
  • [ ] Wiki links [[wiki]] are removed entirely
  • [ ] Escaped brackets \[\[...\]\] rendered as literal [[...]] (use rr-convert.sh)
  • [ ] Callouts rendered as styled tables in max-width wrapper divs
  • [ ] [!error] callouts have single combined box-shadow (chromatic aberration + base) and bold body text
  • [ ] Callout titles use <span style="color:..."> for traffic light dots (not <font>)
  • [ ] Code blocks use <code> without <pre> wrapper
  • [ ] All box-shadow properties are single declarations (RR only keeps the first)

Note: CSS-level conversions (color inverter, border-radius !important, px→em, background shorthand → background-image, absolute positioning removal) apply only when the input already contains inline styles. The Lua filter handles markdown AST transformations; CSS property rewriting would need a separate post-processor if your source uses style attributes extensively.

Install & Usage

1
Create the skills directory
mkdir -p .claude/skills
2
Download the skill file
mkdir -p .claude/skills && curl -o .claude/skills/obsidian-serial-publish.md https://raw.githubusercontent.com/Lknechtli/obsidian-serial-publish/main/SKILL.md
3
Invoke in Claude Code
/obsidian-serial-publish
View source on GitHub

Security Audits

LicenseUnknownSourceWarnRepositoryPass

Frequently Asked Questions

What is obsidian-serial-publish?

Pandoc filter, scripts, ai skill, etc for converting obsidian md files to royalroad compatible html, epub, and more

How to install obsidian-serial-publish?

To install obsidian-serial-publish: create the skills directory (mkdir -p .claude/skills), then run: mkdir -p .claude/skills && curl -o .claude/skills/obsidian-serial-publish.md https://raw.githubusercontent.com/Lknechtli/obsidian-serial-publish/main/SKILL.md. Finally, /obsidian-serial-publish in Claude Code.

What is obsidian-serial-publish best for?

obsidian-serial-publish is a skill categorized under Documentation. Created by Lknechtli.