使用Flask搭建个人静态博客

这篇文章教你如何一步一步地使用搭建自己的博客。由于Flask是一个动态网页的Web框架,所以关于Flask搭建静态博客的文章少之又少。(这是Flask的神奇之处,动态静态网页都支持,日后转换时可省去不必要的麻烦)。本系列以Steven loria的Flask静态博客实例为基础并加以改进,增添了一些更加有用的功能并详细介绍

搭建一个Flask静态博客,你需要:

  • Python3 (Steven loria采用python2.x)
  • Flask
  • Frozen-Flask
  • Flask-Flatpages
  • Pygments(可选,用于代码语法高亮)
  • Github 或 Coding

首先来看我们的目录结构:

博客应用的目录结构

| ---project
│    |---blog
│    │    ├── __init__.py
│    │    ├── settings.py
│    │    ├── views.py
│    │    ├── templates
│    │    │   ├── base.html
│    │    │      ├── page.html
│    │    │   └── posts.html
│    │    ├── static
│    │    │      ├── home.css
│    │    │   └── home.js
│    │--- posts
│    │    ├── pages
│    │    │   ├── firstpost
│    │    │   │   ├── index.html
│    │    ├── posts.html
│    │    ├── index.html
│    ├── app.py
│    ├── freezer.py 

根目录project

在这个目录中project是我们的根目录,根目录包含了:

  • blog: 项目主要内容文件夹,包含了模板,样式,和文章文本
  • posts: 我们运行生成器(冻结器)后生成的文件
  • app.py: 使用Flask动态加载网页
  • freezer.py 冻结器,生成静态网站

blog - 生成器应用

作为整个项目最主要的一环,blog包含以下内容:

  • static: 包含了css、js、图片等静态网页所需的文件
  • templates: 包含了网页的基础模板和子网页模板
  • init.py: 应用初始化文件
  • settings.py: 配置文件
  • views.py: 视图文件,为我们的网站配置了路由

init.py初始化我们的应用

了解init.py基本用法,请自行搜索或移步这里

首先我们需要导入Flask,FlatPages和Frozen-Flask的模块:

1
2
3
from flask import Flask
from flask_flatpages import FlatPages
from flask_frozen import Freezer

初始化Flask(app是初始化后的Flask实例):

1
app = Flask(__name__)

加载配置文件:

app.config.from_pyfile('settings.py')

以创建的Flask实例app为参数初始化Flatpages,Freezer(Frozen-Flask),在这里我们可以看到app是作为参数的,所以表示Flatpages,Freezer只对被传入的app有效果:

1
2
articles = FlatPages(app)
freezer = Freezer(app)

调用我们的视图文件views

1
from blog import views

最后在这里我们调用了blog文件夹下的视图脚本views,由于这里是循环调用(init.py -> 调用views ->调用init.py),所以在调用views之前必须先把所有内容初始化好,否则当views调用init.py时找不到实例会报错。

下面是init.py完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
from flask import Flask
from flask_flatpages import FlatPages
from flask_frozen import Freezer

#init the flask
app = Flask(__name__)

#load the settings from .py file
app.config.from_pyfile('settings.py')
#initialize the article pages
articles = FlatPages(app) #FlatPages use 'app' as arguments
#initialize the freezer
freezer = Freezer(app)

配置文件settings.py

配置文件是我们设置整个博客程序的核心部分,它设置了我们搜索文档的路径,生成文件的位置,使用的扩展样式,调试模式是否生效等。

由于我们要对路径进行修改,我们必须使用os模块来调用并修改参数的绝对路径

import os

并开启调试模式

REPO_NAME = "test"
DEBUG = True


APP_DIR = os.path.dirname(os.path.abspath(__file__))

编写一个返回上一级目录的函数,参数为路径

```Python
def parent_dir(path):
    return os.path.abspath(os.path.join(path,os.pardir))
```    

你要将生成的文件放在根目录下的posts文件夹里面,使用FREEZER_DESTINATION来配置生成路径。

