Blog tiếng Việt để chia sẻ trải nghiệm sống ở Nhật. Trang quản trị hỗ trợ tạo, chỉnh sửa, lưu nháp và xuất bản bài viết, kèm ảnh/video và thông tin blog.
Yêu cầu Python 3.9 trở lên.
cd quang-dong-japan-blog
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
flask --app app create-admin
python app.py
Mở http://127.0.0.1:8080. Lệnh create-admin sẽ hỏi tên đăng nhập và mật khẩu
tối thiểu 12 ký tự. Tài khoản admin chỉ được tạo một lần; ứng dụng không cung
cấp đăng ký công khai. Đăng nhập quản trị trực tiếp tại /admin/login; liên kết
quản trị không hiển thị trên giao diện công khai.
Để thêm quản trị viên hoặc đặt lại mật khẩu, chạy python manage_admins.py,
chọn thao tác a (thêm) hoặc r (đặt lại), rồi nhập tên đăng nhập và mật khẩu
theo lời nhắc. Mật khẩu chỉ được lưu dưới dạng hash; không truyền mật khẩu trên
dòng lệnh.
Nếu hosting không cấp Shell, có thể dùng biến môi trường: đặt
BLOG_ADMIN_USERNAME và BLOG_ADMIN_PASSWORD trong phần Environment của dịch
vụ rồi deploy/restart app. Sau khi khởi động, đăng nhập tại /admin/login.
Giữ biến mật khẩu nếu filesystem/database của gói hosting không bền vững qua
các lần restart; xóa biến này sau khi dùng chỉ khi database được lưu bền vững.
Trong VS Code, chọn interpreter .venv/bin/python (Python: Select Interpreter)
để Pylance dùng đúng Flask/Werkzeug đã cài trong môi trường dự án.
Sao chép .env.example thành .env rồi đặt các biến môi trường cần thiết. .env
không được đưa lên Git:
BLOG_SECRET_KEY: khóa bí mật ngẫu nhiên dài, giữ nguyên qua các lần cập nhật.
Nếu để trống, ứng dụng tạo khóa trong BLOG_DATA_DIR/.secret-key.BLOG_DATA_DIR: thư mục lưu SQLite, khóa bí mật tự sinh và nội dung upload.
Mặc định là thư mục data/ trong project; trên VPS hãy trỏ tới ổ đĩa bền vững
riêng bên ngoài thư mục release.BLOG_ADMIN_USERNAME và BLOG_ADMIN_PASSWORD: tùy chọn để tạo/cập nhật tài
khoản admin khi app khởi động. Chỉ đặt mật khẩu trong phần biến môi trường
bảo mật của hosting, không ghi vào source code. Nếu đặt BLOG_ADMIN_PASSWORD,
app sẽ đảm bảo tài khoản này có mật khẩu đó mỗi lần worker khởi động.BLOG_HOST và BLOG_PORT: địa chỉ bind và cổng khi chạy python app.py.BLOG_COOKIE_SECURE=true: bật cookie chỉ gửi qua HTTPS. Chỉ bật khi reverse
proxy HTTPS đã được cấu hình.MAIL_SERVER=smtp-relay.brevo.com,
MAIL_PORT=587, MAIL_USERNAME (tài khoản SMTP Brevo), MAIL_PASSWORD
(SMTP key), MAIL_DEFAULT_SENDER (địa chỉ gửi đã xác minh trong Brevo),
MAIL_FROM_NAME và BLOG_PUBLIC_URL=https://quangdongofficial.com trong
môi trường của service. Không ghi SMTP key vào source code hay đưa lên Git.
Xác minh domain gửi trong Brevo và cấu hình các DNS records SPF/DKIM theo
hướng dẫn Brevo trong Cloudflare để tăng khả năng gửi thành công.Khách đăng ký tại biểu mẫu cuối trang chủ và phải bấm xác nhận trong email. Sau khi một bài mới được xuất bản (hoặc bản nháp chuyển sang công khai), hệ thống gửi email có tiêu đề, đường dẫn bài viết và đường dẫn hủy đăng ký. Chỉnh sửa một bài đã công khai không gửi lại thông báo. Dịch vụ SMTP cần được cấu hình trước khi email xác nhận hoặc thông báo có thể gửi đi.
Ứng dụng chấp nhận ảnh JPG, PNG, WEBP, GIF và video MP4, WEBM, MOV; mỗi lần request tối đa 60 MB và tối đa 12 tệp media mới cho một bài. Trong trình soạn thảo, đặt con trỏ tại vị trí cần chèn rồi bấm nút Ảnh hoặc Video để nhúng media ngay trong nội dung, hoặc dùng nút Ảnh URL, Video URL, Nhúng web để hiển thị media từ URL HTTPS bên ngoài mà không tải file đó về máy chủ. URL video tệp trực tiếp dùng thẻ video; URL trang web dùng iframe sandbox và cần website nguồn cho phép nhúng (một số nền tảng yêu cầu URL embed riêng hoặc chặn iframe). Ảnh chèn bằng URL có thể đặt mô tả alt/title và chú thích hiển thị bên dưới ảnh; chọn ảnh trong editor rồi bấm “Sửa tiêu đề/chú thích ảnh” để thay đổi. Ảnh tĩnh JPG, PNG và WEBP được tự xoay theo EXIF, thu nhỏ tối đa còn 1920 px mỗi chiều và nén sang WEBP chất lượng 82 trước khi lưu; nếu bản nén lớn hơn ảnh gốc thì ảnh gốc được giữ nguyên. GIF và ảnh động được giữ nguyên để không làm mất hoạt ảnh; video tải lên không bị chuyển đổi.
Tạo virtual environment, cài dependencies và tạo admin như phần chạy local.
Sau đó cấu hình biến môi trường (hoặc nạp file env bằng service manager), trỏ
BLOG_DATA_DIR tới thư mục dữ liệu bền vững mà user chạy ứng dụng có quyền ghi,
và chạy:
gunicorn --workers 2 --bind 127.0.0.1:8080 app:app
Với Caddy, thêm site block riêng cho domain blog để Caddy tự cấp HTTPS:
zenligai.online {
reverse_proxy 127.0.0.1:8080
}
Khối trên chỉ phục vụ domain gốc zenligai.online; giữ nguyên site block hiện
có của callback.zenligai.online để bot tiếp tục hoạt động. Nếu dùng Nginx, cấu
hình tương tự để proxy domain blog tới 127.0.0.1:8080. Không mở trực tiếp cổng
ứng dụng ra Internet. Bật BLOG_COOKIE_SECURE=true khi HTTPS hoạt động. Nên chạy
ứng dụng bằng user hệ thống riêng, hạn chế quyền ghi
vào thư mục dữ liệu, và cấu hình tự khởi động/giám sát bằng systemd hoặc process
manager hiện có.
Mỗi khi tạo bài đã xuất bản, xuất bản bản nháp hoặc lưu chỉnh sửa bài đã xuất
bản, ứng dụng tự tạo một file nén gồm bản SQLite nhất quán, uploads/ và
.secret-key nếu khóa được tạo từ file. Mặc định file nằm trong
BLOG_DATA_DIR/backups/; có thể đặt BLOG_BACKUP_DIR để dùng thư mục khác mà
user chạy ứng dụng có quyền ghi. Ứng dụng chỉ giữ ba bản mới nhất và tự xóa bản
cũ hơn. Nếu backup lỗi, bài viết vẫn được lưu; trang quản trị hiển thị cảnh báo
và lỗi được ghi vào log service.
Để tạo bản backup ban đầu hoặc tạo thủ công:
flask --app app backup-now
Khi khôi phục, dừng service trước, giải nén một file backup-*.tar.gz, rồi chép
blog.sqlite3, uploads/ và .secret-key (nếu có) về thư mục BLOG_DATA_DIR
đúng vị trí, sau đó khởi động lại service. Nếu dùng BLOG_SECRET_KEY thay cho
.secret-key, cần giữ nguyên giá trị biến môi trường đó. Backup trên cùng VPS
không bảo vệ dữ liệu nếu VPS hoặc ổ đĩa bị mất; nên tải định kỳ các file backup
sang một nơi lưu trữ khác.
Khi cập nhật code, giữ nguyên BLOG_DATA_DIR, database, uploads và secret key.
Chỉ cập nhật dependencies theo requirements.txt, sau đó restart service. Không
xóa thư mục dữ liệu khi deploy phiên bản mới.