Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

typub is a multi-platform content publishing pipeline for Typst documents. Write once in Typst, publish everywhere.

Audience and Reading Paths

User Path (publishing content)

Developer Path (contributing to typub)

  • RFC specs: RFC Index
  • ADR decisions: ADR Index
  • Governance history and execution trace: docs/work/ entries

At a Glance

                      content.typ ┬ content.md
                                  │
                    ┌─────────────┴─────────────┐
                    │        typub render       │
                    │            ↓              │
                    │    Semantic Document IR   │
                    └─────────────┬─────────────┘
                                  │
           ┌────────┬────────┬────────┬────────┬────────┐
           ↓        ↓        ↓        ↓        ↓        ↓
        ┌──────┐┌──────┐┌──────┐┌──────┐┌──────┐┌──────┐
        │ Ghost││Dev.to││ Hash ││Notion││  WP  ││Conf. │
        └──────┘└──────┘└──────┘└──────┘└──────┘└──────┘
           ┌────────┬────────┬────────┐
           ↓        ↓        ↓        ↓
        ┌──────┐┌──────┐┌──────┐┌──────┐
        │ Astro││Static││  XHS ││20+ CP│
        └──────┘└──────┘└──────┘└──────┘

Core Capabilities

FeatureDescription
Typst-nativeFirst-class support for Typst documents
Multi-platformPublish to 20+ platforms with one command
AST-centricUnified internal representation for consistent output
Asset handlingAutomatic image embedding, upload, or external storage
RFC-drivenFormal specifications ensure predictable behavior

Supported Target Types

  • API-based adapters (direct publish)
  • Local-output adapters (generated local artifacts)
  • Copy-paste profiles (manual publish via prepared content)

See Adapters for setup model and Platforms for concrete per-platform instructions.

Quick Start

# Install
cargo install typub

# Initialize a content project
typub init

# Publish to Dev.to
typub publish path/to/post -p devto

# Preview for WeChat (copy-paste)
typub dev path/to/post -p wechat

Pipeline (Conceptual)

typub processes content through a 10-stage pipeline:

  1. Resolve — Resolve content input and metadata
  2. Render — Render source content into HTML string
  3. Parse — Parse HTML string into unified AST
  4. Transform — Apply shared AST transformations
  5. Specialize — Create platform-specific payload
  6. Provision — Ensure remote resources exist
  7. Materialize — Upload/resolve assets
  8. Serialize — Convert to platform format
  9. Publish — Send to platform API
  10. Persist — Save publish status

Each adapter implements stages 5-9, inheriting common behavior from stages 1-4.

User Guide Overview

This section is for users who publish content with typub.

Suggested Reading Order

  1. Getting Started
  2. Adapters
  3. Asset Handling
  4. Theme Customization
  5. Copy-paste Profiles

Configuration and Advanced Topics

Platform-Specific References

Getting Started

This guide walks you through installing typub and publishing your first content.

Requirements

  • Rust 1.85+ (edition 2024)
  • Typst (for document compilation)

Installation

cargo install typub

Initialize a Content Project

typub init

This creates the default configuration:

.
├── typub.toml           # typub configuration
├── .typub/               # Status tracking database
└── posts/                # Your content directory

Create Your First Post

Create a Typst document:

// posts/hello-world/content.typ
= Hello World

This is my first post published with typub!

And the metadata file:

# posts/hello-world/meta.toml
title = "Hello World"
created = 2026-02-12
tags = ["tutorial", "typub"]

Configure a Platform

Edit typub.toml to enable a platform:

[platforms.devto]
enabled = true
# API key from environment: DEVTO_API_KEY

Set your API key:

export DEVTO_API_KEY="your-api-key-here"

Publish

# Publish to Dev.to
typub publish posts/hello-world -p devto

# Or publish to all enabled platforms
typub publish posts/hello-world

Development Mode

For local development with live reload:

# Start dev server with live reload
typub dev posts/hello-world -p xiaohongshu

# Or specify a custom port
typub dev posts/hello-world -p xiaohongshu --port 3000

Interactive Dashboard (TUI)

typub provides an interactive terminal dashboard for content management:

# Launch TUI dashboard
typub tui

The TUI provides three main views:

Post List

  • Browse all your posts with publishing status indicators
  • Sort posts by date, title, or modification time (press s)
  • Press Enter to view post details

Post Detail

  • View post metadata and publishing status for each platform
  • Use ↑/↓ to select a platform
  • Press p to preview the selected platform’s rendering
  • Press P to publish to the selected platform
  • Press A to publish to all enabled platforms

Preview

  • View platform-specific preview in plain text
  • Press o to open full HTML preview in browser
  • Scroll with ↑/↓ or PageUp/PageDown

Keyboard Shortcuts:

  • q - Go back or quit
  • r - Reload post list
  • Ctrl+C - Force quit

Check Status

# See what's been published where
typub status posts/hello-world

Next Steps

Basic path

Advanced path

Adapters

Adapters are platform-specific components that transform and publish your content to each target.

Audience

  • This page is a platform-agnostic user guide.
  • For platform-specific values and screenshots, use Platforms Overview.

Basic Setup

1. Enable a platform

Use typub.toml:

[platforms.devto]
enabled = true
published = true

[platforms.ghost]
enabled = true
published = false

2. Add platform credentials via environment variables

export DEVTO_API_KEY="..."
export GHOST_ADMIN_API_KEY="id:secret"

3. Publish

typub publish posts/my-post -p devto

Adapter Categories

  • API-based adapters: direct publish through remote APIs
  • Local-output adapters: generate local files/artifacts
  • Copy-paste profiles: generate content for manual paste workflow

See Platforms Overview for concrete platform entries.

Common Platform Fields

[platforms.<platform_id>]
enabled = true
published = true
asset_strategy = "embed"   # optional, platform-dependent
theme = "..."              # optional
internal_link_target = "..." # optional

Basic vs Advanced

Basic

  • Enable platform
  • Configure required credentials
  • Publish with default behavior

Advanced

  • Override asset strategy
  • Use external storage
  • Override node policy at platform level
  • Fine-tune per-platform extra fields

See Advanced Customization.

Environment Variable Substitution

String values in typub.toml support shell-style environment expansion:

[platforms.hashnode]
api_key = "$HASHNODE_API_KEY"
publication_id = "${HASHNODE_PUBLICATION_ID}"

If a variable is not found and no default is provided, the raw value is kept unchanged.

Asset Handling

typub provides flexible strategies for handling images and other assets in your content.

Audience

  • This page is platform-agnostic and user-facing.
  • For provider-level details and examples, see External Storage.

Basic Usage

Choose an asset strategy

StrategyHow it worksTypical usage
embedBase64 encode inlineSmall images, no upload dependency
uploadUpload to platform storagePlatforms with native media APIs
copyCopy to local outputLocal/static outputs
externalUpload to S3-compatible hostCDN, large assets, or platforms rejecting base64

Configuration

Per-platform Strategy

[platforms.devto]
enabled = true
asset_strategy = "embed"

[platforms.notion]
enabled = true
asset_strategy = "upload"

[platforms.astro]
enabled = true
asset_strategy = "copy"

Basic image reference

#image("./images/diagram.png", width: 80%)

typub resolves and rewrites image references based on the configured strategy.

Advanced Usage

External storage

Configure [storage] when using external:

[storage]
endpoint = "https://s3.amazonaws.com"
bucket = "my-content-bucket"
region = "us-east-1"
url_prefix = "https://cdn.example.com"

Credentials should come from environment variables.

Strategy selection guidance

  • Prefer upload when the platform supports native media upload.
  • Use external for large assets or CDN portability.
  • Use embed for simplicity when content size remains acceptable.
  • Use copy for local/static outputs.

Tracking

typub tracks uploaded assets to avoid duplicate uploads:

# See asset status
typub status --assets posts/my-post

Asset mappings are stored in .typub/status.db.

Theme Customization

This guide explains how to customize typub themes at the project level.

What You Can Customize

  • Add new theme IDs (for example my-brand)
  • Override built-in themes (for example replace elegant)
  • Override base CSS shared by all themes
  • Customize preview-page CSS for specific platforms

Theme Loading Model

typub loads themes in this order:

  1. Built-in themes embedded in the binary
  2. Project-local themes from templates/themes/

User themes override built-ins when IDs are the same.

Theme Files Directory

Create this directory in your project root:

mkdir -p templates/themes

Supported files:

  • <theme-id>.css: a normal theme file (loaded as a theme)
  • _base.css: optional base override applied before all themes
  • _preview-<platform>.css: optional preview page style override

Files with names starting with _ are not treated as themes, except _base.css and _preview-*.css.

Quick Start: Create a New Theme

Create templates/themes/my-brand.css:

h1 {
  color: #0f4c81;
  border-bottom: 2px solid #0f4c81;
  padding-bottom: 0.25em;
}

h2 {
  color: #17324d;
}

a {
  color: #0f4c81;
}

blockquote {
  border-left: 4px solid #0f4c81;
  background: #f3f8fd;
}

p code,
li code {
  background: #ecf4fc;
  color: #17324d;
}

Enable it in typub.toml:

[platforms.wechat]
theme = "my-brand"

Preview:

typub dev posts/my-post -p wechat

Override a Built-in Theme

To override a built-in theme, create a file with the same ID:

  • templates/themes/elegant.css
  • templates/themes/github.css
  • templates/themes/notion.css
  • etc.

typub will use your file instead of the embedded one.

Configure Theme by Scope

Global default (typub.toml)

theme = "minimal"

Per-platform (typub.toml)

[platforms.wechat]
theme = "wechat-green"

Per-post default (meta.toml)

theme = "elegant"

Per-post per-platform (meta.toml)

[platforms.wechat]
theme = "my-brand"

Theme Resolution Order

Highest priority wins:

  1. meta.toml[platforms.<id>].theme
  2. meta.toml.theme
  3. typub.toml[platforms.<id>].theme
  4. typub.toml.theme
  5. Profile default theme

Advanced: Override Shared Base CSS

If you provide templates/themes/_base.css, typub uses it as the base layer for all themes.

Use this when you want consistent typography/spacing across all theme IDs.

Advanced: Customize Preview Page Styles

Preview CSS is independent from article theme CSS.

Examples:

  • templates/themes/_preview-copypaste.css
  • templates/themes/_preview-confluence.css

Use this for toolbar/layout styling in preview pages.

Typst Custom Header (Current Model)

typub now supports user-defined Typst preamble via the preamble config key. This key is resolved through the standard configuration chain and then merged with adapter defaults.

Current hook points in RenderConfig:

  • imports
  • preamble
  • template_before
  • template_after
  • content_transform (for include/render behavior)

At render time, typub generates a wrapper Typst file in this order:

  1. imports
  2. HTML math rule (when output format is HTML/fragment)
  3. preamble
  4. template_before
  5. content include/render
  6. template_after