PROJECT_ROOT = parent_dir(APP_DIR)+'/posts'
FREEZER_DESTINATION = PROJECT_ROOT

将FREEZER_REMOVE_EXTRA_FILES设为False,否则它会在生成文件后清除项目中的其它文件(脚本白写了)..

FREEZER_REMOVE_EXTRA_FILES = False

使用CodeHilite作为markdown语法高亮的插件,

FLATPAGES_MARKDOWN_EXTENSIONS = ['codehilite']

设置你的博客文档所在位置,设在/project/blog/pages/目录下:

FLATPAGES_ROOT = os.path.join(APP_DIR,'pages')
#FLATPAGES_ROOT = APP_DIR+'/pages' 

设博客文档所使用的格式为’.md’(Markdown)

FLATPAGES_EXTENSION ='.md'

代码示例:

#!python
import os

REPO_NAME = "what-is-this"
DEBUG = True


APP_DIR = os.path.dirname(os.path.abspath(__file__))

def parent_dir(path):
    return os.path.abspath(os.path.join(path,os.pardir))

PROJECT_ROOT = parent_dir(APP_DIR)+'/posts'
FREEZER_DESTINATION = PROJECT_ROOT


FREEZER_REMOVE_EXTRA_FILES = False


FLATPAGES_MARKDOWN_EXTENSIONS = ['codehilite']
FLATPAGES_ROOT = os.path.join(APP_DIR,'pages')
#FLATPAGES_ROOT = APP_DIR+'/pages' 
FLATPAGES_EXTENSION ='.md'

views文件(路由)

从我们的blog包(init.py)里面引入初始化的实例

from blog import app,articles

引入flask的模版调用函数render_template

from flask import render_template 

主页

为我们的根目录设置网页,

@app.route('/')
def index():
    return render_template('index.html')

以上代码的内容是将生成的index.html配置到’/‘根目录下,这将是你未来的主页,在网站上就是http://你的网站名.com所显示的页面.

文章列表页

@app.route('/posts.html')

def posts():
    posts = [article for article in articles if 'date' in article.meta]

    #sort posts by date,descending

其中这一句将以date为关键字来识别文档,没有date的文档不会被识别:

posts = [article for article in articles if 'date' in article.meta]

可以写成:

posts=[]
for article in articles:
    if 'date' in article.meta:
        posts.append(article)

然后将我们的文章列表按照关键字-日期(key = lambda page:page.meta[‘date’]),来进行降序排列(sorted是升序,‘reverse = True’ 使之改为降序)

sorted_posts = sorted(posts,reverse = True,key = lambda page:page.meta['date'])

然后调用posts.html模板,将模板中pages以我们获得的文章集合填充
return render_template(‘posts.html’,pages = sorted_posts)

文章页

设置文章的路由,由于文章的路由是变量,每一篇文章都有不同的地址,所以我们使用\path:path以文章名作为文章地址,但是这样会使得生成的文章页和static,posts.html,index.html混在一起,很不美观。所以我们在前面加了一个/article,使得这些文章页都存在/article文件夹下,当你的博客有了自己的域名后,文章页应该像这样: http://www.xxxxxxx.com/article/firstpost

@app.route('/article/<path:path>/')

接着我们定义一个调用文章页模板的函数

def page(path):

是否返回404错误:

article = articles.get_or_404(path)

返回文章页,每篇提取的文章都将会作为文章页模板的填充,然后生成新的文章页

return render_template('article.html',page=article)

完整代码如下:

#for view parts, we need 3 things:templates,flask,flatpages
from flask import render_template #for using render_template
#from __init__.py import app ('flask'), articles ('flatpages')
from app import app,articles


#set the articlelist route
@app.route('/')
def index():
    return render_template('index.html')
