AI 生成: 一个简洁优雅的个人博客网站,包含首页文章列表、文章详情页和关于页面。使用 Python Flask 构建,支持 Markdown 文章渲染。
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
# Personal Blog
|
||||
|
||||
一个简洁优雅的个人博客网站。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 首页文章列表
|
||||
- 文章详情页(支持 Markdown 渲染)
|
||||
- 关于页面
|
||||
- 响应式设计,简洁优雅的界面
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Python 3.8+
|
||||
- Flask
|
||||
- Markdown
|
||||
- HTML5 / CSS3
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
personal-blog/
|
||||
├── app.py # Flask 应用入口
|
||||
├── requirements.txt # 依赖文件
|
||||
├── README.md # 说明文件
|
||||
├── posts/ # Markdown 文章目录
|
||||
│ ├── hello-world.md
|
||||
│ └── python-tips.md
|
||||
├── templates/ # HTML 模板
|
||||
│ ├── base.html
|
||||
│ ├── index.html
|
||||
│ ├── post.html
|
||||
│ └── about.html
|
||||
└── static/
|
||||
└── css/
|
||||
└── style.css
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
1. 安装依赖:
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
2. 启动应用:
|
||||
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
3. 打开浏览器访问:
|
||||
|
||||
```
|
||||
http://127.0.0.1:5000
|
||||
```
|
||||
|
||||
## 添加新文章
|
||||
|
||||
在 `posts/` 目录下创建新的 `.md` 文件,文件名即为 URL 路径。
|
||||
|
||||
文件头部使用 YAML front matter 定义元信息:
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: 我的文章标题
|
||||
date: 2024-01-01
|
||||
summary: 文章摘要
|
||||
---
|
||||
|
||||
正文内容...
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,95 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
个人博客 - Flask 应用入口
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
from flask import Flask, render_template, abort
|
||||
import markdown
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
POSTS_DIR = os.path.join(BASE_DIR, 'posts')
|
||||
|
||||
|
||||
def parse_front_matter(text):
|
||||
"""解析 Markdown 文件的 YAML front matter(简单实现)"""
|
||||
meta = {}
|
||||
body = text
|
||||
match = re.match(r'^---\s*\n(.*?)\n---\s*\n(.*)$', text, re.DOTALL)
|
||||
if match:
|
||||
raw_meta, body = match.group(1), match.group(2)
|
||||
for line in raw_meta.strip().splitlines():
|
||||
if ':' in line:
|
||||
key, value = line.split(':', 1)
|
||||
meta[key.strip()] = value.strip()
|
||||
return meta, body
|
||||
|
||||
|
||||
def load_posts():
|
||||
"""加载所有文章,返回按日期倒序的列表"""
|
||||
posts = []
|
||||
if not os.path.isdir(POSTS_DIR):
|
||||
return posts
|
||||
for filename in os.listdir(POSTS_DIR):
|
||||
if not filename.endswith('.md'):
|
||||
continue
|
||||
slug = filename[:-3]
|
||||
path = os.path.join(POSTS_DIR, filename)
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
raw = f.read()
|
||||
meta, body = parse_front_matter(raw)
|
||||
posts.append({
|
||||
'slug': slug,
|
||||
'title': meta.get('title', slug),
|
||||
'date': meta.get('date', ''),
|
||||
'summary': meta.get('summary', ''),
|
||||
'content': body,
|
||||
})
|
||||
posts.sort(key=lambda p: p['date'], reverse=True)
|
||||
return posts
|
||||
|
||||
|
||||
def get_post(slug):
|
||||
"""根据 slug 获取单篇文章"""
|
||||
for post in load_posts():
|
||||
if post['slug'] == slug:
|
||||
return post
|
||||
return None
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""首页:文章列表"""
|
||||
posts = load_posts()
|
||||
return render_template('index.html', posts=posts)
|
||||
|
||||
|
||||
@app.route('/post/<slug>')
|
||||
def post_detail(slug):
|
||||
"""文章详情页"""
|
||||
post = get_post(slug)
|
||||
if not post:
|
||||
abort(404)
|
||||
html_content = markdown.markdown(
|
||||
post['content'],
|
||||
extensions=['fenced_code', 'codehilite', 'tables']
|
||||
)
|
||||
return render_template('post.html', post=post, content=html_content)
|
||||
|
||||
|
||||
@app.route('/about')
|
||||
def about():
|
||||
"""关于页面"""
|
||||
return render_template('about.html')
|
||||
|
||||
|
||||
@app.errorhandler(404)
|
||||
def not_found(e):
|
||||
return render_template('404.html'), 404
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True, host='127.0.0.1', port=5000)
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
title: 你好,世界
|
||||
date: 2024-01-15
|
||||
summary: 这是博客的第一篇文章,记录下开始写作的这一刻。
|
||||
---
|
||||
|
||||
## 为什么写博客
|
||||
|
||||
写作是最好的思考方式。当你尝试把想法用文字表达出来时,会发现自己对问题的理解是否真正清晰。
|
||||
|
||||
这个博客会记录我在:
|
||||
|
||||
- 编程与技术上的学习
|
||||
- 阅读与思考的收获
|
||||
- 生活里的小确幸
|
||||
|
||||
## 关于技术
|
||||
|
||||
本站使用 **Python Flask** 构建,文章用 Markdown 编写。
|
||||
|
||||
```python
|
||||
from flask import Flask
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return 'Hello, World!'
|
||||
```
|
||||
|
||||
> 保持简单,保持专注。
|
||||
|
||||
希望你能在这里找到一些有价值的东西。
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
title: 5 个实用的 Python 小技巧
|
||||
date: 2024-02-20
|
||||
summary: 分享几个日常开发中非常实用的 Python 技巧,让你的代码更简洁。
|
||||
---
|
||||
|
||||
## 1. 使用 enumerate 获取索引
|
||||
|
||||
```python
|
||||
fruits = ['apple', 'banana', 'cherry']
|
||||
for i, fruit in enumerate(fruits):
|
||||
print(i, fruit)
|
||||
```
|
||||
|
||||
## 2. 列表推导式
|
||||
|
||||
用一行代码完成过滤和映射:
|
||||
|
||||
```python
|
||||
squares = [x * x for x in range(10) if x % 2 == 0]
|
||||
```
|
||||
|
||||
## 3. 使用 zip 同时遍历
|
||||
|
||||
```python
|
||||
names = ['Alice', 'Bob']
|
||||
ages = [25, 30]
|
||||
for name, age in zip(names, ages):
|
||||
print(f'{name} is {age}')
|
||||
```
|
||||
|
||||
## 4. 字典的 get 方法
|
||||
|
||||
避免 KeyError:
|
||||
|
||||
```python
|
||||
config = {'debug': True}
|
||||
level = config.get('level', 'INFO')
|
||||
```
|
||||
|
||||
## 5. 使用 f-string 格式化
|
||||
|
||||
```python
|
||||
name = 'World'
|
||||
print(f'Hello, {name}!')
|
||||
```
|
||||
|
||||
这些小技巧虽然简单,但能让代码更 Pythonic。
|
||||
@@ -0,0 +1,2 @@
|
||||
Flask==3.0.0
|
||||
Markdown==3.5.1
|
||||
@@ -0,0 +1,313 @@
|
||||
/* ---------- 基础样式 ---------- */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
|
||||
"Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||
color: #333;
|
||||
background: #fafafa;
|
||||
line-height: 1.75;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #2b6cb0;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #1a4d80;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
/* ---------- 头部 ---------- */
|
||||
.site-header {
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding: 20px 0;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.header-inner {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.site-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.site-title:hover {
|
||||
color: #2b6cb0;
|
||||
}
|
||||
|
||||
.site-nav a {
|
||||
margin-left: 20px;
|
||||
color: #666;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.site-nav a:hover {
|
||||
color: #2b6cb0;
|
||||
}
|
||||
|
||||
/* ---------- 首页 Hero ---------- */
|
||||
.hero {
|
||||
text-align: center;
|
||||
margin-bottom: 56px;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.hero .subtitle {
|
||||
color: #888;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* ---------- 文章列表 ---------- */
|
||||
.post-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
.post-item {
|
||||
padding-bottom: 32px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.post-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.post-title {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.post-title a {
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.post-title a:hover {
|
||||
color: #2b6cb0;
|
||||
}
|
||||
|
||||
.post-meta {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.post-summary {
|
||||
color: #555;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.read-more {
|
||||
font-size: 14px;
|
||||
color: #2b6cb0;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
color: #999;
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
/* ---------- 文章详情 ---------- */
|
||||
.post-detail {
|
||||
background: #fff;
|
||||
padding: 40px 36px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.post-header {
|
||||
margin-bottom: 32px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.post-header h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.post-content h2 {
|
||||
font-size: 20px;
|
||||
margin: 32px 0 16px;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.post-content h3 {
|
||||
font-size: 17px;
|
||||
margin: 24px 0 12px;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.post-content p {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.post-content ul,
|
||||
.post-content ol {
|
||||
margin: 0 0 16px 24px;
|
||||
}
|
||||
|
||||
.post-content li {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.post-content blockquote {
|
||||
border-left: 3px solid #ddd;
|
||||
padding: 8px 16px;
|
||||
color: #666;
|
||||
background: #f9f9f9;
|
||||
margin: 16px 0;
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
.post-content code {
|
||||
background: #f4f4f4;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-family: "SFMono-Regular", Consolas, Menlo, monospace;
|
||||
}
|
||||
|
||||
.post-content pre {
|
||||
background: #2d2d2d;
|
||||
color: #f8f8f2;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.post-content pre code {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.post-footer {
|
||||
margin-top: 40px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
color: #2b6cb0;
|
||||
}
|
||||
|
||||
/* ---------- 关于页面 ---------- */
|
||||
.about {
|
||||
background: #fff;
|
||||
padding: 40px 36px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.about h1 {
|
||||
font-size: 26px;
|
||||
margin-bottom: 20px;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.about h2 {
|
||||
font-size: 18px;
|
||||
margin: 28px 0 12px;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.about p {
|
||||
margin-bottom: 14px;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.contact-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.contact-list li {
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
/* ---------- 404 ---------- */
|
||||
.not-found {
|
||||
text-align: center;
|
||||
padding: 80px 0;
|
||||
}
|
||||
|
||||
.not-found h1 {
|
||||
font-size: 72px;
|
||||
color: #ddd;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.not-found p {
|
||||
color: #888;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
/* ---------- 底部 ---------- */
|
||||
.site-footer {
|
||||
margin-top: 80px;
|
||||
padding: 32px 0;
|
||||
text-align: center;
|
||||
color: #aaa;
|
||||
font-size: 14px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
/* ---------- 响应式 ---------- */
|
||||
@media (max-width: 640px) {
|
||||
.hero h1 {
|
||||
font-size: 26px;
|
||||
}
|
||||
.post-detail,
|
||||
.about {
|
||||
padding: 28px 20px;
|
||||
}
|
||||
.post-header h1 {
|
||||
font-size: 22px;
|
||||
}
|
||||
.header-inner {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.site-nav a {
|
||||
margin: 0 10px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}页面未找到 - 我的博客{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="not-found">
|
||||
<h1>404</h1>
|
||||
<p>抱歉,你访问的页面不存在。</p>
|
||||
<a href="{{ url_for('index') }}" class="back-link">← 返回首页</a>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,19 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}关于 - 我的博客{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="about">
|
||||
<h1>关于我</h1>
|
||||
<p>你好,欢迎来到我的个人博客!</p>
|
||||
<p>这里记录我在编程、生活、阅读中的思考与成长。我相信写作是最好的思考方式,也希望这些文字能对你有所启发。</p>
|
||||
|
||||
<h2>联系方式</h2>
|
||||
<ul class="contact-list">
|
||||
<li>邮箱:hello@example.com</li>
|
||||
<li>GitHub:<a href="https://github.com/" target="_blank" rel="noopener">github.com</a></li>
|
||||
</ul>
|
||||
|
||||
<h2>关于本站</h2>
|
||||
<p>本站使用 Python Flask 构建,文章以 Markdown 格式编写,简洁、纯粹。</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}我的博客{% endblock %}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<div class="container header-inner">
|
||||
<a href="{{ url_for('index') }}" class="site-title">我的博客</a>
|
||||
<nav class="site-nav">
|
||||
<a href="{{ url_for('index') }}">首页</a>
|
||||
<a href="{{ url_for('about') }}">关于</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<p>© 2024 我的博客 · 用心记录每一刻</p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,30 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}首页 - 我的博客{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="hero">
|
||||
<h1>欢迎来到我的博客</h1>
|
||||
<p class="subtitle">记录思考,分享成长</p>
|
||||
</section>
|
||||
|
||||
<section class="post-list">
|
||||
{% if posts %}
|
||||
{% for post in posts %}
|
||||
<article class="post-item">
|
||||
<h2 class="post-title">
|
||||
<a href="{{ url_for('post_detail', slug=post.slug) }}">{{ post.title }}</a>
|
||||
</h2>
|
||||
<div class="post-meta">
|
||||
<time>{{ post.date }}</time>
|
||||
</div>
|
||||
{% if post.summary %}
|
||||
<p class="post-summary">{{ post.summary }}</p>
|
||||
{% endif %}
|
||||
<a class="read-more" href="{{ url_for('post_detail', slug=post.slug) }}">阅读全文 →</a>
|
||||
</article>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="empty-tip">还没有文章,去 <code>posts/</code> 目录添加吧。</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,21 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}{{ post.title }} - 我的博客{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<article class="post-detail">
|
||||
<header class="post-header">
|
||||
<h1>{{ post.title }}</h1>
|
||||
<div class="post-meta">
|
||||
<time>{{ post.date }}</time>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="post-content">
|
||||
{{ content|safe }}
|
||||
</div>
|
||||
|
||||
<div class="post-footer">
|
||||
<a href="{{ url_for('index') }}" class="back-link">← 返回首页</a>
|
||||
</div>
|
||||
</article>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user