What this means for users

  • Theme CSS files (templates/themes/*.css) customize output style, not Typst wrapper header logic.
  • You can set preamble in config files:
    • typub.toml[platforms.<id>].preamble
    • typub.toml.preamble
    • meta.toml[platforms.<id>].preamble
    • meta.toml.preamble
  • Resolution order follows RFC-0005 (layer 1 to 4), then adapter default.
  • Merge behavior is append-only for compatibility: final_preamble = adapter_preamble + "\\n\\n" + user_preamble when user preamble exists.

Example (typub.toml):

preamble = """
#set text(lang: "zh")
"""

[platforms.wechat]
preamble = """
#set text(size: 11pt)
"""

Adapter examples in this repository

  • confluence sets preamble to disable raw-theme wrapping: #set raw(theme: none).
  • xiaohongshu injects a full preamble (embedded Typst template + #show + #cover) and customizes content_transform.

Contributor entry points

  • crates/typub-adapters-core/src/types.rs (RenderConfig)
  • crates/typub-engine/src/renderer.rs (generate_wrapper injection order)
  • each adapter’s render_config() implementation

Authoring Tips

  • Use direct element selectors (h1, p, blockquote) instead of wrapper-based selectors.
  • Keep selectors simple for better inline-style compatibility on copy-paste platforms.
  • Test with typub dev on your target platform, not only in static HTML output.

Troubleshooting

Theme not applied

  • Verify theme = "<id>" matches filename <id>.css
  • Verify templates/themes/ is under the project root where you run typub
  • Restart typub dev after changing theme files

Built-in style still visible

  • Check for syntax errors in your custom CSS file
  • Confirm file name exactly matches built-in ID when overriding

Copy-paste Profiles

For platforms without public APIs, typub generates formatted content that you copy and paste manually.

Audience

  • This page is user-facing and platform-agnostic.
  • For per-platform operation steps, see Platforms Overview.

Basic Usage

typub dev posts/my-post -p wechat

Then:

  1. Open the local preview URL.
  2. Copy rendered content.
  3. Paste into the target platform editor.

Built-in Profiles

HTML Platforms

PlatformDescription
wechatWeChat Official Account
zhihuZhihu columns
toutiaoToutiao/Jinri Toutiao
bilibiliBilibili articles
weiboWeibo articles
baijiahaoBaidu Baijiahao
wangyihaoNetEase Wangyihao
sohuSohu media
sspaiSspai
oschinaOSChina

Markdown Platforms

PlatformDescription
csdnCSDN
juejinJuejin
segmentfaultSegmentFault
cnblogsCnblogs
mediumMedium
jianshuJianshu
infoqInfoQ China
51cto51CTO
tencentcloudTencent Cloud Developer
aliyunAliyun Developer
huaweicloudHuawei Cloud
elecfansElecfans
modelscopeModelScope
volcengineVolcengine Developer

Advanced Customization

Add or adjust built-in profiles (repository contributors)

Profiles are defined in:

crates/adapters/typub-adapter-copypaste/profiles.toml

Example:

[[profile]]
id = "my-platform"
name = "My Platform"
editor_url = "https://my-platform.com/write"
format = "markdown"   # or "html"
# compat = "wechat"   # optional: use wechat-style HTML transforms

Field reference

FieldRequiredDescription
idYesUnique identifier (used in commands)
nameYesHuman-readable display name
editor_urlYesURL to the platform’s editor
formatYeshtml or markdown
compatNoName of a compatibility function for HTML transforms

Compatibility functions

For HTML platforms that need special handling:

CompatDescription
wechatWeChat-specific CSS inlining and formatting

To add a new compat function, implement it in crates/adapters/typub-adapter-copypaste/src/adapter.rs.

Advanced Customization

This page groups advanced, platform-agnostic customization options for typub users.

Use this after you have completed the basic flow in Getting Started.

1. Configuration Resolution Layers

Many fields follow layered resolution. Highest priority wins:

  1. meta.toml platform-specific (meta.platforms.<id>.*)
  2. meta.toml post-level defaults (field-dependent)
  3. typub.toml platform-specific (platforms.<id>.*)
  4. typub.toml global defaults (field-dependent)
  5. Adapter/default fallback

See RFC-0005 for normative rules.

2. Asset Strategy and Storage

Per-platform strategy

[platforms.devto]
asset_strategy = "external"

External storage

[storage]
endpoint = "https://s3.amazonaws.com"
bucket = "my-bucket"
region = "us-east-1"
url_prefix = "https://cdn.example.com"

Use environment variables for secrets.

See External Storage and RFC-0004.

3. Node Policy Override

You can override adapter default node policy per platform.

Supported actions:

  • pass
  • sanitize
  • drop
  • error

In typub.toml

[platforms.wechat]
node_policy = { raw = "sanitize", unknown = "drop" }

In meta.toml (higher priority)

[platforms.wechat]
node_policy = { raw = "error" }

Partial override is allowed. Unset fields fall back to lower layers and then adapter defaults.

4. Copy-paste Profile Extension (Contributor-Level)

If you need new built-in copy-paste profile behavior:

  • Edit crates/adapters/typub-adapter-copypaste/profiles.toml
  • Adjust compat behavior in crates/adapters/typub-adapter-copypaste/src/adapter.rs when needed

This is a repository customization workflow, not a runtime per-project override.

5. Advanced Debugging

Use dry-run and stage dump to inspect behavior:

typub publish posts/my-post -p wechat -d -v
typub publish posts/my-post -p wechat -d -D transform

6. Typst Preamble Override

typub supports user-defined Typst preamble via preamble, resolved with the same layered model as other fields:

  1. meta.toml[platforms.<id>].preamble
  2. meta.toml.preamble
  3. typub.toml[platforms.<id>].preamble
  4. typub.toml.preamble
  5. adapter default preamble

When a user preamble is resolved, typub appends it after adapter preamble to preserve platform-specific defaults.

See Theme Customization.

External Storage Configuration

typub supports S3-compatible external storage for assets when publishing to platforms that use the External asset strategy. This enables automatic asset upload to cloud storage with deduplication and caching.

Overview

When publishing to platforms like Dev.to, Hashnode, Medium, or Ghost, images and other assets need to be hosted externally. typub handles this automatically by:

  1. Computing content hashes for all assets
  2. Uploading to S3-compatible storage (deduplicated by hash)
  3. Replacing local asset references with public URLs
  4. Caching upload records to avoid re-uploading

Configuration

Basic Configuration

Add a [storage] section to your typub.toml:

[storage]
type = "s3"
endpoint = "https://your-s3-endpoint.com"
bucket = "your-bucket-name"
region = "us-east-1"
url_prefix = "https://cdn.yourdomain.com"

Environment Variables

Credentials should be provided via environment variables for security:

# S3 credentials
export S3_ACCESS_KEY_ID="your-access-key"
export S3_SECRET_ACCESS_KEY="your-secret-key"

Platform-Specific Configuration

You can override storage settings per platform:

[storage]
endpoint = "https://s3.amazonaws.com"
bucket = "default-bucket"
url_prefix = "https://cdn.example.com"

[platforms.devto.storage]
bucket = "devto-assets"
url_prefix = "https://devto-cdn.example.com"

[platforms.medium.storage]
bucket = "medium-assets"
url_prefix = "https://medium-cdn.example.com"

Configuration Reference

FieldDescriptionRequiredEnvironment Variable
typeStorage type (currently only "s3" supported)NoS3_TYPE
endpointS3-compatible endpoint URLFor non-AWSS3_ENDPOINT
bucketBucket nameYesS3_BUCKET
regionAWS region or "auto" for R2For AWSS3_REGION
url_prefixPublic URL prefix for assetsYesS3_URL_PREFIX
access_key_idS3 access keyYesS3_ACCESS_KEY_ID or AWS_ACCESS_KEY_ID
secret_access_keyS3 secret keyYesS3_SECRET_ACCESS_KEY or AWS_SECRET_ACCESS_KEY

Provider Examples

AWS S3

[storage]
bucket = "my-blog-assets"
region = "us-east-1"
url_prefix = "https://my-blog-assets.s3.amazonaws.com"

Cloudflare R2

[storage]
endpoint = "https://<account-id>.r2.cloudflarestorage.com"
bucket = "my-bucket"
region = "auto"
url_prefix = "https://assets.myblog.com"  # Custom domain via R2 public access

MinIO

[storage]
endpoint = "https://minio.example.com"
bucket = "blog-assets"
region = "us-east-1"
url_prefix = "https://minio.example.com/blog-assets"

DigitalOcean Spaces

[storage]
endpoint = "https://nyc3.digitaloceanspaces.com"
bucket = "my-space"
region = "nyc3"
url_prefix = "https://my-space.nyc3.cdn.digitaloceanspaces.com"

Content-Addressable Storage

Assets are stored using content-addressable keys for automatic deduplication:

Key Format: {sha256-hash}.{extension}

Example:

  • Original: images/screenshot.png
  • Object key: a1b2c3d4e5f6...png

This means:

  • Identical files are uploaded only once
  • Changing an image creates a new object (no cache invalidation needed)
  • Old objects can be safely deleted if unreferenced

Upload Caching

typub maintains a SQLite database (.typub/status.db in your project root) that tracks:

  • Content hash of each uploaded asset
  • Remote URL after upload
  • Storage configuration used

When publishing the same content again:

  1. Asset hash is computed
  2. Database is checked for existing upload
  3. If found with matching storage config: skip upload, return cached URL
  4. If not found: upload to storage, record in database

This makes re-publishing near-instant when assets haven’t changed.

Asset Strategy Precedence

For platforms using External asset strategy (e.g., Dev.to, Hashnode, Medium):

Platform default → Platform config → Global config → Error

If no storage is configured and a platform requires external assets, publishing will fail with a clear error message.

Security Best Practices

  1. Never commit credentials to version control
  2. Use environment variables for secrets
  3. Use dedicated IAM users with minimal permissions
  4. Consider presigned URLs for private assets
  5. Enable bucket versioning for backup

IAM Policy Example (AWS)

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:PutObject", "s3:GetObject", "s3:HeadObject"],
      "Resource": "arn:aws:s3:::my-bucket/*"
    }
  ]
}

Troubleshooting

Upload Fails with “Access Denied”

  1. Verify credentials: S3_ACCESS_KEY_ID and S3_SECRET_ACCESS_KEY
  2. Check bucket permissions
  3. Ensure endpoint URL is correct

Assets Not Appearing

  1. Check url_prefix is correct and publicly accessible
  2. Verify bucket allows public reads (or has proper policy)
  3. Check network connectivity to storage endpoint

Duplicate Uploads

  1. Check SQLite database is not corrupted: .typub/status.db
  2. Verify storage config ID matches across runs
  3. Use typub status --list to see recorded assets

Environment Variables Not Recognized

Platform-specific variables take precedence. Use uppercase platform ID:

# Global credentials
export S3_ACCESS_KEY_ID="global-key"

# Platform-specific override for Dev.to
export DEVTO_S3_ACCESS_KEY_ID="devto-key"

Platform-Specific Environment Variables

Each platform can have dedicated storage credentials:

PlatformAccess Key VariableSecret Key Variable
Dev.toDEVTO_S3_ACCESS_KEY_IDDEVTO_S3_SECRET_ACCESS_KEY
HashnodeHASHNODE_S3_ACCESS_KEY_IDHASHNODE_S3_SECRET_ACCESS_KEY
MediumMEDIUM_S3_ACCESS_KEY_IDMEDIUM_S3_SECRET_ACCESS_KEY
GhostGHOST_S3_ACCESS_KEY_IDGHOST_S3_SECRET_ACCESS_KEY

Platform Guides

Choose platform docs by publishing workflow.

Workflow Categories

Quick Entry Points

API Adapters

Local Output

Copy-paste

Direct Publish (API Adapters)

This section groups platforms that publish directly through remote APIs.

Use these guides for direct publish workflows:

For file-output workflows under the same adapter model, see Local Output Adapters.

Confluence

Confluence is Atlassian’s enterprise wiki and documentation platform. typub supports publishing to Confluence Cloud via the REST API with Basic authentication.

Capabilities

FeatureSupport
TagsYes (maps to labels)
CategoriesNo
Internal LinksYes
Draft SupportReversible (status field: current vs draft)
Math RenderingLaTeX (via ADF extension) or PNG (attachments)
Local OutputNo

Asset Strategies

StrategySupportedDefaultNotes
uploadYes*Upload as page attachments
embedNoNot supported
externalNoNot supported
copyNoNot supported

Prerequisites

  • Confluence Cloud instance (Server/Data Center may work but is not tested)
  • Personal Access Token or API token for authentication
  • (Optional) LaTeX Math plugin for formula rendering

Authentication

Confluence Cloud uses Basic authentication with your email and an API token.

Step 1: Create an API Token

  1. Go to Atlassian Account Settings
  2. Click Create API token
  3. Give it a label (e.g., “typub”)
  4. Copy the generated token

Step 2: Configure typub

[platforms.confluence]
base_url = "https://your-company.atlassian.net"  # Your Confluence URL
default_space = "DOCS"                            # Default space key
email = "you@example.com"                         # Your email (or use CONFLUENCE_EMAIL env var)
api_key = "your-api-token"                        # API token (or use CONFLUENCE_API_KEY env var)

Environment Variables:

export CONFLUENCE_EMAIL="you@example.com"
export CONFLUENCE_API_KEY="your-api-token"

Security Warning: Never commit API tokens to version control. Use environment variables instead.

LaTeX Math Rendering

Confluence supports two math rendering strategies:

Uses the Appfire LaTeX Math plugin with Atlassian Document Format (ADF) extensions. This provides native LaTeX rendering without image attachments.

Prerequisites:

  1. Install the LaTeX Math for Confluence app from Atlassian Marketplace
  2. Get the App ID and Environment ID from the app configuration

Configuration:

[platforms.confluence]
math_rendering = "latex"           # Use LaTeX rendering
latex_math_app_id = "your-app-id"  # From Appfire app configuration
latex_math_env_id = "your-env-id"  # From Appfire app configuration

To find your App ID and Environment ID:

  1. In Confluence, use the LaTeX Math macro in a page
  2. Open the browser’s developer tools (F12) → Network tab
  3. Insert a LaTeX formula and look for API calls
  4. The App ID and Environment ID appear in the request URLs

Alternatively, contact your Confluence administrator or check the app’s configuration in Settings → Apps → Manage apps.

Note: When the app ID and env ID are not provided, the LaTeX rendering backend would fall back to use the traditional ac macros, which are going to be deprecated and may not work as expected.

Option 2: PNG Attachments

Render math formulas as PNG images and attach them to the page. This works without any plugins but produces image-based formulas.

[platforms.confluence]
math_rendering = "png"

Generated PNG files are stored in your content’s assets/ folder and uploaded as attachments.

Usage

# Preview content
typub dev posts/my-post -p confluence

# Publish to Confluence
typub publish posts/my-post -p confluence

Confluence Published

Post Configuration

Space Selection

Specify which Confluence space to publish to:

# In your post's meta.toml
[platforms.confluence]
space = "ENG"  # Override default space
parent_id = "123456"  # Optional: parent page ID for hierarchical structure

Note: parent_id only applies when creating new pages. If the page already exists, updating content will not change its parent location. This prevents accidental page moves during content updates.

Labels (Tags)

Tags are automatically synced as Confluence labels:

# In your post's meta.toml
tags = ["rust", "tutorial", "api"]

Label normalization rules:

  • Converted to lowercase
  • Special characters replaced with hyphens
  • Maximum 255 characters
  • Duplicates removed

Draft Mode

Confluence supports draft mode via the published field:

[platforms.confluence]
published = false  # Creates/updates as draft
  • published = true: Page status is current (visible to readers)
  • published = false: Page status is draft (hidden from readers)

You can toggle this at any time.

Asset Handling

All images are uploaded as page attachments. The adapter uses Confluence’s attachment API with deduplication:

  1. Images are uploaded with their original filename
  2. If an attachment with the same name exists, it’s replaced
  3. Image references in content are resolved to attachment URLs

Image Caption and Alt Mapping

  • For a single-image figure, typub maps figcaption to Confluence <ac:caption> on <ac:image>
  • Image alt is mapped to ac:alt (accessibility/metadata), not visible caption
  • If no figcaption exists, typub does not promote alt into <ac:caption>

Supported Image Formats

  • PNG, JPEG, GIF, WebP
  • SVG (extracted and converted to inline elements)

Code Blocks

Confluence uses CDATA for code block content. Complex highlighting may not be preserved. For best results:

  • Use standard Markdown code blocks
  • Language identifiers are preserved where possible
  • Consider using Confluence’s built-in code block macro for advanced features

Internal links between your posts are resolved using Confluence’s link format:

[Related Post](../other-post/index.typ)

The adapter resolves the target post’s Confluence page ID and creates appropriate links.

Troubleshooting

“CONFLUENCE_API_KEY not set”

  • Ensure the environment variable is set or api_key is configured in profiles.toml
  • Verify your shell profile exports the variable correctly

“latex_math_app_id not configured”

  • Set latex_math_app_id and latex_math_env_id in your platform configuration
  • Or switch to math_rendering = "png" for PNG-based math

“Confluence create page error (400)”

  • Check that the space key is correct (case-sensitive)
  • Verify you have permission to create pages in the specified space
  • Ensure required fields (title, space) are properly set

“Confluence attachment upload error (403)”

  • Verify you have attachment upload permissions
  • Check that the page exists before uploading attachments
  • Ensure the API token is not expired

Labels not syncing

  • Labels require Edit permission on the page
  • Invalid characters in labels are automatically replaced
  • Check Confluence’s audit logs for rejection details

Math not rendering

  • For LaTeX mode: Verify the LaTeX Math plugin is installed and activated
  • Check that latex_math_app_id and latex_math_env_id are correct
  • For PNG mode: Ensure PNG generation is working (check assets/ folder)

Page not found after changing slug

  • typub uses the cached page ID for updates
  • If you changed the title/slug, the adapter falls back to title search
  • For manual control, set the page ID in status database

Dev.to

Dev.to is a community of software developers sharing ideas and helping each other grow.

Capabilities

FeatureSupport
TagsYes (max 4)
CategoriesNo
Internal LinksYes
Draft SupportReversible (status field)
Math RenderingPNG
Local OutputNo

Asset Strategies

StrategySupportedDefault
embedYes
uploadNo
externalYes*
copyNo

Note: Dev.to has limited support for embedded Base64 images.

Prerequisites

  • A Dev.to account (free)

Getting Your API Key

Step 1: Sign In to Dev.to

Go to dev.to and sign in to your account.

Step 2: Access Settings

  1. Click your profile picture in the top-right corner
  2. Select Settings from the dropdown menu

Profile Menu

Step 3: Navigate to Extensions

  1. In the left sidebar, click Extensions
  2. Scroll down to find DEV Community API Keys

Extensions Page

Step 4: Generate API Key

  1. Enter a description (e.g., “typub”)
  2. Click Generate API Key

Generate API Key

  1. The key will appear under Active API keys — expand it to copy

Copy API Key

Security Warning:

  • Never commit API keys to version control — use environment variables instead.
  • If you suspect your key has been compromised, revoke it immediately and generate a new one.

Configuration

[platforms.devto]
api_base = "https://dev.to/api"  # optional, this is the default
published = true                  # true for published, false for draft
asset_strategy = "embed"          # or "external"

Environment Variables:

Set DEVTO_API_KEY with your API key:

export DEVTO_API_KEY="your-api-key-here"

Usage

# Preview content
typub dev posts/my-post -p devto

# Publish to Dev.to
typub publish posts/my-post -p devto

Tag Limits

Dev.to allows a maximum of 4 tags per article. If your content has more than 4 tags, typub will use the first 4.

# In your post's meta.toml
tags = ["rust", "webdev", "tutorial", "beginners"]  # All 4 will be used

Troubleshooting

“Unauthorized” error

  • Verify your API key is correct
  • Check that the key hasn’t been revoked in Dev.to settings
  • Ensure DEVTO_API_KEY environment variable is set

Article not appearing

  • Check if published = false in your config (creates draft)
  • Draft articles are only visible to you in the Dev.to dashboard

Images not loading

  • Dev.to requires images to be accessible via public URLs
  • Use asset_strategy = "external" with S3/R2 storage for reliable image hosting
  • Embedded base64 images may hit size limits for large images

Ghost

Ghost is an open-source, professional publishing platform built on Node.js.

Capabilities

FeatureSupport
TagsYes
CategoriesNo
Internal LinksYes
Draft SupportReversible (status field)
Math RenderingSVG
Local OutputNo

Asset Strategies

StrategySupportedDefaultNotes
embedYes*Images embedded as data URIs
uploadYesUpload to Ghost’s image storage
externalYesUpload to S3/R2, use external URLs
copyNoGhost cannot fetch local file paths

Prerequisites

  • A Ghost site (self-hosted or Ghost(Pro))
  • Admin access to create integrations

Getting Your API Key

Ghost uses Admin API keys for authentication. The key format is id:secret where both parts are hex strings.

Step 1: Access Ghost Admin

Navigate to your Ghost admin panel at https://your-site.com/ghost/.

Step 2: Open Integrations

  1. Click the Settings gear icon in the bottom-left corner
  2. Select Integrations from the menu

Settings Menu

Step 3: Create Custom Integration

  1. Scroll down to Custom integrations
  2. Click Add custom integration
  3. Enter a name (e.g., “typub”)
  4. Click Create

Step 4: Copy Admin API Key

  1. In the integration details, locate Admin API Key
  2. Click the key to reveal it
  3. Copy the entire key (format: abc123:def456...)

Copy API Key

Important: The Admin API Key has full write access. Keep it secure and never commit it to version control.

Configuration

[platforms.ghost]
api_base = "https://your-site.com"  # Your Ghost site URL
published = true                     # true for published, false for draft
asset_strategy = "embed"             # or "upload", "external"

Environment Variables:

Set GHOST_ADMIN_API_KEY with your Admin API key:

export GHOST_ADMIN_API_KEY="your-id:your-secret"

Or in your shell profile:

# ~/.bashrc or ~/.zshrc
export GHOST_ADMIN_API_KEY="abc123def456:789xyz..."

Usage

# Preview content
typub dev posts/my-post -p ghost

# Publish to Ghost
typub publish posts/my-post -p ghost

Troubleshooting

“Invalid API key” error

  • Ensure the key format is id:secret (two hex strings separated by colon)
  • Verify the key hasn’t been regenerated in Ghost Admin
  • Check that api_base doesn’t include /ghost/api/ suffix

“Unauthorized” error

  • Confirm the integration is still active in Ghost Admin
  • Try regenerating the API key and updating your environment variable

Images not appearing

  • For self-hosted Ghost, ensure your site URL is publicly accessible
  • Try using asset_strategy = "upload" to upload images directly to Ghost

Hashnode

Hashnode is a blogging platform for developers with built-in features like custom domains, newsletters, and analytics.

Capabilities

FeatureSupport
TagsYes (max 5)
CategoriesNo
Internal LinksYes
Draft SupportYes (separate draft objects)
Math RenderingLaTeX (MathJax)
Local OutputNo

Asset Strategies

StrategySupportedDefault
embedNo
uploadNo
externalYes*
copyNo

Prerequisites

  • A Hashnode account (free)
  • A Hashnode publication (blog)

Getting Your API Token

Step 1: Access Developer Settings

  1. Sign in to hashnode.com
  2. Click your profile picture → Account Settings

Account Settings

Step 2: Generate Personal Access Token

  1. In the left sidebar, click Developer
  2. Click Generate new token
  3. Copy the token from the table (use the copy button)

Developer Page

Security Warning:

  • Never commit tokens to version control — use environment variables instead.
  • If you suspect your token has been compromised, click Revoke and generate a new one.

Step 3: Find Your Publication

  1. Go to your Hashnode homepage
  2. Find Your blogs section

Your Blogs

Step 4: Get Publication ID

  1. Click Dashboard on your blog
  2. The Publication ID is in the URL: https://hashnode.com/{publication-id}/dashboard

Dashboard

Configuration

[platforms.hashnode]
publication_id = "your-publication-id"  # Publication ID from step 4
asset_strategy = "external"             # Only external URLs supported

Environment Variables:

Set HASHNODE_API_TOKEN with your personal access token:

export HASHNODE_API_TOKEN="your-token-here"

Usage

# Preview content
typub dev posts/my-post -p hashnode

# Publish to Hashnode
typub publish posts/my-post -p hashnode

Tag Limits

Hashnode allows a maximum of 5 tags per article. If your content has more than 5 tags, typub will use the first 5.

# In your post's meta.toml
tags = ["rust", "webdev", "tutorial", "beginners", "programming"]  # All 5 will be used

Troubleshooting

“Unauthorized” error

  • Verify your API token is correct
  • Check that the token hasn’t expired or been revoked
  • Ensure HASHNODE_API_TOKEN environment variable is set

Article not appearing

  • Check if published = false in your configuration
  • Draft articles are only visible in your Hashnode dashboard
  • Verify the publication ID is correct

Images not loading

  • Hashnode only supports external image URLs
  • Configure S3/R2 storage and use asset_strategy = "external" (default)
  • Images must be publicly accessible via URL

Notion

Notion is an all-in-one workspace for notes, docs, and project management.

Capabilities

FeatureSupport
TagsYes (via multi-select property)
CategoriesNo
Internal LinksYes
Draft SupportNone
Math RenderingLaTeX
Local OutputNo

Asset Strategies

StrategySupportedDefault
embedNo
uploadYes*
externalYes
copyNo

Prerequisites

  • A Notion account (free or paid)
  • A database to publish content to

Getting Your API Token

Notion uses Internal Integrations for API access.

Step 1: Create Integration

  1. Go to notion.so/my-integrations
  2. Click + New integration

My Integrations Page

Step 2: Configure Integration

  1. Enter a name (e.g., “typub”)
  2. Select the workspace to associate with
  3. Keep Internal integration selected
  4. Click Submit

Create Integration

Step 3: Copy Secret Token

  1. On the integration page, find Internal Integration Secret
  2. Click Show then Copy

Copy Secret

Important: This token has access to pages you explicitly share with it. Keep it secure.

Step 4: Get Data Source ID

  1. Open your target database in Notion
  2. Click (View settings) → Manage data sources

Manage Data Sources

  1. Click next to the data source → Copy data source ID

Copy Data Source ID

Step 5: Connect Integration to Database

  1. Open the database page
  2. Click (more) → Connections
  3. Click + Add connection and select your integration

Connect Integration

Configuration

[platforms.notion]
data_source_id = "your-database-id"  # Database ID from step 4
tags_property = "Tags"                # Name of multi-select property for tags
asset_strategy = "upload"             # or "external"

Environment Variables:

Set NOTION_API_KEY with your integration secret:

export NOTION_API_KEY="secret_abc123..."

Usage

# Preview content
typub dev posts/my-post -p notion

# Publish to Notion
typub publish posts/my-post -p notion

Database Setup

Your Notion database should have these properties:

PropertyTypeRequiredNotes
TitleTitleYesPage title (default Name column)
TagsMulti-selectNoFor tag sync

Additional properties can be added but won’t be synced by typub.

Image Captions and Alt Text

Notion image blocks use caption for visible image text.

  • If the source contains figcaption, typub writes it to Notion image.caption (highest priority)
  • If there is no figcaption, typub falls back to image alt and writes it to image.caption
  • typub does not send image.alt in API payloads

This behavior matches the current Notion API validation for image blocks.

Troubleshooting

“Unauthorized” error

  • Verify your NOTION_API_KEY is correct
  • Ensure the integration is connected to the target database (Step 5)
  • Check that the database ID is correct (no ?v= suffix)

Tags not syncing

  • Verify tags_property matches the exact property name in Notion
  • The property must be of type “Multi-select”
  • Tags are created automatically if they don’t exist

Images not appearing

  • Notion requires images to be uploaded or have public URLs
  • Use asset_strategy = "upload" (default) or asset_strategy = "external"
  • Embedded base64 images are not supported

validation_error mentions image.alt should be not present

  • This means the request body included an unsupported image.alt field
  • typub now maps visible caption text to image.caption only

WordPress

WordPress is the world’s most popular content management system, powering over 40% of websites. typub supports publishing to WordPress via the REST API with JWT authentication.

Capabilities

FeatureSupport
TagsYes
CategoriesYes
Internal LinksYes
Draft SupportReversible (status field)
Math RenderingSVG
Local OutputNo

Asset Strategies

StrategySupportedDefaultNotes
embedYesImages embedded as data URIs
uploadYes*Upload to WordPress Media Library
externalYesUse S3/R2 URLs
copyNoWordPress requires upload or URL

Prerequisites

  • A self-hosted WordPress site (WordPress.com is not supported)
  • Administrator access to install plugins
  • JWT Authentication plugin installed and configured

Setting Up JWT Authentication

WordPress REST API requires authentication for content creation. The recommended method is JWT (JSON Web Token) authentication.

Step 1: Install JWT Authentication Plugin

Option A: Via WordPress Admin (GUI)

  1. Log in to your WordPress admin panel
  2. Go to PluginsAdd New
  3. Search for “JWT Authentication for WP REST API”
  4. Install and activate the plugin

Install Plugin

Option B: Via WP-CLI (Command Line)

If you have WP-CLI available (common in Docker setups):

wp plugin install jwt-authentication-for-wp-rest-api --activate

Step 2: Configure JWT Plugin (If Needed)

Note: Some hosting providers or Docker images may have this pre-configured. If the plugin works without manual configuration, you can skip this step.

The JWT plugin may require configuration in your wp-config.php file:

// Add these lines to wp-config.php (before "/* That's all, stop editing! */")

define('JWT_AUTH_SECRET_KEY', 'your-secret-key-here');
define('JWT_AUTH_CORS_ENABLE', true);

Security Note: Generate a strong secret key. You can use WordPress Salt Generator or any secure random string (64+ characters).

Docker Deployment

For Docker-based WordPress installations, you can set the secret key via environment variables:

# docker-compose.yml
services:
  wordpress:
    image: wordpress:latest
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: exampleuser
      WORDPRESS_DB_PASSWORD: examplepass
      WORDPRESS_DB_NAME: exampledb
      # Add JWT secret key
      WORDPRESS_CONFIG_EXTRA: |
        define('JWT_AUTH_SECRET_KEY', 'your-strong-secret-key-here');
        define('JWT_AUTH_CORS_ENABLE', true);

Or mount a custom wp-config.php with the JWT constants already defined.

Shared Hosting

On shared hosting, you may also need to enable HTTP Authorization headers in .htaccess:

RewriteEngine on
RewriteCond %{HTTP:Authorization} ^(.*)
RewriteRule ^(.*) - [E=HTTP_AUTHORIZATION:%1]

Step 3: Generate JWT Token

WordPress does not provide a UI for generating JWT tokens. You need to make an API request:

curl -X POST "https://your-site.com/wp-json/jwt-auth/v1/token" \
  -H "Content-Type: application/json" \
  -d '{"username": "your-username", "password": "your-password"}'

The response will contain your JWT token:

{
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
  "user_email": "you@example.com",
  "user_nicename": "your-username",
  "user_display_name": "Your Name"
}

Important: Copy the token value. This is your API key for typub.

Step 4: Verify Token Works

Test your token:

curl -X GET "https://your-site.com/wp-json/wp/v2/users/me" \
  -H "Authorization: Bearer YOUR_TOKEN_HERE"

If successful, you’ll see your user profile data.

Configuration

[platforms.wordpress]
base_url = "https://your-site.com"  # Your WordPress site URL (required)
api_key = "your-jwt-token"           # JWT token (or use WORDPRESS_API_KEY env var)
asset_strategy = "upload"            # Default: upload to Media Library
published = true                      # true for published, false for draft

Environment Variables:

Set WORDPRESS_API_KEY with your JWT token:

export WORDPRESS_API_KEY="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."

Or in your shell profile:

# ~/.bashrc or ~/.zshrc
export WORDPRESS_API_KEY="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."

Security Warning:

  • Never commit JWT tokens to version control — use environment variables instead.
  • JWT tokens are long-lived. If compromised, regenerate by changing your WordPress password and creating a new token.
  • Store the token securely; it provides full write access to your WordPress site.

Usage

# Preview content
typub dev posts/my-post -p wordpress

# Publish to WordPress
typub publish posts/my-post -p wordpress

Taxonomy Sync

WordPress supports both tags and categories, which typub will automatically sync:

  • Tags in your post’s front matter are synced to WordPress tags
  • Categories in your post’s front matter are synced to WordPress categories
  • If a tag or category doesn’t exist, it will be created automatically
# In your post's meta.toml
tags = ["wordpress", "tutorial", "cms"]
categories = ["Web Development", "Tutorials"]

Asset Handling

Upload Strategy (Default)

With asset_strategy = "upload", images are uploaded to your WordPress Media Library:

  1. typub uploads each image via the WordPress REST API
  2. Images appear in your Media Library
  3. Posts reference the uploaded image URLs

External Strategy

With asset_strategy = "external", use S3/R2 storage:

[storage]
type = "s3"
endpoint = "https://your-r2-endpoint.r2.cloudflarestorage.com"
bucket = "your-bucket"
region = "auto"
public_url_prefix = "https://cdn.your-domain.com"

[platforms.wordpress]
asset_strategy = "external"

Embed Strategy

With asset_strategy = "embed", images are embedded as Base64 data URIs. This is not recommended for large images due to size limitations.

Troubleshooting

“Unauthorized” error

  • Verify your JWT token is correct and not expired
  • Ensure WORDPRESS_API_KEY environment variable is set
  • Check that JWT plugin is properly configured
  • Try regenerating your token

“JWT not valid” error

  • The token may have expired or been invalidated
  • Regenerate by creating a new token via the API
  • Check that JWT_AUTH_SECRET_KEY is consistently configured

“Rest cannot access” error

  • Ensure the user has sufficient permissions (Editor or Administrator role)
  • Check that REST API is enabled on your WordPress site
  • Verify no security plugins are blocking REST API access

Images not uploading

  • Check your PHP upload limits in php.ini (upload_max_filesize, post_max_size)
  • Ensure the user has upload permissions
  • Try using asset_strategy = "external" with S3/R2 storage

Posts not updating

  • typub finds existing posts by slug
  • If you changed the slug, the old post won’t be found
  • Use the WordPress post ID in your local status database to force updates

CORS errors

  • Ensure JWT_AUTH_CORS_ENABLE is set to true
  • Check your server’s CORS headers configuration

Local Output Adapters

This section groups adapters that generate local artifacts instead of publishing directly through remote APIs.

Use these guides for file-first workflows:

Astro

The astro adapter outputs Markdown files with YAML frontmatter, designed for Astro Content Collections.

Platform Features

  • Outputs Markdown files consumable by Astro projects
  • Supports YAML frontmatter (title, date, tags, categories)
  • Preserves LaTeX math formulas with $...$ syntax
  • Images copied to local directory with relative paths

Capabilities

FeatureSupport
TagsYes (output to frontmatter)
CategoriesYes (output to frontmatter)
Internal LinksYes
Draft SupportNone (local output, no draft concept)
Math RenderingLaTeX only (must preserve formula source)
Local OutputYes

Asset Strategies

StrategySupportedDefaultNotes
copyYes*Copy images to assets dir
embedYesBase64 inline
externalYesUse external CDN URLs
uploadNoNot applicable

Math Rendering

StrategySupportDefaultNotes
latexYes*Preserves $...$ syntax for MathJax
svgNoSVG cannot reconstruct Markdown formulas
pngNoPNG cannot reconstruct Markdown formulas

Note: The Astro adapter only supports latex math rendering mode because Markdown output requires preserving formula source code for proper display.

Prerequisites

  • An Astro project
  • Typst installed (for rendering)

Configuration

[platforms.astro]
output_dir = "output/astro"  # Markdown output directory
asset_strategy = "copy"      # Default, copy images locally

Content Format

Output Structure

output/astro/
└── your-post-slug/
    ├── index.md      # Markdown + YAML frontmatter
    └── assets/       # Image resources (if using copy strategy)
        └── image1.png

Frontmatter Format

The generated index.md includes YAML frontmatter:

---
title: Your Post Title
date: 2026-02-17
tags:
  - rust
  - typst
categories:
  - programming
---
# Your Post Title

Content here...

Math Formulas

Inline formula: $E = mc^2$

Block formula:

$$
\int_0^\infty e^{-x^2} dx = \frac{\sqrt{\pi}}{2}
$$

Usage

Preview

typub dev posts/my-post -p astro

Publish

typub publish posts/my-post -p astro

Outputs Markdown file to output/astro/{slug}/index.md.

Integrate with Astro Project

Configure the output directory in Astro’s Content Collections:

// src/content/config.ts
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";

const blog = defineCollection({
  loader: glob({ pattern: "**/index.md", base: "./output/astro" }),
  schema: z.object({
    title: z.string(),
    date: z.date(),
    tags: z.array(z.string()).optional(),
    categories: z.array(z.string()).optional(),
  }),
});

export const collections = { blog };

Using with astro-typst

Besides using typub to generate Markdown, you can also use astro-typst to render Typst content directly in Astro.

astro-typst Approach

// astro-typst renders .typ files directly
import TypstDocument from 'astro-typst';

<TypstDocument src="./content.typ" />

Comparison

ApproachProsCons
typub → MDStandard Markdown, good compatibilityExtra build step required
astro-typstDirect rendering, live previewRequires Astro plugin setup
  1. Use typub for content management: Write in Typst in posts/ directory
  2. astro-typst for live preview: Render .typ files directly during development
  3. typub for multi-platform publishing: Also supports Xiaohongshu, WeChat, etc.

Advanced Configuration

Custom Slug

Specify in your article’s meta.toml:

[platforms.astro]
slug = "custom-url-slug"

Output Directory

[platforms.astro]
output_dir = "content/blog"  # Output directly to Astro content directory
asset_strategy = "copy"

Troubleshooting

Math formulas not displaying

Ensure your Astro project has MathJax or KaTeX configured:

<!-- Add to Astro layout -->
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
<script
  id="MathJax-script"
  async
  src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"
></script>

Image path issues

When using copy strategy, images use relative paths ./assets/image.png. Ensure your Markdown renderer supports relative paths.

Tags/Categories not showing

Check your meta.toml configuration:

tags = ["tag1", "tag2"]
categories = ["category1"]

Static

The static adapter generates standalone HTML files that can be directly deployed to static hosting platforms like GitHub Pages, Netlify, and Vercel.

Platform Features

  • Generates complete index.html files with embedded styles
  • Supports multiple themes
  • Directly deployable to static hosting platforms
  • Code syntax highlighting support

Capabilities

FeatureSupport
TagsNo (static HTML has no metadata)
CategoriesNo
Internal LinksYes
Draft SupportNone (local output, no draft concept)
Math RenderingSVG / PNG
Local OutputYes

Asset Strategies

StrategySupportedDefaultNotes
copyYes*Copy images to assets dir
embedYesBase64 inline
externalYesUse external CDN URLs
uploadNoNot applicable

Math Rendering

StrategySupportDefaultNotes
svgYes*Vector graphics, scalable
pngYesBitmap, good compatibility
latexNoRequires MathJax runtime

Prerequisites

  • Typst installed (for rendering)

Configuration

[platforms.static]
output_dir = "output/static"  # HTML output directory
theme = "minimal"             # Theme name
asset_strategy = "copy"       # Default, copy images locally

Available Themes

ThemeDescription
minimalClean white background, good for technical docs
elegantElegant typography, good for blog posts
darkDark theme

For creating your own theme IDs or overriding built-ins, see Theme Customization.

Content Format

Output Structure

output/static/
└── your-post-slug/
    ├── index.html      # Complete HTML file
    └── assets/         # Image resources (if using copy strategy)
        └── image1.png

HTML Structure

The generated index.html includes:

  • Complete <html>, <head>, <body> structure
  • Inline CSS styles
  • Code highlighting (using highlight.js)
  • Math formula rendering

Usage

Preview

typub dev posts/my-post -p static

Opens a preview page in your browser showing the generated HTML.

Publish

typub publish posts/my-post -p static

Outputs HTML file to output/static/{slug}/index.html.

Deploy to GitHub Pages

# 1. Publish content
typub publish posts/ -p static

# 2. Copy output to gh-pages branch
cp -r output/static/* .

# 3. Push to GitHub
git add .
git commit -m "Publish static pages"
git push origin gh-pages

Deploy to Netlify

  1. Set output/static as your publish directory
  2. Or use Netlify CLI:
netlify deploy --dir=output/static --prod

Example Output

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Your Post Title</title>
    <style>
      /* Inline styles */
      body {
        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto;
        max-width: 800px;
        margin: 0 auto;
        padding: 20px;
      }
      /* ... more styles ... */
    </style>
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github.min.css"
    />
  </head>
  <body>
    <h1>Your Post Title</h1>
    <p>Content here...</p>
  </body>
</html>

Advanced Configuration

Custom Theme

[platforms.static]
theme = "elegant"

Use External Image Hosting

[platforms.static]
asset_strategy = "external"

Requires external storage (S3/R2) configuration. See External Storage Configuration.

Output to Project Root

[platforms.static]
output_dir = "."  # Output directly to current directory

Comparison with Astro Adapter

FeatureStatic AdapterAstro Adapter
Output FormatComplete HTMLMarkdown + frontmatter
DeploymentDirect hostingRequires Astro project
Theme SupportBuilt-in themesControlled by Astro project
Content MgmtNoneNone
Use CaseSimple static sitesAstro Content Collections

Troubleshooting

Styles not applied

Ensure CSS links in the generated HTML are accessible. If using CDN resources (like highlight.js), you need network connectivity.

Math formulas appear blank

Check Typst installation and version:

typst --version

Images not displaying

  1. When using copy strategy, ensure image paths are correct
  2. When using embed strategy, check Base64 encoding
  3. When using external strategy, ensure CDN URLs are accessible

小红书 (Xiaohongshu)

小红书是中国流行的生活方式分享平台,以图文笔记和短视频为主。typub 支持将内容转换为幻灯片图片,供手动上传到小红书。

平台特点

小红书没有开放 API,因此 typub 采用图片生成 + 手动上传的方式:

  1. typub 将你的文章内容转换为精美的幻灯片图片
  2. 图片保存到本地输出目录
  3. 你手动在小红书 App 中上传这些图片

Capabilities

FeatureSupport
TagsNo(需手动添加)
CategoriesNo
Internal LinksNo(不支持外链)
Draft SupportNone(本地输出,无草稿概念)
Math RenderingSVG / PNG
Local OutputYes

Asset Strategies

StrategySupportedDefaultNotes
embedYes*图片内嵌到幻灯片中
uploadNo小红书不支持 API 上传
externalNo小红书不支持外链图片
copyNo不适用

Prerequisites

  • Typst 已安装(用于渲染幻灯片)
  • 小红书 App(用于手动上传)

Configuration

[platforms.xiaohongshu]
output_dir = "output/xiaohongshu"  # 幻灯片输出目录

Content Format

小红书适配器将你的文章内容(Typst 或 Markdown)渲染为精美的幻灯片图片。

内容文件

typub 会自动检测以下内容文件(按优先级):

  • content.typ — Typst 格式
  • content.md — Markdown 格式

可选的 slides.typ

如果你有现成的 Typst 幻灯片文件 slides.typ,typub 也会检测到它。但通常 typub 会自动将 content.typcontent.md 转换为幻灯片格式。

元数据配置

meta.toml 中可以设置以下小红书专属字段:

[platforms.xiaohongshu]
subtitle = "副标题(可选)"
author = "@你的用户名"

文章结构建议

小红书内容以图文为主,建议:

  • 使用一级标题(= Title)分隔不同幻灯片/页面
  • 每个段落简洁明了
  • 图片会自动嵌入幻灯片

Usage

预览:

typub dev posts/my-post -p xiaohongshu

Preview Example

“发布”:

typub publish posts/my-post -p xiaohongshu

生成完成后,幻灯片图片会保存在 {platforms.xiaohongshu.output_dir}/{slug}/ 目录下。

Publishing to 小红书

Step 1: 生成幻灯片

typub publish posts/my-post -p xiaohongshu

终端会显示:

Generated 5 slides at: output/xiaohongshu/my-post
Upload these images manually to 小红书

Step 2: 打开小红书 App

  1. 打开小红书 App
  2. 点击底部的 + 按钮
  3. 选择 图文

Step 3: 上传图片

  1. 点击 相册
  2. 选择生成的幻灯片图片(按顺序选择)
  3. 点击 下一步

Step 4: 添加标题和标签

  1. 输入标题(建议与文章标题一致)
  2. 添加话题标签
  3. 编写简介(可选)
  4. 点击 发布

Best Practices

标题建议

小红书标题建议:

  • 控制在 20 字以内
  • 使用吸引眼球的表达
  • 可以使用 emoji

内容长度

每张幻灯片建议:

  • 文字不超过 100 字
  • 重点突出,便于快速阅读
  • 适合手机竖屏浏览

图片数量

小红书图文笔记:

  • 最多可上传 18 张图片
  • 建议控制在 5-10 张
  • 第一张图最重要(封面)

Troubleshooting

“No slide images generated” 错误

  • 确保已安装 Typst:typst --version
  • 确保目录中有 content.typcontent.md 文件
  • 如果问题仍然存在,尝试手动运行 typst compile 查看错误

Typst 渲染失败

  • 检查 Typst 版本是否最新
  • 确保内容语法正确
  • 查看 typub 的错误输出

图片显示不正确

  • 确保图片路径正确
  • 图片格式应为 PNG 或 JPG
  • 检查图片尺寸是否合适

幻灯片数量过多

  • 调整文章结构,合并内容
  • 减少一级标题数量
  • 考虑拆分为多篇文章

Character Limits

小红书内容限制:

项目限制
标题20 字(推荐)
正文1000 字
图片数量最多 18 张
话题标签最多 5 个

Note: 这些是小红书平台的限制,typub 不会强制检查。请在上传前自行控制内容长度。

Copy-paste Platforms

This section groups platforms that require a manual copy-paste workflow.

Use these guides for clipboard-based publishing:

Typical Workflow

typub dev posts/my-post -p wechat

Then:

  1. Open the local preview URL.
  2. Copy rendered content.
  3. Paste into the target platform editor.

HTML Copy-paste Platforms

These profiles render styled HTML content for manual paste.

微信公众号

微信公众号是中国最大的内容发布平台之一,支持图文、音频、视频等多种内容形式。

平台能力

特性支持情况
输出格式HTML(富文本)
默认主题wechat-green
特殊转换li_span_wrap(列表文字不分行)

资源策略

策略支持默认
embed(Base64 内嵌)*
external(外部存储)

平台限制/注意事项

  • 图片:不支持外链图片,必须使用内嵌或手动上传
  • SVG:支持内联 SVG,但复杂 SVG 可能渲染异常
  • 字体:仅支持系统默认字体,自定义字体会被忽略
  • CSS:样式必须内联,不支持外部样式表或 <style> 标签
  • 列表:需要特殊处理防止文字分行(typub 已自动处理)
  • 代码块:支持,但语法高亮依赖内联样式

发布流程

1. 预览内容

typub dev posts/my-post -p wechat

浏览器会自动打开预览页面。

预览页面

2. 复制内容

点击预览页面的 复制内容 按钮。

3. 打开编辑器

访问 微信公众号后台,登录后点击 内容管理草稿箱新建图文

4. 粘贴内容

  1. 在编辑器中点击正文区域
  2. 使用 Ctrl+V(Windows)或 Cmd+V(Mac)粘贴
  3. 检查格式是否正确

粘贴内容

5. 处理图片

如果使用 asset_strategy = "embed",图片已经内嵌,无需额外操作。

如果图片显示为占位符或链接:

  1. 点击图片占位符
  2. 选择 上传图片 或从素材库选择

6. 发布

  1. 添加标题、摘要、封面图
  2. 点击 发布定时发布

配置选项

[platforms.wechat]
theme = "wechat-green"      # 可选主题:elegant, github, notion
asset_strategy = "embed"    # 推荐使用 embed

可用主题

主题说明
wechat-green微信绿色调,默认主题
elegant简约黑白风格
githubGitHub 风格
notionNotion 风格

常见问题

Q: 粘贴后格式丢失?

A: 确保使用预览页面的“复制内容“按钮,而不是直接复制 HTML 文件内容。部分浏览器可能需要授予剪贴板权限。

Q: 图片显示不出来?

A:

  • 检查是否使用了 asset_strategy = "embed"
  • 如果使用 external,需要在微信后台手动上传图片
  • 微信不支持外链图片,必须使用内嵌或上传

Q: 代码块样式不正确?

A: typub 使用内联样式来保持代码块格式。如果样式异常,尝试切换主题:

[platforms.wechat]
theme = "github"  # 使用 GitHub 风格

Q: 列表项文字被分成多行?

A: typub 默认启用 li_span_wrap 转换规则来防止这个问题。如果仍然出现,请检查是否正确使用了预览功能。

知乎

知乎是中国最大的知识分享平台,支持专栏文章和回答。

平台能力

特性支持情况
输出格式Markdown(导入到编辑器)
默认主题elegant
特殊转换

资源策略

策略支持默认
embed(Base64 内嵌)
external(外部存储)*

平台限制/注意事项

  • 图片:不支持外链图片,需要上传到知乎图床
  • SVG:不支持 SVG,会被忽略或显示异常
  • 图片格式:不支持内嵌图片(Base64),必须使用外部存储并手动上传
  • 字体:仅支持系统字体
  • CSS:样式会被过滤,仅保留基础格式
  • 代码块:支持,但目前语言无法自动识别,需手动选择

发布流程

1. 预览内容

typub dev posts/my-post -p zhihu

浏览器会自动打开预览页面。

2. 复制内容

点击预览页面的 复制内容 按钮。

3. 打开编辑器

访问 知乎专栏写文章

4. 导入 Markdown

  1. 在编辑器正文区域粘贴(Ctrl+VCmd+V
  2. 知乎会识别 Markdown 格式并提示“识别到特殊格式,请确认是否将 Markdown 解析为正确格式“
  3. 点击 确认并解析

识别 Markdown

  1. 内容将被转换为富文本格式,包括图片、公式等

解析后效果

5. 处理图片

由于知乎不支持内嵌图片,需要手动上传:

  1. 对于每个图片占位符,点击并选择 上传图片
  2. 选择对应的本地图片文件
  3. 或者使用 asset_strategy = "external"(默认),先上传到 S3/R2,再复制链接

推荐做法:使用 external 策略(默认),先运行 typub publish 上传图片,然后在知乎中使用图片链接。

6. 发布

  1. 添加标题
  2. 选择话题标签
  3. 点击 发布

配置选项

[platforms.zhihu]
theme = "elegant"           # 可选主题
asset_strategy = "external" # 默认值

推荐配置

由于知乎不支持内嵌图片,建议配置外部存储:

[storage]
type = "s3"
endpoint = "https://your-r2-endpoint.r2.cloudflarestorage.com"
bucket = "your-bucket"
region = "auto"
public_url_prefix = "https://cdn.your-domain.com"

[platforms.zhihu]
asset_strategy = "external"

常见问题

Q: 图片显示为空白或占位符?

A: 知乎不支持内嵌图片。解决方案:

  1. 配置外部存储(S3/R2)
  2. 设置 asset_strategy = "external"(默认值)
  3. 运行 typub dev 上传图片
  4. 预览页面会显示实际图片 URL

Q: 数学公式不显示?

A: typub 会将公式转换为 LaTeX 格式($...$$$...$$),知乎编辑器会自动渲染。如果公式未正确显示,请检查 LaTeX 语法是否正确。

Q: 样式与预览不一致?

A: 知乎会过滤大部分自定义样式。预览页面的效果仅供参考,实际显示以知乎为准。

Q: 代码块没有语法高亮?

A: 目前 typub 导出的代码块在知乎无法正确识别语言,需要在粘贴后手动选择代码语言。这是一个已知问题,未来版本会修复。

今日头条

今日头条是字节跳动旗下的内容分发平台,支持图文、视频等多种内容形式。

平台能力

特性支持情况
输出格式HTML(富文本)
代码高亮支持
资源策略embed(Base64)
数学公式支持(PNG渲染)

资源策略

策略支持默认说明
embed*Base64内嵌图片
external使用S3/R2外链

使用方法

今日头条使用复制粘贴工作流:

# 预览内容
typub dev posts/my-post -p toutiao
  1. 浏览器打开预览页面
  2. 点击 复制内容 按钮
  3. 打开 头条号创作
  4. 粘贴内容到编辑器

样例

平台限制

数学公式

  • 支持方式:通过 PNG 图片渲染数学公式
  • Inline 公式限制:粘贴后会被自动转换为 Block(独立段落)格式
  • 建议:如果文章包含大量数学公式,建议在粘贴后检查排版

链接过滤

  • 外部链接:头条会自动过滤(移除)文档中的链接
  • 影响范围:所有 <a href="..."> 标签都会被移除
  • 建议:如有重要链接,可在文末以纯文本形式列出

平台注意事项

  • 图片处理:头条会自动处理粘贴的Base64图片,上传到其CDN
  • 代码块:支持语法高亮的代码块显示
  • 内容审核:文章发布需要通过平台审核
  • 排版建议:头条读者偏好图文并茂的内容

提示

  • 标题推荐使用吸引眼球的表达
  • 配图建议使用高清大图
  • 摘要部分会在列表页展示,需精心撰写
  • 可设置封面图,建议尺寸900x500
  • 文章分类需要在编辑器中手动选择

发布流程

  1. 复制粘贴内容后,检查格式
  2. 检查链接:确认重要链接是否被过滤
  3. 检查公式:确认数学公式显示是否正确
  4. 设置文章分类
  5. 添加封面图(可选)
  6. 填写摘要(可选,系统可自动提取)
  7. 点击发布,等待审核

哔哩哔哩专栏

哔哩哔哩(B站)是国内领先的弹幕视频网站,同时提供专栏功能发布图文内容。

平台能力

特性支持情况
输出格式HTML(富文本)
代码高亮不支持
资源策略不支持
数学公式不支持
图片上传需手动

使用方法

哔哩哔哩专栏使用复制粘贴工作流:

# 预览内容
typub dev posts/my-post -p bilibili
  1. 浏览器打开预览页面
  2. 点击 复制内容 按钮
  3. 打开 哔哩哔哩专栏
  4. 粘贴内容到编辑器

Preview

平台限制

B站专栏编辑器功能较为基础,仅支持基本格式,发布前请检查:

格式支持

  • 支持:标题、段落、加粗、斜体、列表
  • 不支持:代码高亮、代码块、数学公式、表格、图片上传
  • 代码处理:代码块会被渲染为普通文本,无语法高亮
  • 图片处理:不支持粘贴或上传图片,需要手动在编辑器中添加

链接

  • 支持插入链接,但需要手动在编辑器中添加
  • 粘贴的链接可能被转为纯文本

平台注意事项

  • 内容审核:文章发布需要通过平台审核
  • 字数要求:专栏文章有最低字数要求
  • 排版建议:B 站用户偏好图文并茂、轻松活泼的内容
  • 封面图:需单独设置,建议尺寸 1146x717
  • 图片限制:正文不支持图片,如有图片需求请考虑其他平台

提示

  • 标题建议吸引眼球,符合 B 站社区风格
  • 代码内容建议截图后作为图片插入
  • 可添加视频链接与文章联动
  • 如需大量配图,建议考虑其他平台

发布流程

  1. 复制粘贴内容后,检查格式
  2. 检查代码块:确认代码显示是否正确(无高亮)
  3. 设置专栏封面图
  4. 选择文章分类
  5. 点击发布,等待审核

Markdown Copy-paste Platforms

These profiles render Markdown for manual paste.

51CTO 博客

51CTO 博客是中国知名的 IT 技术博客平台,支持 Markdown 格式发布文章。

平台能力

特性支持情况
输出格式Markdown
默认主题无(纯 Markdown)
特殊转换

资源策略

策略支持默认
embed(Base64 内嵌)*
external(外部存储)

平台限制/注意事项

  • Markdown 语法:支持标准 Markdown,包括 GFM 扩展
  • 图片:支持 Base64 内嵌和外链图片
  • 代码块:支持语法高亮
  • 数学公式:支持 LaTeX 语法($...$$$...$$
  • 表格:支持 GFM 表格语法

发布流程

1. 预览内容

typub dev posts/my-post -p 51cto

浏览器会打开预览页面,显示生成的 Markdown 内容。

2. 复制内容

点击预览页面的 复制内容 按钮,将 Markdown 文本复制到剪贴板。

3. 打开编辑器

访问 51CTO 博客发布页面

4. 粘贴内容

在 Markdown 编辑区域粘贴内容,右侧会实时预览渲染效果。

粘贴内容

5. 处理图片

方式一:使用 Base64 内嵌(推荐)

51CTO 编辑器支持 Base64 内嵌图片,使用默认的 asset_strategy = "embed" 即可,无需额外处理。

方式二:使用外链

如果使用 asset_strategy = "external",图片 URL 会直接嵌入 Markdown 中,无需额外处理。

方式三:上传到 51CTO

  1. 点击编辑器工具栏的 图片 按钮
  2. 选择本地上传
  3. 替换 Markdown 中的图片链接

6. 发布

  1. 填写标题
  2. 选择文章分类和标签
  3. 点击 发布

配置选项

[platforms.51cto]
# asset_strategy = "embed"  # 默认,使用 Base64 内嵌图片

Markdown 特性支持

特性支持说明
标题# ~ ######
列表有序、无序、嵌套
代码块支持语法高亮
表格GFM 格式
引用> 语法
链接外链、内部链接
图片Base64、外链或上传
数学公式LaTeX 语法
任务列表- [ ] / - [x]
脚注不支持

常见问题

Q: 代码块没有语法高亮?

A: 确保代码块指定了语言标识:

```python
print("hello")
```

Q: 图片无法显示?

A: 51CTO 支持 Base64 内嵌图片,默认配置即可正常显示。如果使用外链图片,检查 URL 是否可访问。

Q: 数学公式渲染异常?

A: 51CTO 使用 KaTeX 渲染公式。某些 LaTeX 命令可能不支持。常见问题:

  • 避免使用 \begin{align} 等复杂环境
  • 使用 \displaystyle 替代 \dfrac

阿里云开发者社区

阿里云开发者社区是阿里云官方的技术内容平台,支持 Markdown 格式发布文章。

平台能力

特性支持情况
输出格式Markdown
默认主题无(纯 Markdown)
特殊转换

资源策略

策略支持默认
embed(Base64 内嵌)*
external(外部存储)

平台限制/注意事项

  • Markdown 语法:支持标准 Markdown,包括 GFM 扩展
  • 图片:支持 Base64 内嵌和外链图片
  • 代码块:支持语法高亮
  • 数学公式:支持 LaTeX 语法($...$$$...$$
  • 表格:支持 GFM 表格语法

发布流程

1. 预览内容

typub dev posts/my-post -p aliyun

浏览器会打开预览页面,显示生成的 Markdown 内容。

2. 复制内容

点击预览页面的 复制内容 按钮,将 Markdown 文本复制到剪贴板。

3. 打开编辑器

访问 阿里云开发者社区写文章

4. 粘贴内容

在 Markdown 编辑区域粘贴内容,右侧会实时预览渲染效果。

粘贴内容

5. 处理图片

方式一:使用 Base64 内嵌(推荐)

阿里云编辑器支持 Base64 内嵌图片,使用默认的 asset_strategy = "embed" 即可,无需额外处理。

方式二:使用外链

如果使用 asset_strategy = "external",图片 URL 会直接嵌入 Markdown 中,无需额外处理。

方式三:上传到阿里云

  1. 点击编辑器工具栏的 图片 按钮
  2. 选择本地上传
  3. 替换 Markdown 中的图片链接

6. 发布

  1. 填写标题
  2. 选择文章分类和标签
  3. 点击 发布

配置选项

[platforms.aliyun]
# asset_strategy = "embed"  # 默认,使用 Base64 内嵌图片

Markdown 特性支持

特性支持说明
标题# ~ ######
列表有序、无序、嵌套
代码块支持语法高亮
表格GFM 格式
引用> 语法
链接外链、内部链接
图片Base64、外链或上传
数学公式LaTeX 语法
任务列表- [ ] / - [x]
脚注不支持

常见问题

Q: 代码块没有语法高亮?

A: 确保代码块指定了语言标识:

```python
print("hello")
```

Q: 图片无法显示?

A: 阿里云支持 Base64 内嵌图片,默认配置即可正常显示。如果使用外链图片,检查 URL 是否可访问。

Q: 数学公式渲染异常?

A: 阿里云使用 KaTeX 渲染公式。某些 LaTeX 命令可能不支持。常见问题:

  • 避免使用 \begin{align} 等复杂环境
  • 使用 \displaystyle 替代 \dfrac

博客园 (CNBlogs)

博客园是中国知名的技术博客平台,支持 Markdown 格式发布文章。

平台能力

特性支持情况
输出格式Markdown
默认主题无(纯 Markdown)
特殊转换

资源策略

策略支持默认
embed(Base64 内嵌)*
external(外部存储)

平台限制/注意事项

  • Markdown 语法:支持标准 Markdown,包括 GFM 扩展
  • 编辑器选择:推荐使用 MarkdownEditor.md 编辑器
  • 图片:支持 Base64 内嵌和外链图片
  • 代码块:支持语法高亮
  • 数学公式:支持 LaTeX 语法,但需要手动开启
  • 表格:支持 GFM 表格语法

发布流程

1. 预览内容

typub dev posts/my-post -p cnblogs

浏览器会打开预览页面,显示生成的 Markdown 内容。

2. 复制内容

点击预览页面的 复制内容 按钮,将 Markdown 文本复制到剪贴板。

3. 打开编辑器

访问 博客园写文章

4. 选择编辑器

点击右上角的 编辑器 下拉菜单,选择 MarkdownEditor.md

选择编辑器

提示:Markdown 和 Editor.md 都能支持我们需要的功能。Editor.md 提供实时预览。

5. 粘贴内容

在 Markdown 编辑区域粘贴内容。

粘贴内容

6. 开启数学公式支持

博客园默认不开启数学公式渲染,需要手动开启:

  1. 点击编辑器右侧的 数学公式 按钮
  2. 勾选 启用数学公式支持
  3. 选择渲染引擎(推荐 MathJax3)
  4. 点击 确定

开启数学公式

7. 处理图片

方式一:使用 Base64 内嵌(推荐)

博客园 Markdown 编辑器支持 Base64 内嵌图片,使用默认的 asset_strategy = "embed" 即可,无需额外处理。

方式二:使用外链

如果使用 asset_strategy = "external",图片 URL 会直接嵌入 Markdown 中,无需额外处理。

方式三:上传到博客园

  1. 点击编辑器工具栏的 图片 按钮
  2. 选择本地上传
  3. 替换 Markdown 中的图片链接

8. 发布

  1. 填写标题
  2. 选择文章分类和标签
  3. 点击 发布

配置选项

[platforms.cnblogs]
# asset_strategy = "embed"  # 默认,使用 Base64 内嵌图片

Markdown 特性支持

特性支持说明
标题# ~ ######
列表有序、无序、嵌套
代码块支持语法高亮
表格GFM 格式
引用> 语法
链接外链、内部链接
图片Base64、外链或上传
数学公式LaTeX 语法(需手动开启)
任务列表- [ ] / - [x]
脚注不支持

常见问题

Q: 数学公式不渲染?

A: 博客园默认不开启数学公式支持。请按照上述步骤手动开启:

  1. 点击 数学公式 按钮
  2. 勾选 启用数学公式支持
  3. 选择 MathJax3 引擎

Q: 代码块没有语法高亮?

A: 确保代码块指定了语言标识:

```python
print("hello")
```

Q: 图片无法显示?

A: 博客园支持 Base64 内嵌图片,默认配置即可正常显示。如果使用外链图片,检查 URL 是否可访问。

Q: 应该选择哪个编辑器?

A: 推荐使用 MarkdownEditor.md

  • Markdown:默认 Markdown 编辑器,简洁
  • Editor.md:提供实时预览功能

TinyMCE 和 TinyMCE5 是 HTML 富文本编辑器,不适合直接粘贴 Markdown。

CSDN

CSDN 是中国最大的 IT 技术社区,支持 Markdown 格式发布文章。

平台能力

特性支持情况
输出格式Markdown
默认主题无(纯 Markdown)
特殊转换

资源策略

策略支持默认
embed(Base64 内嵌)
external(外部存储)*

平台限制/注意事项

  • Markdown 语法:支持标准 Markdown,包括 GFM 扩展
  • 图片:支持外链图片,但建议使用 CSDN 图床以保证稳定性
  • 代码块:支持语法高亮
  • 数学公式:支持 LaTeX 语法($...$$$...$$
  • 表格:支持 GFM 表格语法

发布流程

1. 预览内容

typub dev posts/my-post -p csdn

浏览器会打开预览页面,显示生成的 Markdown 内容。

2. 复制内容

点击预览页面的 复制内容 按钮,将 Markdown 文本复制到剪贴板。

3. 打开编辑器

访问 CSDN Markdown 编辑器

4. 粘贴内容

  1. 在左侧编辑区域粘贴 Markdown 内容
  2. 右侧会实时预览渲染效果

CSDN 编辑器

5. 处理图片

方式一:使用外链(推荐)

如果使用 asset_strategy = "external"(默认),图片 URL 会直接嵌入 Markdown 中,无需额外处理。

方式二:上传到 CSDN 图床

  1. 点击编辑器工具栏的 图片 按钮
  2. 选择 本地上传
  3. 替换 Markdown 中的图片链接

6. 发布

  1. 点击 发布文章
  2. 填写标题、摘要、标签
  3. 选择文章类型和可见性
  4. 点击 发布

配置选项

[platforms.csdn]
asset_strategy = "external"  # 默认值
output_dir = "output/csdn"   # 可选,自定义输出目录

Markdown 特性支持

特性支持说明
标题# ~ ######
列表有序、无序、嵌套
代码块支持语法高亮
表格GFM 格式
引用> 语法
链接外链、内部链接
图片外链或上传
数学公式LaTeX 语法
任务列表- [ ] / - [x]
脚注不支持

常见问题

Q: 代码块没有语法高亮?

A: 确保代码块指定了语言标识:

```python
print("hello")
```

Q: 图片无法显示?

A: 检查图片 URL 是否可访问。建议:

  1. 使用 asset_strategy = "external" 并配置可靠的 CDN
  2. 或者手动上传到 CSDN 图床

Q: 数学公式渲染异常?

A: CSDN 使用 KaTeX 渲染公式。某些 LaTeX 命令可能不支持。常见问题:

  • 避免使用 \begin{align} 等复杂环境
  • 使用 \displaystyle 替代 \dfrac

InfoQ 写作社区

InfoQ 写作社区是极客邦旗下的技术内容平台,支持 Markdown 格式发布文章。

平台能力

特性支持情况
输出格式Markdown
默认主题无(纯 Markdown)
特殊转换

资源策略

策略支持默认
embed(Base64 内嵌)*
external(外部存储)

平台限制/注意事项

  • Markdown 语法:支持标准 Markdown,包括 GFM 扩展
  • 图片:支持 Base64 内嵌和外链图片
  • 代码块:支持语法高亮
  • 数学公式:支持 LaTeX 语法($...$$$...$$
  • 表格:支持 GFM 表格语法

发布流程

1. 预览内容

typub dev posts/my-post -p infoq

浏览器会打开预览页面,显示生成的 Markdown 内容。

2. 复制内容

点击预览页面的 复制内容 按钮,将 Markdown 文本复制到剪贴板。

3. 打开编辑器

访问 InfoQ 写作社区

4. 粘贴内容

在 Markdown 编辑区域粘贴内容,右侧会实时预览渲染效果。

粘贴内容

5. 处理图片

方式一:使用 Base64 内嵌(推荐)

InfoQ 编辑器支持 Base64 内嵌图片,使用默认的 asset_strategy = "embed" 即可,无需额外处理。

方式二:使用外链

如果使用 asset_strategy = "external",图片 URL 会直接嵌入内容中,无需额外处理。

方式三:上传到 InfoQ

  1. 点击编辑器工具栏的 图片 按钮
  2. 选择本地上传
  3. 图片会自动插入到文章中

6. 发布

  1. 填写标题
  2. 点击 发布 按钮
  3. 选择文章分类和标签

配置选项

[platforms.infoq]
# asset_strategy = "embed"  # 默认,使用 Base64 内嵌图片

Markdown 特性支持

特性支持说明
标题# ~ ######
列表有序、无序、嵌套
代码块支持语法高亮
表格GFM 格式
引用> 语法
链接外链、内部链接
图片Base64、外链或上传
数学公式LaTeX 语法
任务列表- [ ] / - [x]
脚注不支持

常见问题

Q: 代码块没有语法高亮?

A: 确保代码块指定了语言标识:

```python
print("hello")
```

Q: 图片无法显示?

A: InfoQ 支持 Base64 内嵌图片,默认配置即可正常显示。如果使用外链图片,检查 URL 是否可访问。

Q: 数学公式渲染异常?

A: InfoQ 使用 KaTeX 渲染公式。某些 LaTeX 命令可能不支持。常见问题:

  • 避免使用 \begin{align} 等复杂环境
  • 使用 \displaystyle 替代 \dfrac

简书

简书是一个简洁的写作和阅读平台,支持Markdown格式。

平台能力

特性支持情况
输出格式Markdown
资源策略external(外链)
数学渲染LaTeX
代码高亮平台内置

资源策略

策略支持默认说明
embed不支持Base64图片
external*使用S3/R2外链

使用方法

# 预览内容
typub dev posts/my-post -p jianshu
  1. 浏览器打开预览页面
  2. 点击 复制内容 按钮
  3. 打开 简书写作
  4. 粘贴内容

平台注意事项

  • 图片要求:简书不支持Base64内嵌图片,必须使用外部链接
  • 格式兼容:简书对Markdown的支持较为标准
  • 数学公式:支持LaTeX格式
  • 字数统计:简书显示实时字数统计

配置示例

[storage]
type = "s3"
endpoint = "https://your-r2-endpoint.r2.cloudflarestorage.com"
bucket = "your-bucket"
region = "auto"
url_prefix = "https://cdn.your-domain.com"

[platforms.jianshu]
asset_strategy = "external"

提示

  • 简书适合长文写作,排版简洁
  • 可设置文章为仅自己可见(私密)
  • 支持文集功能整理系列文章
  • 发布后可在我的文章中管理

掘金

掘金是面向开发者的技术社区平台,支持Markdown格式文章发布。

平台能力

特性支持情况
输出格式Markdown
默认主题github
资源策略external(外链)
数学渲染LaTeX
代码高亮平台内置

资源策略

策略支持默认说明
embed不支持Base64图片
external*使用S3/R2外链

使用方法

掘金使用复制粘贴工作流:

# 预览内容
typub dev posts/my-post -p juejin

# 发布到掘金
typub pub -p juejin posts/my-post
  1. 内容自动复制到剪贴板
  2. 浏览器自动打开 掘金创作中心
  3. 粘贴内容

平台注意事项

  • 图片来源:掘金不支持Base64内嵌图片,必须使用外部链接
  • 配置存储:需要在 profiles.toml 中配置S3/R2存储
  • 代码高亮:掘金编辑器会自动处理代码块语法高亮
  • 数学公式:支持LaTeX格式,使用 $...$$$...$$ 分隔符

换行保留

掘金编辑器在粘贴时会去除“多余“的空白行。typub 已针对此问题进行了优化:

  • 自动在段落、标题、列表等块级元素之间输出双倍换行
  • 确保粘贴后格式正确保留

配置示例

[storage]
type = "s3"
endpoint = "https://your-r2-endpoint.r2.cloudflarestorage.com"
bucket = "your-bucket"
region = "auto"
url_prefix = "https://cdn.your-domain.com"

[platforms.juejin]
asset_strategy = "external"

提示

  • 掘金对文章标题有字数限制,建议控制在30字以内
  • 封面图需要单独在编辑器中设置
  • 标签在掘金编辑器中选择,最多5个
  • 文章发布后可在创作中心管理

Medium

Medium is a popular online publishing platform with a focus on long-form content and thought leadership.

Capabilities

FeatureSupport
Output FormatHTML (rich text)
Default Themenotion
TablesNo
ImagesNo (manual)
Math FormulasNo
Code HighlightPartial (no lang)

Asset Strategies

StrategySupportedNotes
embedProblematicMedium rejects base64 images
externalProblematicMedium strips external image URLs on paste

⚠️ Neither embed nor external strategy works for images on Medium due to platform restrictions.

Usage

Medium uses the copy-paste workflow:

# Preview content
typub dev posts/my-post -p medium
  1. Browser opens with the rendered preview
  2. Click Copy Content button
  3. Open Medium Editor
  4. Paste the content

Platform Limitations

⚠️ Important: Medium has significant platform limitations that are outside typub’s control. Consider using a different platform if your content heavily relies on images, tables, or formulas.

Images Not Supported

Both embed and external strategies fail on Medium:

StrategyResult
embed (Base64)Medium rejects base64 data URIs entirely
external (S3/R2 URLs)Medium strips <img> tags with external URLs during paste

Workaround: After pasting text content, manually upload images through Medium’s editor using the image upload button.

Math Formulas Not Supported

Math rendering does not work on Medium:

  • PNG images: Stripped along with other images
  • SVG: Not supported by Medium
  • LaTeX: Medium has no LaTeX support

Workaround:

  • Convert formulas to images and manually upload them
  • Use Unicode symbols for simple equations (e.g., ×, ÷, ², π)
  • Write formulas in plain text notation

Tables Not Supported

Medium does not support HTML tables. Tables will display as raw HTML code or error messages.

Workaround: Convert tables to:

  • Bullet/numbered lists
  • Text descriptions
  • Screenshots of rendered tables (then manually upload)

Code Blocks Lose Language Labels

Syntax highlighting is preserved (inline <span> styles), but Medium does not recognize language attributes. Code blocks display with colors but:

  • No language indicator
  • No syntax-aware editing

Workaround: Manually add a caption above code blocks to indicate the language.

What Works Well

  • Headings (H1, H2, H3)
  • Paragraphs and text formatting (bold, italic, links)
  • Bullet and numbered lists
  • Blockquotes
  • Horizontal rules
  • Inline code (backticks)
  • Colored code blocks (no language label)

Tips

  • Keep titles concise—Medium has character limits
  • Use headers (H2, H3) for clear structure
  • Avoid tables; use lists or text descriptions instead
  • Plan to manually upload all images after pasting
  • Convert math formulas to images or Unicode symbols
  • Add language hints above code blocks for context

When to Choose Another Platform

Consider using an alternative platform if your content:

  • Contains many images
  • Requires mathematical formulas
  • Uses data tables
  • Needs preserved image/asset URLs

Alternatives with better support: Dev.to, Ghost, Hashnode, WordPress

SegmentFault 思否

SegmentFault(思否)是中文技术问答社区和博客平台,支持Markdown格式。

平台能力

特性支持情况
输出格式Markdown
资源策略external(外链)
数学分隔符\(...\) 行内,$$...$$ 块级
代码高亮平台内置

资源策略

策略支持默认说明
embed不支持Base64图片
external*使用S3/R2外链

使用方法

# 预览内容
typub dev posts/my-post -p segmentfault
  1. 浏览器打开预览页面
  2. 点击 复制内容 按钮
  3. 打开 思否写文章
  4. 粘贴内容

Edit

渲染结果示例

Preview

数学公式分隔符

SegmentFault使用特殊的数学分隔符组合:

  • 行内公式\(...\)
  • 块级公式$$...$$

typub会自动进行处理。

配置示例

[storage]
type = "s3"
endpoint = "https://your-r2-endpoint.r2.cloudflarestorage.com"
bucket = "your-bucket"
region = "auto"
url_prefix = "https://cdn.your-domain.com"

[platforms.segmentfault]
asset_strategy = "external"

提示

  • 思否的阅读体验偏向技术文章
  • 文章可关联到问答问题
  • 支持设置专栏收录
  • 图片必须使用外部链接

腾讯云开发者社区

腾讯云开发者社区是腾讯云官方的技术内容平台,支持 Markdown 格式发布文章。

平台能力

特性支持情况
输出格式Markdown
默认主题无(纯 Markdown)
特殊转换

资源策略

策略支持默认
embed(Base64 内嵌)*
external(外部存储)

平台限制/注意事项

  • Markdown 语法:支持标准 Markdown,包括 GFM 扩展
  • 图片:支持外链图片,建议使用腾讯云 COS 或可靠 CDN
  • 代码块:支持语法高亮
  • 数学公式:支持 LaTeX 语法($...$$$...$$
  • 表格:支持 GFM 表格语法

发布流程

1. 预览内容

typub dev posts/my-post -p tencentcloud

浏览器会打开预览页面,显示生成的 Markdown 内容。

2. 复制内容

点击预览页面的 复制内容 按钮,将 Markdown 文本复制到剪贴板。

3. 打开编辑器

访问 腾讯云开发者社区写作中心

腾讯云编辑器

4. 粘贴内容

  1. 点击右上角 切换 MD 编辑器 进入 Markdown 模式
  2. 在左侧编辑区域粘贴 Markdown 内容
  3. 右侧会实时预览渲染效果

粘贴内容

5. 处理图片

方式一:使用 Base64 内嵌(推荐)

腾讯云 Markdown 编辑器支持 Base64 内嵌图片,使用默认的 asset_strategy = "embed" 即可,无需额外处理。

方式二:使用外链

如果使用 asset_strategy = "external",图片 URL 会直接嵌入 Markdown 中,无需额外处理。

方式三:上传到腾讯云

  1. 点击编辑器工具栏的 图片 按钮
  2. 选择本地上传或使用腾讯云 COS
  3. 替换 Markdown 中的图片链接

6. 发布

  1. 填写标题
  2. 选择文章分类和标签
  3. 点击 去发布存草稿

配置选项

[platforms.tencentcloud]
# asset_strategy = "embed"  # 默认,使用 Base64 内嵌图片

Markdown 特性支持

特性支持说明
标题# ~ ######
列表有序、无序、嵌套
代码块支持语法高亮
表格GFM 格式
引用> 语法
链接外链、内部链接
图片Base64、外链或上传
数学公式LaTeX 语法
任务列表- [ ] / - [x]
脚注不支持

常见问题

Q: 代码块没有语法高亮?

A: 确保代码块指定了语言标识:

```python
print("hello")
```

Q: 图片无法显示?

A: 腾讯云支持 Base64 内嵌图片,默认配置即可正常显示。如果使用外链图片,检查 URL 是否可访问。

Q: 数学公式渲染异常?

A: 腾讯云使用 KaTeX 渲染公式。某些 LaTeX 命令可能不支持。常见问题:

  • 避免使用 \begin{align} 等复杂环境
  • 使用 \displaystyle 替代 \dfrac

Q: 表格显示不正确?

A: 确保表格使用标准 GFM 格式,且有表头分隔行:

| 列1 | 列2 |
| --- | --- |
| A   | B   |

Developer Guide

Audience: contributors, adapter authors, and integrators.

This section contains platform-agnostic developer-facing references.

Sections

RFC Index

This section contains normative specifications for typub behavior.

Suggested Reading Order

RFC-0001: typub: a typst-first local cms

Version: 0.1.0 | Status: normative | Phase: stable


1. Summary

[RFC-0001:C-SUMMARY] Summary (Informative)

Typub is a local-first, Typst-first CMS intended for writers who want plain files, deterministic rendering, and multi-platform publishing without surrendering ownership to a hosted editor.

This RFC establishes the project vision and boundaries for RFC-0001. It is intentionally informative and does not define normative protocol or API requirements.

Scope for RFC-0001:

  • Define the high-level product identity and target user workflow.
  • Define the principles that guide future normative RFCs.
  • Define explicit non-goals to prevent accidental scope creep.

Out of scope for RFC-0001:

  • Adapter-specific API contracts.
  • Storage and schema contracts.
  • Detailed CLI or renderer behavior guarantees.

This RFC is vision-only by design. Normative requirements will be introduced in subsequent RFCs.

Since: v0.1.0

[RFC-0001:C-VISION] Product Vision (Informative)

Typub is intended to be treated as a writing system first and a distribution system second.

Vision pillars:

  1. Typst-first authoring

    • Typst is the primary source format.
    • Markdown support exists for interoperability, not as the design center.
  2. Local-first ownership

    • Content, metadata, assets, and publish status remain local project artifacts.
    • Platform integrations are adapters, not sources of truth.
  3. Multi-platform by composition

    • A single source post can be transformed and published to multiple targets through adapter boundaries.
    • Platform divergence is expected and should be isolated behind adapter contracts.
  4. Deterministic pipeline

    • Rendering, transformation, and publish steps should remain auditable and reproducible from repository state.

Rationale: This direction aligns typub with long-lived technical writing workflows where files, version control, and portability matter more than SaaS-native editing UX.

Since: v0.1.0

[RFC-0001:C-GOALS] Goals and Non-Goals (Informative)

Goals:

  • A clear authoring-to-publish flow centered on local files.
  • Platform adapters that preserve core intent while allowing target-specific output constraints.
  • Governance readiness so future implementation RFCs can reference RFC-0001 as baseline intent.

Non-goals:

  • Becoming a hosted CMS.
  • Enforcing identical output semantics across all publishing targets.
  • Abstracting away every platform limitation at the cost of architectural clarity.

Forward references: Future normative RFCs SHOULD reference RFC-0001 for product alignment and define concrete MUST/SHOULD/MAY requirements at the feature level.

Since: v0.1.0


2. Specification

[RFC-0001:C-SPEC-PLACEHOLDER] Specification Scope Note (Informative)

This section intentionally contains no normative clauses in RFC-0001.

Normative requirements (MUST/SHOULD/MAY) will be specified in follow-up RFCs that cover concrete behavior and contracts.

Since: v0.1.0


Changelog

v0.1.0 (2026-02-11)

Initial draft

RFC-0002: publish pipeline contract

Version: 0.2.1 | Status: normative | Phase: impl


1. Summary

[RFC-0002:C-SUMMARY] Summary (Informative)

RFC-0002 defines the normative contract for typub’s publish pipeline, grounded in product intent from RFC-0001.

This RFC specifies:

  • The required pipeline stages and their ordering.
  • The contract between shared pipeline logic and platform adapters.
  • Failure handling and status persistence semantics during publish.

This RFC does not define platform-specific API details. Those belong to adapter-specific RFCs/ADRs. Update and republish semantics are out of scope for this draft and will be defined in a follow-up RFC.

Since: v0.1.0


2. Specification

[RFC-0002:C-PIPELINE-STAGES] Pipeline Stages (Normative)

The publish operation MUST execute in this logical order:

  1. Resolve: Resolve content input and metadata.
  2. Render: Render source content (Typst/Markdown) into HTML string.
  3. Parse: Parse rendered content into a semantic document IR root (Document) rather than a bare node vector.
  4. Transform: Apply shared, adapter-agnostic transformations on semantic IR.
  5. Specialize: Perform adapter-specific payload planning, including collecting pending asset references and adapter metadata. This stage MUST NOT serialize IR to final target output and SHOULD avoid remote side effects.
  6. Provision: Find or create remote target identity when required by the target API. This stage is OPTIONAL when publish can resolve identity atomically.
  7. Materialize: Resolve asset references required by selected asset strategy. This stage MUST operate on the document asset index and/or specialization context, and MUST NOT require placeholder string replacement. This stage MAY use provision context from Stage 6. This stage is OPTIONAL when no remote asset materialization is needed.
  8. Serialize: Convert resolved semantic IR to target output format (for example Markdown, Confluence storage format, Notion blocks). This is the first stage producing final output payload.
  9. Publish: Execute adapter publish operation using serialized payload.
  10. Persist: Persist successful publish result into status tracking.

IR preservation principle: Stages 3 through 7 MUST operate on typed semantic IR. Final target serialization MUST NOT occur before Stage 8.

Rationale: Delaying serialization until after materialization avoids placeholder-collision classes of bugs and preserves type-safe transformations.

Implementation MAY combine adjacent stages internally, and MAY use different intermediate representations across adapter families, but externally observable behavior (side effects and error propagation) MUST be equivalent to this order.

A stage MUST NOT mutate source content files.

Since: v0.1.0

[RFC-0002:C-ADAPTER-BOUNDARY] Adapter Boundary Contract (Normative)

For this RFC, an adapter is the platform-specific integration component that translates shared pipeline output into target-specific publish API requests and responses.

Each adapter implementation MUST declare:

  • required input/output format expectations,
  • asset strategy behavior,
  • capability surface (for example tags/categories/internal-links support),
  • unsupported behavior per capability gap (warn+degrade or hard error).

Capability declarations SHOULD be available in a machine-readable or centrally documented form.

Shared pipeline logic MUST remain platform-agnostic where practical. Adapter code MUST encapsulate platform-specific API calls and target-specific output shaping.

An adapter MUST NOT bypass status tracking writes on successful publish.

Since: v0.1.0

[RFC-0002:C-FAILURE-SEMANTICS] Failure and Status Semantics (Normative)

For this RFC, post+platform key means (post slug, platform name), where platform name is the adapter identifier.

If any stage before adapter publish fails, the operation MUST terminate without recording a successful publish result.

If adapter publish fails, the operation MUST surface an error and MUST NOT mark content as published in status tracking.

A successful publish record MUST, at minimum, persist published=true and published_at for the post+platform key. When provided by the target platform, platform-specific identifier MUST be persisted. URL MAY be absent.

Status persistence for a successful publish MUST be atomic at the local storage layer (for example, a single database transaction or crash-safe single-file replace), so a partial successful record is not observable.

If status persistence fails after adapter publish succeeds, the operation MUST be surfaced to the caller as a failure and MUST include reconciliation guidance to recover local status for the already-published remote state.

Retry behavior MAY be implemented by callers, but retries MUST preserve idempotent status semantics (no duplicate successful entries for one final published state).

Since: v0.1.0


Changelog

v0.2.1 (2026-02-21)

Terminology alignment with semantic IR

Added

  • Update C-PIPELINE-STAGES to Document-root semantic IR and asset-index materialization language

v0.2.0 (2026-02-11)

Add Stage 5 (Provision) to the pipeline

Added

  • Add Provision stage between Finalize and Materialize for remote target identity resolution
  • Renumber Materialize to Stage 6, Publish to Stage 7, Persist to Stage 8

v0.1.0 (2026-02-11)

Initial draft

RFC-0003: update and republish semantics

Version: 0.1.0 | Status: normative | Phase: impl


1. Summary

[RFC-0003:C-SUMMARY] Summary (Informative)

RFC-0003 defines update and republish semantics for typub and complements RFC-0002.

This RFC specifies:

  • how the system decides create vs update for a post+platform key,
  • idempotency guarantees for repeated publish attempts,
  • conflict and retry behavior when remote state diverges,
  • status tracking requirements for successful republish outcomes.

This RFC does not define platform-specific API payload details. Adapter-specific mapping remains implementation-specific as long as it satisfies the normative behavior here.

Since: v0.1.0


2. Specification

[RFC-0003:C-DECISION-KEY] Create-vs-Update Decision Key (Normative)

The system MUST determine create-vs-update behavior per post+platform key, where post+platform key means (post slug, adapter identifier).

Decision order:

  1. If a previously published remote identifier exists in local status for the post+platform key, the publish attempt MUST execute as an update path using that identifier.
  2. If no remote identifier exists, the adapter MAY resolve an update target via a deterministic platform-native lookup key (for example slug, title, or database-specific key).
  3. Cached URL MAY be used only as a non-authoritative hint. Cached URL alone MUST NOT be treated as identity.
  4. If no update target is resolved, the publish attempt MAY proceed to create only after duplicate precheck according to RFC-0003:C-CONFLICTS.

The decision key semantics in this RFC MUST remain consistent with status semantics in RFC-0002:C-FAILURE-SEMANTICS.

Since: v0.1.0

[RFC-0003:C-IDEMPOTENCY] Republish Idempotency (Normative)

Repeated publish attempts for the same post+platform key and same logical content state MUST be idempotent from the perspective of local status and SHOULD avoid creating duplicate remote objects.

Requirements:

  • The system MUST maintain at most one final successful status record per post+platform key.
  • A retried attempt MUST update the existing status record instead of creating duplicate successful records.
  • Republish of changed content MAY result in a different remote revision/version, but local status MUST continue to represent one current successful published state for the key.
  • If a duplicate remote object cannot be avoided due to target-platform limitations, the adapter MUST surface reconciliation guidance and retain one canonical local status record.

Callers MAY retry failed operations, but retries MUST preserve these idempotent status semantics.

Since: v0.1.0

[RFC-0003:C-CONFLICTS] Conflict and Retry Policy (Normative)

When remote state diverges from local assumptions during update, the adapter MUST surface a conflict-class error unless a fallback-to-create path is explicitly supported by the adapter behavior and duplicate precheck passes.

Conflict examples include:

  • missing remote object for stored remote identifier,
  • remote version/precondition mismatch,
  • immutable-state rejection by target platform,
  • duplicate-create candidate detected during create precheck.

Duplicate-create rule:

  • If update target cannot be resolved and create precheck indicates an equivalent remote object already exists, the operation MUST fail with conflict-class error.
  • In this case, the adapter MUST NOT create a new remote object.

Retry policy:

  • Callers MAY retry transient failures.
  • Callers SHOULD NOT blindly retry deterministic conflict errors without changing local or remote state.
  • If fallback-to-create is used after update miss, the operation MUST be observable in logs as an update->create transition.

Since: v0.1.0

[RFC-0003:C-STATUS] Republish Status Semantics (Normative)

On successful create or update, status persistence MUST satisfy RFC-0002:C-FAILURE-SEMANTICS and additionally meet the following republish rules:

  • The persisted post+platform record MUST represent the latest successful publish outcome.
  • If a remote identifier changes because of update->create fallback, local status MUST be replaced with the new identifier atomically.
  • If remote publish succeeds but local status persistence fails, the operation MUST fail and MUST include reconciliation guidance consistent with RFC-0002:C-FAILURE-SEMANTICS.

A successful republish record MAY omit URL when target platform does not provide one.

Since: v0.1.0


Changelog

v0.1.0 (2026-02-11)

Initial draft

RFC-0004: external asset storage

Version: 0.2.3 | Status: normative | Phase: impl


1. Summary

[RFC-0004:C-SUMMARY] Summary (Informative)

RFC-0004 defines the normative contract for external asset storage in typub, extending the asset strategy system defined in RFC-0002.

This RFC specifies:

  • The External asset strategy variant for S3-compatible object storage.
  • Configuration requirements for external storage backends.
  • Asset upload tracking and caching semantics.
  • Integration with the publish pipeline’s Materialize stage.

This RFC does not define:

  • Specific storage provider implementations (AWS S3, Cloudflare R2, MinIO).
  • Image optimization or transformation logic.
  • Asset garbage collection strategies.

Scope: This specification applies when an adapter declares External as its asset strategy and external storage is configured.

Rationale: Platforms like HashNode and Dev.to strip or ignore embedded data URIs and cannot access local file paths. External object storage provides publicly accessible URLs that work universally across publishing platforms.

Since: v0.1.0


2. Specification

[RFC-0004:C-EXTERNAL-STRATEGY] External Asset Strategy (Normative)

The asset strategy system MUST support an External variant in addition to the existing Copy, Embed, and Upload variants.

When an adapter uses the External strategy:

  1. The system MUST upload local asset files to a configured external object storage service before the Publish stage.
  2. The system MUST replace local asset references in the finalized payload with publicly accessible URLs from the external storage.
  3. The system MUST NOT proceed to the Publish stage if any asset upload fails.

Strategy declaration: An adapter MAY declare External as a supported strategy in its ImageStrategyPolicy.

Unsupported strategy handling: If a user configures asset_strategy = "external" for an adapter that does not declare External in its ImageStrategyPolicy, the system MUST fail at configuration validation with an error that:

  • Names the adapter.
  • States that External is not supported.
  • Lists the strategies that the adapter does support.

The system MUST NOT silently fall back to another strategy.

Distinction from Upload: The External strategy MUST be treated as distinct from Upload. The Upload strategy indicates platform-native upload APIs (for example, Confluence attachments or Notion File Upload API), while External indicates third-party object storage independent of the target platform.

Rationale: Some platforms (HashNode, Dev.to) lack native asset upload APIs but accept external URLs. The External strategy provides a universal fallback for platforms that cannot host assets natively. Fail-fast validation prevents publishing with broken images.

Since: v0.1.0

[RFC-0004:C-STORAGE-CONFIG] Storage Configuration (Normative)

External storage configuration MUST support both global and per-platform scopes, with deterministic precedence rules.

Precedence ladder: When resolving a configuration field, the system MUST apply the following precedence order (highest to lowest):

  1. Platform-specific environment variable (for example, HASHNODE_S3_BUCKET).
  2. Platform-specific configuration file value.
  3. Global environment variable (for example, S3_BUCKET).
  4. Global configuration file value.

The first non-empty value in this order wins. The system MUST NOT merge partial values from different levels for a single field.

Global configuration: The system MUST support a global storage configuration that applies to all platforms using the External strategy.

Per-platform override: A platform MAY override specific storage configuration fields. Platform-specific values take precedence over global values at the same source level (environment or file).

Required configuration fields: The storage configuration MUST include at minimum:

  • Storage type identifier (for example, “s3” for S3-compatible storage).
  • Bucket or container name.
  • Public URL prefix for constructing accessible URLs.

Optional configuration fields:

  • Endpoint URL (for S3-compatible services). If absent, the system MUST use empty string for identifier computation.
  • Region (for example, “us-east-1”). If absent, the system MUST use empty string for identifier computation.

Credential fields: The storage configuration MUST support:

  • Access key identifier.
  • Secret access key.

Credential values MUST NOT be included in the storage configuration identifier used for cache invalidation.

Storage configuration identifier: The system MUST compute a deterministic storage configuration identifier by:

  1. Collecting these fields: type, endpoint, bucket, region, public_url_prefix.
  2. Normalizing each field:
    • type: lowercase, trimmed.
    • endpoint: if absent, use empty string. Otherwise: parse as URL; lowercase the scheme and host only; preserve path case; remove trailing slash from path; remove default port (:443 for https, :80 for http).
    • bucket: as-is (case-sensitive per S3 spec).
    • region: lowercase, trimmed. If absent, use empty string.
    • public_url_prefix: parse as URL; lowercase the scheme and host only; preserve path case; remove trailing slash from path.
  3. Concatenating as: {type}|{endpoint}|{bucket}|{region}|{public_url_prefix}.
  4. Computing SHA-256 hash of the concatenated string (UTF-8 encoded).
  5. Using the full 64 hex characters as the identifier.

Examples:

  • Input: type=s3, endpoint=https://S3.us-east-1.amazonaws.com/MyPath/, bucket=my-bucket, region=us-east-1, public_url_prefix=https://CDN.example.com/Assets/

  • Normalized: s3|https://s3.us-east-1.amazonaws.com/MyPath|my-bucket|us-east-1|https://cdn.example.com/Assets

  • Identifier: full 64-char SHA-256 hex

  • Input: type=s3, endpoint=(absent), bucket=my-bucket, region=(absent), public_url_prefix=https://cdn.example.com/

  • Normalized: s3||my-bucket||https://cdn.example.com

  • Identifier: full 64-char SHA-256 hex of that string

Validation: The system MUST validate storage configuration before attempting any upload. If required fields are missing or invalid, the system MUST fail with a descriptive error before the Materialize stage.

If an adapter uses the External strategy but no storage configuration is present, the system MUST fail with a configuration error rather than falling back to another strategy.

Rationale: A single precedence ladder eliminates ambiguity when environment variables and file values conflict at different scopes. Lowercasing only the host (not path) preserves case-sensitive path semantics per RFC 3986. Using the full SHA-256 hash eliminates collision risk for safety-critical cache keys. Explicit empty-string handling for absent optional fields ensures cross-implementation determinism. Excluding secrets from the identifier avoids cache invalidation on credential rotation.

Since: v0.1.0

[RFC-0004:C-UPLOAD-TRACKING] Asset Upload Tracking (Normative)

The system MUST persist asset upload records in local storage to enable caching and deduplication.

Two-index model: The system MUST maintain two logical indices:

  1. Content index: (storage_config_id, content_hash, extension)(remote_key, remote_url). This enables cross-path deduplication for files with identical content and extension.
  2. Path index: (local_path, storage_config_id)(content_hash, extension). This tracks the last-uploaded state for each local path.

Record fields: Each upload record MUST include at minimum:

  • Local asset path (relative to content directory).
  • Content hash of the uploaded file (SHA-256, lowercase hex, 64 characters).
  • Normalized extension (lowercase, alphanumeric only, or empty string).
  • Remote object key.
  • Public URL of the uploaded asset.
  • Upload timestamp.
  • Storage configuration identifier (as defined in RFC-0004:C-STORAGE-CONFIG).

Caching semantics: Before uploading an asset, the system MUST:

  1. Compute the content hash of the local file.
  2. Compute the normalized extension (see below).
  3. Check the content index for (storage_config_id, content_hash, extension):
    • If found, reuse the existing remote_url without uploading. Update the path index.
    • If not found, proceed to upload.
  4. After successful upload, atomically persist to both indices.

Per-asset atomicity: Each successful asset upload MUST be persisted atomically and independently. A failure uploading asset N MUST NOT roll back records for assets 1 through N-1.

This enables efficient retry: on retry, already-uploaded assets are skipped via the content index lookup.

Extension normalization: The extension MUST be normalized as follows:

  1. Extract the file extension from the original filename (characters after the last .).
  2. Convert to lowercase.
  3. Remove all characters not matching [a-z0-9].
  4. If the result is empty (no extension, or all characters removed), use empty string.

Examples:

  • image.PNGpng
  • photo.JPEGjpeg
  • data.tar.gzgz
  • README → `` (empty)
  • file.MP3!mp3
  • weird.??? → `` (empty, all invalid)

Object key format: The remote object key MUST be constructed as:

  • If extension is non-empty: {content_hash}.{extension}
  • If extension is empty: {content_hash}

Where:

  • content_hash is the lowercase hex-encoded SHA-256 hash of the file content (64 characters).
  • extension is the normalized extension (lowercase alphanumeric only).

Examples:

  • a1b2c3d4...64chars.png
  • e5f6a7b8...64chars.jpg
  • f9a8b7c6...64chars (no extension)

This format is purely content-addressable. Identical content with identical normalized extension MUST produce the same remote object key, regardless of original filename or path.

Overwrite semantics: Because object keys are derived from content hash, the key itself proves content identity. If the remote object already exists with the same key:

  1. The upload operation SHOULD succeed idempotently (overwrite or no-op).
  2. The system MUST treat AlreadyExists, PreconditionFailed, or equivalent responses as success.
  3. No remote content verification is required; the content-addressable key guarantees equivalence.

Rationale: Including extension in the content index key ensures that identical bytes with different file types (for example, a file served as both .bin and .dat) are stored separately, preserving MIME type inference from extension. The two-index model separates concerns: content deduplication uses hash+extension lookup, while path tracking enables efficient change detection. Per-asset persistence avoids redundant uploads on retry without requiring cleanup of partial remote state. Content-addressable keys eliminate the need for remote checksum verification.

Since: v0.1.0

[RFC-0004:C-PIPELINE-INTEGRATION] Pipeline Integration (Normative)

External asset upload MUST occur during the Materialize stage (Stage 7) as defined in RFC-0002:C-PIPELINE-STAGES.

Scope: This clause applies to both External and Upload asset strategies. Both strategies require deferred asset processing after specialization.

Semantic IR-centric design: Per RFC-0009:C-DOCUMENT-ROOT and RFC-0009:C-ASSET-REFERENCE, the publish pipeline maintains a semantic document IR root from Parse through Materialize. Content nodes MUST reference assets by stable asset identifiers. Materialize MUST resolve those identifiers via the document asset index and strategy context, not by replacing inline string placeholders.

Stage ordering:

  1. The Specialize stage (Stage 5) MUST produce payload/state containing:
    • Semantic IR with asset references by stable identifiers.
    • Pending asset set derived from referenced asset identifiers.
    • Effective asset strategy configuration.
  2. The Materialize stage (Stage 7) MUST:
    • Upload assets to storage (external S3 for External, platform-native for Upload) when required.
    • Resolve each referenced asset identifier to final delivery metadata (for example remote URL variants) in document asset index and/or specialization context.
  3. The Serialize stage (Stage 8) MUST convert resolved semantic IR to target format.
  4. The Publish stage (Stage 9) MUST receive payload with all required asset references resolved per target policy.

Shared infrastructure: Implementations SHOULD provide shared utilities for:

  1. Collecting referenced asset identifiers and building pending asset sets during Specialize.
  2. Resolving asset URLs/variants during Materialize.
  3. Serializing resolved semantic IR during Serialize.

Per-asset processing: For each referenced asset identifier, the system MUST:

  1. Resolve source metadata from the asset index.
  2. For External strategy: a. Compute content hash (SHA-256, lowercase hex, 64 characters). b. Compute normalized extension per RFC-0004:C-UPLOAD-TRACKING. c. Check content index for cache hit; upload if not found.
  3. For Upload strategy: a. Upload to platform-native storage API.
  4. On successful upload, persist tracking records as appropriate.
  5. Record resolved delivery data so serialization can emit target-consumable references.

The system MAY process assets in any order, including in parallel, provided all required resolutions are completed before Serialize.

Failure handling: If an asset upload fails during Materialize:

  1. The system MUST NOT proceed to Serialize.
  2. The system MUST surface a descriptive error identifying the failed asset (by identifier and source path when available).
  3. Successfully uploaded assets from the current batch MUST remain in tracking records.
  4. Successfully uploaded assets MAY remain in remote storage.

Idempotency: Retry of a failed publish operation MUST be safe and efficient. The system MUST:

  1. Skip upload for assets already present in tracking records (for External, content index hit).
  2. Handle remote AlreadyExists or overwrite responses as success per RFC-0004:C-UPLOAD-TRACKING.

The system is not required to maintain or resume from any particular ordering across retries.

Preview flow: For preview operations that do not require remote upload:

  1. Materialize MAY resolve asset identifiers to local preview URLs or project-relative references in preview sidecar/context.
  2. Preview-only URLs MUST NOT be committed into IR conformance-surface fields (including persisted Document.assets variants used for publish conformance).
  3. Serialize MAY emit preview-compatible references directly from preview sidecar/context or unresolved source metadata when target policy permits.

Rationale: Identifier/index-based processing preserves semantic IR purity, avoids string-collision classes of bugs, and keeps materialization deterministic and adapter-agnostic.

Since: v0.1.0

[RFC-0004:C-ERROR-SEMANTICS] Error Semantics (Normative)

The system MUST provide clear, actionable error messages for external storage failures.

Configuration errors: When storage configuration is missing or invalid, the error MUST:

  • Identify which configuration field is missing or invalid.
  • Indicate whether the error is at global or platform-specific scope.
  • Suggest corrective action (for example, “set S3_ACCESS_KEY_ID environment variable or add access_key_id to storage configuration”).

Upload errors: When an asset upload fails, the error MUST:

  • Identify the local asset path that failed.
  • Include the underlying storage service error message.
  • Indicate whether the failure is retryable (for example, network timeout vs. access denied).

Credential errors: When storage credentials are rejected, the error MUST NOT expose credential values. The error SHOULD indicate which credential source was used (environment variable or configuration file).

Rationale: Clear error messages reduce debugging time and prevent users from publishing with broken images.

Since: v0.1.0

[RFC-0004:C-URL-CONSTRUCTION] URL Construction (Normative)

The system MUST construct public URLs deterministically from the configured prefix and object key.

Prefix normalization: The system MUST normalize public_url_prefix before use:

  1. Parse as URL.
  2. Lowercase the scheme and host only; preserve path case.
  3. Remove any trailing / characters from the path.
  4. The normalized prefix is stored and used for all URL construction.

URL join algorithm: Given the normalized public_url_prefix and object_key, the public URL MUST be: {public_url_prefix}/{object_key}.

Object key construction: The object key MUST be constructed per RFC-0004:C-UPLOAD-TRACKING:

  • If normalized extension is non-empty: {content_hash}.{extension}
  • If normalized extension is empty: {content_hash}

Where:

  • content_hash: lowercase hex-encoded SHA-256 (64 characters).
  • extension: normalized extension per RFC-0004:C-UPLOAD-TRACKING (lowercase alphanumeric only).

The object key requires no percent-encoding because it contains only hex characters, dots, and lowercase alphanumerics.

Examples:

Original filenameNormalized extensionObject key
image.PNGpnga1b2...64chars.png
photo.JPEGjpega1b2...64chars.jpeg
README(empty)a1b2...64chars
data.tar.gzgza1b2...64chars.gz
file.???(empty)a1b2...64chars

Full URL example:

  • public_url_prefix (configured): https://CDN.example.com/Assets/
  • public_url_prefix (normalized): https://cdn.example.com/Assets
  • Content hash: a1b2c3d4e5f6... (64 chars)
  • Original filename: my image.png
  • Normalized extension: png
  • Object key: a1b2c3d4e5f6...64chars.png
  • Public URL: https://cdn.example.com/Assets/a1b2c3d4e5f6...64chars.png

Rationale: Normalizing only the scheme and host (not path) preserves case-sensitive path semantics per RFC 3986. Using only safe characters in object keys avoids encoding complexity and URL parsing issues. Explicit handling of empty extension ensures consistent behavior for extensionless files.

Since: v0.1.0

[RFC-0004:C-ASSET-LOCATION] Asset Location Constraints (Normative)

All assets referenced in content MUST be located within the project root as defined in RFC-0005:C-PROJECT-ROOT.

Validation: Before processing an asset, the system MUST verify that:

  1. The asset path resolves to a location within the project root.
  2. The asset file exists and is readable.

If an asset path resolves outside the project root, the system MUST fail with an error that:

  • Identifies the offending asset path.
  • States that assets must be within the project directory.
  • Suggests moving the asset into the project or using a symlink within the project tree.

Storage: Per RFC-0005:C-PROJECT-ROOT, asset paths stored in the status database MUST be relative to the project root. This enables project portability.

Rationale: Constraining assets to the project tree ensures:

  1. The entire project can be moved, synced, or version-controlled as a unit.
  2. Relative paths in the status database remain valid across machines.
  3. No accidental references to system files or files in unrelated directories.

Since: v0.2.1


Changelog

v0.2.3 (2026-02-21)

Preview flow boundary alignment

Added

  • Require preview URL resolution to stay in sidecar/context, not conformance IR fields

v0.2.2 (2026-02-21)

Semantic IR pipeline integration alignment

Added

  • Update C-PIPELINE-INTEGRATION to asset identifier and document asset-index resolution model

v0.2.1 (2026-02-13)

v0.2.0 (2026-02-12)

Amended C-PIPELINE-INTEGRATION to specify placeholder token mechanism for asset references, avoiding fragile regex-based URL replacement

Added

  • Add C-ASSET-LOCATION clause requiring assets within project root

v0.1.0 (2026-02-12)

Initial draft

RFC-0005: configuration hierarchy and post lifecycle

Version: 0.2.2 | Status: normative | Phase: impl


1. Summary

[RFC-0005:C-SUMMARY] Summary (Informative)

This RFC defines the configuration hierarchy and post lifecycle management for typub.

Scope:

  • Three-layer configuration system (global, per-content, platform-specific) with 5-level resolution
  • Resolution order for configuration values
  • Draft/publish lifecycle states and transitions
  • Platform capability declarations for lifecycle support
  • Status tracking schema for remote state

Out of scope:

  • Platform-specific API details (covered by adapter implementations)
  • Content rendering pipeline (covered by RFC-0002)

Rationale: As typub supports more platforms with varying draft/publish capabilities, a unified abstraction is needed to:

  1. Allow per-content override of global settings
  2. Declare platform capabilities in a type-safe manner
  3. Track remote lifecycle state for correct transition handling
  4. Provide consistent behavior across platforms with different API models

Since: v0.1.0


2. Specification

[RFC-0005:C-CONFIG-LAYERS] Configuration Layers (Normative)

typub MUST support a three-layer configuration system:

  1. Global configuration (typub.toml): Project-wide defaults that apply to all content items unless overridden.

  2. Per-content configuration (meta.toml): Content-specific settings that override global defaults for that content item.

  3. Platform-specific configuration: Both global and per-content configurations MAY include platform-specific sections.


Configuration File Locations

  • Global configuration: typub.toml at the project root.
  • Per-content configuration: meta.toml within each content directory.

The directory containing typub.toml defines the project root. See RFC-0005:C-PROJECT-ROOT for path resolution semantics.


Configuration Structure

Global configuration (typub.toml):

# Global defaults (layer 4)
published = true

# Global platform-specific (layer 3)
[platforms.hashnode]
published = false

[platforms.devto]
published = true

Per-content configuration (meta.toml):

title = "My Post"
created = "2026-01-15"

# Per-content default (layer 2)
published = false

# Per-content platform-specific (layer 1)
[platforms.hashnode]
published = true

Rationale: This layered approach allows users to set sensible defaults while retaining fine-grained control. The three-layer system (global, per-content, platform-specific) combined with nested platform sections creates a 5-level resolution chain that covers all practical use cases without excessive complexity.

Since: v0.1.0

[RFC-0005:C-RESOLUTION-ORDER] Resolution Order (Normative)

For any configuration key, the implementation MUST resolve values using a 5-level fallback chain:

  1. meta.toml[platforms.<platform_id>].<key> — per-content platform-specific
  2. meta.toml<key> — per-content default
  3. typub.toml[platforms.<platform_id>].<key> — global platform-specific
  4. typub.toml<key> — global default
  5. Adapter-defined default value

The first non-null value found MUST be used. If no value is found at any level, the adapter-defined default MUST apply.


Resolution Algorithm

#![allow(unused)]
fn main() {
fn resolve<T>(
    content_meta: &Meta,
    platform_id: &str,
    global_config: &Config,
    adapter_default: T,
    key: impl Fn(&PlatformConfig) -> Option<T>,
    global_key: impl Fn(&Config) -> Option<T>,
    content_key: impl Fn(&Meta) -> Option<T>,
) -> T {
    // Layer 1: per-content platform-specific
    content_meta.platforms.get(platform_id).and_then(&key)
        // Layer 2: per-content default
        .or_else(|| content_key(content_meta))
        // Layer 3: global platform-specific
        .or_else(|| global_config.platforms.get(platform_id).and_then(&key))
        // Layer 4: global default
        .or_else(|| global_key(global_config))
        // Layer 5: adapter default
        .unwrap_or(adapter_default)
}
}

Example: Resolving published for Hashnode

Given:

  • meta.toml: no [platforms.hashnode] section, published = false at top level
  • typub.toml: [platforms.hashnode].published = true, no global published
  • Adapter default: true

Resolution:

  1. Check meta.toml[platforms.hashnode].published → not found
  2. Check meta.toml.publishedfound: false → use this value

Result: published = false


Example: Resolving published for Dev.to

Given:

  • meta.toml: no relevant settings
  • typub.toml: [platforms.devto].published = false, published = true at top level
  • Adapter default: true

Resolution:

  1. Check meta.toml[platforms.devto].published → not found
  2. Check meta.toml.published → not found
  3. Check typub.toml[platforms.devto].publishedfound: false → use this value

Result: published = false


Rationale: The 5-level resolution order follows the principle of specificity: more specific configurations override more general ones. Per-content settings always take precedence over global settings, and platform-specific settings take precedence over general settings within the same scope.

Since: v0.1.0

[RFC-0005:C-DRAFT-SUPPORT] Draft Support Capability (Normative)

Each platform adapter MUST declare its draft support capability using the DraftSupport enum:

#![allow(unused)]
fn main() {
pub enum DraftSupport {
    /// Platform has no draft concept. Content is always published immediately.
    None,

    /// Same object with a status field that can be toggled.
    /// `reversible` indicates whether publish -> draft transition is supported.
    StatusField { reversible: bool },

    /// Draft and published content are separate objects with different IDs.
    /// Transition from draft to published requires a platform-specific mutation.
    SeparateObjects,
}
}

The AdapterCapability struct MUST include a draft_support field:

#![allow(unused)]
fn main() {
pub struct AdapterCapability {
    pub id: &'static str,
    pub name: &'static str,
    pub tags: CapabilitySupport,
    pub categories: CapabilitySupport,
    pub internal_links: CapabilitySupport,
    pub draft_support: DraftSupport,  // NEW
    pub notes: &'static str,
}
}

Rationale: Different platforms have fundamentally different models for draft content:

  • Some platforms (Dev.to, Ghost, WordPress, Confluence) use a status field on the same object
  • Some platforms (Hashnode) maintain separate draft and published objects
  • Some platforms (Notion) have no draft concept at all

Declaring this capability allows the pipeline to handle transitions correctly.

Since: v0.1.0

[RFC-0005:C-LIFECYCLE-TRANSITIONS] Lifecycle Transitions (Normative)

State transitions MUST follow platform-specific rules based on the platform type.

Platform Classification:

Platforms are classified into two categories:

  1. API-based platforms: Platforms with remote APIs (Hashnode, Dev.to, Ghost, WordPress, Confluence, Notion)
  2. Local output platforms: Platforms that produce local files or clipboard output (Astro, Xiaohongshu, Copypaste)

Local Output Platform Rules

For local output platforms, the lifecycle decision is trivial:

  • The published configuration MUST be ignored.
  • The implementation MUST always write/update the local output.
  • No draft/publish lifecycle tracking is applicable.
  • The implementation MAY store a status row for change detection (content_hash).

API-based Platform Rules

For API-based platforms, the implementation MUST apply the following decision logic.

Precondition check:

Before applying the decision table, the implementation MUST determine:

  1. has_remote_object: Whether platform_id is present in the status row
  2. remote_status: The stored lifecycle state (“draft” or “published”)
  3. desired_published: The resolved published configuration value per RFC-0005:C-RESOLUTION-ORDER
  4. draft_support: The platform’s declared DraftSupport capability

Data integrity guard:

If has_remote_object = true and remote_status is not one of {"draft", "published"}:

  • The implementation MUST fail with a diagnostic error.
  • The error message MUST indicate the invalid remote_status value and the affected content/platform.
  • The implementation MUST NOT proceed with any API operation.

This guard ensures corrupted or incomplete status data does not cause undefined behavior.

Decision Table (API-based platforms only):

has_remote_objectremote_statusdesired_publishedDraftSupportAction
false-trueAnyCreate as published
false-falseStatusField/SeparateObjectsCreate as draft
false-falseNoneCreate as published (ignore config)
true“published”trueAnyUpdate existing published content
true“published”falseStatusField { reversible: true }Update status to draft
true“published”falseStatusField { reversible: false }Warn: cannot unpublish; update content only
true“published”falseSeparateObjectsWarn: cannot unpublish; update content only
true“published”falseNoneUpdate published content (ignore config)
true“draft”trueStatusFieldUpdate status to published
true“draft”trueSeparateObjectsExecute publishDraft mutation
true“draft”trueNoneN/A (None never creates draft)
true“draft”falseStatusField/SeparateObjectsUpdate existing draft content
true“draft”falseNoneN/A (None never creates draft)

DraftSupport-specific Behaviors

DraftSupport::None (API-based):

  • The published configuration MUST be ignored.
  • Content MUST always be created/updated as published.
  • The implementation MUST store remote_status = "published" after each operation.

DraftSupport::StatusField:

  • If reversible is true: Both draft-to-publish and publish-to-draft transitions are permitted.
  • If reversible is false: Only draft-to-publish is permitted; publish-to-draft MUST log a warning and update content without changing status.

DraftSupport::SeparateObjects:

  • Draft-to-publish: MUST use platform-specific “publish draft” mutation.
  • After successful publish, the implementation MUST:
    • Update platform_id to the new published object ID.
    • Update remote_status to “published”.
    • Update url to the public URL.
  • Publish-to-draft: MUST NOT be supported; MUST log a warning and update content without changing status.

Rationale: Separating local output from API-based platforms eliminates data model confusion. The data integrity guard ensures corrupted status rows cause immediate, diagnosable failures rather than silent misbehavior.

Since: v0.1.0

[RFC-0005:C-STATUS-TRACKING] Status Tracking (Normative)

The status database tracks publish results and remote state.

Schema:

CREATE TABLE IF NOT EXISTS platform_status (
    slug TEXT NOT NULL,
    platform TEXT NOT NULL,
    published INTEGER NOT NULL,
    url TEXT,
    platform_id TEXT,
    published_at TEXT,
    content_hash TEXT,
    remote_status TEXT,
    PRIMARY KEY (slug, platform)
);

Row Persistence Policy

API-based platforms: The implementation MUST store a status row for each content item created or updated on an API-based platform. This includes both draft creation/updates and published content creation/updates.

Local output platforms: The implementation MAY store a status row for change detection purposes. This is OPTIONAL; implementations that do not need content_hash-based change detection are not required to store rows for local output platforms.


Platform-specific Field Semantics

FieldAPI-based platformsLocal output platforms
platform_idRemote object identifier (MUST be set)NULL (no remote object)
remote_status“draft” or “published” (MUST be set)NULL (lifecycle not applicable)
urlPublic URL or draft URLLocal output path or NULL
content_hashContent hash for change detectionContent hash for change detection

PlatformStatus struct

#![allow(unused)]
fn main() {
pub struct PlatformStatus {
    pub published: bool,
    pub last_publish: Option<PublishResult>,
    pub content_hash: Option<String>,
    /// Remote lifecycle state (API-based platforms only).
    /// - Some("draft"): Content exists as draft
    /// - Some("published"): Content exists as published
    /// - None: Local output platform (lifecycle not applicable)
    pub remote_status: Option<String>,
}
}

Persistence Requirements

API-based Platforms

After each successful operation (create or update, draft or published), the implementation MUST store:

  • platform_id: The remote object identifier
  • remote_status: “draft” or “published” matching the actual remote state
  • url: The public URL (for published) or platform-specific draft URL (for draft)
  • content_hash: Hash of the content at time of operation

For DraftSupport::SeparateObjects, after a draft-to-publish transition:

  • The implementation MUST update platform_id to the new published object ID.
  • The implementation MUST update remote_status to “published”.
  • The implementation MUST update url to the public URL.

Local Output Platforms

If the implementation chooses to store a status row:

  • platform_id: MUST be NULL
  • remote_status: MUST be NULL
  • url: MAY be the local output path
  • content_hash: Hash of the content for change detection

Rationale: API-based platforms require state tracking for correct lifecycle transitions. The row persistence policy explicitly covers both draft and published operations to avoid ambiguity. Local output platforms have no remote state; row storage is optional and only useful for content_hash-based change detection.

Since: v0.1.0

[RFC-0005:C-PLATFORM-CAPABILITIES] Platform Capabilities (Informative)

This clause documents the draft support capabilities of each platform adapter.

API-based platforms:

PlatformDraftSupportNotes
HashnodeSeparateObjectsDraft and Post are different objects with different IDs
Dev.toStatusField { reversible: true }Same endpoint, toggle published field
GhostStatusField { reversible: true }Same endpoint, toggle status field
WordPressStatusField { reversible: true }Same endpoint, toggle status field
ConfluenceStatusField { reversible: true }Uses ?status=draft and publish via blueprint endpoint
NotionNoneNo draft concept in API

Local output platforms:

PlatformDraftSupportNotes
AstroNoneLocal file output
XiaohongshuNoneLocal file output
CopypasteNoneClipboard output

Platform-specific notes:

  • Hashnode: Requires publishDraft mutation for draft-to-publish. Draft IDs become invalid after publishing.
  • Confluence: Drafts accessed via ?status=draft query parameter. Publish via PUT /wiki/rest/api/content/blueprint/instance/{draftId}.
  • Notion: Pages are created directly; no draft workflow in the API.

Since: v0.1.0

[RFC-0005:C-PROJECT-ROOT] Project Root Definition (Normative)

The project root is the directory containing typub.toml.

All relative paths stored in the status database (status.db) MUST be resolved relative to the project root.

The system MUST reject paths that resolve outside the project root with a descriptive error.

Path normalization for storage: When persisting paths to the status database:

  1. Absolute paths MUST be converted to relative paths (relative to project root).
  2. If the path cannot be expressed as relative to the project root (i.e., the asset is outside the project tree), the system MUST reject the operation with an error.
  3. Relative paths MUST use forward slashes (/) as path separators, regardless of operating system.
  4. Relative paths MUST NOT begin with ./ or contain .. components after normalization.

Path resolution on read: When reading paths from the status database:

  1. Relative paths MUST be resolved against the project root to obtain absolute paths for file operations.
  2. The resolved path MUST be validated to exist within the project root.

Rationale: Storing relative paths enables project portability — the entire project directory (including typub.toml, content, and .typub/status.db) can be moved, synced, or shared across machines without breaking path references.

Since: v0.2.0

[RFC-0005:C-RENDER-PREAMBLE] Typst Render Preamble Resolution and Merge (Normative)

typub MUST support an optional preamble configuration key for Typst render wrapper injection.

The preamble key MUST resolve using the standard 5-level resolution order defined in RFC-0005:C-RESOLUTION-ORDER:

  1. meta.toml[platforms.<platform_id>].preamble
  2. meta.toml.preamble
  3. typub.toml[platforms.<platform_id>].preamble
  4. typub.toml.preamble
  5. Adapter-defined default (render config default)

Resolution semantics MUST use nullable presence (Option<String> equivalent):

  • Missing value at a layer MUST be treated as None and fall through.
  • Present value at a layer MUST be treated as Some(value) and stop fallback.

During Render stage wrapper assembly, implementations MUST preserve adapter-specific preamble behavior and append resolved user preamble after adapter preamble when present:

final_preamble = adapter_preamble + "\n\n" + user_preamble

If no user preamble is resolved, adapter preamble MUST remain unchanged.

Rationale:

  • Preserves existing platform-specific preamble contracts (for example Confluence and Xiaohongshu).
  • Keeps user customization aligned with the unified configuration resolution model.

Since: v0.2.2


Changelog

v0.2.2 (2026-02-23)

Add Typst preamble resolution and merge semantics

Added

  • Define optional preamble key with 5-layer resolution and adapter-preserving append behavior in Render stage

v0.2.1 (2026-02-22)

Terminology consistency for global config file

Added

  • Replace stale config.toml references with typub.toml in C-RESOLUTION-ORDER

v0.2.0 (2026-02-13)

Add C-PROJECT-ROOT clause and rename config.toml to typub.toml

v0.1.0 (2026-02-12)

Initial draft

RFC-0006: adapter extension API

Version: 0.1.1 | Status: normative | Phase: impl


1. Summary

[RFC-0006:C-SUMMARY] Summary (Informative)

This RFC specifies an adapter extension API that enables third-party adapters to be registered and used by the typub CLI without modifying core source code.

Scope:

  • Defines the AdapterRegistrar API for runtime registration of adapter factories and capabilities.
  • Specifies capability lookup order when multiple sources provide capabilities.
  • Specifies backward compatibility constraints for existing built-in adapters and configurations.

Out of Scope:

  • Dynamic library plugin loading (deferred to a future RFC).
  • Data-driven adapters via manifest/template (deferred to a future RFC).

Rationale: The current adapter architecture requires editing multiple core files (Adapter enum, factory array, adapters.toml) to add new adapters. This creates friction for third-party integrations and makes the codebase harder to maintain. A registration-based API decouples adapter creation from core dispatch logic, enabling extension crates to inject adapters at startup.

References:

  • RFC-0002 — publish pipeline contract (defines adapter boundary and pipeline stages)
  • ADR-0002 — shared types in typub-core (capability enums)

Since: v0.1.0


2. Specification

[RFC-0006:C-REGISTRAR-API] Adapter Registrar API (Normative)

The AdapterRegistrar struct MUST provide the following methods:

  1. register_factory(platform_id: &str, factory: AdapterFactory) -> Result<()>

    • Registers a factory function that creates an adapter instance for the given platform ID.
    • The factory MUST be invoked with the current Config when AdapterRegistry::new() is called (eager construction).
    • If a factory is already registered for the same platform ID, the registration MUST return an error.
    • If platform_id is empty, the registration MUST return an error.
  2. register_capability(platform_id: &str, capability: AdapterCapability) -> Result<()>

    • Registers capability metadata for the given platform ID.
    • If a capability is already registered for the same platform ID, the registration MUST return an error.
    • If platform_id is empty, the registration MUST return an error.

The AdapterFactory type alias MUST be defined as:

#![allow(unused)]
fn main() {
pub type AdapterFactory = fn(&Config) -> Result<Box<dyn PlatformAdapter>>;
}

The AdapterCapability struct MUST use owned strings (String or Cow<'static, str>) for the id, name, short_code, and notes fields.

Construction semantics: Adapter construction is eager — all registered factories are invoked during AdapterRegistry::new(). If any factory fails, the entire registry construction MUST fail with an error that identifies the failing platform ID. Lazy construction is out of scope for this RFC.

Rationale: Eager construction matches existing behavior and ensures all configuration errors are surfaced at startup. Rejecting empty platform IDs prevents silent misregistration. Using Cow<'static, str> preserves zero-cost access for built-in capabilities while enabling owned strings for external adapters.

Since: v0.1.0

[RFC-0006:C-CAPABILITY-LOOKUP] Capability Lookup Order (Normative)

The adapter_capability(platform_id: &str) function in the main runtime path MUST resolve capabilities in the following order:

  1. Built-in API adapter capabilities (from BUILTIN_ADAPTERS static table, matched by id field)
  2. Copy-paste profile capabilities (from copypaste::find_profile() matched by platform ID)

The platform_id parameter is the platform identifier (for example, ghost, notion, wechat).

The first match MUST be returned. If no match is found in any source, the function MUST return None.

The lookup MUST NOT modify global state.

Implementations MAY provide additional extension capability sources in alternate bootstrap paths, but those paths MUST preserve deterministic precedence and MUST NOT change built-in capability semantics for unchanged platform IDs.

Rationale: This reflects the current runtime architecture, where first-party capabilities are resolved from static capability tables and copy-paste profiles. It keeps capability lookup deterministic while leaving room for future extension bootstrap modes.

Since: v0.1.0

[RFC-0006:C-EXTERNAL-VARIANT] External Adapter Variant (Normative)

The adapter registry MUST store adapter instances as trait objects (Box<dyn PlatformAdapter>) keyed by platform ID.

A dedicated Adapter enum with an External variant is NOT required by this RFC.

Implementations MAY introduce wrapper enums internally, but externally visible behavior MUST remain:

  1. Factory-created adapters are registered under stable platform IDs.
  2. All PlatformAdapter methods are invoked through the trait boundary.
  3. Adapter selection and execution semantics remain equivalent to direct trait-object dispatch.

Rationale: The current implementation already uses trait-object dispatch directly in the registry. Requiring an additional wrapper enum would add indirection without changing behavior.

Since: v0.1.0

[RFC-0006:C-BUILTIN-MIGRATION] Built-in Adapter Migration (Normative)

Built-in adapters MUST provide a stable creation and capability surface suitable for centralized registry construction.

At minimum, each first-party adapter crate MUST expose:

  • a factory entrypoint (create(config) -> Box<dyn PlatformAdapter> or equivalent), and
  • a canonical capability declaration (CAPABILITY or equivalent).

The main runtime registry MAY be constructed via:

  1. a centralized built-in factory table, or
  2. a registrar-based assembly path, provided externally observable behavior is equivalent.

Built-in adapter registration semantics MUST preserve:

  • API adapters are active only when their config section is present and enabled = true.
  • Copy-paste profiles are active unless explicitly disabled.
  • User-defined type = "manual" platforms are mapped to copy-paste adapters.

If multiple construction paths exist, tests MUST verify they are behaviorally equivalent for first-party adapters.

Rationale: This codifies current behavior while keeping room for future registrar-first consolidation without forcing an intermediate architecture step.

Since: v0.1.0

[RFC-0006:C-BACKWARD-COMPAT] Backward Compatibility (Normative)

The adapter extension API MUST NOT introduce breaking changes to:

  1. Configuration semantics: Existing typub.toml platform configurations MUST continue to work without modification.

  2. CLI behavior: Existing CLI commands (publish, build, preview) MUST produce semantically equivalent results for built-in platforms.

    Equivalence contract: Two outputs are semantically equivalent if they match on:

    • Exit code (success vs failure).
    • Published content body (ignoring whitespace normalization).
    • Asset URLs or references (same assets resolved).
    • Status file updates (same platform status recorded).

    Differences in log output, formatting, and non-functional metadata (timestamps, tool version) are permitted.

  3. PlatformAdapter trait: Required trait methods MUST NOT be removed or have their signatures changed in incompatible ways. New methods with default implementations MAY be added.

  4. Pipeline contract: The publish pipeline stages defined in RFC-0002 MUST NOT be modified.

  5. Extension isolation: The presence of an extension adapter MUST NOT alter the output for platforms that do not use that extension. An extension that registers platform ID “foo” MUST NOT affect the behavior of platform ID “bar”.

Error messages for unknown platforms MUST include:

  • The platform ID that was not found.
  • Guidance that registration may be missing (e.g., “not registered” or “may need to be registered”).

The exact wording of error messages is not normative.

Rationale: Users rely on stable configuration and CLI behavior. Breaking changes would force migration effort and risk regressions in production workflows. The equivalence contract provides a testable definition for parity verification.

Since: v0.1.0

[RFC-0006:C-CLI-BOOTSTRAP] CLI Bootstrap Hook (Normative)

The CLI MUST initialize adapter availability before command dispatch.

Bootstrap MUST:

  1. Construct the first-party adapter registry from configured platforms.
  2. Ensure platform capability lookup is available for selected targets.
  3. Pass the resulting registry into pipeline execution.

Implementations MAY support additional extension bootstrap hooks (for example registrar-based extension crates), but such hooks are OPTIONAL in this RFC revision.

When extensions are enabled, bootstrap MUST preserve first-party behavior for unchanged platform IDs.

The CLI MUST operate correctly when only first-party adapters are present.

Rationale: This reflects the current production path and keeps extension wiring as an optional, non-blocking capability.

Since: v0.1.0

[RFC-0006:C-REQUIRED-TESTS] Required Tests (Normative)

The implementation MUST include the following test categories:

Negative tests:

  1. Duplicate registration: register_factory and register_capability MUST return an error when the same platform ID is registered twice.
  2. Empty ID: register_factory and register_capability MUST return an error when platform_id is empty.
  3. Capability precedence: Tests MUST verify that runtime-registered capabilities take precedence over built-in capabilities for the same platform ID.

Parity tests: 4. For each built-in adapter, a test MUST verify that the registrar-based path produces semantically equivalent output to the legacy path (if the legacy path still exists during transition). Equivalence is defined per RFC-0006:C-BACKWARD-COMPAT.

Isolation tests: 5. Tests MUST verify that registering an extension adapter for platform “foo” does not alter the output of publish, build, or preview for platform “bar”.

Override warning tests: 6. Tests MUST verify that overriding a built-in capability emits a warning unless suppress_capability_override_warning = true is set.

Rationale: Explicit test requirements ensure the implementation is verifiable and reduce the risk of regressions or undefined behavior at edge cases.

Since: v0.1.0


Changelog

v0.1.1 (2026-02-22)

Align adapter extension clauses with current runtime architecture

Added

  • Replace External-enum and mandatory registrar bootstrap wording with trait-object registry semantics
  • Update capability lookup and built-in migration clauses to current implementation model

v0.1.0 (2026-02-14)

Initial draft

RFC-0007: adapter workspace subcrates

Version: 0.1.2 | Status: normative | Phase: impl


1. Summary

[RFC-0007:C-SUMMARY] Summary (Informative)

This RFC specifies the extraction of built-in adapters from the main typub crate into separate workspace subcrates.

Scope:

  • Defines the target workspace layout with adapter subcrates.
  • Specifies the shared adapter types crate (typub-adapters-core).
  • Specifies crate boundaries and dependency rules.
  • Specifies feature gates for optional adapter inclusion.

Out of Scope:

  • Third-party adapter crates (covered by RFC-0006).
  • Runtime plugin loading.
  • Migration path from current layout (covered by work items).

Rationale: The current monolithic layout has several drawbacks:

  1. All adapters share dependencies, even when only a subset is needed (e.g., Ghost requires JWT crates that others do not).
  2. Adding adapters requires modifying core files (Adapter enum, registry factory array).
  3. Compile times scale with total adapter count rather than enabled adapters.
  4. Testing adapters in isolation is difficult.

Extracting adapters into subcrates enables:

  • Feature-gated inclusion for smaller binaries and faster compiles.
  • Cleaner separation of concerns.
  • First-party and third-party adapters follow the same patterns.
  • Independent testing of each adapter.

References:

  • RFC-0006 — adapter extension API (defines AdapterRegistrar, External variant)
  • ADR-0002 — shared types in typub-core
  • ADR-0003 — extraction of typub-html subcrate

Since: v0.1.0


2. Specification

[RFC-0007:C-WORKSPACE-LAYOUT] Workspace Layout (Normative)

The workspace MUST include the following crates:

  • typub — main CLI binary
  • typub-core — capability types (existing)
  • typub-html — HTML parsing/serialization (existing)
  • typub-adapters-core — shared adapter abstractions (new)

Adapter crates MUST follow the naming convention typub-adapter-{platform_id}.

Adapter crates SHOULD be placed under crates/adapters/ to group related crates:

crates/
├── typub-core/
├── typub-html/
├── typub-adapters-core/
└── adapters/
    ├── typub-adapter-ghost/
    ├── typub-adapter-notion/
    └── ...

The workspace Cargo.toml MUST list all adapter crates as members.

New first-party adapters MAY be added without modifying this RFC, provided they follow the naming convention and placement guidelines above.

Rationale: Consistent naming enables tooling and documentation automation. The crates/adapters/ subdirectory groups related crates without polluting the top-level. Pattern-based rules avoid RFC churn when adding new adapters.

Since: v0.1.0

[RFC-0007:C-SHARED-TYPES] Shared Adapter Types Crate (Normative)

A new crate typub-adapters-core MUST be created to hold shared adapter abstractions.

This crate MUST export:

  • PlatformAdapter trait (moved from src/adapters/mod.rs)
  • AdapterPayload struct
  • PublishContext struct
  • RenderConfig struct
  • OutputFormat enum
  • AdapterCapability struct (with Cow<'static, str> fields per RFC-0006:C-REGISTRAR-API)
  • AdapterRegistrar struct and AdapterFactory type alias per RFC-0006:C-REGISTRAR-API

This crate MUST re-export from dependencies:

  • From typub-core: AssetStrategy, MathRendering, MathDelimiters, DraftSupport, CapabilitySupport, CapabilityGapBehavior
  • From semantic IR crate (typub-html at present): document IR surface required by adapters, including Document, Block, and Inline (or equivalent top-level semantic IR types defined by current RFC baseline)

This crate MUST NOT depend on:

  • Main typub crate (no circular dependency)
  • Any adapter implementation crate

Rationale: A shared types crate provides a stable interface that both the main crate and adapter crates depend on. This avoids circular dependencies and enables third-party adapters to compile against a minimal dependency set. Re-exporting semantic document IR types (instead of legacy HTML-structured node vectors) aligns adapter boundaries with semantic IR v2 and reduces duplicated migration logic.

Since: v0.1.0

[RFC-0007:C-ADAPTER-CRATE] Adapter Crate Structure (Normative)

Each adapter crate MUST:

  1. Implement the PlatformAdapter trait from typub-adapters-core.

  2. Expose a public register(registrar: &mut AdapterRegistrar) function that:

    • Calls registrar.register_factory(platform_id, factory) for the adapter factory.
    • Calls registrar.register_capability(platform_id, capability) for the adapter capability.
    • The platform_id MUST match the crate’s platform identifier (e.g., typub-adapter-ghost"ghost").
  3. Have a Cargo.toml with:

    • name = "typub-adapter-{platform_id}"
    • version matching the workspace version
    • Dependency on typub-adapters-core (not typub)
    • Adapter-specific dependencies (e.g., hmac, jwt for Ghost)
  4. Contain all adapter-specific logic:

    • API client (if applicable)
    • Semantic IR transformations
    • Type definitions

Each adapter crate SHOULD include integration tests that can run independently of the main CLI.

Adding new first-party adapters: Create a new crate following the above structure, add it to workspace members, add a feature gate in main crate, and add the feature-gated register() call to CLI bootstrap.

Rationale: Self-contained adapter crates enable independent testing, versioning, and optional inclusion. The register function provides a uniform entry point consistent with RFC-0006:C-CLI-BOOTSTRAP.

Since: v0.1.0

[RFC-0007:C-DEPENDENCY-RULES] Dependency Rules (Normative)

The following dependency rules MUST be enforced:

Allowed dependencies:

CrateMay depend on
typub-core(none in workspace)
typub-htmltypub-core
typub-adapters-coretypub-core, typub-html
typub-adapter-*typub-adapters-core
typub (main)all of the above

Adapter crates SHOULD depend only on typub-adapters-core, which re-exports needed types from typub-core and typub-html. Direct dependencies on typub-core or typub-html MAY be added only when adapter-specific needs require types not re-exported by typub-adapters-core.

Forbidden dependencies:

CrateMUST NOT depend on
typub-coreany other workspace crate
typub-htmltypub-adapters-core, typub-adapter-*, typub
typub-adapters-coretypub-adapter-*, typub
typub-adapter-*typub, other typub-adapter-* crates

Rationale: Strict layering prevents circular dependencies and ensures adapter crates can compile without the full CLI. No adapter should depend on another adapter to maintain isolation. Preferring typub-adapters-core avoids redundant dependency declarations.

Since: v0.1.0

[RFC-0007:C-FEATURE-GATES] Feature Gates (Normative)

Adapter feature gates in the main typub crate are OPTIONAL.

Implementations MAY use Cargo features to control first-party adapter inclusion, but this RFC does not require a specific feature matrix (for example adapter-all / adapter-{platform}) for conformance.

If feature gates are used, the implementation MUST ensure:

  1. Disabled adapters are not registered at runtime.
  2. Enabled adapters preserve the same runtime semantics as non-feature-gated builds.

If feature gates are not used, the default runtime path MUST still support selective adapter activation via platform configuration (enabled = true/false) and adapter requires_config semantics.

A build with no optional adapter features enabled (when such features exist) SHOULD still compile and provide non-adapter CLI commands (--help, --version). Commands requiring unavailable adapters MUST fail with a clear diagnostic.

Rationale: The current implementation relies primarily on runtime registration/activation semantics. Making feature gates optional avoids constraining implementation to one packaging strategy while preserving behavior guarantees.

Since: v0.1.0

[RFC-0007:C-RFC-0006-COMPAT] RFC-0006 Compatibility (Normative)

This RFC MUST be compatible with RFC-0006 (adapter extension API).

Specifically:

  1. The AdapterRegistrar defined in RFC-0006:C-REGISTRAR-API MUST be implemented in typub-adapters-core.
  2. Each adapter crate’s register() entrypoint (when provided) MUST use the same AdapterRegistrar API surface as extension adapters.
  3. Runtime adapter execution MUST be based on PlatformAdapter trait-object semantics, whether registry construction is registrar-based or factory-table-based.
  4. First-party and extension adapter wiring MUST preserve stable platform-ID behavior and MUST NOT alter semantics for unrelated platform IDs.

Rationale: Compatibility is defined by shared adapter contracts and runtime behavior, not by a single mandatory bootstrap implementation shape.

Since: v0.1.0


Changelog

v0.1.2 (2026-02-22)

Make adapter feature-gate model optional

Added

  • Relax C-FEATURE-GATES from mandatory adapter-all matrix to optional build strategy
  • Update RFC-0006 compatibility clause to behavior-based compatibility

v0.1.1 (2026-02-21)

Adapter wording alignment

Added

  • Replace remaining AST wording with semantic IR terminology in adapter crate responsibilities

v0.1.0 (2026-02-14)

Initial draft

Added

  • Resolve versioning contradiction; loosen workspace layout clause

RFC-0009: semantic document IR v2

Version: 0.2.3 | Status: normative | Phase: test


1. Summary

[RFC-0009:C-SUMMARY] Summary (Informative)

This RFC defines the normative semantic document IR v2 conformance surface for typub.

Scope:

  • Document-rooted semantic IR model (Document, Block, Inline, assets, footnotes, metadata).
  • Determinism and validation requirements for IR production and consumption.
  • Controlled downgrade semantics (Raw vs Unknown) and adapter policy declaration requirements.
  • Explicit semantic separation between math nodes and non-math SVG nodes.

Out of scope:

  • Parser implementation internals.
  • Adapter-specific target syntax details.
  • Asset backend implementation details (storage drivers, upload protocols).
  • Cross-version persisted IR compatibility (IR v2 is an in-process pipeline contract, not a long-term storage format).

Relationship to other RFCs:

  • The deprecated predecessor IR RFC MUST NOT be used as IR v2 conformance surface.
  • RFC-0002 and RFC-0004 define pipeline and materialization contracts that consume this IR.

Since: v0.2.0


2. Specification

[RFC-0009:C-SEMANTIC-BOUNDARY] Semantic-only IR boundary (Normative)

The IR MUST be semantic-first. It MUST NOT contain execution-state fields such as absolute filesystem paths, preview-only URLs, temporary upload placeholders, adapter/runtime handles, or other publish-run context.

Preview-only URL resolution data MUST be carried in non-conformance sidecar/context (for example materialization context), not in the IR conformance surface. If an implementation uses transient in-memory overlays during preview, those overlays MUST NOT participate in conformance serialization, equivalence checks, or persisted IR artifacts.

Derived cache payloads MAY appear only when all of the following are true:

  1. The payload is derivable from canonical semantic source (Document semantic content plus stable renderer config).
  2. The payload is explicitly optional and safely droppable without semantic loss.
  3. The payload MUST NOT be required for semantic validation, semantic equivalence, or adapter capability negotiation.

Pipeline state needed for provisioning/materialization MUST live outside the IR conformance surface in context/pipeline metadata.

Since: v0.2.0

[RFC-0009:C-ASSET-REFERENCE] Asset references are document-indexed (Normative)

Image and binary resources MUST be referenced by stable asset identifiers from content nodes. The document root MUST provide an asset index mapping asset identifiers to source metadata and resolved variants. Emitters MUST resolve resources through this index and MUST NOT infer resource identity from inline path strings.

Rendered binary artifacts produced by enrich/materialize pipelines (including math or SVG raster outputs) MUST also resolve through asset identifiers and Document.assets.

Document.assets resolved variants MUST contain only publish-conformance data that is reproducible across runs for identical inputs and configuration. Preview-only resolution data MUST remain in preview sidecar/context and MUST NOT be persisted or serialized as IR conformance-surface data.

Strategy-specific transport forms (for example data URI emission for Embed) are materialization/serialization concerns and MUST NOT redefine semantic identity in content nodes.

Since: v0.2.0

[RFC-0009:C-MATH-MODEL] Math must use explicit inline/block nodes (Normative)

Math MUST be represented by explicit inline and block math node variants. Implementations MUST NOT infer block math from structural heuristics such as a paragraph containing a single SVG fragment.

Math nodes MUST remain semantically distinct from non-math SVG nodes. Implementations MUST NOT classify generic SVG graphics as math for pipeline convenience.

Math nodes MAY carry canonical source. When canonical source is present, it MUST include explicit source kind (Typst or LaTeX) and source text.

Math nodes MUST carry at least one of:

  1. Canonical source.
  2. Rendered payload.

Rendered math payloads MAY be carried as optional derived cache/enrich data, but only if they satisfy RFC-0009:C-SEMANTIC-BOUNDARY (optional, droppable, and non-semantic for equivalence/validation).

Binary rendered payloads (for example PNG) MUST be represented through asset references resolved via Document.assets, not as inline strategy-specific transport encodings in semantic math nodes.

Since: v0.2.0

[RFC-0009:C-DOCUMENT-ROOT] Document root and ownership (Normative)

The IR root MUST be a document object that owns blocks, asset index, footnote definitions, and document metadata. Footnote references in inline content MUST resolve to document-scoped footnote definitions. Cross-node resources and references MUST be addressable from this single root.

Since: v0.2.0

[RFC-0009:C-ATTRS-LAYERING] Typed attrs and passthrough layering (Normative)

Attributes MUST be modeled as typed fields plus passthrough attributes. Semantically significant attributes used by pipeline logic or emitters MUST be represented by typed fields. Unknown or target-specific attributes MAY be preserved in a passthrough map. Passthrough maps in the conformance surface MUST use deterministic ordering.

Since: v0.2.0

[RFC-0009:C-RAW-UNKNOWN-POLICY] Raw and Unknown handling policy (Normative)

The IR MUST distinguish Raw nodes from Unknown nodes. Raw nodes carry literal source payload under explicit trust/origin metadata. Unknown nodes represent unmodeled structures without implicit execution.

Each adapter MUST provide a machine-readable Raw/Unknown policy declaration in its capability declaration surface.

The declared policy MUST include, at minimum, one action for each category (Raw, Unknown) from this closed set: pass, sanitize, drop, error.

Validation timing requirements:

  1. Adapter registration/loading MUST fail if the declaration is missing or contains invalid actions.
  2. Publish-time adapter selection MUST fail if no valid declaration is available for the selected adapter.
  3. CI/governance checks MUST verify declaration presence for first-party adapters.

Adapter execution MUST apply the declared policy consistently.

Since: v0.2.0

[RFC-0009:C-DETERMINISM] Deterministic normalization requirements (Normative)

IR production and serialization MUST be deterministic for identical inputs and configuration.

Implementations MUST canonicalize ordered collections where semantic order is not input-dependent, including passthrough attribute maps and style sets.

For IR v2, a style set is the value carried by inline styled content to represent one or more text styles. Canonicalization requirements for style sets are:

  1. Deduplicate styles by style identity.
  2. Serialize styles in this canonical order: Bold, Italic, Strikethrough, Underline, Mark, Superscript, Subscript, Kbd.
  3. Preserve equivalence such that semantically identical style sets produce byte-identical conformance serialization.

Non-deterministic map iteration in conformance output is prohibited.

Since: v0.2.0

[RFC-0009:C-VALIDATION] IR validation invariants (Normative)

Implementations MUST validate IR invariants before adapter specialization. Validation failures MUST be surfaced as explicit errors and MUST NOT silently degrade core semantics.

At minimum this validation MUST enforce:

  1. Heading levels are within 1..=6.
  2. All asset references resolve to existing entries in Document.assets.
  3. List nesting is structurally valid for the unified list model.
  4. Math payload validity.
  5. SVG payload validity for explicit SVG nodes.

For clause (4), the minimum interoperable validity rule is:

  • A math node MUST contain at least one of canonical source or rendered payload.
  • If canonical source is present, source kind MUST be one of the RFC-defined kinds (Typst or LaTeX).
  • If canonical source is present, source text MUST be non-empty after trimming ASCII whitespace.

For clause (5), the minimum interoperable validity rule is:

  • An explicit SVG node MUST contain at least one of canonical SVG source or rendered payload.

Implementations MAY add stricter parser-based validation (for example full Typst/LaTeX parse checks), but such strictness MUST be explicitly documented and MUST produce deterministic outcomes for fixed parser version and configuration.

Since: v0.2.0

[RFC-0009:C-IR-TYPE-SURFACE] IR type surface and conformance (Normative)

The conformance surface of IR v2 MUST be explicitly defined in this RFC and MUST NOT depend on external, non-RFC schema documents.

At minimum, a conforming implementation MUST provide the following semantic structure:

  1. Document root

    • A single Document root object.
    • Document MUST own: blocks, footnotes, assets, and meta.
    • blocks is ordered and preserves source reading order.
    • footnotes and assets MUST be key-addressable maps with deterministic ordering.
  2. Block and Inline model

    • Block and Inline MUST be closed tagged unions (or equivalent sum types) with explicit variant tags.
    • Block MUST include variants covering at least: heading, paragraph, quote, code block, divider, list, definition list, table, figure, admonition, details, math block, SVG block, unknown block, raw block.
    • Inline MUST include variants covering at least: text, code, soft break, hard break, styled span, link, image, footnote ref, math inline, SVG inline, unknown inline, raw inline.
  3. Attribute layering

    • Conforming attrs MUST be split into typed fields and passthrough map.
    • Passthrough maps MUST use deterministic key ordering.
  4. Style set surface

    • Styled inline content MUST carry a style set.
    • A style set MUST be represented as a collection of TextStyle values.
    • Conformance serialization MUST use the canonical ordering rule defined in RFC-0009:C-DETERMINISM.
  5. Assets and references

    • Image/binary references inside content MUST use stable asset identifiers.
    • Asset metadata and resolved variants MUST be stored in Document.assets, not embedded as ad-hoc runtime fields in content nodes.
    • Document.assets resolved variants are limited to reproducible publish-conformance data; preview-only resolution data MUST remain outside the conformance surface.
  6. Renderable payload model

    • Math nodes and SVG nodes MUST both use an explicit renderable payload model.
    • The renderable payload model MUST support canonical source (optional) and rendered payloads (optional), but at least one MUST be present per node.
    • Binary rendered artifacts MUST be representable by asset identifier reference so Embed/Upload/External can be resolved in materialize/serialize without changing semantic node identity.
  7. Math and SVG semantics

    • Math MUST be represented with explicit inline/block math nodes.
    • Non-math SVG content MUST be represented with explicit inline/block SVG nodes.
    • Implementations MUST NOT collapse generic SVG nodes into math nodes.
  8. List semantics

    • List structure MUST be represented as a unified list model with explicit list kind and recursive list items.
    • Per-item marker semantics (including task checked state) MUST be representable without relying on serializer heuristics.
  9. Controlled downgrade

    • Unknown and Raw nodes MUST be distinct variants with explicit handling semantics.
    • Raw variants MUST carry trust/origin metadata sufficient for policy enforcement.
  10. Legacy exclusion

  • Legacy pre-v2 HtmlElement/InlineFragment/ImageMarker shapes MUST NOT be used as the conformance surface for IR v2.

Implementations MAY add internal helper fields or transient representations, but externally visible IR conformance (serialization contracts, adapter interfaces, and validation input/output) MUST satisfy this clause.

Since: v0.2.0


Changelog

v0.2.3 (2026-02-21)

Clarify shared SVG and math render-payload semantics

Added

  • Separate explicit SVG nodes from math nodes in conformance type surface
  • Allow math canonical source to be optional while requiring source-or-rendered presence
  • Require binary rendered artifacts to resolve through Document.assets for Embed/Upload/External workflows

v0.2.2 (2026-02-21)

Clarify Document.assets resolved-variant boundary

Added

  • Constrain Document.assets resolved variants to reproducible publish-conformance data
  • Require preview-only resolution data to remain sidecar/context and out of conformance IR

v0.2.1 (2026-02-21)

Close audit gaps on conformance interoperability

Added

  • Clarify preview-only URL boundary between conformance IR and preview sidecar context
  • Pin heading level validation range to 1..=6
  • Define interoperable canonical style-set ordering
  • Strengthen first-party Raw/Unknown governance checks to MUST

v0.2.0 (2026-02-21)

Clarify conformance and validation for audit closure

Added

  • Add C-SUMMARY scope/out-of-scope
  • Define IR type surface in C-IR-TYPE-SURFACE
  • Clarify semantic boundary for optional derived caches
  • Define minimum interoperable math validation rule
  • Make Raw/Unknown policy declaration machine-readable and enforceable
  • Define style set structure and canonicalization requirements

v0.1.0 (2026-02-21)

Initial draft

ADR Index

This section records major architecture and implementation decisions.

Suggested Reading Order

ADR-0001: code block syntax highlighting preservation

Status: accepted | Date: 2026-02-13

References: RFC-0002

Context

Copy-paste HTML platforms (WeChat, Zhihu, etc.) require syntax highlighting to be preserved in code blocks, but the current parser discards it.

Problem Statement

In src/html_utils/util.rs::extract_code_text (lines 27-38), we intentionally strip all HTML tags (including <span> tags with highlighting styles) to produce plain text for the CodeBlock::code field. This was designed for Confluence, which uses CDATA sections requiring pure text.

However, copy-paste HTML platforms like WeChat preserve and display highlighted code when pasted from the preview. Currently, users see monochrome code after pasting, losing syntax highlighting entirely.

Constraints

  1. RFC-0002:C-PIPELINE-STAGES defines CodeBlock { code, language, attrs } — changing this structure affects all adapters.
  2. Confluence needs plain text (CDATA cannot contain HTML entities).
  3. Copy-paste HTML platforms need styled HTML for best UX.
  4. Markdown platforms need plain text + language tag (not HTML).

Options Considered

  1. Store both plain and highlighted HTML in CodeBlock (dual storage).
  2. Add a capability flag to control parser behavior per-adapter.
  3. Re-highlight at serialization time using a Rust syntax highlighter.

Decision

We will store both plain text and highlighted HTML in CodeBlock, and add a code_highlight capability to let each adapter/profile declare which representation it needs.

Data Structure Change

#![allow(unused)]
fn main() {
// Before
CodeBlock { code: String, language: String, attrs: Attrs }

// After
CodeBlock {
    code: String,                 // Plain text (always available)
    highlighted: Option<String>,  // HTML with syntax highlighting (if rendered)
    language: String,
    attrs: Attrs
}
}

Capability Addition

Add code_highlight field to adapters.toml and profiles.toml:

# adapters.toml
[[adapter]]
id = "ghost"
code_highlight = true   # Use highlighted HTML in code blocks
...

[[adapter]]
id = "confluence"
code_highlight = false  # Use plain text (CDATA requirement)
...

# profiles.toml
[[profile]]
id = "wechat"
format = "html"
code_highlight = true   # Preserve syntax highlighting
...

[[profile]]
id = "csdn"
format = "markdown"
code_highlight = false  # Markdown uses plain code
...

Why This Approach

  1. Dual storage — both representations available; no information lost.
  2. Explicit capability — each platform declares its need; no guessing.
  3. Zero behavioral change by defaultcode_highlight = false preserves current behavior.
  4. Single source of truth — both representations come from the same render pass.

Implementation Notes

  1. Parser change: parse_code_block stores raw inner_html in highlighted field.
  2. Capability check: Serializers check adapter.code_highlight() to decide which field to use.
  3. Confluence: code_highlight = false → uses code for CDATA.
  4. Ghost/WordPress: code_highlight = true → uses highlighted for styled HTML.
  5. Markdown profiles: code_highlight = false by default (Markdown doesn’t support inline HTML).

Consequences

Positive

  • Copy-paste HTML platforms preserve syntax highlighting — better UX for WeChat, Zhihu, etc.
  • Language detection works in editor paste — data-lang attribute preserved in highlighted HTML.
  • Each platform explicitly declares its code rendering preference — no implicit behavior.
  • API adapters like Ghost/WordPress can also benefit from syntax highlighting.
  • Future-proof — new adapters declare their capability; no code change needed.

Negative

  • Slight memory overhead — highlighted field duplicates content. Mitigation: code blocks are typically small.
  • Schema change in HtmlElement::CodeBlock — all pattern matches must be updated. Mitigation: compiler enforces exhaustive matching.
  • New capability field in two TOML files — requires updating adapters.toml and profiles.toml. Mitigation: defaults to false for backward compatibility.

Neutral

  • highlighted field is None for code blocks constructed programmatically. Serializers fall back to code.
  • code_highlight = false is the default for all platforms initially; existing behavior unchanged until explicitly enabled.

Alternatives Considered

Capability flag only (no dual storage): Parser checks code_highlight flag at runtime and conditionally strips HTML. Rejected: loses information; can’t switch representation later without re-parsing.

Re-highlight at serialization: Use syntect or tree-sitter at serialize time to re-add highlighting. Rejected: adds heavy dependency; may produce different colors than Typst original; runtime cost.

Dual storage only (no capability): Always store both, serializer auto-detects based on format field. Rejected: API adapters have no format field; need explicit control per-platform.

ADR-0002: extract shared types into typub-core subcrate

Status: accepted | Date: 2026-02-13

References: RFC-0002, RFC-0005

Context

build.rs and the main typub crate both need capability types (MathRendering, AssetStrategy, etc.) but cannot share them because build.rs runs at build time and cannot depend on the crate being compiled.

Problem Statement

build.rs deserializes TOML capability fields as String and manually maps them to Rust enum expression strings (e.g., math_rendering_expr("svg")"MathRendering::Svg"). This is fragile — typos in TOML surface as panics in match arms rather than serde errors, and there is no compile-time guarantee that build-time and runtime enum definitions stay in sync. Theme IDs are also bare String/&str with no type distinction from other strings.

Constraints

  • RFC-0002 defines the pipeline stages including capability resolution
  • RFC-0005 defines the configuration hierarchy including theme resolution
  • build.rs generates static &'static data — the shared types must be lightweight enough for both build-time deserialization and runtime use
  • Existing use crate::adapters::{MathRendering, ...} import paths should remain valid via re-exports

Options Considered

  1. Extract enums to a typub-core subcrate (chosen)
  2. Keep string-based build.rs with better validation
  3. Use proc-macros to generate enums from TOML

Decision

We will extract capability enums and ThemeId newtype into a crates/typub-core/ subcrate because:

  1. Single source of truth: Enum variants are defined once and shared by both build.rs (build-dependency) and the main crate (normal dependency).
  2. Serde validation at build time: TOML strings are deserialized into typed enums via serde, catching typos immediately with clear error messages instead of panics in match arms.
  3. Type safety for theme IDs: ThemeId(String) newtype prevents accidental confusion between theme IDs and other strings.

Implementation Notes

  • Each enum implements code_expr(&self) -> &'static str for build-time code generation
  • CapabilitySupport and DraftSupport need custom serde deserializers due to flattened TOML representations
  • The main crate re-exports all typub-core types from their original module paths to avoid import churn
  • ThemeId implements Deref<Target = str> for ergonomic use at call sites

Consequences

Positive

  • Typos in adapters.toml produce clear serde deserialization errors at build time
  • Adding a new enum variant only requires editing typub-core; build.rs string-mapping functions are eliminated
  • ThemeId prevents passing arbitrary strings where theme IDs are expected
  • Workspace structure enables future subcrate extraction if needed

Negative

  • Additional crate adds marginal compilation overhead (mitigation: typub-core is tiny with minimal dependencies — only serde)
  • Workspace conversion changes Cargo.lock path behavior (mitigation: Cargo handles this transparently)

Neutral

  • Existing import paths (use crate::adapters::MathRendering) continue to work via re-exports — no downstream churn

Alternatives Considered

Keep string-based build.rs with better validation: Add unit tests for TOML-to-enum mapping. Rejected: Still duplicates enum knowledge between build.rs and runtime code; adding a variant requires changes in two places.

Use proc-macros to generate enums from TOML: Derive enums at compile time from adapters.toml. Rejected: Proc-macro crates add significant complexity and compile time for marginal benefit over a simple shared crate.

ADR-0003: extract html_utils into typub-html subcrate

Status: accepted | Date: 2026-02-13

References: ADR-0002

Context

The html_utils module is the most widely used internal module in typub, with 11+ modules depending on it for HTML AST types (HtmlElement, InlineFragment), serialization, parsing, and transformation.

Problem Statement

Currently html_utils is embedded in the main crate, making it impossible to use independently. As the crate grows, extracting stable, well-bounded modules improves:

  • Compilation times (parallel crate compilation)
  • Code organization (clear boundaries)
  • Potential reuse in other tools

Constraints

  • ADR-0002 established the workspace pattern with typub-core
  • Existing use crate::html_utils::* imports must continue to work
  • Module has zero internal dependencies (ideal extraction candidate)

Options Considered

  1. Extract to typub-html subcrate with workspace dependencies (chosen)
  2. Keep embedded in main crate

Decision

Extract src/html_utils/ into crates/typub-html/ subcrate:

  1. Move all files from src/html_utils/ to crates/typub-html/src/
  2. Create crates/typub-html/Cargo.toml using workspace dependencies
  3. Add pub use typub_html as html_utils; in main crate for import compatibility
  4. Unify all shared dependencies to [workspace.dependencies] in root Cargo.toml
  5. Update all subcrates to use { workspace = true } for shared deps

Consequences

Positive

  • Follows pattern established by ADR-0002
  • Unified dependency versions across workspace
  • Improved compile times through parallelism
  • Clear module boundaries

Negative

  • Additional subcrate to maintain
  • Must keep re-export for backward compatibility

ADR-0004: Logging System Architecture

Status: accepted | Date: 2026-02-14

References: RFC-0007, ADR-0002

Context

typub-ui currently provides both CLI output utilities (progress bars, spinners, styled messages) and logging functions (debug, info, warn, error). This coupling creates problematic dependencies: typub-storage must depend on the entire typub-ui crate just to use logging during upload operations, violating separation of concerns.

Problem Statement

  1. Coupling violation: typub-storage (a service layer) depends on typub-ui (a presentation layer) for logging
  2. No standard logging: Current implementation uses custom eprintln!-based output, incompatible with the Rust logging ecosystem
  3. Limited debuggability: No structured logging, no log level filtering, no span tracing for complex operations
  4. Library use friction: If typub is used as a library, callers cannot integrate with their own logging infrastructure

Constraints

  • RFC-0007 dependency rules require clean layering
  • Must maintain backward-compatible CLI output (colors, icons, formatting)
  • Async operations (S3 uploads) need progress reporting

Options Considered

  1. Split into typub-log (simple) + typub-ui with current eprintln! approach
  2. Introduce tracing crate as logging foundation with custom CLI formatter (recommended)
  3. Use log crate facade with env_logger
  4. Keep current monolithic typub-ui

Decision

We will introduce tracing as the logging foundation and create a new typub-log crate that:

  1. Provides tracing-based logging with re-exported macros (debug!, info!, warn!, error!)
  2. Implements a custom tracing-subscriber layer for CLI-formatted output (icons, colors)
  3. Exposes a ProgressReporter trait for decoupling storage from UI

Implementation Phases

Phase 1: Create typub-log crate

  • Add tracing and tracing-subscriber dependencies
  • Implement CliLayer for custom CLI formatting
  • Re-export tracing macros for crate-wide use
  • typub-log has zero internal dependencies (Layer 0)

Phase 2: Define ProgressReporter trait in typub-log

  • Create trait in typub-log (semantically related to logging/reporting)
  • Provide () (null reporter) default implementation
  • FnReporter wrapper for simple closures

Phase 3: Refactor typub-storage

  • Replace typub-ui dependency with typub-log
  • Accept &dyn ProgressReporter in upload functions
  • Use tracing macros for structured logging

Phase 4: Update typub-ui

  • Implement ProgressReporter trait
  • Re-export typub-log for convenience
  • Keep indicatif-based progress bars and spinners

New Dependency Graph

typub-log (Layer 0)
├── tracing
├── tracing-subscriber
├── ProgressReporter trait
└── (no internal deps)

typub-core (Layer 0)
└── (no internal deps)

typub-storage (Layer 2)
├── typub-core
├── typub-config
└── typub-log (NOT typub-ui)

typub-ui (Layer 2)
├── typub-log (re-export)
├── indicatif
├── owo-colors
└── implements ProgressReporter

RFC-0007 Amendment Required

This ADR requires amending RFC-0007 to add typub-log as a Layer 0 crate in the dependency table. The amendment should specify:

  • typub-log is Layer 0 (no internal dependencies)
  • All crates MAY depend on typub-log for logging
  • typub-storage MUST NOT depend on typub-ui (use typub-log instead)

Migration Notes

Macro syntax change: The tracing macros use structured fields instead of format strings:

#![allow(unused)]
fn main() {
// Before (typub-ui)
ui::debug(&format!("Processing file {}", path.display()));

// After (tracing)
tracing::debug!(file = %path.display(), "Processing file");
}

The % sigil formats the value with Display; ? uses Debug. This change enables structured logging but requires updating call sites.

RUST_LOG integration: The custom CliLayer works with standard EnvFilter:

#![allow(unused)]
fn main() {
// Enable debug logs for storage operations
RUST_LOG=typub_storage=debug typub publish
}

Consequences

Positive

  • Clean layering: typub-storage no longer depends on typub-ui, respecting RFC-0007 dependency rules
  • Ecosystem compatibility: Standard tracing allows RUST_LOG filtering (RUST_LOG=typub::storage=debug)
  • Structured logging: Enables #[instrument] for automatic span creation and field extraction
  • Library-friendly: Callers can use their own tracing subscriber
  • Better debugging: Spans track async operation chains (e.g., upload → transform → publish)
  • Testable: ProgressReporter trait allows mock injection for unit tests

Negative

  • Additional dependency: tracing + tracing-subscriber add ~50KB to compile time (mitigation: these are widely used, likely already in dependency tree via other crates)
  • Learning curve: Team needs familiarity with tracing macros and spans (mitigation: provide examples in crate docs)
  • Migration effort: Existing ui::debug() calls need conversion to tracing::debug! with syntax changes (mitigation: structured fields enable better filtering and analysis)
  • Two logging systems temporarily: During migration, old and new systems coexist (mitigation: Phase-by-phase migration)

Neutral

  • typub-ui becomes smaller, focused on indicatif-based progress indicators
  • CLI output appearance remains unchanged (custom formatter preserves icons/colors)
  • May enable future features: JSON logging for CI, trace export for debugging

Alternatives Considered

Simple split: Create typub-log with current eprintln approach. Rejected: No ecosystem compatibility, no structured logging, doesn’t solve library-use friction.

log crate + env_logger: Use log facade with simple env_logger backend. Rejected: No span support, less powerful than tracing for async debugging, env_logger lacks CLI-friendly formatting.

Keep monolithic typub-ui: Do nothing, accept the coupling. Rejected: Violates RFC-0007 layering, blocks library use, no path to structured logging.

ADR-0005: centralize main-crate resolution helpers

Status: accepted | Date: 2026-02-15

References: RFC-0004, RFC-0005

Context

Main crate resolution logic is duplicated across modules: internal_link_target is resolved in both src/resolved_config.rs and src/internal_links.rs, and asset strategy resolution appears in both src/resolved_config.rs and src/adapters/mod.rs. This duplication risks drift and inconsistent precedence handling.

Problem Statement

We need a single source of truth for resolution rules (per RFC-0005 and RFC-0004) so config precedence and validation remain consistent across pipeline stages and adapter helpers.

Constraints

  • Resolution ordering is governed by RFC-0005.
  • Asset strategy validation and supported strategies are governed by RFC-0004.
  • Changes must be internal refactors without behavior changes.

Options Considered

  1. Keep duplicate helpers in place and synchronize manually.
  2. Centralize resolution helpers and have all modules call the shared implementation.
  3. Move all resolution into adapter subcrates.

Decision

We will centralize main-crate resolution helpers by making ResolvedConfig the canonical path for internal_link_target resolution and by consolidating asset strategy resolution to a single helper used by both pipeline and adapter helpers.

Reasons:

  1. Consistency: A single implementation reduces drift and enforces RFC-0005 precedence everywhere.
  2. Maintainability: Refactors and future config changes touch one location.
  3. Auditability: Behavior is easier to reason about and verify.

Implementation Notes

  • Replace internal_links::resolve_internal_link_target with a call into ResolvedConfig (or a shared helper within resolved_config).
  • Replace adapters::resolve_platform_asset_strategy* with a thin wrapper around the centralized helper.
  • No external behavior changes; only reuse shared logic.

Consequences

Positive

  • One canonical resolution path for internal_link_target and asset strategies.
  • Lower drift risk across pipeline/adapters layers.
  • Simpler tests and clearer audit trail.

Negative

  • Short-term refactor churn in core modules (mitigation: keep changes small and add targeted tests).
  • Some helper APIs may change or be removed (mitigation: preserve wrappers temporarily if needed).

Neutral

  • No behavior changes expected; only internal wiring adjustments.

Alternatives Considered

Keep duplicate helpers: Rejected because it invites drift and inconsistent precedence.

Move all resolution into adapter subcrates: Rejected because main crate still needs resolution for pipeline and config; increases coupling.

ADR-0006: extract typub-engine and make TUI default

Status: accepted | Date: 2026-02-15

References: RFC-0004, ADR-0005

Context

The main crate currently owns pipeline orchestration, rendering, asset handling, internal link resolution, project root logic, and UI/TUI modules. This makes the CLI crate both the composition root and the engine, increasing coupling and making reuse difficult.

Problem Statement

We need a clear separation between the CLI/app wiring and the reusable build engine, and we want TUI to be a default capability without a feature gate.

Constraints

  • Existing subcrates already capture core types (typub-core), config (typub-config), HTML utilities (typub-html), storage (typub-storage), and adapter interfaces (typub-adapters-core).
  • The extraction should avoid over-fragmentation: prefer a single engine crate over many tiny crates.
  • No behavior changes beyond enabling TUI by default.

Options Considered

  1. Keep all engine logic in the main crate and only remove the TUI feature gate.
  2. Extract a single typub-engine crate and move TUI/i18n into typub-ui (preferred).
  3. Split engine into many subcrates (pipeline, renderer, assets, links, etc.).

Decision

We will extract a single typub-engine crate that owns the build pipeline (pipeline, renderer, assets, internal links, resolved config, project, cache, sorting, taxonomy) and move TUI and i18n into typub-ui, while making TUI enabled by default (no feature gate).

Reasons:

  1. Separation of concerns: The main crate becomes a thin CLI composition root.
  2. Reusability: The engine can be reused by watcher mode or future front-ends.
  3. Low fragmentation: One engine crate avoids excessive crate sprawl.

Implementation Notes

  • typub-engine depends on existing core/config/html/storage/adapters-core crates.
  • The main crate depends on typub-engine and typub-ui and wires CLI commands.
  • Remove cfg(feature = "tui") gates and make TUI paths always available.

Consequences

Positive

  • Clear separation between CLI wiring and build engine.
  • Easier reuse for watcher mode and future front-ends.
  • Reduced coupling in main crate.

Negative

  • Short-term migration cost and import churn (mitigation: staged move and strict builds).
  • Potential temporary duplication of re-exports during transition (mitigation: keep API surface minimal).

Neutral

  • TUI becomes always built, which slightly increases binary dependencies.

Alternatives Considered

Keep engine in main: Rejected because it preserves high coupling and blocks reuse.

Split into many small crates: Rejected because of fragmentation and cycle risk.

ADR-0007: refactor crate layout for reuse and metadata normalization

Status: accepted | Date: 2026-02-15

References: ADR-0003

Context

Context

We are restructuring the typub workspace to maximize reuse of internal crates outside the project while reducing duplication and dependency tangles. Current functionality such as project root/path utilities, internal link rewriting, asset AST processing, and taxonomy handling are split across typub-engine, typub-storage, and typub-adapters-core, causing overlap and limiting reuse.

Problem Statement

We need a clearer crate layout and ownership boundaries so that reusable capabilities can be extracted as independent crates, and metadata handling can evolve beyond taxonomy into a unified normalization layer. The current taxonomy logic exists in both typub-engine and typub-adapters-core, and asset/path/link logic is duplicated across multiple crates.

Constraints

  • Avoid circular dependencies (e.g., configtheme).
  • Reusable crates must remain “pure”: no UI, no runtime initialization, no project-specific I/O.
  • The refactor must preserve existing runtime behavior for engine, CLI, and TUI.

Options Considered

  • Keep current structure and only clean up duplication in-place.
  • Extract minimal new crates for project/path, link rewriting, and asset AST processing, and centralize metadata normalization in adapters-core.
  • Move everything into typub-core (rejected due to high coupling risk).

Decision

Describe the decision that was made. What is the change that we’re proposing and/or doing?

Consequences

Positive

  • Reusable crates can be published or consumed outside typub with minimal dependencies.
  • Metadata handling becomes consistent and extensible beyond taxonomy.
  • Reduced duplication across engine/storage/adapters improves maintainability.

Negative

  • API renames (taxonomymetadata) require refactors across call sites.
  • Short-term churn: module moves and dependency updates across multiple crates.
  • Potential migration complexity for existing adapters (mitigation: mechanical rename + compiler-guided updates).

Neutral

  • Engine behavior should remain the same; only structure and ownership change.

Alternatives Considered

Keep current structure with localized cleanups: Rejected; does not improve reuse boundaries or remove duplication.

Move all shared logic into typub-core: Rejected; creates a high-coupling core and increases cycle risk.

ADR-0008: Use distinct ImageMarker types for pending state

Status: accepted | Date: 2026-02-17

References: RFC-0002

Context

The codebase has two image-related concepts:

  1. HtmlElement::Image / InlineFragment::InlineImage — resolved images with URLs
  2. HtmlElement::ImageMarker — pending images awaiting deferred upload

For block-level images, the distinction is explicit via types. However, inline images use InlineFragment::InlineImage for both resolved and pending states, distinguishing them by examining the src field at runtime:

#![allow(unused)]
fn main() {
// Current: runtime check to determine pending state
if !src.starts_with("http://") && !src.starts_with("https://") {
    paths.push(PathBuf::from(src));  // pending
}
}

This violates the Rust principle of encoding invariants in types. The pending state is not visible in the type system.

Decision

Add InlineFragment::ImageMarker variant for pending inline images, mirroring the block-level HtmlElement::ImageMarker:

#![allow(unused)]
fn main() {
enum InlineFragment {
    // ...
    InlineImage { src: String, alt: String, attrs: Attrs },  // resolved
    ImageMarker { path: String, alt: String, attrs: Attrs }, // pending
}
}

Remove the display field from HtmlElement::ImageMarker since the block/inline distinction is now encoded in the type itself (HtmlElement vs InlineFragment).

This creates a clean state machine:

  • Block: ImageMarker → [upload] → Image
  • Inline: InlineFragment::ImageMarker → [upload] → InlineFragment::InlineImage

Consequences

Benefits:

  • Type system enforces pending/resolved distinction at compile time
  • govctl check validates no unresolved markers before serialize
  • Unified tracking logic for both block and inline markers
  • Simpler format adapters (no runtime URL prefix checks)

Trade-offs:

  • More enum variants to handle in match expressions
  • Need recursive traversal for InlineFragment::ImageMarker
  • Requires updates to all consumers of InlineFragment

Affected files:

  • typub-html/src/types.rs — add variant, remove display field
  • typub-html/src/svg.rs — generate correct marker types
  • typub-assets-ast/src/lib.rs — recursive tracking and resolution
  • Adapter format modules — handle new variant

ADR-0009: split local output adapters: astro for markdown, static for html

Status: accepted | Date: 2026-02-17

References: RFC-0007

Context

The current astro adapter outputs standalone HTML files (index.html) to a local directory. However, Astro is a framework that consumes Markdown/MDX source files, not pre-rendered HTML.

This creates a mismatch:

  1. Users cannot use the output with Astro’s build system (astro build/astro dev)
  2. The output is more suitable for static hosting, not Astro Content Collections
  3. Users wanting Markdown output for SSGs have no supported option

Additionally, there is no dedicated adapter for generating standalone HTML files that can be directly deployed to static hosts (GitHub Pages, Netlify, etc.).

Decision

Split the local output adapters into two distinct adapters:

  1. astro - Outputs Markdown files with YAML front-matter

    • Output: {slug}/index.md or {slug}.md
    • Assets: copied to {slug}/assets/ (relative paths)
    • Front-matter: title, date, draft, tags, categories
    • Compatible with Astro Content Collections
  2. static - Outputs standalone HTML files (current astro behavior)

    • Output: {slug}/index.html
    • Assets: copied to {slug}/assets/ or embedded
    • Theming: applies theme CSS inline
    • Directly deployable to static hosts

Consequences

Benefits:

  • Astro users can integrate output into their Astro project
  • Static HTML generation remains available under new static adapter
  • Clear separation of concerns

Risks:

  • Breaking change for existing astro adapter users (need migration guide)
  • Increased maintenance for two adapters with similar functionality

Migration path:

  • Users currently using astro for HTML output should switch to static
  • Users wanting Markdown output can now use the refactored astro

ADR-0010: add requires_config to AdapterCapability for explicit platform registration

Status: accepted | Date: 2026-02-18

References: RFC-0006

Context

Currently, all adapters require explicit configuration in typub.toml to be available for use. This creates friction for local-output adapters like astro, static, and xiaohongshu that don’t need any external credentials or settings - they can work with sensible defaults immediately.

Previously, AdapterRegistry::new only registered adapters where the platform config had enabled = true. This meant users had to add [platforms.astro] and enabled = true to their config just to use local-output adapters.

Per ADR-0009, we split the output adapters into astro (markdown with frontmatter) and static (HTML). Both are local-output adapters that don’t require external service configuration.

Decision

Add a requires_config: bool field to AdapterCapability:

  1. Field definition in AdapterCapability struct:

    • requires_config: bool - indicates whether the platform requires configuration in typub.toml
  2. Platform classification:

    • requires_config = false: astro, static, xiaohongshu, all copypaste profiles
    • requires_config = true: devto, ghost, wordpress, hashnode, confluence, notion
  3. Updated AdapterRegistry::new logic:

    • For requires_config = true adapters: only register if config exists AND is enabled
    • For requires_config = false adapters: register by default, unless explicitly disabled

This allows users to run typub publish -p astro or typub publish -p static without any configuration in typub.toml.

Consequences

Benefits:

  • Zero-config local output: typub publish -p static works immediately
  • Clear separation between platforms needing credentials vs pure local output
  • Users can still explicitly disable local adapters if needed

Drawbacks:

  • One more field to maintain in AdapterCapability
  • Breaking change: all adapter definitions must include requires_config

Migration:

  • This is a breaking change for the adapter capability struct
  • All existing adapter configs must be updated to include the new field

ADR-0011: Unify image types to inline-only representation

Status: accepted | Date: 2026-02-19

References: ADR-0008

Context

Per ADR-0008, the codebase has four image-related variants:

  • HtmlElement::Image — block-level resolved image
  • HtmlElement::ImageMarker — block-level pending image
  • InlineFragment::InlineImage — inline resolved image
  • InlineFragment::ImageMarker — inline pending image

This creates redundancy: the same image data is represented in two places (HtmlElement vs InlineFragment).

Problem Statement

The block/inline distinction for images is a rendering concern, not a content concern. In HTML, <img> is always an inline element — the “block-level” appearance comes from the container (e.g., <p><img></p>), not the image itself.

Maintaining separate types for block and inline images:

  1. Duplicates code and logic
  2. Requires adapters to handle both variants
  3. Mixes content representation with presentation concerns

Constraints

  • ADR-0008 established the ImageMarker pattern for deferred upload
  • Adapters (Notion, Confluence) have different handling for block vs inline images

Options Considered

  1. Keep current design — 4 variants, explicit block/inline
  2. Unify to inline-only — Remove HtmlElement Image variants, detect single-image paragraphs at render time

Decision

We will remove HtmlElement::Image and HtmlElement::ImageMarker variants and unify image representation to inline-only because:

  1. Single source of truth: Images are always represented as InlineFragment::InlineImage or InlineFragment::ImageMarker
  2. Separation of concerns: IR represents content; adapters decide presentation (block vs inline rendering)
  3. Simpler parsing: Standalone <img> elements parse as Paragraph { fragments: [InlineImage] }

Implementation Notes

Parsing changes:

  • Remove parse_image() function from blocks.rs
  • <img> at block level becomes Paragraph with single InlineImage

Adapter changes:

  • Add helper function is_single_image_paragraph(fragments: &[InlineFragment]) -> bool
  • Notion: Single-image paragraph → image block
  • Confluence: Single-image paragraph → <ac:image> block
  • HTML: All images render as <img>

Consequences

Positive

  • Simpler type system: 2 image variants instead of 4
  • Clear separation: IR for content, adapters for presentation
  • Easier maintenance: Single code path for image handling

Negative

  • Adapters need to detect single-image paragraphs for special rendering (mitigation: provide helper function in adapters-core)
  • Breaking change to IR structure (mitigation: we explicitly allow breaking changes per task description)

Neutral

  • Standalone <img> HTML produces Paragraph instead of Image element

Alternatives Considered

Keep 4 variants: Explicit block/inline distinction. Rejected: Adds unnecessary complexity, mixes content and presentation concerns.

ADR-0012: Unify SVG types to inline-only representation

Status: accepted | Date: 2026-02-19

References: ADR-0011, RFC-0009

Context

SVG handling was previously unified to inline-only representation to simplify legacy IR shape and align with the then-current image pattern.

The current semantic IR baseline is defined by RFC-0009, which introduces explicit math nodes and document-root semantics. This ADR is retained as historical implementation context, but normative IR modeling now follows RFC-0009.

Decision

Keep this ADR as an accepted historical record of the legacy SVG unification step.

For ongoing and future implementation work, treat RFC-0009 as normative and model math/embedded equation content using explicit semantic math nodes rather than SVG-only structural conventions.

Consequences

Positive:

  • Preserves traceability of why legacy code paths treat SVG as inline-first.
  • Removes normative ambiguity by making RFC-0009 the active source of truth.

Negative:

  • Legacy adapter helpers that infer block math from SVG wrappers are transitional and should be phased out under the RFC-0009 migration plan.

Neutral:

  • Historical behavior remains documented without constraining v2 semantic IR design.

ADR-0013: AST v2 big-bang migration strategy

Status: accepted | Date: 2026-02-21

References: RFC-0009

Context

RFC-0009 introduces a semantic document IR that removes execution-state data from AST. Because this project has a single active user and no external compatibility commitments, maintaining dual AST stacks would add unnecessary complexity and delay.

Decision

Adopt a big-bang migration for AST v2. Replace old AST models and remove ImageMarker/local_path transitional semantics in one implementation wave. Do not preserve long-term compatibility shims inside AST. Keep platform-specific workarounds in serializer policy layers only when unavoidable.

Consequences

Positive: faster convergence to clean architecture, fewer special-case branches, clearer invariants. Negative: temporary breakage risk during migration and larger single change-set. Mitigations: strict RFC-0009 validation rules, focused end-to-end snapshots on key adapters, and immediate cleanup of deprecated paths once new AST is live.

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[Unreleased]

Fixed

  • FootnoteId changed to numeric type for correct sorting (WI-2026-02-27-001)

[0.1.0] - 2026-02-23

Added

  • Draft RFC defining create-vs-update decision and idempotency semantics (WI-2026-02-11-014)
  • Specify conflict/retry behavior and status persistence requirements for republish (WI-2026-02-11-014)
  • CopyPasteAdapter with CopyFormat enum and KNOWN_PROFILES table (WI-2026-02-11-021)
  • 10 initial platform profiles (5 StyledHtml + 5 Markdown) (WI-2026-02-11-021)
  • type=manual custom platform support in registry (WI-2026-02-11-021)
  • profiles.toml with all 10 built-in copy-paste profiles (WI-2026-02-11-022)
  • build.rs generates BUILTIN_PROFILES static array from profiles.toml (WI-2026-02-11-022)
  • Add commented # enabled = false example in config.template.toml (WI-2026-02-11-023)
  • 14 new copypaste profiles (5 html + 9 markdown) (WI-2026-02-12-002)
  • HashNode adapter module with GraphQL API integration (WI-2026-02-12-003)
  • HashNode registered in AdapterRegistry with capability entry (WI-2026-02-12-003)
  • StorageConfig accessible via PublishContext per RFC-0004:C-STORAGE-CONFIG (WI-2026-02-12-005)
  • materialize_payload uploads External assets to S3 per RFC-0004:C-PIPELINE-INTEGRATION (WI-2026-02-12-005)
  • Asset references replaced with remote URLs in payload (WI-2026-02-12-005)
  • RFC-0002 v0.3.0 defines 9-stage AST-centric pipeline (WI-2026-02-12-006)
  • RFC-0004 v0.3.0 updates C-PIPELINE-INTEGRATION for AST modification (WI-2026-02-12-006)
  • AdapterPayload carries AST (Vec<HtmlElement>) not strings (WI-2026-02-12-006)
  • Materialize stage modifies AST nodes (ImageMarkerImage) (WI-2026-02-12-006)
  • Serialize trait method added to PlatformAdapter (WI-2026-02-12-006)
  • Shared resolve_asset_urls() modifies AST in Materialize (WI-2026-02-12-007)
  • Hashnode/Devto implement serialize_payload (AST→Markdown) (WI-2026-02-12-007)
  • WordPress implements serialize_payload (AST→HTML) (WI-2026-02-12-007)
  • Notion implements serialize_payload (AST→blocks) (WI-2026-02-12-007)
  • Confluence implements serialize_payload (AST→storage format) (WI-2026-02-12-007)
  • Attrs type alias and attrs field on all HtmlElement variants for HTML attribute preservation (WI-2026-02-12-008)
  • Helper constructors (paragraph_text, heading_text, etc.) for simplified element creation (WI-2026-02-12-008)
  • Add subst crate dependency with toml feature (WI-2026-02-12-013)
  • PlatformConfig::get_str() expands ${VAR} syntax (WI-2026-02-12-013)
  • StorageConfig fields support ${VAR} expansion (WI-2026-02-12-013)
  • Ghost adapter module with client, types, and tests (WI-2026-02-12-015)
  • Support Admin API for post CRUD (WI-2026-02-12-015)
  • Support tags and internal links (WI-2026-02-12-015)
  • publishAs field in GraphQL mutations (WI-2026-02-12-017)
  • DraftSupport enum and extend AdapterCapability (WI-2026-02-12-018)
  • published field to Config, PlatformConfig, ContentMeta, PostPlatformConfig (WI-2026-02-12-018)
  • resolve_published() with 5-level fallback chain (WI-2026-02-12-018)
  • data integrity guard for remote_status (WI-2026-02-12-018)
  • determine_lifecycle_action() implementing decision table (WI-2026-02-12-018)
  • Hashnode adapter lifecycle support with publishDraft mutation (WI-2026-02-12-018)
  • Dev.to, Ghost, WordPress, Confluence adapter lifecycle support (WI-2026-02-12-018)
  • Refactor status command to use comfy-table tabular view (WI-2026-02-12-020)
  • TUI module with ratatui (feature-gated) (WI-2026-02-12-020)
  • Post list view with navigation (WI-2026-02-12-020)
  • Post detail view with platform statuses (WI-2026-02-12-020)
  • Inline HTML preview using html2text (WI-2026-02-12-020)
  • Publish action with progress display (WI-2026-02-12-020)
  • Watch command auto-opens preview when platform specified (WI-2026-02-12-022)
  • Extract SVG elements from Typst HTML output (WI-2026-02-12-026)
  • Upload math SVG as Confluence attachments with content-hash naming (WI-2026-02-12-026)
  • Replace inline SVG with ac:image references in Confluence format (WI-2026-02-12-026)
  • Cache uploaded SVGs in asset_uploads table to avoid duplicates (WI-2026-02-12-026)
  • Support inline vs block math styling (WI-2026-02-12-026)
  • Notion math formula support via equation blocks (WI-2026-02-12-027)
  • Create preview_single_platform function as pipeline assembler (WI-2026-02-12-029)
  • Add prepare_preview_elements and build_preview to PlatformAdapter trait (WI-2026-02-12-029)
  • Validate assets are within project root (WI-2026-02-12-030)
  • 4 new theme CSS files (wechat-green, dark, notion, github) (WI-2026-02-13-001)
  • 5-level theme resolution chain (WI-2026-02-13-001)
  • default_theme field in profiles.toml (WI-2026-02-13-001)
  • Themes embedded at compile time via build.rs with user override support (WI-2026-02-13-001)
  • internal_link_target field in ContentMeta, PostPlatformConfig, Config, PlatformConfig (WI-2026-02-13-002)
  • resolve_href_for_copypaste and resolve_internal_link_target functions in internal_links.rs (WI-2026-02-13-002)
  • get_first_published_url in StatusTracker for alphabetical platform selection (WI-2026-02-13-002)
  • Unit tests for copypaste internal link resolution (WI-2026-02-13-002)
  • ResolvedConfig struct with all resolvable fields (WI-2026-02-13-003)
  • ResolvedConfig::resolve() implements 4-level chain for all fields (WI-2026-02-13-003)
  • Unit tests for ResolvedConfig resolution (WI-2026-02-13-003)
  • TransformRule enum with enumset for declarative AST transforms (WI-2026-02-13-006)
  • SerializeOptions with li_span_wrap flag in serialize.rs (WI-2026-02-13-006)
  • CopyPaste adapter supports AssetStrategy::External (WI-2026-02-13-007)
  • External assets replaced with storage URLs in output (WI-2026-02-13-007)
  • i18n module with Locale enum and t() function (WI-2026-02-13-008)
  • Locale detection from LANG/LC_ALL environment variable (WI-2026-02-13-008)
  • Chinese and English translations for preview UI strings (WI-2026-02-13-008)
  • Sorting module with SortField/SortOrder enums and sort function (WI-2026-02-13-009)
  • PostFilter struct with platform/status/tag/title_regex predicates (WI-2026-02-13-009)
  • TUI interactive sort with s/S keys (WI-2026-02-13-009)
  • Command aliases: ls for list, pub for publish, b for build, w for watch, pre for preview (WI-2026-02-13-009)
  • Short flags for list options (-s sort, -p platform, -t tag, -T title, -P published, -u pending) (WI-2026-02-13-009)
  • Short flag -d for publish –dry-run (WI-2026-02-13-009)
  • long_about and after_help with examples for list and publish commands (WI-2026-02-13-009)
  • Main CLI after_help with common workflows (WI-2026-02-13-009)
  • Adaptive compact platform display with short codes when table exceeds terminal width (WI-2026-02-13-009)
  • Platform short codes for known platforms (gh, wp, nt, etc.) with numbered fallback (p1, p2) for custom (WI-2026-02-13-009)
  • Legend printed after table in compact mode (WI-2026-02-13-009)
  • Create adapters.toml with 8 API adapters including short_code and local_output fields (WI-2026-02-13-010)
  • Add short_code field to all profiles in profiles.toml (WI-2026-02-13-010)
  • Update build.rs to generate builtin_adapters.rs from adapters.toml (WI-2026-02-13-010)
  • Create docs/guide/platforms/ directory structure (WI-2026-02-13-013)
  • Ghost platform guide with API key instructions (WI-2026-02-13-013)
  • Dev.to platform guide with API key instructions (WI-2026-02-13-013)
  • Notion platform guide with API key instructions (WI-2026-02-13-013)
  • WeChat platform guide (Chinese) (WI-2026-02-13-013)
  • Zhihu platform guide (Chinese) (WI-2026-02-13-013)
  • CSDN platform guide (Chinese) (WI-2026-02-13-013)
  • ADR-0001 accepted: dual storage + code_highlight capability (WI-2026-02-13-015)
  • code_highlight field in adapters.toml and profiles.toml (WI-2026-02-13-015)
  • CodeBlock stores both plain text and highlighted HTML (WI-2026-02-13-015)
  • Tests for inline and block math in Markdown (WI-2026-02-13-017)
  • Create typub-core subcrate with 6 capability enums and ThemeId newtype (WI-2026-02-13-018)
  • Create typub-html subcrate with html_utils modules (WI-2026-02-13-019)
  • workspace.package with common metadata (WI-2026-02-13-020)
  • MIT LICENSE file (WI-2026-02-13-020)
  • release.toml configuration (WI-2026-02-13-020)
  • Ghost image upload via /images/upload endpoint (WI-2026-02-13-023)
  • Ghost materialize_payload handles Upload strategy (WI-2026-02-13-023)
  • default_asset_strategy field to profiles.toml with fallback to embed (WI-2026-02-13-024)
  • Wire field through build.rs and BuiltinProfile struct (WI-2026-02-13-024)
  • MathRendering::Png variant in typub-core (WI-2026-02-13-026)
  • svg_to_png() function using resvg (WI-2026-02-13-026)
  • HTML serialization outputs PNG img tags (WI-2026-02-13-026)
  • ModelScope profile uses math_rendering=png (WI-2026-02-13-026)
  • RFC drafted with normative clauses for AdapterRegistrar API (WI-2026-02-14-003)
  • RFC specifies capability registration and lookup order (WI-2026-02-14-003)
  • RFC specifies backward compatibility constraints (WI-2026-02-14-003)
  • RFC drafted with crate boundary specifications (WI-2026-02-14-004)
  • RFC specifies shared adapter types crate (WI-2026-02-14-004)
  • RFC specifies migration path from current layout (WI-2026-02-14-004)
  • typub-adapters-core crate exists in crates/ (WI-2026-02-14-005)
  • typub-config crate exists in crates/ (WI-2026-02-14-005)
  • typub-storage crate exists in crates/ (WI-2026-02-14-005)
  • PlatformAdapter trait defined in typub-adapters-core (WI-2026-02-14-005)
  • AdapterRegistrar implemented per RFC-0006 (WI-2026-02-14-005)
  • typub-adapter-ghost crate extracted (WI-2026-02-14-006)
  • typub-adapter-notion crate extracted (WI-2026-02-14-006)
  • all 9 adapter crates extracted with register() functions (WI-2026-02-14-006)
  • feature gates configured per RFC-0007:C-FEATURE-GATES (WI-2026-02-14-006)
  • Create typub-log crate with tracing foundation (WI-2026-02-14-009)
  • Implement CliLayer for CLI-formatted output (WI-2026-02-14-009)
  • Define ProgressReporter trait in typub-log (WI-2026-02-14-009)
  • Unit tests and mock tests for Notion adapter core behaviors (WI-2026-02-14-012)
  • Unit tests and mock tests for Ghost adapter core behaviors (WI-2026-02-15-001)
  • Preview integration coverage maintained for Ghost adapter (WI-2026-02-15-001)
  • Add register() function in config.rs per RFC-0006 (WI-2026-02-15-002)
  • Unit tests for registration, asset strategy, render config, config validation, trait methods (WI-2026-02-15-002)
  • Preview snapshot integration test for WordPress adapter (WI-2026-02-15-002)
  • PNG math images upload for Upload/External strategies (WI-2026-02-17-001)
  • Confluence adapter uploads PNG math as attachments (WI-2026-02-17-001)
  • CopyPaste adapter with External strategy uploads PNG math (WI-2026-02-17-001)
  • math_rendering and math_delimiters fields in PlatformConfig (WI-2026-02-17-002)
  • supported_math_renderings in AdapterCapability (WI-2026-02-17-002)
  • resolve_math_rendering_from_config helper with validation (WI-2026-02-17-002)
  • ImageMarker display attribute for inline/block distinction (WI-2026-02-17-002)
  • Confluence PNG math support for inline and block (WI-2026-02-17-002)
  • InlineFragment::ImageMarker variant (WI-2026-02-17-003)
  • Astro adapter outputs Markdown with front-matter (WI-2026-02-17-004)
  • New static adapter generates standalone HTML (WI-2026-02-17-004)
  • –assets flag added to status command (WI-2026-02-17-005)
  • asset uploads displayed in table format when flag is set (WI-2026-02-17-005)
  • Add requires_config field to AdapterCapability (WI-2026-02-18-001)
  • Update all adapters with requires_config value (WI-2026-02-18-001)
  • Modify AdapterRegistry to auto-register platforms without config requirement (WI-2026-02-18-001)
  • Add helper methods default_xxx() for slices (WI-2026-02-18-002)
  • Strikethrough node type supported in content model (WI-2026-02-18-003)
  • Markdown renderer outputs text for strikethrough (WI-2026-02-18-003)
  • ListItem struct with recursive children field for nested list support (WI-2026-02-18-004)
  • TaskItem struct with checked field for task list support (WI-2026-02-18-004)
  • DefinitionItem struct for definition list (dl/dt/dd) support (WI-2026-02-18-004)
  • TableCell struct with colspan, rowspan, and align fields (WI-2026-02-18-004)
  • New inline fragments (Underline, Mark, Superscript, Subscript, Kbd, LineBreak) (WI-2026-02-18-004)
  • HTML parser handles nested ul/ol within li elements (WI-2026-02-18-004)
  • HTML parser handles task lists and definition lists (WI-2026-02-18-004)
  • Markdown renderer outputs correct nested list indentation (WI-2026-02-18-004)
  • All adapters updated with fallback handling for new types (WI-2026-02-18-004)
  • Parse block SVG as Paragraph with Svg fragment (WI-2026-02-19-002)
  • Add is_svg_block helper in adapters-core (WI-2026-02-19-002)
  • Create TextStyle enum and unify styled variants into InlineFragment::Styled (WI-2026-02-19-004)
  • Add convenience constructors (bold, italic, etc.) to InlineFragment (WI-2026-02-19-004)
  • Implement –debug-stage CLI option (WI-2026-02-19-005)
  • Support all pipeline stages: resolve, render, parse, transform, specialize, provision, materialize, serialize, publish, persist (WI-2026-02-19-005)
  • Dry-run mode copies assets to temp dir instead of skipping upload (WI-2026-02-19-006)
  • InlineFragment::Image has local_path: Option<PathBuf> field (WI-2026-02-20-001)
  • resolve_preview_image_paths() function in typub-html/transform.rs (WI-2026-02-20-001)
  • dev server serves /__asset__/* paths for preview images (WI-2026-02-20-001)
  • Add Details element to HtmlElement enum with summary and children (WI-2026-02-21-001)
  • Parse HTML details/summary elements (WI-2026-02-21-001)
  • Serialize Details to HTML (WI-2026-02-21-001)
  • Render Details in Markdown renderer (WI-2026-02-21-001)
  • Define and adopt AST v2 schema with typed attrs plus passthrough (WI-2026-02-21-004)
  • Remove ImageMarker/local_path execution semantics from core AST (WI-2026-02-21-004)
  • Introduce v2 core IR types in typub-html and make them the single source of truth for conformance serialization (WI-2026-02-21-004)
  • Support user-defined Typst preamble with layered resolution (WI-2026-02-22-001)
  • Preserve adapter defaults while allowing user preamble override/append behavior (WI-2026-02-22-001)

Changed

  • Clarify strict pipeline stage semantics in RFC-0002 (WI-2026-02-11-001)
  • Tighten status correctness and reconciliation wording (WI-2026-02-11-001)
  • Existing adapters implement stage-4 finalization and stage-5 publish hooks (WI-2026-02-11-002)
  • Pipeline execution no longer relies on legacy adapter fallback path (WI-2026-02-11-002)
  • Devto and WordPress split stage-4 payload finalization from stage-5 publish calls (WI-2026-02-11-003)
  • Notion and Confluence remove monolithic publish wrappers in favor of staged hooks (WI-2026-02-11-003)
  • PlatformAdapter trait removes monolithic publish method (WI-2026-02-11-004)
  • All adapters publish only via finalize_payload and publish_payload (WI-2026-02-11-004)
  • Stage-3 shared transforms execute in pipeline module (WI-2026-02-11-005)
  • Devto/WordPress remove duplicate adapter-local internal-link rewrite (WI-2026-02-11-005)
  • PublishResult URL is optional and status persistence handles absent URL (WI-2026-02-11-006)
  • Notion opts into shared stage-3 link rewrite and removes duplicate adapter-local rewrite (WI-2026-02-11-006)
  • Astro finalize_payload performs stage-4 shaping (assets+themed body) (WI-2026-02-11-007)
  • WeChat and Xiaohongshu finalize_payload are non-empty and carry publish-ready payload (WI-2026-02-11-007)
  • Remove default empty finalize_payload so every adapter must implement stage-4 (WI-2026-02-11-008)
  • Adapter capability matrix includes per-gap behavior semantics (warn+degrade vs hard error) (WI-2026-02-11-008)
  • CLI taxonomy compatibility warnings derive from capability gap policy (WI-2026-02-11-008)
  • Stage-3 runtime enforces internal-link unsupported behavior from capability policy (WI-2026-02-11-009)
  • Internal-link detection utility provides machine-checkable target discovery from rendered HTML (WI-2026-02-11-009)
  • Remove trait-level process_assets hook and keep stage-4 asset handling inside finalize_payload (WI-2026-02-11-010)
  • WordPress taxonomy remote-id shaping is moved into stage-4 finalize payload (WI-2026-02-11-010)
  • Remove dead internal-link fallback branches in Notion/Confluence when shared rewrite is enabled (WI-2026-02-11-010)
  • RFC-0002 pipeline stages define optional remote asset materialization stage between finalize and publish (WI-2026-02-11-011)
  • pipeline implementation comments align stage numbering with updated RFC contract (WI-2026-02-11-011)
  • RFC-0002:C-PIPELINE-STAGES lists short names for all seven stages (WI-2026-02-11-012)
  • RFC-0002:C-PIPELINE-STAGES uses one-word stage names suitable for code identifiers (WI-2026-02-11-013)
  • src/pipeline.rs stage comments use same one-word names as RFC (WI-2026-02-11-013)
  • RFC-0003 decision order prioritizes remote-id then deterministic platform key; cached URL is hint-only (WI-2026-02-11-015)
  • RFC-0003 mandates duplicate-create rejection as conflict (WI-2026-02-11-015)
  • Adapters prefer status remote-id as update target before deterministic lookup fallback (WI-2026-02-11-016)
  • Create path runs only after duplicate-safe precheck per adapter (WI-2026-02-11-016)
  • Extract RenderedOutput::html() helper to eliminate 12x duplicate error check (WI-2026-02-11-018)
  • Extract downcast_payload helper to eliminate 7x duplicate downcast boilerplate (WI-2026-02-11-018)
  • Extract Notion create_page_with_blocks helper to eliminate 3x block-split copy-paste (WI-2026-02-11-018)
  • Remove Notion elements_to_blocks duplication of Paragraph/ParagraphRich handling (WI-2026-02-11-018)
  • Remove dead Confluence elements_to_confluence_html indirection (WI-2026-02-11-018)
  • Use shared mime_type_from_path in Notion and Confluence instead of hand-rolled maps (WI-2026-02-11-018)
  • Extract CapabilitySupport::gap_behavior method to replace 3x copy-paste (WI-2026-02-11-018)
  • Replace module-level allow(dead_code) with targeted annotations (WI-2026-02-11-018)
  • add materialize_payload to PlatformAdapter trait with default pass-through (RFC-0002 Stage 5) (WI-2026-02-11-019)
  • wire Stage 5 materialize call in pipeline.rs between finalize and publish (WI-2026-02-11-019)
  • WordPress finalize_payload no longer makes remote API calls; tag/category resolution and asset upload moved to materialize_payload (C1) (WI-2026-02-11-019)
  • WordPress create_post and update_post merged into upsert_post (N3) (WI-2026-02-11-019)
  • WordPress auth-check pattern extracted to http_utils::ensure_success_with_auth_hint (N4) (WI-2026-02-11-019)
  • Dev.to adds title-based lookup fallback per RFC-0003 (C2) (WI-2026-02-11-019)
  • WeChat regex compilation uses LazyLock statics (N5) (WI-2026-02-11-019)
  • preview boilerplate extracted to shared helpers (C5) (WI-2026-02-11-019)
  • AdapterRegistry only constructs enabled adapters (C6) (WI-2026-02-11-019)
  • WordPress slug resolution extracted to helper (L1) (WI-2026-02-11-020)
  • Xiaohongshu preview uses write_preview_file (L5) (WI-2026-02-11-020)
  • AdapterRegistry error message clarified (L6) (WI-2026-02-11-020)
  • WeChat adapter migrated to CopyPasteProfile entry (WI-2026-02-11-021)
  • copypaste.rs uses generated code instead of hand-written KNOWN_PROFILES (WI-2026-02-11-022)
  • Rename enabled_platforms() to default_platforms() with updated semantics (WI-2026-02-11-023)
  • Specialize stages collect metadata only, no string serialization (WI-2026-02-12-007)
  • All HtmlElement text-bearing variants use Vec<InlineFragment> directly (removed plain/Rich split) (WI-2026-02-12-008)
  • InlineFragment::Link uses fragments Vec instead of text String for nested inline support (WI-2026-02-12-008)
  • HTML/Markdown/Notion/Confluence serializers updated for unified variants (WI-2026-02-12-008)
  • WordPress adapter split into wordpress/{mod,types,client,tests}.rs (WI-2026-02-12-012)
  • Hashnode adapter split into hashnode/{mod,types,client,tests}.rs (WI-2026-02-12-012)
  • DevTo adapter split into devto/{mod,types,client,tests}.rs (WI-2026-02-12-012)
  • Stage 7/8 adapter payload typing failures fail fast instead of defaulting to empty values (WI-2026-02-12-014)
  • Unify InlineSvg and Svg into single Svg type with inline attr (WI-2026-02-12-027)
  • Deduplicate HTML parsing logic in html_utils (WI-2026-02-12-027)
  • Notion API upgraded to 2025-09-03 (WI-2026-02-12-028)
  • Add PipelineMode enum to distinguish preview/publish contexts (WI-2026-02-12-029)
  • Extract shared pipeline stages into reusable functions (WI-2026-02-12-029)
  • Update all adapters to implement new trait methods (WI-2026-02-12-029)
  • Update cmd_preview to use preview_single_platform (WI-2026-02-12-029)
  • Rename config.toml to typub.toml in CLI default (WI-2026-02-12-030)
  • Update all source references from config.toml to typub.toml (WI-2026-02-12-030)
  • Store asset paths as relative to project root in status.db (WI-2026-02-12-030)
  • Extract inline CSS from adapters to _preview-*.css files (WI-2026-02-13-001)
  • UI/UX refinement of existing themes (WI-2026-02-13-001)
  • CopyPaste adapter enables shared link rewrite (supports_shared_link_rewrite -> true) (WI-2026-02-13-002)
  • Pipeline uses copypaste-specific resolution for copypaste platforms (WI-2026-02-13-002)
  • Config structure flattened - removed [general] section, content_dir/output_dir at root (WI-2026-02-13-002)
  • Remove standalone resolve_* functions, use ResolvedConfig (WI-2026-02-13-003)
  • Pipeline and adapters use ResolvedConfig instead of ad-hoc resolution (WI-2026-02-13-003)
  • compat_fn signature takes AST elements instead of HTML string (WI-2026-02-13-006)
  • Update all compat functions to operate on HtmlElement (WI-2026-02-13-006)
  • copypaste.rs and xiaohongshu.rs use i18n::t() for UI strings (WI-2026-02-13-008)
  • list command shows compact table with sort/filter/limit flags (WI-2026-02-13-009)
  • status command requires path and shows detailed single-post info (WI-2026-02-13-009)
  • crossterm moved to unconditional dependency for terminal width detection (WI-2026-02-13-009)
  • Update AdapterCapability and BuiltinProfile structs with short_code (WI-2026-02-13-010)
  • platform_short_code() uses generated data instead of hardcoded match (WI-2026-02-13-010)
  • is_local_output_platform() uses local_output field from adapters.toml (WI-2026-02-13-010)
  • build-book.sh for dynamic SUMMARY.md generation (WI-2026-02-13-013)
  • Convert root Cargo.toml to workspace with typub-core as member (WI-2026-02-13-018)
  • build.rs uses typed enums from typub-core instead of String fields (WI-2026-02-13-018)
  • Main crate re-exports enums from typub-core, preserving existing import paths (WI-2026-02-13-018)
  • Replace raw String theme fields with ThemeId newtype (WI-2026-02-13-018)
  • Add workspace.dependencies for unified version management (WI-2026-02-13-019)
  • All crates use workspace = true for shared dependencies (WI-2026-02-13-019)
  • Main crate re-exports typub_html as html_utils for path compatibility (WI-2026-02-13-019)
  • internal crates use version in workspace.dependencies (WI-2026-02-13-020)
  • all crates inherit workspace metadata (WI-2026-02-13-020)
  • Remove copy strategy from devto and wordpress adapters.toml (WI-2026-02-13-023)
  • Update CopyPasteAdapter to use per-profile default (WI-2026-02-13-024)
  • Ghost adapter reorganized into adapter/config/model modules following Notion exemplar (WI-2026-02-15-001)
  • WordPress adapter reorganized into adapter/config/model modules following Notion exemplar (WI-2026-02-15-002)
  • Centralize asset strategy resolution in main crate to a single path (WI-2026-02-15-004)
  • Eliminate duplicate internal_link_target resolution by routing through ResolvedConfig (WI-2026-02-15-004)
  • Use config-based project root for renderer instead of cwd (WI-2026-02-15-005)
  • Split pipeline module into stage-focused submodules without behavior changes (WI-2026-02-15-005)
  • Eliminate any remaining duplicate resolution logic in main crate (WI-2026-02-15-005)
  • Remove pure re-export config module and update imports (WI-2026-02-15-006)
  • Remove main-crate re-export-only aliases where unused (WI-2026-02-15-006)
  • TUI is enabled by default; typub-tui remains a standalone crate; typub-ui owns i18n (WI-2026-02-15-007)
  • Core build pipeline moved into typub-engine crate (WI-2026-02-15-007)
  • Update engine/tui/cli usages to new crate/module layout (WI-2026-02-15-008)
  • Remove display field from HtmlElement::ImageMarker (WI-2026-02-17-003)
  • convert_svg_to_png_markers generates correct marker types (WI-2026-02-17-003)
  • Recursive traversal in build_pending_asset_list_from_elements (WI-2026-02-17-003)
  • Recursive resolution in resolve_asset_urls (WI-2026-02-17-003)
  • Change BuiltinProfile asset_strategy to asset_strategies slice (WI-2026-02-18-002)
  • Change BuiltinProfile math_rendering to math_renderings slice (WI-2026-02-18-002)
  • Change BuiltinProfile math_delimiters to math_delimiters slice (WI-2026-02-18-002)
  • Standalone parses as Paragraph with single InlineImage (WI-2026-02-19-001)
  • Inline SvgInfo fields into InlineFragment::Svg variant (WI-2026-02-19-003)
  • Update all pattern matching to use unified Styled variant (WI-2026-02-19-004)
  • Update RFC-0008 to reflect unified styling approach (WI-2026-02-19-004)
  • convert_svg_to_png_markers uses relative paths instead of absolute (WI-2026-02-20-001)
  • preview_single_platform calls resolve_preview_image_paths after Specialize (WI-2026-02-20-001)
  • TransformRule renamed to SerializeRule in typub-html (WI-2026-02-21-002)
  • transform_rules renamed to serialize_rules in profiles.toml (WI-2026-02-21-002)
  • code_highlight derived from format instead of stored as field (WI-2026-02-21-002)
  • Pipeline transform/materialize/serialize stages run on v2 IR only with no legacy compatibility adapter layer (WI-2026-02-21-004)
  • Parser emits v2 Document directly; legacy HtmlElement/InlineFragment/ImageMarker constructors are removed from parse path (WI-2026-02-21-004)
  • typub-adapters-core and all first-party adapters compile and publish from v2 IR types (WI-2026-02-21-004)

Removed

  • Unused SVG hash/upload code from Confluence adapter (WI-2026-02-12-027)
  • Hashnode Copy strategy code path (not in supported list) (WI-2026-02-13-025)
  • Hashnode Embed strategy code path (not in supported list) (WI-2026-02-13-025)
  • Remove HtmlElement::Image and HtmlElement::ImageMarker variants (WI-2026-02-19-001)
  • Remove HtmlElement::Svg variant (WI-2026-02-19-002)
  • HTML file:// URL rewriting code from dev_server (WI-2026-02-20-001)
  • Remove legacy AST definitions and fallback conversion utilities after adapter migration completes (WI-2026-02-21-004)

Fixed

  • WeChat wechat_compat produces valid HTML for unstyled strong/em tags (WI-2026-02-11-018)
  • Notion find_existing_page propagates errors instead of swallowing them (WI-2026-02-11-018)
  • Notion and Confluence _ctx parameter renamed to ctx where used (N1) (WI-2026-02-11-019)
  • Notion check_status no longer mutates database schema (N2) (WI-2026-02-11-019)
  • Confluence check_status implemented using find_page_by_title (C3) (WI-2026-02-11-019)
  • Dev.to rejects asset_strategy=upload at config time (C4) (WI-2026-02-11-019)
  • Dev.to publish_payload uses title lookup when no cached ID (M1, RFC-0003) (WI-2026-02-11-020)
  • WeChat preview uses extract_preview_body for image rewriting (M2) (WI-2026-02-11-020)
  • Xiaohongshu preview logs warning on copy failure (M3) (WI-2026-02-11-020)
  • Dev.to find_article_by_title uses api_url helper (L2) (WI-2026-02-11-020)
  • http_utils ensure_success_with_auth_hint propagates body read errors (L3) (WI-2026-02-11-020)
  • WeChat outdated comment corrected (L4) (WI-2026-02-11-020)
  • watch command respects –platform flag and falls back to default platforms (WI-2026-02-11-025)
  • watch rebuilds for each target platform, not just astro (WI-2026-02-11-025)
  • Confluence materialize rewrites ImageMarker to Image in AST before serialize (WI-2026-02-12-009)
  • Notion upload materialize rewrites ImageMarker to Image in AST and blocks serialize reads resolved nodes (WI-2026-02-12-009)
  • Confluence and Notion materialize fail fast on asset upload errors and stop before serialize (WI-2026-02-12-009)
  • Deferred pending assets are built from AST ImageMarker references for deferred strategies (WI-2026-02-12-009)
  • Confluence serialize removes AST-HTML-AST round trip and remains AST-centric (WI-2026-02-12-011)
  • Shared adapter helper enforces unresolved ImageMarker guard before deferred serialize (WI-2026-02-12-011)
  • Adapter code organization aligns around pure formatter modules and stage-oriented payload flow (WI-2026-02-12-011)
  • Hashnode slug lookup propagates non-not-found errors instead of silently creating duplicates (WI-2026-02-12-014)
  • Confluence attachment materialization uses server-returned attachment title for rendered references (WI-2026-02-12-014)
  • External strategy triggers image_as_marker (WI-2026-02-12-016)
  • Local-output platforms show dash instead of pending (WI-2026-02-12-021)
  • TUI preview uses selected platform (WI-2026-02-12-021)
  • Output directory uses platform ID as subdirectory (WI-2026-02-12-022)
  • Math equations render as SVG in HTML output (WI-2026-02-12-023)
  • Markdown TeX math renders as SVG in HTML output (WI-2026-02-12-024)
  • Confluence republish updates existing attachments (WI-2026-02-12-025)
  • StorageConfig correctly merges global and per-platform config (WI-2026-02-13-003)
  • Remove .content wrapper from theme CSS and themes.rs (WI-2026-02-13-006)
  • WeChat preview output has no unsupported div/section tags (WI-2026-02-13-006)
  • Inline math renders inline in wechat preview (WI-2026-02-13-011)
  • External asset strategy resolves images correctly for zhihu (WI-2026-02-13-012)
  • Copy-paste HTML platforms preserve syntax highlighting (WI-2026-02-13-015)
  • LaTeX backslashes not double-escaped in Markdown output (WI-2026-02-13-017)
  • packages/math-to-string.typ created during init (WI-2026-02-13-021)
  • renderer ensures math-to-string.typ exists before compile (WI-2026-02-13-021)
  • Extra blank lines between list items removed (WI-2026-02-13-022)
  • Verify zero behavioral change for existing profiles (WI-2026-02-13-024)
  • Pipeline uses lifecycle action to determine correct remote_status (WI-2026-02-13-025)
  • build_preview default uses pipeline elements (WI-2026-02-14-002)
  • preview() method removed from trait (WI-2026-02-14-002)
  • src/adapters/{adapter}/ directories removed (WI-2026-02-14-007)
  • hardcoded factory array removed from AdapterRegistry::new() (WI-2026-02-14-007)
  • Adapter enum uses only External variant (WI-2026-02-14-007)
  • typub-storage uses typub-log instead of typub-ui (WI-2026-02-14-009)
  • typub-adapters-core no longer re-exports typub-ui (WI-2026-02-14-011)
  • All adapters use tracing macros (WI-2026-02-14-011)
  • Harmonize asset strategy resolution and error handling across adapters/core (WI-2026-02-15-003)
  • Normalize render_config defaults and marker behavior (WI-2026-02-15-003)
  • Standardize adapter test scaffolding (new_for_test) (WI-2026-02-15-003)
  • Fix Markdown clipboard handling for copypaste adapters (WI-2026-02-17-006)
  • Adapters detect single-image paragraph for block-level rendering (WI-2026-02-19-001)
  • Change Admonition.fragments to Admonition.children: Vec<HtmlElement> (WI-2026-02-20-003)
  • Update parse_admonition to use recursive block parsing (WI-2026-02-20-003)
  • Update Notion adapter to render children blocks inside callouts (WI-2026-02-20-003)
  • Update HTML/theme adapters for block-level admonition content (WI-2026-02-20-003)
  • Update adapters (Notion, Confluence, Ghost) for Details (WI-2026-02-21-001)
  • Remove extra <p> wrapper in Confluence admonition rendering (WI-2026-02-21-003)
  • Validation enforces heading range 1..=6, math source minimum validity, and resolvable asset references under v2 IR (WI-2026-02-21-004)
  • Deterministic serialization test coverage includes passthrough maps and style-set canonical order (WI-2026-02-21-004)