Markdown + Git做简单的Blog内容发布

开发一套博客系统的话,主要分两部分。一部分是内容发布,另一部是内容展示。

内容发布需要一个编辑方便,所见即所得的编辑器,并能将编辑内容转成html后保存起来。由于我想采用Markdown编辑,且不想实现Markdown内容转html的开发(使用过Python-markdown2,感觉并不是很理想),所以,我这里采用MarkdownPad2进行编辑,然后生成Html保存起来,由Python程序按照设计的规则解析成Html,并存入数据库。(当然,我们也可以采用其它Markdown编辑器实现类似功能)。

内容展示需要事先选好网站模板和设计API接口。我这里直接在网上找了套Bootstrap模板,用Nginx做API,Lua实现数据的读取和html的嵌套。

流程图

下图为内容发布的流程图:

流程图

编辑文章

使用MarkdownPad2编辑文章内容,Markdown的编辑器可以自己设置,我们这里设置为gitHub离线模式。

格式规范

1
2
3
4
5
6
7
8
<!--
<tag>文章标签,多个标签之间用空格,手动设置,,例如:标签1 标签2</tag>
<title>文章标题,手动设置</title>
<img>导读部分显示的图片,手动设置,可以没有</img>
<intro>导读部分内容,字节长度为500,手动设置</intro>
-->

正文部分

拷贝html内容

通过MarkdownPad的浏览器预览,可以预览文章展示的效果,根据需要可以进行调整。

浏览器预览后,通过浏览器查看源码,即获取到html内容。

为了便于维护样式和设计的模块化,我们应该将样式作为独立的css文件,在html中引用。此处只负责生成文章内容的html。所以,只需拷贝body标签的内容到html文件即可。

发布内容

编写git hook的post-receive脚本,将提交的html文件内容同步到数据库(mysql或redis)。

git hooks的部署使用见利用Git hooks实现自动化部署

此处有两种方案:

  1. html内容全部同步到数据库,阅读时,从数据库获取文章内容
  2. 只同步预览部分的内容,阅读时,通过文件包含,直接引用对应的文章文件。

无论选择哪一种方案,都需要处理标签对应关系的存储。

建表语句

文章表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CREATE TABLE `t_article` (
`id` int(11) NOT NULL,
`title` varchar(100) NOT NULL COMMENT '标题',
`content` varchar(1000) DEFAULT NULL COMMENT '预览内容',
`html` mediumtext DEFAULT NULL COMMENT 'body内容',
`tag_id` varchar(255) DEFAULT NULL COMMENT '标签id,多个用空格分隔',
`tag_name` varchar(500) DEFAULT NULL COMMENT '标签名称,多个用空格分隔',
`file_path` varchar(500) DEFAULT NULL COMMENT '文件路径',
`img_path` varchar(500) DEFAULT NULL COMMENT '图片路径',
`author` varchar(100) DEFAULT NULL COMMENT '作者',
`read_count` int(11) DEFAULT '1' COMMENT '阅读量',
`status` tinyint(1) DEFAULT NULL COMMENT '状态,0:无效,1:有效',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`id`),
KEY `i_time` (`create_time`,`status`) USING BTREE
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='文章表';

标签表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CREATE TABLE `t_tag` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '标签id',
`tag` varchar(20) NOT NULL COMMENT '标签名称',
`type` tinyint(1) DEFAULT '2' COMMENT '类型:1:目录标签,2:普通标签',
`status` tinyint(1) DEFAULT '1' COMMENT '状态:0:无效,1:有效',
PRIMARY KEY (`id`),
KEY `i_type` (`type`,`status`) USING BTREE
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

alter table t_tag AUTO_INCREMENT=100;

insert into t_tag(id,tag,type,status) values(1,'工作',1,1);
insert into t_tag(id,tag,type,status) values(2,'生活',1,1);
insert into t_tag(tag,type,status) values('Hadoop',2,1);
insert into t_tag(tag,type,status) values('Hive',2,1);
insert into t_tag(tag,type,status) values('Storm',2,1);

文章标签表

