/org_blog

Primary LanguageCSS

基于org和Flask的纯文本博客系统

修改记录

v0.1
初稿完成,简述50行代码完成基于org-mode和flask的纯文本博客系统 [2020-03-21 Sat]
v0.2
增加提交结点说明,目标变更为搭建完整博客系统 [2020-07-06 Mon]
v0.3
增加内容说明

简介

将org文件转化为博客是我期待以久的事情,之前看过使用Hexo此类的博客系统,但是一直不想搭,最近学完了整套Flask,觉得用Flask框架配合org导出(org-export)的html文件搭一个静态博客应当是一件非常容易的事情,于是这个博客系统的方案诞生了。

博客地址为:http://lantianzero.club

博客系统方案

这个方案是因Emacs的org-mode而生的,org-mode和Emacs的配置是必须的;另外,它还依赖于nginx和Python及其安装包[包括Flask、uwsgi及supervisor等]

  • 博客的文本内容由Emacs内置的org-mode生成,文本内容在org文件中编写并整理,配置org-export选项后经org-publish的方式发布,形成html文档;
  • 博客应用服务的基础http服务由uwsgi和nginx支撑,内部逻辑基于Flask框架编写,主要使用其中的Jinja2模板和Flask提供的hook
  • 前端使用uikit作为框架基础,由于我并不怎么了解css,所以这部分不在博客中介绍
  • 应用扫描指定博客目录的html文件,提取为博客索引,根据索引访问某篇博客时,通过正则表达式解析html内容,填充入Jinja2模板中,经前端渲染后反馈用户

选择的理由

  • 优点:
    1. 依赖极少配置简单:org博客方案有许多成熟的例子,虽然我没有逐一使用,但就我了解,这些东西都有相当的依赖,要么需要进行一些繁杂的配置;
    2. 没有重复造轮子:org文体的转化由org-mode自带的org-export完成,不需要第三方工具解析,也不需要进行多次转换,org-export支持的特性都能得到有效保证;
    3. 对页面拥有绝对的控制力,界面的效果完全取决于你的前端水平
  • 缺点:
    1. 非开箱及用,需要个人理解–50行python代码
    2. 界面效果完全取决于前端水平,如果追求界面精致美观,那么需要更多的前端知识,否则可能造成用户体验较差(比如我)
    3. 如果需求诸如评论类的交互功能,需要更多的Flask基础,不过对于静态博客而言,了解Jinja2和Flask提供的hook已经足够;

配置说明

org-mode配置

org-mode的配置主要是防止使用org-html-publish-to-html使加入内嵌的css和javascript,这样方便我们处理样式,尤其是导出的源码

;; 导出的源代码内容使用单独的css文件控制样式
(setq org-html-htmlize-output-type 'css)
;; 不生成默认的css及javascript内容
(setq org-html-head-include-default-style nil)
(setq org-html-head-include-scripts nil)

(setq org-publish-project-alist
      '(("pre_pub"                          ;; project的名称
         :base-directory "path/of/org/file" ;; org文件的目录
         :base-extension "org"              ;; 扩展名称
         :publishing-directory "path/to/publish" ;; 导出目录
         :publishing-function org-html-publish-to-html  ;; 导出函数
         ;; :auto-sitemap t                                ; 自动生成网站地图,暂不需要
         )))

python代码

  1. 使用before_request这个hook以更新索引列表 这个函数获取blog所在目录的所有文件,然后生成html文件来表并绑定至g对象
    @app.before_request
    def get_static_html_list():
        static_html_dir = os.path.join(os.getcwd(), 'static', config.BLOG_DIR)
        if os.path.isdir(static_html_dir):
            static_html_list = os.listdir(static_html_dir)
            try:
                # 删除.gitkeep
                static_html_list.remove(".gitkeep")
            except ValueError:
                logging.info(f"don't have .gitkeep")
            finally:
                g.HTML_LIST = static_html_list
        else:
            g.HTML_LIST = ""
        
  2. 解析org生成的html文件 因为org-mode生成的html极为规则,这些直接使用正则表达式抽取其中的title和body,以便于后面Jinja2填充;
    title_re = re.compile(r'<title>([\s\S]*)<\/title>')
    body_re = re.compile(r'<body>([\s\S]*)<\/body>')
    
    
    class OrgBlog():
        """解析传入的ox-html文件,匹配其title和body
        如果title不存在,则返回""
        如果body不存在,则返回ox-html文件的所有内容
        """
    
        def __init__(self, oxhtml):
            with open(oxhtml, 'r', encoding='utf-8') as fp:
                try:
                    html_context = fp.read()
                except IOError as ioerr:
                    raise f"Read orghtml Failed, Error: {ioerr}"
                else:
                    title_ma = title_re.search(html_context)
                    body_ma = body_re.search(html_context)
                finally:
                    self.org_title = title_ma.group(1) if title_ma else ""
                    self.org_content = body_ma.group(1) \
                        if body_ma else html_context
        
  3. 博客索引的过滤器 这里使用template_filter类自定义Jinja2过滤器,从g对象中获取文件,通过OrgBlog类获取其title
    @app.template_filter('html_title')
    def get_static_title(ox_html):
        """函数以启动文件为根目录,ox_html为其在static/static_html/下的相对位置"""
        file_path = os.path.join(os.getcwd(), "static", config.BLOG_DIR, ox_html)
        title = OrgBlog(file_path).org_title
        return title
        
  4. Jinja2模板内容
    1. 抽取title内容生成索引
      {%- for html in g.HTML_LIST %}
      <li><a href="{{ url_for('show_blog', blog_file=html) }}">{{html|html_title}}</a></li>
      {% endfor %}
              
    2. 抽取文件内容填充模板
      {% block title %}{{ title }}{% endblock %}
      {% block content %}{{ content|safe }}{% endblock %}
              
  5. 根据选择的索引返回文件由Jinja2渲染
    @app.route('/blog/<path:blog_file>')
    def show_blog(blog_file):
        """函数以启动文件为根目录,blog_file为其在static/static_html下的相对位置"""
        file_path = os.path.join(os.getcwd(), "static", config.BLOG_DIR, blog_file)
        org_blog = OrgBlog(file_path)
    
        title = org_blog.org_title
        content = org_blog.org_content
        return render_template("blog_detail.html", title=title, content=content)
        
  6. uwsgi及supervisor将在其它博客中说明,完成后于此处增加链接

图片上传及源码

由于目前环境中暂未涉及图片及其它二进制文件,所以博客内容中没有考虑二进制文件

注记

50行代码完成Blog系统:commit节点:df7039d6562dc00d8a31267372c36ee401b2102

在debian系统上,要注意同时安装~uwsgi-plugin-python3~这个package,否则不支持python虚拟环境,无法访问

Note

g对象的简要说明

g对象是基于当前request的,只是用来串联当前请求的上下文,它并不是一个传统意义上的全局变量

TodoList

优化blog系统,减少每次访问的运算量

  • State “DONE” from “TODO” [2021-12-04 Sat 19:10]
    finish
当前blog在每次访问时,都需要遍历所有文件,然后得出其中的标题和摘要,性能低下;

考虑使用其它监控方式,在文件更新或其它有必要的时间,对更新所有title的内容

solution

Set a global value ORG_BLOGS, it’s a dynamic dict, like {“file_name”: OrgBlog(file_name),…}

init the post to OrgBlog, or judge the properity mtime for every instance of OrgBlog then update.

支持图片

考虑使用七牛云的链接,以本地静态存储做为可选方式,同步优化org-publish的发布系统

更新readme

包括本readme,以及blog中对博客系统的说明