commit
d0dd599db3
@ -0,0 +1 @@
|
||||
default_app_config = 'djangoblog.apps.DjangoblogAppConfig'
|
||||
@ -0,0 +1,11 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
class DjangoblogAppConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'djangoblog'
|
||||
|
||||
def ready(self):
|
||||
super().ready()
|
||||
# Import and load plugins here
|
||||
from .plugin_manage.loader import load_plugins
|
||||
load_plugins()
|
||||
@ -0,0 +1,19 @@
|
||||
import os
|
||||
import logging
|
||||
from django.conf import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
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.
|
||||
"""
|
||||
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}")
|
||||
except ImportError as e:
|
||||
logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)
|
||||
@ -0,0 +1,114 @@
|
||||
# Deploying DjangoBlog with Docker
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
This project fully supports containerized deployment using Docker, providing you with a fast, consistent, and isolated runtime environment. We recommend using `docker-compose` to launch the entire blog service stack with a single command.
|
||||
|
||||
## 1. Prerequisites
|
||||
|
||||
Before you begin, please ensure you have the following software installed on your system:
|
||||
- [Docker Engine](https://docs.docker.com/engine/install/)
|
||||
- [Docker Compose](https://docs.docker.com/compose/install/) (Included with Docker Desktop for Mac and Windows)
|
||||
|
||||
## 2. Recommended Method: Using `docker-compose` (One-Click Deployment)
|
||||
|
||||
This is the simplest and most recommended way to deploy. It automatically creates and manages the Django application, a MySQL database, and an optional Elasticsearch service for you.
|
||||
|
||||
### Step 1: Start the Basic Services
|
||||
|
||||
From the project's root directory, run the following command:
|
||||
|
||||
```bash
|
||||
# Build and start the containers in detached mode (includes Django app and MySQL)
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
`docker-compose` will read the `docker-compose.yml` file, pull the necessary images, build the project image, and start all services.
|
||||
|
||||
- **Access Your Blog**: Once the services are up, you can access the blog by navigating to `http://127.0.0.1` in your browser.
|
||||
- **Data Persistence**: MySQL data files will be stored in the `data/mysql` directory within the project root, ensuring that your data persists across container restarts.
|
||||
|
||||
### Step 2: (Optional) Enable Elasticsearch for Full-Text Search
|
||||
|
||||
If you want to use Elasticsearch for more powerful full-text search capabilities, you can include the `docker-compose.es.yml` configuration file:
|
||||
|
||||
```bash
|
||||
# Build and start all services in detached mode (Django, MySQL, Elasticsearch)
|
||||
docker-compose -f docker-compose.yml -f deploy/docker-compose/docker-compose.es.yml up -d --build
|
||||
```
|
||||
- **Data Persistence**: Elasticsearch data will be stored in the `data/elasticsearch` directory.
|
||||
|
||||
### Step 3: First-Time Initialization
|
||||
|
||||
After the containers start for the first time, you'll need to execute some initialization commands inside the application container.
|
||||
|
||||
```bash
|
||||
# Get a shell inside the djangoblog application container (named 'web')
|
||||
docker-compose exec web bash
|
||||
|
||||
# Inside the container, run the following commands:
|
||||
# Create a superuser account (follow the prompts to set username, email, and password)
|
||||
python manage.py createsuperuser
|
||||
|
||||
# (Optional) Create some test data
|
||||
python manage.py create_testdata
|
||||
|
||||
# (Optional, if ES is enabled) Create the search index
|
||||
python manage.py rebuild_index
|
||||
|
||||
# Exit the container
|
||||
exit
|
||||
```
|
||||
|
||||
## 3. Alternative Method: Using the Standalone Docker Image
|
||||
|
||||
If you already have an external MySQL database running, you can run the DjangoBlog application image by itself.
|
||||
|
||||
```bash
|
||||
# Pull the latest image from Docker Hub
|
||||
docker pull liangliangyy/djangoblog:latest
|
||||
|
||||
# Run the container and connect it to your external database
|
||||
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
|
||||
```
|
||||
|
||||
- **Access Your Blog**: After startup, visit `http://127.0.0.1:8000`.
|
||||
- **Create Superuser**: `docker exec -it djangoblog python manage.py createsuperuser`
|
||||
|
||||
## 4. Configuration (Environment Variables)
|
||||
|
||||
Most of the project's configuration is managed through environment variables. You can modify them in the `docker-compose.yml` file or pass them using the `-e` flag with the `docker run` command.
|
||||
|
||||
| Environment Variable | Default/Example Value | Notes |
|
||||
|---------------------------|--------------------------------------------------------------------------|---------------------------------------------------------------------|
|
||||
| `DJANGO_SECRET_KEY` | `your-strong-secret-key` | **Must be changed to a random, complex string!** |
|
||||
| `DJANGO_DEBUG` | `False` | Toggles Django's debug mode. |
|
||||
| `DJANGO_MYSQL_HOST` | `mysql` | Database hostname. |
|
||||
| `DJANGO_MYSQL_PORT` | `3306` | Database port. |
|
||||
| `DJANGO_MYSQL_DATABASE` | `djangoblog` | Database name. |
|
||||
| `DJANGO_MYSQL_USER` | `root` | Database username. |
|
||||
| `DJANGO_MYSQL_PASSWORD` | `djangoblog_123` | Database password. |
|
||||
| `DJANGO_REDIS_URL` | `redis:6379/0` | Redis connection URL (for caching). |
|
||||
| `DJANGO_ELASTICSEARCH_HOST`| `elasticsearch:9200` | Elasticsearch host address. |
|
||||
| `DJANGO_EMAIL_HOST` | `smtp.example.org` | Email server address. |
|
||||
| `DJANGO_EMAIL_PORT` | `465` | Email server port. |
|
||||
| `DJANGO_EMAIL_USER` | `user@example.org` | Email account username. |
|
||||
| `DJANGO_EMAIL_PASSWORD` | `your-email-password` | Email account password. |
|
||||
| `DJANGO_EMAIL_USE_SSL` | `True` | Whether to use SSL. |
|
||||
| `DJANGO_EMAIL_USE_TLS` | `False` | Whether to use TLS. |
|
||||
| `DJANGO_ADMIN_EMAIL` | `admin@example.org` | Admin email for receiving error reports. |
|
||||
| `DJANGO_BAIDU_NOTIFY_URL` | `http://data.zz.baidu.com/...` | Push API from [Baidu Webmaster Tools](https://ziyuan.baidu.com/linksubmit/index). |
|
||||
|
||||
---
|
||||
|
||||
After deployment, please review and adjust these environment variables according to your needs, especially `DJANGO_SECRET_KEY` and the database and email settings.
|
||||
@ -0,0 +1,141 @@
|
||||
# Deploying DjangoBlog with Kubernetes
|
||||
|
||||
This document guides you through deploying the DjangoBlog application on a Kubernetes (K8s) cluster. We provide a complete set of `.yaml` configuration files in the `deploy/k8s` directory to deploy a full service stack, including the DjangoBlog application, Nginx, MySQL, Redis, and Elasticsearch.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
This deployment utilizes a microservices-based, cloud-native architecture:
|
||||
|
||||
- **Core Components**: Each core service (DjangoBlog, Nginx, MySQL, Redis, Elasticsearch) runs as a separate `Deployment`.
|
||||
- **Configuration Management**: Nginx configurations and Django application environment variables are managed via `ConfigMap`. **Note: For sensitive information like passwords, using `Secret` is highly recommended.**
|
||||
- **Service Discovery**: All services are exposed internally within the cluster as `ClusterIP` type `Service`, enabling communication via service names.
|
||||
- **External Access**: An `Ingress` resource is used to route external HTTP traffic to the Nginx service, which acts as the single entry point for the entire blog application.
|
||||
- **Data Persistence**: A `local-storage` solution based on node-local paths is used. This requires you to manually create storage directories on a specific K8s node and statically bind them using `PersistentVolume` (PV) and `PersistentVolumeClaim` (PVC).
|
||||
|
||||
## 1. Prerequisites
|
||||
|
||||
Before you begin, please ensure you have the following:
|
||||
|
||||
- A running Kubernetes cluster.
|
||||
- The `kubectl` command-line tool configured to connect to your cluster.
|
||||
- An [Nginx Ingress Controller](https://kubernetes.github.io/ingress-nginx/deploy/) installed and configured in your cluster.
|
||||
- Filesystem access to one of the nodes in your cluster (defaulted to `master` in the configs) to create local storage directories.
|
||||
|
||||
## 2. Deployment Steps
|
||||
|
||||
### Step 1: Create a Namespace
|
||||
|
||||
We recommend deploying all DjangoBlog-related resources in a dedicated namespace for better management.
|
||||
|
||||
```bash
|
||||
# Create a namespace named 'djangoblog'
|
||||
kubectl create namespace djangoblog
|
||||
```
|
||||
|
||||
### Step 2: Configure Persistent Storage
|
||||
|
||||
This setup uses Local Persistent Volumes. You need to create the data storage directories on a node within your cluster (the default is the `master` node in `pv.yaml`).
|
||||
|
||||
```bash
|
||||
# Log in to your master node
|
||||
ssh user@master-node
|
||||
|
||||
# Create the required storage directories
|
||||
sudo mkdir -p /mnt/local-storage-db
|
||||
sudo mkdir -p /mnt/local-storage-djangoblog
|
||||
sudo mkdir -p /mnt/resource/
|
||||
sudo mkdir -p /mnt/local-storage-elasticsearch
|
||||
|
||||
# Log out from the node
|
||||
exit
|
||||
```
|
||||
**Note**: If you wish to store data on a different node or use different paths, you must modify the `nodeAffinity` and `local.path` settings in the `deploy/k8s/pv.yaml` file.
|
||||
|
||||
After creating the directories, apply the storage-related configurations:
|
||||
|
||||
```bash
|
||||
# Apply the StorageClass
|
||||
kubectl apply -f deploy/k8s/storageclass.yaml
|
||||
|
||||
# Apply the PersistentVolumes (PVs)
|
||||
kubectl apply -f deploy/k8s/pv.yaml
|
||||
|
||||
# Apply the PersistentVolumeClaims (PVCs)
|
||||
kubectl apply -f deploy/k8s/pvc.yaml
|
||||
```
|
||||
|
||||
### Step 3: Configure the Application
|
||||
|
||||
Before deploying the application, you need to edit the `deploy/k8s/configmap.yaml` file to modify sensitive information and custom settings.
|
||||
|
||||
**It is strongly recommended to change the following fields:**
|
||||
- `DJANGO_SECRET_KEY`: Change to a random, complex string.
|
||||
- `DJANGO_MYSQL_PASSWORD` and `MYSQL_ROOT_PASSWORD`: Change to your own secure database password.
|
||||
|
||||
```bash
|
||||
# Edit the ConfigMap file
|
||||
vim deploy/k8s/configmap.yaml
|
||||
|
||||
# Apply the configuration
|
||||
kubectl apply -f deploy/k8s/configmap.yaml
|
||||
```
|
||||
|
||||
### Step 4: Deploy the Application Stack
|
||||
|
||||
Now, we can deploy all the core services.
|
||||
|
||||
```bash
|
||||
# Deploy the Deployments (DjangoBlog, MySQL, Redis, Nginx, ES)
|
||||
kubectl apply -f deploy/k8s/deployment.yaml
|
||||
|
||||
# Deploy the Services (to create internal endpoints for the Deployments)
|
||||
kubectl apply -f deploy/k8s/service.yaml
|
||||
```
|
||||
|
||||
The deployment may take some time. You can run the following command to check if all Pods are running successfully (STATUS should be `Running`):
|
||||
|
||||
```bash
|
||||
kubectl get pods -n djangoblog -w
|
||||
```
|
||||
|
||||
### Step 5: Expose the Application Externally
|
||||
|
||||
Finally, expose the Nginx service to external traffic by applying the `Ingress` rule.
|
||||
|
||||
```bash
|
||||
# Apply the Ingress rule
|
||||
kubectl apply -f deploy/k8s/gateway.yaml
|
||||
```
|
||||
|
||||
Once deployed, you can access your blog via the external IP address of your Ingress Controller. Use the following command to find the address:
|
||||
|
||||
```bash
|
||||
kubectl get ingress -n djangoblog
|
||||
```
|
||||
|
||||
### Step 6: First-Time Initialization
|
||||
|
||||
Similar to the Docker deployment, you need to get a shell into the DjangoBlog application Pod to perform database initialization and create a superuser on the first run.
|
||||
|
||||
```bash
|
||||
# First, get the name of a djangoblog pod
|
||||
kubectl get pods -n djangoblog | grep djangoblog
|
||||
|
||||
# Exec into one of the Pods (replace [pod-name] with the name from the previous step)
|
||||
kubectl exec -it [pod-name] -n djangoblog -- bash
|
||||
|
||||
# Inside the Pod, run the following commands:
|
||||
# Create a superuser account (follow the prompts)
|
||||
python manage.py createsuperuser
|
||||
|
||||
# (Optional) Create some test data
|
||||
python manage.py create_testdata
|
||||
|
||||
# (Optional, if ES is enabled) Create the search index
|
||||
python manage.py rebuild_index
|
||||
|
||||
# Exit the Pod
|
||||
exit
|
||||
```
|
||||
|
||||
Congratulations! You have successfully deployed DjangoBlog on your Kubernetes cluster.
|
||||
@ -0,0 +1 @@
|
||||
# This file makes this a Python package
|
||||
@ -0,0 +1,142 @@
|
||||
import json
|
||||
from django.utils.html import strip_tags
|
||||
from django.template.defaultfilters import truncatewords
|
||||
from djangoblog.plugin_manage.base_plugin import BasePlugin
|
||||
from djangoblog.plugin_manage import hooks
|
||||
from blog.models import Article, Category, Tag
|
||||
from djangoblog.utils import get_blog_setting
|
||||
|
||||
|
||||
class SeoOptimizerPlugin(BasePlugin):
|
||||
PLUGIN_NAME = 'SEO 优化器'
|
||||
PLUGIN_DESCRIPTION = '为文章、页面等提供 SEO 优化,动态生成 meta 标签和 JSON-LD 结构化数据。'
|
||||
PLUGIN_VERSION = '0.2.0'
|
||||
PLUGIN_AUTHOR = 'Gemini'
|
||||
|
||||
def register_hooks(self):
|
||||
hooks.register('head_meta', self.dispatch_seo_generation)
|
||||
|
||||
def _get_article_seo_data(self, context, request, blog_setting):
|
||||
article = context.get('article')
|
||||
if not isinstance(article, Article):
|
||||
return None
|
||||
|
||||
description = strip_tags(article.body)[:150]
|
||||
keywords = ",".join([tag.name for tag in article.tags.all()]) or blog_setting.site_keywords
|
||||
|
||||
meta_tags = f'''
|
||||
<meta property="og:type" content="article"/>
|
||||
<meta property="og:title" content="{article.title}"/>
|
||||
<meta property="og:description" content="{description}"/>
|
||||
<meta property="og:url" content="{request.build_absolute_uri()}"/>
|
||||
<meta property="article:published_time" content="{article.pub_time.isoformat()}"/>
|
||||
<meta property="article:modified_time" content="{article.last_modify_time.isoformat()}"/>
|
||||
<meta property="article:author" content="{article.author.username}"/>
|
||||
<meta property="article:section" content="{article.category.name}"/>
|
||||
'''
|
||||
for tag in article.tags.all():
|
||||
meta_tags += f'<meta property="article:tag" content="{tag.name}"/>'
|
||||
meta_tags += f'<meta property="og:site_name" content="{blog_setting.site_name}"/>'
|
||||
|
||||
structured_data = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
"mainEntityOfPage": {"@type": "WebPage", "@id": request.build_absolute_uri()},
|
||||
"headline": article.title,
|
||||
"description": description,
|
||||
"image": request.build_absolute_uri(article.get_first_image_url()),
|
||||
"datePublished": article.pub_time.isoformat(),
|
||||
"dateModified": article.last_modify_time.isoformat(),
|
||||
"author": {"@type": "Person", "name": article.author.username},
|
||||
"publisher": {"@type": "Organization", "name": blog_setting.site_name}
|
||||
}
|
||||
if not structured_data.get("image"):
|
||||
del structured_data["image"]
|
||||
|
||||
return {
|
||||
"title": f"{article.title} | {blog_setting.site_name}",
|
||||
"description": description,
|
||||
"keywords": keywords,
|
||||
"meta_tags": meta_tags,
|
||||
"json_ld": structured_data
|
||||
}
|
||||
|
||||
def _get_category_seo_data(self, context, request, blog_setting):
|
||||
category_name = context.get('tag_name')
|
||||
if not category_name:
|
||||
return None
|
||||
|
||||
category = Category.objects.filter(name=category_name).first()
|
||||
if not category:
|
||||
return None
|
||||
|
||||
title = f"{category.name} | {blog_setting.site_name}"
|
||||
description = strip_tags(category.name) or blog_setting.site_description
|
||||
keywords = category.name
|
||||
|
||||
# BreadcrumbList structured data for category page
|
||||
breadcrumb_items = [{"@type": "ListItem", "position": 1, "name": "首页", "item": request.build_absolute_uri('/')}]
|
||||
breadcrumb_items.append({"@type": "ListItem", "position": 2, "name": category.name, "item": request.build_absolute_uri()})
|
||||
|
||||
structured_data = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
"itemListElement": breadcrumb_items
|
||||
}
|
||||
|
||||
return {
|
||||
"title": title,
|
||||
"description": description,
|
||||
"keywords": keywords,
|
||||
"meta_tags": "",
|
||||
"json_ld": structured_data
|
||||
}
|
||||
|
||||
def _get_default_seo_data(self, context, request, blog_setting):
|
||||
# Homepage and other default pages
|
||||
structured_data = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebSite",
|
||||
"url": request.build_absolute_uri('/'),
|
||||
"potentialAction": {
|
||||
"@type": "SearchAction",
|
||||
"target": f"{request.build_absolute_uri('/search/')}?q={{search_term_string}}",
|
||||
"query-input": "required name=search_term_string"
|
||||
}
|
||||
}
|
||||
return {
|
||||
"title": f"{blog_setting.site_name} | {blog_setting.site_description}",
|
||||
"description": blog_setting.site_description,
|
||||
"keywords": blog_setting.site_keywords,
|
||||
"meta_tags": "",
|
||||
"json_ld": structured_data
|
||||
}
|
||||
|
||||
def dispatch_seo_generation(self, metas, context):
|
||||
request = context.get('request')
|
||||
if not request:
|
||||
return metas
|
||||
|
||||
view_name = request.resolver_match.view_name
|
||||
blog_setting = get_blog_setting()
|
||||
|
||||
seo_data = None
|
||||
if view_name == 'blog:detailbyid':
|
||||
seo_data = self._get_article_seo_data(context, request, blog_setting)
|
||||
elif view_name == 'blog:category_detail':
|
||||
seo_data = self._get_category_seo_data(context, request, blog_setting)
|
||||
|
||||
if not seo_data:
|
||||
seo_data = self._get_default_seo_data(context, request, blog_setting)
|
||||
|
||||
json_ld_script = f'<script type="application/ld+json">{json.dumps(seo_data.get("json_ld", {}), ensure_ascii=False, indent=4)}</script>'
|
||||
|
||||
return f"""
|
||||
<title>{seo_data.get("title", "")}</title>
|
||||
<meta name="description" content="{seo_data.get("description", "")}">
|
||||
<meta name="keywords" content="{seo_data.get("keywords", "")}">
|
||||
{seo_data.get("meta_tags", "")}
|
||||
{json_ld_script}
|
||||
"""
|
||||
|
||||
plugin = SeoOptimizerPlugin()
|
||||
Binary file not shown.
Loading…
Reference in new issue