这是全互联网最小巧的博客,没有比这更小的了。虽然小巧,但功能一点儿也不弱,支持文章的分页展示,文章表格,图片和代码语法高亮。文章无限制分类,访问量统计,按时间和按点击量排序,展示最新文章,最热文章,文章留言评论等功能。
前言
如果你也想拥有一个属于自己的个性博客,这种尝试将大幅降低准入门槛,让你看到实现一个个人博客网站是多么的简单。其它的又是建库建表的,或是需要登录管理后台管理,我还是觉得不够简单好用。
用这种写好的markdown文档的形式,你可以直接把markdown笔记变成动态的博客展示出来。后续可基于微信小程序做个入口,如果想要发布文章,则直接把编写好的markdown文件提交发送过去就可以啦,操作使用上更加简单和快捷。这才应该是现在博客该有的特色和创新,原有的旧的登录网站后台编辑和使用的方式,太落后了。
况且使用这种方式还有个好处,可以方便多人同时使用。比如可以在公司内部搭建一个公共博客平台,做个简单入口界面,让每个人都可以往上面提交文档,这也是个不错的内部方案交流平台和技术分享平台。相比传统word文档的方式优点很多,方便文档的查看、归档和线上展示及分享,连留言评论都有啦。
项目开源地址:
tiny-blog: 极小的个人博客,虽然很小,但是很全。使用python+flask+html实现。
体验地址:
我的个人博客
实现原理
其实就是一个基于markdown的轻量级博客,需要按一定格式编写好markdown文件,提交到后台服务。当用户访问网页时由后台读取文件内容并经由流行的markdown-it组件渲染。
后台使用python的flask框架,整个代码仅三百多行。相当的小巧,很容易根据需要修改源码去增加一些其他的功能。当然一个css或js文件都不可能这么短,这里仅指的是后台代码。网页使用了流行的markdown-it(MarkDown渲染),highlight.js(语法高亮)和mermaid.js(画流程图、时序图等的js库)组件。留言评论功能,使用比较火的Valine博客评论系统。
网站模板我是从素材火网站上找了一个好看的博客网站模板。如果你有中意的其他博客网站模板,也可以很容易的改造过来用。文末介绍和分享几个好看的博客模板。
效果展示:
Flask介绍
Flask是一个轻量级的Python Web框架,用于构建Web应用程序和API。它基于Werkzeug工具箱和Jinja2模板引擎,并且具有灵活性和可扩展性。
Flask提供了一些基本的功能,例如路由、请求和响应处理、会话管理等,同时支持各种扩展,可以轻松添加身份验证、数据库集成、邮件发送等功能。由于其简单的学习曲线和活跃的社区支持,Flask已成为最受欢迎的Python Web框架之一。
源码实现
源码实现很简单,仅有三个文件,总代码量三百多行。为了提高访问速度,设计了一种基于字典map的简单数据结构。只有应用启动时加载一次文件,后续文章访问直接从字典数据中取数据,避免每次访问都读文件,提高访问效率。为了方便的查看数据结构,加载完文件后保存了几个json格式的文件。通过json文件很容易看出来。
main.py
# encoding: utf-8
# author:yangyongzhen
from flask import Flask,render_template,request,redirect
import articles
import atexit
import json
import sysapp=Flask(__name__,template_folder="templates", static_folder="static", static_url_path="/static")# 首页
@app.route('/')
def index():articles.Stat.totalVisit += 1page = 1page = request.args.get('page',1,int)print(page)nums = len(articles.AllArts)print(nums)allpage = nums / 5if nums %5 != 0:allpage = int(nums/5) + 1print(allpage)curArts = articles.AllArtsif (page * 5) < nums :curArts = articles.AllArts[(page-1)*5 : page*5]else:curArts = articles.AllArts[(page-1)*5 : nums]#分页表tabs = []for i in range(0,allpage):print(i)tabs.append(i)print(tabs)return render_template("index.html",arts=curArts,news=articles.NewArts[:9],hots=articles.HotArts[:9],items=articles.ItemList,curPage=page,tab=tabs) #加入变量传递@app.route('/about')
def about():msg="my name is caojianhua, China up!"return render_template("about.html",data=msg) #加入变量传递#分类页
@app.route('/items')
def items():try:id = request.args.get('id')print(id)allArts = []allMaps = articles.ItemMap[id]#遍历 hashfor v in allMaps.values():allArts.append(v) #按日期排序allArts.sort(key=lambda x: x['date'],reverse=True)#print(allArts)page = 1page = request.args.get('page',1,int)print(page)nums = len(allArts)print(nums)allpage = int(nums / 5)if nums %5 != 0:allpage = int(nums/5) + 1print(allpage)curArts = allArtsif (page * 5) < nums :curArts = allArts[(page-1)*5 : page*5]else:curArts = allArts[(page-1)*5 : nums]#分页表tabs = []for i in range(0,allpage):#print(i)tabs.append(i)#print(tabs)return render_template("items.html",arts=curArts,item=id,news=articles.NewArts[:9],hots=articles.HotArts[:9],curPage=page,tab=tabs) except Exception as e:print("Exception occured")print(e)return render_template('404.html')# 文章详情页
@app.route('/article_detail')
def article_detail():art = articles.Article("","","","","","","","",0,0)item_index = 0try:id = request.args.get('id')#print(id)#取出分类item = articles.ArtRouteMap[id]['item']print(item)art = articles.ItemMap[item][id] #art_index = NewPosts.index(id)articles.Stat.artStat[id]['visitCnt'] += 1articles.Stat.totalVisit += 1articles.ItemMap[item][id]['visitCnt'] = articles.Stat.artStat[id]['visitCnt']#print(art)#print(item_index)#print(art_index)return render_template("article_detail.html",data=art,news=articles.NewArts[:9],hots=articles.HotArts[:9] ) except Exception as e:print("Exception occured")return render_template('404.html')# 说说页面
@app.route('/moodList')
def moodList():msg="my name is caojianhua, China up!"return render_template("moodList.html",data=msg) # 留言页面
@app.route('/comment')
def comment():msg="my name is caojianhua, China up!"return render_template("comment.html",data=msg) @app.route('/404')
def error_404():print("error_404")return render_template("404.html") @app.errorhandler(404)
def page_not_found(e):print("page_not_found")print(e)return render_template('404.html'), 404@app.errorhandler(Exception)
def handle_exception(e):# 捕获其他异常print("handle_exception")print(e)return render_template('404.html'), 500def saveData():#保存访问量数据 with open('statistic.json1', 'w',encoding='utf-8') as f:json.dump(articles.Stat.__dict__, f,ensure_ascii=False)# 监控退出时保存统计信息
@atexit.register
def exit_handler():print("应用程序退出")saveData()if __name__=="__main__":#启动时加载访问量数据articles.getPosts()app.run(port=8000,host="127.0.0.1",debug=False)print("over")
articles.py
# encoding: utf-8
# author:yangyongzhen
import glob
import os
import pprint
import hashlib
import json
import statistic#访问量阅读量统计信息
Stat = statistic.AllStat()# AllArts 总的文章,(按时间排过序的)
AllArts = []
# NewArts 最新文章
NewArts = []
# HotArts 热门文章
HotArts = []
# 文章分类信息
ItemList = []ItemMap = {}
ArtRouteMap = {}#文章信息类定义
class Article(object):def __init__(self, id, item, title, date, summary, body, imgFile, author, cmtCnt, visitCnt):self.id = idself.item = itemself.title = titleself.date = dateself.summary = summaryself.body = bodyself.imgFile = imgFileself.author = authorself.cmtCnt = cmtCntself.visitCnt = visitCntdef mPrint(self):print(self.id)print(self.item)print(self.title)print(self.date)print(self.summary)print(self.body)print(self.imgFile)print(self.author)class ArticlesData():def __init__(self):self.item = ""self.articlesMap = {}# ArticleRoute 文章的分类和路径信息
class ArticleRoute:def __init__(self, item, name):self.item = itemself.name = name#文章分类 是个集合类型,无重复元素
class ItemCfg:def __init__(self):self.items = set()def strTrip(src):str = src.replace(" ", "")str = str.replace("\r", "")str = str.replace("\n", "")return str#从文件中读取信息,并返回itemMap,artRouteMap,items
def getPosts():global ItemMap,ArtRouteMap,ItemList,NewArts# 打开访问量统计JSON文件并读取内容with open('statistic.json1', 'r',encoding='utf-8') as file:json_data = file.read()# 将JSON字符串反序列化为字典对象Stat.__dict__ = json.loads(json_data)print(Stat.totalVisit)# 获取"posts/"目录下的所有文件files = glob.glob("posts/*")# 按修改日期进行排序files = sorted(files, key=lambda x: os.path.getmtime(x))articleMap = {}itemMap = {}artRouteMap = {}#文章分类,是个集合类型,无重复items = set()# 遍历文件列表for i, f in enumerate(files):#print(i)#print(f)file = ffile = os.path.basename(file) # 转换路径分隔符print(file)file = os.path.splitext(file)[0] # 移除".md"后缀fileread = open(f, 'r',encoding='utf-8').read()lines = fileread.split('\n')#print(lines)title = lines[0].strip()date = strTrip(lines[1].strip())summary = lines[2].strip()imgfile = lines[3].strip()id = hashlib.md5(file.encode(encoding='UTF-8'))id = id.hexdigest()#print(id.hexdigest())item = strTrip(lines[4].strip())author = lines[5].strip()imgfile = strTrip(imgfile)body = '\n'.join(lines[6:]) # 获取第6行及以后的行ar = ArticleRoute(item, title)artRouteMap[id] = ar.__dict__visitCnt = 0if id in Stat.artStat:visitCnt = Stat.artStat[id]['visitCnt']#print(visitCnt)else:Stat.artStat[id] = statistic.ArtStat(title,visitCnt,0).__dict__post = Article(id,item,title, date, summary, body, imgfile, author, 0,visitCnt)articleMap[id] = postitems.add(item)#json_data = json.dumps(posts,ensure_ascii=False)#print(json_data)itemList = []for itm in items:itmMp= {}for v in articleMap.values():if(v.item == itm):itmMp[v.id] = v.__dict__itemMap[itm] = itmMpitemList.append(itm)#保存为json文件with open('Articles.json1', 'w',encoding='utf-8') as f:json.dump(itemMap, f,ensure_ascii=False)with open('artRouteMap.json1', 'w',encoding='utf-8') as f:json.dump(artRouteMap, f,ensure_ascii=False)itemList.sort()#遍历字典for key, val in itemMap.items():for idx,art in val.items():AllArts.append(art)HotArts.append(art) #按日期排序AllArts.sort(key=lambda x: x['date'],reverse=True)#按访问量排序HotArts.sort(key=lambda x: x['visitCnt'],reverse=True)NewArts = AllArtsItemList = itemList#print((HotArts[0]))ItemMap = itemMapArtRouteMap = artRouteMapreturn itemMap,artRouteMap,itemListif __name__=="__main__":p,p1,items = getPosts()#print(p)print(type(p))print(type(p1))print(p['go学习笔记'])print(p1['3eaf98ad2b949b3f93d16aeaa1f45ab0'])print(items)
statistic.py
# encoding: utf-8
# author:yangyongzhen
# 文章和总阅读量统计#文章访问量统计
#param:名称,访问量,评论量
class ArtStat:def __init__(self, title,visitCnt,cmtCnt):self.title = titleself.visitCnt = visitCntself.commentCnt = cmtCnt#总的统计
class AllStat:def __init__(self):#总访问量self.totalVisit = 0self.artStat = {}if __name__=="__main__":pass
如何使用
除了flask,无其它依赖。下载源码,pip install flask安装好依赖。然后python main.py即可运行起来。编写新文章,需要按照特定格式写前面几行。因为文章作者,文章分类和日期等信息,都在一个文件里写的。
文章内容格式如下(test.md):
This is a title!
2020-10-22
你好aaaaaaaasssssssssssssssss! 这是你第一次使用 **Markdown编辑器** 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。
08.jpg
感悟
yangyongzhenThis is the main post!# Markdown1!
## Markdown2!
### 欢迎使用Markdown编辑器Name | Age
--------|------
Bob | 27
Alice | 23你好! 这是你第一次使用 **Markdown编辑器** 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。```golang
package mainimport "fmt"func main() {fmt.Println("hello ")
}
```
网页前端小技巧
导航栏选中高亮
网站头部导航栏根据选择自动高亮显示,代码在silder.js中实现:
var obj=null;
var As=document.querySelector(".w_header_nav").getElementsByTagName('a');
obj = As[0];
for(i=1;i<As.length;i++){if(window.location.href.indexOf(As[i].href)>=0)
obj=As[i];}
console.log(obj)
// 添加class属性
obj.classList.add("active");
返回到顶部
返回到顶部,可复用js代码:
var $backToTopEle=$('<a href="javascript:void(0)" class="Hui-iconfont toTop" title="返回顶部" alt="返回顶部" style="display:none">^^</a>').appendTo($("body")).click(function(){$("html, body").animate({ scrollTop: 0 }, 120);
});
var backToTopFun = function() {var st = $(document).scrollTop(), winh = $(window).height();(st > 0)? $backToTopEle.show(): $backToTopEle.hide();/*IE6下的定位*/if(!window.XMLHttpRequest){$backToTopEle.css("top", st + winh - 166);}
};$(function(){$(window).on("scroll",backToTopFun);backToTopFun();});
分页显示js代码 :
/*
*page plugin 1.0
*/
(function ($) {//默认参数var defaults = {totalPages: 9,//总页数liNums: 9,//分页的数字按钮数(建议取奇数)activeClass: 'active' ,//active类firstPage: '首页',//首页按钮名称lastPage: '末页',//末页按钮名称prv: '«',//前一页按钮名称next: '»',//后一页按钮名称hasFirstPage: true,//是否有首页按钮hasLastPage: true,//是否有末页按钮hasPrv: true,//是否有前一页按钮hasNext: true,//是否有后一页按钮callBack : function(page){//回掉,page选中页数}};//插件名称$.fn.Page = function (options) {//覆盖默认参数var opts = $.extend(defaults, options);//主函数return this.each(function () {var obj = $(this);var l = opts.totalPages;var n = opts.liNums;var active = opts.activeClass;var str = '';var str1 = '<li><a href="javascript:" class="'+ active +'">1</a></li>';if (l > 1&&l < n+1) {for (i = 2; i < l + 1; i++) {str += '<li><a href="javascript:">' + i + '</a></li>';}}else if(l > n){for (i = 2; i < n + 1; i++) {str += '<li><a href="javascript:">' + i + '</a></li>';}}var dataHtml = '';if(opts.hasNext){dataHtml += '<div class="next fr">' + opts.next + '</div>';}if(opts.hasLastPage){dataHtml += '<div class="last fr">' + opts.lastPage + '</div>';}dataHtml += '<ul class="pagingUl">' + str1 + str + '</ul>';if(opts.hasFirstPage){dataHtml += '<div class="first fr">' + opts.firstPage + '</div>';}if(opts.hasPrv){dataHtml += '<div class="prv fr">' + opts.prv + '</div>';}obj.html(dataHtml).off("click");//防止插件重复调用时,重复绑定事件obj.on('click', '.next', function () {var pageshow = parseInt($('.' + active).html());var nums,flag;var a = n % 2;if(a == 0){//偶数nums = n;flag = true;}else if(a == 1){//奇数nums = (n+1);flag = false;}if(pageshow >= l) {return;}else if(pageshow > 0&&pageshow <= nums/2){//最前几项$('.' + active).removeClass(active).parent().next().find('a').addClass(active);}else if((pageshow > l-nums/2&&pageshow < l&&flag==false)||(pageshow > l-nums/2-1&&pageshow < l&&flag==true)){//最后几项$('.' + active).removeClass(active).parent().next().find('a').addClass(active);}else{$('.' + active).removeClass(active).parent().next().find('a').addClass(active);fpageShow(pageshow+1);}opts.callBack(pageshow+1);});obj.on('click', '.prv', function () {var pageshow = parseInt($('.' + active).html());var nums = odevity(n);if (pageshow <= 1) {return;}else if((pageshow > 1&&pageshow <= nums/2)||(pageshow > l-nums/2&&pageshow < l+1)){//最前几项或最后几项$('.' + active).removeClass(active).parent().prev().find('a').addClass(active);}else {$('.' + active).removeClass(active).parent().prev().find('a').addClass(active);fpageShow(pageshow-1);}opts.callBack(pageshow-1);});obj.on('click', '.first', function(){var activepage = parseInt($('.' + active).html());if (activepage <= 1){return}//当前第一页opts.callBack(1);fpagePrv(0);});obj.on('click', '.last', function(){var activepage = parseInt($('.' + active).html());if (activepage >= l){return;}//当前最后一页opts.callBack(l);if(l>n){fpageNext(n-1);}else{fpageNext(l-1);}});obj.on('click', 'li', function(){var $this = $(this);var pageshow = parseInt($this.find('a').html());var nums = odevity(n);opts.callBack(pageshow);if(l>n){if(pageshow > l-nums/2&&pageshow < l+1){//最后几项fpageNext((n-1)-(l-pageshow));}else if(pageshow > 0&&pageshow < nums/2){//最前几项fpagePrv(pageshow-1);}else{fpageShow(pageshow);}}else{$('.' + active).removeClass(active);$this.find('a').addClass(active);}});//重新渲染结构/*activePage 当前项*/function fpageShow(activePage){var nums = odevity(n);var pageStart = activePage - (nums/2-1);var str1 = '';for(i=0;i<n;i++){str1 += '<li><a href="javascript:" class="">' + (pageStart+i) + '</a></li>'}obj.find('ul').html(str1);obj.find('ul li').eq(nums/2-1).find('a').addClass(active);}/*index 选中项索引*/function fpagePrv(index){var str1 = '';if(l>n-1){for(i=0;i<n;i++){str1 += '<li><a href="javascript:" class="">' + (i+1) + '</a></li>'}}else{for(i=0;i<l;i++){str1 += '<li><a href="javascript:" class="">' + (i+1) + '</a></li>'}}obj.find('ul').html(str1);obj.find('ul li').eq(index).find('a').addClass(active);}/*index 选中项索引*/function fpageNext(index){var str1 = '';if(l>n-1){for(i=l-(n-1);i<l+1;i++){str1 += '<li><a href="javascript:" class="">' + i + '</a></li>'}obj.find('ul').html(str1);obj.find('ul li').eq(index).find('a').addClass(active);}else{for(i=0;i<l;i++){str1 += '<li><a href="javascript:" class="">' + (i+1) + '</a></li>'}obj.find('ul').html(str1);obj.find('ul li').eq(index).find('a').addClass(active);}}//判断liNums的奇偶性function odevity(n){var a = n % 2;if(a == 0){//偶数return n;}else if(a == 1){//奇数return (n+1);}}});}
})(jQuery);
使用方法,在需要分页展示的文章下方添加以下div标签:
<div class="page">
</div>
在网页头部引入以下js:
<script type="text/javascript" src="plugin/fenye.page.js"></script>
<script type="text/javascript">
$(function(){$("#page").Page({totalPages: 7,//分页总数liNums: 5,//分页的数字按钮数(建议取奇数)activeClass: 'activP', //active 类样式定义callBack : function(page){//console.log(page)}});
})
</script>
markdown文档渲染
<ul class="info>
<div class="md_render">
</div>
</ul>
<script>var md;var defaults = {html: false, // Enable HTML tags in sourcexhtmlOut: false, // Use '/' to close single tags (<br />)breaks: false, // Convert '\n' in paragraphs into <br>langPrefix: 'language-', // CSS language prefix for fenced blockslinkify: true, // autoconvert URL-like texts to linkstypographer: true, // Enable smartypants and other sweet transforms// options below are for demo only_highlight: true,_strict: false,_view: 'html' // html / src / debug};defaults.highlight = function (str, lang) {var esc = md.utils.escapeHtml;if (lang && hljs.getLanguage(lang)) {try {return '<pre class="hljs"><code>' +hljs.highlight(lang, str, true).value +'</code></pre>';} catch (__) {}}return '<pre class="hljs"><code>' + esc(str) + '</code></pre>';};md = window.markdownit(defaults).use(window.markdownitCheckbox).use(window.markdownitEmoji).use(window.markdownitFootnote);var dat = document.querySelector('#enrolled_id').value;$('.md_render').html(md.render(dat));</script>
这里有个坑需要注意,jinjia2模版是在后端渲染的,所以在网页的js代码中想使用模版变量不是那么美,不能像网页中{{data}}的方式使用。这里采用了一种取巧的方式,通过一个隐藏标签,先把需要的内容在html网页中渲染出来,再在js中通过操作dom的方式去获取。
<input type="hidden" id="enrolled_id" value="{{ data.body }}">
其他资源
(自适应手机端)响应式个人博客网站模板
我的博客模版
JSON在线 | JSON解析格式化—SO JSON在线工具
风宇个人博客
丰富的计算机技术分享,多样化的互联网资源分享,第1页 - 吱吱工具箱butterPig