Intro

You probably don’t know this but shindakun.net has been effectively offline now for months. Currently, visiting it will only show a white page and a pop-up, no content is accessible. This is a side effect to my upgrading to a new server and moving away from Ghost as my platform of choice.

I love(d) Ghost but it just got too large to effectively run multiple sites on a small instance on Digital Ocean. I want to get the site back up and running but this time using Hugo. This seems like a great opportunity to make a new series of ATLG posts… plus it’s been a while since I’ve written any Go.

Post Data

Before shutting down the original server I exported a copy of the Ghost database as JSON. I should be able to use this to build out the needed Markdown files to make up the content of the site. Here’s the first post.

{
	"id": "60710b90705967038fe662d6",
	"uuid": "71ba3d71-ac18-4f33-82f7-1962baa83a07",
	"title": "db test",
	"slug": "db-test",
	"mobiledoc": "{\"version\":\"0.3.1\",\"markups\":[],\"atoms\":[],\"cards\":[[\"markdown\",{\"cardName\":\"card-markdown\",\"markdown\":\"\\n<strike>This is a db test</strike>.\\n\"}]],\"sections\":[[10,0]],\"ghostVersion\":\"3.0\"}",
	"html": "<!--kg-card-begin: markdown--><p><strike>This is a db test</strike>.</p>\n<!--kg-card-end: markdown-->",
	"comment_id": "2",
	"plaintext": "This is a db test.",
	"feature_image": null,
	"featured": 0,
	"type": "post",
	"status": "published",
	"locale": null,
	"visibility": "public",
	"email_recipient_filter": "none",
	"author_id": "60710b8d705967038fe66214",
	"created_at": "2004-08-09T19:11:20.000Z",
	"updated_at": "2004-08-09T19:11:20.000Z",
	"published_at": "2004-08-09T19:11:20.000Z",
	"custom_excerpt": null,
	"codeinjection_head": null,
	"codeinjection_foot": null,
	"custom_template": null,
	"canonical_url": null
},

Yes, I have some posts dating back to 2004! Back then I wrote my own custom blog software in PHP. It was a neat time, too bad none of the code survived. It almost makes me want to try and do it again only in Go… but I’ll resist that urge.

Quick and Dirty

Alright let’s write a program which will read our JSON and spit out a single post. First things first we need a struct that maps to our “database”. We get this by simply slapping the JSON into the awesome JSON to Go converter.