@app.route('/posts.html')
def posts():
    #posts = [article for article in articles if 'date' in article.meta]
    posts=[]
    for article in articles:
        if 'date' in article.meta:
            posts.append(article)

    #sort posts by date,descending

    sorted_posts = sorted(posts,reverse = True,key = lambda page:page.meta['date'])#Because of key is date, so in .md file date cannot be write in wrong format like Date
    #pages may related to template index.html

    return render_template('posts.html',pages = sorted_posts)



#where does path come from
@app.route('/article/<path:path>/')
def page(path):
    #path is the filename of a page,https://github.com/ChenghaoQ/ChenghaoQ.git without the file extension
    #e.g."first-post
    article = articles.get_or_404(path)

    #page may related to the template referal
    #article is the data we extracted by python,and now we need to use template, in template, the name is page->\{\{ page.meta.title }}
    #So page = article
    return render_template('article.html',page=article)

##博客文档格式及模板

先看我们的文档格式例子:

#!markdown
title: Bacon Ipsum
date: 2013-07-14 12:00:00

Bacon ipsum dolor sit amet ball tip tongue pancetta jowl sirloin rump. Chuck tail pork cow, fatback jerky hamburger pancetta leberkas pig .

[Read more](http://baconipsum.com/)

Markdown和YAML

  • Markdown:一种标记型语言,通过简单的字符将文本文档识别为html,以空白行开始,每个一个空白行都会生成一个p标签的段落
  • YAML是一個可讀性高,用來表達資料序列的格式。在我们文档的格式中,title:和date: 都被YAML提取生成dict,然后在模板里使用和调用,YAML通过冒号’:’识别,不可有空白行间隔,否则会被markdown识别为段落

模板简单语法

Jinja支持和python类似的条件语句和循环语句。

if语句:

>

上面的代码表示

for循环:

###模板与Markdown,YAML的关系

还记不记得在我们的视图文件views里面,关于文章页的路由模板调用

return render_template('article.html',page=article)

这里article是我们使用Flatpage读出来的文档内容,而page则是该内容在模板里对应的名称,于是在模板里,我们使用:

  • 表示标题
  • 表示日期
  • 表示文章正文部分

代码高亮

Markdown默认使用四个空格或者一个制表符的缩进来表示代码部分,由于使用codehilite,我们不可以用3个’`‘(反引号)来申明代码语言,但我们可以使用’:::’三个冒号来声明

:::python
from blog import app

效果如下

:::python
from blog import app

然后你可以对你文章中类名为codehilite的div块设置你喜爱的css样式.

添加博客首页摘要功能

目前很多静态博客都支持博客首页的文章摘要功能,Flask没有内置此功能。但是这并不难实现,因为Flask使用的Jinja模板有过滤器功能,我们可以向Flask实例注册一个自定义摘要过滤器:

向Flask实例app注册一个名为excerpt的过滤器,但是我们需要先引入app.

from blog import app

注册:

@app.template_filter('excerpt')

excerpt过滤器调用的函数,使用python来对文章进行切片:

def excerpt_spliter(article):
    sep='<!--More-->'
    if sep in article:
        pass
    else:
        sep = '\n'
    return Markup(pygmented_markdown(article.split(sep,1)[0]))

由于单独提取并切片后的内容只是普通文本,并不是Markdown,所以我们需要使用markdown里的markdown函数重新将其生成为HTML格式

from markdown import markdown

但是如果我们想要使用pygments来高亮语法,我们需要使用flask-Flatpages内带有Codehilite扩展的Markdown

from flask_flatpages import pygmented_markdown

完整代码如下

from markupsafe import Markup
from flask_flatpages import pygmented_markdown

@app.template_filter('excerpt')
def excerpt_spliter(article):
    sep='<!--More-->'
    if sep in article:
        pass
    else:
        sep = '\n'
    return Markup(pygmented_markdown(article.split(sep,1)[0]))

冻结器

冻结器非常简单,引入blog包然后使用Freezer加载即可

import blog
if __name__ =='__main__':
    blog.freezer.freeze()