An open API service indexing awesome lists of open source software.

https://github.com/p4suta/ghost-to-md

CLI tool to convert Ghost CMS JSON exports to Markdown files with YAML front matter
https://github.com/p4suta/ghost-to-md

clean-architecture cli ghost-cms go markdown static-site-generator

Last synced: 18 days ago
JSON representation

CLI tool to convert Ghost CMS JSON exports to Markdown files with YAML front matter

Awesome Lists containing this project

README

          

# ghost-to-md

CLI tool to convert Ghost CMS JSON exports into Markdown files with YAML front matter.

[![Go](https://img.shields.io/badge/Go-1.26-00ADD8?logo=go)](https://go.dev/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Dependencies](https://img.shields.io/badge/Dependencies-1%20external-brightgreen.svg)](#design-decisions)

## Overview

**ghost-to-md** parses Ghost CMS JSON export files and generates individual Markdown files, each with YAML front matter containing metadata (title, date, tags, authors, etc.). The output is directly compatible with static site generators such as Hugo, Jekyll, Astro, and Zola.

Built with Clean Architecture principles and a single external dependency (`golang.org/x/net/html`), this tool handles Ghost-specific HTML structures including all 10 types of Ghost editor cards (`kg-*` classes).

## Features

- **Ghost JSON parsing** - Reads `db[0].data` structure with posts, tags, users, and join tables (`posts_tags`, `posts_authors`, `posts_meta`)
- **10 Ghost card types** - Image, gallery, code, bookmark, callout, toggle, embed, button, product, and file cards
- **YAML front matter** - Title, slug, date, tags, authors, description, featured image, canonical URL, visibility
- **Filtering** - By status (`published`/`draft`/`scheduled`), page inclusion, internal tag handling
- **URL rewriting** - Replace `__GHOST_URL__` placeholders with actual base URL
- **Single binary** - No runtime dependencies, cross-platform

## Architecture

```
┌─────────────────────────────────────────────────┐
│ cmd/main.go │
│ (manual DI, CLI flags) │
├─────────────────────────────────────────────────┤
│ usecase │
│ MigrateUseCase (orchestrator) │
├──────────┬──────────────────┬───────────────────┤
│ port │ port │ port │
│ Reader │ Converter │ Writer │
├──────────┼──────────────────┼───────────────────┤
│ ghostjson│ htmlconv │ fswriter │
│ (adapter)│ (adapter) │ (adapter) │
├──────────┴──────────────────┴───────────────────┤
│ domain │
│ Post, Tag, Author, FrontMatter, Resolver │
│ (pure Go, zero dependencies) │
└─────────────────────────────────────────────────┘
```

The project follows **Ports & Adapters** (hexagonal) architecture. The domain layer has zero external dependencies. Port interfaces are defined by the consumer (usecase layer), and adapters implement these interfaces. Dependencies always point inward.

## Installation

```bash
# Build from source
git clone https://github.com/P4suta/ghost-to-md.git
cd ghost-to-md
go build ./cmd/ghost-to-md
```

## Usage

```
Usage: ghost-to-md [options]

Options:
-output string Output directory path (default "./output")
-status string Filter by status (published/draft/scheduled)
-include-pages Include pages in output (default true)
-internal-tags Include internal (#) tags in front matter
-url string Replace __GHOST_URL__ with this base URL
-verbose Enable verbose logging
-version Show version
```

### Examples

```bash
# Basic conversion
ghost-to-md export.json

# Published posts only, with URL rewriting
ghost-to-md -status published -url https://example.com export.json

# Output to custom directory, include internal tags
ghost-to-md -output ./content/posts -internal-tags export.json

# Drafts only, verbose logging
ghost-to-md -status draft -verbose export.json
```

## Output Format

Each post generates a Markdown file named `YYYY-MM-DD-slug.md`:

```markdown
---
title: "My Blog Post: A Guide"
slug: my-blog-post-a-guide
date: 2026-01-15T09:00:00Z
lastmod: 2026-02-20T14:30:00Z
tags:
- Go
- Tutorial
authors:
- John Doe
description: "A comprehensive guide to building CLI tools"
featured_image: https://example.com/images/cover.jpg
---

## Introduction

This is the **converted** Markdown content...
```

## Project Structure

```
cmd/ghost-to-md/
main.go # Entry point, CLI flags, manual DI
internal/
domain/
model.go # Post, Tag, Author, Article, RawExport
resolver.go # ResolveRelationships (joins flat tables)
frontmatter.go # YAML front matter generation
naming.go # Filename from slug and date
port/
reader.go # ExportReader interface
converter.go # ContentConverter interface
writer.go # ArticleWriter interface
adapter/
ghostjson/
schema.go # JSON struct definitions, intBool type
reader.go # Ghost JSON deserializer
htmlconv/
converter.go # HTML-to-Markdown converter
handlers.go # Per-element handlers (headings, lists, etc.)
ghost_cards.go # Ghost kg-* card handlers (10 types)
fswriter/
writer.go # Writes .md files to disk
usecase/
migrate.go # MigrateUseCase orchestrator
options.go # MigrateOptions, ProgressReporter
testdata/
fixtures/minimal.json # Test fixture for Ghost JSON reader
golden/ # Golden files for HTML-to-Markdown tests
```

## Design Decisions

| Decision | Rationale |
|----------|-----------|
| **Single external dependency** (`golang.org/x/net/html`) | Minimizes supply chain risk; stdlib handles JSON, flags, filesystem, logging |
| **Hand-written YAML front matter** | Avoids pulling in a YAML library for write-only serialization; `escapeYAML` handles special characters |
| **`intBool` custom type** | Ghost JSON inconsistently encodes booleans as `true`/`false` or `0`/`1`; `intBool` unmarshals both via custom `UnmarshalJSON` |
| **Single-method interfaces** | `ExportReader`, `ContentConverter`, `ArticleWriter` each have one method, following Go's `io.Reader` convention |
| **Relationship resolution in domain** | `ResolveRelationships` joins flat Ghost tables (`posts_tags`, `posts_authors`, `posts_meta`) into structured `Post` objects, keeping JSON parsing separate from domain logic |
| **Golden file tests** | HTML-to-Markdown conversion tested against committed golden files; update with `UPDATE_GOLDEN=1 go test ./...` |

## Testing

```bash
go test ./... # Run all tests
go vet ./... # Static analysis
make test # Same as go test ./...
```

Testing approach:
- **Table-driven tests** - Standard Go idiom for parameterized test cases
- **Golden file tests** - Expected Markdown output committed to `testdata/golden/`; regenerate with `UPDATE_GOLDEN=1`
- **`testing/fstest`** - In-memory filesystem for writer tests
- **No external test dependencies** - All tests use stdlib `testing` package

## License

[MIT](LICENSE)

---

*This project includes a detailed [technical design document](ghost-to-md%20%E6%8A%80%E8%A1%93%E8%A8%AD%E8%A8%88%E6%9B%B8.md) written in Japanese.*