經過疲憊滿滿的外科實習後,回來看自己的部落格,才發現很久沒有更新內容了。不但有一些伺服器管理的細節忘記,也覺得以前為了維護網站運作順暢,載入快速的各種細項設定太過繁瑣,不利於在未來疲憊繁忙的從醫之路上,維持心力更新文章。

以前習慣的網站架設平台是Wordpress, 然而,自從學會Markdown語法後,發現從Markdown編輯器複製文字到Wordpress,總是容易產生問題,無法順暢的銜接整個寫作流程。而且,因為沒有時間精力設計重新打造整個佈景主題以及擴充套件,變成要用別人寫好的東西拼湊,導致整個站台會有部分細節無法最佳化,以及冗余的語法躲在各個角落影響站台運行。

基於以上想法,當換站之後,偷得一點閒暇,便研究起了之前聽說輕量快速的Ghost平台,做了一點功課之後,花了一兩天時間便把整個網站轉換過去了,對於結果十分滿意,以下是整個轉換過程的紀錄、兩個平台的優缺點比較,以及個人的一些評述。

Ghost vs Wordpress 使用比較評述

用了幾天之後,目前主要覺得Ghost的優點有:

  1. 使用node.js 撰寫,在效能配備較差的主機上較順暢

  2. 後台撰寫介面乾淨簡潔,完整支持Markdown

    Writing-interface

  3. 完整支持自訂Meta & Opengraph等標籤

    Meta-customize-2

  4. 可預覽分享至社群網站的樣貌(封面圖片、摘要)

    Share-to-social-1

  5. 支援Google AMP可自訂模版

不過Ghost畢竟才剛起步開發,相較於Wordpress仍有許多不足的,像是:

  1. 不支援分類,僅支援標籤

  2. 導覽列貌似只支援一層

  3. 缺少大量的外掛來擴充功能

  4. 頁面元件自訂性較仰賴程式碼修改,不若Wordpress小工具編輯方便

  5. 與其他平台流通性仍低,匯入不易

我在匯入Wordpress文章時,其實標籤跟分類全部跑掉,所以我必須一篇一篇手動檢視重新上標籤,這是一件惱人的事。另外Wordpress因為外掛數量極多,許多有用的功能都可以透過外掛添加,如圖片上傳自動壓縮優化、自動分享至多個社群平台等等,Ghost目前還缺少這個部分,不過App架構正在開發中,未來或許可以擴充更多功能。

依照設計哲學,Ghost是Wordpress開發者最初理念再度經簡化的產物,寫作、出版、分享,將這一整個流程簡化並順暢執行,構建簡潔不費力的個人部落格平台,是Ghost的目標,也確實完美達成。而Wordpress發展到現在,擁有更完善成熟的架構,擴充性強,也因此泛用性高,從個人部落格到商業形象網站都可以用,但是與Ghost相比在單純寫作出版的目標上略顯臃腫。

轉換事前準備

備份文章與圖片

首先當然要備份資料,主要是圖片跟文章。文章的話Ghost官方有提供一個Wordpress擴充套件可以轉出Ghost平台可以讀的JSON檔,但是看更新紀錄是只更新到五年前且僅在Wordpress舊版測試過,我自己在Wordpress 4.8 無法成功轉出。後來我在Github找到一個npm工具wp2ghost,他是吃wordpress 本身可以匯出的XML備份檔然後在轉成JSON檔。

Wordpress [工具]-> [匯出] -> wordpress-XXXXX.xml

下載wp2ghost.js, 然後依照說明npm 安裝後使用

node bin/wp2ghost.js > ghost.json

在轉出以前,可以利用編輯器的搜尋取代功能,先改XML內隨著網站變動的內容。像因為我從wordpress轉換到ghost, 圖片的路徑位址會跑掉,所以我必須尋找取代如下:

https://example.com/wp-content/uploads -> 
https://example.com/content/images

PS: wordpress路徑
|-wordpress
|--wp-admin
|--wp-content
|---uploads  //所有上傳的圖片資料
|--index.php
|--etc......

Ghost如果放在/var/www/ghost 底下
|-ghost
|--content
|---apps
|---data
|---images   //Ghost所有上傳的圖片儲存處
|---logs
|---thmes
|--system
|--etc......