type GhostDatabase struct {
	Db []struct {
		Meta struct {
			ExportedOn int64  `json:"exported_on"`
			Version    string `json:"version"`
		} `json:"meta"`
		Data struct {
			Posts []struct {
				ID                   string      `json:"id"`
				UUID                 string      `json:"uuid"`
				Title                string      `json:"title"`
				Slug                 string      `json:"slug"`
				Mobiledoc            string      `json:"mobiledoc"`
				HTML                 string      `json:"html"`
				CommentID            string      `json:"comment_id"`
				Plaintext            string      `json:"plaintext"`
				FeatureImage         interface{} `json:"feature_image"`
				Featured             int         `json:"featured"`
				Type                 string      `json:"type"`
				Status               string      `json:"status"`
				Locale               interface{} `json:"locale"`
				Visibility           string      `json:"visibility"`
				EmailRecipientFilter string      `json:"email_recipient_filter"`
				AuthorID             string      `json:"author_id"`
				CreatedAt            time.Time   `json:"created_at"`
				UpdatedAt            time.Time   `json:"updated_at"`
				PublishedAt          time.Time   `json:"published_at"`
				CustomExcerpt        interface{} `json:"custom_excerpt"`
				CodeinjectionHead    interface{} `json:"codeinjection_head"`
				CodeinjectionFoot    interface{} `json:"codeinjection_foot"`
				CustomTemplate       interface{} `json:"custom_template"`
				CanonicalURL         interface{} `json:"canonical_url"`
			} `json:"posts"`
			PostsAuthors []struct {
				ID        string `json:"id"`
				PostID    string `json:"post_id"`
				AuthorID  string `json:"author_id"`
				SortOrder int    `json:"sort_order"`
			} `json:"posts_authors"`
			PostsMeta []interface{} `json:"posts_meta"`
			PostsTags []struct {
				ID        string `json:"id"`
				PostID    string `json:"post_id"`
				TagID     string `json:"tag_id"`
				SortOrder int    `json:"sort_order"`
			} `json:"posts_tags"`
			Roles []struct {
				ID          string    `json:"id"`
				Name        string    `json:"name"`
				Description string    `json:"description"`
				CreatedAt   time.Time `json:"created_at"`
				UpdatedAt   time.Time `json:"updated_at"`
			} `json:"roles"`
			RolesUsers []struct {
				ID     string `json:"id"`
				RoleID string `json:"role_id"`
				UserID string `json:"user_id"`
			} `json:"roles_users"`
			Settings []struct {
				ID        string      `json:"id"`
				Group     string      `json:"group"`
				Key       string      `json:"key"`
				Value     string      `json:"value"`
				Type      string      `json:"type"`
				Flags     interface{} `json:"flags"`
				CreatedAt time.Time   `json:"created_at"`
				UpdatedAt time.Time   `json:"updated_at"`
			} `json:"settings"`
			Tags []struct {
				ID                 string      `json:"id"`
				Name               string      `json:"name"`
				Slug               string      `json:"slug"`
				Description        interface{} `json:"description"`
				FeatureImage       interface{} `json:"feature_image"`
				ParentID           interface{} `json:"parent_id"`
				Visibility         string      `json:"visibility"`
				OgImage            interface{} `json:"og_image"`
				OgTitle            interface{} `json:"og_title"`
				OgDescription      interface{} `json:"og_description"`
				TwitterImage       interface{} `json:"twitter_image"`
				TwitterTitle       interface{} `json:"twitter_title"`
				TwitterDescription interface{} `json:"twitter_description"`
				MetaTitle          interface{} `json:"meta_title"`
				MetaDescription    interface{} `json:"meta_description"`
				CodeinjectionHead  interface{} `json:"codeinjection_head"`
				CodeinjectionFoot  interface{} `json:"codeinjection_foot"`
				CanonicalURL       interface{} `json:"canonical_url"`
				AccentColor        interface{} `json:"accent_color"`
				CreatedAt          time.Time   `json:"created_at"`
				UpdatedAt          time.Time   `json:"updated_at"`
			} `json:"tags"`
			Users []struct {
				ID              string      `json:"id"`
				Name            string      `json:"name"`
				Slug            string      `json:"slug"`
				Password        string      `json:"password"`
				Email           string      `json:"email"`
				ProfileImage    string      `json:"profile_image"`
				CoverImage      interface{} `json:"cover_image"`
				Bio             interface{} `json:"bio"`
				Website         interface{} `json:"website"`
				Location        interface{} `json:"location"`
				Facebook        interface{} `json:"facebook"`
				Twitter         interface{} `json:"twitter"`
				Accessibility   string      `json:"accessibility"`
				Status          string      `json:"status"`
				Locale          interface{} `json:"locale"`
				Visibility      string      `json:"visibility"`
				MetaTitle       interface{} `json:"meta_title"`
				MetaDescription interface{} `json:"meta_description"`
				Tour            interface{} `json:"tour"`
				LastSeen        time.Time   `json:"last_seen"`
				CreatedAt       time.Time   `json:"created_at"`
				UpdatedAt       time.Time   `json:"updated_at"`
			} `json:"users"`
		} `json:"data"`
	} `json:"db"`
}

Perfect. Note that not all of the struct is strictly required but I’ll leave it in for now. Who knows I might write something to pull the tags for posts too.

Reading the File

To start with we’re going to open our file. Note that this is indeed a quick and dirty version, maybe we’ll add some proper error handling later.

	file, err := os.Open("shindakun-dot-net.ghost.2022-03-18-22-02-58.json")
	if err != nil {
		fmt.Println(err)
	}

