🐍😺🐙 Как сделать блог разработчика на GitHub Pages с помощью Django
Расскажем, как превратить Django в генератор статических сайтов и сделать полноценный блог с пагинацией, сортировкой записей по тегам, подсветкой синтаксиса кода, контактной формой, подпиской на рассылку и поддержкой 20 различных тем оформления.
Обзор проекта
Это генератор статических сайтов (SSG) на Django, который использует базу данных (а не Markdown файлы) для интеграции контента в HTML/CSS/JS страницы. Результат работы генератора – полностью статический блог, который с виду работает как динамический: смена темы оформления выполняются с помощью JavaScript, а обработка форм для отправки сообщения и подписки на рассылку реализована с использованием стороннего сервиса Formspree. Такой блог можно разместить на абсолютно любом статическом хостинге, включая GitHub Pages.
Демо-версию блога можно посмотреть здесь, а код доступен в репозитории django-ssg.
Особенности GitHub Pages
GitHub Pages – статический хостинг: использовать бэкенд-логику и базы данных там нельзя. В качестве бэкенда на этой платформе по умолчанию используется SSG (генератор статических сайтов) Jekyll. Он поддерживает ограниченное количество тем, а поскольку набор допустимых плагинов тоже ограничен, кастомизация и расширение функциональности Jekyll-сайтов представляют собой серьезную задачу. Существует только одно решение этой проблемы: загружать на хостинг уже готовые HTML/CSS/JS страницы.
Генерировать такие страницы можно с помощью абсолютно любого SSG – их число растет в геометрической прогрессии, есть из чего выбрать. Однако зачастую сделать собственный генератор бывает гораздо проще, чем разбираться в настройках готового SSG. Разработка генератора – несложный и увлекательный процесс: мы уже публиковали туториал о создании SSG на основе Flask и плагина Frozen-Flask. Как и абсолютное большинство других генераторов, он создает статические страницы, используя HTML/CSS/JS шаблоны и контент из файлов Markdown.
Если же вы привыкли к Django, и вместо возни с Markdown предпочитаете экспортировать записи из базы данных, для создания SSG можно воспользоваться расширением django-distill.
Как работает django-distill
Расширение django-distill выгодно отличается от других SSG-плагинов для Django (например, django-bakery) предельной простотой использования и высокой скоростью обработки файлов. Изменения в динамическую версию приложения вносятся только в одном месте – urls.py, обычные маршруты path
заменяются «дистиллированными». Например, достаточно заменить стандартный маршрут index
на этот, и расширение сгенерирует статический файл index.html:
distill_path('', IndexView.as_view(), name='index', distill_file='index.html'),)
А для экспорта всего приложения в статическую версию нужно всего лишь выполнить команду:
python manage.py distill-local mysite --collectstatic
Ограничения django-distill
Плагин не обрабатывает URL, в которых содержатся запросы и параметры:
http://mysite.com/?page=3 http://mysite.com/blog?article_id=123&title=abc
По этой причине django-distill не будет обрабатывать динамическую пагинацию: он экспортирует лишь первую страницу index.html, но не последующие. Хотя разработчики плагина не предлагают решение этой проблемы, оно существует – рассмотрим его ниже.
Как сделать статический сайт из приложения Django
Начнем работу с создания и активации виртуального окружения:
python -m venv progblog\venv
Перейдите в директорию проекта и активируйте окружение:
venv\scripts\activate
Установите Django, а затем django-distill:
pip install django pip install django-distill
Создайте новый проект:
django-admin startproject config .
И приложение blog:
python manage.py startapp blog
Настройки проекта
В файле settings.py нужно сделать следующие настройки:
- Добавить django-distill и blog в раздел INSTALLED_APPS:
INSTALLED_APPS = [ 'django_distill', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'blog.apps.BlogConfig', ]
- Изменить язык на русский, а время – на нужный часовой пояс:
LANGUAGE_CODE = 'ru' TIME_ZONE = 'Europe/Moscow'
- Прописать пути к статическим и медийным файлам:
STATIC_URL = '/static/' STATIC_ROOT = BASE_DIR / "staticfiles" STATICFILES_DIRS = [BASE_DIR / "static"] MEDIA_URL = '/media/' MEDIA_ROOT = BASE_DIR / "media"
В urls.py проекта нужно добавить маршруты блога и обработку путей к статическим файлам и изображениям:
from django.contrib import admin from django.urls import path, include from django.conf import settings from django.conf.urls.static import static urlpatterns = [ path('admin/', admin.site.urls), path('', include('blog.urls')), ] urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
Разработка блога
Прежде всего нужно создать базу данных с помощью команды:
python manage.py migrate
На этом же этапе стоит создать админский аккаунт:
python manage.py createsuperuser
После этого можно переходить к созданию схемы базы. Создайте модели Post и Tag в файле models.py:
from django.db import models class Tag(models.Model): name = models.SlugField(max_length=25, db_index=True, unique=True, help_text='Тег') def __str__(self): return self.name class Post(models.Model): created = models.DateTimeField(auto_now_add=True, help_text='Дата создания поста') read = models.CharField(max_length=2, default='1', null=True, blank=True, help_text='Время на чтение') title = models.CharField(max_length=200, help_text='Заголовок поста') slug = models.SlugField(help_text='Слаг поста', db_index=True, unique=True, null=True, blank=True) content = models.TextField(help_text='Содержание поста') image = models.FileField(upload_to='images/', null=True, blank=True) tags = models.ManyToManyField(Tag, help_text='Теги') def __str__(self): return self.title
Подготовьте и примените миграции:
python manage.py makemigrations python manage.py migrate
Готовую заполненную базу данных можно взять в репозитории проекта.
Представления
Представления находятся в файле views.py:
- IndexView(ListView) выводит записи на главную страницу и обеспечивает пагинацию (по шесть постов для каждой страницы):
class IndexView(ListView): template_name = 'index.html' model = Post allow_empty = False ordering = '-created' context_object_name = 'posts' paginate_by = 6
- PostView(DetailView) показывает отдельные записи и предоставляет ссылки на предыдущий и последующий посты (если они существуют):
class PostView(DetailView): template_name = 'post.html' model = Post def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) post = self.object prev_post = Post.objects.filter(created__lt=post.created).order_by('-created').first() next_post = Post.objects.filter(created__gt=post.created).order_by('created').first() context['prev_post'] = prev_post context['next_post'] = next_post return context
- TagView(ListView) обеспечивает выборку постов, отмеченных определенным тегом. Эти записи также разбиваются пагинатором по 6 постов на страницу:
class TagView(ListView): template_name = 'tag.html' context_object_name = 'posts' paginate_by = 6 def get_queryset(self): return Post.objects.filter(tags__name=self.kwargs['tag']).prefetch_related('tags') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['tag'] = get_object_or_404(Tag, name=self.kwargs['tag']) return context
- Страница с контактной формой не использует серверную логику Django, поэтому шаблонизатор собирает ее из двух шаблонов и выводит без каких-либо манипуляций:
class ContactView(TemplateView): template_name = 'contact.html'
Шаблоны
Проект использует несколько шаблонов. Основные шаблоны:
Частичные шаблоны используются для включения боковой панели, пагинации и т.п. в основные шаблоны:
Все нужны скрипты находятся в статической директории и подключаются в шаблоне base.html.
Генерация статических страниц
В отличие от стандартного динамического приложения Django, вся логика генератора сосредоточена не в views.py, а в urls.py. Использование distill_path
вместо обычного path
дает плагину django-distill понять, что все страницы, соответствующие этому маршруту, нужно экспортировать в виде HTML/CSS/JS файлов. Если distill_path
нужно сгенерировать множество страниц по одному и тому же шаблону – например, страницы всех записей в хронологическом порядке или по определенному тегу, – то для получения всех слагов и всех тегов он использует соответствующие функции:
def get_posts(): for post in Post.objects.all(): yield {'slug': post.slug} def get_tags(): for tag in Tag.objects.all(): yield {'tag': tag.name} ... distill_path('posts/<slug:slug>.html', PostView.as_view(), name='blog-post', distill_func=get_posts), distill_path('tags/<slug:tag>.html', TagView.as_view(), name='blog-tag', distill_func=get_tags),
И напротив, генератор создает index.html и contact.html в единственных экземплярах, поэтому получение дополнительных данных из функций им не требуется:
distill_path('', IndexView.as_view(), name='blog-index', distill_file='index.html'), ... distill_path('contact.html', ContactView.as_view(), name='contact', distill_file='contact.html'),
Обработка пагинации
Упомянутая выше проблема с обработкой стандартной динамической пагинации решается с помощью этой функции:
def get_paginated_index(): posts = Post.objects.all() paginator = Paginator(posts, 6) for page_num in range(1, paginator.num_pages + 1): yield {'page': page_num}
И этого маршрута:
distill_path('pages/page<int:page>.html', IndexView.as_view(), name='blog-index-paginated', distill_func=get_paginated_index),
Кроме того, статическая пагинация требует специфической реализации в шаблоне _pagination.html. Этот код обеспечивает вывод страницы index.html при нажатии на первую кнопку пагинации. Все остальные страницы будут выводиться как page2.html, page3.html и так далее:
{% if is_paginated %} <div class="pagination"> {% if page_obj.has_previous %} <a class="btn btn-outline-primary mb-4" href="{% if page_obj.previous_page_number == 1 %}/{% else %}/pages/page{{ page_obj.previous_page_number }}.html{% endif %}">Предыдущая</a> {% endif %} {% for num in page_obj.paginator.page_range %} {% if page_obj.number == num %} <a class="btn btn-primary mb-4">{{ num }}</a> {% else %} <a class="btn btn-outline-primary mb-4" href="{% if num == 1 %}/{% else %}/pages/page{{ num }}.html{% endif %}">{{ num }}</a> {% endif %} {% endfor %} {% if page_obj.has_next %} <a class="btn btn-outline-primary mb-4" href="/pages/page{{ page_obj.next_page_number }}.html">Следующая</a> {% endif %} </div> {% endif %}
Обработка форм
Статический сайт без серверной логики не может обрабатывать пользовательский ввод (контактные формы и комментарии). Но решить эту проблему можно с использованием подходящего стороннего сервиса. Для обработки форм, например, оптимален Formspree и подобные ему сервисы. После регистрации в сервисе можно создать любую форму и получить код для ее обработки, который нужно вставить в action
:
<form id="contact-form" action="https://formspree.io/f/xenyqrle" method="POST">
Другие сервисы, похожие на Formspree: Formspark, Formcarry, Getform, FormKeep, FormBackend.
Подсветка синтаксиса
Для подсветки кода блог использует библиотеку highlight.js, которая поддерживает 192+ языков и 498 тем. В этом проекте используется тема Dracula, выбрать другую тему можно в разделе Examples библиотеки highlight.js.
Экспорт статических страниц из приложения Django
Когда ваш сайт полностью готов к загрузке на хостинг, нужно выполнить упомянутую выше команду:
python manage.py distill-local mysite --collectstatic
После чего django-distill экспортирует HTML/CSS/JS страницы в директорию mysite. Перед загрузкой на хостинг стоит убедиться, что статическая версия сайта функционирует корректно. Для этого нужно вызвать cmd в директории сайта и выполнить команду для запуска локального сервера:
python -m http.server
После этого статическую версию сайта можно будет открыть в браузере по адресу http://localhost:8000/.
Нюансы хостинга GitHub Pages
GitHub Pages разрешает создать 1 сайт с адресом username.github.io и сколько угодно сайтов типа username.github.io/repo-name. Чтобы GitHub Pages начал раздавать статические файлы из репозитория, нужно этот репозиторий настроить следующим образом:
- Перейдите в раздел Settings в репозитории, в котором находятся файлы сайта, и выберите Pages.
- На странице Pages выберите нужную ветку – скорее всего это будет main, – и нажмите Save:
Все! Через несколько секунд сайт начнет работать.
Если что-то не получилось, пишите в комментариях – поможем!
🐍 Python: от новичка до Junior за 32 урока
Ключевые моменты курса по Python, который действительно заслуживает внимания:
- 4 практических проекта для портфолио: от парсера до Telegram-бота
- Преподают специалисты из Мегафона с опытом в ML и NLP
- 90+ часов теории и практики с доступом навсегда
- Обучение можно совмещать с работой
Особенно впечатляет программа по алгоритмам и структурам данных в бонусном модуле — редко встретишь такую глубину на базовом курсе.