然而,我在terminal輸入指令轉換時碰到問題,wp2ghost.js無法成功轉換,幸虧有輸出在哪一行出現問題,所以就開Sublime text 起來看XML檔案,一開始看不懂錯誤訊息指定的那一行出什麼問題,結果比對作者在原始碼資料夾內附的範例檔案後,發現可能是舊的wordpress版本輸出的XML檔案每篇文章都會空一行間隔

<item>
......文章資料內容
</item>
///舊版在這邊有空一行,新版沒有
<item>
......文章資料內容
</item>

所以我用Sublime text 搜尋取代全部加入空行後,果然wp2ghost就成功轉過去了,剩下不成功的都是特殊字元,自行增刪處理就可以了。

圖片的話就是依照上面路徑所列,自行下載圖片後在重新上傳到Ghost相對應的資料夾內即可。

留言回覆轉移

這部份比較麻煩,因為Ghost目前沒有自己的留言系統,未來也可能不想要有(我懷疑有多少人會用Wordpress內建的留言系統嗎XD),所以官方建議是先在Wordpress啟用Disqus以後,在轉移原本的Wordpress留言到Disqus內,然後在新的Ghost站台加入Disqus留言系統,原本的留言就會回復了,所以如果本來就有在用Disqus留言系統,那基本上是小菜一碟。

網址轉移(?)

或許有人想問網站文章產生的網址結構,從舊的wordpress搬到ghost可以保留嗎?很抱歉,我這部份是直接放棄(反正我的讀者群比較少,泣~)再加上wordpress原先產生的網址是中文照搬,可是ghost的網址系統會自動把中文轉成羅馬拼音,又加深了保留連結的困難度,所以除非有更好的辦法,否則建議有龐大讀者群不想失去太多的話不要輕易搬移。

Ghost安裝

Ghost 依照官方文件要求的環境如下:

  • Ubuntu 16.04
  • MySQL
  • NGINX (minimum of 1.9.5 for SSL)
  • Systemd
  • Node v6 installed via NodeSource
  • At least 1GB memory (swap can be used)
  • A non-root user for running ghost commands

我的網站是放在Google Cloud platform, 因為各家VPS同樣需求的操作應該都大同小異,所以這邊僅簡單紀錄一下:

1. 製作snapshot,包含原先wordpress舊網站的系統
2. 刪除VM個體
3. 重新製作一個VM個體,根據需求選擇配備地點線路
4. Linux開機,ssh連入, 新增一個non-root user(不用特地命名ghost)
5. 安裝nginx, mysql-server, 設定好root帳號密碼
6. curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash //加入node套件源
7. sudo apt-get install -y nodejs //安裝Node.js
8. sudo npm i -g ghost-cli //安裝Ghost
9. sudo mkdir -p /var/www/ghost
10. sudo chown ghost:ghost /var/www/ghost //官方文件寫要使用自己創的帳號,但是我實際跑起來會出現問題......用Ghost反而沒問題
11. cd /var/www/ghost && ghost install //開始安裝囉

安裝過程中會問你Mysql的資料庫帳號密碼,跟wordpress一樣,另外也整合了Let's encrypt 的功能,可以直接幫你設定好憑證跟nginx讓網站使用https,如果要套用https的話一開始的網址就記得要填https開頭喔。

Ghost基本設定

安裝好以後,就可以從網址連進去跑跑看了,一開始看到的應該是Ghost預設的佈景Casper以及一些預設範例內容,可以連到https://example.com/ghost進行後台設定。創建帳號時輸入Gmail地址好像會自動讀取Gmail的大頭貼。

Ghost的能設定的項目相較wordpress簡單,我大概設定了以下幾項:

  • Labs -> Deletes all content 先把原先的範例刪除
  • Labs -> Import content 把準備好從wordpress轉出的json檔輸進來
  • General -> 輸入網站標題、描述,上傳Publication icon(favicon)
  • Design -> 輸入導覽列項目及連結,以及上傳佈景主題啟用

其他的就依照需求填入了。

Ghost進階修改

從結構上來說,其實Ghost的佈景主題元件構成很像wordpress, 不過也可以理解,因為起頭者是wordpress的前執行長。

可以參考Ghost的官方Documentation, 結構從命名上很好理解,舉例來說,post.hbs 就是程式會依照其輸出每篇文章的模版,所以要加Disqus 留言系統的話就是要放在這裡。要在sidebar裡添加小工具的話就是找sidebar.hbs,依此類推。

加入Disqus以及Facebook留言系統

以下是我放在post.hbs裡facebook 留言系統以及Disqus系統的代碼片段

