Post

Seamless Migration from Medium to Self-Hosted Website

Migrating Medium content to GitHub Pages (with Jekyll/Chirpy)

Seamless Migration from Medium to Self-Hosted Website

ℹ️ℹ️ℹ️ The following content is translated by OpenAI.

Click here to view the original Chinese version. | 點此查看本文中文版


Seamless Migration from Medium to Self-Hosted Website

Migrating Medium content to GitHub Pages (with Jekyll/Chirpy)

[zhgchg.li](http://zhgchg.li){:target="_blank"}

zhgchg.li

Background

After running my Medium account for four years and accumulating over 65 articles, I realized I had invested nearly 1000 hours into it. I initially chose Medium for its simplicity and convenience, allowing me to focus on writing without worrying about other aspects. Before that, I had tried self-hosting with WordPress, but I found myself spending too much time on environment setup, styling, and plugins. No matter how I adjusted things, I was never satisfied. Once I got everything set up, I discovered slow loading times, poor reading experiences, and an unintuitive backend for writing articles, which led to infrequent updates.

As I wrote more articles on Medium and gained some traffic and followers, I began to desire control over my content rather than relying on a third-party platform (e.g., losing all my work if Medium shut down). Therefore, I started looking for a secondary backup site two years ago. I planned to continue using Medium while also publishing content on a site I could control. At that time, I found a solution in Google Sites, but honestly, it was only suitable as a personal “portal” with limited article writing features, making it impossible to transfer all my hard work.

Ultimately, I returned to self-hosting, but this time I opted for a static website instead of a dynamic one (e.g., WordPress). While static sites support fewer features, I wanted a writing function and a clean, customizable browsing experience—nothing more!

The workflow for a static website is straightforward: write articles locally in Markdown format, then convert them into static web pages using a static site generator and upload them to the server. This results in fast-loading static pages and a great browsing experience!

Writing in Markdown format allows for compatibility across various platforms. If you’re not used to it, you can find online or offline Markdown writing tools that provide an experience similar to writing directly on Medium.

In summary, this solution meets my needs for a smooth browsing experience and a convenient writing interface.

Results

[zhgchg.li](http://zhgchg.li){:target="_blank"}

zhgchg.li

  • Supports customizable display styles
  • Allows for custom page adjustments (e.g., inserting ads, JS widgets)
  • Supports custom pages
  • Allows for custom domain names
  • Fast loading of static pages and great browsing experience
  • Uses Git for version control, preserving all historical versions of articles
  • Fully automated synchronization of Medium articles to the website

2025/01/18 Update 🎉🎉🎉

Environment and Tools

Installing Ruby

Here’s an example based on my environment; please Google how to install Ruby for other operating systems.

  • macOS Monterey 12.1
  • rbenv
  • ruby 2.6.5

Install Brew

1
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

Enter the above command in Terminal to install Brew.

Install rbenv

1
brew install rbenv ruby-build

Although macOS comes with Ruby, it’s recommended to use rbenv to install a separate version of Ruby. Enter the above command in Terminal to install rbenv.

1
rbenv init

Enter the above command in Terminal to initialize rbenv.

  • Close and reopen Terminal.

In Terminal, enter rbenv to check if the installation was successful!

Success!

Use rbenv to Install Ruby

1
rbenv install 2.6.5

Enter the above command in Terminal to install Ruby version 2.6.5.

1
rbenv global 2.6.5

Enter the above command in Terminal to switch the Ruby version used in Terminal from the system version to the rbenv version.

In Terminal, enter rbenv versions to check the current settings:

In Terminal, enter ruby -v to check the current Ruby version and gem -v to check the current RubyGems status:

*Ruby should also have installed RubyGems by default.

Success!

Install Jekyll, Bundler, and ZMediumToMarkdown

1
gem install jekyll bundler ZMediumToMarkdown

Enter the above command in Terminal to install Jekyll, Bundler, and ZMediumToMarkdown.

Done!

Create a Jekyll Blog from a Template

The default Jekyll Blog style is very simple. You can find a style you like from the following websites and apply it:

The installation method generally uses gem-based themes, but some repositories offer installation via Fork or even one-click installation. Each template may have different installation methods, so please refer to the template’s usage instructions.

Additionally, please note that since we are deploying to GitHub Pages, not all templates are compatible according to the official documentation.

Chirpy Template

I will demonstrate using the template Chirpy, which provides a very straightforward one-click installation method.

Other templates rarely offer similar one-click installations. If you’re unfamiliar with Jekyll or GitHub Pages, starting with this template is a good way to get acquainted. I may update this article later to discuss other template installation methods.

You can also find templates on GitHub that can be directly Forked (e.g., al-folio) for immediate use. If none of these options work, you will need to manually install the template and research how to set up GitHub Pages deployment. I’ve done some preliminary research on this but haven’t succeeded yet; I’ll return to this article to share any findings later.

Create a Git Repo from the Git Template

https://github.com/cotes2020/chirpy-starter/generate

  • Repository name: GithubAccount/OrganizationName.github.io ( Make sure to use this format )
  • Ensure you select “Public” for the repository.

Click “Create repository from template” to complete the repo creation.

Git Clone the Project

1
git clone git@github.com:zhgchgli0718/zhgchgli0718.github.io.git

Clone the repo you just created.

Run bundle to install dependencies:

Run bundle lock — add-platform x86_64-linux to lock the version:

Modify Website Settings

Open the _config.yml configuration file to make your settings:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# The Site Configuration

# Import the theme
theme: jekyll-theme-chirpy

# Change the following value to '/PROJECT_NAME' ONLY IF your site type is GitHub Pages Project sites
# and doesn't have a custom domain.
# baseurl: ''

# The language of the webpage › http://www.lingoes.net/en/translator/langcode.htm
# If it has the same name as one of the files in folder `_data/locales`, the layout language will also be changed,
# otherwise, the layout language will use the default value of 'en'.
lang: en

# Additional parameters for datetime localization, optional. › https://github.com/iamkun/dayjs/tree/dev/src/locale
prefer_datetime_locale:

# Change to your timezone › http://www.timezoneconverter.com/cgi-bin/findzone/findzone
timezone:

# jekyll-seo-tag settings › https://github.com/jekyll/jekyll-seo-tag/blob/master/docs/usage.md
# ↓ --------------------------

title: ZhgChgLi                          # the main title

tagline: Live a life you will remember.   # it will display as the sub-title

description: >-                        # used by seo meta and the atom feed
    ZhgChgLi iOS Developer, eager to learn, teaching and learning, loves movies/TV shows/Western music/sports/life

# fill in the protocol & hostname for your site, e.g., 'https://username.github.io'
url: 'https://zhgchg.li'

github:
  username: ZhgChgLi             # change to your github username

twitter:
  username: zhgchgli            # change to your twitter username

social:
  # Change to your full name.
  # It will be displayed as the default author of the posts and the copyright owner in the Footer
  name: ZhgChgLi
  email: zhgchgli@gmail.com             # change to your email address
  links:
    - https://medium.com/@zhgchgli
    - https://github.com/ZhgChgLi
    - https://www.linkedin.com/in/zhgchgli

google_site_verification:               # fill in to your verification string

# ↑ --------------------------
# The end of `jekyll-seo-tag` settings

google_analytics:
  id: G-6WZJENT8WR                 # fill in your Google Analytics ID
  # Google Analytics pageviews report settings
  pv:
    proxy_endpoint:   # fill in the Google Analytics superProxy endpoint of Google App Engine
    cache_path:       # the local PV cache data, friendly to visitors from GFW region

# Prefer color scheme setting.
#
# Note: Keep empty will follow the system prefer color by default,
# and there will be a toggle to switch the theme between dark and light
# on the bottom left of the sidebar.
#
# Available options:
#
#     light  - Use the light color scheme
#     dark   - Use the dark color scheme
#
theme_mode:   # [light|dark]

# The CDN endpoint for images.
# Notice that once it is assigned, the CDN url
# will be added to all image (site avatar & posts' images) paths starting with '/'
#
# e.g. 'https://cdn.com'
img_cdn:

# the avatar on sidebar, support local or CORS resources
avatar: '/assets/images/zhgchgli.jpg'

# boolean type, the global switch for ToC in posts.
toc: true

comments:
  active: disqus        # The global switch for posts comments, e.g., 'disqus'.  Keep it empty means disable
  # The active options are as follows:
  disqus:
    shortname: zhgchgli    # fill with the Disqus shortname. › https://help.disqus.com/en/articles/1717111-what-s-a-shortname
  # utterances settings › https://utteranc.es/
  utterances:
    repo:         # <gh-username>/<repo>
    issue_term:   # < url | pathname | title | ...>
  # Giscus options › https://giscus.app
  giscus:
    repo:             # <gh-username>/<repo>
    repo_id:
    category:
    category_id:
    mapping:          # optional, default to 'pathname'
    input_position:   # optional, default to 'bottom'
    lang:             # optional, default to the value of `site.lang`

# Self-hosted static assets, optional › https://github.com/cotes2020/chirpy-static-assets
assets:
  self_host:
    enabled:      # boolean, keep empty means false
    # specify the Jekyll environment, empty means both
    # only works if `assets.self_host.enabled` is 'true'
    env:          # [development|production]

paginate: 10

# ------------ The following options are not recommended to be modified ------------------

kramdown:
  syntax_highlighter: rouge
  syntax_highlighter_opts:   # Rouge Options › https://github.com/jneen/rouge#full-options
    css_class: highlight
    # default_lang: console
    span:
      line_numbers: false
    block:
      line_numbers: true
      start_line: 1

collections:
  tabs:
    output: true
    sort_by: order

defaults:
  - scope:
      path: ''          # An empty string here means all files in the project
      type: posts
    values:
      layout: post
      comments: true    # Enable comments in posts.
      toc: true         # Display TOC column in posts.
      # DO NOT modify the following parameter unless you are confident enough
      # to update the code of all other post links in this project.
      permalink: /posts/:title/
  - scope:
      path: _drafts
    values:
      comments: false
  - scope:
      path: ''
      type: tabs             # see `site.collections`
    values:
      layout: page
      permalink: /:title/
  - scope:
      path: assets/img/favicons
    values:
      swcache: true
  - scope:
      path: assets/js/dist
    values:
      swcache: true

sass:
  style: compressed

compress_html:
  clippings: all
  comments: all
  endings: all
  profile: false
  blanklines: false
  ignore:
    envs: [development]

exclude:
  - '*.gem'
  - '*.gemspec'
  - tools
  - README.md
  - LICENSE
  - gulpfile.js
  - node_modules
  - package*.json

jekyll-archives:
  enabled: [categories, tags]
  layouts:
    category: category
    tag: tag
  permalinks:
    tag: /tags/:name/
    category: /categories/:name/

Please replace the settings with your content as per the comments.

⚠️ Any adjustments to _config.yml require restarting the local site for the changes to take effect!

Preview the Website

Once the dependencies are installed, you can run bundle exec jekyll s to start the local site:

Copy the URL http://127.0.0.1:4000/ and paste it into your browser to open it.

Local preview successful!

Keep this Terminal open, and the local site will remain active. The Terminal will continuously update the site access logs, making it easier for us to debug.

We can open another new Terminal for subsequent operations.

Jekyll Directory Structure

Depending on the template, there may be different folders and configuration files. The article directory is:

  • _posts/: Articles will be placed in this directory. Article file naming convention: YYYY-MM-DD-article-file-name.md
  • assets/: The resource directory for the website, where images for the site or images within articles should be placed.

Other directories like _includes, _layouts, _sites, _tabs, etc., allow for advanced customization.

Jekyll uses Liquid as its page template engine, where page templates are composed in a manner similar to inheritance:

Users can freely customize their pages. The engine first checks if the user has created a corresponding customization file for the page. If not, it checks if a template exists. If neither is available, it will display the original Jekyll style.

This means we can easily customize any page by simply creating a file with the same name in the corresponding directory!

Creating/Editing Articles

  • We can start by deleting all example article files in the _posts/ directory.

Using Visual Studio Code (free) or Typora (paid), we can create Markdown files. Here, we will use Visual Studio Code as an example:

  • Article file naming convention: YYYY-MM-DD-article-file-name.md
  • It is recommended to use English for the file name (for SEO optimization), as this name will be part of the URL path.

Article Top Meta:

1
2
3
4
5
6
7
8
9
---
layout: post
title:  "Hello"
description: ZhgChgLi's first article
date:   2022-07-16 10:03:36 +0800
categories: Jekyll Life
author: ZhgChgLi
tags: [ios]
---
  • layout: post
  • title: Article title (og:title)
  • description: Article description (og:description)
  • date: Article publication date (cannot be in the future)
  • author: Author (meta:author)
  • tags: Tags (can be multiple)
  • categories: Category (single, use spaces to separate parent and child categories, e.g., Jekyll Life -> Jekyll directory under Life directory)

Article Content:

Write using Markdown format:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
---
layout: post
title:  "Hello"
description: ZhgChgLi's first article
date:   2022-07-16 10:03:36 +0800
categories: Jekyll Life
author: ZhgChgLi
tags: [ios]
---
# HiHi!
Hello there!
I am **ZhgChgLi**
Image:
![](/assets/post_images/DSC_2297.jpg)
> _If you have any questions or suggestions, feel free to [contact me](https://www.zhgchg.li/contact)._

Result:

⚠️ Article adjustments do not require restarting the website; changes will be rendered immediately. If the modified content does not appear after a while, it may be due to formatting errors in the article content, which can be checked in the Terminal for reasons.

Downloading Articles from Medium and Converting to Markdown for Jekyll

With a basic understanding of Jekyll, we can move forward by using the ZMediumToMarkdown tool to download existing articles from Medium and convert them into Markdown format to place in our Blog folder.

After navigating to the blog directory, run the following command to download all articles from the specified Medium user:

1
ZMediumToMarkdown -j your Medium account

Wait for all articles to finish downloading…

If you encounter any download issues or unexpected errors, feel free to contact me. This downloader is developed by me (see development insights), and I can help you resolve issues quickly.

Once the download is complete, you can preview the results on your local site.

Done!! We have seamlessly imported Medium articles into Jekyll!

You can check if the articles are formatted correctly and if any images are missing. If you find any issues, please report them to me for assistance.

Uploading Content to the Repo

Once the local preview looks good, we need to push the content to the GitHub Repo.

Use the following Git commands in order:

1
2
3
git add .
git commit -m "update post"
git push

After pushing, return to GitHub to see the Actions running the CD:

Wait about 5 minutes…

Deployment complete!

Initial Deployment Settings

After the first deployment, you need to change the following settings:

Otherwise, visiting the website will only show:

1
--- layout: home # Index page ---

After clicking “Save,” it won’t take effect immediately; you need to return to the “Actions” page and wait for the deployment to complete again.

Once redeployed, you will successfully access the website:

Demo -> zhgchg.li

Now you have your own free Jekyll personal blog!

About Deployment

Every time you push content to the Repo, it will trigger a redeployment. You need to wait for the deployment to succeed for the changes to take effect.

Binding a Custom Domain

If you don’t like the zhgchgli0718.github.io GitHub URL, you can purchase a domain you like from Namecheap or register a free .tk domain using Dot.tk.

After purchasing the domain, go to the domain management panel:

Add the following four Type A Record entries:

1
2
3
4
A Record @ 185.199.108.153
A Record @ 185.199.109.153
A Record @ 185.199.110.153
A Record @ 185.199.111.153

After adding the settings in the domain management panel, return to the GitHub Repo Settings:

Enter your domain in the Custom domain field and click “Save.”

Once the DNS is propagated, you can replace the original github.io URL with zhgchg.li.

⚠️ DNS settings may take at least 5 minutes to 72 hours to take effect. If verification fails, please try again later.

Cloud-Based, Fully Automated Medium Sync Mechanism

Do you find it cumbersome to manually run ZMediumToMarkdown on your computer every time there’s a new article and then push it to the project?

ZMediumToMarkdown also offers a convenient GitHub Action feature that allows you to automate the synchronization of Medium articles to your website without needing to use your computer.

Go to the Repo’s Actions settings:

Click “New workflow”

Click “set up a workflow yourself”

  • Change the file name to: ZMediumToMarkdown.yml
  • The file content should be as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
name: ZMediumToMarkdown
on:
  workflow_dispatch:
  schedule:
    - cron: "10 1 15 * *" # At 01:10 on day-of-month 15.

jobs:
  ZMediumToMarkdown:
    runs-on: ubuntu-latest
    steps:
    - name: ZMediumToMarkdown Automatic Bot
      uses: ZhgChgLi/ZMediumToMarkdown@main
      with:
        command: '-j your Medium account'
  • cron: Set the execution frequency (weekly? monthly? daily?). Here, it is set to automatically execute at 1:15 AM on the 15th of every month.
  • command: Enter your Medium account after -j.

Click “Start commit” in the upper right corner -> “Commit new file”

The GitHub Action has been created.

After creation, return to Actions, and you will see the ZMediumToMarkdown Action.

In addition to automatic execution at scheduled times, you can also manually trigger execution by following these steps:

Actions -> ZMediumToMarkdown -> Run workflow -> Run workflow.

After execution, ZMediumToMarkdown will directly run the script to synchronize Medium articles to the Repo via GitHub Action:

Once completed, it will also trigger a redeployment, and after redeployment, the latest content will appear on the website. 🚀

No manual operation required! This means you can continue to update Medium articles in the future, and the script will automatically synchronize the content to your website from the cloud!

My Blog Repo

If you have any questions or suggestions, feel free to contact me.


This article was first published on Medium ➡️ Click Here

Automatically converted and synchronized using ZMediumToMarkdown and Medium-to-jekyll-starter.

Improve this page on Github.

Buy me a beer

1,305 Total Views
Last Statistics Date: 2025-03-11 | 929 Views on Medium.
This post is licensed under CC BY 4.0 by the author.