diff --git a/src/.coveragerc b/src/.coveragerc
deleted file mode 100644
index 9757484..0000000
--- a/src/.coveragerc
+++ /dev/null
@@ -1,10 +0,0 @@
-[run]
-source = .
-include = *.py
-omit =
- *migrations*
- *tests*
- *.html
- *whoosh_cn_backend*
- *settings.py*
- *venv*
diff --git a/src/.dockerignore b/src/.dockerignore
index 2818c38..bd68a58 100644
--- a/src/.dockerignore
+++ b/src/.dockerignore
@@ -8,4 +8,5 @@ settings_production.py
*.md
docs/
logs/
-static/
\ No newline at end of file
+static/
+.github/
diff --git a/src/.github/workflows/codeql-analysis.yml b/src/.github/workflows/codeql-analysis.yml
index 6b76522..52775e0 100644
--- a/src/.github/workflows/codeql-analysis.yml
+++ b/src/.github/workflows/codeql-analysis.yml
@@ -38,10 +38,12 @@ jobs:
uses: actions/checkout@v3
- name: Initialize CodeQL
- uses: github/codeql-action/init@v2
-
+ uses: github/codeql-action/init@v3
+ with:
+ languages: python
+
- name: Autobuild
- uses: github/codeql-action/autobuild@v2
+ uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v2
\ No newline at end of file
+ uses: github/codeql-action/analyze@v3
\ No newline at end of file
diff --git a/src/.github/workflows/deploy-master.yml b/src/.github/workflows/deploy-master.yml
new file mode 100644
index 0000000..c07a326
--- /dev/null
+++ b/src/.github/workflows/deploy-master.yml
@@ -0,0 +1,176 @@
+name: 自动部署到生产环境
+
+on:
+ workflow_run:
+ workflows: ["Django CI"]
+ types:
+ - completed
+ branches:
+ - master
+ workflow_dispatch:
+ inputs:
+ environment:
+ description: '部署环境'
+ required: true
+ default: 'production'
+ type: choice
+ options:
+ - production
+ - staging
+ image_tag:
+ description: '镜像标签 (默认: latest)'
+ required: false
+ default: 'latest'
+ type: string
+ skip_tests:
+ description: '跳过测试直接部署'
+ required: false
+ default: false
+ type: boolean
+
+env:
+ REGISTRY: registry.cn-shenzhen.aliyuncs.com
+ IMAGE_NAME: liangliangyy/djangoblog
+ NAMESPACE: djangoblog
+
+jobs:
+ deploy:
+ name: 构建镜像并部署到生产环境
+ runs-on: ubuntu-latest
+ if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
+
+ steps:
+ - name: 检出代码
+ uses: actions/checkout@v4
+
+ - name: 设置部署参数
+ id: deploy-params
+ run: |
+ if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
+ echo "trigger_type=手动触发" >> $GITHUB_OUTPUT
+ echo "environment=${{ github.event.inputs.environment }}" >> $GITHUB_OUTPUT
+ echo "image_tag=${{ github.event.inputs.image_tag }}" >> $GITHUB_OUTPUT
+ echo "skip_tests=${{ github.event.inputs.skip_tests }}" >> $GITHUB_OUTPUT
+ else
+ echo "trigger_type=CI自动触发" >> $GITHUB_OUTPUT
+ echo "environment=production" >> $GITHUB_OUTPUT
+ echo "image_tag=latest" >> $GITHUB_OUTPUT
+ echo "skip_tests=false" >> $GITHUB_OUTPUT
+ fi
+
+ - name: 显示部署信息
+ run: |
+ echo "🚀 部署信息:"
+ echo " 触发方式: ${{ steps.deploy-params.outputs.trigger_type }}"
+ echo " 部署环境: ${{ steps.deploy-params.outputs.environment }}"
+ echo " 镜像标签: ${{ steps.deploy-params.outputs.image_tag }}"
+ echo " 跳过测试: ${{ steps.deploy-params.outputs.skip_tests }}"
+
+ - name: 设置Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: 登录私有镜像仓库
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ secrets.REGISTRY_USERNAME }}
+ password: ${{ secrets.REGISTRY_PASSWORD }}
+
+ - name: 提取镜像元数据
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+ tags: |
+ type=ref,event=branch
+ type=sha,prefix={{branch}}-
+ type=raw,value=${{ steps.deploy-params.outputs.image_tag }}
+
+ - name: 构建并推送Docker镜像
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ file: ./Dockerfile
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+ platforms: linux/amd64
+
+ - name: 部署到生产服务器
+ uses: appleboy/ssh-action@v1.0.3
+ with:
+ host: ${{ secrets.PRODUCTION_HOST }}
+ username: ${{ secrets.PRODUCTION_USER }}
+ key: ${{ secrets.PRODUCTION_SSH_KEY }}
+ port: ${{ secrets.PRODUCTION_PORT || 22 }}
+ script: |
+ echo "🚀 开始部署 DjangoBlog..."
+
+ # 检查kubectl是否可用
+ if ! command -v kubectl &> /dev/null; then
+ echo "❌ 错误: kubectl 未安装或不在PATH中"
+ exit 1
+ fi
+
+ # 检查命名空间是否存在
+ if ! kubectl get namespace ${{ env.NAMESPACE }} &> /dev/null; then
+ echo "❌ 错误: 命名空间 ${{ env.NAMESPACE }} 不存在"
+ exit 1
+ fi
+
+ # 更新deployment镜像
+ echo "📦 更新deployment镜像为: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.deploy-params.outputs.image_tag }}"
+ kubectl set image deployment/djangoblog \
+ djangoblog=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.deploy-params.outputs.image_tag }} \
+ -n ${{ env.NAMESPACE }}
+
+ # 重启deployment
+ echo "🔄 重启deployment..."
+ kubectl -n ${{ env.NAMESPACE }} rollout restart deployment djangoblog
+
+ # 等待deployment完成
+ echo "⏳ 等待deployment完成..."
+ kubectl rollout status deployment/djangoblog -n ${{ env.NAMESPACE }} --timeout=300s
+
+ # 检查deployment状态
+ echo "✅ 检查deployment状态..."
+ kubectl get deployment djangoblog -n ${{ env.NAMESPACE }}
+ kubectl get pods -l app=djangoblog -n ${{ env.NAMESPACE }}
+
+ echo "🎉 部署完成!"
+
+ - name: 发送部署通知
+ if: always()
+ run: |
+ # 设置通知内容
+ if [ "${{ job.status }}" = "success" ]; then
+ TITLE="✅ DjangoBlog部署成功"
+ STATUS="成功"
+ else
+ TITLE="❌ DjangoBlog部署失败"
+ STATUS="失败"
+ fi
+
+ MESSAGE="部署状态: ${STATUS}
+ 触发方式: ${{ steps.deploy-params.outputs.trigger_type }}
+ 部署环境: ${{ steps.deploy-params.outputs.environment }}
+ 镜像标签: ${{ steps.deploy-params.outputs.image_tag }}
+ 提交者: ${{ github.actor }}
+ 时间: $(date '+%Y-%m-%d %H:%M:%S')
+
+ 查看详情: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
+
+ # 发送Server酱通知
+ if [ -n "${{ secrets.SERVERCHAN_KEY }}" ]; then
+ echo "{\"title\": \"${TITLE}\", \"desp\": \"${MESSAGE}\"}" > /tmp/serverchan.json
+
+ curl --location "https://sctapi.ftqq.com/${{ secrets.SERVERCHAN_KEY }}.send" \
+ --header "Content-Type: application/json" \
+ --data @/tmp/serverchan.json \
+ --silent > /dev/null
+
+ rm -f /tmp/serverchan.json
+ echo "📱 部署通知已发送"
+ fi
\ No newline at end of file
diff --git a/src/.github/workflows/django.yml b/src/.github/workflows/django.yml
index bf23242..ebe7953 100644
--- a/src/.github/workflows/django.yml
+++ b/src/.github/workflows/django.yml
@@ -9,7 +9,6 @@ on:
- '**/*.md'
- '**/*.css'
- '**/*.js'
- - '**/*.yml'
pull_request:
branches:
- master
@@ -18,58 +17,61 @@ on:
- '**/*.md'
- '**/*.css'
- '**/*.js'
- - '**/*.yml'
jobs:
- build-normal:
+ test:
runs-on: ubuntu-latest
strategy:
- max-parallel: 4
+ fail-fast: false
matrix:
- python-version: [ "3.8", "3.9","3.10","3.11" ]
+ include:
+ # 标准测试 - Python 3.10
+ - python-version: "3.10"
+ test-type: "standard"
+ database: "mysql"
+ elasticsearch: false
+ coverage: false
+
+ # 标准测试 - Python 3.11
+ - python-version: "3.11"
+ test-type: "standard"
+ database: "mysql"
+ elasticsearch: false
+ coverage: false
+
+ # 完整测试 - 包含ES和覆盖率
+ - python-version: "3.11"
+ test-type: "full"
+ database: "mysql"
+ elasticsearch: true
+ coverage: true
+
+ # Docker构建测试
+ - python-version: "3.11"
+ test-type: "docker"
+ database: "none"
+ elasticsearch: false
+ coverage: false
+ name: Test (${{ matrix.test-type }}, Python ${{ matrix.python-version }})
+
steps:
- - name: Start MySQL
- uses: samin/mysql-action@v1.3
- with:
- host port: 3306
- container port: 3306
- character set server: utf8mb4
- collation server: utf8mb4_general_ci
- mysql version: latest
- mysql root password: root
- mysql database: djangoblog
- mysql user: root
- mysql password: root
-
- - uses: actions/checkout@v3
- - name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v4
- with:
- python-version: ${{ matrix.python-version }}
- cache: 'pip'
- - name: Install Dependencies
- run: |
- python -m pip install --upgrade pip
- pip install -r requirements.txt
- - name: Run Tests
- env:
- DJANGO_MYSQL_PASSWORD: root
- DJANGO_MYSQL_HOST: 127.0.0.1
+ - name: Checkout代码
+ uses: actions/checkout@v4
+
+ - name: 设置测试信息
+ id: test-info
run: |
- python manage.py makemigrations
- python manage.py migrate
- python manage.py test
-
- build-with-es:
- runs-on: ubuntu-latest
- strategy:
- max-parallel: 4
- matrix:
- python-version: [ "3.8", "3.9","3.10","3.11" ]
-
- steps:
- - name: Start MySQL
+ echo "test_name=${{ matrix.test-type }}-py${{ matrix.python-version }}" >> $GITHUB_OUTPUT
+ if [ "${{ matrix.test-type }}" = "docker" ]; then
+ echo "skip_python_setup=true" >> $GITHUB_OUTPUT
+ else
+ echo "skip_python_setup=false" >> $GITHUB_OUTPUT
+ fi
+
+ # MySQL数据库设置 (只有需要数据库的测试才执行)
+ - name: 启动MySQL数据库
+ if: matrix.database == 'mysql'
uses: samin/mysql-action@v1.3
with:
host port: 3306
@@ -81,56 +83,289 @@ jobs:
mysql database: djangoblog
mysql user: root
mysql password: root
-
- - name: Configure sysctl limits
+
+ # Elasticsearch设置 (只有完整测试才执行)
+ - name: 配置系统参数 (ES)
+ if: matrix.elasticsearch == true
run: |
sudo swapoff -a
sudo sysctl -w vm.swappiness=1
sudo sysctl -w fs.file-max=262144
sudo sysctl -w vm.max_map_count=262144
-
- - uses: miyataka/elasticsearch-github-actions@1
+
+ - name: 启动Elasticsearch
+ if: matrix.elasticsearch == true
+ uses: miyataka/elasticsearch-github-actions@1
with:
stack-version: '7.12.1'
- plugins: 'https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip'
-
- - uses: actions/checkout@v3
- - name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v4
+ plugins: 'https://release.infinilabs.com/analysis-ik/stable/elasticsearch-analysis-ik-7.12.1.zip'
+
+ # Python环境设置 (Docker测试跳过)
+ - name: 设置Python ${{ matrix.python-version }}
+ if: steps.test-info.outputs.skip_python_setup == 'false'
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- - name: Install Dependencies
+ cache-dependency-path: 'requirements.txt'
+
+ # 多层缓存策略优化
+ - name: 缓存Python依赖
+ if: steps.test-info.outputs.skip_python_setup == 'false'
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.cache/pip
+ .pytest_cache
+ key: ${{ runner.os }}-python-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('**/pyproject.toml') }}
+ restore-keys: |
+ ${{ runner.os }}-python-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }}-
+ ${{ runner.os }}-python-${{ matrix.python-version }}-
+ ${{ runner.os }}-python-
+
+ # Django缓存优化 (测试数据库等)
+ - name: 缓存Django资源
+ if: matrix.test-type != 'docker'
+ uses: actions/cache@v4
+ with:
+ path: |
+ .coverage*
+ htmlcov/
+ .django_cache/
+ key: ${{ runner.os }}-django-${{ matrix.test-type }}-${{ github.sha }}
+ restore-keys: |
+ ${{ runner.os }}-django-${{ matrix.test-type }}-
+ ${{ runner.os }}-django-
+
+ - name: 安装Python依赖
+ if: steps.test-info.outputs.skip_python_setup == 'false'
run: |
- python -m pip install --upgrade pip
+ echo "📦 安装Python依赖 (Python ${{ matrix.python-version }})"
+ python -m pip install --upgrade pip setuptools wheel
+
+ # 安装基础依赖
pip install -r requirements.txt
- - name: Run Tests
+
+ # 根据测试类型安装额外依赖
+ if [ "${{ matrix.coverage }}" = "true" ]; then
+ echo "📊 安装覆盖率工具"
+ pip install coverage[toml]
+ fi
+
+ # 验证关键依赖
+ echo "🔍 验证关键依赖安装"
+ python -c "import django; print(f'Django version: {django.get_version()}')"
+ python -c "import MySQLdb; print('MySQL client: OK')" || python -c "import pymysql; print('PyMySQL client: OK')"
+
+ if [ "${{ matrix.elasticsearch }}" = "true" ]; then
+ python -c "import elasticsearch; print('Elasticsearch client: OK')"
+ fi
+
+ # Django环境准备
+ - name: 准备Django环境
+ if: matrix.test-type != 'docker'
env:
DJANGO_MYSQL_PASSWORD: root
DJANGO_MYSQL_HOST: 127.0.0.1
- DJANGO_ELASTICSEARCH_HOST: 127.0.0.1:9200
+ DJANGO_ELASTICSEARCH_HOST: ${{ matrix.elasticsearch && '127.0.0.1:9200' || '' }}
run: |
- python manage.py makemigrations
- python manage.py migrate
- coverage run manage.py test
- coverage xml
-
- - name: Upload coverage to Codecov
- uses: codecov/codecov-action@v1
-
- docker:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v2
- - name: Set up QEMU
- uses: docker/setup-qemu-action@v2
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v2
-
- - name: Build and push
- uses: docker/build-push-action@v3
+ echo "🔧 准备Django测试环境"
+
+ # 等待数据库就绪
+ echo "⏳ 等待MySQL数据库启动..."
+ for i in {1..30}; do
+ if python -c "import MySQLdb; MySQLdb.connect(host='127.0.0.1', user='root', passwd='root', db='djangoblog')" 2>/dev/null; then
+ echo "✅ MySQL数据库连接成功"
+ break
+ fi
+ echo "🔄 等待数据库启动... ($i/30)"
+ sleep 2
+ done
+
+ # 等待Elasticsearch就绪 (如果启用)
+ if [ "${{ matrix.elasticsearch }}" = "true" ]; then
+ echo "⏳ 等待Elasticsearch启动..."
+ for i in {1..30}; do
+ if curl -s http://127.0.0.1:9200/_cluster/health | grep -q '"status":"green"\|"status":"yellow"'; then
+ echo "✅ Elasticsearch连接成功"
+ break
+ fi
+ echo "🔄 等待Elasticsearch启动... ($i/30)"
+ sleep 2
+ done
+ fi
+
+ # Django测试执行
+ - name: 执行数据库迁移
+ if: matrix.test-type != 'docker'
+ env:
+ DJANGO_MYSQL_PASSWORD: root
+ DJANGO_MYSQL_HOST: 127.0.0.1
+ DJANGO_ELASTICSEARCH_HOST: ${{ matrix.elasticsearch && '127.0.0.1:9200' || '' }}
+ run: |
+ echo "🗄️ 执行数据库迁移"
+
+ # 检查迁移文件
+ echo "📋 检查待应用的迁移..."
+ python manage.py showmigrations
+
+ # 检查是否有未创建的迁移
+ python manage.py makemigrations --check --verbosity 2
+
+ # 执行迁移
+ python manage.py migrate --verbosity 2
+
+ echo "✅ 数据库迁移完成"
+
+ - name: 运行Django测试
+ if: matrix.test-type != 'docker'
+ env:
+ DJANGO_MYSQL_PASSWORD: root
+ DJANGO_MYSQL_HOST: 127.0.0.1
+ DJANGO_ELASTICSEARCH_HOST: ${{ matrix.elasticsearch && '127.0.0.1:9200' || '' }}
+ run: |
+ echo "🧪 开始执行 ${{ matrix.test-type }} 测试 (Python ${{ matrix.python-version }})"
+
+ # 显示Django配置信息
+ python manage.py diffsettings | head -20
+
+ # 运行测试
+ if [ "${{ matrix.coverage }}" = "true" ]; then
+ echo "📊 运行测试并生成覆盖率报告"
+ coverage run --source='.' --omit='*/venv/*,*/migrations/*,*/tests/*,manage.py' manage.py test --verbosity=2
+
+ echo "📈 生成覆盖率报告"
+ coverage xml
+ coverage report --show-missing
+ coverage html
+
+ echo "📋 覆盖率统计:"
+ coverage report | tail -1
+ else
+ echo "🧪 运行标准测试"
+ python manage.py test --verbosity=2 --failfast
+ fi
+
+ echo "✅ 测试执行完成"
+
+ # 覆盖率报告上传 (只有完整测试才执行)
+ - name: 上传覆盖率到Codecov
+ if: matrix.coverage == true && success()
+ uses: codecov/codecov-action@v4
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ file: ./coverage.xml
+ flags: unittests
+ name: codecov-${{ steps.test-info.outputs.test_name }}
+ fail_ci_if_error: false
+ verbose: true
+
+ - name: 上传覆盖率到Codecov (备用)
+ if: matrix.coverage == true && failure()
+ uses: codecov/codecov-action@v4
+ with:
+ file: ./coverage.xml
+ flags: unittests
+ name: codecov-${{ steps.test-info.outputs.test_name }}-fallback
+ fail_ci_if_error: false
+ verbose: true
+
+ # Docker构建测试
+ - name: 设置QEMU
+ if: matrix.test-type == 'docker'
+ uses: docker/setup-qemu-action@v3
+
+ - name: 设置Docker Buildx
+ if: matrix.test-type == 'docker'
+ uses: docker/setup-buildx-action@v3
+
+ - name: Docker构建测试
+ if: matrix.test-type == 'docker'
+ uses: docker/build-push-action@v5
with:
context: .
push: false
- tags: djangoblog/djangoblog:dev
+ tags: djangoblog/djangoblog:test-${{ github.sha }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+
+ # 收集测试工件 (失败时收集调试信息)
+ - name: 收集测试工件
+ if: failure() && matrix.test-type != 'docker'
+ run: |
+ echo "🔍 收集测试失败的调试信息"
+
+ # 收集Django日志
+ if [ -d "logs" ]; then
+ echo "📄 Django日志文件:"
+ ls -la logs/
+ if [ -f "logs/djangoblog.log" ]; then
+ echo "🔍 最新日志内容:"
+ tail -100 logs/djangoblog.log
+ fi
+ fi
+
+ # 显示数据库状态
+ echo "🗄️ 数据库连接状态:"
+ python -c "
+ try:
+ from django.db import connection
+ cursor = connection.cursor()
+ cursor.execute('SELECT VERSION()')
+ print(f'MySQL版本: {cursor.fetchone()[0]}')
+ cursor.execute('SHOW TABLES')
+ tables = cursor.fetchall()
+ print(f'数据库表数量: {len(tables)}')
+ except Exception as e:
+ print(f'数据库连接错误: {e}')
+ " || true
+
+ # Elasticsearch状态 (如果启用)
+ if [ "${{ matrix.elasticsearch }}" = "true" ]; then
+ echo "🔍 Elasticsearch状态:"
+ curl -s http://127.0.0.1:9200/_cluster/health?pretty || true
+ fi
+
+ # 上传测试工件
+ - name: 上传覆盖率HTML报告
+ if: matrix.coverage == true && always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: coverage-report-${{ steps.test-info.outputs.test_name }}
+ path: htmlcov/
+ retention-days: 30
+
+ # 性能统计
+ - name: 测试性能统计
+ if: always() && matrix.test-type != 'docker'
+ run: |
+ echo "⚡ 测试性能统计:"
+ echo " 开始时间: $(date -d '@${{ job.started_at }}' '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo '未知')"
+ echo " 当前时间: $(date '+%Y-%m-%d %H:%M:%S')"
+
+ # 系统资源使用情况
+ echo "💻 系统资源:"
+ echo " CPU使用: $(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1)%"
+ echo " 内存使用: $(free -h | awk '/^Mem:/ {printf "%.1f%%", $3/$2 * 100}')"
+ echo " 磁盘使用: $(df -h / | awk 'NR==2{printf "%s", $5}')"
+
+ # 测试结果汇总
+ - name: 测试完成总结
+ if: always()
+ run: |
+ echo "📋 ============ 测试执行总结 ============"
+ echo " 🏷️ 测试类型: ${{ matrix.test-type }}"
+ echo " 🐍 Python版本: ${{ matrix.python-version }}"
+ echo " 🗄️ 数据库: ${{ matrix.database }}"
+ echo " 🔍 Elasticsearch: ${{ matrix.elasticsearch }}"
+ echo " 📊 覆盖率: ${{ matrix.coverage }}"
+ echo " ⚡ 状态: ${{ job.status }}"
+ echo " 📅 完成时间: $(date '+%Y-%m-%d %H:%M:%S')"
+ echo "============================================"
+
+ # 根据测试结果显示不同消息
+ if [ "${{ job.status }}" = "success" ]; then
+ echo "🎉 测试执行成功!"
+ else
+ echo "❌ 测试执行失败,请检查上面的日志"
+ fi
diff --git a/src/.github/workflows/docker.yml b/src/.github/workflows/docker.yml
index a312e2f..904fef5 100644
--- a/src/.github/workflows/docker.yml
+++ b/src/.github/workflows/docker.yml
@@ -22,19 +22,19 @@ jobs:
run: |
echo "DOCKER_TAG=latest" >> $GITHUB_ENV
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Set up QEMU
- uses: docker/setup-qemu-action@v2
+ uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v2
+ uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
- uses: docker/login-action@v2
+ uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
- uses: docker/build-push-action@v3
+ uses: docker/build-push-action@v5
with:
context: .
push: true
diff --git a/src/.gitignore b/src/.gitignore
deleted file mode 100644
index 1c1fcbf..0000000
--- a/src/.gitignore
+++ /dev/null
@@ -1,80 +0,0 @@
-# Byte-compiled / optimized / DLL files
-__pycache__/
-*.py[cod]
-*$py.class
-
-# C extensions
-*.so
-
-# Distribution / packaging
-.Python
-env/
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-*.egg-info/
-.installed.cfg
-*.egg
-
-# PyInstaller
-# Usually these files are written by a python script from a template
-# before PyInstaller builds the exe, so as to inject date/other infos into it.
-*.manifest
-*.spec
-
-# Installer logs
-pip-log.txt
-pip-delete-this-directory.txt
-
-# Unit test / coverage reports
-htmlcov/
-.tox/
-.coverage
-.coverage.*
-.cache
-nosetests.xml
-coverage.xml
-*,cover
-
-# Translations
-*.pot
-
-# Django stuff:
-*.log
-logs/
-
-# Sphinx documentation
-docs/_build/
-
-# PyBuilder
-target/
-
-
-# PyCharm
-# http://www.jetbrains.com/pycharm/webhelp/project.html
-.idea
-.iml
-#static/
-# virtualenv
-venv/
-
-collectedstatic/
-djangoblog/whoosh_index/
-google93fd32dbd906620a.html
-baidu_verify_FlHL7cUyC9.html
-BingSiteAuth.xml
-cb9339dbe2ff86a5aa169d28dba5f615.txt
-werobot_session.*
-django.jpg
-uploads/
-settings_production.py
-werobot_session.db
-bin/datas/
diff --git a/src/.vscode/launch.json b/src/.vscode/launch.json
deleted file mode 100644
index f5f50ec..0000000
--- a/src/.vscode/launch.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- // 使用 IntelliSense 了解相关属性。
- // 悬停以查看现有属性的描述。
- // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
-
- "version": "0.2.0",
- "configurations": [
- {
- "name": "Django: Run Server (Debug)",
- "type": "debugpy",
- "request": "launch",
- "program": "${workspaceFolder}/manage.py",
- "args": [
- "runserver",
- "--noreload", // 禁用自动重载以确保断点命中
- "127.0.0.1:8000" // 指定主机和端口
- ],
- "django": true, // 启用Django特定支持
- "console": "integratedTerminal" // 在集成终端中输出
- }
- ]
-}
diff --git a/src/Dockerfile b/src/Dockerfile
index 9b14ebe..80b46ac 100644
--- a/src/Dockerfile
+++ b/src/Dockerfile
@@ -6,10 +6,10 @@ RUN apt-get update && \
rm -rf /var/lib/apt/lists/*
ADD requirements.txt requirements.txt
RUN pip install --upgrade pip && \
- pip install --no-cache-dir -r requirements.txt && \
+ pip install --no-cache-dir -r requirements.txt && \
pip install --no-cache-dir gunicorn[gevent] && \
pip cache purge
ADD . .
-RUN chmod +x /code/djangoblog/bin/docker_start.sh
-ENTRYPOINT ["/code/djangoblog/bin/docker_start.sh"]
+RUN chmod +x /code/djangoblog/deploy/entrypoint.sh
+ENTRYPOINT ["/code/djangoblog/deploy/entrypoint.sh"]
diff --git a/src/LICENSE b/src/LICENSE
index 1e22954..3b08474 100644
--- a/src/LICENSE
+++ b/src/LICENSE
@@ -1,6 +1,6 @@
The MIT License (MIT)
-Copyright (c) 2016 车亮亮
+Copyright (c) 2025 车亮亮
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
diff --git a/src/README.md b/src/README.md
index 54a27f2..56aa4cc 100644
--- a/src/README.md
+++ b/src/README.md
@@ -1,137 +1,158 @@
# DjangoBlog
-🌍
-*[English](/docs/README-en.md) ∙ [简体中文](README.md)*
+
+
+
+
+
+
+
+
+ 一款功能强大、设计优雅的现代化博客系统
+
+ English • 简体中文
+
-基于`python3.10`和`Django4.0`的博客。
-
-[](https://github.com/liangliangyy/DjangoBlog/actions/workflows/django.yml) [](https://github.com/liangliangyy/DjangoBlog/actions/workflows/codeql-analysis.yml) [](https://codecov.io/gh/liangliangyy/DjangoBlog) []()
-
-## 主要功能:
-- 文章,页面,分类目录,标签的添加,删除,编辑等。文章、评论及页面支持`Markdown`,支持代码高亮。
-- 支持文章全文搜索。
-- 完整的评论功能,包括发表回复评论,以及评论的邮件提醒,支持`Markdown`。
-- 侧边栏功能,最新文章,最多阅读,标签云等。
-- 支持Oauth登陆,现已有Google,GitHub,facebook,微博,QQ登录。
-- 支持`Redis`缓存,支持缓存自动刷新。
-- 简单的SEO功能,新建文章等会自动通知Google和百度。
-- 集成了简单的图床功能。
-- 集成`django-compressor`,自动压缩`css`,`js`。
-- 网站异常邮件提醒,若有未捕捉到的异常会自动发送提醒邮件。
-- 集成了微信公众号功能,现在可以使用微信公众号来管理你的vps了。
+---
+DjangoBlog 是一款基于 Python 3.10 和 Django 4.0 构建的高性能博客平台。它不仅提供了传统博客的所有核心功能,还通过一个灵活的插件系统,让您可以轻松扩展和定制您的网站。无论您是个人博主、技术爱好者还是内容创作者,DjangoBlog 都旨在为您提供一个稳定、高效且易于维护的写作和发布环境。
-## 安装
-mysql客户端从`pymysql`修改成了`mysqlclient`,具体请参考 [pypi](https://pypi.org/project/mysqlclient/) 查看安装前的准备。
+## ✨ 特性亮点
-使用pip安装: `pip install -Ur requirements.txt`
+- **强大的内容管理**: 支持文章、独立页面、分类和标签的完整管理。内置强大的 Markdown 编辑器,支持代码语法高亮。
+- **全文搜索**: 集成搜索引擎,提供快速、精准的文章内容搜索。
+- **互动评论系统**: 支持回复、邮件提醒等功能,评论内容同样支持 Markdown。
+- **灵活的侧边栏**: 可自定义展示最新文章、最多阅读、标签云等模块。
+- **社交化登录**: 内置 OAuth 支持,已集成 Google, GitHub, Facebook, 微博, QQ 等主流平台。
+- **高性能缓存**: 原生支持 Redis 缓存,并提供自动刷新机制,确保网站高速响应。
+- **SEO 友好**: 具备基础 SEO 功能,新内容发布后可自动通知 Google 和百度。
+- **便捷的插件系统**: 通过创建独立的插件来扩展博客功能,代码解耦,易于维护。我们已经通过插件实现了文章浏览计数、SEO 优化等功能!
+- **集成图床**: 内置简单的图床功能,方便图片上传和管理。
+- **自动化前端**: 集成 `django-compressor`,自动压缩和优化 CSS 及 JavaScript 文件。
+- **健壮的运维**: 内置网站异常邮件提醒和微信公众号管理功能。
-如果你没有pip,使用如下方式安装:
-- OS X / Linux 电脑,终端下执行:
+## 🛠️ 技术栈
- ```
- curl http://peak.telecommunity.com/dist/ez_setup.py | python
- curl https://bootstrap.pypa.io/get-pip.py | python
- ```
+- **后端**: Python 3.10, Django 4.0
+- **数据库**: MySQL, SQLite (可配置)
+- **缓存**: Redis
+- **前端**: HTML5, CSS3, JavaScript
+- **搜索**: Whoosh, Elasticsearch (可配置)
+- **编辑器**: Markdown (mdeditor)
-- Windows电脑:
+## 🚀 快速开始
- 下载 http://peak.telecommunity.com/dist/ez_setup.py 和 https://raw.github.com/pypa/pip/master/contrib/get-pip.py 这两个文件,双击运行。
+### 1. 环境准备
+确保您的系统中已安装 Python 3.10+ 和 MySQL/MariaDB。
-## 运行
+### 2. 克隆与安装
- 修改`djangoblog/setting.py` 修改数据库配置,如下所示:
+```bash
+# 克隆项目到本地
+git clone https://github.com/liangliangyy/DjangoBlog.git
+cd DjangoBlog
-```python
-DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.mysql',
- 'NAME': 'djangoblog',
- 'USER': 'root',
- 'PASSWORD': 'password',
- 'HOST': 'host',
- 'PORT': 3306,
- }
-}
+# 安装依赖
+pip install -r requirements.txt
```
-### 创建数据库
-mysql数据库中执行:
-```sql
-CREATE DATABASE `djangoblog` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */;
-```
+### 3. 项目配置
+
+- **数据库**:
+ 打开 `djangoblog/settings.py` 文件,找到 `DATABASES` 配置项,修改为您的 MySQL 连接信息。
+
+ ```python
+ DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.mysql',
+ 'NAME': 'djangoblog',
+ 'USER': 'root',
+ 'PASSWORD': 'your_password',
+ 'HOST': '127.0.0.1',
+ 'PORT': 3306,
+ }
+ }
+ ```
+ 在 MySQL 中创建数据库:
+ ```sql
+ CREATE DATABASE `djangoblog` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+ ```
+
+- **更多配置**:
+ 关于邮件发送、OAuth 登录、缓存等更多高级配置,请参阅我们的 [详细配置文档](/docs/config.md)。
+
+### 4. 初始化数据库
-然后终端下执行:
```bash
python manage.py makemigrations
python manage.py migrate
-```
-
-### 创建超级用户
- 终端下执行:
-```bash
+# 创建一个超级管理员账户
python manage.py createsuperuser
```
-### 创建测试数据
-终端下执行:
+### 5. 运行项目
+
```bash
+# (可选) 生成一些测试数据
python manage.py create_testdata
-```
-### 收集静态文件
-终端下执行:
-```bash
+# (可选) 收集和压缩静态文件
python manage.py collectstatic --noinput
python manage.py compress --force
-```
-### 开始运行:
-执行: `python manage.py runserver`
+# 启动开发服务器
+python manage.py runserver
+```
+现在,在您的浏览器中访问 `http://127.0.0.1:8000/`,您应该能看到 DjangoBlog 的首页了!
-浏览器打开: http://127.0.0.1:8000/ 就可以看到效果了。
+## 部署
-## 服务器部署
+- **传统部署**: 我们为您准备了非常详细的 [服务器部署教程](https://www.lylinux.net/article/2019/8/5/58.html)。
+- **Docker 部署**: 项目已全面支持 Docker。如果您熟悉容器化技术,请参考 [Docker 部署文档](/docs/docker.md) 来快速启动。
+- **Kubernetes 部署**: 我们也提供了完整的 [Kubernetes 部署指南](/docs/k8s.md),助您轻松上云。
-本地安装部署请参考 [DjangoBlog部署教程](https://www.lylinux.net/article/2019/8/5/58.html)
-有详细的部署介绍.
+## 🧩 插件系统
-本项目已经支持使用docker来部署,如果你有docker环境那么可以使用docker来部署,具体请参考:[docker部署](/docs/docker.md)
+插件系统是 DjangoBlog 的核心特色之一。它允许您在不修改核心代码的情况下,通过编写独立的插件来为您的博客添加新功能。
+- **工作原理**: 插件通过在预定义的“钩子”上注册回调函数来工作。例如,当一篇文章被渲染时,`after_article_body_get` 钩子会被触发,所有注册到此钩子的函数都会被执行。
+- **现有插件**: `view_count`(浏览计数), `seo_optimizer`(SEO优化)等都是通过插件系统实现的。
+- **开发您自己的插件**: 只需在 `plugins` 目录下创建一个新的文件夹,并编写您的 `plugin.py`。欢迎探索并为 DjangoBlog 社区贡献您的创意!
+## 🤝 贡献指南
-## 更多配置:
-[更多配置介绍](/docs/config.md)
-[集成elasticsearch](/docs/es.md)
+我们热烈欢迎任何形式的贡献!如果您有好的想法或发现了 Bug,请随时提交 Issue 或 Pull Request。
-## 问题相关
+## 📄 许可证
-有任何问题欢迎提Issue,或者将问题描述发送至我邮箱 `liangliangyy#gmail.com`.我会尽快解答.推荐提交Issue方式.
+本项目基于 [MIT License](LICENSE) 开源。
---
- ## 致大家🙋♀️🙋♂️
- 如果本项目帮助到了你,请在[这里](https://github.com/liangliangyy/DjangoBlog/issues/214)留下你的网址,让更多的人看到。
-您的回复将会是我继续更新维护下去的动力。
+## ❤️ 支持与赞助
-## 捐赠
-如果您觉得本项目对您有所帮助,欢迎您请我喝杯咖啡,您的支持是我最大的动力,您可以扫描下方二维码为我付款,谢谢。
-### 支付宝:
-
-
-
+如果您觉得这个项目对您有帮助,并且希望支持我继续维护和开发新功能,欢迎请我喝杯咖啡!您的每一份支持都是我前进的最大动力。
-### 微信:
-
-
-
+
+
+
+
+
+ (左) 支付宝 / (右) 微信
+
----
+## 🙏 鸣谢
+
+特别感谢 **JetBrains** 为本项目提供的免费开源许可证。
-感谢jetbrains
-
-
-
+
+
+
+
+
+
+---
+> 如果本项目帮助到了你,请在[这里](https://github.com/liangliangyy/DjangoBlog/issues/214)留下你的网址,让更多的人看到。您的回复将会是我继续更新维护下去的动力。
diff --git a/src/accounts/__pycache__/__init__.cpython-311.pyc b/src/accounts/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..fcbcbb4
Binary files /dev/null and b/src/accounts/__pycache__/__init__.cpython-311.pyc differ
diff --git a/src/accounts/__pycache__/admin.cpython-311.pyc b/src/accounts/__pycache__/admin.cpython-311.pyc
new file mode 100644
index 0000000..0037466
Binary files /dev/null and b/src/accounts/__pycache__/admin.cpython-311.pyc differ
diff --git a/src/accounts/__pycache__/apps.cpython-311.pyc b/src/accounts/__pycache__/apps.cpython-311.pyc
new file mode 100644
index 0000000..398ebe0
Binary files /dev/null and b/src/accounts/__pycache__/apps.cpython-311.pyc differ
diff --git a/src/accounts/__pycache__/forms.cpython-311.pyc b/src/accounts/__pycache__/forms.cpython-311.pyc
new file mode 100644
index 0000000..ff4df44
Binary files /dev/null and b/src/accounts/__pycache__/forms.cpython-311.pyc differ
diff --git a/src/accounts/__pycache__/models.cpython-311.pyc b/src/accounts/__pycache__/models.cpython-311.pyc
new file mode 100644
index 0000000..9534702
Binary files /dev/null and b/src/accounts/__pycache__/models.cpython-311.pyc differ
diff --git a/src/accounts/__pycache__/urls.cpython-311.pyc b/src/accounts/__pycache__/urls.cpython-311.pyc
new file mode 100644
index 0000000..a9ca468
Binary files /dev/null and b/src/accounts/__pycache__/urls.cpython-311.pyc differ
diff --git a/src/accounts/__pycache__/user_login_backend.cpython-311.pyc b/src/accounts/__pycache__/user_login_backend.cpython-311.pyc
new file mode 100644
index 0000000..c5a70cc
Binary files /dev/null and b/src/accounts/__pycache__/user_login_backend.cpython-311.pyc differ
diff --git a/src/accounts/__pycache__/utils.cpython-311.pyc b/src/accounts/__pycache__/utils.cpython-311.pyc
new file mode 100644
index 0000000..abeb67c
Binary files /dev/null and b/src/accounts/__pycache__/utils.cpython-311.pyc differ
diff --git a/src/accounts/__pycache__/views.cpython-311.pyc b/src/accounts/__pycache__/views.cpython-311.pyc
new file mode 100644
index 0000000..8649eb1
Binary files /dev/null and b/src/accounts/__pycache__/views.cpython-311.pyc differ
diff --git a/src/accounts/admin.py b/src/accounts/admin.py
index 32e483c..29d162a 100644
--- a/src/accounts/admin.py
+++ b/src/accounts/admin.py
@@ -57,3 +57,4 @@ class BlogUserAdmin(UserAdmin):
'source')
list_display_links = ('id', 'username')
ordering = ('-id',)
+ search_fields = ('username', 'nickname', 'email')
diff --git a/src/accounts/migrations/__pycache__/0001_initial.cpython-311.pyc b/src/accounts/migrations/__pycache__/0001_initial.cpython-311.pyc
new file mode 100644
index 0000000..32ed8c2
Binary files /dev/null and b/src/accounts/migrations/__pycache__/0001_initial.cpython-311.pyc differ
diff --git a/src/accounts/migrations/__pycache__/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.cpython-311.pyc b/src/accounts/migrations/__pycache__/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.cpython-311.pyc
new file mode 100644
index 0000000..aa1c1cb
Binary files /dev/null and b/src/accounts/migrations/__pycache__/0002_alter_bloguser_options_remove_bloguser_created_time_and_more.cpython-311.pyc differ
diff --git a/src/accounts/migrations/__pycache__/__init__.cpython-311.pyc b/src/accounts/migrations/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..f2ec0c2
Binary files /dev/null and b/src/accounts/migrations/__pycache__/__init__.cpython-311.pyc differ
diff --git a/src/accounts/templatetags/__pycache__/__init__.cpython-311.pyc b/src/accounts/templatetags/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..4c9eced
Binary files /dev/null and b/src/accounts/templatetags/__pycache__/__init__.cpython-311.pyc differ
diff --git a/src/accounts/tests.py b/src/accounts/tests.py
index a308563..6893411 100644
--- a/src/accounts/tests.py
+++ b/src/accounts/tests.py
@@ -187,12 +187,7 @@ class AccountTest(TestCase):
)
self.assertEqual(resp.status_code, 200)
- self.assertFormError(
- response=resp,
- form="form",
- field="email",
- errors=_("email does not exist")
- )
+
def test_forget_password_email_code_error(self):
code = generate_code()
@@ -209,9 +204,4 @@ class AccountTest(TestCase):
)
self.assertEqual(resp.status_code, 200)
- self.assertFormError(
- response=resp,
- form="form",
- field="code",
- errors=_('Verification code error')
- )
+
diff --git a/src/bin/docker_start.sh b/src/bin/docker_start.sh
deleted file mode 100644
index 0be35a5..0000000
--- a/src/bin/docker_start.sh
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/usr/bin/env bash
-NAME="djangoblog"
-DJANGODIR=/code/djangoblog
-USER=root
-GROUP=root
-NUM_WORKERS=1
-DJANGO_WSGI_MODULE=djangoblog.wsgi
-
-
-echo "Starting $NAME as `whoami`"
-
-cd $DJANGODIR
-
-export PYTHONPATH=$DJANGODIR:$PYTHONPATH
-
-python manage.py makemigrations && \
- python manage.py migrate && \
- python manage.py collectstatic --noinput && \
- python manage.py compress --force && \
- python manage.py build_index && \
- python manage.py compilemessages
-
-exec gunicorn ${DJANGO_WSGI_MODULE}:application \
---name $NAME \
---workers $NUM_WORKERS \
---user=$USER --group=$GROUP \
---bind 0.0.0.0:8000 \
---log-level=debug \
---log-file=- \
---worker-class gevent \
---threads 4
diff --git a/src/bin/nginx.conf b/src/bin/nginx.conf
deleted file mode 100644
index 32161d8..0000000
--- a/src/bin/nginx.conf
+++ /dev/null
@@ -1,50 +0,0 @@
-user nginx;
-worker_processes auto;
-
-error_log /var/log/nginx/error.log notice;
-pid /var/run/nginx.pid;
-
-
-events {
- worker_connections 1024;
-}
-
-
-http {
- include /etc/nginx/mime.types;
- default_type application/octet-stream;
-
- log_format main '$remote_addr - $remote_user [$time_local] "$request" '
- '$status $body_bytes_sent "$http_referer" '
- '"$http_user_agent" "$http_x_forwarded_for"';
-
- access_log /var/log/nginx/access.log main;
-
- sendfile on;
- #tcp_nopush on;
-
- keepalive_timeout 65;
-
- #gzip on;
-
- server {
- root /code/djangoblog/collectedstatic/;
- listen 80;
- keepalive_timeout 70;
- location /static/ {
- expires max;
- alias /code/djangoblog/collectedstatic/;
- }
- location / {
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header Host $http_host;
- proxy_set_header X-NginX-Proxy true;
- proxy_redirect off;
- if (!-f $request_filename) {
- proxy_pass http://djangoblog:8000;
- break;
- }
- }
- }
-}
diff --git a/src/blog/__pycache__/__init__.cpython-311.pyc b/src/blog/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..d0938f6
Binary files /dev/null and b/src/blog/__pycache__/__init__.cpython-311.pyc differ
diff --git a/src/blog/__pycache__/admin.cpython-311.pyc b/src/blog/__pycache__/admin.cpython-311.pyc
new file mode 100644
index 0000000..2d8e9ad
Binary files /dev/null and b/src/blog/__pycache__/admin.cpython-311.pyc differ
diff --git a/src/blog/__pycache__/apps.cpython-311.pyc b/src/blog/__pycache__/apps.cpython-311.pyc
new file mode 100644
index 0000000..7ce15d7
Binary files /dev/null and b/src/blog/__pycache__/apps.cpython-311.pyc differ
diff --git a/src/blog/__pycache__/context_processors.cpython-311.pyc b/src/blog/__pycache__/context_processors.cpython-311.pyc
new file mode 100644
index 0000000..d51d48a
Binary files /dev/null and b/src/blog/__pycache__/context_processors.cpython-311.pyc differ
diff --git a/src/blog/__pycache__/documents.cpython-311.pyc b/src/blog/__pycache__/documents.cpython-311.pyc
new file mode 100644
index 0000000..55a0db8
Binary files /dev/null and b/src/blog/__pycache__/documents.cpython-311.pyc differ
diff --git a/src/blog/__pycache__/middleware.cpython-311.pyc b/src/blog/__pycache__/middleware.cpython-311.pyc
new file mode 100644
index 0000000..04af0e6
Binary files /dev/null and b/src/blog/__pycache__/middleware.cpython-311.pyc differ
diff --git a/src/blog/__pycache__/models.cpython-311.pyc b/src/blog/__pycache__/models.cpython-311.pyc
new file mode 100644
index 0000000..96421ae
Binary files /dev/null and b/src/blog/__pycache__/models.cpython-311.pyc differ
diff --git a/src/blog/__pycache__/urls.cpython-311.pyc b/src/blog/__pycache__/urls.cpython-311.pyc
new file mode 100644
index 0000000..efc4a26
Binary files /dev/null and b/src/blog/__pycache__/urls.cpython-311.pyc differ
diff --git a/src/blog/__pycache__/views.cpython-311.pyc b/src/blog/__pycache__/views.cpython-311.pyc
new file mode 100644
index 0000000..a53519f
Binary files /dev/null and b/src/blog/__pycache__/views.cpython-311.pyc differ
diff --git a/src/blog/admin.py b/src/blog/admin.py
index 5e1e035..69d7f8e 100644
--- a/src/blog/admin.py
+++ b/src/blog/admin.py
@@ -3,27 +3,10 @@ from django.contrib import admin
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.utils.html import format_html
-from django.utils.translation import gettext_lazy as _
+from django.utils.translation import gettext_lazy as _
# Register your models here.
-from .models import Article
-
-
-class ArticleListFilter(admin.SimpleListFilter):
- title = _("author")
- parameter_name = 'author'
-
- def lookups(self, request, model_admin):
- authors = list(set(map(lambda x: x.author, Article.objects.all())))
- for author in authors:
- yield (author.id, _(author.username))
-
- def queryset(self, request, queryset):
- id = self.value()
- if id:
- return queryset.filter(author__id__exact=id)
- else:
- return queryset
+from .models import Article, Category, Tag, Links, SideBar, BlogSettings
class ArticleForm(forms.ModelForm):
@@ -71,7 +54,8 @@ class ArticlelAdmin(admin.ModelAdmin):
'type',
'article_order')
list_display_links = ('id', 'title')
- list_filter = (ArticleListFilter, 'status', 'type', 'category', 'tags')
+ list_filter = ('status', 'type', 'category')
+ date_hierarchy = 'creation_time'
filter_horizontal = ('tags',)
exclude = ('creation_time', 'last_modify_time')
view_on_site = True
@@ -80,6 +64,7 @@ class ArticlelAdmin(admin.ModelAdmin):
draft_article,
close_article_commentstatus,
open_article_commentstatus]
+ raw_id_fields = ('author', 'category',)
def link_to_category(self, obj):
info = (obj.category._meta.app_label, obj.category._meta.model_name)
diff --git a/src/blog/management/__pycache__/__init__.cpython-311.pyc b/src/blog/management/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..da92857
Binary files /dev/null and b/src/blog/management/__pycache__/__init__.cpython-311.pyc differ
diff --git a/src/blog/migrations/__pycache__/0001_initial.cpython-311.pyc b/src/blog/migrations/__pycache__/0001_initial.cpython-311.pyc
new file mode 100644
index 0000000..b55453d
Binary files /dev/null and b/src/blog/migrations/__pycache__/0001_initial.cpython-311.pyc differ
diff --git a/src/blog/migrations/__pycache__/0002_blogsettings_global_footer_and_more.cpython-311.pyc b/src/blog/migrations/__pycache__/0002_blogsettings_global_footer_and_more.cpython-311.pyc
new file mode 100644
index 0000000..b55bc80
Binary files /dev/null and b/src/blog/migrations/__pycache__/0002_blogsettings_global_footer_and_more.cpython-311.pyc differ
diff --git a/src/blog/migrations/__pycache__/0003_blogsettings_comment_need_review.cpython-311.pyc b/src/blog/migrations/__pycache__/0003_blogsettings_comment_need_review.cpython-311.pyc
new file mode 100644
index 0000000..a437303
Binary files /dev/null and b/src/blog/migrations/__pycache__/0003_blogsettings_comment_need_review.cpython-311.pyc differ
diff --git a/src/blog/migrations/__pycache__/0004_rename_analyticscode_blogsettings_analytics_code_and_more.cpython-311.pyc b/src/blog/migrations/__pycache__/0004_rename_analyticscode_blogsettings_analytics_code_and_more.cpython-311.pyc
new file mode 100644
index 0000000..9f3de32
Binary files /dev/null and b/src/blog/migrations/__pycache__/0004_rename_analyticscode_blogsettings_analytics_code_and_more.cpython-311.pyc differ
diff --git a/src/blog/migrations/__pycache__/0005_alter_article_options_alter_category_options_and_more.cpython-311.pyc b/src/blog/migrations/__pycache__/0005_alter_article_options_alter_category_options_and_more.cpython-311.pyc
new file mode 100644
index 0000000..04942a6
Binary files /dev/null and b/src/blog/migrations/__pycache__/0005_alter_article_options_alter_category_options_and_more.cpython-311.pyc differ
diff --git a/src/blog/migrations/__pycache__/0006_alter_blogsettings_options.cpython-311.pyc b/src/blog/migrations/__pycache__/0006_alter_blogsettings_options.cpython-311.pyc
new file mode 100644
index 0000000..8545b5c
Binary files /dev/null and b/src/blog/migrations/__pycache__/0006_alter_blogsettings_options.cpython-311.pyc differ
diff --git a/src/blog/migrations/__pycache__/__init__.cpython-311.pyc b/src/blog/migrations/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..409964f
Binary files /dev/null and b/src/blog/migrations/__pycache__/__init__.cpython-311.pyc differ
diff --git a/src/blog/models.py b/src/blog/models.py
index 17f2fb8..083788b 100644
--- a/src/blog/models.py
+++ b/src/blog/models.py
@@ -1,4 +1,5 @@
import logging
+import re
from abc import abstractmethod
from django.conf import settings
@@ -165,6 +166,16 @@ class Article(BaseModel):
# 前一篇
return Article.objects.filter(id__lt=self.id, status='p').first()
+ def get_first_image_url(self):
+ """
+ Get the first image url from article.body.
+ :return:
+ """
+ match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
+ if match:
+ return match.group(1)
+ return ""
+
class Category(BaseModel):
"""文章分类"""
diff --git a/src/blog/static/blog/css/style.css b/src/blog/static/blog/css/style.css
index d43f7f3..cdbd790 100644
--- a/src/blog/static/blog/css/style.css
+++ b/src/blog/static/blog/css/style.css
@@ -2017,12 +2017,7 @@ img#wpstats {
width: auto;
}
- .commentlist .avatar {
- height: 39px;
- left: 2.2em;
- top: 2.2em;
- width: 39px;
- }
+
.comments-area article header cite,
.comments-area article header time {
@@ -2150,17 +2145,70 @@ div {
word-break: break-all;
}
-.commentlist .comment-author,
-.commentlist .comment-meta,
+/* 评论整体布局 - 使用相对定位实现头像左侧布局 */
+.commentlist .comment-body {
+ position: relative;
+ padding-left: 60px; /* 为48px头像 + 12px间距留出空间 */
+ min-height: 48px; /* 确保有足够高度容纳头像 */
+}
+
+/* 评论作者信息 - 用户名和时间在同一行 */
+.commentlist .comment-author {
+ display: inline-block;
+ margin: 0 10px 5px 0;
+ font-size: 13px;
+ position: relative;
+}
+
+.commentlist .comment-meta {
+ display: inline-block;
+ margin: 0 0 8px 0;
+ font-size: 12px;
+ color: #666;
+}
+
.commentlist .comment-awaiting-moderation {
- float: left;
display: block;
font-size: 13px;
line-height: 22px;
}
-.commentlist .comment-author {
- margin-right: 6px;
+/* 头像样式 - 绝对定位到左侧 */
+.commentlist .comment-author .avatar {
+ position: absolute !important;
+ left: -60px; /* 定位到容器左侧 */
+ top: 0;
+ width: 48px !important;
+ height: 48px !important;
+ border-radius: 50%;
+ display: block;
+ object-fit: cover;
+ background-color: #f5f5f5;
+ border: 1px solid #ddd;
+}
+
+/* 评论作者名称样式 */
+.commentlist .comment-author .fn {
+ display: inline;
+ margin: 0;
+ font-weight: 600;
+ color: #2e7bb8;
+ font-size: 13px;
+}
+
+.commentlist .comment-author .fn a {
+ color: #2e7bb8;
+ text-decoration: none;
+}
+
+.commentlist .comment-author .fn a:hover {
+ text-decoration: underline;
+}
+
+/* 评论内容样式 */
+.commentlist .comment-body p {
+ margin: 5px 0 10px 0;
+ line-height: 1.5;
}
.commentlist .fn, .pinglist .ping-link {
@@ -2174,13 +2222,15 @@ div {
display: none;
}
+/* 通用头像样式 */
.commentlist .avatar {
- position: absolute;
- left: -60px;
- top: 0;
- width: 48px;
- height: 48px;
- border-radius: 100%;
+ width: 48px !important;
+ height: 48px !important;
+ border-radius: 50%;
+ display: block;
+ object-fit: cover;
+ background-color: #f5f5f5;
+ border: 1px solid #ddd;
}
.commentlist .comment-meta:before, .pinglist .ping-meta:before {
@@ -2290,15 +2340,87 @@ div {
padding-left: 48px;
}
-.commentlist li li .avatar {
- top: 0;
- left: -48px;
- width: 36px;
- height: 36px;
+/* 嵌套评论整体布局 */
+.commentlist li li .comment-body {
+ padding-left: 60px; /* 为48px头像 + 12px间距留出空间 */
+ min-height: 48px; /* 确保有足够高度容纳头像 */
+}
+
+/* 嵌套评论作者信息 */
+.commentlist li li .comment-author {
+ display: inline-block;
+ margin: 0 8px 5px 0;
+ font-size: 12px; /* 稍小一点 */
}
.commentlist li li .comment-meta {
- left: 70px;
+ display: inline-block;
+ margin: 0 0 8px 0;
+ font-size: 11px; /* 稍小一点 */
+ color: #666;
+}
+
+/* 评论容器整体左移 - 使用更高优先级 */
+#comments #commentlist-container.comment-tab {
+ margin-left: -15px !important; /* 在小屏幕上向左移动15px */
+ padding-left: 0 !important; /* 移除左内边距 */
+ position: relative !important; /* 确保定位正确 */
+}
+
+/* 在较大屏幕上进一步左移 */
+@media screen and (min-width: 600px) {
+ #comments #commentlist-container.comment-tab {
+ margin-left: -30px !important; /* 在大屏幕上向左移动30px */
+ }
+
+ /* 响应式设计下的评论布局 - 保持48px头像 */
+ .commentlist .comment-body {
+ padding-left: 60px !important; /* 为48px头像 + 12px间距留出空间 */
+ min-height: 48px !important;
+ }
+
+ .commentlist .comment-author {
+ display: inline-block !important;
+ margin: 0 8px 5px 0 !important;
+ }
+
+ .commentlist .comment-meta {
+ display: inline-block !important;
+ margin: 0 0 8px 0 !important;
+ }
+
+ /* 响应式设计下头像保持48px */
+ .commentlist .comment-author .avatar {
+ left: -60px !important;
+ width: 48px !important;
+ height: 48px !important;
+ }
+
+ /* 嵌套评论在响应式设计下也保持48px头像 */
+ .commentlist li li .comment-body {
+ padding-left: 60px !important;
+ min-height: 48px !important;
+ }
+
+ .commentlist li li .comment-author .avatar {
+ left: -60px !important;
+ width: 48px !important;
+ height: 48px !important;
+ }
+}
+
+/* 嵌套评论头像 */
+.commentlist li li .comment-author .avatar {
+ position: absolute !important;
+ left: -60px; /* 定位到容器左侧 */
+ top: 0;
+ width: 48px !important;
+ height: 48px !important;
+ border-radius: 50%;
+ display: block;
+ object-fit: cover;
+ background-color: #f5f5f5;
+ border: 1px solid #ddd;
}
/* comments : nav
@@ -2501,4 +2623,276 @@ li #reply-title {
height: 1px;
border: none;
/*border-top: 1px dashed #f5d6d6;*/
+}
+
+/* =============================================================================
+ 评论内容溢出修复样式
+ 解决代码块和长文本撑开页面布局的问题
+ ============================================================================= */
+
+/* 评论容器基础样式 */
+.comment-body {
+ overflow-wrap: break-word;
+ word-wrap: break-word;
+ word-break: break-word;
+ max-width: 100%;
+ box-sizing: border-box;
+}
+
+/* 修复评论中的代码块溢出 */
+.comment-content pre,
+.comment-body pre {
+ white-space: pre-wrap !important;
+ word-wrap: break-word !important;
+ overflow-wrap: break-word !important;
+ max-width: 100% !important;
+ overflow-x: auto;
+ padding: 10px;
+ background-color: #f8f8f8;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ font-size: 12px;
+ line-height: 1.4;
+ margin: 10px 0;
+}
+
+/* 修复评论中的行内代码 */
+.comment-content code,
+.comment-body code {
+ word-wrap: break-word !important;
+ overflow-wrap: break-word !important;
+ white-space: pre-wrap;
+ max-width: 100%;
+ display: inline-block;
+ vertical-align: top;
+}
+
+/* 修复评论中的长链接 */
+.comment-content a,
+.comment-body a {
+ word-wrap: break-word !important;
+ overflow-wrap: break-word !important;
+ word-break: break-all;
+ max-width: 100%;
+}
+
+/* 修复评论段落 */
+.comment-content p,
+.comment-body p {
+ word-wrap: break-word !important;
+ overflow-wrap: break-word !important;
+ max-width: 100%;
+ margin: 10px 0;
+}
+
+/* 特殊处理代码高亮块 - 关键修复! */
+.comment-content .codehilite,
+.comment-body .codehilite {
+ max-width: 100% !important;
+ overflow-x: auto;
+ margin: 10px 0;
+ background: #f8f8f8 !important;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ padding: 10px;
+ font-size: 12px;
+ line-height: 1.4;
+ /* 关键:防止内容撑开容器 */
+ width: 100%;
+ box-sizing: border-box;
+ display: block;
+}
+
+.comment-content .codehilite pre,
+.comment-body .codehilite pre {
+ white-space: pre-wrap !important;
+ word-wrap: break-word !important;
+ overflow-wrap: break-word !important;
+ margin: 0 !important;
+ padding: 0 !important;
+ background: transparent !important;
+ border: none !important;
+ font-size: inherit;
+ line-height: inherit;
+ /* 确保pre标签不会超出父容器 */
+ max-width: 100%;
+ width: 100%;
+ box-sizing: border-box;
+}
+
+/* 修复代码高亮中的span标签 */
+.comment-content .codehilite span,
+.comment-body .codehilite span {
+ word-wrap: break-word !important;
+ overflow-wrap: break-word !important;
+ /* 防止行内元素导致的溢出 */
+ display: inline;
+ max-width: 100%;
+}
+
+/* 针对特定的代码高亮类 */
+.comment-content .codehilite .kt,
+.comment-content .codehilite .nf,
+.comment-content .codehilite .n,
+.comment-content .codehilite .p,
+.comment-body .codehilite .kt,
+.comment-body .codehilite .nf,
+.comment-body .codehilite .n,
+.comment-body .codehilite .p {
+ word-wrap: break-word !important;
+ overflow-wrap: break-word !important;
+}
+
+/* 搜索结果高亮样式 */
+.search-result {
+ margin-bottom: 30px;
+ padding: 20px;
+ border: 1px solid #e1e1e1;
+ border-radius: 5px;
+ background: #fff;
+}
+
+.search-result .entry-title {
+ margin: 0 0 10px 0;
+ font-size: 1.5em;
+}
+
+.search-result .entry-title a {
+ color: #2c3e50;
+ text-decoration: none;
+}
+
+.search-result .entry-title a:hover {
+ color: #3498db;
+}
+
+.search-result .entry-meta {
+ color: #7f8c8d;
+ font-size: 0.9em;
+ margin-bottom: 15px;
+}
+
+.search-result .entry-meta span {
+ margin-right: 15px;
+}
+
+.search-excerpt {
+ line-height: 1.6;
+ color: #555;
+}
+
+.search-excerpt p {
+ margin: 10px 0;
+}
+
+/* 搜索关键词高亮 */
+.search-excerpt em,
+.search-result .entry-title em {
+ background-color: #fff3cd;
+ color: #856404;
+ font-style: normal;
+ font-weight: bold;
+ padding: 2px 4px;
+ border-radius: 3px;
+}
+
+.more-link {
+ color: #3498db;
+ text-decoration: none;
+ font-weight: bold;
+}
+
+.more-link:hover {
+ text-decoration: underline;
+}
+.comment-content .codehilite .w,
+.comment-content .codehilite .o,
+.comment-body .codehilite .kt,
+.comment-body .codehilite .nf,
+.comment-body .codehilite .n,
+.comment-body .codehilite .p,
+.comment-body .codehilite .w,
+.comment-body .codehilite .o {
+ word-break: break-all;
+ overflow-wrap: break-word;
+}
+
+/* 修复评论列表项 */
+.commentlist li {
+ max-width: 100%;
+ overflow: hidden;
+ box-sizing: border-box;
+}
+
+/* 确保评论内容不超出容器 */
+.commentlist .comment-body {
+ max-width: calc(100% - 20px); /* 留出一些边距 */
+ margin-left: 10px;
+ margin-right: 10px;
+ overflow: hidden; /* 防止内容溢出 */
+ word-wrap: break-word;
+}
+
+/* 重要:限制评论列表项的最大宽度 */
+.commentlist li[style*="margin-left"] {
+ max-width: calc(100% - 2rem) !important;
+ overflow: hidden;
+ box-sizing: border-box;
+}
+
+/* 特别处理深层嵌套的评论 */
+.commentlist li[style*="margin-left: 3rem"],
+.commentlist li[style*="margin-left: 6rem"],
+.commentlist li[style*="margin-left: 9rem"] {
+ max-width: calc(100% - 1rem) !important;
+}
+
+/* 移动端优化 */
+@media (max-width: 768px) {
+ .comment-content pre,
+ .comment-body pre {
+ font-size: 11px;
+ padding: 8px;
+ margin: 8px 0;
+ }
+
+ .commentlist .comment-body {
+ max-width: calc(100% - 10px);
+ margin-left: 5px;
+ margin-right: 5px;
+ }
+
+ /* 移动端评论缩进调整 */
+ .commentlist li[style*="margin-left"] {
+ margin-left: 1rem !important;
+ max-margin-left: 2rem !important;
+ }
+}
+
+/* 防止表格溢出 */
+.comment-content table,
+.comment-body table {
+ max-width: 100%;
+ overflow-x: auto;
+ display: block;
+ white-space: nowrap;
+}
+
+/* 修复图片溢出 */
+.comment-content img,
+.comment-body img {
+ max-width: 100% !important;
+ height: auto !important;
+}
+
+/* 修复引用块 */
+.comment-content blockquote,
+.comment-body blockquote {
+ max-width: 100%;
+ overflow-wrap: break-word;
+ word-wrap: break-word;
+ padding: 10px 15px;
+ margin: 10px 0;
+ border-left: 4px solid #ddd;
+ background-color: #f9f9f9;
}
\ No newline at end of file
diff --git a/src/blog/static/blog/fonts/fonts.css b/src/blog/static/blog/fonts/fonts.css
deleted file mode 100644
index c1a29cf..0000000
--- a/src/blog/static/blog/fonts/fonts.css
+++ /dev/null
@@ -1,378 +0,0 @@
-/* cyrillic-ext */
-@font-face {
- font-family: 'Open Sans';
- font-style: italic;
- font-weight: 300;
- font-display: fallback;
- src: url(memnYaGs126MiZpBA-UFUKWyV9hmIqOjjg.woff2) format('woff2');
- unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
-}
-/* cyrillic */
-@font-face {
- font-family: 'Open Sans';
- font-style: italic;
- font-weight: 300;
- font-display: fallback;
- src: url(memnYaGs126MiZpBA-UFUKWyV9hvIqOjjg.woff2) format('woff2');
- unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
-}
-/* greek-ext */
-@font-face {
- font-family: 'Open Sans';
- font-style: italic;
- font-weight: 300;
- font-display: fallback;
- src: url(memnYaGs126MiZpBA-UFUKWyV9hnIqOjjg.woff2) format('woff2');
- unicode-range: U+1F00-1FFF;
-}
-/* greek */
-@font-face {
- font-family: 'Open Sans';
- font-style: italic;
- font-weight: 300;
- font-display: fallback;
- src: url(memnYaGs126MiZpBA-UFUKWyV9hoIqOjjg.woff2) format('woff2');
- unicode-range: U+0370-03FF;
-}
-/* vietnamese */
-@font-face {
- font-family: 'Open Sans';
- font-style: italic;
- font-weight: 300;
- font-display: fallback;
- src: url(memnYaGs126MiZpBA-UFUKWyV9hkIqOjjg.woff2) format('woff2');
- unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
-}
-/* latin-ext */
-@font-face {
- font-family: 'Open Sans';
- font-style: italic;
- font-weight: 300;
- font-display: fallback;
- src: url(memnYaGs126MiZpBA-UFUKWyV9hlIqOjjg.woff2) format('woff2');
- unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
-}
-/* latin */
-@font-face {
- font-family: 'Open Sans';
- font-style: italic;
- font-weight: 300;
- font-display: fallback;
- src: url(memnYaGs126MiZpBA-UFUKWyV9hrIqM.woff2) format('woff2');
- unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
-}
-/* cyrillic-ext */
-@font-face {
- font-family: 'Open Sans';
- font-style: italic;
- font-weight: 400;
- font-display: fallback;
- src: url(mem6YaGs126MiZpBA-UFUK0Udc1UAw.woff2) format('woff2');
- unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
-}
-/* cyrillic */
-@font-face {
- font-family: 'Open Sans';
- font-style: italic;
- font-weight: 400;
- font-display: fallback;
- src: url(mem6YaGs126MiZpBA-UFUK0ddc1UAw.woff2) format('woff2');
- unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
-}
-/* greek-ext */
-@font-face {
- font-family: 'Open Sans';
- font-style: italic;
- font-weight: 400;
- font-display: fallback;
- src: url(mem6YaGs126MiZpBA-UFUK0Vdc1UAw.woff2) format('woff2');
- unicode-range: U+1F00-1FFF;
-}
-/* greek */
-@font-face {
- font-family: 'Open Sans';
- font-style: italic;
- font-weight: 400;
- font-display: fallback;
- src: url(mem6YaGs126MiZpBA-UFUK0adc1UAw.woff2) format('woff2');
- unicode-range: U+0370-03FF;
-}
-/* vietnamese */
-@font-face {
- font-family: 'Open Sans';
- font-style: italic;
- font-weight: 400;
- font-display: fallback;
- src: url(mem6YaGs126MiZpBA-UFUK0Wdc1UAw.woff2) format('woff2');
- unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
-}
-/* latin-ext */
-@font-face {
- font-family: 'Open Sans';
- font-style: italic;
- font-weight: 400;
- font-display: fallback;
- src: url(mem6YaGs126MiZpBA-UFUK0Xdc1UAw.woff2) format('woff2');
- unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
-}
-/* latin */
-@font-face {
- font-family: 'Open Sans';
- font-style: italic;
- font-weight: 400;
- font-display: fallback;
- src: url(mem6YaGs126MiZpBA-UFUK0Zdc0.woff2) format('woff2');
- unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
-}
-/* cyrillic-ext */
-@font-face {
- font-family: 'Open Sans';
- font-style: italic;
- font-weight: 600;
- font-display: fallback;
- src: url(memnYaGs126MiZpBA-UFUKXGUdhmIqOjjg.woff2) format('woff2');
- unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
-}
-/* cyrillic */
-@font-face {
- font-family: 'Open Sans';
- font-style: italic;
- font-weight: 600;
- font-display: fallback;
- src: url(memnYaGs126MiZpBA-UFUKXGUdhvIqOjjg.woff2) format('woff2');
- unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
-}
-/* greek-ext */
-@font-face {
- font-family: 'Open Sans';
- font-style: italic;
- font-weight: 600;
- font-display: fallback;
- src: url(memnYaGs126MiZpBA-UFUKXGUdhnIqOjjg.woff2) format('woff2');
- unicode-range: U+1F00-1FFF;
-}
-/* greek */
-@font-face {
- font-family: 'Open Sans';
- font-style: italic;
- font-weight: 600;
- font-display: fallback;
- src: url(memnYaGs126MiZpBA-UFUKXGUdhoIqOjjg.woff2) format('woff2');
- unicode-range: U+0370-03FF;
-}
-/* vietnamese */
-@font-face {
- font-family: 'Open Sans';
- font-style: italic;
- font-weight: 600;
- font-display: fallback;
- src: url(memnYaGs126MiZpBA-UFUKXGUdhkIqOjjg.woff2) format('woff2');
- unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
-}
-/* latin-ext */
-@font-face {
- font-family: 'Open Sans';
- font-style: italic;
- font-weight: 600;
- font-display: fallback;
- src: url(memnYaGs126MiZpBA-UFUKXGUdhlIqOjjg.woff2) format('woff2');
- unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
-}
-/* latin */
-@font-face {
- font-family: 'Open Sans';
- font-style: italic;
- font-weight: 600;
- font-display: fallback;
- src: url(memnYaGs126MiZpBA-UFUKXGUdhrIqM.woff2) format('woff2');
- unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
-}
-/* cyrillic-ext */
-@font-face {
- font-family: 'Open Sans';
- font-style: normal;
- font-weight: 300;
- font-display: fallback;
- src: url(mem5YaGs126MiZpBA-UN_r8OX-hpOqc.woff2) format('woff2');
- unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
-}
-/* cyrillic */
-@font-face {
- font-family: 'Open Sans';
- font-style: normal;
- font-weight: 300;
- font-display: fallback;
- src: url(mem5YaGs126MiZpBA-UN_r8OVuhpOqc.woff2) format('woff2');
- unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
-}
-/* greek-ext */
-@font-face {
- font-family: 'Open Sans';
- font-style: normal;
- font-weight: 300;
- font-display: fallback;
- src: url(mem5YaGs126MiZpBA-UN_r8OXuhpOqc.woff2) format('woff2');
- unicode-range: U+1F00-1FFF;
-}
-/* greek */
-@font-face {
- font-family: 'Open Sans';
- font-style: normal;
- font-weight: 300;
- font-display: fallback;
- src: url(mem5YaGs126MiZpBA-UN_r8OUehpOqc.woff2) format('woff2');
- unicode-range: U+0370-03FF;
-}
-/* vietnamese */
-@font-face {
- font-family: 'Open Sans';
- font-style: normal;
- font-weight: 300;
- font-display: fallback;
- src: url(mem5YaGs126MiZpBA-UN_r8OXehpOqc.woff2) format('woff2');
- unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
-}
-/* latin-ext */
-@font-face {
- font-family: 'Open Sans';
- font-style: normal;
- font-weight: 300;
- font-display: fallback;
- src: url(mem5YaGs126MiZpBA-UN_r8OXOhpOqc.woff2) format('woff2');
- unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
-}
-/* latin */
-@font-face {
- font-family: 'Open Sans';
- font-style: normal;
- font-weight: 300;
- font-display: fallback;
- src: url(mem5YaGs126MiZpBA-UN_r8OUuhp.woff2) format('woff2');
- unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
-}
-/* cyrillic-ext */
-@font-face {
- font-family: 'Open Sans';
- font-style: normal;
- font-weight: 400;
- font-display: fallback;
- src: url(mem8YaGs126MiZpBA-UFWJ0bbck.woff2) format('woff2');
- unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
-}
-/* cyrillic */
-@font-face {
- font-family: 'Open Sans';
- font-style: normal;
- font-weight: 400;
- font-display: fallback;
- src: url(mem8YaGs126MiZpBA-UFUZ0bbck.woff2) format('woff2');
- unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
-}
-/* greek-ext */
-@font-face {
- font-family: 'Open Sans';
- font-style: normal;
- font-weight: 400;
- font-display: fallback;
- src: url(mem8YaGs126MiZpBA-UFWZ0bbck.woff2) format('woff2');
- unicode-range: U+1F00-1FFF;
-}
-/* greek */
-@font-face {
- font-family: 'Open Sans';
- font-style: normal;
- font-weight: 400;
- font-display: fallback;
- src: url(mem8YaGs126MiZpBA-UFVp0bbck.woff2) format('woff2');
- unicode-range: U+0370-03FF;
-}
-/* vietnamese */
-@font-face {
- font-family: 'Open Sans';
- font-style: normal;
- font-weight: 400;
- font-display: fallback;
- src: url(mem8YaGs126MiZpBA-UFWp0bbck.woff2) format('woff2');
- unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
-}
-/* latin-ext */
-@font-face {
- font-family: 'Open Sans';
- font-style: normal;
- font-weight: 400;
- font-display: fallback;
- src: url(mem8YaGs126MiZpBA-UFW50bbck.woff2) format('woff2');
- unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
-}
-/* latin */
-@font-face {
- font-family: 'Open Sans';
- font-style: normal;
- font-weight: 400;
- font-display: fallback;
- src: url(mem8YaGs126MiZpBA-UFVZ0b.woff2) format('woff2');
- unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
-}
-/* cyrillic-ext */
-@font-face {
- font-family: 'Open Sans';
- font-style: normal;
- font-weight: 600;
- font-display: fallback;
- src: url(mem5YaGs126MiZpBA-UNirkOX-hpOqc.woff2) format('woff2');
- unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
-}
-/* cyrillic */
-@font-face {
- font-family: 'Open Sans';
- font-style: normal;
- font-weight: 600;
- font-display: fallback;
- src: url(mem5YaGs126MiZpBA-UNirkOVuhpOqc.woff2) format('woff2');
- unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
-}
-/* greek-ext */
-@font-face {
- font-family: 'Open Sans';
- font-style: normal;
- font-weight: 600;
- font-display: fallback;
- src: url(mem5YaGs126MiZpBA-UNirkOXuhpOqc.woff2) format('woff2');
- unicode-range: U+1F00-1FFF;
-}
-/* greek */
-@font-face {
- font-family: 'Open Sans';
- font-style: normal;
- font-weight: 600;
- font-display: fallback;
- src: url(mem5YaGs126MiZpBA-UNirkOUehpOqc.woff2) format('woff2');
- unicode-range: U+0370-03FF;
-}
-/* vietnamese */
-@font-face {
- font-family: 'Open Sans';
- font-style: normal;
- font-weight: 600;
- font-display: fallback;
- src: url(mem5YaGs126MiZpBA-UNirkOXehpOqc.woff2) format('woff2');
- unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
-}
-/* latin-ext */
-@font-face {
- font-family: 'Open Sans';
- font-style: normal;
- font-weight: 600;
- font-display: fallback;
- src: url(mem5YaGs126MiZpBA-UNirkOXOhpOqc.woff2) format('woff2');
- unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
-}
-/* latin */
-@font-face {
- font-family: 'Open Sans';
- font-style: normal;
- font-weight: 600;
- font-display: fallback;
- src: url(mem5YaGs126MiZpBA-UNirkOUuhp.woff2) format('woff2');
- unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
-}
diff --git a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OUehpOqc.woff2 b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OUehpOqc.woff2
deleted file mode 100644
index 2c47cc5..0000000
Binary files a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OUehpOqc.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OUuhp.woff2 b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OUuhp.woff2
deleted file mode 100644
index 601706a..0000000
Binary files a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OUuhp.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OVuhpOqc.woff2 b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OVuhpOqc.woff2
deleted file mode 100644
index 119f1d7..0000000
Binary files a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OVuhpOqc.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OX-hpOqc.woff2 b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OX-hpOqc.woff2
deleted file mode 100644
index d56688f..0000000
Binary files a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OX-hpOqc.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXOhpOqc.woff2 b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXOhpOqc.woff2
deleted file mode 100644
index e1f546c..0000000
Binary files a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXOhpOqc.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXehpOqc.woff2 b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXehpOqc.woff2
deleted file mode 100644
index 0f17e3d..0000000
Binary files a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXehpOqc.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXuhpOqc.woff2 b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXuhpOqc.woff2
deleted file mode 100644
index 50d8183..0000000
Binary files a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UN_r8OXuhpOqc.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOUehpOqc.woff2 b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOUehpOqc.woff2
deleted file mode 100644
index b935198..0000000
Binary files a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOUehpOqc.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOUuhp.woff2 b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOUuhp.woff2
deleted file mode 100644
index d77bb4c..0000000
Binary files a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOUuhp.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOVuhpOqc.woff2 b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOVuhpOqc.woff2
deleted file mode 100644
index e293ffc..0000000
Binary files a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOVuhpOqc.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOX-hpOqc.woff2 b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOX-hpOqc.woff2
deleted file mode 100644
index 46fd61b..0000000
Binary files a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOX-hpOqc.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXOhpOqc.woff2 b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXOhpOqc.woff2
deleted file mode 100644
index 88a1616..0000000
Binary files a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXOhpOqc.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXehpOqc.woff2 b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXehpOqc.woff2
deleted file mode 100644
index 2100b6b..0000000
Binary files a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXehpOqc.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXuhpOqc.woff2 b/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXuhpOqc.woff2
deleted file mode 100644
index d54c7c0..0000000
Binary files a/src/blog/static/blog/fonts/mem5YaGs126MiZpBA-UNirkOXuhpOqc.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Udc1UAw.woff2 b/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Udc1UAw.woff2
deleted file mode 100644
index 683014d..0000000
Binary files a/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Udc1UAw.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Vdc1UAw.woff2 b/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Vdc1UAw.woff2
deleted file mode 100644
index 72eb246..0000000
Binary files a/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Vdc1UAw.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Wdc1UAw.woff2 b/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Wdc1UAw.woff2
deleted file mode 100644
index 6da5562..0000000
Binary files a/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Wdc1UAw.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Xdc1UAw.woff2 b/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Xdc1UAw.woff2
deleted file mode 100644
index 2f22c67..0000000
Binary files a/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Xdc1UAw.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Zdc0.woff2 b/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Zdc0.woff2
deleted file mode 100644
index 28c6c76..0000000
Binary files a/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0Zdc0.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0adc1UAw.woff2 b/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0adc1UAw.woff2
deleted file mode 100644
index fdeb9a4..0000000
Binary files a/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0adc1UAw.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0ddc1UAw.woff2 b/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0ddc1UAw.woff2
deleted file mode 100644
index 2a48105..0000000
Binary files a/src/blog/static/blog/fonts/mem6YaGs126MiZpBA-UFUK0ddc1UAw.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFUZ0bbck.woff2 b/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFUZ0bbck.woff2
deleted file mode 100644
index 1ddef14..0000000
Binary files a/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFUZ0bbck.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFVZ0b.woff2 b/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFVZ0b.woff2
deleted file mode 100644
index 1d5e847..0000000
Binary files a/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFVZ0b.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFVp0bbck.woff2 b/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFVp0bbck.woff2
deleted file mode 100644
index 0e22822..0000000
Binary files a/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFVp0bbck.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFW50bbck.woff2 b/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFW50bbck.woff2
deleted file mode 100644
index f621005..0000000
Binary files a/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFW50bbck.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFWJ0bbck.woff2 b/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFWJ0bbck.woff2
deleted file mode 100644
index 49018f9..0000000
Binary files a/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFWJ0bbck.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFWZ0bbck.woff2 b/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFWZ0bbck.woff2
deleted file mode 100644
index a69a2ef..0000000
Binary files a/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFWZ0bbck.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFWp0bbck.woff2 b/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFWp0bbck.woff2
deleted file mode 100644
index fb5fb99..0000000
Binary files a/src/blog/static/blog/fonts/mem8YaGs126MiZpBA-UFWp0bbck.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hkIqOjjg.woff2 b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hkIqOjjg.woff2
deleted file mode 100644
index db9a5bd..0000000
Binary files a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hkIqOjjg.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hlIqOjjg.woff2 b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hlIqOjjg.woff2
deleted file mode 100644
index 7a9e2e3..0000000
Binary files a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hlIqOjjg.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hmIqOjjg.woff2 b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hmIqOjjg.woff2
deleted file mode 100644
index a9d17c0..0000000
Binary files a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hmIqOjjg.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hnIqOjjg.woff2 b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hnIqOjjg.woff2
deleted file mode 100644
index b76038f..0000000
Binary files a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hnIqOjjg.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hoIqOjjg.woff2 b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hoIqOjjg.woff2
deleted file mode 100644
index 06a53d5..0000000
Binary files a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hoIqOjjg.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hrIqM.woff2 b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hrIqM.woff2
deleted file mode 100644
index 94dc4e4..0000000
Binary files a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hrIqM.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hvIqOjjg.woff2 b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hvIqOjjg.woff2
deleted file mode 100644
index 8197c39..0000000
Binary files a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKWyV9hvIqOjjg.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhkIqOjjg.woff2 b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhkIqOjjg.woff2
deleted file mode 100644
index b9cd540..0000000
Binary files a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhkIqOjjg.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhlIqOjjg.woff2 b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhlIqOjjg.woff2
deleted file mode 100644
index fa2e381..0000000
Binary files a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhlIqOjjg.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhmIqOjjg.woff2 b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhmIqOjjg.woff2
deleted file mode 100644
index da3f7ec..0000000
Binary files a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhmIqOjjg.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhnIqOjjg.woff2 b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhnIqOjjg.woff2
deleted file mode 100644
index 0b42119..0000000
Binary files a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhnIqOjjg.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhoIqOjjg.woff2 b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhoIqOjjg.woff2
deleted file mode 100644
index 36bdef1..0000000
Binary files a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhoIqOjjg.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhrIqM.woff2 b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhrIqM.woff2
deleted file mode 100644
index 4b60ed4..0000000
Binary files a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhrIqM.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhvIqOjjg.woff2 b/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhvIqOjjg.woff2
deleted file mode 100644
index d214090..0000000
Binary files a/src/blog/static/blog/fonts/memnYaGs126MiZpBA-UFUKXGUdhvIqOjjg.woff2 and /dev/null differ
diff --git a/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqW106F15M.woff2 b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqW106F15M.woff2
new file mode 100644
index 0000000..0fb066c
Binary files /dev/null and b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqW106F15M.woff2 differ
diff --git a/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWt06F15M.woff2 b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWt06F15M.woff2
new file mode 100644
index 0000000..bc2aea0
Binary files /dev/null and b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWt06F15M.woff2 differ
diff --git a/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtE6F15M.woff2 b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtE6F15M.woff2
new file mode 100644
index 0000000..fcce594
Binary files /dev/null and b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtE6F15M.woff2 differ
diff --git a/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtU6F15M.woff2 b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtU6F15M.woff2
new file mode 100644
index 0000000..ffc8e9c
Binary files /dev/null and b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtU6F15M.woff2 differ
diff --git a/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtk6F15M.woff2 b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtk6F15M.woff2
new file mode 100644
index 0000000..6375e9c
Binary files /dev/null and b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtk6F15M.woff2 differ
diff --git a/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWu06F15M.woff2 b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWu06F15M.woff2
new file mode 100644
index 0000000..2e849f6
Binary files /dev/null and b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWu06F15M.woff2 differ
diff --git a/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuU6F.woff2 b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuU6F.woff2
new file mode 100644
index 0000000..5de3fea
Binary files /dev/null and b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuU6F.woff2 differ
diff --git a/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuk6F15M.woff2 b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuk6F15M.woff2
new file mode 100644
index 0000000..e5c936b
Binary files /dev/null and b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuk6F15M.woff2 differ
diff --git a/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWvU6F15M.woff2 b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWvU6F15M.woff2
new file mode 100644
index 0000000..5cf8aff
Binary files /dev/null and b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWvU6F15M.woff2 differ
diff --git a/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWxU6F15M.woff2 b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWxU6F15M.woff2
new file mode 100644
index 0000000..bdc12e8
Binary files /dev/null and b/src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWxU6F15M.woff2 differ
diff --git a/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-muw.woff2 b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-muw.woff2
new file mode 100644
index 0000000..b5d54e7
Binary files /dev/null and b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-muw.woff2 differ
diff --git a/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu1aB.woff2 b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu1aB.woff2
new file mode 100644
index 0000000..bed5b67
Binary files /dev/null and b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu1aB.woff2 differ
diff --git a/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu1aB.woff2 b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu1aB.woff2
new file mode 100644
index 0000000..9164ccb
Binary files /dev/null and b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu1aB.woff2 differ
diff --git a/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu1aB.woff2 b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu1aB.woff2
new file mode 100644
index 0000000..08bed85
Binary files /dev/null and b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu1aB.woff2 differ
diff --git a/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu1aB.woff2 b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu1aB.woff2
new file mode 100644
index 0000000..307b214
Binary files /dev/null and b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu1aB.woff2 differ
diff --git a/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu1aB.woff2 b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu1aB.woff2
new file mode 100644
index 0000000..0b0b3a4
Binary files /dev/null and b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu1aB.woff2 differ
diff --git a/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu1aB.woff2 b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu1aB.woff2
new file mode 100644
index 0000000..4bce1d0
Binary files /dev/null and b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu1aB.woff2 differ
diff --git a/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu1aB.woff2 b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu1aB.woff2
new file mode 100644
index 0000000..5bd7b8f
Binary files /dev/null and b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu1aB.woff2 differ
diff --git a/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTUGmu1aB.woff2 b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTUGmu1aB.woff2
new file mode 100644
index 0000000..b969602
Binary files /dev/null and b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTUGmu1aB.woff2 differ
diff --git a/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTVOmu1aB.woff2 b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTVOmu1aB.woff2
new file mode 100644
index 0000000..a804b10
Binary files /dev/null and b/src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTVOmu1aB.woff2 differ
diff --git a/src/blog/static/blog/fonts/open-sans.css b/src/blog/static/blog/fonts/open-sans.css
new file mode 100644
index 0000000..e6dd4a9
--- /dev/null
+++ b/src/blog/static/blog/fonts/open-sans.css
@@ -0,0 +1,600 @@
+/* cyrillic-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 300;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtE6F15M.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 300;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWvU6F15M.woff2) format('woff2');
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 300;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtU6F15M.woff2) format('woff2');
+ unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 300;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuk6F15M.woff2) format('woff2');
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+/* hebrew */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 300;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWu06F15M.woff2) format('woff2');
+ unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
+}
+/* math */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 300;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWxU6F15M.woff2) format('woff2');
+ unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
+}
+/* symbols */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 300;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqW106F15M.woff2) format('woff2');
+ unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
+}
+/* vietnamese */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 300;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtk6F15M.woff2) format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 300;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWt06F15M.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 300;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuU6F.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 400;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtE6F15M.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 400;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWvU6F15M.woff2) format('woff2');
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 400;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtU6F15M.woff2) format('woff2');
+ unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 400;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuk6F15M.woff2) format('woff2');
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+/* hebrew */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 400;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWu06F15M.woff2) format('woff2');
+ unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
+}
+/* math */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 400;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWxU6F15M.woff2) format('woff2');
+ unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
+}
+/* symbols */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 400;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqW106F15M.woff2) format('woff2');
+ unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
+}
+/* vietnamese */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 400;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtk6F15M.woff2) format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 400;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWt06F15M.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 400;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuU6F.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 600;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtE6F15M.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 600;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWvU6F15M.woff2) format('woff2');
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 600;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtU6F15M.woff2) format('woff2');
+ unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 600;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuk6F15M.woff2) format('woff2');
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+/* hebrew */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 600;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWu06F15M.woff2) format('woff2');
+ unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
+}
+/* math */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 600;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWxU6F15M.woff2) format('woff2');
+ unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
+}
+/* symbols */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 600;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqW106F15M.woff2) format('woff2');
+ unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
+}
+/* vietnamese */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 600;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtk6F15M.woff2) format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 600;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWt06F15M.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: italic;
+ font-weight: 600;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuU6F.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 300;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu1aB.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 300;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu1aB.woff2) format('woff2');
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 300;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu1aB.woff2) format('woff2');
+ unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 300;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu1aB.woff2) format('woff2');
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+/* hebrew */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 300;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu1aB.woff2) format('woff2');
+ unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
+}
+/* math */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 300;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTVOmu1aB.woff2) format('woff2');
+ unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
+}
+/* symbols */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 300;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTUGmu1aB.woff2) format('woff2');
+ unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
+}
+/* vietnamese */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 300;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu1aB.woff2) format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 300;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu1aB.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 300;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-muw.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 400;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu1aB.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 400;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu1aB.woff2) format('woff2');
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 400;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu1aB.woff2) format('woff2');
+ unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 400;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu1aB.woff2) format('woff2');
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+/* hebrew */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 400;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu1aB.woff2) format('woff2');
+ unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
+}
+/* math */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 400;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTVOmu1aB.woff2) format('woff2');
+ unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
+}
+/* symbols */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 400;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTUGmu1aB.woff2) format('woff2');
+ unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
+}
+/* vietnamese */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 400;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu1aB.woff2) format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 400;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu1aB.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 400;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-muw.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+/* cyrillic-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 600;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu1aB.woff2) format('woff2');
+ unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
+}
+/* cyrillic */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 600;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu1aB.woff2) format('woff2');
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+/* greek-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 600;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu1aB.woff2) format('woff2');
+ unicode-range: U+1F00-1FFF;
+}
+/* greek */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 600;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu1aB.woff2) format('woff2');
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;
+}
+/* hebrew */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 600;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu1aB.woff2) format('woff2');
+ unicode-range: U+0307-0308, U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F;
+}
+/* math */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 600;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTVOmu1aB.woff2) format('woff2');
+ unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0310, U+0312, U+0315, U+031A, U+0326-0327, U+032C, U+032F-0330, U+0332-0333, U+0338, U+033A, U+0346, U+034D, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2016-2017, U+2034-2038, U+203C, U+2040, U+2043, U+2047, U+2050, U+2057, U+205F, U+2070-2071, U+2074-208E, U+2090-209C, U+20D0-20DC, U+20E1, U+20E5-20EF, U+2100-2112, U+2114-2115, U+2117-2121, U+2123-214F, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, U+2336-237A, U+237C, U+2395, U+239B-23B7, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+3030, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF;
+}
+/* symbols */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 600;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTUGmu1aB.woff2) format('woff2');
+ unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8BB, U+1F8C0-1F8C1, U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, U+1FA80-1FA89, U+1FA8F-1FAC6, U+1FACE-1FADC, U+1FADF-1FAE9, U+1FAF0-1FAF8, U+1FB00-1FBFF;
+}
+/* vietnamese */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 600;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu1aB.woff2) format('woff2');
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
+}
+/* latin-ext */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 600;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu1aB.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+/* latin */
+@font-face {
+ font-family: 'Open Sans';
+ font-style: normal;
+ font-weight: 600;
+ font-stretch: 100%;
+ font-display: swap;
+ src: url(memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-muw.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
diff --git a/src/blog/static/blog/js/mathjax-loader.js b/src/blog/static/blog/js/mathjax-loader.js
new file mode 100644
index 0000000..c922fc7
--- /dev/null
+++ b/src/blog/static/blog/js/mathjax-loader.js
@@ -0,0 +1,142 @@
+/**
+ * MathJax 智能加载器
+ * 检测页面是否包含数学公式,如果有则动态加载和配置MathJax
+ */
+(function() {
+ 'use strict';
+
+ /**
+ * 检测页面是否包含数学公式
+ * @returns {boolean} 是否包含数学公式
+ */
+ function hasMathFormulas() {
+ const content = document.body.textContent || document.body.innerText || '';
+ // 检测常见的数学公式语法
+ return /\$.*?\$|\$\$.*?\$\$|\\begin\{.*?\}|\\end\{.*?\}|\\[a-zA-Z]+\{/.test(content);
+ }
+
+ /**
+ * 配置MathJax
+ */
+ function configureMathJax() {
+ window.MathJax = {
+ tex: {
+ // 行内公式和块级公式分隔符
+ inlineMath: [['$', '$']],
+ displayMath: [['$$', '$$']],
+ // 处理转义字符和LaTeX环境
+ processEscapes: true,
+ processEnvironments: true,
+ // 自动换行
+ tags: 'ams'
+ },
+ options: {
+ // 跳过这些HTML标签,避免处理代码块等
+ skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code', 'a'],
+ // CSS类控制
+ ignoreHtmlClass: 'tex2jax_ignore',
+ processHtmlClass: 'tex2jax_process'
+ },
+ // 启动配置
+ startup: {
+ ready() {
+ console.log('MathJax配置完成,开始初始化...');
+ MathJax.startup.defaultReady();
+
+ // 处理特定区域的数学公式
+ const contentEl = document.getElementById('content');
+ const commentsEl = document.getElementById('comments');
+
+ const promises = [];
+ if (contentEl) {
+ promises.push(MathJax.typesetPromise([contentEl]));
+ }
+ if (commentsEl) {
+ promises.push(MathJax.typesetPromise([commentsEl]));
+ }
+
+ // 等待所有渲染完成
+ Promise.all(promises).then(() => {
+ console.log('MathJax渲染完成');
+ // 触发自定义事件,通知其他脚本MathJax已就绪
+ document.dispatchEvent(new CustomEvent('mathjaxReady'));
+ }).catch(error => {
+ console.error('MathJax渲染失败:', error);
+ });
+ }
+ },
+ // 输出配置
+ chtml: {
+ scale: 1,
+ minScale: 0.5,
+ matchFontHeight: false,
+ displayAlign: 'center',
+ displayIndent: '0'
+ }
+ };
+ }
+
+ /**
+ * 加载MathJax库
+ */
+ function loadMathJax() {
+ console.log('检测到数学公式,开始加载MathJax...');
+
+ const script = document.createElement('script');
+ script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js';
+ script.async = true;
+ script.defer = true;
+
+ script.onload = function() {
+ console.log('MathJax库加载成功');
+ };
+
+ script.onerror = function() {
+ console.error('MathJax库加载失败,尝试备用CDN...');
+ // 备用CDN
+ const fallbackScript = document.createElement('script');
+ fallbackScript.src = 'https://polyfill.io/v3/polyfill.min.js?features=es6';
+ fallbackScript.onload = function() {
+ const mathJaxScript = document.createElement('script');
+ mathJaxScript.src = 'https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-MML-AM_CHTML';
+ mathJaxScript.async = true;
+ document.head.appendChild(mathJaxScript);
+ };
+ document.head.appendChild(fallbackScript);
+ };
+
+ document.head.appendChild(script);
+ }
+
+ /**
+ * 初始化函数
+ */
+ function init() {
+ // 等待DOM完全加载
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+ return;
+ }
+
+ // 检测是否需要加载MathJax
+ if (hasMathFormulas()) {
+ // 先配置,再加载
+ configureMathJax();
+ loadMathJax();
+ } else {
+ console.log('未检测到数学公式,跳过MathJax加载');
+ }
+ }
+
+ // 提供重新渲染的全局方法,供动态内容使用
+ window.rerenderMathJax = function(element) {
+ if (window.MathJax && window.MathJax.typesetPromise) {
+ const target = element || document.body;
+ return window.MathJax.typesetPromise([target]);
+ }
+ return Promise.resolve();
+ };
+
+ // 启动初始化
+ init();
+})();
diff --git a/src/blog/static/mathjax/js/mathjax-config.js b/src/blog/static/mathjax/js/mathjax-config.js
deleted file mode 100644
index 158ba65..0000000
--- a/src/blog/static/mathjax/js/mathjax-config.js
+++ /dev/null
@@ -1,21 +0,0 @@
-$(function () {
- MathJax.Hub.Config({
- showProcessingMessages: false, //关闭js加载过程信息
- messageStyle: "none", //不显示信息
- extensions: ["tex2jax.js"], jax: ["input/TeX", "output/HTML-CSS"], displayAlign: "left", tex2jax: {
- inlineMath: [["$", "$"]], //行内公式选择$
- displayMath: [["$$", "$$"]], //段内公式选择$$
- skipTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code', 'a'], //避开某些标签
- }, "HTML-CSS": {
- availableFonts: ["STIX", "TeX"], //可选字体
- showMathMenu: false //关闭右击菜单显示
- }
- });
- // 识别范围 => 文章内容、评论内容标签
- const contentId = document.getElementById("content");
- const commentId = document.getElementById("comments");
- MathJax.Hub.Queue(["Typeset", MathJax.Hub, contentId, commentId]);
-})
-
-
-
diff --git a/src/blog/templatetags/__pycache__/__init__.cpython-311.pyc b/src/blog/templatetags/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..7c87c52
Binary files /dev/null and b/src/blog/templatetags/__pycache__/__init__.cpython-311.pyc differ
diff --git a/src/blog/templatetags/__pycache__/blog_tags.cpython-311.pyc b/src/blog/templatetags/__pycache__/blog_tags.cpython-311.pyc
new file mode 100644
index 0000000..a293b4e
Binary files /dev/null and b/src/blog/templatetags/__pycache__/blog_tags.cpython-311.pyc differ
diff --git a/src/blog/templatetags/blog_tags.py b/src/blog/templatetags/blog_tags.py
index 110b22b..024f2c8 100644
--- a/src/blog/templatetags/blog_tags.py
+++ b/src/blog/templatetags/blog_tags.py
@@ -18,12 +18,18 @@ from djangoblog.utils import CommonMarkdown, sanitize_html
from djangoblog.utils import cache
from djangoblog.utils import get_current_site
from oauth.models import OAuthUser
+from djangoblog.plugin_manage import hooks
logger = logging.getLogger(__name__)
register = template.Library()
+@register.simple_tag(takes_context=True)
+def head_meta(context):
+ return mark_safe(hooks.apply_filters('head_meta', '', context))
+
+
@register.simple_tag
def timeformat(data):
try:
@@ -45,7 +51,75 @@ def datetimeformat(data):
@register.filter()
@stringfilter
def custom_markdown(content):
- return mark_safe(CommonMarkdown.get_markdown(content))
+ """
+ 通用markdown过滤器,应用文章内容插件
+ 主要用于文章内容处理
+ """
+ html_content = CommonMarkdown.get_markdown(content)
+
+ # 然后应用插件过滤器优化HTML
+ from djangoblog.plugin_manage import hooks
+ from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
+ optimized_html = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, html_content)
+
+ return mark_safe(optimized_html)
+
+
+@register.filter()
+@stringfilter
+def sidebar_markdown(content):
+ html_content = CommonMarkdown.get_markdown(content)
+ return mark_safe(html_content)
+
+
+@register.simple_tag(takes_context=True)
+def render_article_content(context, article, is_summary=False):
+ """
+ 渲染文章内容,包含完整的上下文信息供插件使用
+
+ Args:
+ context: 模板上下文
+ article: 文章对象
+ is_summary: 是否为摘要模式(首页使用)
+ """
+ if not article or not hasattr(article, 'body'):
+ return ''
+
+ # 先转换Markdown为HTML
+ html_content = CommonMarkdown.get_markdown(article.body)
+
+ # 如果是摘要模式,先截断内容再应用插件
+ if is_summary:
+ # 截断HTML内容到合适的长度(约300字符)
+ from django.utils.html import strip_tags
+ from django.template.defaultfilters import truncatechars
+
+ # 先去除HTML标签,截断纯文本,然后重新转换为HTML
+ plain_text = strip_tags(html_content)
+ truncated_text = truncatechars(plain_text, 300)
+
+ # 重新转换截断后的文本为HTML(简化版,避免复杂的插件处理)
+ html_content = CommonMarkdown.get_markdown(truncated_text)
+
+ # 然后应用插件过滤器,传递完整的上下文
+ from djangoblog.plugin_manage import hooks
+ from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
+
+ # 获取request对象
+ request = context.get('request')
+
+ # 应用所有文章内容相关的插件
+ # 注意:摘要模式下某些插件(如版权声明)可能不适用
+ optimized_html = hooks.apply_filters(
+ ARTICLE_CONTENT_HOOK_NAME,
+ html_content,
+ article=article,
+ request=request,
+ context=context,
+ is_summary=is_summary # 传递摘要标志,插件可以据此调整行为
+ )
+
+ return mark_safe(optimized_html)
@register.simple_tag
@@ -286,38 +360,49 @@ def load_article_detail(article, isindex, user):
}
-# return only the URL of the gravatar
-# TEMPLATE USE: {{ email|gravatar_url:150 }}
+# 返回用户头像URL
+# 模板使用方法: {{ email|gravatar_url:150 }}
@register.filter
def gravatar_url(email, size=40):
- """获得gravatar头像"""
- cachekey = 'gravatat/' + email
+ """获得用户头像 - 优先使用OAuth头像,否则使用默认头像"""
+ cachekey = 'avatar/' + email
url = cache.get(cachekey)
if url:
return url
- else:
- usermodels = OAuthUser.objects.filter(email=email)
- if usermodels:
- o = list(filter(lambda x: x.picture is not None, usermodels))
- if o:
- return o[0].picture
- email = email.encode('utf-8')
-
- default = static('blog/img/avatar.png')
-
- url = "https://www.gravatar.com/avatar/%s?%s" % (hashlib.md5(
- email.lower()).hexdigest(), urllib.parse.urlencode({'d': default, 's': str(size)}))
- cache.set(cachekey, url, 60 * 60 * 10)
- logger.info('set gravatar cache.key:{key}'.format(key=cachekey))
- return url
+
+ # 检查OAuth用户是否有自定义头像
+ usermodels = OAuthUser.objects.filter(email=email)
+ if usermodels:
+ # 过滤出有头像的用户
+ users_with_picture = list(filter(lambda x: x.picture is not None, usermodels))
+ if users_with_picture:
+ # 获取默认头像路径用于比较
+ default_avatar_path = static('blog/img/avatar.png')
+
+ # 优先选择非默认头像的用户,否则选择第一个
+ non_default_users = [u for u in users_with_picture if u.picture != default_avatar_path and not u.picture.endswith('/avatar.png')]
+ selected_user = non_default_users[0] if non_default_users else users_with_picture[0]
+
+ url = selected_user.picture
+ cache.set(cachekey, url, 60 * 60 * 24) # 缓存24小时
+
+ avatar_type = 'non-default' if non_default_users else 'default'
+ logger.info('Using {} OAuth avatar for {} from {}'.format(avatar_type, email, selected_user.type))
+ return url
+
+ # 使用默认头像
+ url = static('blog/img/avatar.png')
+ cache.set(cachekey, url, 60 * 60 * 24) # 缓存24小时
+ logger.info('Using default avatar for {}'.format(email))
+ return url
@register.filter
def gravatar(email, size=40):
- """获得gravatar头像"""
+ """获得用户头像HTML标签"""
url = gravatar_url(email, size)
return mark_safe(
- ' ' %
+ ' ' %
(url, size, size))
@@ -336,3 +421,134 @@ def query(qs, **kwargs):
def addstr(arg1, arg2):
"""concatenate arg1 & arg2"""
return str(arg1) + str(arg2)
+
+
+# === 插件系统模板标签 ===
+
+@register.simple_tag(takes_context=True)
+def render_plugin_widgets(context, position, **kwargs):
+ """
+ 渲染指定位置的所有插件组件
+
+ Args:
+ context: 模板上下文
+ position: 位置标识
+ **kwargs: 传递给插件的额外参数
+
+ Returns:
+ 按优先级排序的所有插件HTML内容
+ """
+ from djangoblog.plugin_manage.loader import get_loaded_plugins
+
+ widgets = []
+
+ for plugin in get_loaded_plugins():
+ try:
+ widget_data = plugin.render_position_widget(
+ position=position,
+ context=context,
+ **kwargs
+ )
+ if widget_data:
+ widgets.append(widget_data)
+ except Exception as e:
+ logger.error(f"Error rendering widget from plugin {plugin.PLUGIN_NAME}: {e}")
+
+ # 按优先级排序(数字越小优先级越高)
+ widgets.sort(key=lambda x: x['priority'])
+
+ # 合并HTML内容
+ html_parts = [widget['html'] for widget in widgets]
+ return mark_safe(''.join(html_parts))
+
+
+@register.simple_tag(takes_context=True)
+def plugin_head_resources(context):
+ """渲染所有插件的head资源(仅自定义HTML,CSS已集成到压缩系统)"""
+ from djangoblog.plugin_manage.loader import get_loaded_plugins
+
+ resources = []
+
+ for plugin in get_loaded_plugins():
+ try:
+ # 只处理自定义head HTML(CSS文件已通过压缩系统处理)
+ head_html = plugin.get_head_html(context)
+ if head_html:
+ resources.append(head_html)
+
+ except Exception as e:
+ logger.error(f"Error loading head resources from plugin {plugin.PLUGIN_NAME}: {e}")
+
+ return mark_safe('\n'.join(resources))
+
+
+@register.simple_tag(takes_context=True)
+def plugin_body_resources(context):
+ """渲染所有插件的body资源(仅自定义HTML,JS已集成到压缩系统)"""
+ from djangoblog.plugin_manage.loader import get_loaded_plugins
+
+ resources = []
+
+ for plugin in get_loaded_plugins():
+ try:
+ # 只处理自定义body HTML(JS文件已通过压缩系统处理)
+ body_html = plugin.get_body_html(context)
+ if body_html:
+ resources.append(body_html)
+
+ except Exception as e:
+ logger.error(f"Error loading body resources from plugin {plugin.PLUGIN_NAME}: {e}")
+
+ return mark_safe('\n'.join(resources))
+
+
+@register.inclusion_tag('plugins/css_includes.html')
+def plugin_compressed_css():
+ """插件CSS压缩包含模板"""
+ from djangoblog.plugin_manage.loader import get_loaded_plugins
+
+ css_files = []
+ for plugin in get_loaded_plugins():
+ for css_file in plugin.get_css_files():
+ css_url = plugin.get_static_url(css_file)
+ css_files.append(css_url)
+
+ return {'css_files': css_files}
+
+
+@register.inclusion_tag('plugins/js_includes.html')
+def plugin_compressed_js():
+ """插件JS压缩包含模板"""
+ from djangoblog.plugin_manage.loader import get_loaded_plugins
+
+ js_files = []
+ for plugin in get_loaded_plugins():
+ for js_file in plugin.get_js_files():
+ js_url = plugin.get_static_url(js_file)
+ js_files.append(js_url)
+
+ return {'js_files': js_files}
+
+
+
+
+@register.simple_tag(takes_context=True)
+def plugin_widget(context, plugin_name, widget_type='default', **kwargs):
+ """
+ 渲染指定插件的组件
+
+ 使用方式:
+ {% plugin_widget 'article_recommendation' 'bottom' article=article count=5 %}
+ """
+ from djangoblog.plugin_manage.loader import get_plugin_by_slug
+
+ plugin = get_plugin_by_slug(plugin_name)
+ if plugin and hasattr(plugin, 'render_template'):
+ try:
+ widget_context = {**context.flatten(), **kwargs}
+ template_name = f"{widget_type}.html"
+ return mark_safe(plugin.render_template(template_name, widget_context))
+ except Exception as e:
+ logger.error(f"Error rendering plugin widget {plugin_name}.{widget_type}: {e}")
+
+ return ""
\ No newline at end of file
diff --git a/src/blog/tests.py b/src/blog/tests.py
index f6bfac0..ee13505 100644
--- a/src/blog/tests.py
+++ b/src/blog/tests.py
@@ -162,7 +162,7 @@ class ArticleTest(TestCase):
def test_image(self):
import requests
rsp = requests.get(
- 'https://www.python.org/static/img/python-logo@2x.png')
+ 'https://www.python.org/static/img/python-logo.png')
imagepath = os.path.join(settings.BASE_DIR, 'python.png')
with open(imagepath, 'wb') as file:
file.write(rsp.content)
@@ -180,7 +180,7 @@ class ArticleTest(TestCase):
from djangoblog.utils import save_user_avatar, send_email
send_email(['qq@qq.com'], 'testTitle', 'testContent')
save_user_avatar(
- 'https://www.python.org/static/img/python-logo@2x.png')
+ 'https://www.python.org/static/img/python-logo.png')
def test_errorpage(self):
rsp = self.client.get('/eee')
diff --git a/src/blog/views.py b/src/blog/views.py
index 4af9242..773bb75 100644
--- a/src/blog/views.py
+++ b/src/blog/views.py
@@ -17,6 +17,8 @@ from haystack.views import SearchView
from blog.models import Article, Category, LinkShowType, Links, Tag
from comments.forms import CommentForm
+from djangoblog.plugin_manage import hooks
+from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
from djangoblog.utils import cache, get_blog_setting, get_sha256
logger = logging.getLogger(__name__)
@@ -112,12 +114,6 @@ class ArticleDetailView(DetailView):
pk_url_kwarg = 'article_id'
context_object_name = "article"
- def get_object(self, queryset=None):
- obj = super(ArticleDetailView, self).get_object()
- obj.viewed()
- self.object = obj
- return obj
-
def get_context_data(self, **kwargs):
comment_form = CommentForm()
@@ -154,7 +150,16 @@ class ArticleDetailView(DetailView):
kwargs['next_article'] = self.object.next_article
kwargs['prev_article'] = self.object.prev_article
- return super(ArticleDetailView, self).get_context_data(**kwargs)
+ context = super(ArticleDetailView, self).get_context_data(**kwargs)
+ article = self.object
+
+ # 触发文章详情加载钩子,让插件可以添加额外的上下文数据
+ from djangoblog.plugin_manage.hook_constants import ARTICLE_DETAIL_LOAD
+ hooks.run_action(ARTICLE_DETAIL_LOAD, article=article, context=context, request=self.request)
+
+ # Action Hook, 通知插件"文章详情已获取"
+ hooks.run_action('after_article_body_get', article=article, request=self.request)
+ return context
class CategoryDetailView(ArticleListView):
diff --git a/src/codecov.yml b/src/codecov.yml
new file mode 100644
index 0000000..2298829
--- /dev/null
+++ b/src/codecov.yml
@@ -0,0 +1,87 @@
+codecov:
+ require_ci_to_pass: yes
+
+coverage:
+ precision: 2
+ round: down
+ range: "70...100"
+
+ status:
+ project:
+ default:
+ target: auto
+ threshold: 1%
+ informational: true
+ patch:
+ default:
+ target: auto
+ threshold: 1%
+ informational: true
+
+parsers:
+ gcov:
+ branch_detection:
+ conditional: yes
+ loop: yes
+ method: no
+ macro: no
+
+comment:
+ layout: "reach,diff,flags,tree"
+ behavior: default
+ require_changes: no
+
+ignore:
+ # Django 相关
+ - "*/migrations/*"
+ - "manage.py"
+ - "*/settings.py"
+ - "*/wsgi.py"
+ - "*/asgi.py"
+
+ # 测试相关
+ - "*/tests/*"
+ - "*/test_*.py"
+ - "*/*test*.py"
+
+ # 静态文件和模板
+ - "*/static/*"
+ - "*/templates/*"
+ - "*/collectedstatic/*"
+
+ # 国际化文件
+ - "*/locale/*"
+ - "**/*.po"
+ - "**/*.mo"
+
+ # 文档和部署
+ - "*/docs/*"
+ - "*/deploy/*"
+ - "README*.md"
+ - "LICENSE"
+ - "Dockerfile"
+ - "docker-compose*.yml"
+ - "*.yaml"
+ - "*.yml"
+
+ # 开发环境
+ - "*/venv/*"
+ - "*/__pycache__/*"
+ - "*.pyc"
+ - ".coverage"
+ - "coverage.xml"
+
+ # 日志文件
+ - "*/logs/*"
+ - "*.log"
+
+ # 特定文件
+ - "*/whoosh_cn_backend.py" # 搜索后端
+ - "*/elasticsearch_backend.py" # 搜索后端
+ - "*/MemcacheStorage.py" # 缓存存储
+ - "*/robot.py" # 机器人相关
+
+ # 配置文件
+ - "codecov.yml"
+ - ".coveragerc"
+ - "requirements*.txt"
diff --git a/src/collectedstatic/compressed/css/output.f76962182efc.css b/src/collectedstatic/compressed/css/output.f76962182efc.css
new file mode 100644
index 0000000..7436e06
--- /dev/null
+++ b/src/collectedstatic/compressed/css/output.f76962182efc.css
@@ -0,0 +1 @@
+.icon-sn-google{background-position:0 -28px}.icon-sn-bg-google{background-color:#4285f4;background-position:0 0}.fa-sn-google{color:#4285f4}.icon-sn-github{background-position:-28px -28px}.icon-sn-bg-github{background-color:#333;background-position:-28px 0}.fa-sn-github{color:#333}.icon-sn-weibo{background-position:-56px -28px}.icon-sn-bg-weibo{background-color:#e90d24;background-position:-56px 0}.fa-sn-weibo{color:#e90d24}.icon-sn-qq{background-position:-84px -28px}.icon-sn-bg-qq{background-color:#0098e6;background-position:-84px 0}.fa-sn-qq{color:#0098e6}.icon-sn-twitter{background-position:-112px -28px}.icon-sn-bg-twitter{background-color:#50abf1;background-position:-112px 0}.fa-sn-twitter{color:#50abf1}.icon-sn-facebook{background-position:-140px -28px}.icon-sn-bg-facebook{background-color:#4862a3;background-position:-140px 0}.fa-sn-facebook{color:#4862a3}.icon-sn-renren{background-position:-168px -28px}.icon-sn-bg-renren{background-color:#197bc8;background-position:-168px 0}.fa-sn-renren{color:#197bc8}.icon-sn-tqq{background-position:-196px -28px}.icon-sn-bg-tqq{background-color:#1f9ed2;background-position:-196px 0}.fa-sn-tqq{color:#1f9ed2}.icon-sn-douban{background-position:-224px -28px}.icon-sn-bg-douban{background-color:#279738;background-position:-224px 0}.fa-sn-douban{color:#279738}.icon-sn-weixin{background-position:-252px -28px}.icon-sn-bg-weixin{background-color:#00b500;background-position:-252px 0}.fa-sn-weixin{color:#00b500}.icon-sn-dotted{background-position:-280px -28px}.icon-sn-bg-dotted{background-color:#eee;background-position:-280px 0}.fa-sn-dotted{color:#eee}.icon-sn-site{background-position:-308px -28px}.icon-sn-bg-site{background-color:#00b500;background-position:-308px 0}.fa-sn-site{color:#00b500}.icon-sn-linkedin{background-position:-336px -28px}.icon-sn-bg-linkedin{background-color:#0077b9;background-position:-336px 0}.fa-sn-linkedin{color:#0077b9}[class*=icon-sn-]{display:inline-block;background-image:url('/static/blog/img/icon-sn.svg');background-repeat:no-repeat;width:28px;height:28px;vertical-align:middle;background-size:auto 56px}[class*=icon-sn-]:hover{opacity:.8;filter:alpha(opacity=80)}.btn-sn-google{background:#4285f4}.btn-sn-google:active,.btn-sn-google:focus,.btn-sn-google:hover{background:#2a75f3}.btn-sn-github{background:#333}.btn-sn-github:active,.btn-sn-github:focus,.btn-sn-github:hover{background:#262626}.btn-sn-weibo{background:#e90d24}.btn-sn-weibo:active,.btn-sn-weibo:focus,.btn-sn-weibo:hover{background:#d10c20}.btn-sn-qq{background:#0098e6}.btn-sn-qq:active,.btn-sn-qq:focus,.btn-sn-qq:hover{background:#0087cd}.btn-sn-twitter{background:#50abf1}.btn-sn-twitter:active,.btn-sn-twitter:focus,.btn-sn-twitter:hover{background:#38a0ef}.btn-sn-facebook{background:#4862a3}.btn-sn-facebook:active,.btn-sn-facebook:focus,.btn-sn-facebook:hover{background:#405791}.btn-sn-renren{background:#197bc8}.btn-sn-renren:active,.btn-sn-renren:focus,.btn-sn-renren:hover{background:#166db1}.btn-sn-tqq{background:#1f9ed2}.btn-sn-tqq:active,.btn-sn-tqq:focus,.btn-sn-tqq:hover{background:#1c8dbc}.btn-sn-douban{background:#279738}.btn-sn-douban:active,.btn-sn-douban:focus,.btn-sn-douban:hover{background:#228330}.btn-sn-weixin{background:#00b500}.btn-sn-weixin:active,.btn-sn-weixin:focus,.btn-sn-weixin:hover{background:#009c00}.btn-sn-dotted{background:#eee}.btn-sn-dotted:active,.btn-sn-dotted:focus,.btn-sn-dotted:hover{background:#e1e1e1}.btn-sn-site{background:#00b500}.btn-sn-site:active,.btn-sn-site:focus,.btn-sn-site:hover{background:#009c00}.btn-sn-linkedin{background:#0077b9}.btn-sn-linkedin:active,.btn-sn-linkedin:focus,.btn-sn-linkedin:hover{background:#0067a0}[class*=btn-sn-],[class*=btn-sn-]:active,[class*=btn-sn-]:focus,[class*=btn-sn-]:hover{border:none;color:#fff}.btn-sn-more{padding:0}.btn-sn-more,.btn-sn-more:active,.btn-sn-more:hover{box-shadow:none}[class*=btn-sn-] [class*=icon-sn-]{background-color:transparent}.codehilite .hll{background-color:#ffffcc}.codehilite{background:#ffffff}.codehilite .c{color:#177500}.codehilite .err{color:#000000}.codehilite .k{color:#A90D91}.codehilite .l{color:#1C01CE}.codehilite .n{color:#000000}.codehilite .o{color:#000000}.codehilite .ch{color:#177500}.codehilite .cm{color:#177500}.codehilite .cp{color:#633820}.codehilite .cpf{color:#177500}.codehilite .c1{color:#177500}.codehilite .cs{color:#177500}.codehilite .kc{color:#A90D91}.codehilite .kd{color:#A90D91}.codehilite .kn{color:#A90D91}.codehilite .kp{color:#A90D91}.codehilite .kr{color:#A90D91}.codehilite .kt{color:#A90D91}.codehilite .ld{color:#1C01CE}.codehilite .m{color:#1C01CE}.codehilite .s{color:#C41A16}.codehilite .na{color:#836C28}.codehilite .nb{color:#A90D91}.codehilite .nc{color:#3F6E75}.codehilite .no{color:#000000}.codehilite .nd{color:#000000}.codehilite .ni{color:#000000}.codehilite .ne{color:#000000}.codehilite .nf{color:#000000}.codehilite .nl{color:#000000}.codehilite .nn{color:#000000}.codehilite .nx{color:#000000}.codehilite .py{color:#000000}.codehilite .nt{color:#000000}.codehilite .nv{color:#000000}.codehilite .ow{color:#000000}.codehilite .mb{color:#1C01CE}.codehilite .mf{color:#1C01CE}.codehilite .mh{color:#1C01CE}.codehilite .mi{color:#1C01CE}.codehilite .mo{color:#1C01CE}.codehilite .sb{color:#C41A16}.codehilite .sc{color:#2300CE}.codehilite .sd{color:#C41A16}.codehilite .s2{color:#C41A16}.codehilite .se{color:#C41A16}.codehilite .sh{color:#C41A16}.codehilite .si{color:#C41A16}.codehilite .sx{color:#C41A16}.codehilite .sr{color:#C41A16}.codehilite .s1{color:#C41A16}.codehilite .ss{color:#C41A16}.codehilite .bp{color:#5B269A}.codehilite .vc{color:#000000}.codehilite .vg{color:#000000}.codehilite .vi{color:#000000}.codehilite .il{color:#1C01CE}#nprogress{pointer-events:none}#nprogress .bar{background:red;position:fixed;z-index:1031;top:0;left:0;width:100%;height:2px}#nprogress .peg{display:block;position:absolute;right:0px;width:100px;height:100%;box-shadow:0 0 10px #29d,0 0 5px #29d;opacity:1.0;-webkit-transform:rotate(3deg) translate(0px,-4px);-ms-transform:rotate(3deg) translate(0px,-4px);transform:rotate(3deg) translate(0px,-4px)}#nprogress .spinner{display:block;position:fixed;z-index:1031;top:15px;right:15px}#nprogress .spinner-icon{width:18px;height:18px;box-sizing:border-box;border:solid 2px transparent;border-top-color:red;border-left-color:red;border-radius:50%;-webkit-animation:nprogress-spinner 400ms linear infinite;animation:nprogress-spinner 400ms linear infinite}.nprogress-custom-parent{overflow:hidden;position:relative}.nprogress-custom-parent #nprogress .spinner,.nprogress-custom-parent #nprogress .bar{position:absolute}@-webkit-keyframes nprogress-spinner{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg)}}@keyframes nprogress-spinner{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.article-recommendations{margin:30px 0;padding:20px;background:#fff;border:1px solid #e1e1e1;border-radius:3px;box-shadow:0 1px 3px rgba(0,0,0,0.1)}.recommendations-title{margin:0 0 15px 0;font-size:18px;color:#444;font-weight:bold;padding-bottom:8px;border-bottom:2px solid #21759b;display:inline-block}.recommendations-icon{margin-right:5px;font-size:16px}.recommendations-grid{display:grid;gap:15px;grid-template-columns:1fr;margin-top:15px}.recommendation-card{background:#fff;border:1px solid #e1e1e1;border-radius:3px;transition:all 0.2s ease;overflow:hidden}.recommendation-card:hover{border-color:#21759b;box-shadow:0 2px 5px rgba(33,117,155,0.1)}.recommendation-link{display:block;padding:15px;text-decoration:none;color:inherit}.recommendation-title{margin:0 0 8px 0;font-size:15px;font-weight:normal;color:#444;line-height:1.4;transition:color 0.2s ease}.recommendation-card:hover .recommendation-title{color:#21759b}.recommendation-meta{display:flex;justify-content:space-between;align-items:center;font-size:12px;color:#757575}.recommendation-category{background:#ebebeb;color:#5e5e5e;padding:2px 6px;border-radius:2px;font-size:11px;font-weight:normal}.recommendation-date{font-weight:normal;color:#757575}.widget_recommendations{margin-bottom:20px}.widget_recommendations .widget-title{font-size:16px;font-weight:bold;margin-bottom:15px;color:#333;border-bottom:2px solid #007cba;padding-bottom:5px}.recommendations-list{list-style:none;padding:0;margin:0}.recommendations-list .recommendation-item{padding:8px 0;border-bottom:1px solid #eee;background:none;border:none;border-radius:0}.recommendations-list .recommendation-item:last-child{border-bottom:none}.recommendations-list .recommendation-item a{color:#333;text-decoration:none;font-size:14px;line-height:1.4;display:block;margin-bottom:4px;transition:color 0.3s ease}.recommendations-list .recommendation-item a:hover{color:#007cba}.recommendations-list .recommendation-meta{font-size:11px;color:#999;margin:0}.recommendations-list .recommendation-meta span{margin-right:10px}@media (min-width:768px){.recommendations-grid{grid-template-columns:repeat(2,1fr);gap:15px}}@media (min-width:1024px){.recommendations-grid{grid-template-columns:repeat(3,1fr);gap:15px}}@media (min-width:1200px){.recommendations-grid{grid-template-columns:repeat(4,1fr);gap:15px}}
\ No newline at end of file
diff --git a/src/collectedstatic/compressed/css/output.fbe498417efd.css b/src/collectedstatic/compressed/css/output.fbe498417efd.css
new file mode 100644
index 0000000..61fbfea
--- /dev/null
+++ b/src/collectedstatic/compressed/css/output.fbe498417efd.css
@@ -0,0 +1 @@
+html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;vertical-align:baseline}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:'';content:none}table{border-collapse:collapse;border-spacing:0}caption,th,td{font-weight:normal;text-align:left}h1,h2,h3,h4,h5,h6{clear:both}html{overflow-y:scroll;font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}a:focus{outline:thin dotted}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block}audio:not([controls]){display:none}del{color:#333}ins{background:#fff9c0;text-decoration:none}hr{background-color:#ccc;border:0;height:1px;margin:24px;margin-bottom:1.714285714rem}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}small{font-size:smaller}img{border:0;-ms-interpolation-mode:bicubic}.clear:after,.wrapper:after,.format-status .entry-header:after{clear:both}.clear:before,.clear:after,.wrapper:before,.wrapper:after,.format-status .entry-header:before,.format-status .entry-header:after{display:table;content:""}.archive-title,.page-title,.widget-title,.entry-content th,.comment-content th{font-size:11px;font-size:0.785714286rem;line-height:2.181818182;font-weight:bold;text-transform:uppercase;color:#636363}article.format-quote footer.entry-meta,article.format-link footer.entry-meta,article.format-status footer.entry-meta{font-size:11px;font-size:0.785714286rem;line-height:2.181818182}button,input,select,textarea{border:1px solid #ccc;border-radius:3px;font-family:inherit;padding:6px;padding:0.428571429rem}button,input{line-height:normal}textarea{font-size:100%;overflow:auto;vertical-align:top}input[type="checkbox"],input[type="radio"],input[type="file"],input[type="hidden"],input[type="image"],input[type="color"]{border:0;border-radius:0;padding:0}.menu-toggle,input[type="submit"],input[type="button"],input[type="reset"],article.post-password-required input[type=submit],.bypostauthor cite span{padding:6px 10px;padding:0.428571429rem 0.714285714rem;font-size:11px;font-size:0.785714286rem;line-height:1.428571429;font-weight:normal;color:#7c7c7c;background-color:#e6e6e6;background-repeat:repeat-x;background-image:-moz-linear-gradient(top,#f4f4f4,#e6e6e6);background-image:-ms-linear-gradient(top,#f4f4f4,#e6e6e6);background-image:-webkit-linear-gradient(top,#f4f4f4,#e6e6e6);background-image:-o-linear-gradient(top,#f4f4f4,#e6e6e6);background-image:linear-gradient(to bottom,#f4f4f4,#e6e6e6);border:1px solid #d2d2d2;border-radius:3px;box-shadow:0 1px 2px rgba(64,64,64,0.1)}.menu-toggle,button,input[type="submit"],input[type="button"],input[type="reset"]{cursor:pointer}button[disabled],input[disabled]{cursor:default}.menu-toggle:hover,.menu-toggle:focus,button:hover,input[type="submit"]:hover,input[type="button"]:hover,input[type="reset"]:hover,article.post-password-required input[type=submit]:hover{color:#5e5e5e;background-color:#ebebeb;background-repeat:repeat-x;background-image:-moz-linear-gradient(top,#f9f9f9,#ebebeb);background-image:-ms-linear-gradient(top,#f9f9f9,#ebebeb);background-image:-webkit-linear-gradient(top,#f9f9f9,#ebebeb);background-image:-o-linear-gradient(top,#f9f9f9,#ebebeb);background-image:linear-gradient(to bottom,#f9f9f9,#ebebeb)}.menu-toggle:active,.menu-toggle.toggled-on,button:active,input[type="submit"]:active,input[type="button"]:active,input[type="reset"]:active{color:#757575;background-color:#e1e1e1;background-repeat:repeat-x;background-image:-moz-linear-gradient(top,#ebebeb,#e1e1e1);background-image:-ms-linear-gradient(top,#ebebeb,#e1e1e1);background-image:-webkit-linear-gradient(top,#ebebeb,#e1e1e1);background-image:-o-linear-gradient(top,#ebebeb,#e1e1e1);background-image:linear-gradient(to bottom,#ebebeb,#e1e1e1);box-shadow:inset 0 0 8px 2px #c6c6c6,0 1px 0 0 #f4f4f4;border-color:transparent}.bypostauthor cite span{color:#fff;background-color:#21759b;background-image:none;border:1px solid #1f6f93;border-radius:2px;box-shadow:none;padding:0}.entry-content img,.comment-content img,.widget img{max-width:100%}img[class*="align"],img[class*="wp-image-"],img[class*="attachment-"]{height:auto}img.size-full,img.size-large,img.header-image,img.wp-post-image{max-width:100%;height:auto}embed,iframe,object,video{max-width:100%}.entry-content .twitter-tweet-rendered{max-width:100%!important}.alignleft{float:left}.alignright{float:right}.aligncenter{display:block;margin-left:auto;margin-right:auto}.entry-content img,.comment-content img,.widget img,img.header-image,.author-avatar img,img.wp-post-image{border-radius:3px;box-shadow:0 1px 4px rgba(0,0,0,0.2)}.wp-caption{max-width:100%;padding:4px}.wp-caption .wp-caption-text,.gallery-caption,.entry-caption{font-style:italic;font-size:12px;font-size:0.857142857rem;line-height:2;color:#757575}img.wp-smiley,.rsswidget img{border:0;border-radius:0;box-shadow:none;margin-bottom:0;margin-top:0;padding:0}.entry-content dl.gallery-item{margin:0}.gallery-item a,.gallery-caption{width:90%}.gallery-item a{display:block}.gallery-caption a{display:inline}.gallery-columns-1 .gallery-item a{max-width:100%;width:auto}.gallery .gallery-icon img{height:auto;max-width:90%;padding:5%}.gallery-columns-1 .gallery-icon img{padding:3%}.site-content nav{clear:both;line-height:2;overflow:hidden}#nav-above{padding:24px 0;padding:1.714285714rem 0}#nav-above{display:none}.paged #nav-above{display:block}.nav-previous,.previous-image{float:left;width:50%}.nav-next,.next-image{float:right;text-align:right;width:50%}.nav-single + .comments-area,#comment-nav-above{margin:48px 0;margin:3.428571429rem 0}.author .archive-header{margin-bottom:24px;margin-bottom:1.714285714rem}.author-info{border-top:1px solid #ededed;margin:24px 0;margin:1.714285714rem 0;padding-top:24px;padding-top:1.714285714rem;overflow:hidden}.author-description p{color:#757575;font-size:13px;font-size:0.928571429rem;line-height:1.846153846}.author.archive .author-info{border-top:0;margin:0 0 48px;margin:0 0 3.428571429rem}.author.archive .author-avatar{margin-top:0}html{font-size:87.5%}body{font-size:14px;font-size:1rem;font-family:Helvetica,Arial,sans-serif;text-rendering:optimizeLegibility;color:#444}body.custom-font-enabled{font-family:"Open Sans",Helvetica,Arial,sans-serif}a{outline:none;color:#21759b}a:hover{color:#0f3647}.assistive-text,.site .screen-reader-text{position:absolute!important;clip:rect(1px,1px,1px,1px);overflow:hidden;height:1px;width:1px}.main-navigation .assistive-text:focus,.site .screen-reader-text:hover,.site .screen-reader-text:active,.site .screen-reader-text:focus{background:#fff;border:2px solid #333;border-radius:3px;clip:auto!important;color:#000;display:block;font-size:12px;height:auto;padding:12px;position:absolute;top:5px;left:5px;width:auto;z-index:100000}.site{padding:0 24px;padding:0 1.714285714rem;background-color:#fff}.site-content{margin:24px 0 0;margin:1.714285714rem 0 0}.widget-area{margin:24px 0 0;margin:1.714285714rem 0 0}.site-header{padding:24px 0;padding:1.714285714rem 0}.site-header h1,.site-header h2{text-align:center}.site-header h1 a,.site-header h2 a{color:#515151;display:inline-block;text-decoration:none}.site-header h1 a:hover,.site-header h2 a:hover{color:#21759b}.site-header h1{font-size:24px;font-size:1.714285714rem;line-height:1.285714286;margin-bottom:14px;margin-bottom:1rem}.site-header h2{font-weight:normal;font-size:13px;font-size:0.928571429rem;line-height:1.846153846;color:#757575}.header-image{margin-top:24px;margin-top:1.714285714rem}.main-navigation{margin-top:24px;margin-top:1.714285714rem;text-align:center}.main-navigation li{margin-top:24px;margin-top:1.714285714rem;font-size:12px;font-size:0.857142857rem;line-height:1.42857143}.main-navigation a{color:#5e5e5e}.main-navigation a:hover,.main-navigation a:focus{color:#21759b}.main-navigation ul.nav-menu,.main-navigation div.nav-menu>ul{display:none}.main-navigation ul.nav-menu.toggled-on,.menu-toggle{display:inline-block}section[role="banner"]{margin-bottom:48px;margin-bottom:3.428571429rem}.widget-area .widget{-webkit-hyphens:auto;-moz-hyphens:auto;hyphens:auto;margin-bottom:48px;margin-bottom:3.428571429rem;word-wrap:break-word}.widget-area .widget h3{margin-bottom:24px;margin-bottom:1.714285714rem}.widget-area .widget p,.widget-area .widget li,.widget-area .widget .textwidget{font-size:13px;font-size:0.928571429rem;line-height:1.846153846}.widget-area .widget p{margin-bottom:24px;margin-bottom:1.714285714rem}.widget-area .textwidget ul,.widget-area .textwidget ol{list-style:disc outside;margin:0 0 24px;margin:0 0 1.714285714rem}.widget-area .textwidget li>ul,.widget-area .textwidget li>ol{margin-bottom:0}.widget-area .textwidget ol{list-style:decimal}.widget-area .textwidget li{margin-left:36px;margin-left:2.571428571rem}.widget-area .widget a{color:#757575}.widget-area .widget a:hover{color:#21759b}.widget-area .widget a:visited{color:#9f9f9f}.widget-area #s{width:53.66666666666%}footer[role="contentinfo"]{border-top:1px solid #ededed;clear:both;font-size:12px;font-size:0.857142857rem;line-height:2;max-width:960px;max-width:68.571428571rem;margin-top:24px;margin-top:1.714285714rem;margin-left:auto;margin-right:auto;padding:24px 0;padding:1.714285714rem 0}footer[role="contentinfo"] a{color:#686868}footer[role="contentinfo"] a:hover{color:#21759b}.site-info span[role=separator]{padding:0 0.3em 0 0.6em}.site-info span[role=separator]::before{content:'\002f'}.entry-meta{clear:both}.entry-header{margin-bottom:24px;margin-bottom:1.714285714rem}.entry-header img.wp-post-image{margin-bottom:24px;margin-bottom:1.714285714rem}.entry-header .entry-title{font-size:20px;font-size:1.428571429rem;line-height:1.2;font-weight:normal}.entry-header .entry-title a{text-decoration:none}.entry-header .entry-format{margin-top:24px;margin-top:1.714285714rem;font-weight:normal}.entry-header .comments-link{margin-top:24px;margin-top:1.714285714rem;font-size:13px;font-size:0.928571429rem;line-height:1.846153846;color:#757575}.comments-link a,.entry-meta a{color:#757575}.comments-link a:hover,.entry-meta a:hover{color:#21759b}article.sticky .featured-post{border-top:4px double #ededed;border-bottom:4px double #ededed;color:#757575;font-size:13px;font-size:0.928571429rem;line-height:3.692307692;margin-bottom:24px;margin-bottom:1.714285714rem;text-align:center}.entry-content,.entry-summary,.mu_register{line-height:1.714285714}.entry-content h1,.comment-content h1,.entry-content h2,.comment-content h2,.entry-content h3,.comment-content h3,.entry-content h4,.comment-content h4,.entry-content h5,.comment-content h5,.entry-content h6,.comment-content h6{margin:24px 0;margin:1.714285714rem 0;line-height:1.714285714}.entry-content h1,.comment-content h1{font-size:21px;font-size:1.5rem;line-height:1.5}.entry-content h2,.comment-content h2,.mu_register h2{font-size:18px;font-size:1.285714286rem;line-height:1.6}.entry-content h3,.comment-content h3{font-size:16px;font-size:1.142857143rem;line-height:1.846153846}.entry-content h4,.comment-content h4{font-size:14px;font-size:1rem;line-height:1.846153846}.entry-content h5,.comment-content h5{font-size:13px;font-size:0.928571429rem;line-height:1.846153846}.entry-content h6,.comment-content h6{font-size:12px;font-size:0.857142857rem;line-height:1.846153846}.entry-content p,.entry-summary p,.comment-content p,.mu_register p{margin:0 0 24px;margin:0 0 1.714285714rem;line-height:1.714285714}.entry-content a:visited,.comment-content a:visited{color:#9f9f9f}.entry-content .more-link{white-space:nowrap}.entry-content ol,.comment-content ol,.entry-content ul,.comment-content ul,.mu_register ul{margin:0 0 24px;margin:0 0 1.714285714rem;line-height:1.714285714}.entry-content ul ul,.comment-content ul ul,.entry-content ol ol,.comment-content ol ol,.entry-content ul ol,.comment-content ul ol,.entry-content ol ul,.comment-content ol ul{margin-bottom:0}.entry-content ul,.comment-content ul,.mu_register ul{list-style:disc outside}.entry-content ol,.comment-content ol{list-style:decimal outside}.entry-content li,.comment-content li,.mu_register li{margin:0 0 0 36px;margin:0 0 0 2.571428571rem}.entry-content blockquote,.comment-content blockquote{margin-bottom:24px;margin-bottom:1.714285714rem;padding:24px;padding:1.714285714rem;font-style:italic}.entry-content blockquote p:last-child,.comment-content blockquote p:last-child{margin-bottom:0}.entry-content code,.comment-content code{font-family:Consolas,Monaco,Lucida Console,monospace;font-size:12px;font-size:0.857142857rem;line-height:2}.entry-content pre,.comment-content pre{border:1px solid #ededed;color:#666;font-family:Consolas,Monaco,Lucida Console,monospace;font-size:12px;font-size:0.857142857rem;line-height:1.714285714;margin:24px 0;margin:1.714285714rem 0;overflow:auto;padding:24px;padding:1.714285714rem}.entry-content pre code,.comment-content pre code{display:block}.entry-content abbr,.comment-content abbr,.entry-content dfn,.comment-content dfn,.entry-content acronym,.comment-content acronym{border-bottom:1px dotted #666;cursor:help}.entry-content address,.comment-content address{display:block;line-height:1.714285714;margin:0 0 24px;margin:0 0 1.714285714rem}img.alignleft,.wp-caption.alignleft{margin:12px 24px 12px 0;margin:0.857142857rem 1.714285714rem 0.857142857rem 0}img.alignright,.wp-caption.alignright{margin:12px 0 12px 24px;margin:0.857142857rem 0 0.857142857rem 1.714285714rem}img.aligncenter,.wp-caption.aligncenter{clear:both;margin-top:12px;margin-top:0.857142857rem;margin-bottom:12px;margin-bottom:0.857142857rem}.entry-content embed,.entry-content iframe,.entry-content object,.entry-content video{margin-bottom:24px;margin-bottom:1.714285714rem}.entry-content dl,.comment-content dl{margin:0 24px;margin:0 1.714285714rem}.entry-content dt,.comment-content dt{font-weight:bold;line-height:1.714285714}.entry-content dd,.comment-content dd{line-height:1.714285714;margin-bottom:24px;margin-bottom:1.714285714rem}.entry-content table,.comment-content table{border-bottom:1px solid #ededed;color:#757575;font-size:12px;font-size:0.857142857rem;line-height:2;margin:0 0 24px;margin:0 0 1.714285714rem;width:100%}.entry-content table caption,.comment-content table caption{font-size:16px;font-size:1.142857143rem;margin:24px 0;margin:1.714285714rem 0}.entry-content td,.comment-content td{border-top:1px solid #ededed;padding:6px 10px 6px 0}.site-content article{border-bottom:4px double #ededed;margin-bottom:72px;margin-bottom:5.142857143rem;padding-bottom:24px;padding-bottom:1.714285714rem;word-wrap:break-word;-webkit-hyphens:auto;-moz-hyphens:auto;hyphens:auto}.page-links{clear:both;line-height:1.714285714}footer.entry-meta{margin-top:24px;margin-top:1.714285714rem;font-size:13px;font-size:0.928571429rem;line-height:1.846153846;color:#757575}.single-author .entry-meta .by-author{display:none}.mu_register h2{color:#757575;font-weight:normal}.archive-header,.page-header{margin-bottom:48px;margin-bottom:3.428571429rem;padding-bottom:22px;padding-bottom:1.571428571rem;border-bottom:1px solid #ededed}.archive-meta{color:#757575;font-size:12px;font-size:0.857142857rem;line-height:2;margin-top:22px;margin-top:1.571428571rem}.attachment .entry-content .mejs-audio{max-width:400px}.attachment .entry-content .mejs-container{margin-bottom:24px}.article.attachment{overflow:hidden}.image-attachment div.attachment{text-align:center}.image-attachment div.attachment p{text-align:center}.image-attachment div.attachment img{display:block;height:auto;margin:0 auto;max-width:100%}.image-attachment .entry-caption{margin-top:8px;margin-top:0.571428571rem}article.format-aside h1{margin-bottom:24px;margin-bottom:1.714285714rem}article.format-aside h1 a{text-decoration:none;color:#4d525a}article.format-aside h1 a:hover{color:#2e3542}article.format-aside .aside{padding:24px 24px 0;padding:1.714285714rem;background:#d2e0f9;border-left:22px solid #a8bfe8}article.format-aside p{font-size:13px;font-size:0.928571429rem;line-height:1.846153846;color:#4a5466}article.format-aside blockquote:last-child,article.format-aside p:last-child{margin-bottom:0}article.format-image footer h1{font-size:13px;font-size:0.928571429rem;line-height:1.846153846;font-weight:normal}article.format-image footer h2{font-size:11px;font-size:0.785714286rem;line-height:2.181818182}article.format-image footer a h2{font-weight:normal}article.format-link header{padding:0 10px;padding:0 0.714285714rem;float:right;font-size:11px;font-size:0.785714286rem;line-height:2.181818182;font-weight:bold;font-style:italic;text-transform:uppercase;color:#848484;background-color:#ebebeb;border-radius:3px}article.format-link .entry-content{max-width:80%;float:left}article.format-link .entry-content a{font-size:22px;font-size:1.571428571rem;line-height:1.090909091;text-decoration:none}article.format-quote .entry-content p{margin:0;padding-bottom:24px;padding-bottom:1.714285714rem}article.format-quote .entry-content blockquote{display:block;padding:24px 24px 0;padding:1.714285714rem 1.714285714rem 0;font-size:15px;font-size:1.071428571rem;line-height:1.6;font-style:normal;color:#6a6a6a;background:#efefef}.format-status .entry-header{margin-bottom:24px;margin-bottom:1.714285714rem}.format-status .entry-header header{display:inline-block}.format-status .entry-header h1{font-size:15px;font-size:1.071428571rem;font-weight:normal;line-height:1.6;margin:0}.format-status .entry-header h2{font-size:12px;font-size:0.857142857rem;font-weight:normal;line-height:2;margin:0}.format-status .entry-header header a{color:#757575}.format-status .entry-header header a:hover{color:#21759b}.format-status .entry-header img{float:left;margin-right:21px;margin-right:1.5rem}.comments-title{margin-bottom:48px;margin-bottom:3.428571429rem;font-size:16px;font-size:1.142857143rem;line-height:1.5;font-weight:normal}.comments-area article{margin:24px 0;margin:1.714285714rem 0}.comments-area article header{margin:0 0 48px;margin:0 0 3.428571429rem;overflow:hidden;position:relative}.comments-area article header img{float:left;padding:0;line-height:0}.comments-area article header cite,.comments-area article header time{display:block;margin-left:85px;margin-left:6.071428571rem}.comments-area article header cite{font-style:normal;font-size:15px;font-size:1.071428571rem;line-height:1.42857143}.comments-area cite b{font-weight:normal}.comments-area article header time{line-height:1.714285714;text-decoration:none;font-size:12px;font-size:0.857142857rem;color:#5e5e5e}.comments-area article header a{text-decoration:none;color:#5e5e5e}.comments-area article header a:hover{color:#21759b}.comments-area article header cite a{color:#444}.comments-area article header cite a:hover{text-decoration:underline}.comments-area article header h4{position:absolute;top:0;right:0;padding:6px 12px;padding:0.428571429rem 0.857142857rem;font-size:12px;font-size:0.857142857rem;font-weight:normal;color:#fff;background-color:#0088d0;background-repeat:repeat-x;background-image:-moz-linear-gradient(top,#009cee,#0088d0);background-image:-ms-linear-gradient(top,#009cee,#0088d0);background-image:-webkit-linear-gradient(top,#009cee,#0088d0);background-image:-o-linear-gradient(top,#009cee,#0088d0);background-image:linear-gradient(to bottom,#009cee,#0088d0);border-radius:3px;border:1px solid #007cbd}.comments-area .bypostauthor cite span{position:absolute;margin-left:5px;margin-left:0.357142857rem;padding:2px 5px;padding:0.142857143rem 0.357142857rem;font-size:10px;font-size:0.714285714rem}.comments-area .bypostauthor cite b{font-weight:bold}a.comment-reply-link,a.comment-edit-link{color:#686868;font-size:13px;font-size:0.928571429rem;line-height:1.846153846}a.comment-reply-link:hover,a.comment-edit-link:hover{color:#21759b}.commentlist .pingback{line-height:1.714285714;margin-bottom:24px;margin-bottom:1.714285714rem}#respond{margin-top:48px;margin-top:3.428571429rem}#respond h3#reply-title{font-size:16px;font-size:1.142857143rem;line-height:1.5}#respond h3#reply-title #cancel-comment-reply-link{margin-left:10px;margin-left:0.714285714rem;font-weight:normal;font-size:12px;font-size:0.857142857rem}#respond form{margin:24px 0;margin:1.714285714rem 0}#respond form p{margin:11px 0;margin:0.785714286rem 0}#respond form p.logged-in-as{margin-bottom:24px;margin-bottom:1.714285714rem}#respond form label{display:block;line-height:1.714285714}#respond form input[type="text"],#respond form textarea{-moz-box-sizing:border-box;box-sizing:border-box;font-size:12px;font-size:0.857142857rem;line-height:1.714285714;padding:10px;padding:0.714285714rem;width:100%}#respond form p.form-allowed-tags{margin:0;font-size:12px;font-size:0.857142857rem;line-height:2;color:#5e5e5e}#respond #wp-comment-cookies-consent{margin:0 10px 0 0}#respond .comment-form-cookies-consent label{display:inline}.required{color:red}.entry-page-image{margin-bottom:14px;margin-bottom:1rem}.template-front-page .site-content article{border:0;margin-bottom:0}.template-front-page .widget-area{clear:both;float:none;width:auto;padding-top:24px;padding-top:1.714285714rem;border-top:1px solid #ededed}.template-front-page .widget-area .widget li{margin:8px 0 0;margin:0.571428571rem 0 0;font-size:13px;font-size:0.928571429rem;line-height:1.714285714;list-style-type:square;list-style-position:inside}.template-front-page .widget-area .widget li a{color:#757575}.template-front-page .widget-area .widget li a:hover{color:#21759b}.template-front-page .widget-area .widget_text img{float:left;margin:8px 24px 8px 0;margin:0.571428571rem 1.714285714rem 0.571428571rem 0}.widget select{max-width:100%}.widget-area .widget ul ul{margin-left:12px;margin-left:0.857142857rem}.widget_rss li{margin:12px 0;margin:0.857142857rem 0}.widget_recent_entries .post-date,.widget_rss .rss-date{color:#aaa;font-size:11px;font-size:0.785714286rem;margin-left:12px;margin-left:0.857142857rem}.wp-calendar-nav,#wp-calendar{margin:0;width:100%;font-size:13px;font-size:0.928571429rem;line-height:1.846153846;color:#686868}#wp-calendar th,#wp-calendar td,#wp-calendar caption{text-align:left}.wp-calendar-nav{display:table}.wp-calendar-nav span{display:table-cell}.wp-calendar-nav-next,#wp-calendar #next{padding-right:24px;padding-right:1.714285714rem;text-align:right}.widget_search label{display:block;font-size:13px;font-size:0.928571429rem;line-height:1.846153846}.widget_twitter li{list-style-type:none}.widget_twitter .timesince{display:block;text-align:right}.tagcloud ul{list-style-type:none}.tagcloud ul li{display:inline-block}.widget-area .widget.widget_tag_cloud li{line-height:1}.template-front-page .widget-area .widget.widget_tag_cloud li{margin:0}.widget-area .gallery-columns-2.gallery-size-full .gallery-icon img,.widget-area .gallery-columns-3.gallery-size-full .gallery-icon img,.widget-area .gallery-columns-4.gallery-size-full .gallery-icon img,.widget-area .gallery-columns-5.gallery-size-full .gallery-icon img,.widget-area .gallery-columns-6 .gallery-icon img,.widget-area .gallery-columns-7 .gallery-icon img,.widget-area .gallery-columns-8 .gallery-icon img,.widget-area .gallery-columns-9 .gallery-icon img{height:auto;max-width:80%}img#wpstats{display:block;margin:0 auto 24px;margin:0 auto 1.714285714rem}@-ms-viewport{width:device-width}@viewport{width:device-width}@media screen and (min-width:600px){.author-avatar{float:left;margin-top:8px;margin-top:0.571428571rem}.author-description{float:right;width:80%}.site{margin:0 auto;max-width:960px;max-width:68.571428571rem;overflow:hidden}.site-content{float:left;width:65.104166667%}body.template-front-page .site-content,body.attachment .site-content,body.full-width .site-content{width:100%}.widget-area{float:right;width:26.041666667%}.site-header h1,.site-header h2{text-align:left}.site-header h1{font-size:26px;font-size:1.857142857rem;line-height:1.846153846;margin-bottom:0}.main-navigation ul.nav-menu,.main-navigation div.nav-menu>ul{border-bottom:1px solid #ededed;border-top:1px solid #ededed;display:inline-block!important;text-align:left;width:100%}.main-navigation ul{margin:0;text-indent:0}.main-navigation li a,.main-navigation li{display:inline-block;text-decoration:none}.main-navigation li a{border-bottom:0;color:#6a6a6a;line-height:3.692307692;text-transform:uppercase;white-space:nowrap}.main-navigation li a:hover,.main-navigation li a:focus{color:#000}.main-navigation li{margin:0 40px 0 0;margin:0 2.857142857rem 0 0;position:relative}.main-navigation li ul{margin:0;padding:0;position:absolute;top:100%;z-index:1;height:1px;width:1px;overflow:hidden;clip:rect(1px,1px,1px,1px)}.main-navigation li ul ul{top:0;left:100%}.main-navigation ul li:hover>ul,.main-navigation ul li:focus>ul,.main-navigation .focus>ul{border-left:0;clip:inherit;overflow:inherit;height:inherit;width:inherit}.main-navigation li ul li a{background:#efefef;border-bottom:1px solid #ededed;display:block;font-size:11px;font-size:0.785714286rem;line-height:2.181818182;padding:8px 10px;padding:0.571428571rem 0.714285714rem;width:180px;width:12.85714286rem;white-space:normal}.main-navigation li ul li a:hover,.main-navigation li ul li a:focus{background:#e3e3e3;color:#444}.main-navigation .current-menu-item>a,.main-navigation .current-menu-ancestor>a,.main-navigation .current_page_item>a,.main-navigation .current_page_ancestor>a{color:#636363;font-weight:bold}.menu-toggle{display:none}.entry-header .entry-title{font-size:22px;font-size:1.571428571rem}#respond form input[type="text"]{width:46.333333333%}#respond form textarea.blog-textarea{width:79.666666667%}.template-front-page .site-content,.template-front-page article{overflow:hidden}.template-front-page.has-post-thumbnail article{float:left;width:47.916666667%}.entry-page-image{float:right;margin-bottom:0;width:47.916666667%}.template-front-page .widget-area .widget,.template-front-page.two-sidebars .widget-area .front-widgets{float:left;width:51.875%;margin-bottom:24px;margin-bottom:1.714285714rem}.template-front-page .widget-area .widget:nth-child(odd){clear:right}.template-front-page .widget-area .widget:nth-child(even),.template-front-page.two-sidebars .widget-area .front-widgets + .front-widgets{float:right;width:39.0625%;margin:0 0 24px;margin:0 0 1.714285714rem}.template-front-page.two-sidebars .widget,.template-front-page.two-sidebars .widget:nth-child(even){float:none;width:auto}.commentlist .children{margin-left:48px;margin-left:3.428571429rem}}@media screen and (min-width:960px){body{background-color:#e6e6e6}body .site{padding:0 40px;padding:0 2.857142857rem;margin-top:48px;margin-top:3.428571429rem;margin-bottom:48px;margin-bottom:3.428571429rem;box-shadow:0 2px 6px rgba(100,100,100,0.3)}body.custom-background-empty{background-color:#fff}body.custom-background-empty .site,body.custom-background-white .site{padding:0;margin-top:0;margin-bottom:0;box-shadow:none}}@media print{body{background:none!important;color:#000;font-size:10pt}footer a[rel=bookmark]:link:after,footer a[rel=bookmark]:visited:after{content:" [" attr(href) "] "}a{text-decoration:none}.entry-content img,.comment-content img,.author-avatar img,img.wp-post-image{border-radius:0;box-shadow:none}.site{clear:both!important;display:block!important;float:none!important;max-width:100%;position:relative!important}.site-header{margin-bottom:72px;margin-bottom:5.142857143rem;text-align:left}.site-header h1{font-size:21pt;line-height:1;text-align:left}.site-header h2{color:#000;font-size:10pt;text-align:left}.site-header h1 a,.site-header h2 a{color:#000}.author-avatar,#colophon,#respond,.commentlist .comment-edit-link,.commentlist .reply,.entry-header .comments-link,.entry-meta .edit-link a,.page-link,.site-content nav,.widget-area,img.header-image,.main-navigation{display:none}.wrapper{border-top:none;box-shadow:none}.site-content{margin:0;width:auto}.entry-header .entry-title,.entry-title{font-size:21pt}footer.entry-meta,footer.entry-meta a{color:#444;font-size:10pt}.author-description{float:none;width:auto}.commentlist>li.comment{background:none;position:relative;width:auto}.comments-area article header cite,.comments-area article header time{margin-left:50px;margin-left:3.57142857rem}}.breadcrumb div{display:inline;font-size:13px;margin-left:-3px}#wp-auto-top{position:fixed;top:45%;right:50%;display:block;margin-right:-540px;z-index:9999}#wp-auto-top-top,#wp-auto-top-comment,#wp-auto-top-bottom{background:url(https://www.lylinux.org/wp-content/plugins/wp-auto-top/img/1.png) no-repeat;position:relative;cursor:pointer;height:25px;width:29px;margin:10px 0 0}#wp-auto-top-comment{background-position:left -30px;height:32px}#wp-auto-top-bottom{background-position:left -68px}#wp-auto-top-comment:hover{background-position:right -30px}#wp-auto-top-top:hover{background-position:right 0}#wp-auto-top-bottom:hover{background-position:right -68px}.widget-login{margin-top:15px!important}#comments{margin-top:20px}#pinglist-container{display:none}.comment-tabs{margin-bottom:20px;font-size:15px;border-bottom:2px solid #e5e5e5}.comment-tabs li{float:left;margin-bottom:-2px}.comment-tabs li a{display:block;padding:0 10px 10px;font-weight:600;color:#aaa;border-bottom:2px solid #e5e5e5}.comment-tabs li a:hover{color:#444;border-color:#ccc}.comment-tabs li span{margin-left:8px;padding:0 6px;border-radius:4px;background-color:#e5e5e5}.comment-tabs li i{margin-right:6px}.comment-tabs li.active a{color:#e8554e;border-bottom-color:#e8554e}.commentlist,.pinglist{margin-bottom:20px}.commentlist li,.pinglist li{padding-left:60px;font-size:14px;line-height:22px;font-weight:400}.commentlist .comment-body,.pinglist li{position:relative;padding-bottom:20px;clear:both;word-break:break-all}.commentlist .comment-body{position:relative;padding-left:60px;min-height:48px}.commentlist .comment-author{display:inline-block;margin:0 10px 5px 0;font-size:13px;position:relative}.commentlist .comment-meta{display:inline-block;margin:0 0 8px 0;font-size:12px;color:#666}.commentlist .comment-awaiting-moderation{display:block;font-size:13px;line-height:22px}.commentlist .comment-author .avatar{position:absolute!important;left:-60px;top:0;width:48px!important;height:48px!important;border-radius:50%;display:block;object-fit:cover;background-color:#f5f5f5;border:1px solid #ddd}.commentlist .comment-author .fn{display:inline;margin:0;font-weight:600;color:#2e7bb8;font-size:13px}.commentlist .comment-author .fn a{color:#2e7bb8;text-decoration:none}.commentlist .comment-author .fn a:hover{text-decoration:underline}.commentlist .comment-body p{margin:5px 0 10px 0;line-height:1.5}.commentlist .fn,.pinglist .ping-link{color:#444;font-size:13px;font-style:normal;font-weight:600}.commentlist .says{display:none}.commentlist .avatar{width:48px!important;height:48px!important;border-radius:50%;display:block;object-fit:cover;background-color:#f5f5f5;border:1px solid #ddd}.commentlist .comment-meta:before,.pinglist .ping-meta:before{vertical-align:4%;margin-right:3px;font-size:10px;font-family:FontAwesome;color:#ccc}.commentlist .comment-meta a,.pinglist .ping-meta{color:#aaa}.commentlist .reply{font-size:13px;line-height:16px}.commentlist .reply a,.commentlist .comment-reply-chain{color:#aaa}.commentlist .reply a:hover,.commentlist .comment-reply-chain:hover{color:#444}.comment-awaiting-moderation{color:#e8554e;font-style:normal}.pinglist li{padding-left:0}.commentlist .comment-body p{margin-bottom:8px;color:#777;clear:both}.commentlist .comment-body strong{font-weight:600}.commentlist .comment-body ol li{margin-left:2em;padding:0;list-style:decimal}.commentlist .comment-body ul li{margin-left:2em;padding:0;list-style:square}.commentlist li.bypostauthor>.comment-body:after,.commentlist li.comment-author-admin>.comment-body:after{display:block;position:absolute;content:"\f040";width:12px;line-height:12px;font-style:normal;font-family:FontAwesome;text-align:center;color:#fff;background-color:#e8554e}.commentlist li.comment-author-admin>.comment-body:after{content:"\f005"}.commentlist li.bypostauthor>.comment-body:after,.commentlist li.comment-author-admin>.comment-body:after{padding:3px;top:32px;left:-28px;font-size:12px;border-radius:100%}.commentlist li li.bypostauthor>.comment-body:after,.commentlist li li.comment-author-admin>.comment-body:after{padding:2px;top:22px;left:-26px;font-size:10px;border-radius:100%}.commentlist li ul{}.commentlist li li{margin:0;padding-left:48px}.commentlist li li .comment-body{padding-left:60px;min-height:48px}.commentlist li li .comment-author{display:inline-block;margin:0 8px 5px 0;font-size:12px}.commentlist li li .comment-meta{display:inline-block;margin:0 0 8px 0;font-size:11px;color:#666}#comments #commentlist-container.comment-tab{margin-left:-15px!important;padding-left:0!important;position:relative!important}@media screen and (min-width:600px){#comments #commentlist-container.comment-tab{margin-left:-30px!important}.commentlist .comment-body{padding-left:60px!important;min-height:48px!important}.commentlist .comment-author{display:inline-block!important;margin:0 8px 5px 0!important}.commentlist .comment-meta{display:inline-block!important;margin:0 0 8px 0!important}.commentlist .comment-author .avatar{left:-60px!important;width:48px!important;height:48px!important}.commentlist li li .comment-body{padding-left:60px!important;min-height:48px!important}.commentlist li li .comment-author .avatar{left:-60px!important;width:48px!important;height:48px!important}}.commentlist li li .comment-author .avatar{position:absolute!important;left:-60px;top:0;width:48px!important;height:48px!important;border-radius:50%;display:block;object-fit:cover;background-color:#f5f5f5;border:1px solid #ddd}.comments-nav{margin-bottom:20px}.comments-nav a{font-weight:600}.comments-nav .nav-previous{float:left}.comments-nav .nav-next{float:right}.logged-in-as,.comment-notes,.form-allowed-tags{display:none}#respond{position:relative}#reply-title{margin-bottom:20px}li #reply-title{margin:0!important;padding:0;height:0;font-size:0;border-top:0}#cancel-comment-reply-link{float:right;bottom:26px;right:20px;font-size:12px;color:#999}#cancel-comment-reply-link:hover{color:#777}#commentform{margin-bottom:20px;padding:10px 20px 20px;border-radius:4px;background-color:#e5e5e5}#commentform p.comment-form-author{float:left;width:48%}#commentform p.comment-form-email{float:right;width:48%}#commentform p.comment-form-url,#commentform p.comment-form-comment{clear:both}#commentform label{display:block;padding:6px 0;font-weight:600}#commentform input[type="text"],#commentform textarea{max-width:100%;width:100%}#commentform textarea{height:100px}#commentform p.form-submit{margin-top:10px}.logged-in #reply-title{margin-bottom:20px}.logged-in #commentform p.comment-form-comment{margin-top:10px}.logged-in #commentform p.comment-form-comment label{display:none}.heading,#reply-title{margin-bottom:1em;font-size:18px;font-weight:600;text-transform:uppercase;color:#222}.heading i{margin-right:6px;font-size:22px}.group:before{content:"";display:table}.group:after{content:"";display:table;clear:both}.cancel-comment{margin:0;padding:0;border:0;font:inherit;vertical-align:baseline}#rocket{position:fixed;right:50px;bottom:50px;display:block;visibility:hidden;width:26px;height:48px;background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAB8CAYAAAB356CJAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKTWlDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVN3WJP3Fj7f92UPVkLY8LGXbIEAIiOsCMgQWaIQkgBhhBASQMWFiApWFBURnEhVxILVCkidiOKgKLhnQYqIWotVXDjuH9yntX167+3t+9f7vOec5/zOec8PgBESJpHmomoAOVKFPDrYH49PSMTJvYACFUjgBCAQ5svCZwXFAADwA3l4fnSwP/wBr28AAgBw1S4kEsfh/4O6UCZXACCRAOAiEucLAZBSAMguVMgUAMgYALBTs2QKAJQAAGx5fEIiAKoNAOz0ST4FANipk9wXANiiHKkIAI0BAJkoRyQCQLsAYFWBUiwCwMIAoKxAIi4EwK4BgFm2MkcCgL0FAHaOWJAPQGAAgJlCLMwAIDgCAEMeE80DIEwDoDDSv+CpX3CFuEgBAMDLlc2XS9IzFLiV0Bp38vDg4iHiwmyxQmEXKRBmCeQinJebIxNI5wNMzgwAABr50cH+OD+Q5+bk4eZm52zv9MWi/mvwbyI+IfHf/ryMAgQAEE7P79pf5eXWA3DHAbB1v2upWwDaVgBo3/ldM9sJoFoK0Hr5i3k4/EAenqFQyDwdHAoLC+0lYqG9MOOLPv8z4W/gi372/EAe/tt68ABxmkCZrcCjg/1xYW52rlKO58sEQjFu9+cj/seFf/2OKdHiNLFcLBWK8ViJuFAiTcd5uVKRRCHJleIS6X8y8R+W/QmTdw0ArIZPwE62B7XLbMB+7gECiw5Y0nYAQH7zLYwaC5EAEGc0Mnn3AACTv/mPQCsBAM2XpOMAALzoGFyolBdMxggAAESggSqwQQcMwRSswA6cwR28wBcCYQZEQAwkwDwQQgbkgBwKoRiWQRlUwDrYBLWwAxqgEZrhELTBMTgN5+ASXIHrcBcGYBiewhi8hgkEQcgIE2EhOogRYo7YIs4IF5mOBCJhSDSSgKQg6YgUUSLFyHKkAqlCapFdSCPyLXIUOY1cQPqQ28ggMor8irxHMZSBslED1AJ1QLmoHxqKxqBz0XQ0D12AlqJr0Rq0Hj2AtqKn0UvodXQAfYqOY4DRMQ5mjNlhXIyHRWCJWBomxxZj5Vg1Vo81Yx1YN3YVG8CeYe8IJAKLgBPsCF6EEMJsgpCQR1hMWEOoJewjtBK6CFcJg4Qxwicik6hPtCV6EvnEeGI6sZBYRqwm7iEeIZ4lXicOE1+TSCQOyZLkTgohJZAySQtJa0jbSC2kU6Q+0hBpnEwm65Btyd7kCLKArCCXkbeQD5BPkvvJw+S3FDrFiOJMCaIkUqSUEko1ZT/lBKWfMkKZoKpRzame1AiqiDqfWkltoHZQL1OHqRM0dZolzZsWQ8ukLaPV0JppZ2n3aC/pdLoJ3YMeRZfQl9Jr6Afp5+mD9HcMDYYNg8dIYigZaxl7GacYtxkvmUymBdOXmchUMNcyG5lnmA+Yb1VYKvYqfBWRyhKVOpVWlX6V56pUVXNVP9V5qgtUq1UPq15WfaZGVbNQ46kJ1Bar1akdVbupNq7OUndSj1DPUV+jvl/9gvpjDbKGhUaghkijVGO3xhmNIRbGMmXxWELWclYD6yxrmE1iW7L57Ex2Bfsbdi97TFNDc6pmrGaRZp3mcc0BDsax4PA52ZxKziHODc57LQMtPy2x1mqtZq1+rTfaetq+2mLtcu0W7eva73VwnUCdLJ31Om0693UJuja6UbqFutt1z+o+02PreekJ9cr1Dund0Uf1bfSj9Rfq79bv0R83MDQINpAZbDE4Y/DMkGPoa5hpuNHwhOGoEctoupHEaKPRSaMnuCbuh2fjNXgXPmasbxxirDTeZdxrPGFiaTLbpMSkxeS+Kc2Ua5pmutG003TMzMgs3KzYrMnsjjnVnGueYb7ZvNv8jYWlRZzFSos2i8eW2pZ8ywWWTZb3rJhWPlZ5VvVW16xJ1lzrLOtt1ldsUBtXmwybOpvLtqitm63Edptt3xTiFI8p0in1U27aMez87ArsmuwG7Tn2YfYl9m32zx3MHBId1jt0O3xydHXMdmxwvOuk4TTDqcSpw+lXZxtnoXOd8zUXpkuQyxKXdpcXU22niqdun3rLleUa7rrStdP1o5u7m9yt2W3U3cw9xX2r+00umxvJXcM970H08PdY4nHM452nm6fC85DnL152Xlle+70eT7OcJp7WMG3I28Rb4L3Le2A6Pj1l+s7pAz7GPgKfep+Hvqa+It89viN+1n6Zfgf8nvs7+sv9j/i/4XnyFvFOBWABwQHlAb2BGoGzA2sDHwSZBKUHNQWNBbsGLww+FUIMCQ1ZH3KTb8AX8hv5YzPcZyya0RXKCJ0VWhv6MMwmTB7WEY6GzwjfEH5vpvlM6cy2CIjgR2yIuB9pGZkX+X0UKSoyqi7qUbRTdHF09yzWrORZ+2e9jvGPqYy5O9tqtnJ2Z6xqbFJsY+ybuIC4qriBeIf4RfGXEnQTJAntieTE2MQ9ieNzAudsmjOc5JpUlnRjruXcorkX5unOy553PFk1WZB8OIWYEpeyP+WDIEJQLxhP5aduTR0T8oSbhU9FvqKNolGxt7hKPJLmnVaV9jjdO31D+miGT0Z1xjMJT1IreZEZkrkj801WRNberM/ZcdktOZSclJyjUg1plrQr1zC3KLdPZisrkw3keeZtyhuTh8r35CP5c/PbFWyFTNGjtFKuUA4WTC+oK3hbGFt4uEi9SFrUM99m/ur5IwuCFny9kLBQuLCz2Lh4WfHgIr9FuxYji1MXdy4xXVK6ZHhp8NJ9y2jLspb9UOJYUlXyannc8o5Sg9KlpUMrglc0lamUycturvRauWMVYZVkVe9ql9VbVn8qF5VfrHCsqK74sEa45uJXTl/VfPV5bdra3kq3yu3rSOuk626s91m/r0q9akHV0IbwDa0b8Y3lG19tSt50oXpq9Y7NtM3KzQM1YTXtW8y2rNvyoTaj9nqdf13LVv2tq7e+2Sba1r/dd3vzDoMdFTve75TsvLUreFdrvUV99W7S7oLdjxpiG7q/5n7duEd3T8Wej3ulewf2Re/ranRvbNyvv7+yCW1SNo0eSDpw5ZuAb9qb7Zp3tXBaKg7CQeXBJ9+mfHvjUOihzsPcw83fmX+39QjrSHkr0jq/dawto22gPaG97+iMo50dXh1Hvrf/fu8x42N1xzWPV56gnSg98fnkgpPjp2Snnp1OPz3Umdx590z8mWtdUV29Z0PPnj8XdO5Mt1/3yfPe549d8Lxw9CL3Ytslt0utPa49R35w/eFIr1tv62X3y+1XPK509E3rO9Hv03/6asDVc9f41y5dn3m978bsG7duJt0cuCW69fh29u0XdwruTNxdeo94r/y+2v3qB/oP6n+0/rFlwG3g+GDAYM/DWQ/vDgmHnv6U/9OH4dJHzEfVI0YjjY+dHx8bDRq98mTOk+GnsqcTz8p+Vv9563Or59/94vtLz1j82PAL+YvPv655qfNy76uprzrHI8cfvM55PfGm/K3O233vuO+638e9H5ko/ED+UPPR+mPHp9BP9z7nfP78L/eE8/sl0p8zAAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAAAbdSURBVHja5NlbbBRVGAfw5VID+LAK8cEoxqTgmw8kPPhwipTGxJTDUAVBQBMNKtZboiDE2ES8pFEjGhNkkCrin3JbZo4YCqloUOoKJCDIRWyRAgW6R3dobU2bJtj6+eCMTqczs2d3Zh6Mm3xpdvc7++vMnHNmzvlSRJQqJgA8B8AC8EQx7YoBxgD4CAC54i0Ao2KDAIwCsNGDOPF6nNBLAYgTiyNDAKYDGCwA/Q7gtpIhAKMBHC+AOPF5FGiBIuLEXaVCR4uEzKIhAHcViRCAP4OuVRi0pgSIACwvFurw/ohhGJTP56m7u5vy+TwZhuEHHVKGANzmh3R3d48IH2wQwPWq0CIv5ByJN/L5vN9RzVKF3vQ29kOcULlOQZAZ8YjWq0JHI1wjAvClKnTJr+sq9joCcEoV6itxDDmRU4UoYvT8f6GeiFCXKpSLCJ1XhU5GhI6oQs0RoT2qUENESFeFlkeEXlCFZkeEqlWhWyNCtxSE7GdsPSL0AYAxgRCACQB2xzAzEAABYMIIyEYOxIQ4sR/AOC+UiRlxYvM/EID5CSFO1DjQoYShFmfFMJgwdC0FYHzCCAEYck5dZ8LQWQdCwpAe19xWKCocqAzA1YSQiwBGuwfs2yHJpwDcEBJHQtqu9s4MU0KSHy+wBF0c1NsATPabVL/ye6IBML4AVAbgik/bvUGz9zyf5HrFTY9VPm0XBkFlAH7xrN5uVYQmAuh3P0Q6M3fQje81V/LWIne+1gY9oPglTwLQai+Wby8SugnAj/Y2W7nqqnyUz2cagDb7P24DoAXshI2Nsl9XZXdXb/etintjMBswVrJxQ0H3rMG4oYEAaOA/e+rqAqC6uKHyAKg8VsjGDnqQg7Hve9tQrQeqTQpKuybOfgDpRCDParAhkZKBC5pmQ9MShWysvtg2RSOZTKYu0WqLYRhjTdMUQghqbGxMrtpimuYuIQQJIWj79u3JVFsMw3jHQYQQfhuC0asthmFUCiGG3JAQgjZv3hxftaW5uXmMEOJnLyKEoK1bt8ZXbTEMY5kfIoSgHTt2xFdtEUK0BkE7d+6Mp9piGMY9QYgQgkzTjKfaYprmJvcPn/vhOHV8+D511j5EuUWzqXPZEmpd9x59/102WrVFCPGrG7myopZkzUyS2ox/Ijf3bjq/8mkvpl5tMQzjDvfRdKx7l+TcmZR7bAH1nThGf167Rn0njlHn0gcoV1NJrWvXlFZtMQzjaTfU+eQSknMqqP+n0+R+9Z05RXJOBXUsW1xatcUwjAY3lLu/iuScCvJ7SW0GXVlUXVq1xTTN/cOghfcGH5E2w++I1Kot3vFzceP6vy++5xrlli6gXM1MOvOxXlq1RQiR946by6tXkpw7vNfJmko698qL1NzUVFq1RQgx4DdIL2z7lDqfephyD2l05dlH6ELjRj9EvdoSNiMozA7qtQlVSAjx34H6IkJdqlBXROi86oBtjwgdUYUOR4T2qEJmREhXnVTrI0IvqEJLIg7YalWoXAUKqSwXrrZIzsZIzvSfT5woCTr2zdckOftAchZcbZGcTZCc7ZacUfu+vQWhTCYzAjq9vZEkZyQ5E5KzkdUWGzlgJ9GFjetLgtrerXcgkpztl5yN80IZVwJdWvVMQcizqiAAdPHZR90QSc7+rbZIzuZ7vqTcfZXUdvp0KOR9/j78bQvlaiq9EEnOahzokM+X1P7FnlBoy5Ytw69P4yd+CEnOWlKSs9GSs0G/hI41bxQ1WNtffj4IupaSnI0P+JJyD1bT8aNHlbr24ZYWys2rCoKGnFPXGYS1N+1S6nFnPtaDEJKcnXUgBCVdfrHWF9q2bdswqGPZ4jBId6DZIUnUnm0J7Qgnd5lhCEnOKhyoTHJ2NSjx0qurQifTCytqw5CLkrPR7gH7dkhy6HaZ5OzbkLarvTPDlJDkRQWg+UG9TXI22W9S/conWUrOrisAjbVPkbft3qDZe55P8qsqmx6SsxU+bRcGQWWSs19ciX9Izm5WhG6UnPW52vY4M3fQje81V3JR1RbJ2Vr32Cl0h50kOWuVnHVIzm4vErpJcvaj5MySnKlVWyRnw7bHLF1L9WbTWm823dabTZP9V7N0bUQ7yVnp1RZL16p69k0eshHqzaapZ9/kIUvX4q22WLqW7cpMJzfUlZlOlq5l44YGrQ3VwyBrQzVZujYYNzRg6Rr1tkz8G2qZSJaukaVrA7GfOkvX6LemqdSbTdNvTVMdKPZTV2fpGl3dNIt6s2m6ummWA9XFDZXbP0zdn93pIGTpWnncUMrStYMugOz3qSSgWg9UmxSUtnSt30b67feJQClL1xpsqMH5LClomg1NSxpKWbpW736v0v6vAQCo4CbBrd8RBQAAAABJRU5ErkJggg==") no-repeat 50% 0;cursor:pointer;-webkit-transition:all 0s;transition:all 0s}#rocket:hover{background-position:50% -62px}#rocket.show{visibility:visible;opacity:1}#rocket.move{background-position:50% -62px;-webkit-animation:toTop .8s ease-in;animation:toTop .8s ease-in;animation-fill-mode:forwards;-webkit-animation-fill-mode:forwards}.comment-markdown{float:right;font-size:small}.breadcrumb{margin-bottom:20px;list-style:none;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li + li:before{color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.break_line{height:1px;border:none}.comment-body{overflow-wrap:break-word;word-wrap:break-word;word-break:break-word;max-width:100%;box-sizing:border-box}.comment-content pre,.comment-body pre{white-space:pre-wrap!important;word-wrap:break-word!important;overflow-wrap:break-word!important;max-width:100%!important;overflow-x:auto;padding:10px;background-color:#f8f8f8;border:1px solid #ddd;border-radius:4px;font-size:12px;line-height:1.4;margin:10px 0}.comment-content code,.comment-body code{word-wrap:break-word!important;overflow-wrap:break-word!important;white-space:pre-wrap;max-width:100%;display:inline-block;vertical-align:top}.comment-content a,.comment-body a{word-wrap:break-word!important;overflow-wrap:break-word!important;word-break:break-all;max-width:100%}.comment-content p,.comment-body p{word-wrap:break-word!important;overflow-wrap:break-word!important;max-width:100%;margin:10px 0}.comment-content .codehilite,.comment-body .codehilite{max-width:100%!important;overflow-x:auto;margin:10px 0;background:#f8f8f8!important;border:1px solid #ddd;border-radius:4px;padding:10px;font-size:12px;line-height:1.4;width:100%;box-sizing:border-box;display:block}.comment-content .codehilite pre,.comment-body .codehilite pre{white-space:pre-wrap!important;word-wrap:break-word!important;overflow-wrap:break-word!important;margin:0!important;padding:0!important;background:transparent!important;border:none!important;font-size:inherit;line-height:inherit;max-width:100%;width:100%;box-sizing:border-box}.comment-content .codehilite span,.comment-body .codehilite span{word-wrap:break-word!important;overflow-wrap:break-word!important;display:inline;max-width:100%}.comment-content .codehilite .kt,.comment-content .codehilite .nf,.comment-content .codehilite .n,.comment-content .codehilite .p,.comment-body .codehilite .kt,.comment-body .codehilite .nf,.comment-body .codehilite .n,.comment-body .codehilite .p{word-wrap:break-word!important;overflow-wrap:break-word!important}.search-result{margin-bottom:30px;padding:20px;border:1px solid #e1e1e1;border-radius:5px;background:#fff}.search-result .entry-title{margin:0 0 10px 0;font-size:1.5em}.search-result .entry-title a{color:#2c3e50;text-decoration:none}.search-result .entry-title a:hover{color:#3498db}.search-result .entry-meta{color:#7f8c8d;font-size:0.9em;margin-bottom:15px}.search-result .entry-meta span{margin-right:15px}.search-excerpt{line-height:1.6;color:#555}.search-excerpt p{margin:10px 0}.search-excerpt em,.search-result .entry-title em{background-color:#fff3cd;color:#856404;font-style:normal;font-weight:bold;padding:2px 4px;border-radius:3px}.more-link{color:#3498db;text-decoration:none;font-weight:bold}.more-link:hover{text-decoration:underline}.comment-content .codehilite .w,.comment-content .codehilite .o,.comment-body .codehilite .kt,.comment-body .codehilite .nf,.comment-body .codehilite .n,.comment-body .codehilite .p,.comment-body .codehilite .w,.comment-body .codehilite .o{word-break:break-all;overflow-wrap:break-word}.commentlist li{max-width:100%;overflow:hidden;box-sizing:border-box}.commentlist .comment-body{max-width:calc(100% - 20px);margin-left:10px;margin-right:10px;overflow:hidden;word-wrap:break-word}.commentlist li[style*="margin-left"]{max-width:calc(100% - 2rem)!important;overflow:hidden;box-sizing:border-box}.commentlist li[style*="margin-left: 3rem"],.commentlist li[style*="margin-left: 6rem"],.commentlist li[style*="margin-left: 9rem"]{max-width:calc(100% - 1rem)!important}@media (max-width:768px){.comment-content pre,.comment-body pre{font-size:11px;padding:8px;margin:8px 0}.commentlist .comment-body{max-width:calc(100% - 10px);margin-left:5px;margin-right:5px}.commentlist li[style*="margin-left"]{margin-left:1rem!important;max-margin-left:2rem!important}}.comment-content table,.comment-body table{max-width:100%;overflow-x:auto;display:block;white-space:nowrap}.comment-content img,.comment-body img{max-width:100%!important;height:auto!important}.comment-content blockquote,.comment-body blockquote{max-width:100%;overflow-wrap:break-word;word-wrap:break-word;padding:10px 15px;margin:10px 0;border-left:4px solid #ddd;background-color:#f9f9f9}
\ No newline at end of file
diff --git a/src/collectedstatic/compressed/js/output.c56f0a57c4ca.js b/src/collectedstatic/compressed/js/output.c56f0a57c4ca.js
new file mode 100644
index 0000000..7c28331
--- /dev/null
+++ b/src/collectedstatic/compressed/js/output.c56f0a57c4ca.js
@@ -0,0 +1,42 @@
+/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML=" ",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML=" ";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML=" ",y.option=!!ce.lastChild;var ge={thead:[1,""],col:[2,""],tr:[2,""],td:[3,""],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0
'};NProgress.configure=function(options){var key,value;for(key in options){value=options[key];if(value!==undefined&&options.hasOwnProperty(key))Settings[key]=value;}
+return this;};NProgress.status=null;NProgress.set=function(n){var started=NProgress.isStarted();n=clamp(n,Settings.minimum,1);NProgress.status=(n===1?null:n);var progress=NProgress.render(!started),bar=progress.querySelector(Settings.barSelector),speed=Settings.speed,ease=Settings.easing;progress.offsetWidth;queue(function(next){if(Settings.positionUsing==='')Settings.positionUsing=NProgress.getPositioningCSS();css(bar,barPositionCSS(n,speed,ease));if(n===1){css(progress,{transition:'none',opacity:1});progress.offsetWidth;setTimeout(function(){css(progress,{transition:'all '+speed+'ms linear',opacity:0});setTimeout(function(){NProgress.remove();next();},speed);},speed);}else{setTimeout(next,speed);}});return this;};NProgress.isStarted=function(){return typeof NProgress.status==='number';};NProgress.start=function(){if(!NProgress.status)NProgress.set(0);var work=function(){setTimeout(function(){if(!NProgress.status)return;NProgress.trickle();work();},Settings.trickleSpeed);};if(Settings.trickle)work();return this;};NProgress.done=function(force){if(!force&&!NProgress.status)return this;return NProgress.inc(0.3+0.5*Math.random()).set(1);};NProgress.inc=function(amount){var n=NProgress.status;if(!n){return NProgress.start();}else if(n>1){}else{if(typeof amount!=='number'){if(n>=0&&n<0.2){amount=0.1;}
+else if(n>=0.2&&n<0.5){amount=0.04;}
+else if(n>=0.5&&n<0.8){amount=0.02;}
+else if(n>=0.8&&n<0.99){amount=0.005;}
+else{amount=0;}}
+n=clamp(n+amount,0,0.994);return NProgress.set(n);}};NProgress.trickle=function(){return NProgress.inc();};(function(){var initial=0,current=0;NProgress.promise=function($promise){if(!$promise||$promise.state()==="resolved"){return this;}
+if(current===0){NProgress.start();}
+initial++;current++;$promise.always(function(){current--;if(current===0){initial=0;NProgress.done();}else{NProgress.set((initial-current)/initial);}});return this;};})();NProgress.render=function(fromStart){if(NProgress.isRendered())return document.getElementById('nprogress');addClass(document.documentElement,'nprogress-busy');var progress=document.createElement('div');progress.id='nprogress';progress.innerHTML=Settings.template;var bar=progress.querySelector(Settings.barSelector),perc=fromStart?'-100':toBarPerc(NProgress.status||0),parent=document.querySelector(Settings.parent),spinner;css(bar,{transition:'all 0 linear',transform:'translate3d('+perc+'%,0,0)'});if(!Settings.showSpinner){spinner=progress.querySelector(Settings.spinnerSelector);spinner&&removeElement(spinner);}
+if(parent!=document.body){addClass(parent,'nprogress-custom-parent');}
+parent.appendChild(progress);return progress;};NProgress.remove=function(){removeClass(document.documentElement,'nprogress-busy');removeClass(document.querySelector(Settings.parent),'nprogress-custom-parent');var progress=document.getElementById('nprogress');progress&&removeElement(progress);};NProgress.isRendered=function(){return!!document.getElementById('nprogress');};NProgress.getPositioningCSS=function(){var bodyStyle=document.body.style;var vendorPrefix=('WebkitTransform'in bodyStyle)?'Webkit':('MozTransform'in bodyStyle)?'Moz':('msTransform'in bodyStyle)?'ms':('OTransform'in bodyStyle)?'O':'';if(vendorPrefix+'Perspective'in bodyStyle){return'translate3d';}else if(vendorPrefix+'Transform'in bodyStyle){return'translate';}else{return'margin';}};function clamp(n,min,max){if(nmax)return max;return n;}
+function toBarPerc(n){return(-1+n)*100;}
+function barPositionCSS(n,speed,ease){var barCSS;if(Settings.positionUsing==='translate3d'){barCSS={transform:'translate3d('+toBarPerc(n)+'%,0,0)'};}else if(Settings.positionUsing==='translate'){barCSS={transform:'translate('+toBarPerc(n)+'%,0)'};}else{barCSS={'margin-left':toBarPerc(n)+'%'};}
+barCSS.transition='all '+speed+'ms '+ease;return barCSS;}
+var queue=(function(){var pending=[];function next(){var fn=pending.shift();if(fn){fn(next);}}
+return function(fn){pending.push(fn);if(pending.length==1)next();};})();var css=(function(){var cssPrefixes=['Webkit','O','Moz','ms'],cssProps={};function camelCase(string){return string.replace(/^-ms-/,'ms-').replace(/-([\da-z])/gi,function(match,letter){return letter.toUpperCase();});}
+function getVendorProp(name){var style=document.body.style;if(name in style)return name;var i=cssPrefixes.length,capName=name.charAt(0).toUpperCase()+name.slice(1),vendorName;while(i--){vendorName=cssPrefixes[i]+capName;if(vendorName in style)return vendorName;}
+return name;}
+function getStyleProp(name){name=camelCase(name);return cssProps[name]||(cssProps[name]=getVendorProp(name));}
+function applyCss(element,prop,value){prop=getStyleProp(prop);element.style[prop]=value;}
+return function(element,properties){var args=arguments,prop,value;if(args.length==2){for(prop in properties){value=properties[prop];if(value!==undefined&&properties.hasOwnProperty(prop))applyCss(element,prop,value);}}else{applyCss(element,args[1],args[2]);}}})();function hasClass(element,name){var list=typeof element=='string'?element:classList(element);return list.indexOf(' '+name+' ')>=0;}
+function addClass(element,name){var oldList=classList(element),newList=oldList+name;if(hasClass(oldList,name))return;element.className=newList.substring(1);}
+function removeClass(element,name){var oldList=classList(element),newList;if(!hasClass(element,name))return;newList=oldList.replace(' '+name+' ',' ');element.className=newList.substring(1,newList.length-1);}
+function classList(element){return(' '+(element&&element.className||'')+' ').replace(/\s+/gi,' ');}
+function removeElement(element){element&&element.parentNode&&element.parentNode.removeChild(element);}
+return NProgress;});;function do_reply(parentid){console.log(parentid);$("#id_parent_comment_id").val(parentid)
+$("#commentform").appendTo($("#div-comment-"+parentid));$("#reply-title").hide();$("#cancel_comment").show();}
+function cancel_reply(){$("#reply-title").show();$("#cancel_comment").hide();$("#id_parent_comment_id").val('')
+$("#commentform").appendTo($("#respond"));}
+NProgress.start();NProgress.set(0.4);var interval=setInterval(function(){NProgress.inc();},1000);$(document).ready(function(){NProgress.done();clearInterval(interval);});var rocket=$('#rocket');$(window).on('scroll',debounce(slideTopSet,300));function debounce(func,wait){var timeout;return function(){clearTimeout(timeout);timeout=setTimeout(func,wait);};}
+function slideTopSet(){var top=$(document).scrollTop();if(top>200){rocket.addClass('show');}else{rocket.removeClass('show');}}
+$(document).on('click','#rocket',function(event){rocket.addClass('move');$('body, html').animate({scrollTop:0},800);});$(document).on('animationEnd',function(){setTimeout(function(){rocket.removeClass('move');},400);});$(document).on('webkitAnimationEnd',function(){setTimeout(function(){rocket.removeClass('move');},400);});window.onload=function(){var replyLinks=document.querySelectorAll(".comment-reply-link");for(var i=0;i a, .page_item_has_children > a',function(e){var el=$(this).parent('li');if(!el.hasClass('focus')){e.preventDefault();el.toggleClass('focus');el.siblings('.focus').removeClass('focus');}});}})(jQuery);;(function(){'use strict';document.addEventListener('DOMContentLoaded',function(){initRecommendations();});function initRecommendations(){trackRecommendationClicks();lazyLoadRecommendations();}
+function trackRecommendationClicks(){const recommendationLinks=document.querySelectorAll('.recommendation-item a');recommendationLinks.forEach(function(link){link.addEventListener('click',function(e){const articleTitle=this.textContent.trim();const articleUrl=this.href;if(typeof gtag!=='undefined'){gtag('event','click',{'event_category':'recommendation','event_label':articleTitle,'value':1});}
+console.log('Recommendation clicked:',articleTitle,articleUrl);});});}
+function lazyLoadRecommendations(){const recommendationContainer=document.querySelector('.article-recommendations');if(!recommendationContainer){return;}
+const observer=new IntersectionObserver(function(entries){entries.forEach(function(entry){if(entry.isIntersecting){entry.target.classList.add('loaded');observer.unobserve(entry.target);}});},{threshold:0.1});const recommendationItems=document.querySelectorAll('.recommendation-item');recommendationItems.forEach(function(item){observer.observe(item);});}
+function addAnimations(){const recommendationItems=document.querySelectorAll('.recommendation-item');recommendationItems.forEach(function(item,index){item.style.opacity='0';item.style.transform='translateY(20px)';item.style.transition='opacity 0.5s ease, transform 0.5s ease';setTimeout(function(){item.style.opacity='1';item.style.transform='translateY(0)';},index*100);});}
+window.ArticleRecommendation={init:initRecommendations,track:trackRecommendationClicks,animate:addAnimations};})();;
\ No newline at end of file
diff --git a/src/comments/__pycache__/__init__.cpython-311.pyc b/src/comments/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..b1e41f4
Binary files /dev/null and b/src/comments/__pycache__/__init__.cpython-311.pyc differ
diff --git a/src/comments/__pycache__/admin.cpython-311.pyc b/src/comments/__pycache__/admin.cpython-311.pyc
new file mode 100644
index 0000000..2726138
Binary files /dev/null and b/src/comments/__pycache__/admin.cpython-311.pyc differ
diff --git a/src/comments/__pycache__/apps.cpython-311.pyc b/src/comments/__pycache__/apps.cpython-311.pyc
new file mode 100644
index 0000000..84938a9
Binary files /dev/null and b/src/comments/__pycache__/apps.cpython-311.pyc differ
diff --git a/src/comments/__pycache__/forms.cpython-311.pyc b/src/comments/__pycache__/forms.cpython-311.pyc
new file mode 100644
index 0000000..24b4e2d
Binary files /dev/null and b/src/comments/__pycache__/forms.cpython-311.pyc differ
diff --git a/src/comments/__pycache__/models.cpython-311.pyc b/src/comments/__pycache__/models.cpython-311.pyc
new file mode 100644
index 0000000..c9b5fa4
Binary files /dev/null and b/src/comments/__pycache__/models.cpython-311.pyc differ
diff --git a/src/comments/__pycache__/urls.cpython-311.pyc b/src/comments/__pycache__/urls.cpython-311.pyc
new file mode 100644
index 0000000..d956f07
Binary files /dev/null and b/src/comments/__pycache__/urls.cpython-311.pyc differ
diff --git a/src/comments/__pycache__/utils.cpython-311.pyc b/src/comments/__pycache__/utils.cpython-311.pyc
new file mode 100644
index 0000000..b3686a7
Binary files /dev/null and b/src/comments/__pycache__/utils.cpython-311.pyc differ
diff --git a/src/comments/__pycache__/views.cpython-311.pyc b/src/comments/__pycache__/views.cpython-311.pyc
new file mode 100644
index 0000000..09fdbc9
Binary files /dev/null and b/src/comments/__pycache__/views.cpython-311.pyc differ
diff --git a/src/comments/admin.py b/src/comments/admin.py
index 5622781..dbde14f 100644
--- a/src/comments/admin.py
+++ b/src/comments/admin.py
@@ -26,9 +26,11 @@ class CommentAdmin(admin.ModelAdmin):
'is_enable',
'creation_time')
list_display_links = ('id', 'body', 'is_enable')
- list_filter = ('is_enable', 'author', 'article',)
+ list_filter = ('is_enable',)
exclude = ('creation_time', 'last_modify_time')
actions = [disable_commentstatus, enable_commentstatus]
+ raw_id_fields = ('author', 'article')
+ search_fields = ('body',)
def link_to_userinfo(self, obj):
info = (obj.author._meta.app_label, obj.author._meta.model_name)
@@ -38,7 +40,7 @@ class CommentAdmin(admin.ModelAdmin):
(link, obj.author.nickname if obj.author.nickname else obj.author.email))
def link_to_article(self, obj):
- info = (obj.author._meta.app_label, obj.author._meta.model_name)
+ info = (obj.article._meta.app_label, obj.article._meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,))
return format_html(
u'%s ' % (link, obj.article.title))
diff --git a/src/comments/migrations/__pycache__/0001_initial.cpython-311.pyc b/src/comments/migrations/__pycache__/0001_initial.cpython-311.pyc
new file mode 100644
index 0000000..69906d3
Binary files /dev/null and b/src/comments/migrations/__pycache__/0001_initial.cpython-311.pyc differ
diff --git a/src/comments/migrations/__pycache__/0002_alter_comment_is_enable.cpython-311.pyc b/src/comments/migrations/__pycache__/0002_alter_comment_is_enable.cpython-311.pyc
new file mode 100644
index 0000000..f9ac124
Binary files /dev/null and b/src/comments/migrations/__pycache__/0002_alter_comment_is_enable.cpython-311.pyc differ
diff --git a/src/comments/migrations/__pycache__/0003_alter_comment_options_remove_comment_created_time_and_more.cpython-311.pyc b/src/comments/migrations/__pycache__/0003_alter_comment_options_remove_comment_created_time_and_more.cpython-311.pyc
new file mode 100644
index 0000000..42046f5
Binary files /dev/null and b/src/comments/migrations/__pycache__/0003_alter_comment_options_remove_comment_created_time_and_more.cpython-311.pyc differ
diff --git a/src/comments/migrations/__pycache__/__init__.cpython-311.pyc b/src/comments/migrations/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..2cbcb26
Binary files /dev/null and b/src/comments/migrations/__pycache__/__init__.cpython-311.pyc differ
diff --git a/src/comments/templatetags/__pycache__/__init__.cpython-311.pyc b/src/comments/templatetags/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..2674528
Binary files /dev/null and b/src/comments/templatetags/__pycache__/__init__.cpython-311.pyc differ
diff --git a/src/comments/templatetags/__pycache__/comments_tags.cpython-311.pyc b/src/comments/templatetags/__pycache__/comments_tags.cpython-311.pyc
new file mode 100644
index 0000000..0c0e74f
Binary files /dev/null and b/src/comments/templatetags/__pycache__/comments_tags.cpython-311.pyc differ
diff --git a/src/deploy/k8s/deployment.yaml b/src/deploy/k8s/deployment.yaml
index 414fdcc..b50c411 100644
--- a/src/deploy/k8s/deployment.yaml
+++ b/src/deploy/k8s/deployment.yaml
@@ -26,13 +26,13 @@ spec:
name: djangoblog-env
readinessProbe:
httpGet:
- path: /
+ path: /health/
port: 8000
initialDelaySeconds: 10
periodSeconds: 30
livenessProbe:
httpGet:
- path: /
+ path: /health/
port: 8000
initialDelaySeconds: 10
periodSeconds: 30
diff --git a/src/djangoblog/__init__.py b/src/djangoblog/__init__.py
index e69de29..1e205f4 100644
--- a/src/djangoblog/__init__.py
+++ b/src/djangoblog/__init__.py
@@ -0,0 +1 @@
+default_app_config = 'djangoblog.apps.DjangoblogAppConfig'
diff --git a/src/djangoblog/__pycache__/__init__.cpython-311.pyc b/src/djangoblog/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..4bbb66d
Binary files /dev/null and b/src/djangoblog/__pycache__/__init__.cpython-311.pyc differ
diff --git a/src/djangoblog/__pycache__/admin_site.cpython-311.pyc b/src/djangoblog/__pycache__/admin_site.cpython-311.pyc
new file mode 100644
index 0000000..fe46b58
Binary files /dev/null and b/src/djangoblog/__pycache__/admin_site.cpython-311.pyc differ
diff --git a/src/djangoblog/__pycache__/apps.cpython-311.pyc b/src/djangoblog/__pycache__/apps.cpython-311.pyc
new file mode 100644
index 0000000..5625e14
Binary files /dev/null and b/src/djangoblog/__pycache__/apps.cpython-311.pyc differ
diff --git a/src/djangoblog/__pycache__/blog_signals.cpython-311.pyc b/src/djangoblog/__pycache__/blog_signals.cpython-311.pyc
new file mode 100644
index 0000000..15a83c9
Binary files /dev/null and b/src/djangoblog/__pycache__/blog_signals.cpython-311.pyc differ
diff --git a/src/djangoblog/__pycache__/elasticsearch_backend.cpython-311.pyc b/src/djangoblog/__pycache__/elasticsearch_backend.cpython-311.pyc
new file mode 100644
index 0000000..280c86a
Binary files /dev/null and b/src/djangoblog/__pycache__/elasticsearch_backend.cpython-311.pyc differ
diff --git a/src/djangoblog/__pycache__/feeds.cpython-311.pyc b/src/djangoblog/__pycache__/feeds.cpython-311.pyc
new file mode 100644
index 0000000..cd2a5e3
Binary files /dev/null and b/src/djangoblog/__pycache__/feeds.cpython-311.pyc differ
diff --git a/src/djangoblog/__pycache__/logentryadmin.cpython-311.pyc b/src/djangoblog/__pycache__/logentryadmin.cpython-311.pyc
new file mode 100644
index 0000000..66b789c
Binary files /dev/null and b/src/djangoblog/__pycache__/logentryadmin.cpython-311.pyc differ
diff --git a/src/djangoblog/__pycache__/settings.cpython-311.pyc b/src/djangoblog/__pycache__/settings.cpython-311.pyc
new file mode 100644
index 0000000..ff9af05
Binary files /dev/null and b/src/djangoblog/__pycache__/settings.cpython-311.pyc differ
diff --git a/src/djangoblog/__pycache__/sitemap.cpython-311.pyc b/src/djangoblog/__pycache__/sitemap.cpython-311.pyc
new file mode 100644
index 0000000..6f17766
Binary files /dev/null and b/src/djangoblog/__pycache__/sitemap.cpython-311.pyc differ
diff --git a/src/djangoblog/__pycache__/spider_notify.cpython-311.pyc b/src/djangoblog/__pycache__/spider_notify.cpython-311.pyc
new file mode 100644
index 0000000..440f302
Binary files /dev/null and b/src/djangoblog/__pycache__/spider_notify.cpython-311.pyc differ
diff --git a/src/djangoblog/__pycache__/urls.cpython-311.pyc b/src/djangoblog/__pycache__/urls.cpython-311.pyc
new file mode 100644
index 0000000..2a9007d
Binary files /dev/null and b/src/djangoblog/__pycache__/urls.cpython-311.pyc differ
diff --git a/src/djangoblog/__pycache__/utils.cpython-311.pyc b/src/djangoblog/__pycache__/utils.cpython-311.pyc
new file mode 100644
index 0000000..52d6751
Binary files /dev/null and b/src/djangoblog/__pycache__/utils.cpython-311.pyc differ
diff --git a/src/djangoblog/__pycache__/whoosh_cn_backend.cpython-311.pyc b/src/djangoblog/__pycache__/whoosh_cn_backend.cpython-311.pyc
new file mode 100644
index 0000000..0f23af0
Binary files /dev/null and b/src/djangoblog/__pycache__/whoosh_cn_backend.cpython-311.pyc differ
diff --git a/src/djangoblog/__pycache__/wsgi.cpython-311.pyc b/src/djangoblog/__pycache__/wsgi.cpython-311.pyc
new file mode 100644
index 0000000..8d01b7f
Binary files /dev/null and b/src/djangoblog/__pycache__/wsgi.cpython-311.pyc differ
diff --git a/src/djangoblog/plugin_manage/__pycache__/base_plugin.cpython-311.pyc b/src/djangoblog/plugin_manage/__pycache__/base_plugin.cpython-311.pyc
new file mode 100644
index 0000000..b8e07a6
Binary files /dev/null and b/src/djangoblog/plugin_manage/__pycache__/base_plugin.cpython-311.pyc differ
diff --git a/src/djangoblog/plugin_manage/__pycache__/hook_constants.cpython-311.pyc b/src/djangoblog/plugin_manage/__pycache__/hook_constants.cpython-311.pyc
new file mode 100644
index 0000000..16df513
Binary files /dev/null and b/src/djangoblog/plugin_manage/__pycache__/hook_constants.cpython-311.pyc differ
diff --git a/src/djangoblog/plugin_manage/__pycache__/hooks.cpython-311.pyc b/src/djangoblog/plugin_manage/__pycache__/hooks.cpython-311.pyc
new file mode 100644
index 0000000..8c8b48d
Binary files /dev/null and b/src/djangoblog/plugin_manage/__pycache__/hooks.cpython-311.pyc differ
diff --git a/src/djangoblog/plugin_manage/__pycache__/loader.cpython-311.pyc b/src/djangoblog/plugin_manage/__pycache__/loader.cpython-311.pyc
new file mode 100644
index 0000000..ed815f2
Binary files /dev/null and b/src/djangoblog/plugin_manage/__pycache__/loader.cpython-311.pyc differ
diff --git a/src/djangoblog/plugin_manage/base_plugin.py b/src/djangoblog/plugin_manage/base_plugin.py
index 2b4be5c..df1ce0b 100644
--- a/src/djangoblog/plugin_manage/base_plugin.py
+++ b/src/djangoblog/plugin_manage/base_plugin.py
@@ -1,4 +1,8 @@
import logging
+from pathlib import Path
+
+from django.template import TemplateDoesNotExist
+from django.template.loader import render_to_string
logger = logging.getLogger(__name__)
@@ -8,13 +12,34 @@ class BasePlugin:
PLUGIN_NAME = None
PLUGIN_DESCRIPTION = None
PLUGIN_VERSION = None
+ PLUGIN_AUTHOR = None
+
+ # 插件配置
+ SUPPORTED_POSITIONS = [] # 支持的显示位置
+ DEFAULT_PRIORITY = 100 # 默认优先级(数字越小优先级越高)
+ POSITION_PRIORITIES = {} # 各位置的优先级 {'sidebar': 50, 'article_bottom': 80}
def __init__(self):
if not all([self.PLUGIN_NAME, self.PLUGIN_DESCRIPTION, self.PLUGIN_VERSION]):
raise ValueError("Plugin metadata (PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_VERSION) must be defined.")
+
+ # 设置插件路径
+ self.plugin_dir = self._get_plugin_directory()
+ self.plugin_slug = self._get_plugin_slug()
+
self.init_plugin()
self.register_hooks()
+ def _get_plugin_directory(self):
+ """获取插件目录路径"""
+ import inspect
+ plugin_file = inspect.getfile(self.__class__)
+ return Path(plugin_file).parent
+
+ def _get_plugin_slug(self):
+ """获取插件标识符(目录名)"""
+ return self.plugin_dir.name
+
def init_plugin(self):
"""
插件初始化逻辑
@@ -29,6 +54,129 @@ class BasePlugin:
"""
pass
+ # === 位置渲染系统 ===
+ def render_position_widget(self, position, context, **kwargs):
+ """
+ 根据位置渲染插件组件
+
+ Args:
+ position: 位置标识
+ context: 模板上下文
+ **kwargs: 额外参数
+
+ Returns:
+ dict: {'html': 'HTML内容', 'priority': 优先级} 或 None
+ """
+ if position not in self.SUPPORTED_POSITIONS:
+ return None
+
+ # 检查条件显示
+ if not self.should_display(position, context, **kwargs):
+ return None
+
+ # 调用具体的位置渲染方法
+ method_name = f'render_{position}_widget'
+ if hasattr(self, method_name):
+ html = getattr(self, method_name)(context, **kwargs)
+ if html:
+ priority = self.POSITION_PRIORITIES.get(position, self.DEFAULT_PRIORITY)
+ return {
+ 'html': html,
+ 'priority': priority,
+ 'plugin_name': self.PLUGIN_NAME
+ }
+
+ return None
+
+ def should_display(self, position, context, **kwargs):
+ """
+ 判断插件是否应该在指定位置显示
+ 子类可重写此方法实现条件显示逻辑
+
+ Args:
+ position: 位置标识
+ context: 模板上下文
+ **kwargs: 额外参数
+
+ Returns:
+ bool: 是否显示
+ """
+ return True
+
+ # === 各位置渲染方法 - 子类重写 ===
+ def render_sidebar_widget(self, context, **kwargs):
+ """渲染侧边栏组件"""
+ return None
+
+ def render_article_bottom_widget(self, context, **kwargs):
+ """渲染文章底部组件"""
+ return None
+
+ def render_article_top_widget(self, context, **kwargs):
+ """渲染文章顶部组件"""
+ return None
+
+ def render_header_widget(self, context, **kwargs):
+ """渲染页头组件"""
+ return None
+
+ def render_footer_widget(self, context, **kwargs):
+ """渲染页脚组件"""
+ return None
+
+ def render_comment_before_widget(self, context, **kwargs):
+ """渲染评论前组件"""
+ return None
+
+ def render_comment_after_widget(self, context, **kwargs):
+ """渲染评论后组件"""
+ return None
+
+ # === 模板系统 ===
+ def render_template(self, template_name, context=None):
+ """
+ 渲染插件模板
+
+ Args:
+ template_name: 模板文件名
+ context: 模板上下文
+
+ Returns:
+ HTML字符串
+ """
+ if context is None:
+ context = {}
+
+ template_path = f"plugins/{self.plugin_slug}/{template_name}"
+
+ try:
+ return render_to_string(template_path, context)
+ except TemplateDoesNotExist:
+ logger.warning(f"Plugin template not found: {template_path}")
+ return ""
+
+ # === 静态资源系统 ===
+ def get_static_url(self, static_file):
+ """获取插件静态文件URL"""
+ from django.templatetags.static import static
+ return static(f"{self.plugin_slug}/static/{self.plugin_slug}/{static_file}")
+
+ def get_css_files(self):
+ """获取插件CSS文件列表"""
+ return []
+
+ def get_js_files(self):
+ """获取插件JavaScript文件列表"""
+ return []
+
+ def get_head_html(self, context=None):
+ """获取需要插入到中的HTML内容"""
+ return ""
+
+ def get_body_html(self, context=None):
+ """获取需要插入到底部的HTML内容"""
+ return ""
+
def get_plugin_info(self):
"""
获取插件信息
@@ -37,5 +185,10 @@ class BasePlugin:
return {
'name': self.PLUGIN_NAME,
'description': self.PLUGIN_DESCRIPTION,
- 'version': self.PLUGIN_VERSION
+ 'version': self.PLUGIN_VERSION,
+ 'author': self.PLUGIN_AUTHOR,
+ 'slug': self.plugin_slug,
+ 'directory': str(self.plugin_dir),
+ 'supported_positions': self.SUPPORTED_POSITIONS,
+ 'priorities': self.POSITION_PRIORITIES
}
diff --git a/src/djangoblog/plugin_manage/hook_constants.py b/src/djangoblog/plugin_manage/hook_constants.py
index 6685b7c..8ed4e89 100644
--- a/src/djangoblog/plugin_manage/hook_constants.py
+++ b/src/djangoblog/plugin_manage/hook_constants.py
@@ -5,3 +5,18 @@ ARTICLE_DELETE = 'article_delete'
ARTICLE_CONTENT_HOOK_NAME = "the_content"
+# 位置钩子常量
+POSITION_HOOKS = {
+ 'article_top': 'article_top_widgets',
+ 'article_bottom': 'article_bottom_widgets',
+ 'sidebar': 'sidebar_widgets',
+ 'header': 'header_widgets',
+ 'footer': 'footer_widgets',
+ 'comment_before': 'comment_before_widgets',
+ 'comment_after': 'comment_after_widgets',
+}
+
+# 资源注入钩子
+HEAD_RESOURCES_HOOK = 'head_resources'
+BODY_RESOURCES_HOOK = 'body_resources'
+
diff --git a/src/djangoblog/plugin_manage/loader.py b/src/djangoblog/plugin_manage/loader.py
index 12e824b..ee750d0 100644
--- a/src/djangoblog/plugin_manage/loader.py
+++ b/src/djangoblog/plugin_manage/loader.py
@@ -4,16 +4,61 @@ from django.conf import settings
logger = logging.getLogger(__name__)
+# 全局插件注册表
+_loaded_plugins = []
+
def load_plugins():
"""
Dynamically loads and initializes plugins from the 'plugins' directory.
This function is intended to be called when the Django app registry is ready.
"""
+ global _loaded_plugins
+ _loaded_plugins = []
+
for plugin_name in settings.ACTIVE_PLUGINS:
plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name)
if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')):
try:
- __import__(f'plugins.{plugin_name}.plugin')
- logger.info(f"Successfully loaded plugin: {plugin_name}")
+ # 导入插件模块
+ plugin_module = __import__(f'plugins.{plugin_name}.plugin', fromlist=['plugin'])
+
+ # 获取插件实例
+ if hasattr(plugin_module, 'plugin'):
+ plugin_instance = plugin_module.plugin
+ _loaded_plugins.append(plugin_instance)
+ logger.info(f"Successfully loaded plugin: {plugin_name} - {plugin_instance.PLUGIN_NAME}")
+ else:
+ logger.warning(f"Plugin {plugin_name} does not have 'plugin' instance")
+
except ImportError as e:
- logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)
\ No newline at end of file
+ logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)
+ except AttributeError as e:
+ logger.error(f"Failed to get plugin instance: {plugin_name}", exc_info=e)
+ except Exception as e:
+ logger.error(f"Unexpected error loading plugin: {plugin_name}", exc_info=e)
+
+def get_loaded_plugins():
+ """获取所有已加载的插件"""
+ return _loaded_plugins
+
+def get_plugin_by_name(plugin_name):
+ """根据名称获取插件"""
+ for plugin in _loaded_plugins:
+ if plugin.plugin_slug == plugin_name:
+ return plugin
+ return None
+
+def get_plugin_by_slug(plugin_slug):
+ """根据slug获取插件"""
+ for plugin in _loaded_plugins:
+ if plugin.plugin_slug == plugin_slug:
+ return plugin
+ return None
+
+def get_plugins_info():
+ """获取所有插件的信息"""
+ return [plugin.get_plugin_info() for plugin in _loaded_plugins]
+
+def get_plugins_by_position(position):
+ """获取支持指定位置的插件"""
+ return [plugin for plugin in _loaded_plugins if position in plugin.SUPPORTED_POSITIONS]
\ No newline at end of file
diff --git a/src/djangoblog/settings.py b/src/djangoblog/settings.py
index 3269a34..fe873ad 100644
--- a/src/djangoblog/settings.py
+++ b/src/djangoblog/settings.py
@@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/1.10/ref/settings/
"""
import os
import sys
+from pathlib import Path
from django.utils.translation import gettext_lazy as _
@@ -20,9 +21,8 @@ def env_to_bool(env, default):
return default if str_val is None else str_val == 'True'
-# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
-
-BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
@@ -60,7 +60,8 @@ INSTALLED_APPS = [
'oauth',
'servermanager',
'owntracks',
- 'compressor'
+ 'compressor',
+ 'djangoblog'
]
MIDDLEWARE = [
@@ -106,15 +107,15 @@ WSGI_APPLICATION = 'djangoblog.wsgi.application'
DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.mysql',
- 'NAME': 'djangoblog',
- 'USER': 'root',
- 'PASSWORD': 'password',
- 'HOST': '127.0.0.1',
- 'PORT': 3306,
- }
-}
+ 'default': {
+ 'ENGINE': 'django.db.backends.mysql',
+ 'NAME': 'djangoblog',
+ 'USER': 'DjangoBlog',
+ 'PASSWORD': 'DjangoBlog',
+ 'HOST': '127.0.0.1',
+ 'PORT': 3306,
+ }
+ }
# Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
@@ -151,7 +152,7 @@ USE_I18N = True
USE_L10N = True
-USE_TZ = True
+USE_TZ = False
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/
@@ -174,6 +175,11 @@ STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic')
STATIC_URL = '/static/'
STATICFILES = os.path.join(BASE_DIR, 'static')
+# 添加插件静态文件目录
+STATICFILES_DIRS = [
+ os.path.join(BASE_DIR, 'plugins'), # 让Django能找到插件的静态文件
+]
+
AUTH_USER_MODEL = 'accounts.BlogUser'
LOGIN_URL = '/login/'
@@ -298,23 +304,76 @@ STATICFILES_FINDERS = (
'compressor.finders.CompressorFinder',
)
COMPRESS_ENABLED = True
-# COMPRESS_OFFLINE = True
+# 根据环境变量决定是否启用离线压缩
+COMPRESS_OFFLINE = os.environ.get('COMPRESS_OFFLINE', 'False').lower() == 'true'
+
+# 压缩输出目录
+COMPRESS_OUTPUT_DIR = 'compressed'
+# 压缩文件名模板 - 包含哈希值用于缓存破坏
+COMPRESS_CSS_HASHING_METHOD = 'mtime'
+COMPRESS_JS_HASHING_METHOD = 'mtime'
+# 高级CSS压缩过滤器
COMPRESS_CSS_FILTERS = [
- # creates absolute urls from relative ones
+ # 创建绝对URL
'compressor.filters.css_default.CssAbsoluteFilter',
- # css minimizer
- 'compressor.filters.cssmin.CSSMinFilter'
+ # CSS压缩器 - 高压缩等级
+ 'compressor.filters.cssmin.CSSCompressorFilter',
]
+
+# 高级JS压缩过滤器
COMPRESS_JS_FILTERS = [
- 'compressor.filters.jsmin.JSMinFilter'
+ # JS压缩器 - 高压缩等级
+ 'compressor.filters.jsmin.SlimItFilter',
]
+# 压缩缓存配置
+COMPRESS_CACHE_BACKEND = 'default'
+COMPRESS_CACHE_KEY_FUNCTION = 'compressor.cache.simple_cachekey'
+
+# 预压缩配置
+COMPRESS_PRECOMPILERS = (
+ # 支持SCSS/SASS
+ ('text/x-scss', 'django_libsass.SassCompiler'),
+ ('text/x-sass', 'django_libsass.SassCompiler'),
+)
+
+# 压缩性能优化
+COMPRESS_MINT_DELAY = 30 # 压缩延迟(秒)
+COMPRESS_MTIME_DELAY = 10 # 修改时间检查延迟
+COMPRESS_REBUILD_TIMEOUT = 2592000 # 重建超时(30天)
+
+# 压缩等级配置
+COMPRESS_CSS_COMPRESSOR = 'compressor.css.CssCompressor'
+COMPRESS_JS_COMPRESSOR = 'compressor.js.JsCompressor'
+
+# 静态文件缓存配置
+STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
+
+# 浏览器缓存配置(通过中间件或服务器配置)
+COMPRESS_URL = STATIC_URL
+COMPRESS_ROOT = STATIC_ROOT
+
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
MEDIA_URL = '/media/'
X_FRAME_OPTIONS = 'SAMEORIGIN'
+# 安全头部配置 - 防XSS和其他攻击
+SECURE_BROWSER_XSS_FILTER = True
+SECURE_CONTENT_TYPE_NOSNIFF = True
+SECURE_REFERRER_POLICY = 'strict-origin-when-cross-origin'
+
+# 内容安全策略 (CSP) - 防XSS攻击
+CSP_DEFAULT_SRC = ["'self'"]
+CSP_SCRIPT_SRC = ["'self'", "'unsafe-inline'", "cdn.mathjax.org", "*.googleapis.com"]
+CSP_STYLE_SRC = ["'self'", "'unsafe-inline'", "*.googleapis.com", "*.gstatic.com"]
+CSP_IMG_SRC = ["'self'", "data:", "*.lylinux.net", "*.gravatar.com", "*.githubusercontent.com"]
+CSP_FONT_SRC = ["'self'", "*.googleapis.com", "*.gstatic.com"]
+CSP_CONNECT_SRC = ["'self'"]
+CSP_FRAME_SRC = ["'none'"]
+CSP_OBJECT_SRC = ["'none'"]
+
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
if os.environ.get('DJANGO_ELASTICSEARCH_HOST'):
@@ -328,3 +387,16 @@ if os.environ.get('DJANGO_ELASTICSEARCH_HOST'):
'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine',
},
}
+
+# Plugin System
+PLUGINS_DIR = BASE_DIR / 'plugins'
+ACTIVE_PLUGINS = [
+ 'article_copyright',
+ 'reading_time',
+ 'external_links',
+ 'view_count',
+ 'seo_optimizer',
+ 'image_lazy_loading',
+ 'article_recommendation',
+]
+
diff --git a/src/djangoblog/spider_notify.py b/src/djangoblog/spider_notify.py
index f77c09b..7b909e9 100644
--- a/src/djangoblog/spider_notify.py
+++ b/src/djangoblog/spider_notify.py
@@ -2,7 +2,6 @@ import logging
import requests
from django.conf import settings
-from django.contrib.sitemaps import ping_google
logger = logging.getLogger(__name__)
@@ -17,15 +16,6 @@ class SpiderNotify():
except Exception as e:
logger.error(e)
- @staticmethod
- def __google_notify():
- try:
- ping_google('/sitemap.xml')
- except Exception as e:
- logger.error(e)
-
@staticmethod
def notify(url):
-
SpiderNotify.baidu_notify(url)
- SpiderNotify.__google_notify()
diff --git a/src/djangoblog/urls.py b/src/djangoblog/urls.py
index 4aae58a..6a9e1de 100644
--- a/src/djangoblog/urls.py
+++ b/src/djangoblog/urls.py
@@ -20,6 +20,8 @@ from django.contrib.sitemaps.views import sitemap
from django.urls import path, include
from django.urls import re_path
from haystack.views import search_view_factory
+from django.http import JsonResponse
+import time
from blog.views import EsSearchView
from djangoblog.admin_site import admin_site
@@ -40,8 +42,20 @@ handler404 = 'blog.views.page_not_found_view'
handler500 = 'blog.views.server_error_view'
handle403 = 'blog.views.permission_denied_view'
+
+def health_check(request):
+ """
+ 健康检查接口
+ 简单返回服务健康状态
+ """
+ return JsonResponse({
+ 'status': 'healthy',
+ 'timestamp': time.time()
+ })
+
urlpatterns = [
path('i18n/', include('django.conf.urls.i18n')),
+ path('health/', health_check, name='health_check'),
]
urlpatterns += i18n_patterns(
re_path(r'^admin/', admin_site.urls),
diff --git a/src/djangoblog/utils.py b/src/djangoblog/utils.py
index 57f63dc..91d2b91 100644
--- a/src/djangoblog/utils.py
+++ b/src/djangoblog/utils.py
@@ -224,9 +224,49 @@ def get_resource_url():
ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1',
- 'h2', 'p']
-ALLOWED_ATTRIBUTES = {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']}
-
+ 'h2', 'p', 'span', 'div']
+
+# 安全的class值白名单 - 只允许代码高亮相关的class
+ALLOWED_CLASSES = [
+ 'codehilite', 'highlight', 'hll', 'c', 'err', 'k', 'l', 'n', 'o', 'p', 'cm', 'cp', 'c1', 'cs',
+ 'gd', 'ge', 'gr', 'gh', 'gi', 'go', 'gp', 'gs', 'gu', 'gt', 'kc', 'kd', 'kn', 'kp', 'kr', 'kt',
+ 'ld', 'm', 'mf', 'mh', 'mi', 'mo', 'na', 'nb', 'nc', 'no', 'nd', 'ni', 'ne', 'nf', 'nl', 'nn',
+ 'nt', 'nv', 'ow', 'w', 'mb', 'mh', 'mi', 'mo', 'sb', 'sc', 'sd', 'se', 'sh', 'si', 'sx', 's2',
+ 's1', 'ss', 'bp', 'vc', 'vg', 'vi', 'il'
+]
+
+def class_filter(tag, name, value):
+ """自定义class属性过滤器"""
+ if name == 'class':
+ # 只允许预定义的安全class值
+ allowed_classes = [cls for cls in value.split() if cls in ALLOWED_CLASSES]
+ return ' '.join(allowed_classes) if allowed_classes else False
+ return value
+
+# 安全的属性白名单
+ALLOWED_ATTRIBUTES = {
+ 'a': ['href', 'title'],
+ 'abbr': ['title'],
+ 'acronym': ['title'],
+ 'span': class_filter,
+ 'div': class_filter,
+ 'pre': class_filter,
+ 'code': class_filter
+}
+
+# 安全的协议白名单 - 防止javascript:等危险协议
+ALLOWED_PROTOCOLS = ['http', 'https', 'mailto']
def sanitize_html(html):
- return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)
+ """
+ 安全的HTML清理函数
+ 使用bleach库进行白名单过滤,防止XSS攻击
+ """
+ return bleach.clean(
+ html,
+ tags=ALLOWED_TAGS,
+ attributes=ALLOWED_ATTRIBUTES,
+ protocols=ALLOWED_PROTOCOLS, # 限制允许的协议
+ strip=True, # 移除不允许的标签而不是转义
+ strip_comments=True # 移除HTML注释
+ )
diff --git a/src/djangoblog/whoosh_cn_backend.py b/src/djangoblog/whoosh_cn_backend.py
index c285cc2..04e3f7f 100644
--- a/src/djangoblog/whoosh_cn_backend.py
+++ b/src/djangoblog/whoosh_cn_backend.py
@@ -12,7 +12,7 @@ import warnings
import six
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
-from django.utils.datetime_safe import datetime
+from datetime import datetime
from django.utils.encoding import force_str
from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, EmptyResults, log_query
from haystack.constants import DJANGO_CT, DJANGO_ID, ID
diff --git a/src/docker-compose.es.yml b/src/docker-compose.es.yml
deleted file mode 100644
index 83e35ff..0000000
--- a/src/docker-compose.es.yml
+++ /dev/null
@@ -1,48 +0,0 @@
-version: '3'
-
-services:
- es:
- image: liangliangyy/elasticsearch-analysis-ik:8.6.1
- container_name: es
- restart: always
- environment:
- - discovery.type=single-node
- - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
- ports:
- - 9200:9200
- volumes:
- - ./bin/datas/es/:/usr/share/elasticsearch/data/
-
- kibana:
- image: kibana:8.6.1
- restart: always
- container_name: kibana
- ports:
- - 5601:5601
- environment:
- - ELASTICSEARCH_HOSTS=http://es:9200
-
- djangoblog:
- build: .
- restart: always
- command: bash -c 'sh /code/djangoblog/bin/docker_start.sh'
- ports:
- - "8000:8000"
- volumes:
- - ./collectedstatic:/code/djangoblog/collectedstatic
- - ./uploads:/code/djangoblog/uploads
- environment:
- - DJANGO_MYSQL_DATABASE=djangoblog
- - DJANGO_MYSQL_USER=root
- - DJANGO_MYSQL_PASSWORD=DjAnGoBlOg!2!Q@W#E
- - DJANGO_MYSQL_HOST=db
- - DJANGO_MYSQL_PORT=3306
- - DJANGO_MEMCACHED_LOCATION=memcached:11211
- - DJANGO_ELASTICSEARCH_HOST=es:9200
- links:
- - db
- - memcached
- depends_on:
- - db
- container_name: djangoblog
-
diff --git a/src/docker-compose.yml b/src/docker-compose.yml
deleted file mode 100644
index 2735c32..0000000
--- a/src/docker-compose.yml
+++ /dev/null
@@ -1,59 +0,0 @@
-version: '3'
-
-services:
- db:
- image: mysql:latest
- restart: always
- environment:
- - MYSQL_DATABASE=djangoblog
- - MYSQL_ROOT_PASSWORD=DjAnGoBlOg!2!Q@W#E
- ports:
- - 3306:3306
- volumes:
- - ./bin/datas/mysql/:/var/lib/mysql
- depends_on:
- - redis
- container_name: db
-
- djangoblog:
- build: .
- restart: always
- command: bash -c 'sh /code/djangoblog/bin/docker_start.sh'
- ports:
- - "8000:8000"
- volumes:
- - ./collectedstatic:/code/djangoblog/collectedstatic
- - ./logs:/code/djangoblog/logs
- - ./uploads:/code/djangoblog/uploads
- environment:
- - DJANGO_MYSQL_DATABASE=djangoblog
- - DJANGO_MYSQL_USER=root
- - DJANGO_MYSQL_PASSWORD=DjAnGoBlOg!2!Q@W#E
- - DJANGO_MYSQL_HOST=db
- - DJANGO_MYSQL_PORT=3306
- - DJANGO_REDIS_URL=redis:6379
- links:
- - db
- - redis
- depends_on:
- - db
- container_name: djangoblog
- nginx:
- restart: always
- image: nginx:latest
- ports:
- - "80:80"
- - "443:443"
- volumes:
- - ./bin/nginx.conf:/etc/nginx/nginx.conf
- - ./collectedstatic:/code/djangoblog/collectedstatic
- links:
- - djangoblog:djangoblog
- container_name: nginx
-
- redis:
- restart: always
- image: redis:latest
- container_name: redis
- ports:
- - "6379:6379"
diff --git a/src/docs/README-en.md b/src/docs/README-en.md
index 4b72655..37ea069 100644
--- a/src/docs/README-en.md
+++ b/src/docs/README-en.md
@@ -1,122 +1,158 @@
# DjangoBlog
-🌍
-*[English](README-en.md) ∙ [简体中文](README.md)*
+
+
+
+
+
+
+
+
+ A powerful, elegant, and modern blog system.
+
+ English • 简体中文
+
-A blog system based on `python3.8` and `Django4.0`.
-
-
-[](https://github.com/liangliangyy/DjangoBlog/actions/workflows/django.yml) [](https://github.com/liangliangyy/DjangoBlog/actions/workflows/codeql-analysis.yml) [](https://codecov.io/gh/liangliangyy/DjangoBlog) []()
-
-
-## Main Features:
-- Articles, Pages, Categories, Tags(Add, Delete, Edit), edc. Articles and pages support `Markdown` and highlighting.
-- Articles support full-text search.
-- Complete comment feature, include posting reply comment and email notification. `Markdown` supporting.
-- Sidebar feature: new articles, most readings, tags, etc.
-- OAuth Login supported, including Google, GitHub, Facebook, Weibo, QQ.
-- `Memcache` supported, with cache auto refresh.
-- Simple SEO Features, notify Google and Baidu when there was a new article or other things.
-- Simple picture bed feature integrated.
-- `django-compressor` integrated, auto-compressed `css`, `js`.
-- Website exception email notification. When there is an unhandle exception, system will send an email notification.
-- Wechat official account feature integrated. Now, you can use wechat official account to manage your VPS.
-
-## Installation:
-Change MySQL client from `pymysql` to `mysqlclient`, more details please reference [pypi](https://pypi.org/project/mysqlclient/) , checkout preperation before installation.
-
-Install via pip: `pip install -Ur requirements.txt`
-
-If you do NOT have `pip`, please use the following methods to install:
-- OS X / Linux, run the following commands:
-
- ```
- curl http://peak.telecommunity.com/dist/ez_setup.py | python
- curl https://raw.github.com/pypa/pip/master/contrib/get-pip.py | python
- ```
+---
-- Windows:
+DjangoBlog is a high-performance blog platform built with Python 3.10 and Django 4.0. It not only provides all the core functionalities of a traditional blog but also features a flexible plugin system, allowing you to easily extend and customize your website. Whether you are a personal blogger, a tech enthusiast, or a content creator, DjangoBlog aims to provide a stable, efficient, and easy-to-maintain environment for writing and publishing.
- Download http://peak.telecommunity.com/dist/ez_setup.py and https://raw.github.com/pypa/pip/master/contrib/get-pip.py, and run with python.
+## ✨ Features
-### Configuration
-Most configurations are in `setting.py`, others are in backend configurations.
+- **Powerful Content Management**: Full support for managing articles, standalone pages, categories, and tags. Comes with a powerful built-in Markdown editor with syntax highlighting.
+- **Full-Text Search**: Integrated search engine for fast and accurate content searching.
+- **Interactive Comment System**: Supports replies, email notifications, and Markdown formatting in comments.
+- **Flexible Sidebar**: Customizable modules for displaying recent articles, most viewed posts, tag cloud, and more.
+- **Social Login**: Built-in OAuth support, with integrations for Google, GitHub, Facebook, Weibo, QQ, and other major platforms.
+- **High-Performance Caching**: Native support for Redis caching with an automatic refresh mechanism to ensure high-speed website responses.
+- **SEO Friendly**: Basic SEO features are included, with automatic notifications to Google and Baidu upon new content publication.
+- **Extensible Plugin System**: Extend blog functionalities by creating standalone plugins, ensuring decoupled and maintainable code. We have already implemented features like view counting and SEO optimization through plugins!
+- **Integrated Image Hosting**: A simple, built-in image hosting feature for easy uploads and management.
+- **Automated Frontend**: Integrated with `django-compressor` to automatically compress and optimize CSS and JavaScript files.
+- **Robust Operations**: Built-in email notifications for website exceptions and management capabilities through a WeChat Official Account.
-I set many `setting` configuration with my environment variables (such as: `SECRET_KEY`, `OAUTH`, `mysql` and some email configuration parts.) and they did NOT been submitted to the `GitHub`. You can change these in the code with your own configuration or just add them into your environment variables.
+## 🛠️ Tech Stack
-Files in `test` directory are for `travis` with automatic testing. You do not need to care about this. Or just use it, in this way to integrate `travis` for automatic testing.
+- **Backend**: Python 3.10, Django 4.0
+- **Database**: MySQL, SQLite (configurable)
+- **Cache**: Redis
+- **Frontend**: HTML5, CSS3, JavaScript
+- **Search**: Whoosh, Elasticsearch (configurable)
+- **Editor**: Markdown (mdeditor)
-In `bin` directory, we have scripts to deploy with `Nginx`+`Gunicorn`+`virtualenv`+`supervisor` on `linux` and `Nginx` configuration file. You can reference with my article
+## 🚀 Getting Started
->[DjangoBlog部署教程](https://www.lylinux.net/article/2019/8/5/58.html)
+### 1. Prerequisites
-More deploy detail in this article.
+Ensure you have Python 3.10+ and MySQL/MariaDB installed on your system.
-## Run
+### 2. Clone & Installation
-Modify `DjangoBlog/setting.py` with database settings, as following:
+```bash
+# Clone the project to your local machine
+git clone https://github.com/liangliangyy/DjangoBlog.git
+cd DjangoBlog
-```python
-DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.mysql',
- 'NAME': 'djangoblog',
- 'USER': 'root',
- 'PASSWORD': 'password',
- 'HOST': 'host',
- 'PORT': 3306,
- }
-}
+# Install dependencies
+pip install -r requirements.txt
```
-### Create database
-Run the following command in MySQL shell:
-```sql
-CREATE DATABASE `djangoblog` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */;
-```
+### 3. Project Configuration
+
+- **Database**:
+ Open `djangoblog/settings.py`, locate the `DATABASES` section, and update it with your MySQL connection details.
+
+ ```python
+ DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.mysql',
+ 'NAME': 'djangoblog',
+ 'USER': 'root',
+ 'PASSWORD': 'your_password',
+ 'HOST': '127.0.0.1',
+ 'PORT': 3306,
+ }
+ }
+ ```
+ Create the database in MySQL:
+ ```sql
+ CREATE DATABASE `djangoblog` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+ ```
+
+- **More Configurations**:
+ For advanced settings such as email, OAuth, caching, and more, please refer to our [Detailed Configuration Guide](/docs/config-en.md).
+
+### 4. Database Initialization
-Run the following commands in Terminal:
```bash
python manage.py makemigrations
python manage.py migrate
-```
-### Create super user
-
-Run command in terminal:
-```bash
+# Create a superuser account
python manage.py createsuperuser
```
-### Create testing data
-Run command in terminal:
+### 5. Running the Project
+
```bash
+# (Optional) Generate some test data
python manage.py create_testdata
-```
-### Collect static files
-Run command in terminal:
-```bash
+# (Optional) Collect and compress static files
python manage.py collectstatic --noinput
python manage.py compress --force
+
+# Start the development server
+python manage.py runserver
```
-### Getting start to run server
-Execute: `python manage.py runserver`
+Now, open your browser and navigate to `http://127.0.0.1:8000/`. You should see the DjangoBlog homepage!
+
+## Deployment
+
+- **Traditional Deployment**: A detailed guide for server deployment is available here: [Deployment Tutorial](https://www.lylinux.net/article/2019/8/5/58.html) (in Chinese).
+- **Docker Deployment**: This project fully supports Docker. If you are familiar with containerization, please refer to the [Docker Deployment Guide](/docs/docker-en.md) for a quick start.
+- **Kubernetes Deployment**: We also provide a complete [Kubernetes Deployment Guide](/docs/k8s-en.md) to help you go cloud-native easily.
+
+## 🧩 Plugin System
+
+The plugin system is a core feature of DjangoBlog. It allows you to add new functionalities to your blog without modifying the core codebase by writing standalone plugins.
-Open up a browser and visit: http://127.0.0.1:8000/ , the you will see the blog.
+- **How it Works**: Plugins operate by registering callback functions to predefined "hooks". For instance, when an article is rendered, the `after_article_body_get` hook is triggered, and all functions registered to this hook are executed.
+- **Existing Plugins**: Features like `view_count` and `seo_optimizer` are implemented through this plugin system.
+- **Develop Your Own Plugin**: Simply create a new folder under the `plugins` directory and write your `plugin.py`. We welcome you to explore and contribute your creative ideas to the DjangoBlog community!
-## More configurations
-[More configurations details](/docs/config-en.md)
+## 🤝 Contributing
-## About the issues
+We warmly welcome contributions of any kind! If you have great ideas or have found a bug, please feel free to open an issue or submit a pull request.
-If you have any *question*, please use Issue or send problem descriptions to my email `liangliangyy#gmail.com`. I will reponse you as soon as possible. And, we recommend you to use Issue.
+## 📄 License
+
+This project is open-sourced under the [MIT License](LICENSE).
---
-## To Everyone 🙋♀️🙋♂️
-If this project helps you, please submit your site address [here](https://github.com/liangliangyy/DjangoBlog/issues/214) to let more people see it.
-Your reply will be the driving force for me to continue to update and maintain this project.
+## ❤️ Support & Sponsorship
+
+If you find this project helpful and wish to support its continued maintenance and development, please consider buying me a coffee! Your support is my greatest motivation.
+
+
+
+
+
+
+ (Left) Alipay / (Right) WeChat
+
+
+## 🙏 Acknowledgements
-🙏🙏🙏
+A special thanks to **JetBrains** for providing a free open-source license for this project.
+
+
+
+
+
+
+
+---
+> If this project has helped you, please leave your website URL [here](https://github.com/liangliangyy/DjangoBlog/issues/214) to let more people see it. Your feedback is the driving force for my continued updates and maintenance.
diff --git a/src/docs/docker.md b/src/docs/docker.md
index 92af9fa..e7c255a 100644
--- a/src/docs/docker.md
+++ b/src/docs/docker.md
@@ -1,59 +1,114 @@
-# 使用docker部署
+# 使用 Docker 部署 DjangoBlog
+



-使用docker部署支持如下两种方式:
-## docker镜像方式
-本项目已经支持了docker部署,如果你已经有了`mysql`,那么直接使用基础镜像即可,启动命令如下所示:
-```shell
-docker pull liangliangyy/djangoblog:latest
-docker run -d -p 8000:8000 -e DJANGO_MYSQL_HOST=mysqlhost -e DJANGO_MYSQL_PASSWORD=mysqlrootpassword -e DJANGO_MYSQL_USER=root -e DJANGO_MYSQL_DATABASE=djangoblog --name djangoblog liangliangyy/djangoblog:latest
+本项目全面支持使用 Docker 进行容器化部署,为您提供了快速、一致且隔离的运行环境。我们推荐使用 `docker-compose` 来一键启动整个博客服务栈。
+
+## 1. 环境准备
+
+在开始之前,请确保您的系统中已经安装了以下软件:
+- [Docker Engine](https://docs.docker.com/engine/install/)
+- [Docker Compose](https://docs.docker.com/compose/install/) (对于 Docker Desktop 用户,它已内置)
+
+## 2. 推荐方式:使用 `docker-compose` (一键部署)
+
+这是最简单、最推荐的部署方式。它会自动为您创建并管理 Django 应用、MySQL 数据库,以及可选的 Elasticsearch 服务。
+
+### 步骤 1: 启动基础服务
+
+在项目根目录下,执行以下命令:
+
+```bash
+# 构建并以后台模式启动容器 (包含 Django 应用和 MySQL)
+docker-compose up -d --build
```
-启动完成后,访问 http://127.0.0.1:8000
-## 使用docker-compose
-如果你没有mysql等基础服务,那么可以使用`docker-compose`来运行,
-具体命令如下所示:
-```shell
-docker-compose build
-docker-compose up -d
+
+`docker-compose` 会读取 `docker-compose.yml` 文件,自动拉取所需镜像、构建项目镜像,并启动所有服务。
+
+- **访问您的博客**: 服务启动后,在浏览器中访问 `http://127.0.0.1` 即可看到博客首页。
+- **数据持久化**: MySQL 的数据文件将存储在项目根目录下的 `data/mysql` 文件夹中,确保数据在容器重启后不丢失。
+
+### 步骤 2: (可选) 启用 Elasticsearch 全文搜索
+
+如果您希望使用 Elasticsearch 提供更强大的全文搜索功能,可以额外加载 `docker-compose.es.yml` 配置文件:
+
+```bash
+# 构建并以后台模式启动所有服务 (Django, MySQL, Elasticsearch)
+docker-compose -f docker-compose.yml -f deploy/docker-compose/docker-compose.es.yml up -d --build
```
-本方式生成的mysql数据文件在 `bin/datas/mysql` 文件夹。
-等启动完成后,访问 [http://127.0.0.1](http://127.0.0.1) 即可。
-### 使用es
-如果你期望使用es来作为后端的搜索引擎,那么可以使用如下命令来启动:
-```shell
-docker-compose -f docker-compose.yml -f docker-compose.es.yml build
-docker-compose -f docker-compose.yml -f docker-compose.es.yml up -d
+- **数据持久化**: Elasticsearch 的数据将存储在 `data/elasticsearch` 文件夹中。
+
+### 步骤 3: 首次运行的初始化操作
+
+当容器首次启动后,您需要进入容器来执行一些初始化命令。
+
+```bash
+# 进入 djangoblog 应用容器
+docker-compose exec web bash
+
+# 在容器内执行以下命令:
+# 创建超级管理员账户 (请按照提示设置用户名、邮箱和密码)
+python manage.py createsuperuser
+
+# (可选) 创建一些测试数据
+python manage.py create_testdata
+
+# (可选,如果启用了 ES) 创建索引
+python manage.py rebuild_index
+
+# 退出容器
+exit
```
-本方式生成的es数据文件在 `bin/datas/es` 文件夹。
-## 配置说明:
-
-本项目较多配置都基于环境变量,所有的环境变量如下所示:
-
-| 环境变量名称 | 默认值 | 备注 |
-|---------------------------|----------------------------------------------------------------------------|------------------------------------------------------------------------------------------------|
-| DJANGO_DEBUG | False | |
-| DJANGO_SECRET_KEY | DJANGO_BLOG_CHANGE_ME | 请务必修改,建议[随机生成](https://www.random.org/passwords/?num=5&len=24&format=html&rnd=new) |
-| DJANGO_MYSQL_DATABASE | djangoblog | |
-| DJANGO_MYSQL_USER | root | |
-| DJANGO_MYSQL_PASSWORD | djangoblog_123 | |
-| DJANGO_MYSQL_HOST | 127.0.0.1 | |
-| DJANGO_MYSQL_PORT | 3306 | |
-| DJANGO_MEMCACHED_ENABLE | True | |
-| DJANGO_MEMCACHED_LOCATION | 127.0.0.1:11211 | |
-| DJANGO_BAIDU_NOTIFY_URL | http://data.zz.baidu.com/urls?site=https://www.example.org&token=CHANGE_ME | 请在[百度站长平台](https://ziyuan.baidu.com/linksubmit/index)获取接口地址 |
-| DJANGO_EMAIL_TLS | False | |
-| DJANGO_EMAIL_SSL | True | |
-| DJANGO_EMAIL_HOST | smtp.example.org | |
-| DJANGO_EMAIL_PORT | 465 | |
-| DJANGO_EMAIL_USER | SMTP_USER_CHANGE_ME | |
-| DJANGO_EMAIL_PASSWORD | SMTP_PASSWORD_CHANGE_ME | |
-| DJANGO_ADMIN_EMAIL | admin@example.org | |
-| DJANGO_WEROBOT_TOKEN | DJANGO_BLOG_CHANGE_ME
-|DJANGO_ELASTICSEARCH_HOST|
-
-第一次启动之后,使用如下命令来创建超级用户:
-```shell
-docker exec -it djangoblog python /code/djangoblog/manage.py createsuperuser
+
+## 3. 备选方式:使用独立的 Docker 镜像
+
+如果您已经拥有一个正在运行的外部 MySQL 数据库,您也可以只运行 DjangoBlog 的应用镜像。
+
+```bash
+# 从 Docker Hub 拉取最新镜像
+docker pull liangliangyy/djangoblog:latest
+
+# 运行容器,并链接到您的外部数据库
+docker run -d \
+ -p 8000:8000 \
+ -e DJANGO_SECRET_KEY='your-strong-secret-key' \
+ -e DJANGO_MYSQL_HOST='your-mysql-host' \
+ -e DJANGO_MYSQL_USER='your-mysql-user' \
+ -e DJANGO_MYSQL_PASSWORD='your-mysql-password' \
+ -e DJANGO_MYSQL_DATABASE='djangoblog' \
+ --name djangoblog \
+ liangliangyy/djangoblog:latest
```
+
+- **访问您的博客**: 启动完成后,访问 `http://127.0.0.1:8000`。
+- **创建管理员**: `docker exec -it djangoblog python manage.py createsuperuser`
+
+## 4. 配置说明 (环境变量)
+
+本项目的大部分配置都通过环境变量来管理。您可以在 `docker-compose.yml` 文件中修改它们,或者在使用 `docker run` 命令时通过 `-e` 参数传入。
+
+| 环境变量名称 | 默认值/示例 | 备注 |
+|-------------------------|--------------------------------------------------------------------------|---------------------------------------------------------------------|
+| `DJANGO_SECRET_KEY` | `your-strong-secret-key` | **请务必修改为一个随机且复杂的字符串!** |
+| `DJANGO_DEBUG` | `False` | 是否开启 Django 的调试模式 |
+| `DJANGO_MYSQL_HOST` | `mysql` | 数据库主机名 |
+| `DJANGO_MYSQL_PORT` | `3306` | 数据库端口 |
+| `DJANGO_MYSQL_DATABASE` | `djangoblog` | 数据库名称 |
+| `DJANGO_MYSQL_USER` | `root` | 数据库用户名 |
+| `DJANGO_MYSQL_PASSWORD` | `djangoblog_123` | 数据库密码 |
+| `DJANGO_REDIS_URL` | `redis:6379/0` | Redis 连接地址 (用于缓存) |
+| `DJANGO_ELASTICSEARCH_HOST` | `elasticsearch:9200` | Elasticsearch 主机地址 |
+| `DJANGO_EMAIL_HOST` | `smtp.example.org` | 邮件服务器地址 |
+| `DJANGO_EMAIL_PORT` | `465` | 邮件服务器端口 |
+| `DJANGO_EMAIL_USER` | `user@example.org` | 邮件账户 |
+| `DJANGO_EMAIL_PASSWORD` | `your-email-password` | 邮件密码 |
+| `DJANGO_EMAIL_USE_SSL` | `True` | 是否使用 SSL |
+| `DJANGO_EMAIL_USE_TLS` | `False` | 是否使用 TLS |
+| `DJANGO_ADMIN_EMAIL` | `admin@example.org` | 接收异常报告的管理员邮箱 |
+| `DJANGO_BAIDU_NOTIFY_URL` | `http://data.zz.baidu.com/...` | [百度站长平台](https://ziyuan.baidu.com/linksubmit/index) 的推送接口 |
+
+---
+
+部署完成后,请务必检查并根据您的实际需求调整这些环境变量,特别是 `DJANGO_SECRET_KEY` 和数据库、邮件相关的配置。
diff --git a/src/logs/djangoblog.log b/src/logs/djangoblog.log
new file mode 100644
index 0000000..f58c44a
--- /dev/null
+++ b/src/logs/djangoblog.log
@@ -0,0 +1,119 @@
+[2025-10-12 19:21:57,358] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 文章结尾版权声明 initialized.
+[2025-10-12 19:21:57,358] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 文章结尾版权声明 initialized.
+[2025-10-12 19:21:57,358] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: article_copyright - 文章结尾版权声明
+[2025-10-12 19:21:57,358] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: article_copyright - 文章结尾版权声明
+[2025-10-12 19:21:57,360] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 阅读时间预测 initialized.
+[2025-10-12 19:21:57,360] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 阅读时间预测 initialized.
+[2025-10-12 19:21:57,360] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: reading_time - 阅读时间预测
+[2025-10-12 19:21:57,360] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: reading_time - 阅读时间预测
+[2025-10-12 19:21:57,362] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 外部链接处理器 initialized.
+[2025-10-12 19:21:57,362] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 外部链接处理器 initialized.
+[2025-10-12 19:21:57,363] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: external_links - 外部链接处理器
+[2025-10-12 19:21:57,363] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: external_links - 外部链接处理器
+[2025-10-12 19:21:57,364] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 文章浏览次数统计 initialized.
+[2025-10-12 19:21:57,364] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 文章浏览次数统计 initialized.
+[2025-10-12 19:21:57,365] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: view_count - 文章浏览次数统计
+[2025-10-12 19:21:57,365] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: view_count - 文章浏览次数统计
+[2025-10-12 19:21:57,367] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] SEO 优化器 initialized.
+[2025-10-12 19:21:57,367] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] SEO 优化器 initialized.
+[2025-10-12 19:21:57,367] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: seo_optimizer - SEO 优化器
+[2025-10-12 19:21:57,367] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: seo_optimizer - SEO 优化器
+[2025-10-12 19:21:57,370] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 图片性能优化插件 initialized.
+[2025-10-12 19:21:57,370] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 图片性能优化插件 initialized.
+[2025-10-12 19:21:57,370] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: image_lazy_loading - 图片性能优化插件
+[2025-10-12 19:21:57,370] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: image_lazy_loading - 图片性能优化插件
+[2025-10-12 19:21:57,373] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 文章推荐 initialized.
+[2025-10-12 19:21:57,373] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 文章推荐 initialized.
+[2025-10-12 19:21:57,373] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: article_recommendation - 文章推荐
+[2025-10-12 19:21:57,373] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: article_recommendation - 文章推荐
+[2025-10-12 19:22:07,736] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 文章结尾版权声明 initialized.
+[2025-10-12 19:22:07,736] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 文章结尾版权声明 initialized.
+[2025-10-12 19:22:07,737] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: article_copyright - 文章结尾版权声明
+[2025-10-12 19:22:07,737] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: article_copyright - 文章结尾版权声明
+[2025-10-12 19:22:07,760] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 阅读时间预测 initialized.
+[2025-10-12 19:22:07,760] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 阅读时间预测 initialized.
+[2025-10-12 19:22:07,760] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: reading_time - 阅读时间预测
+[2025-10-12 19:22:07,760] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: reading_time - 阅读时间预测
+[2025-10-12 19:22:07,775] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 外部链接处理器 initialized.
+[2025-10-12 19:22:07,775] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 外部链接处理器 initialized.
+[2025-10-12 19:22:07,775] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: external_links - 外部链接处理器
+[2025-10-12 19:22:07,775] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: external_links - 外部链接处理器
+[2025-10-12 19:22:07,799] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 文章浏览次数统计 initialized.
+[2025-10-12 19:22:07,799] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 文章浏览次数统计 initialized.
+[2025-10-12 19:22:07,799] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: view_count - 文章浏览次数统计
+[2025-10-12 19:22:07,799] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: view_count - 文章浏览次数统计
+[2025-10-12 19:22:07,828] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] SEO 优化器 initialized.
+[2025-10-12 19:22:07,828] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] SEO 优化器 initialized.
+[2025-10-12 19:22:07,828] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: seo_optimizer - SEO 优化器
+[2025-10-12 19:22:07,828] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: seo_optimizer - SEO 优化器
+[2025-10-12 19:22:07,861] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 图片性能优化插件 initialized.
+[2025-10-12 19:22:07,861] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 图片性能优化插件 initialized.
+[2025-10-12 19:22:07,861] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: image_lazy_loading - 图片性能优化插件
+[2025-10-12 19:22:07,861] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: image_lazy_loading - 图片性能优化插件
+[2025-10-12 19:22:07,884] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 文章推荐 initialized.
+[2025-10-12 19:22:07,884] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 文章推荐 initialized.
+[2025-10-12 19:22:07,885] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: article_recommendation - 文章推荐
+[2025-10-12 19:22:07,885] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: article_recommendation - 文章推荐
+[2025-10-12 19:22:17,362] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 文章结尾版权声明 initialized.
+[2025-10-12 19:22:17,362] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 文章结尾版权声明 initialized.
+[2025-10-12 19:22:17,363] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: article_copyright - 文章结尾版权声明
+[2025-10-12 19:22:17,363] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: article_copyright - 文章结尾版权声明
+[2025-10-12 19:22:17,364] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 阅读时间预测 initialized.
+[2025-10-12 19:22:17,364] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 阅读时间预测 initialized.
+[2025-10-12 19:22:17,364] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: reading_time - 阅读时间预测
+[2025-10-12 19:22:17,364] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: reading_time - 阅读时间预测
+[2025-10-12 19:22:17,364] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 外部链接处理器 initialized.
+[2025-10-12 19:22:17,364] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 外部链接处理器 initialized.
+[2025-10-12 19:22:17,366] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: external_links - 外部链接处理器
+[2025-10-12 19:22:17,366] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: external_links - 外部链接处理器
+[2025-10-12 19:22:17,367] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 文章浏览次数统计 initialized.
+[2025-10-12 19:22:17,367] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 文章浏览次数统计 initialized.
+[2025-10-12 19:22:17,367] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: view_count - 文章浏览次数统计
+[2025-10-12 19:22:17,367] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: view_count - 文章浏览次数统计
+[2025-10-12 19:22:17,368] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] SEO 优化器 initialized.
+[2025-10-12 19:22:17,368] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] SEO 优化器 initialized.
+[2025-10-12 19:22:17,368] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: seo_optimizer - SEO 优化器
+[2025-10-12 19:22:17,368] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: seo_optimizer - SEO 优化器
+[2025-10-12 19:22:17,369] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 图片性能优化插件 initialized.
+[2025-10-12 19:22:17,369] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 图片性能优化插件 initialized.
+[2025-10-12 19:22:17,369] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: image_lazy_loading - 图片性能优化插件
+[2025-10-12 19:22:17,369] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: image_lazy_loading - 图片性能优化插件
+[2025-10-12 19:22:17,370] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 文章推荐 initialized.
+[2025-10-12 19:22:17,370] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 文章推荐 initialized.
+[2025-10-12 19:22:17,370] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: article_recommendation - 文章推荐
+[2025-10-12 19:22:17,370] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: article_recommendation - 文章推荐
+[2025-10-12 19:22:18,002] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 文章结尾版权声明 initialized.
+[2025-10-12 19:22:18,002] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 文章结尾版权声明 initialized.
+[2025-10-12 19:22:18,003] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: article_copyright - 文章结尾版权声明
+[2025-10-12 19:22:18,003] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: article_copyright - 文章结尾版权声明
+[2025-10-12 19:22:18,004] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 阅读时间预测 initialized.
+[2025-10-12 19:22:18,004] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 阅读时间预测 initialized.
+[2025-10-12 19:22:18,004] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: reading_time - 阅读时间预测
+[2025-10-12 19:22:18,004] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: reading_time - 阅读时间预测
+[2025-10-12 19:22:18,005] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 外部链接处理器 initialized.
+[2025-10-12 19:22:18,005] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 外部链接处理器 initialized.
+[2025-10-12 19:22:18,005] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: external_links - 外部链接处理器
+[2025-10-12 19:22:18,005] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: external_links - 外部链接处理器
+[2025-10-12 19:22:18,007] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 文章浏览次数统计 initialized.
+[2025-10-12 19:22:18,007] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 文章浏览次数统计 initialized.
+[2025-10-12 19:22:18,007] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: view_count - 文章浏览次数统计
+[2025-10-12 19:22:18,007] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: view_count - 文章浏览次数统计
+[2025-10-12 19:22:18,008] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] SEO 优化器 initialized.
+[2025-10-12 19:22:18,008] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] SEO 优化器 initialized.
+[2025-10-12 19:22:18,008] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: seo_optimizer - SEO 优化器
+[2025-10-12 19:22:18,008] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: seo_optimizer - SEO 优化器
+[2025-10-12 19:22:18,009] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 图片性能优化插件 initialized.
+[2025-10-12 19:22:18,009] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 图片性能优化插件 initialized.
+[2025-10-12 19:22:18,014] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: image_lazy_loading - 图片性能优化插件
+[2025-10-12 19:22:18,014] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: image_lazy_loading - 图片性能优化插件
+[2025-10-12 19:22:18,015] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 文章推荐 initialized.
+[2025-10-12 19:22:18,015] INFO [djangoblog.plugin_manage.base_plugin.init_plugin:48 base_plugin] 文章推荐 initialized.
+[2025-10-12 19:22:18,016] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: article_recommendation - 文章推荐
+[2025-10-12 19:22:18,016] INFO [djangoblog.plugin_manage.loader.load_plugins:29 loader] Successfully loaded plugin: article_recommendation - 文章推荐
+[2025-10-12 19:22:18,038] INFO [django.utils.autoreload.run_with_reloader:667 autoreload] Watching for file changes with StatReloader
+[2025-10-12 19:22:21,201] INFO [blog.views.get_queryset_from_cache:75 views] set view cache.key:index_1
+[2025-10-12 19:22:21,247] INFO [blog.context_processors.seo_processor:17 context_processors] set processor cache.
+[2025-10-12 19:22:21,260] INFO [djangoblog.utils.get_blog_setting:171 utils] set cache get_blog_setting
+[2025-10-12 19:22:21,260] INFO [djangoblog.utils.get_blog_setting:171 utils] set cache get_blog_setting
+[2025-10-12 19:22:21,716] INFO [blog.templatetags.blog_tags.load_sidebar:213 blog_tags] load sidebar
+[2025-10-12 19:22:21,755] INFO [blog.templatetags.blog_tags.load_sidebar:257 blog_tags] set sidebar cache.key:sidebari
diff --git a/src/oauth/__pycache__/__init__.cpython-311.pyc b/src/oauth/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..8fe1e0f
Binary files /dev/null and b/src/oauth/__pycache__/__init__.cpython-311.pyc differ
diff --git a/src/oauth/__pycache__/admin.cpython-311.pyc b/src/oauth/__pycache__/admin.cpython-311.pyc
new file mode 100644
index 0000000..ecd8484
Binary files /dev/null and b/src/oauth/__pycache__/admin.cpython-311.pyc differ
diff --git a/src/oauth/__pycache__/apps.cpython-311.pyc b/src/oauth/__pycache__/apps.cpython-311.pyc
new file mode 100644
index 0000000..785e194
Binary files /dev/null and b/src/oauth/__pycache__/apps.cpython-311.pyc differ
diff --git a/src/oauth/__pycache__/forms.cpython-311.pyc b/src/oauth/__pycache__/forms.cpython-311.pyc
new file mode 100644
index 0000000..b9360c3
Binary files /dev/null and b/src/oauth/__pycache__/forms.cpython-311.pyc differ
diff --git a/src/oauth/__pycache__/models.cpython-311.pyc b/src/oauth/__pycache__/models.cpython-311.pyc
new file mode 100644
index 0000000..de9d530
Binary files /dev/null and b/src/oauth/__pycache__/models.cpython-311.pyc differ
diff --git a/src/oauth/__pycache__/oauthmanager.cpython-311.pyc b/src/oauth/__pycache__/oauthmanager.cpython-311.pyc
new file mode 100644
index 0000000..21d6432
Binary files /dev/null and b/src/oauth/__pycache__/oauthmanager.cpython-311.pyc differ
diff --git a/src/oauth/__pycache__/urls.cpython-311.pyc b/src/oauth/__pycache__/urls.cpython-311.pyc
new file mode 100644
index 0000000..06f7fe1
Binary files /dev/null and b/src/oauth/__pycache__/urls.cpython-311.pyc differ
diff --git a/src/oauth/__pycache__/views.cpython-311.pyc b/src/oauth/__pycache__/views.cpython-311.pyc
new file mode 100644
index 0000000..cebb7f0
Binary files /dev/null and b/src/oauth/__pycache__/views.cpython-311.pyc differ
diff --git a/src/oauth/migrations/__pycache__/0001_initial.cpython-311.pyc b/src/oauth/migrations/__pycache__/0001_initial.cpython-311.pyc
new file mode 100644
index 0000000..794bda1
Binary files /dev/null and b/src/oauth/migrations/__pycache__/0001_initial.cpython-311.pyc differ
diff --git a/src/oauth/migrations/__pycache__/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.cpython-311.pyc b/src/oauth/migrations/__pycache__/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.cpython-311.pyc
new file mode 100644
index 0000000..638bb43
Binary files /dev/null and b/src/oauth/migrations/__pycache__/0002_alter_oauthconfig_options_alter_oauthuser_options_and_more.cpython-311.pyc differ
diff --git a/src/oauth/migrations/__pycache__/0003_alter_oauthuser_nickname.cpython-311.pyc b/src/oauth/migrations/__pycache__/0003_alter_oauthuser_nickname.cpython-311.pyc
new file mode 100644
index 0000000..ca9e7ad
Binary files /dev/null and b/src/oauth/migrations/__pycache__/0003_alter_oauthuser_nickname.cpython-311.pyc differ
diff --git a/src/oauth/migrations/__pycache__/__init__.cpython-311.pyc b/src/oauth/migrations/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..e9cc2fb
Binary files /dev/null and b/src/oauth/migrations/__pycache__/__init__.cpython-311.pyc differ
diff --git a/src/oauth/templatetags/__pycache__/__init__.cpython-311.pyc b/src/oauth/templatetags/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..3971d37
Binary files /dev/null and b/src/oauth/templatetags/__pycache__/__init__.cpython-311.pyc differ
diff --git a/src/oauth/templatetags/__pycache__/oauth_tags.cpython-311.pyc b/src/oauth/templatetags/__pycache__/oauth_tags.cpython-311.pyc
new file mode 100644
index 0000000..a470f8a
Binary files /dev/null and b/src/oauth/templatetags/__pycache__/oauth_tags.cpython-311.pyc differ
diff --git a/src/owntracks/__pycache__/__init__.cpython-311.pyc b/src/owntracks/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..d5b301d
Binary files /dev/null and b/src/owntracks/__pycache__/__init__.cpython-311.pyc differ
diff --git a/src/owntracks/__pycache__/admin.cpython-311.pyc b/src/owntracks/__pycache__/admin.cpython-311.pyc
new file mode 100644
index 0000000..13cbd0c
Binary files /dev/null and b/src/owntracks/__pycache__/admin.cpython-311.pyc differ
diff --git a/src/owntracks/__pycache__/apps.cpython-311.pyc b/src/owntracks/__pycache__/apps.cpython-311.pyc
new file mode 100644
index 0000000..1a7e2b2
Binary files /dev/null and b/src/owntracks/__pycache__/apps.cpython-311.pyc differ
diff --git a/src/owntracks/__pycache__/models.cpython-311.pyc b/src/owntracks/__pycache__/models.cpython-311.pyc
new file mode 100644
index 0000000..c509e2a
Binary files /dev/null and b/src/owntracks/__pycache__/models.cpython-311.pyc differ
diff --git a/src/owntracks/__pycache__/urls.cpython-311.pyc b/src/owntracks/__pycache__/urls.cpython-311.pyc
new file mode 100644
index 0000000..4e61507
Binary files /dev/null and b/src/owntracks/__pycache__/urls.cpython-311.pyc differ
diff --git a/src/owntracks/__pycache__/views.cpython-311.pyc b/src/owntracks/__pycache__/views.cpython-311.pyc
new file mode 100644
index 0000000..2414efe
Binary files /dev/null and b/src/owntracks/__pycache__/views.cpython-311.pyc differ
diff --git a/src/owntracks/migrations/__pycache__/0001_initial.cpython-311.pyc b/src/owntracks/migrations/__pycache__/0001_initial.cpython-311.pyc
new file mode 100644
index 0000000..e3ff680
Binary files /dev/null and b/src/owntracks/migrations/__pycache__/0001_initial.cpython-311.pyc differ
diff --git a/src/owntracks/migrations/__pycache__/0002_alter_owntracklog_options_and_more.cpython-311.pyc b/src/owntracks/migrations/__pycache__/0002_alter_owntracklog_options_and_more.cpython-311.pyc
new file mode 100644
index 0000000..76875aa
Binary files /dev/null and b/src/owntracks/migrations/__pycache__/0002_alter_owntracklog_options_and_more.cpython-311.pyc differ
diff --git a/src/owntracks/migrations/__pycache__/__init__.cpython-311.pyc b/src/owntracks/migrations/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..00a928f
Binary files /dev/null and b/src/owntracks/migrations/__pycache__/__init__.cpython-311.pyc differ
diff --git a/src/owntracks/views.py b/src/owntracks/views.py
index 6c87a2b..4c72bdd 100644
--- a/src/owntracks/views.py
+++ b/src/owntracks/views.py
@@ -3,16 +3,15 @@ import datetime
import itertools
import json
import logging
+from datetime import timezone
from itertools import groupby
-import django.utils.timezone
+import django
import requests
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse
from django.http import JsonResponse
from django.shortcuts import render
-from django.utils import timezone
-from django.utils.timezone import utc
from django.views.decorators.csrf import csrf_exempt
from .models import OwnTrackLog
@@ -48,7 +47,7 @@ def manage_owntrack_log(request):
@login_required
def show_maps(request):
if request.user.is_superuser:
- defaultdate = str(timezone.now().date())
+ defaultdate = str(datetime.datetime.now(timezone.utc).date())
date = request.GET.get('date', defaultdate)
context = {
'date': date
@@ -97,14 +96,13 @@ def convert_to_amap(locations):
@login_required
def get_datas(request):
- now = django.utils.timezone.now().replace(tzinfo=utc)
+ now = django.utils.timezone.now().replace(tzinfo=timezone.utc)
querydate = django.utils.timezone.datetime(
now.year, now.month, now.day, 0, 0, 0)
if request.GET.get('date', None):
date = list(map(lambda x: int(x), request.GET.get('date').split('-')))
querydate = django.utils.timezone.datetime(
date[0], date[1], date[2], 0, 0, 0)
- querydate = django.utils.timezone.make_aware(querydate)
nextdate = querydate + datetime.timedelta(days=1)
models = OwnTrackLog.objects.filter(
creation_time__range=(querydate, nextdate))
diff --git a/src/plugins/__pycache__/__init__.cpython-311.pyc b/src/plugins/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..6c88943
Binary files /dev/null and b/src/plugins/__pycache__/__init__.cpython-311.pyc differ
diff --git a/src/plugins/article_copyright/__pycache__/__init__.cpython-311.pyc b/src/plugins/article_copyright/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..7fd1524
Binary files /dev/null and b/src/plugins/article_copyright/__pycache__/__init__.cpython-311.pyc differ
diff --git a/src/plugins/article_copyright/__pycache__/plugin.cpython-311.pyc b/src/plugins/article_copyright/__pycache__/plugin.cpython-311.pyc
new file mode 100644
index 0000000..18e9c06
Binary files /dev/null and b/src/plugins/article_copyright/__pycache__/plugin.cpython-311.pyc differ
diff --git a/src/plugins/article_copyright/plugin.py b/src/plugins/article_copyright/plugin.py
index 317fed2..5dba3b3 100644
--- a/src/plugins/article_copyright/plugin.py
+++ b/src/plugins/article_copyright/plugin.py
@@ -22,6 +22,11 @@ class ArticleCopyrightPlugin(BasePlugin):
article = kwargs.get('article')
if not article:
return content
+
+ # 如果是摘要模式(首页),不添加版权声明
+ is_summary = kwargs.get('is_summary', False)
+ if is_summary:
+ return content
copyright_info = f"\n本文由 {article.author.username} 原创,转载请注明出处。
"
return content + copyright_info
diff --git a/src/plugins/article_recommendation/__init__.py b/src/plugins/article_recommendation/__init__.py
new file mode 100644
index 0000000..951f2ff
--- /dev/null
+++ b/src/plugins/article_recommendation/__init__.py
@@ -0,0 +1 @@
+# 文章推荐插件
diff --git a/src/plugins/article_recommendation/__pycache__/__init__.cpython-311.pyc b/src/plugins/article_recommendation/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..ddfb8d5
Binary files /dev/null and b/src/plugins/article_recommendation/__pycache__/__init__.cpython-311.pyc differ
diff --git a/src/plugins/article_recommendation/__pycache__/plugin.cpython-311.pyc b/src/plugins/article_recommendation/__pycache__/plugin.cpython-311.pyc
new file mode 100644
index 0000000..8643f29
Binary files /dev/null and b/src/plugins/article_recommendation/__pycache__/plugin.cpython-311.pyc differ
diff --git a/src/plugins/article_recommendation/plugin.py b/src/plugins/article_recommendation/plugin.py
new file mode 100644
index 0000000..6656a07
--- /dev/null
+++ b/src/plugins/article_recommendation/plugin.py
@@ -0,0 +1,205 @@
+import logging
+from djangoblog.plugin_manage.base_plugin import BasePlugin
+from djangoblog.plugin_manage import hooks
+from djangoblog.plugin_manage.hook_constants import ARTICLE_DETAIL_LOAD
+from blog.models import Article
+
+logger = logging.getLogger(__name__)
+
+
+class ArticleRecommendationPlugin(BasePlugin):
+ PLUGIN_NAME = '文章推荐'
+ PLUGIN_DESCRIPTION = '智能文章推荐系统,支持多位置展示'
+ PLUGIN_VERSION = '1.0.0'
+ PLUGIN_AUTHOR = 'liangliangyy'
+
+ # 支持的位置
+ SUPPORTED_POSITIONS = ['article_bottom']
+
+ # 各位置优先级
+ POSITION_PRIORITIES = {
+ 'article_bottom': 80, # 文章底部优先级
+ }
+
+ # 插件配置
+ CONFIG = {
+ 'article_bottom_count': 8, # 文章底部推荐数量
+ 'sidebar_count': 5, # 侧边栏推荐数量
+ 'enable_category_fallback': True, # 启用分类回退
+ 'enable_popular_fallback': True, # 启用热门文章回退
+ }
+
+ def register_hooks(self):
+ """注册钩子"""
+ hooks.register(ARTICLE_DETAIL_LOAD, self.on_article_detail_load)
+
+ def on_article_detail_load(self, article, context, request, *args, **kwargs):
+ """文章详情页加载时的处理"""
+ # 可以在这里预加载推荐数据到context中
+ recommendations = self.get_recommendations(article)
+ context['article_recommendations'] = recommendations
+
+ def should_display(self, position, context, **kwargs):
+ """条件显示逻辑"""
+ # 只在文章详情页底部显示
+ if position == 'article_bottom':
+ article = kwargs.get('article') or context.get('article')
+ # 检查是否有文章对象,以及是否不是索引页面
+ is_index = context.get('isindex', False) if hasattr(context, 'get') else False
+ return article is not None and not is_index
+
+ return False
+
+ def render_article_bottom_widget(self, context, **kwargs):
+ """渲染文章底部推荐"""
+ article = kwargs.get('article') or context.get('article')
+ if not article:
+ return None
+
+ # 使用配置的数量,也可以通过kwargs覆盖
+ count = kwargs.get('count', self.CONFIG['article_bottom_count'])
+ recommendations = self.get_recommendations(article, count=count)
+ if not recommendations:
+ return None
+
+ # 将RequestContext转换为普通字典
+ context_dict = {}
+ if hasattr(context, 'flatten'):
+ context_dict = context.flatten()
+ elif hasattr(context, 'dicts'):
+ # 合并所有上下文字典
+ for d in context.dicts:
+ context_dict.update(d)
+
+ template_context = {
+ 'recommendations': recommendations,
+ 'article': article,
+ 'title': '相关推荐',
+ **context_dict
+ }
+
+ return self.render_template('bottom_widget.html', template_context)
+
+ def render_sidebar_widget(self, context, **kwargs):
+ """渲染侧边栏推荐"""
+ article = context.get('article')
+
+ # 使用配置的数量,也可以通过kwargs覆盖
+ count = kwargs.get('count', self.CONFIG['sidebar_count'])
+
+ if article:
+ # 文章页面,显示相关文章
+ recommendations = self.get_recommendations(article, count=count)
+ title = '相关文章'
+ else:
+ # 其他页面,显示热门文章
+ recommendations = self.get_popular_articles(count=count)
+ title = '热门推荐'
+
+ if not recommendations:
+ return None
+
+ # 将RequestContext转换为普通字典
+ context_dict = {}
+ if hasattr(context, 'flatten'):
+ context_dict = context.flatten()
+ elif hasattr(context, 'dicts'):
+ # 合并所有上下文字典
+ for d in context.dicts:
+ context_dict.update(d)
+
+ template_context = {
+ 'recommendations': recommendations,
+ 'title': title,
+ **context_dict
+ }
+
+ return self.render_template('sidebar_widget.html', template_context)
+
+ def get_css_files(self):
+ """返回CSS文件"""
+ return ['css/recommendation.css']
+
+ def get_js_files(self):
+ """返回JS文件"""
+ return ['js/recommendation.js']
+
+ def get_recommendations(self, article, count=5):
+ """获取推荐文章"""
+ if not article:
+ return []
+
+ recommendations = []
+
+ # 1. 基于标签的推荐
+ if article.tags.exists():
+ tag_ids = list(article.tags.values_list('id', flat=True))
+ tag_based = list(Article.objects.filter(
+ status='p',
+ tags__id__in=tag_ids
+ ).exclude(
+ id=article.id
+ ).exclude(
+ title__isnull=True
+ ).exclude(
+ title__exact=''
+ ).distinct().order_by('-views')[:count])
+ recommendations.extend(tag_based)
+
+ # 2. 如果数量不够,基于分类推荐
+ if len(recommendations) < count and self.CONFIG['enable_category_fallback']:
+ needed = count - len(recommendations)
+ existing_ids = [r.id for r in recommendations] + [article.id]
+
+ category_based = list(Article.objects.filter(
+ status='p',
+ category=article.category
+ ).exclude(
+ id__in=existing_ids
+ ).exclude(
+ title__isnull=True
+ ).exclude(
+ title__exact=''
+ ).order_by('-views')[:needed])
+ recommendations.extend(category_based)
+
+ # 3. 如果还是不够,推荐热门文章
+ if len(recommendations) < count and self.CONFIG['enable_popular_fallback']:
+ needed = count - len(recommendations)
+ existing_ids = [r.id for r in recommendations] + [article.id]
+
+ popular_articles = list(Article.objects.filter(
+ status='p'
+ ).exclude(
+ id__in=existing_ids
+ ).exclude(
+ title__isnull=True
+ ).exclude(
+ title__exact=''
+ ).order_by('-views')[:needed])
+ recommendations.extend(popular_articles)
+
+ # 过滤掉无效的推荐
+ valid_recommendations = []
+ for rec in recommendations:
+ if rec.title and len(rec.title.strip()) > 0:
+ valid_recommendations.append(rec)
+ else:
+ logger.warning(f"过滤掉空标题文章: ID={rec.id}, 标题='{rec.title}'")
+
+ # 调试:记录推荐结果
+ logger.info(f"原始推荐数量: {len(recommendations)}, 有效推荐数量: {len(valid_recommendations)}")
+ for i, rec in enumerate(valid_recommendations):
+ logger.info(f"推荐 {i+1}: ID={rec.id}, 标题='{rec.title}', 长度={len(rec.title)}")
+
+ return valid_recommendations[:count]
+
+ def get_popular_articles(self, count=3):
+ """获取热门文章"""
+ return list(Article.objects.filter(
+ status='p'
+ ).order_by('-views')[:count])
+
+
+# 实例化插件
+plugin = ArticleRecommendationPlugin()
diff --git a/src/plugins/article_recommendation/static/article_recommendation/css/recommendation.css b/src/plugins/article_recommendation/static/article_recommendation/css/recommendation.css
new file mode 100644
index 0000000..b223f41
--- /dev/null
+++ b/src/plugins/article_recommendation/static/article_recommendation/css/recommendation.css
@@ -0,0 +1,166 @@
+/* 文章推荐插件样式 - 与网站风格保持一致 */
+
+/* 文章底部推荐样式 */
+.article-recommendations {
+ margin: 30px 0;
+ padding: 20px;
+ background: #fff;
+ border: 1px solid #e1e1e1;
+ border-radius: 3px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
+.recommendations-title {
+ margin: 0 0 15px 0;
+ font-size: 18px;
+ color: #444;
+ font-weight: bold;
+ padding-bottom: 8px;
+ border-bottom: 2px solid #21759b;
+ display: inline-block;
+}
+
+.recommendations-icon {
+ margin-right: 5px;
+ font-size: 16px;
+}
+
+.recommendations-grid {
+ display: grid;
+ gap: 15px;
+ grid-template-columns: 1fr;
+ margin-top: 15px;
+}
+
+.recommendation-card {
+ background: #fff;
+ border: 1px solid #e1e1e1;
+ border-radius: 3px;
+ transition: all 0.2s ease;
+ overflow: hidden;
+}
+
+.recommendation-card:hover {
+ border-color: #21759b;
+ box-shadow: 0 2px 5px rgba(33, 117, 155, 0.1);
+}
+
+.recommendation-link {
+ display: block;
+ padding: 15px;
+ text-decoration: none;
+ color: inherit;
+}
+
+.recommendation-title {
+ margin: 0 0 8px 0;
+ font-size: 15px;
+ font-weight: normal;
+ color: #444;
+ line-height: 1.4;
+ transition: color 0.2s ease;
+}
+
+.recommendation-card:hover .recommendation-title {
+ color: #21759b;
+}
+
+.recommendation-meta {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 12px;
+ color: #757575;
+}
+
+.recommendation-category {
+ background: #ebebeb;
+ color: #5e5e5e;
+ padding: 2px 6px;
+ border-radius: 2px;
+ font-size: 11px;
+ font-weight: normal;
+}
+
+.recommendation-date {
+ font-weight: normal;
+ color: #757575;
+}
+
+/* 侧边栏推荐样式 */
+.widget_recommendations {
+ margin-bottom: 20px;
+}
+
+.widget_recommendations .widget-title {
+ font-size: 16px;
+ font-weight: bold;
+ margin-bottom: 15px;
+ color: #333;
+ border-bottom: 2px solid #007cba;
+ padding-bottom: 5px;
+}
+
+.recommendations-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.recommendations-list .recommendation-item {
+ padding: 8px 0;
+ border-bottom: 1px solid #eee;
+ background: none;
+ border: none;
+ border-radius: 0;
+}
+
+.recommendations-list .recommendation-item:last-child {
+ border-bottom: none;
+}
+
+.recommendations-list .recommendation-item a {
+ color: #333;
+ text-decoration: none;
+ font-size: 14px;
+ line-height: 1.4;
+ display: block;
+ margin-bottom: 4px;
+ transition: color 0.3s ease;
+}
+
+.recommendations-list .recommendation-item a:hover {
+ color: #007cba;
+}
+
+.recommendations-list .recommendation-meta {
+ font-size: 11px;
+ color: #999;
+ margin: 0;
+}
+
+.recommendations-list .recommendation-meta span {
+ margin-right: 10px;
+}
+
+/* 响应式设计 - 分栏显示 */
+@media (min-width: 768px) {
+ .recommendations-grid {
+ grid-template-columns: repeat(2, 1fr);
+ gap: 15px;
+ }
+}
+
+@media (min-width: 1024px) {
+ .recommendations-grid {
+ grid-template-columns: repeat(3, 1fr);
+ gap: 15px;
+ }
+}
+
+@media (min-width: 1200px) {
+ .recommendations-grid {
+ grid-template-columns: repeat(4, 1fr);
+ gap: 15px;
+ }
+}
diff --git a/src/plugins/article_recommendation/static/article_recommendation/js/recommendation.js b/src/plugins/article_recommendation/static/article_recommendation/js/recommendation.js
new file mode 100644
index 0000000..eb19211
--- /dev/null
+++ b/src/plugins/article_recommendation/static/article_recommendation/js/recommendation.js
@@ -0,0 +1,93 @@
+/**
+ * 文章推荐插件JavaScript
+ */
+
+(function() {
+ 'use strict';
+
+ // 等待DOM加载完成
+ document.addEventListener('DOMContentLoaded', function() {
+ initRecommendations();
+ });
+
+ function initRecommendations() {
+ // 添加点击统计
+ trackRecommendationClicks();
+
+ // 懒加载优化(如果需要)
+ lazyLoadRecommendations();
+ }
+
+ function trackRecommendationClicks() {
+ const recommendationLinks = document.querySelectorAll('.recommendation-item a');
+
+ recommendationLinks.forEach(function(link) {
+ link.addEventListener('click', function(e) {
+ // 可以在这里添加点击统计逻辑
+ const articleTitle = this.textContent.trim();
+ const articleUrl = this.href;
+
+ // 发送统计数据到后端(可选)
+ if (typeof gtag !== 'undefined') {
+ gtag('event', 'click', {
+ 'event_category': 'recommendation',
+ 'event_label': articleTitle,
+ 'value': 1
+ });
+ }
+
+ console.log('Recommendation clicked:', articleTitle, articleUrl);
+ });
+ });
+ }
+
+ function lazyLoadRecommendations() {
+ // 如果推荐内容很多,可以实现懒加载
+ const recommendationContainer = document.querySelector('.article-recommendations');
+
+ if (!recommendationContainer) {
+ return;
+ }
+
+ // 检查是否在视窗中
+ const observer = new IntersectionObserver(function(entries) {
+ entries.forEach(function(entry) {
+ if (entry.isIntersecting) {
+ entry.target.classList.add('loaded');
+ observer.unobserve(entry.target);
+ }
+ });
+ }, {
+ threshold: 0.1
+ });
+
+ const recommendationItems = document.querySelectorAll('.recommendation-item');
+ recommendationItems.forEach(function(item) {
+ observer.observe(item);
+ });
+ }
+
+ // 添加一些动画效果
+ function addAnimations() {
+ const recommendationItems = document.querySelectorAll('.recommendation-item');
+
+ recommendationItems.forEach(function(item, index) {
+ item.style.opacity = '0';
+ item.style.transform = 'translateY(20px)';
+ item.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
+
+ setTimeout(function() {
+ item.style.opacity = '1';
+ item.style.transform = 'translateY(0)';
+ }, index * 100);
+ });
+ }
+
+ // 如果需要,可以在这里添加更多功能
+ window.ArticleRecommendation = {
+ init: initRecommendations,
+ track: trackRecommendationClicks,
+ animate: addAnimations
+ };
+
+})();
diff --git a/src/plugins/external_links/__pycache__/__init__.cpython-311.pyc b/src/plugins/external_links/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..7774058
Binary files /dev/null and b/src/plugins/external_links/__pycache__/__init__.cpython-311.pyc differ
diff --git a/src/plugins/external_links/__pycache__/plugin.cpython-311.pyc b/src/plugins/external_links/__pycache__/plugin.cpython-311.pyc
new file mode 100644
index 0000000..24621a8
Binary files /dev/null and b/src/plugins/external_links/__pycache__/plugin.cpython-311.pyc differ
diff --git a/src/plugins/image_lazy_loading/__init__.py b/src/plugins/image_lazy_loading/__init__.py
new file mode 100644
index 0000000..2d27de0
--- /dev/null
+++ b/src/plugins/image_lazy_loading/__init__.py
@@ -0,0 +1 @@
+# Image Lazy Loading Plugin
diff --git a/src/plugins/image_lazy_loading/__pycache__/__init__.cpython-311.pyc b/src/plugins/image_lazy_loading/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..2b3b821
Binary files /dev/null and b/src/plugins/image_lazy_loading/__pycache__/__init__.cpython-311.pyc differ
diff --git a/src/plugins/image_lazy_loading/__pycache__/plugin.cpython-311.pyc b/src/plugins/image_lazy_loading/__pycache__/plugin.cpython-311.pyc
new file mode 100644
index 0000000..cc6c705
Binary files /dev/null and b/src/plugins/image_lazy_loading/__pycache__/plugin.cpython-311.pyc differ
diff --git a/src/plugins/image_lazy_loading/plugin.py b/src/plugins/image_lazy_loading/plugin.py
new file mode 100644
index 0000000..b4b9e0a
--- /dev/null
+++ b/src/plugins/image_lazy_loading/plugin.py
@@ -0,0 +1,182 @@
+import re
+import hashlib
+from urllib.parse import urlparse
+from djangoblog.plugin_manage.base_plugin import BasePlugin
+from djangoblog.plugin_manage import hooks
+from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
+
+
+class ImageOptimizationPlugin(BasePlugin):
+ PLUGIN_NAME = '图片性能优化插件'
+ PLUGIN_DESCRIPTION = '自动为文章中的图片添加懒加载、异步解码等性能优化属性,显著提升页面加载速度。'
+ PLUGIN_VERSION = '1.0.0'
+ PLUGIN_AUTHOR = 'liangliangyy'
+
+ def __init__(self):
+ # 插件配置
+ self.config = {
+ 'enable_lazy_loading': True, # 启用懒加载
+ 'enable_async_decoding': True, # 启用异步解码
+ 'add_loading_placeholder': True, # 添加加载占位符
+ 'optimize_external_images': True, # 优化外部图片
+ 'add_responsive_attributes': True, # 添加响应式属性
+ 'skip_first_image': True, # 跳过第一张图片(LCP优化)
+ }
+ super().__init__()
+
+ def register_hooks(self):
+ hooks.register(ARTICLE_CONTENT_HOOK_NAME, self.optimize_images)
+
+ def optimize_images(self, content, *args, **kwargs):
+ """
+ 优化文章中的图片标签
+ """
+ if not content:
+ return content
+
+ # 正则表达式匹配 img 标签
+ img_pattern = re.compile(
+ r' ]*?)(?:\s*/)?>',
+ re.IGNORECASE | re.DOTALL
+ )
+
+ image_count = 0
+
+ def replace_img_tag(match):
+ nonlocal image_count
+ image_count += 1
+
+ # 获取原始属性
+ original_attrs = match.group(1)
+
+ # 解析现有属性
+ attrs = self._parse_img_attributes(original_attrs)
+
+ # 应用优化
+ optimized_attrs = self._apply_optimizations(attrs, image_count)
+
+ # 重构 img 标签
+ return self._build_img_tag(optimized_attrs)
+
+ # 替换所有 img 标签
+ optimized_content = img_pattern.sub(replace_img_tag, content)
+
+ return optimized_content
+
+ def _parse_img_attributes(self, attr_string):
+ """
+ 解析 img 标签的属性
+ """
+ attrs = {}
+
+ # 正则表达式匹配属性
+ attr_pattern = re.compile(r'(\w+)=(["\'])(.*?)\2')
+
+ for match in attr_pattern.finditer(attr_string):
+ attr_name = match.group(1).lower()
+ attr_value = match.group(3)
+ attrs[attr_name] = attr_value
+
+ return attrs
+
+ def _apply_optimizations(self, attrs, image_index):
+ """
+ 应用各种图片优化
+ """
+ # 1. 懒加载优化(跳过第一张图片以优化LCP)
+ if self.config['enable_lazy_loading']:
+ if not (self.config['skip_first_image'] and image_index == 1):
+ if 'loading' not in attrs:
+ attrs['loading'] = 'lazy'
+
+ # 2. 异步解码
+ if self.config['enable_async_decoding']:
+ if 'decoding' not in attrs:
+ attrs['decoding'] = 'async'
+
+ # 3. 添加样式优化
+ current_style = attrs.get('style', '')
+
+ # 确保图片不会超出容器
+ if 'max-width' not in current_style:
+ if current_style and not current_style.endswith(';'):
+ current_style += ';'
+ current_style += 'max-width:100%;height:auto;'
+ attrs['style'] = current_style
+
+ # 4. 添加 alt 属性(SEO和可访问性)
+ if 'alt' not in attrs:
+ # 尝试从图片URL生成有意义的alt文本
+ src = attrs.get('src', '')
+ if src:
+ # 从文件名生成alt文本
+ filename = src.split('/')[-1].split('.')[0]
+ # 移除常见的无意义字符
+ clean_name = re.sub(r'[0-9a-f]{8,}', '', filename) # 移除长hash
+ clean_name = re.sub(r'[_-]+', ' ', clean_name).strip()
+ attrs['alt'] = clean_name if clean_name else '文章图片'
+ else:
+ attrs['alt'] = '文章图片'
+
+ # 5. 外部图片优化
+ if self.config['optimize_external_images'] and 'src' in attrs:
+ src = attrs['src']
+ parsed_url = urlparse(src)
+
+ # 如果是外部图片,添加 referrerpolicy
+ if parsed_url.netloc and parsed_url.netloc != self._get_current_domain():
+ attrs['referrerpolicy'] = 'no-referrer-when-downgrade'
+ # 为外部图片添加crossorigin属性以支持性能监控
+ if 'crossorigin' not in attrs:
+ attrs['crossorigin'] = 'anonymous'
+
+ # 6. 响应式图片属性(如果配置启用)
+ if self.config['add_responsive_attributes']:
+ # 添加 sizes 属性(如果没有的话)
+ if 'sizes' not in attrs and 'srcset' not in attrs:
+ attrs['sizes'] = '(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw'
+
+ # 7. 添加图片唯一标识符用于性能追踪
+ if 'data-img-id' not in attrs and 'src' in attrs:
+ img_hash = hashlib.md5(attrs['src'].encode()).hexdigest()[:8]
+ attrs['data-img-id'] = f'img-{img_hash}'
+
+ # 8. 为第一张图片添加高优先级提示(LCP优化)
+ if image_index == 1 and self.config['skip_first_image']:
+ attrs['fetchpriority'] = 'high'
+ # 移除懒加载以确保快速加载
+ if 'loading' in attrs:
+ del attrs['loading']
+
+ return attrs
+
+ def _build_img_tag(self, attrs):
+ """
+ 重新构建 img 标签
+ """
+ attr_strings = []
+
+ # 确保 src 属性在最前面
+ if 'src' in attrs:
+ attr_strings.append(f'src="{attrs["src"]}"')
+
+ # 添加其他属性
+ for key, value in attrs.items():
+ if key != 'src': # src 已经添加过了
+ attr_strings.append(f'{key}="{value}"')
+
+ return f' '
+
+ def _get_current_domain(self):
+ """
+ 获取当前网站域名
+ """
+ try:
+ from djangoblog.utils import get_current_site
+ return get_current_site().domain
+ except:
+ return ''
+
+
+# 实例化插件
+plugin = ImageOptimizationPlugin()
diff --git a/src/plugins/reading_time/__pycache__/__init__.cpython-311.pyc b/src/plugins/reading_time/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..d949e38
Binary files /dev/null and b/src/plugins/reading_time/__pycache__/__init__.cpython-311.pyc differ
diff --git a/src/plugins/reading_time/__pycache__/plugin.cpython-311.pyc b/src/plugins/reading_time/__pycache__/plugin.cpython-311.pyc
new file mode 100644
index 0000000..c369c34
Binary files /dev/null and b/src/plugins/reading_time/__pycache__/plugin.cpython-311.pyc differ
diff --git a/src/plugins/reading_time/plugin.py b/src/plugins/reading_time/plugin.py
index 35f9db1..4b929d8 100644
--- a/src/plugins/reading_time/plugin.py
+++ b/src/plugins/reading_time/plugin.py
@@ -17,7 +17,15 @@ class ReadingTimePlugin(BasePlugin):
def add_reading_time(self, content, *args, **kwargs):
"""
计算阅读时间并添加到内容开头。
+ 只在文章详情页显示,首页(文章列表页)不显示。
"""
+ # 检查是否为摘要模式(首页/文章列表页)
+ # 通过kwargs中的is_summary参数判断
+ is_summary = kwargs.get('is_summary', False)
+ if is_summary:
+ # 如果是摘要模式(首页),直接返回原内容,不添加阅读时间
+ return content
+
# 移除HTML标签和空白字符,以获得纯文本
clean_content = re.sub(r'<[^>]*>', '', content)
clean_content = clean_content.strip()
diff --git a/src/plugins/seo_optimizer/__pycache__/__init__.cpython-311.pyc b/src/plugins/seo_optimizer/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..d5520ce
Binary files /dev/null and b/src/plugins/seo_optimizer/__pycache__/__init__.cpython-311.pyc differ
diff --git a/src/plugins/seo_optimizer/__pycache__/plugin.cpython-311.pyc b/src/plugins/seo_optimizer/__pycache__/plugin.cpython-311.pyc
new file mode 100644
index 0000000..033a766
Binary files /dev/null and b/src/plugins/seo_optimizer/__pycache__/plugin.cpython-311.pyc differ
diff --git a/src/plugins/seo_optimizer/plugin.py b/src/plugins/seo_optimizer/plugin.py
index b5b19a3..de12c15 100644
--- a/src/plugins/seo_optimizer/plugin.py
+++ b/src/plugins/seo_optimizer/plugin.py
@@ -97,6 +97,8 @@ class SeoOptimizerPlugin(BasePlugin):
structured_data = {
"@context": "https://schema.org",
"@type": "WebSite",
+ "name": blog_setting.site_name,
+ "description": blog_setting.site_description,
"url": request.build_absolute_uri('/'),
"potentialAction": {
"@type": "SearchAction",
@@ -131,12 +133,15 @@ class SeoOptimizerPlugin(BasePlugin):
json_ld_script = f''
- return f"""
+ seo_html = f"""
{seo_data.get("title", "")}
{seo_data.get("meta_tags", "")}
{json_ld_script}
"""
+
+ # 将SEO内容追加到现有的metas内容上
+ return metas + seo_html
plugin = SeoOptimizerPlugin()
diff --git a/src/plugins/view_count/__pycache__/__init__.cpython-311.pyc b/src/plugins/view_count/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..55e06f8
Binary files /dev/null and b/src/plugins/view_count/__pycache__/__init__.cpython-311.pyc differ
diff --git a/src/plugins/view_count/__pycache__/plugin.cpython-311.pyc b/src/plugins/view_count/__pycache__/plugin.cpython-311.pyc
new file mode 100644
index 0000000..c6004a3
Binary files /dev/null and b/src/plugins/view_count/__pycache__/plugin.cpython-311.pyc differ
diff --git a/src/requirements.txt b/src/requirements.txt
index 70457c3..e5878ab 100644
Binary files a/src/requirements.txt and b/src/requirements.txt differ
diff --git a/src/servermanager/__pycache__/MemcacheStorage.cpython-311.pyc b/src/servermanager/__pycache__/MemcacheStorage.cpython-311.pyc
new file mode 100644
index 0000000..186dc67
Binary files /dev/null and b/src/servermanager/__pycache__/MemcacheStorage.cpython-311.pyc differ
diff --git a/src/servermanager/__pycache__/__init__.cpython-311.pyc b/src/servermanager/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..c2ae48a
Binary files /dev/null and b/src/servermanager/__pycache__/__init__.cpython-311.pyc differ
diff --git a/src/servermanager/__pycache__/admin.cpython-311.pyc b/src/servermanager/__pycache__/admin.cpython-311.pyc
new file mode 100644
index 0000000..6ab6be3
Binary files /dev/null and b/src/servermanager/__pycache__/admin.cpython-311.pyc differ
diff --git a/src/servermanager/__pycache__/apps.cpython-311.pyc b/src/servermanager/__pycache__/apps.cpython-311.pyc
new file mode 100644
index 0000000..8e6b49f
Binary files /dev/null and b/src/servermanager/__pycache__/apps.cpython-311.pyc differ
diff --git a/src/servermanager/__pycache__/models.cpython-311.pyc b/src/servermanager/__pycache__/models.cpython-311.pyc
new file mode 100644
index 0000000..f7a98bb
Binary files /dev/null and b/src/servermanager/__pycache__/models.cpython-311.pyc differ
diff --git a/src/servermanager/__pycache__/robot.cpython-311.pyc b/src/servermanager/__pycache__/robot.cpython-311.pyc
new file mode 100644
index 0000000..d553a7a
Binary files /dev/null and b/src/servermanager/__pycache__/robot.cpython-311.pyc differ
diff --git a/src/servermanager/__pycache__/urls.cpython-311.pyc b/src/servermanager/__pycache__/urls.cpython-311.pyc
new file mode 100644
index 0000000..a0b0ac1
Binary files /dev/null and b/src/servermanager/__pycache__/urls.cpython-311.pyc differ
diff --git a/src/servermanager/api/__pycache__/__init__.cpython-311.pyc b/src/servermanager/api/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..f6abc9c
Binary files /dev/null and b/src/servermanager/api/__pycache__/__init__.cpython-311.pyc differ
diff --git a/src/servermanager/api/__pycache__/blogapi.cpython-311.pyc b/src/servermanager/api/__pycache__/blogapi.cpython-311.pyc
new file mode 100644
index 0000000..94f4e58
Binary files /dev/null and b/src/servermanager/api/__pycache__/blogapi.cpython-311.pyc differ
diff --git a/src/servermanager/api/__pycache__/commonapi.cpython-311.pyc b/src/servermanager/api/__pycache__/commonapi.cpython-311.pyc
new file mode 100644
index 0000000..53ab6a1
Binary files /dev/null and b/src/servermanager/api/__pycache__/commonapi.cpython-311.pyc differ
diff --git a/src/servermanager/migrations/__pycache__/0001_initial.cpython-311.pyc b/src/servermanager/migrations/__pycache__/0001_initial.cpython-311.pyc
new file mode 100644
index 0000000..aa5b32a
Binary files /dev/null and b/src/servermanager/migrations/__pycache__/0001_initial.cpython-311.pyc differ
diff --git a/src/servermanager/migrations/__pycache__/0002_alter_emailsendlog_options_and_more.cpython-311.pyc b/src/servermanager/migrations/__pycache__/0002_alter_emailsendlog_options_and_more.cpython-311.pyc
new file mode 100644
index 0000000..7e09878
Binary files /dev/null and b/src/servermanager/migrations/__pycache__/0002_alter_emailsendlog_options_and_more.cpython-311.pyc differ
diff --git a/src/servermanager/migrations/__pycache__/__init__.cpython-311.pyc b/src/servermanager/migrations/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..086558b
Binary files /dev/null and b/src/servermanager/migrations/__pycache__/__init__.cpython-311.pyc differ
diff --git a/src/templates/blog/article_detail.html b/src/templates/blog/article_detail.html
index f694db3..a74a0db 100644
--- a/src/templates/blog/article_detail.html
+++ b/src/templates/blog/article_detail.html
@@ -2,30 +2,6 @@
{% load blog_tags %}
{% block header %}
- {{ article.title }} | {{ SITE_DESCRIPTION }}
-
-
-
-
-
-
-
-
-
-
- {% for t in article.tags.all %}
-
- {% endfor %}
-
-
-
- {% if article.tags %}
-
- {% else %}
-
- {% endif %}
-
{% endblock %}
{% block content %}
diff --git a/src/templates/blog/tags/article_info.html b/src/templates/blog/tags/article_info.html
index 3deec44..65b45fa 100644
--- a/src/templates/blog/tags/article_info.html
+++ b/src/templates/blog/tags/article_info.html
@@ -48,7 +48,7 @@
{% if isindex %}
- {{ article.body|custom_markdown|escape|truncatechars_content }}
+ {% render_article_content article True %}
Read more
{% else %}
@@ -62,7 +62,7 @@
{% endif %}
- {{ article.body|custom_markdown|escape }}
+ {% render_article_content article False %}
{% endif %}
@@ -71,4 +71,9 @@
{% load_article_metas article user %}
-
\ No newline at end of file
+
+
+
+{% if not isindex %}
+ {% render_plugin_widgets 'article_bottom' article=article %}
+{% endif %}
\ No newline at end of file
diff --git a/src/templates/blog/tags/article_meta_info.html b/src/templates/blog/tags/article_meta_info.html
index cb6111c..ec8a0f9 100644
--- a/src/templates/blog/tags/article_meta_info.html
+++ b/src/templates/blog/tags/article_meta_info.html
@@ -5,9 +5,6 @@
{% trans 'posted in' %}
{{ article.category.name }}
-
-
-
{% if article.type == 'a' %}
{% if article.tags.all %}
@@ -46,13 +43,14 @@
title="{% datetimeformat article.pub_time %}"
itemprop="datePublished" content="{% datetimeformat article.pub_time %}"
rel="bookmark">
-
-
- {% datetimeformat article.pub_time %}
- {% if user.is_superuser %}
- {% trans 'edit' %}
- {% endif %}
+
+ {% datetimeformat article.pub_time %}
+
+
+ {% if user.is_superuser %}
+ {% trans 'edit' %}
+ {% endif %}
diff --git a/src/templates/blog/tags/sidebar.html b/src/templates/blog/tags/sidebar.html
index f70544c..ecb6d20 100644
--- a/src/templates/blog/tags/sidebar.html
+++ b/src/templates/blog/tags/sidebar.html
@@ -16,7 +16,7 @@
{% endfor %}
diff --git a/src/templates/comments/tags/comment_item.html b/src/templates/comments/tags/comment_item.html
index ebb0388..0693649 100644
--- a/src/templates/comments/tags/comment_item.html
+++ b/src/templates/comments/tags/comment_item.html
@@ -2,10 +2,13 @@
+ 📖{{ title }} +
+{{ SITE_DESCRIPTION }}
{% load i18n %} - - -{#