//以下這段放在前面任意處即可
<div id="fb-root"></div>
<script>(function(d, s, id) {
  var js, fjs = d.getElementsByTagName(s)[0];
  if (d.getElementById(id)) return;
  js = d.createElement(s); js.id = id;
  js.src = 'XXXXXX';  //請自行找facebook留言社群外掛輸出
  fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));</script>

//以下這段要放在{{#post}} - {{/post}}裡面
        {{content}} //這是Ghost用來輸出文章內文的標記,所以之後就是可以放留言的地方
            <div class="fb-comments" data-href="{{@blog.url}}{{url}}" data-width="100%" data-numposts="5"></div>
            <div class="comments-area">
                <div class="comments-inside">
                <div id="disqus_thread"></div>
                <noscript>Please enable JavaScript to view the <a href="http://disqus.com/?ref_noscript">comments powered by Disqus</a>.</noscript>
                </div><!-- .comments-inside -->
            </div><!-- .comments-area -->

            <script type="text/javascript">
            var disqus_shortname = 'example'; //example請代換成自己的
            if ( typeof disqus_shortname !== 'undefined' ) {
            var disqus_title = '{{title}}';
            var disqus_identifier = '{{comment_id}}';
            (function() {
            var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
            dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
            (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
            })();
            }
            </script>

在每篇文章開頭都再輸出一次封面圖片

我的佈景主題在文章總覽輸出過封面圖片以後,內文就沒有再呈現了。我想要再呈現一次,於是修改了一下:

{{#if feature_image}}
                <p><img src="{{img_url feature_image}}" alt=""></p>
{{/if}}
{{content}} //文章內文輸出標記,所以前面就是有封面圖片

從語法可以看到,Ghost真的很像Wordpress, 有簡單的代碼標記構成元素如特色圖片、文章內文、文章標題、作者、日期等,也有判斷目前頁面是首頁、文章內文、固定頁面的語法。詳細情形可以參考Ghost官方說明

Nginx 設定檔優化

我是參考這一篇教學,主要分成幾項:

  • Gzip壓縮
  • 靜態檔案快取
  • https連線優化

首先先在nginx.conf定義cache zone

# 定義一個cache zone 在 /tmp/nginx-cache
proxy_cache_path /tmp/nginx-cache levels=1:2 keys_zone=ghost:100m inactive=24h max_size=1g;  
# 根據自己的配備網站調整

proxy_cache_key "$scheme$host$request_uri";

# 萬一Ghost出現錯誤本身沒有在執行,呼叫cache
proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;

proxy_http_version 1.1;

#在Header裡面顯示除錯資訊
add_header X-Cache $upstream_cache_status;  

然後再去修改安裝Ghost時幫你新增的 /etc/nginx/sites-available/example.com-ssl.conf

server {
    listen 443 ssl http2 fastopen=3 reuseport;
    listen [::]:443 ssl http2 fastopen=3 reuseport;

    server_name example.com;
    root /var/www/ghost/system/nginx-root;

    ssl_certificate /etc/letsencrypt/example.com/fullchain.cer;
    ssl_certificate_key /etc/letsencrypt/example.com/example.com.key;
    include /etc/nginx/snippets/ssl-params.conf;

    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;

    # 靜態檔案快取
    location ~* \.(jpg|jpeg|svg|png|gif|ico|css|js|eot|woff)$ {
        proxy_cache ghost;
        proxy_cache_valid 200 30d;
        proxy_cache_valid 404 10m;
        proxy_ignore_headers "Cache-Control";
        access_log off;
        expires 30d;
        proxy_pass http://proxy_app;
    }

    # API
    location ~ ^/(?:ghost/api) {
        expires 0;
        add_header Cache-Control "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0";
        proxy_pass http://proxy_app;
    }

    # 管理介面
    location ~ ^/(?:ghost) {
        expires 0;
        add_header Cache-Control "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0";
        proxy_pass http://proxy_app;
    }
   
    # 頁面
    location / {
        proxy_cache ghost;
        proxy_cache_valid 200 1d;
        proxy_cache_valid 404 10m;
        
        expires 10m;
        proxy_pass http://proxy_app;
    }

    location ~ /.well-known {
        allow all;
    }
    client_max_body_size 50m;
}
upstream proxy_app {  
    server 127.0.0.1:2368;
    }

主要就是將一些靜態資源以及常被瀏覽的網頁設到nginx cache裡面並設定比較大的過期時間。