从零开始搭建这个博客

缘起

一直以来都想拥有一个属于自己的博客,记录技术思考和生活感悟。在网上看了不少方案,最终决定自己动手写一个——用 Flask + SQLite 的轻量组合,部署在云服务器上。


技术选型

模块 选择 原因
后端框架 Flask 3.1 轻量灵活,Python 生态
数据库 SQLite + SQLAlchemy 单用户博客,无需 MySQL
前端 原生 HTML/CSS/JS 无框架依赖,加载轻快
Markdown Python-Markdown + Pygments 文章编写体验好,代码高亮
部署 Gunicorn + Nginx 简单可靠,反向代理静态文件

核心架构

应用工厂模式

def create_app(config_name=None):
    app = Flask(__name__)
    app.config.from_object(config_map.get(config_name))

    # 初始化扩展
    db.init_app(app)
    login_manager.init_app(app)
    csrf.init_app(app)

    # 注册蓝图
    app.register_blueprint(main_bp)       # 公开路由
    app.register_blueprint(auth_bp, url_prefix='/auth')
    app.register_blueprint(admin_bp, url_prefix='/admin')

    # 初始化数据库
    with app.app_context():
        db.create_all()
        _init_admin(app)

    return app

数据库模型

文章模型是核心,支持 Markdown 渲染、标签管理、分类关联:

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200), nullable=False)
    slug = db.Column(db.String(200), unique=True, index=True)
    content = db.Column(db.Text, nullable=False)  # 原始 Markdown
    tags = db.Column(db.String(500), default='')  # 逗号分隔
    is_published = db.Column(db.Boolean, default=True)
    author_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    category_id = db.Column(db.Integer, db.ForeignKey('category.id'))

    def content_html(self):
        import markdown
        md = markdown.Markdown(extensions=['extra', 'codehilite', 'toc'])
        return md.convert(self.content)

前端亮点

暗夜模式

通过 CSS 变量实现一键切换,状态保存在 localStorage:

:root {
  --bg: #f5f5f5;
  --font-color: #4c4948;
  --card-bg: #fff;
}
[data-theme='dark'] {
  --bg: #0d0d0d;
  --font-color: rgba(255,255,255,0.75);
  --card-bg: #141414;
}

PJAX 无刷新导航

页面切换只替换 #pjax-container 内的内容,无需整页加载:

new Pjax({
  selectors: ['#pjax-container', 'title']
});

一言 · 每日随机语录

每次回到首页,从缓存池中随机挑一句诗词或名言展示:

var pool = JSON.parse(localStorage.getItem('saying_pool')) || [];
el.textContent = pool[Math.floor(Math.random() * pool.length)];

每天凌晨自动轮换 25 条,补充新句子。


NeteaseMiniPlayer v2 · 悬浮音乐播放器

在博客右下角集成了网易云音乐悬浮播放器,使用开源的 NeteaseMiniPlayer v2 组件,支持歌词显示、播放列表、随机播放等丰富功能。

基本用法

在页面中放置一个自定义元素,组件会自动初始化:

<div class="netease-mini-player"
     data-playlist-id="496585734"
     data-embed="false"
     data-position="bottom-left"
     data-lyric="true"
     data-theme="auto"
     data-autoplay="false"
     data-default-minimized="true"
     data-auto-pause="true">
</div>

通过 data-* 属性即可配置播放器行为,无需编写任何 JavaScript。

与 PJAX 配合

PJAX 只刷新 #pjax-container 内的内容,而播放器元素放在容器外面,因此 PJAX 切换页面时播放器不会中断,音乐持续播放:

<div id="pjax-container">
  <!-- 页面内容,PJAX 只替换这里 -->
</div>

<!-- 播放器在容器外,PJAX 不影响 -->
<div class="netease-mini-player" ...>
</div>

音频保活

为防止浏览器在后台标签页时暂停音频,使用了一个 Web Worker 定时唤醒 AudioContext:

var audioWorker = new Worker('/static/js/audio-worker.js');
audioWorker.postMessage({ command: 'start' });

Worker 内部每 15 秒重建一次 AudioContext,配合 visibilitychange 事件,切回页面时自动恢复播放。

暗夜模式适配

播放器支持 data-theme="auto" 自动跟随系统主题。同时本博客在切换主题时也通过 JS 同步更新播放器的主题状态:

function syncPlayerTheme(theme) {
  document.querySelectorAll('.netease-mini-player').forEach(function(el) {
    el.setAttribute('data-theme', theme);
  });
}

📖 完整文档:NeteaseMiniPlayer v2 官方文档

部署

使用 Gunicorn 启动 3 个 Worker,Nginx 反向代理并直接托管静态文件:

location /static/ {
    alias /home/Blogs/app/static/;
    expires 30d;
}
location / {
    proxy_pass http://127.0.0.1;
}

写在最后

这个博客虽然简单,但麻雀虽小五脏俱全。从数据库设计到前端交互,从部署上线到日常维护,每一个环节都是亲手搭建的。后续还会继续完善——评论回复通知、图片管理、文章归档等功能已经在路上了。

感谢你读到这儿,欢迎在评论区留言交流 🚀

评论

暂无评论,来抢沙发吧!