Once opened we need to read the file into memory as a []byte.

	b, err := io.ReadAll(file)
	if err != nil {
		fmt.Println(err)
	}

Now we’ll declare our database variable, db. Using json.Unmarshal() we convert the JSON into the struct we will be using.

	var db GhostDatabase

	err = json.Unmarshal(b, &db)
	if err != nil {
		fmt.Println(err)
	}

Finally, we print out the text of the first post.

	fmt.Printf("%#v", db.Db[0].Data.Posts[0].HTML)

This leaves us with the output

<!--kg-card-begin: markdown--><p><strike>This is a db test</strike>.</p>\n<!--kg-card-end: markdown-->

Next Time

This part of our program is relatively easy. We don’t want the HTML though, what I’d rather do is pull the Markdown out of the mobiledoc value.

"mobiledoc": "{\"version\":\"0.3.1\",\"markups\":[],\"atoms\":[],\"cards\":[[\"markdown\",{\"cardName\":\"card-markdown\",\"markdown\":\"\\n<strike>This is a db test</strike>.\\n\"}]],\"sections\":[[10,0]],\"ghostVersion\":\"3.0\"}",

Which, as luck would have it, is just an embedded JSON object. It looks like we’ll need to do some cleaning up of the object removing escaped quotes. Once cleaned up it should look similar to

{
    "version": "0.3.1",
    "markups": [],
    "atoms": [],
    "cards": [
        [
            "markdown",
            {
                "cardName": "card-markdown",
                "markdown": "\n<strike>This is a db test</strike>.\n"
            }
        ]
    ],
    "sections": [
        [
            10,
            0
        ]
    ],
    "ghostVersion": "3.0"
}

I haven’t looked to closely at the database but I’m hoping when I wrote my posts in Ghost they all were in a single card-markdown. Oh well, we’ll cross that bridge eventually.

Until next time!


Enjoy this post?
How about buying me a coffee?

Code Listing

package main

import (
	"encoding/json"
	"fmt"
	"io"
	"os"
	"time"
)