1
2
3
4
5
CREATE TABLE `t_article_tag` (
`aid` int(11) NOT NULL COMMENT '文章id',
`tagid` int(11) NOT NULL COMMENT '标签id',
UNIQUE KEY `i_aid_tagid` (`aid`,`tagid`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

用户表

1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE TABLE `sys_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_name` varchar(12) DEFAULT NULL COMMENT '用户名',
`password` varchar(50) DEFAULT NULL COMMENT '加密后的密码',
`real_name` varchar(10) DEFAULT NULL COMMENT '真实用户名',
`create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
`last_login_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`),
KEY `i_login` (`user_name`,`password`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='系统用户表';

alter table sys_user AUTO_INCREMENT=100;

** 项目结构 **

目录结构如下:

目录结构

  • assets: 存放静态文件
  • config: 存放Nginx的配置文件
  • html: 各模板的html和Markdown生成的内容html
  • lua:存放各模板对应的业务代码,主要为页面展现读取数据
  • note:为Markdown的源文件
  • scripts:为Git hooks需要触发的脚本

Nginx配置

在OpenResty的Nginx配置如下:

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
user  nginx;
worker_processes auto;

pid logs/nginx.pid;
worker_rlimit_nofile 65535;

events {
use epoll;
worker_connections 1024;
multi_accept on;
}


http {
include mime.types;
default_type application/octet-stream;

log_format main '$remote_addr | $http_x_forwarded_for | [$time_local] | "$request" | $status | "$bytes_sent"'
'| "$request_time" | "$upstream_response_time" | "$http_referer" | "$http_user_agent" | "$gzip_ratio"';


access_log logs/access.log main;
error_log logs/error.log debug;

# 这个将为打开文件指定缓存,默认是没有启用的,max 指定缓存数量,
# 建议和打开文件数一致,inactive 是指经过多长时间文件没被请求后删除缓存。
open_file_cache max=65535 inactive=120s;

# open_file_cache 指令中的inactive 参数时间内文件的最少使用次数,
# 如果超过这个数字,文件描述符一直是在缓存中打开的,如上例,如果有一个
# 文件在inactive 时间内一次没被使用,它将被移除。
open_file_cache_min_uses 1;

# 这个是指多长时间检查一次缓存的有效信息
open_file_cache_valid 30s;

sendfile on;
tcp_nopush on;
tcp_nodelay on;
server_tokens off;

keepalive_timeout 65;
client_header_timeout 10;
client_body_timeout 10;
reset_timedout_connection on;
send_timeout 10;

server_names_hash_bucket_size 128;
client_header_buffer_size 32k;
client_max_body_size 300m;
large_client_header_buffers 4 32k;

gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_http_version 1.0;
gzip_comp_level 3;
gzip_types text/plain application/x-javascript application/javascript text/javascript text/css application/xml image/jpeg image/gif image/png;
gzip_vary on;

#lua模块路径,多个之间”;”分隔,其中”;;”表示默认搜索路径,默认到/usr/servers/nginx下找
#lua_package_path "/work/app/nginx-app/lua/?.lua;;";

#server配置
include /opt/apps/nginx-app/blog/config/nginx-blog.conf;
}

博客项目的Nginx配置如下:

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
server {
listen 80;
server_name kiswo.com wangrun.love;

set $template_root "/opt/apps/nginx-app/blog/html/template-base";
set $work_path "/opt/apps/nginx-app/blog";
set $template_name "base";

#root /opt/apps/nginx-app/blog/;

location = / {
rewrite ^/$ /tag/0/page/1 last;
}

location ^~ /tag/ {
default_type 'text/html';
content_by_lua_file $work_path/lua/$template_name/index.lua;
}

location ~ ^/article/(\d*) {
#lua_code_cache off;
default_type 'text/html';
set $id $1;
content_by_lua_file $work_path/lua/$template_name/article.lua;
}

location = /login.html {
default_type 'text/html';
root $template_root/;
}

location /assets/css/ {
#default_type 'text/css';
concat on;
concat_max_files 20;
access_log off;
expires 30d;
#concat_unique off; js和css合并成一个请求
alias $work_path/assets/$template_name/css/;
}

location /assets/js/ {
#default_type 'application/javascript';
concat on;
concat_max_files 20;
access_log off;
expires 30d;
alias $work_path/assets/$template_name/js/;
}

location /assets/fonts/ {
etag on;
expires max;
access_log off;
alias $work_path/assets/$template_name/fonts/;
}

location /image/ {
etag on;
expires max;
access_log off;
alias $work_path/assets/img/;
}

location = /favicon.ico {
etag on;
expires max;
access_log off;
root $work_path/assets/;
}
}

由于每套模板都是独立的目录文件,所以,如果想切换网址模板的风格,直接template_roottemplate_name两个参数即可。

自动更新内容

通过Git hook实现自动更新文章内容和服务重启。

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
#!/bin/bash

echo "`date +"%Y-%m-%d %H:%M:%S"` Info: start sync blog"
unset $(git rev-parse --local-env-vars)

cd /work/app/nginx-app/blog
git pull

git log --name-status --pretty=oneline -1 > /tmp/git.log

PATH_FLAG="html/articles/"

while read line
do
TYPE=`echo "$line" | awk -F '\t' '{print $1}'`
FILE=`echo "$line" | awk -F '\t' '{print $2}'`

if [ "${TYPE}" = "A" -o "${TYPE}" = "M" -o "${TYPE}" = "D" ]; then
#FILENAME=${FILE##*/}
if [ `expr match ${FILE} ${PATH_FLAG}` -ne 0 ]; then
echo "parse ${FILE}"
python /opt/apps/nginx-app/blog/scripts/parse_html.py ${FILE}
else
echo "the file is ${FILE}, nothing to do."
fi
else
echo "the type is ${TYPE}, nothing to do."
fi

done < /tmp/git.log


echo "`date +"%Y-%m-%d %H:%M:%S"` Info:end sync blog"
sudo /usr/local/openresty/nginx/sbin/nginx -s reload


exit 0

可以直接采取将html文件嵌套在模板中,但是实现搜索功能将会很麻烦,所以,我就通过Python将html内容存入Mysql中。

parse_html.py代码如下:

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
# coding=utf-8

import sys
import MySQLdb
from datetime import *

filepath = sys.argv[1]
array = filepath.split("/")
category = array[1]
filename = array[3]

print "filepath: " + filepath

dict = {'id': '', 'name': filename, 'category': category, 'tag': '', 'title': '', 'img': '',
'intro': '', 'tag_id': '', 'tag_name': '', 'body': ''}


def getIntro(line, bodyDict, markDict):
for k in markDict:
if markDict[k] < 2:
se = "<" + k + ">"
ee = "</" + k + ">"

if line.find(se) >= 0:
if line.find(ee) > 0:
bodyDict[k] = line[len(se):(len(line) - len(ee) - 1)]
markDict[k] = 2
else:
bodyDict[k] = bodyDict[k] + line[len(se):len(line)]
markDict[k] = 1
elif line.find(ee) >= 0:
bodyDict[k] = bodyDict[k] + line[0:(len(line) - len(ee) - 1)]
markDict[k] = 2
else:
if markDict[k] == 1:
bodyDict[k] = bodyDict[k] + line
markDict[k] = 1


def parse_html():
file_html = open("/opt/apps/nginx-apps/blog/" + filepath)
body = ''
flag_body = False
flag_note = False
markDict = {"id": 0, "tag": 0, "title": 0, "img": 0, "intro": 0}
for line in file_html.xreadlines():
#line = line.strip('\n')
if line.find("<body>") >= 0:
flag_body = True
elif flag_body:
if line.find("<!--intro") >= 0:
flag_note = True
elif line.find("intro-->") >= 0:
flag_note = False

if flag_note:
getIntro(line, dict, markDict)

body = body + line

dict['body'] = body

#print "id: " + dict['id']
#print "tag: " + dict['tag']
#print "title: " + dict['title']
#print "img: " + dict['img']
#print "intro: " + dict['intro']
#print "body: " + dict['body']
return dict

def update_data(dict):
conn = MySQLdb.connect(host='127.0.0.1', user='mysql_blog', passwd='mysqBlogPWD2016', db='blog', charset='utf8')
cur = conn.cursor()
try:
tag_id = []
# update tag
tag_arr = dict['tag'].split(",")
for tag in tag_arr:
tag = tag.strip()
sql = "SELECT id FROM t_tag WHERE lower(tag) = %s"
print sql
cur.execute(sql, (tag.lower(),))
results = cur.fetchone()
if not results:
sql = "INSERT INTO t_tag(tag, type, status) VALUES(%s, %s, %s)"
print sql
cur.execute(sql, (tag, "2", "1"))
# time.sleep(1)
ido = conn.insert_id()
ids = str(ido)
tag_id.append(ids)
else:
tag_id.append(str(results[0]))

sql = "SELECT COUNT(1) FROM t_article WHERE id = %s"
print sql
cur.execute(sql, (int(dict['id']),))
results = cur.fetchone()
now_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')

tag_ids = ",".join(tag_id)
#intro = dict['intro'].replace('<br />', '').replace('<br>', '').replace('\n', '').replace('\r', '')
intro = dict['intro'].replace('\n', '').replace('\r', '')
if results[0] <= 0:
sql = "INSERT INTO t_article(id, title, content, html, tag_id, tag_name, img_path, " \
"status, create_time) VALUES(%s, %s, %s, %s, %s, %s, %s, %s, %s)"
print sql
cur.execute(sql, (int(dict['id']), dict['title'], intro, dict['body'], tag_ids, dict['tag'],
dict['img'], '1', now_time))
cur.execute(create_sql(tag_id, int(dict['id'])))
else:
sql = "UPDATE t_article SET title=%s, content=%s, html=%s, tag_id=%s, tag_name=%s, " \
"img_path=%s, update_time=%s WHERE id = %s"

print sql
cur.execute(sql, (dict['title'], intro, dict['body'], tag_ids, dict['tag'],
dict['img'], now_time, int(dict['id'])))

sql = "DELETE FROM t_article_tag WHERE aid = %s"
print sql
cur.execute(sql, (int(dict['id']),))
cur.execute(create_sql(tag_id, int(dict['id'])))

conn.commit()
except Exception, e:
raise e
finally:
cur.close()
conn.close()

def create_sql(tag_id, aid):
sql = "INSERT INTO t_article_tag(aid, tagid) VALUES"
values = []
for tag in tag_id:
values.append('(' + str(aid) + ',' + str(tag) + ')')

sql = sql + ",".join(values)
print sql
return sql


data = parse_html()
update_data(data)
文章作者: OneRain
文章链接: https://kiswo.com/2016/05/28/tools/blog/blog-content-release-with-git/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 OneRain's Blog