Как работает данный блог
Этот сайт зародился в далеком 2009. Тогда я запилил его на php. Задача была простой: легко и просто доносить до общественности свои мысли. Я сделал добавл на главной div, сделал его редактируемым (аля WYSIWYG) и кнопки сохранить
/опубликовать
.
Так на сайте появились первые статьи. Довольно быстро я понял, что мой визивиг очень убогий и не постоянно глючит. + На сайте регулярно нужны были доработки. Самопис все таки. Я е выдержал и перенес все на Joomla.
Про глюки и обновления можно было больше не думать, это прекрасная и надежная система. Это самолет а не система. Порталы, магазины, ракеты работают на Joomla. Но не блоги. Они есть, но это просто неудобно.
Чтобы запилить статью, нужно подождать загрузки админки, авторизоваться, создать статью, заполнить кучу полей, и только затем писать статью. А уж посмотреть результат так это вообще квест, кеш не почистил - ничего не увидел!
Почистил? Не факт, что если видишь ты, то видят и все остальные (Мы же авторизованы под админом).
И тут были подходы это автоматизировать, и тут я делал модуль на главную с визивигом и поэтессами, но общие тормоза CMS Joomla не делали этот процесс комфортным.
Я даже свой визивиг написал https://xdsoft.net/jodit. То тема для отдельной статьи.
Короче я долго страдал. И все это время мечтал о своем старом подходе: зашел на сайт, на главной, ввел текст (желательно на MarkDown), нажал опубликовать - ушел.
Вот я и запилил этот раздел за два вечера. Как я это сделал, расскажу под катом: тут у нас React(create-react-app + typescript), Node JS как сервер, Github actions как CI + CD, и Docker Compose на сервере.
Подготовка VPS
Итак первое, что я сделал это купил новый VPS. Самый дешевый за 300 рублей. На нем поднял Debian и Docker + Docker-Compose. Это все, что мне от него нужно.
На основном домене на в nginx
прописал
location /blog/ {
proxy_pass http://ip:8892;
}
Это хост от нового VPS и порт веб сервера на Express Node JS.
Frontend
Создал новый проект через create-react-app
и запили интерфейс.
Это не так интересно. Тут все схематично так:
const onSave = throttle((e) => {
fetch('/blog/api/save/', {content: e.target.value});
}, 1000);
function Editor() {
return <textarea onInput={onSave}/>
}
Server
Ну и сервер. Тут у нас express
и sequelize
для работы с mysql
.
const express = require('express');
const app = express();
const cookieParser = require('cookie-parser');
app.use(cookieParser());
app.use('/blog/api/', express.json());
const posts = require('./posts'); // модуль для работы с постами
app.use(`/blog/api/posts`, posts);
app.use(express.static(path.join(__dirname, '../build')));
app.listen(8892, () => {
const message = `App listening on port 8892!`;
console.log(message);
});
Сам модуль, который обрабатывает посты организуем так:
posts
controllers
posts.controller.js
index.js
Где в posts.controller.js
exports.get = (req, res) => {};
exports.delete = (req, res) => {};
exports.add = (req, res) => {};
exports.update = (req, res) => {};
exports.list = (req, res) => {};
А в index.js
const { Router } = require('express');
const Controller = require('./controllers/posts.controller');
const router = Router();
router.post('/', Controller.add);
router.get('/', Controller.list);
router.get('/:id', Controller.get);
router.post('/:id', Controller.update);
router.delete('/:id', Controller.delete);
module.exports = router;
Тут важная ремарка: пользователь для работы с постами должен быть авторизован. У меня есть еще модуль
user
. Но про него не столь интересно.
Опишем тут лишь добавление поста:
const { Post } = require('../../../models'); // модели в sequelize
const { unixtime } = require('../../helpers');
exports.add = (req, res) => {
let { content } = req.body;
await Post.sync();
const post = await Post.create({
updated_at: unixtime(),
created_at: unixtime(),
content
});
res.send({success: true, id: post.id})
}
Вот эта прекрасная строчка await Post.sync();
создаст таблицу со всеми полями из модели. Красота!
Модель Post
примерно такая
const { Sequelize } = require('sequelize');
const sequelize = new Sequelize({
database: 'db_blog',
username: 'root',
host: 'host.docker.internal', // Об этом ниже
port: 1111,
password: 'пароль будет ниже',
dialect: 'mysql',
omitNull: false,
logging: false
});
const { Model, DataTypes } = require('sequelize');
class Post extends Model {}
Post.init(
{
id: {
primaryKey: true,
type: DataTypes.UUID,
defaultValue: Sequelize.UUIDV4
},
created_at: { type: DataTypes.INTEGER.UNSIGNED },
updated_at: { type: DataTypes.INTEGER.UNSIGNED },
// userId: {
// type: DataTypes.UUID,
// references: {
// model: User,
// key: 'id'
// }
// },
content: { type: DataTypes.TEXT, allowNull: true },
isPublic: { type: DataTypes.TINYINT, defaultValue: NO }
},
{
sequelize,
modelName: 'post',
tableName: 'posts',
createdAt: false,
updatedAt: false
}
);
// Post.belongsTo(User);
module.exports.Post = Post;
Я не храню HTML, я храню markdown.
Упаковываем все это добро в Docker
.
FROM node:14-alpine
# Create app directory
WORKDIR /usr/src/app
COPY ./package.json ./
COPY ./package-lock.json ./
# Если у вас TS то нужен еще его конфиг
# COPY ./tsconfig.json ./
RUN npm ci
COPY ./.env ./
COPY ./public ./public
COPY ./src ./src
RUN npm run build
COPY ./server ./server
# Это чтобы оставить зависимости только сервера, нам не нужен реакт, мы его уже сбилдили
RUN rm -rf ./node_modules && export NODE_ENV=production && npm ci
WORKDIR /usr/src/app
COPY --from=0 /usr/src/app .
CMD [ "node", "server/index.js" ]
И создаем файлик .github/workflows/main.yml
. Чтобы Github запускал экшоны на каждый пуш.
Я использую два готовых экшена:
docker/setup-qemu-action
и appleboy/ssh-action
, которые билдят докер образ, пушат его на hub.docker.com, затем заходят по SSH на мой сервер и перезапускают docker-compose. (Я фронтэндер, не бейте =))
name: Build and Push Image
on:
push:
branches: [ master ]
jobs:
main:
runs-on: ubuntu-latest
steps:
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
push: true
tags: my-dockerhub-namespace/xdan-blog:latest
build-args: |
SSH_PRIVATE_KEY=${{ secrets.SSH_PRIVATE_KEY }}
-
name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
- name: Executing remote ssh commands using password
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
password: ${{ secrets.PASSWORD }}
port: ${{ secrets.PORT }}
script: cd /path/xdan-blog && docker-compose pull && docker-compose up -d
Не забываем создать секретики в настройка гитхаба.
Ну и остался файлик docker-compose.yml
на сервере:
version: '3.7'
services:
mysql:
image: mysql
container_name: mysql-xdan-blog
ports:
- '1111:3306'
expose:
- 1111
command: --character-set-server=utf8
--collation-server=utf8_general_ci
--default-authentication-plugin=mysql_native_password
--sql-mode=NO_AUTO_VALUE_ON_ZERO
restart: always
environment:
MYSQL_ROOT_PASSWORD: 'Забейте свой пассворд'
MYSQL_DATABASE: db_blog
volumes:
- './data/dump.sql:/docker-entrypoint-initdb.d/dump.sql'
- './data/data:/var/lib/mysql'
xdan-blog:
image: my-dockerhub-namespace/xdan-blog
ports:
- '8892:8892'
volumes:
- ./data/uploads/images:/usr/src/app/server/uploads/images
extra_hosts:
- 'host.docker.internal:host-gateway'
Запускаем наше чудо:
docker-compose up -d
Вот и все. От идеи, до реализации - два вечера. Теперь у меня есть блог, в котором я написал статью за 20 минут и даже не вспотел.
Потом накрутил вставку картинок и SSR.
Добра вам!