歡迎來互換友鏈:記一次Blog遷移到Ghostphp
以前的用了Hexo搭建了Blog,不過因爲某次的操做失誤致使Hexo本地的source源文件所有丟失.留下的只有網頁的Html文件,衆所周知,Hexo是一個本地編譯部署類型的Blog系統,我的感受這種類型的Blog特別不穩定,若是本地出了一些問題那麼線上就GG了,固然,使用git是能夠管理源文件,可是源文件裏又包含不少圖片的狀況下,download和upload一次的時間也會比較長,雖說這幾年都流行這種類型的Blog,但我的看來仍是WEB比較實在。css
本身對Blog也玩了一些,不算特別多,主要是wordpress(php),此次在看Blog系統的時候,第一時間考慮的是wordpress,提及來當時也是wordpress比較早的使用患者了,wordpress的擴展性強,尤爲是目前版本的wordpress,結合主題文件和自定義function,徹底能夠玩出不少花樣,而且支持各類插件balabala,最新的版本還支持REST-API,開發起來也極爲方便,那麼爲何我沒有選用wordpress?以前作了一些wordpress的研究,發如今結合某些主題的時候,wordpress會極慢(php7.2,ssd,i7-7700hq,16gb),本地跑都極慢,固然這個鍋wordpress確定不背的,可是我在本身開發主題的狀況下,寫了一個rest-apihtml
/**
* 構建導航
* @param $res 全部的nav導航list
* @param $hash 承載導航的hash表
*/
private function buildNav(&$res,&$hash){
//組裝導航
foreach($hash as $i =>$value){
$id = $value->ID;
$b =$this->findAndDelete($res,$id);
$value->sub= $b;
// 是否有子目錄
if(count($b)>0){
$this->buildNav($res,$value->sub);
}
}
}
public function getNav($request){
$menu_name = 'main-menu'; // 獲取主導航位置
$locations = get_nav_menu_locations();
$menu_id = $locations[ $menu_name ] ;
$menu = wp_get_nav_menu_object($menu_id); //根據locations反查menu
$res = wp_get_nav_menu_items($menu->term_id);
// 組裝嵌套的導航,
$hash = $this->findAndDelete($res,0);
$this->buildNav($res,$hash);
return rest_ensure_response( $hash );
}
}
複製代碼
代碼比較簡單,獲取後臺的主導航,循環遍歷組裝成嵌套的數組結構,我在後臺新建了3個導航,結果調用這個API(PHP5.6)的狀況須要花費500ms-1s,在PHP7的狀況須要花費200-500ms,這個波動太大了,數據庫連接地址也用了127.0.0.1,再加上本身不怎麼會拍黃片(CURD級別而已),因此雖然愛的深沉,最後仍是棄用了。node
那麼可選擇的還有go語言的那款和ghost了,本人雖然很想去學Go語言,奈何頭髮已很少,仍是選擇了本身熟悉的node.js使用了ghost,這樣後續開發起來也比較方便。mysql
由於只有html文件了,那麼這個時候得想辦法把html轉markdown,原本想簡單點,說話的方式...咳咳,試用了市面上的html2markdown,雖然早知不會達到理想的效果,固然結果也是不出所料的,故只能本身根據文本規則去寫一套轉換器了.nginx
首先利用http-server
本地搭建起一套靜態服務器,接着對網頁的html結構進行分析。git
實現的最終效果以下 原文 github
轉換後 web
頁面的title是.article-title
此class的文本,頁面的全部內容的wrap是.article-entry
,其中須要轉化的markdown的html就是.article-entry >*
,得知了這些信息和結構後就開始着手寫轉化規則了,好比h2 -> ## h2
;首先創建rule列表,寫入經常使用的標籤,這裏的$是nodejs的cheerio,elem是htmlparse2轉換出來的,因此在瀏覽器的某些屬性是沒辦法在nodejs看到的sql
const ruleFunc = {
h1: function ($, elem) {
return `# ${$(elem).text()} \r\n`;
},
img: function ($, elem) {
return `![${$(elem).text()}](${$(elem).attr('src')}) \r\n`;
},
....
}
複製代碼
固然,這些只是經常使用的標籤,光這些標籤還不夠,好比遇到文本節點類型,舉個例子
<p>
我要
<a href="/">個人</a>
滋味
</p>
複製代碼
那麼你不能單純的獲取p.text()而是要去遍歷其內部還包含了哪些標籤,而且轉換出來,好比上述的例子就包含了
文本節點(text)
a標籤(a)
文本節點(text)
複製代碼
對應轉化出來的markdown應該是
我要
[個人](/)
滋味
複製代碼
還好,由markdown生成出來的html不算特別坑爹,什麼意思呢,不會p標籤裏面嵌套亂七八糟的東西(其實這跟hexo主題有關,還好我用的主題比較叼,代碼什麼的都很規範),那麼這個時候就要開始創建p標籤的遍歷規則
p: function ($, elem) {
let markdown = '';
const $subElem = $(elem).contents(); // 獲取當前p標籤下的全部子節點
$subElem.each((index, subElem) => {
const type = subElem.type; // 當前子節點的type是 text 仍是 tag
let name = subElem.name || type; // name屬性===nodeName 也就是當前標籤名
name = name.toLowerCase();
if (ruleFunc[name]) { // 是否在當前解析規則找到
let res = ruleFunc[name]($, subElem); // 若是找到的話則遞歸解析
if (name != 'br' || name != 'text') { // 若是當前節點不是br或者文本節點 都把\r\n給去掉,要否則會出現原本一行的文本由於中間加了某些內容會換行
res = res.replace(/\r\n/gi, '');
}
markdown += res;
}
});
return markdown + '\r\n'; // \r\n爲換行符
},
複製代碼
那麼p標籤的解析規則寫完後,要開始考慮ul和ol這種序號類型了,不過這種類型的也有嵌套的
- web
- - js
- - css
- - html
- backend
- - node.js
複製代碼
像這種嵌套類型的也須要去用遞歸處理一下
ul: function ($, elem) {
const name = elem.name.toLowerCase();
return __list({$, elem, type: name})
},
ol: function ($, elem) {
const name = elem.name.toLowerCase();
return __list({$, elem, type: name})
},
/**
* @param splitStr 默認的開始符是 -
* @param {*} param0
*/
function __list({$, elem, type = 'ul', splitStr = '-', index = 0}) {
let subNodeName = 'li'; // 默認的子節點是li 實際上ol,ul的子節點都是li
let markdown = ``;
splitStr += `\t`; // 默認的分隔符是 製表符
if (type == 'ol') {
splitStr = `${index}.\t` // 若是是ol類型的 則是從0開始的index 實際上這一步有點多餘,在下文有作從新替換
}
$(elem).find(`> ${subNodeName}`).each((subIndex, subElem) => {
const $subList = $(subElem).find(type); //當前子節點下面是否有ul || ol 標籤?
if ($subList.length <= 0) {
if (type == 'ol') {
splitStr = splitStr.replace(index, index + 1); // 若是是ol標籤 則開始符號爲 1. 2. 3. 這種類型的
index++;
}
return markdown += `${splitStr} ${$(subElem).text()} \r\n`
} else {
// 若是存在 ul || ol 則進行二次遞歸處理
let nextSplitStr = splitStr + '-';
if (type == 'ol') {
nextSplitStr = splitStr.replace(index, index + 1);
}
const res = __list({$, elem: $subList, type, splitStr: nextSplitStr, index: index + 1}); // 遞歸處理當前內部的ul節點
markdown += res;
}
});
return markdown;
}
複製代碼
接着處理代碼類型的,這裏要注意的就是轉義和換行,要否則ghost的markdown不識別
figure:function ($,elem) {
const $line = $(elem).find('.code pre .line');
let text = '';
$line.each((index,elem)=>{
text+=`${$(elem).text()} \r\n`;
});
return ` \`\`\` \r\n ${text} \`\`\` \r\n---`
},
複製代碼
那麼作完這兩步後,基本上解析規則已經完成了80%,什麼?你說table和序列圖類型?...這個坑就等着大家來填啦,個人Blog不多用到這兩種類型的。
抓取html這裏則可使用request+cheerio來處理,抓取我Blog中的全部文章,而且創建urlArray,而後遍歷解析就行
async function getUrl(url) {
let list = []
const options = {
uri: url,
transform: function (body) {
return cheerio.load(body);
}
};
console.info(`獲取URL:${url} done`);
const $ = await rp(options);
let $urlList = $('.archives-wrap .archive-article-title');
$urlList.each((index, elem) => {
list.push($(elem).attr('href'))
});
return list;
}
async function start() {
let list = [];
let url = `http://127.0.0.1:8080/archives/`;
list.push(...await getUrl(url));
for (let i = 2; i <=9; i++) {
let currentUrl = url +'page/'+ i;
list.push(...await getUrl(currentUrl));
}
console.log('全部頁面獲取完畢',list);
for(let i of list){
await html2Markdown({url:`http://127.0.0.1:8080${encodeURI(i)}`})
}
}
複製代碼
上述要注意的就是,抓取到的href若是是中文的話,是不會url編碼的,因此在發起請求的時候最好額外處理一下,由於熟悉本身的Blog,因此有些數值都寫死啦~
這裏我要單獨說一下,ghost的搭建是噁心到我了,雖然可以快速搭建起來,可是若是想簡簡單單的線上使用,那就是圖樣圖森破了,由於須要額外的配置,
安裝
npm install ghost-cli -g // ghost管理cli
ghost install local // 正式安裝
複製代碼
運行
ghost start
複製代碼
第一次運行成功後先別急着打開web填信息,先去配置一下mysql模式,sqlit後期擴展性太差了。
找到ghost安裝目錄下生成的config.development.json
配置
"database": {
"client": "mysql",
"connection": {
"host": "127.0.0.1",
"port": 3306,
"user": "root",
"password": "123456",
"database": "testghost"
}
},
------
"url": "https://relsoul.com/",
複製代碼
把database替換爲上述的mysql配置,而後把url替換爲線上url,接着執行ghost restart
便可
這裏利用https://letsencrypt.org 來獲取免費的SSL證書 結合NGINX來配置(服務器爲centos7)
首先須要申請兩個證書 一個是*.relsoul.com 一個是 relsoul.com
安裝
yum install -y epel-release
wget https://dl.eff.org/certbot-auto --no-check-certificate
chmod +x ./certbot-auto
複製代碼
申請通配符 參考此文章進行通配符申請
./certbot-auto certonly -d *.relsoul.com --manual --preferred-challenges dns --server https://acme-v02.api.letsencrypt.org/directory
複製代碼
申請單個證書 參考此篇文章進行單個申請
./certbot-auto certonly --manual --email relsoul@outlook.com --agree-tos --no-eff-email -w /home/wwwroot/challenges/ -d relsoul.com
複製代碼
注意的是必定要加上--manual,不知道爲啥若是不用手動模式,自動的話不會生成驗證文件到個人根目錄,按照命令行的交互提示手動添加驗證文件到網站目錄。
配置
這裏直接給出nginx的配置,基本上比較簡單,80端口訪問默認跳轉443端口就行
server {
listen 80;
server_name www.relsoul.com relsoul.com;
location ^~ /.well-known/acme-challenge/ {
alias /home/wwwroot/challenges/; # 這一步很重要,驗證文件的目錄放置的
try_files $uri =404;
}
# enforce https
location / {
return 301 https://www.relsoul.com$request_uri;
}
}
server {
listen 443 ssl http2;
#listen [::]:80;
server_name relsoul.com;
ssl_certificate /etc/letsencrypt/live/relsoul.com-0001/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/relsoul.com-0001/privkey.pem;
# Example SSL/TLS configuration. Please read into the manual of NGINX before applying these.
ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
keepalive_timeout 70;
ssl_stapling on;
ssl_stapling_verify on;
index index.html index.htm index.php default.html default.htm default.php;
# root /home/wwwroot/ghost;
location / {
proxy_pass http://127.0.0.1:9891; # ghost後臺端口
proxy_set_header Host $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-Host $server_name;
proxy_read_timeout 1200s;
# used for view/edit office file via Office Online Server
client_max_body_size 0;
}
#error_page 404 /404.html;
# location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ {
# expires 30d;
# }
# location ~ .*\.(js|css)?$ {
# expires 12h;
# }
include none.conf;
access_log off;
}
server {
listen 443 ssl http2;
#listen [::]:80;
server_name www.relsoul.com;
ssl_certificate /etc/letsencrypt/live/relsoul.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/relsoul.com/privkey.pem;
# Example SSL/TLS configuration. Please read into the manual of NGINX before applying these.
ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
keepalive_timeout 70;
ssl_stapling on;
ssl_stapling_verify on;
index index.html index.htm index.php default.html default.htm default.php;
# root /home/wwwroot/ghost;
location / {
proxy_pass http://127.0.0.1:9891; # ghost後臺端口,進行反向代理
proxy_set_header Host $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-Host $server_name;
proxy_read_timeout 1200s;
# used for view/edit office file via Office Online Server
client_max_body_size 0;
}
#error_page 404 /404.html;
# location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ {
# expires 30d;
# }
# location ~ .*\.(js|css)?$ {
# expires 12h;
# }
include none.conf;
access_log off;
}
複製代碼
作完上面幾步後就能夠訪問網站進行設置了,默認的設置地址爲http://relsoul.com/ghost
ghost的API是有點蛋疼的,首先ghost有兩種API,一種是PublicApi,一種是AdminApi
PublicApi目前只支持讀,也就是Get,不支持POST,而AdminApi則是處於改版的階段,不過仍是能用的,官方也沒具體說何時廢除,這裏只針對AdminApi進行說明,由於PublicApi的調用太簡單了,文檔也比較全,AdminApi就蛋疼了
client_secret
這個字段!!!真的很噁心,由於你基本上在後臺是找不到的,由於官方也說了AdminApi其實目前是私有的,因此你須要在數據庫找 具體的表看下圖
拿到這個值後就能夠請求獲取accessToken了
拿到這串值後則能夠開始調用POST接口了
Authorization
字段這裏的話前面的字符串是固定的
Bearer <access-token>
,接着看BODY這項
我把JSON單獨拿出來講了
{
"posts":[
{
"title":"tt19", // 文章的title
"tags":[ // tags必須爲此格式,能夠是具體tag的id,也能夠是未存在tag的name,會自動給你新建的,我推薦用{name:xxx} 來作上傳
{
"id":"5c6432badb8806671eaa915c"
},
{
"name":"test2"
}
],
"mobiledoc":"{"version":"0.3.1","atoms":[],"cards":[["markdown",{"markdown":"# ok\n\n```json\n{\n ok: \"ok\"\n}\n```\n\n> xd\n \n<ul>\n <li>aa</li>\n <li>bb</li>\n</ul>\n\nTest"}]],"markups":[],"sections":[[10,0],[1,"p",[]]]}",
"status":"published", // 設置狀態爲發佈
"published_at":"2019-02-13T14:25:58.000Z", // 發佈時間
"published_by":"1", // 默認爲1就行
"created_at":"2016-11-21T15:42:40.000Z" // 建立時間
}
]
}
複製代碼
到了這裏還有一個比較重要的字段就是mobiledoc
,對,提交不是markdown,也不是html,而是要符合mobiledoc規範的,我一開始也懵逼了,覺得須要我調用此庫把markdown再轉一次,後來發現是我想複雜了,其實只須要
{
"version": "0.3.1",
"atoms": [],
"cards": [["markdown", {"markdown": markdown}]],
"markups": [],
"sections": [[10, 0], [1, "p", []]]
};
複製代碼
按照這種格式拼裝一下,變量markdown
是轉換出來的markdown,拼接好後切記轉爲JSON字符串JSON.stringify(mobiledoc)
,那麼瞭解了提交格式等,接下來就能夠開始寫代碼了
async function postBlog({markdown, title, tags, time}) {
const mobiledoc =
{
"version": "0.3.1",
"atoms": [],
"cards": [["markdown", {"markdown": markdown}]],
"markups": [],
"sections": [[10, 0], [1, "p", []]]
};
var options = {
method: 'POST',
uri: 'https://www.relsoul.com/ghost/api/v0.1/posts/',
body: {
"posts": [{
"title": title,
"mobiledoc": JSON.stringify(mobiledoc),
"status": "published",
"published_at": time,
"published_by": "1",
tags: tags,
"created_at": time,
"created_by": time
}]
},
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: "Bearer token"
},
json: true // Automatically stringifies the body to JSON
};
const res = await rp(options);
if (res['posts']) {
console.log('插入成功', title)
} else {
console.error('插入失敗', res);
}
}
複製代碼