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)
}