type GhostDatabase struct {
	Db []struct {
		Meta struct {
			ExportedOn int64  `json:"exported_on"`
			Version    string `json:"version"`
		} `json:"meta"`
		Data struct {
			Posts []struct {
				ID                   string      `json:"id"`
				UUID                 string      `json:"uuid"`
				Title                string      `json:"title"`
				Slug                 string      `json:"slug"`
				Mobiledoc            string      `json:"mobiledoc"`
				HTML                 string      `json:"html"`
				CommentID            string      `json:"comment_id"`
				Plaintext            string      `json:"plaintext"`
				FeatureImage         interface{} `json:"feature_image"`
				Featured             int         `json:"featured"`
				Type                 string      `json:"type"`
				Status               string      `json:"status"`
				Locale               interface{} `json:"locale"`
				Visibility           string      `json:"visibility"`
				EmailRecipientFilter string      `json:"email_recipient_filter"`
				AuthorID             string      `json:"author_id"`
				CreatedAt            time.Time   `json:"created_at"`
				UpdatedAt            time.Time   `json:"updated_at"`
				PublishedAt          time.Time   `json:"published_at"`
				CustomExcerpt        interface{} `json:"custom_excerpt"`
				CodeinjectionHead    interface{} `json:"codeinjection_head"`
				CodeinjectionFoot    interface{} `json:"codeinjection_foot"`
				CustomTemplate       interface{} `json:"custom_template"`
				CanonicalURL         interface{} `json:"canonical_url"`
			} `json:"posts"`
			PostsAuthors []struct {
				ID        string `json:"id"`
				PostID    string `json:"post_id"`
				AuthorID  string `json:"author_id"`
				SortOrder int    `json:"sort_order"`
			} `json:"posts_authors"`
			PostsMeta []interface{} `json:"posts_meta"`
			PostsTags []struct {
				ID        string `json:"id"`
				PostID    string `json:"post_id"`
				TagID     string `json:"tag_id"`
				SortOrder int    `json:"sort_order"`
			} `json:"posts_tags"`
			Roles []struct {
				ID          string    `json:"id"`
				Name        string    `json:"name"`
				Description string    `json:"description"`
				CreatedAt   time.Time `json:"created_at"`
				UpdatedAt   time.Time `json:"updated_at"`
			} `json:"roles"`
			RolesUsers []struct {
				ID     string `json:"id"`
				RoleID string `json:"role_id"`
				UserID string `json:"user_id"`
			} `json:"roles_users"`
			Settings []struct {
				ID        string      `json:"id"`
				Group     string      `json:"group"`
				Key       string      `json:"key"`
				Value     string      `json:"value"`
				Type      string      `json:"type"`
				Flags     interface{} `json:"flags"`
				CreatedAt time.Time   `json:"created_at"`
				UpdatedAt time.Time   `json:"updated_at"`
			} `json:"settings"`
			Tags []struct {
				ID                 string      `json:"id"`
				Name               string      `json:"name"`
				Slug               string      `json:"slug"`
				Description        interface{} `json:"description"`
				FeatureImage       interface{} `json:"feature_image"`
				ParentID           interface{} `json:"parent_id"`
				Visibility         string      `json:"visibility"`
				OgImage            interface{} `json:"og_image"`
				OgTitle            interface{} `json:"og_title"`
				OgDescription      interface{} `json:"og_description"`
				TwitterImage       interface{} `json:"twitter_image"`
				TwitterTitle       interface{} `json:"twitter_title"`
				TwitterDescription interface{} `json:"twitter_description"`
				MetaTitle          interface{} `json:"meta_title"`
				MetaDescription    interface{} `json:"meta_description"`
				CodeinjectionHead  interface{} `json:"codeinjection_head"`
				CodeinjectionFoot  interface{} `json:"codeinjection_foot"`
				CanonicalURL       interface{} `json:"canonical_url"`
				AccentColor        interface{} `json:"accent_color"`
				CreatedAt          time.Time   `json:"created_at"`
				UpdatedAt          time.Time   `json:"updated_at"`
			} `json:"tags"`
			Users []struct {
				ID              string      `json:"id"`
				Name            string      `json:"name"`
				Slug            string      `json:"slug"`
				Password        string      `json:"password"`
				Email           string      `json:"email"`
				ProfileImage    string      `json:"profile_image"`
				CoverImage      interface{} `json:"cover_image"`
				Bio             interface{} `json:"bio"`
				Website         interface{} `json:"website"`
				Location        interface{} `json:"location"`
				Facebook        interface{} `json:"facebook"`
				Twitter         interface{} `json:"twitter"`
				Accessibility   string      `json:"accessibility"`
				Status          string      `json:"status"`
				Locale          interface{} `json:"locale"`
				Visibility      string      `json:"visibility"`
				MetaTitle       interface{} `json:"meta_title"`
				MetaDescription interface{} `json:"meta_description"`
				Tour            interface{} `json:"tour"`
				LastSeen        time.Time   `json:"last_seen"`
				CreatedAt       time.Time   `json:"created_at"`
				UpdatedAt       time.Time   `json:"updated_at"`
			} `json:"users"`
		} `json:"data"`
	} `json:"db"`
}

func main() {
	fmt.Println("ghost2hugo")

	file, err := os.Open("shindakun-dot-net.ghost.2022-03-18-22-02-58.json")
	if err != nil {
		fmt.Println(err)
	}
	
	b, err := io.ReadAll(file)
	if err != nil {
		fmt.Println(err)
	}

	var db GhostDatabase

	err = json.Unmarshal(b, &db)
	if err != nil {
		fmt.Println(err)
	}

	fmt.Printf("%#v", db.Db[0].Data.Posts[0].HTML)
}