15 Commits

Author SHA1 Message Date
fea2765090 mod: новая версия (+блог, странички и другие улучшения)
All checks were successful
Build ETPGRF-site / build (push) Successful in 1m27s
2026-01-29 23:02:30 +03:00
1a7034df66 mod: спрятали url админки в .env 2026-01-29 22:51:12 +03:00
7e7d0a7d49 add: динамическое создание sitemap.xml 2026-01-29 22:33:33 +03:00
a95d677bb7 add: все для робот.тхт 2026-01-29 21:33:24 +03:00
52e960a1d0 mod: меню не налезают на логотипы 2026-01-29 17:57:43 +03:00
0107f8ddba mod: отступы 2026-01-29 16:07:23 +03:00
838aabf0b3 mod: fivicon.ico для Яндекс (120х120) 2026-01-29 01:22:39 +03:00
6266531542 mod: стили бургер-меню 2026-01-28 23:32:38 +03:00
b5ad30e5a6 mod: стили и меню... 2026-01-28 22:45:29 +03:00
fedfae1f74 mod: песочница только в режиме debug 2026-01-28 20:27:10 +03:00
8f39172803 add: view и шаблоны для блогов и страниц 2026-01-27 23:42:09 +03:00
96614748a8 mod: добавлены индексы и составные индексы (ускорение 2026-01-26 17:02:31 +03:00
b967c374a5 add: приложение blog (для страниц и постов) 2026-01-25 12:10:27 +03:00
846c066314 add: счетчик google.analytic
All checks were successful
Build ETPGRF-site / build (push) Successful in 1m27s
2026-01-24 13:48:50 +03:00
d74bee2fc0 add: только текст от логотипов
All checks were successful
Build ETPGRF-site / build (push) Successful in 1m26s
2026-01-24 12:49:55 +03:00
27 changed files with 849 additions and 24 deletions

View File

@@ -10,6 +10,9 @@ ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0
# Укажите здесь URL, по которому вы заходите на сайт (с протоколом и портом) # Укажите здесь URL, по которому вы заходите на сайт (с протоколом и портом)
CSRF_TRUSTED_ORIGINS=http://localhost:8000,http://127.0.0.1:8000,http://0.0.0.0:8000 CSRF_TRUSTED_ORIGINS=http://localhost:8000,http://127.0.0.1:8000,http://0.0.0.0:8000
# URL для доступа к админке Django (можно сменить для безопасности, чтобы боты не могли её найти)
ADMIN_URL=admin/
# Настройки достпа к пакетам в репозитории, чтобы wathtower мог проверять их свежесть и скачивать # Настройки достпа к пакетам в репозитории, чтобы wathtower мог проверять их свежесть и скачивать
REPO_USER=xxxxx REPO_USER=xxxxx
REPO_PASS=xxxxx REPO_PASS=xxxxx

View File

@@ -87,6 +87,14 @@ http {
expires 30d; expires 30d;
} }
# Robots.txt
location = /robots.txt {
alias /app/public/static_collected/robots.txt;
access_log off;
log_not_found off;
expires 30d;
}
location / { location / {
# --- ЗАЩИТА ОТ БРУТФОРСА --- # --- ЗАЩИТА ОТ БРУТФОРСА ---
# Применяем зону 'one', разрешаем "всплеск" до 10 запросов. # Применяем зону 'one', разрешаем "всплеск" до 10 запросов.

View File

23
etpgrf_site/blog/admin.py Normal file
View File

@@ -0,0 +1,23 @@
from django.contrib import admin
from .models import Post
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ('title', 'post_type', 'is_published', 'published_at')
list_filter = ('post_type', 'is_published', 'published_at')
search_fields = ('title', 'content', 'slug')
prepopulated_fields = {'slug': ('title',)}
date_hierarchy = 'published_at'
fieldsets = (
(None, {
'fields': ('title', 'slug', 'post_type', 'is_published', 'published_at')
}),
('Контент', {
'fields': ('image', 'excerpt', 'content')
}),
('SEO', {
'fields': ('seo_title', 'seo_description', 'seo_keywords'),
'classes': ('collapse',)
}),
)

6
etpgrf_site/blog/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class BlogConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'blog'
verbose_name = 'Блог и Страницы'

View File

@@ -0,0 +1,37 @@
# Generated by Django 6.0.1 on 2026-01-25 09:04
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Post',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(help_text='Основной заголовок (H1). Обязательно для заполнения.', max_length=255, verbose_name='Заголовок')),
('slug', models.SlugField(help_text='Уникальная часть адреса. Используйте латиницу, цифры и дефис. Например: my-new-post', max_length=255, unique=True, verbose_name='URL (slug)')),
('post_type', models.CharField(choices=[('B', 'Пост в блог'), ('P', 'Страница')], default='B', help_text='Страница доступна по адресу /slug/, Пост — по адресу /blog/slug/', max_length=1, verbose_name='Тип публикации')),
('is_published', models.BooleanField(default=True, help_text='Снимите галочку, чтобы скрыть публикацию (черновик).', verbose_name='Опубликовано')),
('published_at', models.DateTimeField(default=django.utils.timezone.now, help_text='Дата, которая будет отображаться в блоге. Можно запланировать на будущее.', verbose_name='Дата публикации')),
('content', models.TextField(help_text='Основной текст публикации. Поддерживает HTML.', verbose_name='Контент')),
('excerpt', models.TextField(blank=True, help_text='Отображается в списке постов. Если оставить пустым, будет взято начало контента.', verbose_name='Краткое описание (тизер)')),
('image', models.ImageField(blank=True, help_text='Изображение для превью в ленте и Open Graph (соцсети).', null=True, upload_to='blog/', verbose_name='Обложка')),
('seo_title', models.CharField(blank=True, help_text='Заголовок для поисковиков (<title>). Если пусто, используется основной заголовок.', max_length=255, verbose_name='SEO Title')),
('seo_description', models.TextField(blank=True, help_text='Описание для поисковиков (meta description). Рекомендуется 150-160 символов.', verbose_name='SEO Description')),
('seo_keywords', models.CharField(blank=True, help_text='Ключевые слова через запятую (meta keywords). Сейчас почти не используется поисковиками.', max_length=255, verbose_name='SEO Keywords')),
],
options={
'verbose_name': 'Публикация',
'verbose_name_plural': 'Публикации',
'ordering': ['-published_at'],
},
),
]

View File

@@ -0,0 +1,47 @@
# Generated by Django 6.0.1 on 2026-01-26 13:56
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='post',
name='is_published',
field=models.BooleanField(db_index=True, default=True, help_text='Снимите галочку, чтобы скрыть публикацию (черновик).', verbose_name='Опубликовано'),
),
migrations.AlterField(
model_name='post',
name='post_type',
field=models.CharField(choices=[('B', 'Пост в блог'), ('P', 'Страница')], db_index=True, default='B', help_text='Страница доступна по адресу /slug/, Пост — по адресу /blog/slug/', max_length=1, verbose_name='Тип публикации'),
),
migrations.AlterField(
model_name='post',
name='published_at',
field=models.DateTimeField(db_index=True, default=django.utils.timezone.now, help_text='Дата, которая будет отображаться в блоге. Можно запланировать на будущее.', verbose_name='Дата публикации'),
),
migrations.AlterField(
model_name='post',
name='seo_keywords',
field=models.CharField(blank=True, help_text='Ключевые слова через запятую (meta keywords). Сейчас почти не используется поисковиками,но может пригодиться.', max_length=255, verbose_name='SEO Keywords'),
),
migrations.AlterField(
model_name='post',
name='seo_title',
field=models.CharField(blank=True, help_text='Заголовок для поисковиков (<tt>&lt;title&gt;</tt>). Если пусто, используется основной заголовок.', max_length=255, verbose_name='SEO Title'),
),
migrations.AddIndex(
model_name='post',
index=models.Index(fields=['post_type', 'is_published', '-published_at'], name='blog_post_idx'),
),
migrations.AddIndex(
model_name='post',
index=models.Index(fields=['post_type', 'slug'], name='blog_page_slug_idx'),
),
]

View File

102
etpgrf_site/blog/models.py Normal file
View File

@@ -0,0 +1,102 @@
from django.db import models
from django.utils import timezone
from django.urls import reverse
class PostType(models.TextChoices):
BLOG = 'B', 'Пост в блог'
PAGE = 'P', 'Страница'
class Post(models.Model):
"""
Модель для постов блога и статических страниц.
"""
title = models.CharField(
verbose_name="Заголовок",
max_length=255,
help_text="Основной заголовок (H1). Обязательно для заполнения."
)
slug = models.SlugField(
verbose_name="URL (slug)",
max_length=255,
unique=True,
help_text="Уникальная часть адреса. Используйте латиницу, цифры и дефис. Например: my-new-post"
)
post_type = models.CharField(
verbose_name="Тип публикации",
max_length=1,
choices=PostType.choices,
default=PostType.BLOG,
db_index=True,
help_text="Страница доступна по адресу /slug/, Пост — по адресу /blog/slug/"
)
is_published = models.BooleanField(
verbose_name="Опубликовано",
default=True,
db_index=True,
help_text="Снимите галочку, чтобы скрыть публикацию (черновик)."
)
published_at = models.DateTimeField(
verbose_name="Дата публикации",
default=timezone.now,
db_index=True,
help_text="Дата, которая будет отображаться в блоге. Можно запланировать на будущее."
)
content = models.TextField(
verbose_name="Контент",
help_text="Основной текст публикации. Поддерживает HTML."
)
excerpt = models.TextField(
verbose_name="Краткое описание (тизер)",
blank=True,
help_text="Отображается в списке постов. Если оставить пустым, будет взято начало контента."
)
image = models.ImageField(
verbose_name="Обложка",
upload_to='blog/',
blank=True,
null=True,
help_text="Изображение для превью в ленте и Open Graph (соцсети)."
)
# SEO
seo_title = models.CharField(
verbose_name="SEO Title",
max_length=255,
blank=True,
help_text="Заголовок для поисковиков (<tt>&lt;title&gt;</tt>). Если пусто, используется основной заголовок."
)
seo_description = models.TextField(
verbose_name="SEO Description",
blank=True,
help_text="Описание для поисковиков (meta description). Рекомендуется 150-160 символов."
)
seo_keywords = models.CharField(
verbose_name="SEO Keywords",
max_length=255,
blank=True,
help_text="Ключевые слова через запятую (meta keywords). Сейчас почти не используется поисковиками,"
"но может пригодиться."
)
class Meta:
verbose_name = "Публикация"
verbose_name_plural = "Публикации"
ordering = ['-published_at']
indexes = [
# Индекс для быстрого поиска и сортировки постов блога
models.Index(fields=['post_type', 'is_published', '-published_at'], name='blog_post_idx'),
# Индекс для быстрых страниц (если post_type='P')
models.Index(fields=['post_type', 'slug'], name='blog_page_slug_idx'),
]
def __str__(self):
return self.title
def get_absolute_url(self):
if self.post_type == PostType.PAGE:
return reverse('page_detail', kwargs={'slug': self.slug})
return reverse('post_detail', kwargs={'slug': self.slug})

View File

@@ -0,0 +1,14 @@
from django.contrib.sitemaps import Sitemap
from .models import Post
class PostSitemap(Sitemap):
changefreq = "weekly" # Как часто меняются страницы
priority = 0.9 # Приоритет (от 0.0 до 1.0)
def items(self):
"""Возвращает все опубликованные посты и страницы."""
return Post.objects.filter(is_published=True)
def lastmod(self, obj):
"""Возвращает дату последнего изменения."""
return obj.published_at # Или можно добавить поле updated_at

View File

@@ -0,0 +1,50 @@
{% extends 'typograph/base.html' %}
{% load static %}
{% block title %}{{ page.seo_title|default:page.title }} — ETPGRF{% endblock %}
{% block description %}{{ page.seo_description|default:page.content|striptags|truncatechars:160 }}{% endblock %}
{% block keywords %}{{ page.seo_keywords|default:'типограф, типографика, онлайн типограф, подготовка текста для веба, html типограф, неразрывные пробелы, кавычки елочки, длинное тире, очистка текста от мусора, интернет верстка, муравьев' }} seo_keywords {% endblock %}
{% block og_title %}{{ page.seo_title|default:page.title }}{% endblock %}
{% block og_description %}{{ page.seo_description|default:page.content|striptags|truncatechars:160 }}{% endblock %}
{% block og_image %}{% if page.image %}{{ request.scheme }}://{{ request.get_host }}{{ page.image.url }}{% else %}{{ request.scheme }}://{{ request.get_host }}{% static 'img/etpgrf-logo-for-fb-vk-x.gif' %}{% endif %}{% endblock %}
{% block twitter_title %}{{ page.seo_title|default:page.title }}{% endblock %}
{% block twitter_description %}{{ page.seo_description|default:page.content|striptags|truncatechars:160 }}{% endblock %}
{% block twitter_image %}{% if page.image %}{{ request.scheme }}://{{ request.get_host }}{{ page.image.url }}{% else %}{{ request.scheme }}://{{ request.get_host }}{% static 'img/etpgrf-logo-for-fb-vk-x.gif' %}{% endif %}{% endblock %}
{% block content %}
<div class="row">
{# Левая колонка: Дата и Картинка #}
<div class="col-lg-2 align-self-start text-end mb-4">
<p class="small align-self-end">
<small class="bg-secondary bg-opacity-10 p-2 text-nowrap">
{{ page.published_at|date:"d.M.Y"|lower }}
</small>
</p>
<p>
{% if post.image %}
<img src="{{ post.image.url }}" class="w-100" alt="{{ post.title|safe }}" />
{% else %}
<img src="{% static 'img/etpgrf-logo-for-fb-vk-x.gif' %}" class="w-100" alt="{{ post.title|safe }}" />
{% endif %}
</p>
</div>
{# Правая колонка: Контент #}
<div class="col-lg-10 border-start ps-lg-4 post-page-content">
<h1 class="display-4 mb-4">{{ page.title|safe }}</h1>
{% if page.excerpt %}
<div class="lead bg-secondary bg-opacity-10 p-3 rounded">
{{ page.excerpt|safe }}
</div>
{% endif %}
<div class="page-content mt-4">
{{ page.content|safe }}
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,58 @@
{% extends 'typograph/base.html' %}
{% load static %}
{% block title %}{{ post.seo_title|default:post.title }} — ETPGRF{% endblock %}
{% block description %}{{ post.seo_description|default:post.excerpt|default:post.content|striptags|truncatechars:160 }}{% endblock %}
{% block og_title %}{{ post.seo_title|default:post.title }}{% endblock %}
{% block og_description %}{{ post.seo_description|default:post.excerpt|default:post.content|striptags|truncatechars:160 }}{% endblock %}
{% block og_image %}{% if post.image %}{{ request.scheme }}://{{ request.get_host }}{{ post.image.url }}{% else %}{{ request.scheme }}://{{ request.get_host }}{% static 'img/etpgrf-logo-for-fb-vk-x.gif' %}{% endif %}{% endblock %}
{% block twitter_title %}{{ post.seo_title|default:post.title }}{% endblock %}
{% block twitter_description %}{{ post.seo_description|default:post.excerpt|default:post.content|striptags|truncatechars:160 }}{% endblock %}
{% block twitter_image %}{% if post.image %}{{ request.scheme }}://{{ request.get_host }}{{ post.image.url }}{% else %}{{ request.scheme }}://{{ request.get_host }}{% static 'img/etpgrf-logo-for-fb-vk-x.gif' %}{% endif %}{% endblock %}
{% block content %}
<div class="row">
{# Левая колонка: Дата и Картинка #}
<div class="col-lg-2 align-self-start text-end mb-4">
<p class="small align-self-end">
<small class="bg-secondary bg-opacity-10 p-2 text-nowrap">
{{ post.published_at|date:"d.M.Y"|lower }}
</small>
</p>
<p>
{% if post.image %}
<img src="{{ post.image.url }}" class="w-100" alt="{{ post.title }}" />
{% else %}
<img src="{% static 'img/etpgrf-logo-for-fb-vk-x.gif' %}" class="w-100" alt="{{ post.title }}" />
{% endif %}
</p>
<div class="d-none d-lg-block mt-5">
<a href="{% url 'blog:post_list' %}" class="btn btn-sm btn-outline-secondary w-100">&larr; В блог</a>
</div>
</div>
{# Правая колонка: Контент #}
<div class="col-lg-10 border-start ps-lg-4 post-page-content">
<h1 class="display-4 mb-4">{{ post.title }}</h1>
{% if post.excerpt %}
<p class="lead bg-secondary bg-opacity-10 p-3 rounded">
{{ post.excerpt|linebreaksbr }}
</p>
{% endif %}
<div class="post-content mt-4">
{{ post.content|safe }}
</div>
<div class="d-lg-none mt-5 border-top pt-3">
<a href="{% url 'blog:post_list' %}" class="btn btn-outline-secondary">&larr; Назад к списку статей</a>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,76 @@
{% extends 'typograph/base.html' %}
{% block title %}Блог — ETPGRF{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-8">
<h1 class="mb-4">Блог</h1>
{% for post in page_obj %}
<article class="card mb-4 shadow-sm">
{% if post.image %}
<img src="{{ post.image.url }}" class="card-img-top" alt="{{ post.title }}" style="max-height: 300px; object-fit: cover;" />
{% endif %}
<div class="card-body">
<h2 class="card-title h4">
<a href="{{ post.get_absolute_url }}" class="text-decoration-none text-reset">{{ post.title }}</a>
</h2>
<p class="card-text text-muted small mb-2">
{{ post.published_at|date:"d E Y" }}
</p>
<p class="card-text">
{% if post.excerpt %}
{{ post.excerpt|linebreaks }}
{% else %}
{{ post.content|striptags|truncatewords:30 }}
{% endif %}
</p>
<a href="{{ post.get_absolute_url }}" class="btn btn-outline-primary btn-sm">Читать далее &rarr;</a>
</div>
</article>
{% empty %}
<p class="text-muted">Пока нет записей.</p>
{% endfor %}
{# Пагинация #}
{% if page_obj.has_other_pages %}
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">&laquo;</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">&laquo;</span>
</li>
{% endif %}
{% for i in page_obj.paginator.page_range %}
{% if page_obj.number == i %}
<li class="page-item active">
<span class="page-link">{{ i }}</span>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="?page={{ i }}">{{ i }}</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}">&raquo;</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">&raquo;</span>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
</div>
</div>
{% endblock %}

19
etpgrf_site/blog/urls.py Normal file
View File

@@ -0,0 +1,19 @@
from django.urls import path
from django.conf import settings
from . import views
app_name = 'blog' # Пространство имен для приложения blog
urlpatterns = [
# Лента блога: /blog/
path('', views.post_list, name='post_list'),
]
# Песочница для верстки: /blog/tmp/
# Добавляем ТОЛЬКО если DEBUG=True и ПЕРЕД post_detail
if settings.DEBUG:
urlpatterns.append(path('tmp/', views.tmp_view, name='tmp'))
# Детальная страница поста: /blog/my-awesome-post/
# Этот маршрут должен быть последним, так как он перехватывает всё, что похоже на slug
urlpatterns.append(path('<slug:slug>/', views.post_detail, name='post_detail'))

60
etpgrf_site/blog/views.py Normal file
View File

@@ -0,0 +1,60 @@
from django.shortcuts import render, get_object_or_404
from django.utils import timezone
from django.core.paginator import Paginator
from .models import Post, PostType
def post_list(request):
"""
Отображает список опубликованных постов блога с пагинацией.
"""
# Фильтруем только посты блога, опубликованные и с датой публикации не позднее текущего момента
posts_queryset = Post.objects.filter(
post_type=PostType.BLOG,
is_published=True,
published_at__lte=timezone.now()
).order_by('-published_at') # Сортируем по дате публикации (от новых к старым)
# Настраиваем пагинацию: 10 постов на страницу
paginator = Paginator(posts_queryset, 10)
page_number = request.GET.get('page') # Получаем номер страницы из GET-параметра
page_obj = paginator.get_page(page_number) # Получаем объект страницы
return render(request, 'blog/post_list.html', {'page_obj': page_obj})
def post_detail(request, slug):
"""
Отображает детальную страницу конкретного поста блога.
"""
# Ищем пост по слагу, типу 'BLOG', опубликованный и с датой публикации не позднее текущего момента
post = get_object_or_404(
Post,
slug=slug,
post_type=PostType.BLOG,
is_published=True,
published_at__lte=timezone.now()
)
return render(request, 'blog/post_detail.html', {'post': post})
def page_detail(request, slug):
"""
Отображает детальную страницу статической страницы (например, /privacy-policy/).
"""
# Ищем страницу по слагу, типу 'PAGE' и опубликованную
page = get_object_or_404(
Post,
slug=slug,
post_type=PostType.PAGE,
is_published=True
)
return render(request, 'blog/page_detail.html', {'page': page})
def tmp_view(request):
"""
Временная страница для верстки постов.
Доступна только в DEBUG режиме (или можно оставить, если не мешает).
"""
return render(request, 'blog/tmp.html')

View File

@@ -24,6 +24,10 @@ ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',')
# CSRF Trusted Origins (важно для работы через Nginx/Docker) # CSRF Trusted Origins (важно для работы через Nginx/Docker)
CSRF_TRUSTED_ORIGINS = os.getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost:8000,http://127.0.0.1:8000').split(',') CSRF_TRUSTED_ORIGINS = os.getenv('CSRF_TRUSTED_ORIGINS', 'http://localhost:8000,http://127.0.0.1:8000').split(',')
# URL админки (можно скрыть через .env)
# По умолчанию 'admin/'
ADMIN_URL = os.getenv('ADMIN_URL', 'admin/')
# Application definition # Application definition
@@ -34,7 +38,9 @@ INSTALLED_APPS = [
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'typograph', 'django.contrib.sitemaps', # Sitemap
'typograph', # Основное приложение типографа
'blog', # Приложение для блога и страниц
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@@ -53,7 +59,7 @@ ROOT_URLCONF = 'etpgrf_site.urls'
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [], 'DIRS': [], # Шаблоны ищем внутри приложений (APP_DIRS=True)
'APP_DIRS': True, 'APP_DIRS': True,
'OPTIONS': { 'OPTIONS': {
'context_processors': [ 'context_processors': [

View File

@@ -2,19 +2,30 @@ from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from django.views.generic.base import RedirectView from django.contrib.sitemaps.views import sitemap # Импортируем view для sitemap
from django.contrib.staticfiles.storage import staticfiles_storage from blog import views as blog_views
from blog.sitemaps import PostSitemap # Импортируем наш класс Sitemap
# Словарь с картами сайта
sitemaps = {
'posts': PostSitemap,
}
urlpatterns = [ urlpatterns = [
path(route='adm-in/', view=admin.site.urls), # Админка по секретному URL
path(route='', view=include('typograph.urls')), path(f'{settings.ADMIN_URL}', admin.site.urls),
path('', include('typograph.urls')),
# Блог
path('blog/', include('blog.urls')),
# Sitemap.xml
path('sitemap.xml', sitemap, {'sitemaps': sitemaps}, name='django.contrib.sitemaps.views.sitemap'),
# Статические страницы (ловушка в самом конце)
path('<slug:slug>/', blog_views.page_detail, name='page_detail'),
] ]
if settings.DEBUG: if settings.DEBUG:
# urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
# runserver автоматически раздает статику из STATICFILES_DIRS,
# поэтому добавлять static(settings.STATIC_URL...) НЕ НУЖНО.
# Это только ломает путь, направляя его в STATIC_ROOT.
# А вот медиа runserver не раздает, поэтому это нужно:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@@ -7,7 +7,7 @@
{# --- SEO & Meta Tags --- #} {# --- SEO & Meta Tags --- #}
<title>{% block title %}ETPGRF — единая типографика для веба{% endblock %}</title> <title>{% block title %}ETPGRF — единая типографика для веба{% endblock %}</title>
<meta name="description" content="{% block description %}Бесплатный онлайн-типограф для подготовки текстов к публикации в вебе. Расставка неразрывных пробелов, правильных кавычек («ёлочки»), тире, спецсимволы, отбивка, компоновка, висячая пунктуация. Идеально для верстки сайтов, статей и постов.{% endblock %}"> <meta name="description" content="{% block description %}Бесплатный онлайн-типограф для подготовки текстов к публикации в вебе. Расставка неразрывных пробелов, правильных кавычек («ёлочки»), тире, спецсимволы, отбивка, компоновка, висячая пунктуация. Идеально для верстки сайтов, статей и постов.{% endblock %}">
<meta name="keywords" content="типограф, типографика, онлайн типограф, подготовка текста для веба, html типограф, неразрывные пробелы, кавычки елочки, длинное тире, очистка текста от мусора, интернет верстка, муравьев"> <meta name="keywords" content="{% block keywords %}типограф, типографика, онлайн типограф, подготовка текста для веба, html типограф, неразрывные пробелы, кавычки елочки, длинное тире, очистка текста от мусора, интернет верстка, муравьев{% endblock %}">
<meta name="author" content="Sergei Erjemin"> <meta name="author" content="Sergei Erjemin">
{# --- Open Graph (Facebook, VK, LinkedIn, Telegram) --- #} {# --- Open Graph (Facebook, VK, LinkedIn, Telegram) --- #}
@@ -60,6 +60,23 @@
<a class="navbar-brand" href="/" style="background-image: var(--bg-image-text);" <a class="navbar-brand" href="/" style="background-image: var(--bg-image-text);"
title="ETPGRF — единая типографика для веба"> title="ETPGRF — единая типографика для веба">
</a> </a>
{# Кнопка-бургер для мобильных #}
<button class="navbar-toggler border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
{# Меню #}
<div class="collapse navbar-collapse justify-content-end text-end" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link {% if '/blog/' in request.path %}active fw-bold{% endif %}" href="{% url 'blog:post_list' %}">Блог</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.path == '/donate/' %}active fw-bold{% endif %}" href="/donate/">Поддержать</a>
</li>
</ul>
</div>
</div> </div>
</nav> </nav>
@@ -70,7 +87,7 @@
{# Футер #}<footer class="footer mt-auto py-2 mt-4"> {# Футер #}<footer class="footer mt-auto py-2 mt-4">
<div class="container d-flex justify-content-between align-items-center"> <div class="container d-flex justify-content-between align-items-center">
<span class="text-muted small nowrap me-2">&copy; Sergei Erjemin, 2025&ndash;{% now 'Y' %}.</span> <span class="text-muted small nowrap me-2">&copy; Sergei Erjemin, 2025&ndash;{% now 'Y' %}.</span>
<nobr class="text-muted small mx-2"><i class="bi bi-tags me-1" title="Версия библиотеки etpgrf / Версия сайта"></i>v0.1.3 / v0.1.6 <nobr class="text-muted small mx-2"><i class="bi bi-tags me-1" title="Версия библиотеки etpgrf / Версия сайта"></i>v0.1.3 / v0.2.0
</nobr> </nobr>
{# Сводная статистика (HTMX) #}<span class="text-muted small ms-2" hx-get="{% url 'stats_summary' %}" hx-trigger="load"> {# Сводная статистика (HTMX) #}<span class="text-muted small ms-2" hx-get="{% url 'stats_summary' %}" hx-trigger="load">
... ...

View File

@@ -1,6 +1,12 @@
{% extends 'typograph/base.html' %} {% extends 'typograph/base.html' %}
{% load static %} {% load static %}
{% block title %}ETPGRF — единая типографика для веба{% endblock %}
{% block description %}Бесплатный онлайн-типограф для подготовки текстов к публикации в вебе. Расставка неразрывных пробелов, правильных кавычек («ёлочки»), тире, спецсимволы, отбивка, компоновка, висячая пунктуация. Идеально для верстки сайтов, статей и постов.{% endblock %}
{% block keywords %}типограф, типографика, онлайн типограф, подготовка текста для веба, html типограф, неразрывные пробелы, кавычки елочки, длинное тире, очистка текста от мусора, интернет верстка, муравьев{% endblock %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">

110
poetry.lock generated
View File

@@ -259,6 +259,114 @@ files = [
{file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
] ]
[[package]]
name = "pillow"
version = "12.1.0"
description = "Python Imaging Library (fork)"
optional = false
python-versions = ">=3.10"
files = [
{file = "pillow-12.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:fb125d860738a09d363a88daa0f59c4533529a90e564785e20fe875b200b6dbd"},
{file = "pillow-12.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cad302dc10fac357d3467a74a9561c90609768a6f73a1923b0fd851b6486f8b0"},
{file = "pillow-12.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a40905599d8079e09f25027423aed94f2823adaf2868940de991e53a449e14a8"},
{file = "pillow-12.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:92a7fe4225365c5e3a8e598982269c6d6698d3e783b3b1ae979e7819f9cd55c1"},
{file = "pillow-12.1.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f10c98f49227ed8383d28174ee95155a675c4ed7f85e2e573b04414f7e371bda"},
{file = "pillow-12.1.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8637e29d13f478bc4f153d8daa9ffb16455f0a6cb287da1b432fdad2bfbd66c7"},
{file = "pillow-12.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:21e686a21078b0f9cb8c8a961d99e6a4ddb88e0fc5ea6e130172ddddc2e5221a"},
{file = "pillow-12.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2415373395a831f53933c23ce051021e79c8cd7979822d8cc478547a3f4da8ef"},
{file = "pillow-12.1.0-cp310-cp310-win32.whl", hash = "sha256:e75d3dba8fc1ddfec0cd752108f93b83b4f8d6ab40e524a95d35f016b9683b09"},
{file = "pillow-12.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:64efdf00c09e31efd754448a383ea241f55a994fd079866b92d2bbff598aad91"},
{file = "pillow-12.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:f188028b5af6b8fb2e9a76ac0f841a575bd1bd396e46ef0840d9b88a48fdbcea"},
{file = "pillow-12.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:a83e0850cb8f5ac975291ebfc4170ba481f41a28065277f7f735c202cd8e0af3"},
{file = "pillow-12.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6e53e82ec2db0717eabb276aa56cf4e500c9a7cec2c2e189b55c24f65a3e8c0"},
{file = "pillow-12.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40a8e3b9e8773876d6e30daed22f016509e3987bab61b3b7fe309d7019a87451"},
{file = "pillow-12.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:800429ac32c9b72909c671aaf17ecd13110f823ddb7db4dfef412a5587c2c24e"},
{file = "pillow-12.1.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b022eaaf709541b391ee069f0022ee5b36c709df71986e3f7be312e46f42c84"},
{file = "pillow-12.1.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f345e7bc9d7f368887c712aa5054558bad44d2a301ddf9248599f4161abc7c0"},
{file = "pillow-12.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d70347c8a5b7ccd803ec0c85c8709f036e6348f1e6a5bf048ecd9c64d3550b8b"},
{file = "pillow-12.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1fcc52d86ce7a34fd17cb04e87cfdb164648a3662a6f20565910a99653d66c18"},
{file = "pillow-12.1.0-cp311-cp311-win32.whl", hash = "sha256:3ffaa2f0659e2f740473bcf03c702c39a8d4b2b7ffc629052028764324842c64"},
{file = "pillow-12.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:806f3987ffe10e867bab0ddad45df1148a2b98221798457fa097ad85d6e8bc75"},
{file = "pillow-12.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:9f5fefaca968e700ad1a4a9de98bf0869a94e397fe3524c4c9450c1445252304"},
{file = "pillow-12.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a332ac4ccb84b6dde65dbace8431f3af08874bf9770719d32a635c4ef411b18b"},
{file = "pillow-12.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:907bfa8a9cb790748a9aa4513e37c88c59660da3bcfffbd24a7d9e6abf224551"},
{file = "pillow-12.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efdc140e7b63b8f739d09a99033aa430accce485ff78e6d311973a67b6bf3208"},
{file = "pillow-12.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bef9768cab184e7ae6e559c032e95ba8d07b3023c289f79a2bd36e8bf85605a5"},
{file = "pillow-12.1.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:742aea052cf5ab5034a53c3846165bc3ce88d7c38e954120db0ab867ca242661"},
{file = "pillow-12.1.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6dfc2af5b082b635af6e08e0d1f9f1c4e04d17d4e2ca0ef96131e85eda6eb17"},
{file = "pillow-12.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:609e89d9f90b581c8d16358c9087df76024cf058fa693dd3e1e1620823f39670"},
{file = "pillow-12.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43b4899cfd091a9693a1278c4982f3e50f7fb7cff5153b05174b4afc9593b616"},
{file = "pillow-12.1.0-cp312-cp312-win32.whl", hash = "sha256:aa0c9cc0b82b14766a99fbe6084409972266e82f459821cd26997a488a7261a7"},
{file = "pillow-12.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d70534cea9e7966169ad29a903b99fc507e932069a881d0965a1a84bb57f6c6d"},
{file = "pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c"},
{file = "pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7b5dd7cbae20285cdb597b10eb5a2c13aa9de6cde9bb64a3c1317427b1db1ae1"},
{file = "pillow-12.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:29a4cef9cb672363926f0470afc516dbf7305a14d8c54f7abbb5c199cd8f8179"},
{file = "pillow-12.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:681088909d7e8fa9e31b9799aaa59ba5234c58e5e4f1951b4c4d1082a2e980e0"},
{file = "pillow-12.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:983976c2ab753166dc66d36af6e8ec15bb511e4a25856e2227e5f7e00a160587"},
{file = "pillow-12.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:db44d5c160a90df2d24a24760bbd37607d53da0b34fb546c4c232af7192298ac"},
{file = "pillow-12.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6b7a9d1db5dad90e2991645874f708e87d9a3c370c243c2d7684d28f7e133e6b"},
{file = "pillow-12.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6258f3260986990ba2fa8a874f8b6e808cf5abb51a94015ca3dc3c68aa4f30ea"},
{file = "pillow-12.1.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e115c15e3bc727b1ca3e641a909f77f8ca72a64fff150f666fcc85e57701c26c"},
{file = "pillow-12.1.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6741e6f3074a35e47c77b23a4e4f2d90db3ed905cb1c5e6e0d49bff2045632bc"},
{file = "pillow-12.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:935b9d1aed48fcfb3f838caac506f38e29621b44ccc4f8a64d575cb1b2a88644"},
{file = "pillow-12.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5fee4c04aad8932da9f8f710af2c1a15a83582cfb884152a9caa79d4efcdbf9c"},
{file = "pillow-12.1.0-cp313-cp313-win32.whl", hash = "sha256:a786bf667724d84aa29b5db1c61b7bfdde380202aaca12c3461afd6b71743171"},
{file = "pillow-12.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:461f9dfdafa394c59cd6d818bdfdbab4028b83b02caadaff0ffd433faf4c9a7a"},
{file = "pillow-12.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:9212d6b86917a2300669511ed094a9406888362e085f2431a7da985a6b124f45"},
{file = "pillow-12.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:00162e9ca6d22b7c3ee8e61faa3c3253cd19b6a37f126cad04f2f88b306f557d"},
{file = "pillow-12.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7d6daa89a00b58c37cb1747ec9fb7ac3bc5ffd5949f5888657dfddde6d1312e0"},
{file = "pillow-12.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2479c7f02f9d505682dc47df8c0ea1fc5e264c4d1629a5d63fe3e2334b89554"},
{file = "pillow-12.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f188d580bd870cda1e15183790d1cc2fa78f666e76077d103edf048eed9c356e"},
{file = "pillow-12.1.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fde7ec5538ab5095cc02df38ee99b0443ff0e1c847a045554cf5f9af1f4aa82"},
{file = "pillow-12.1.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ed07dca4a8464bada6139ab38f5382f83e5f111698caf3191cb8dbf27d908b4"},
{file = "pillow-12.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f45bd71d1fa5e5749587613037b172e0b3b23159d1c00ef2fc920da6f470e6f0"},
{file = "pillow-12.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:277518bf4fe74aa91489e1b20577473b19ee70fb97c374aa50830b279f25841b"},
{file = "pillow-12.1.0-cp313-cp313t-win32.whl", hash = "sha256:7315f9137087c4e0ee73a761b163fc9aa3b19f5f606a7fc08d83fd3e4379af65"},
{file = "pillow-12.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0ddedfaa8b5f0b4ffbc2fa87b556dc59f6bb4ecb14a53b33f9189713ae8053c0"},
{file = "pillow-12.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:80941e6d573197a0c28f394753de529bb436b1ca990ed6e765cf42426abc39f8"},
{file = "pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91"},
{file = "pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796"},
{file = "pillow-12.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd"},
{file = "pillow-12.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13"},
{file = "pillow-12.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e"},
{file = "pillow-12.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643"},
{file = "pillow-12.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5"},
{file = "pillow-12.1.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de"},
{file = "pillow-12.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9"},
{file = "pillow-12.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a"},
{file = "pillow-12.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a"},
{file = "pillow-12.1.0-cp314-cp314-win32.whl", hash = "sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030"},
{file = "pillow-12.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94"},
{file = "pillow-12.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4"},
{file = "pillow-12.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2"},
{file = "pillow-12.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61"},
{file = "pillow-12.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51"},
{file = "pillow-12.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc"},
{file = "pillow-12.1.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14"},
{file = "pillow-12.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8"},
{file = "pillow-12.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924"},
{file = "pillow-12.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef"},
{file = "pillow-12.1.0-cp314-cp314t-win32.whl", hash = "sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988"},
{file = "pillow-12.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6"},
{file = "pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831"},
{file = "pillow-12.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ca94b6aac0d7af2a10ba08c0f888b3d5114439b6b3ef39968378723622fed377"},
{file = "pillow-12.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:351889afef0f485b84078ea40fe33727a0492b9af3904661b0abbafee0355b72"},
{file = "pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb0984b30e973f7e2884362b7d23d0a348c7143ee559f38ef3eaab640144204c"},
{file = "pillow-12.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:84cabc7095dd535ca934d57e9ce2a72ffd216e435a84acb06b2277b1de2689bd"},
{file = "pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53d8b764726d3af1a138dd353116f774e3862ec7e3794e0c8781e30db0f35dfc"},
{file = "pillow-12.1.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5da841d81b1a05ef940a8567da92decaa15bc4d7dedb540a8c219ad83d91808a"},
{file = "pillow-12.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19"},
{file = "pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9"},
]
[package.extras]
docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-autobuild", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"]
fpx = ["olefile"]
mic = ["olefile"]
test-arrow = ["arro3-compute", "arro3-core", "nanoarrow", "pyarrow"]
tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma (>=5)", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"]
xmp = ["defusedxml"]
[[package]] [[package]]
name = "python-dotenv" name = "python-dotenv"
version = "1.2.1" version = "1.2.1"
@@ -464,4 +572,4 @@ files = [
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.13" python-versions = "^3.13"
content-hash = "88ffa335edb29f6d8f90c01acef7d584e2a49d0a1361f0fa893b122ed8694ba1" content-hash = "fad76f5756ffa133d1778a1976fd5216450ebf83881fcfacee259b7c41102317"

View File

@@ -26,6 +26,9 @@
--bs-link-color: #90caf9; --bs-link-color: #90caf9;
--bs-link-hover-color: #bbdefb; --bs-link-hover-color: #bbdefb;
--bs-linkcolor: #14abda;
--bs-linkclolor-hover: #90caf9;
--bs-border-color: #37474f; --bs-border-color: #37474f;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15); --bs-border-color-translucent: rgba(255, 255, 255, 0.15);
@@ -38,11 +41,12 @@
} }
/* Небольшие стили для красоты */ /* Небольшие стили для красоты */
html, body { html {
height: 100%; height: 100%;
} }
body { body {
min-height: 100%; /* Используем min-height вместо height */
background-color: var(--bs-body-bg); background-color: var(--bs-body-bg);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -63,6 +67,7 @@ body {
#main-navbar > .container { #main-navbar > .container {
background: no-repeat left; background: no-repeat left;
background-size: contain; background-size: contain;
position: relative; /* Для абсолютного позиционирования бургера */
} }
#main-navbar > .container.logo-big { #main-navbar > .container.logo-big {
@@ -76,6 +81,7 @@ body {
} }
#main-navbar > #logo > .navbar-brand { #main-navbar > #logo > .navbar-brand {
padding: 0; /* Убираем отступы у бренда */
display: block; /* Блок, чтобы работали размеры */ display: block; /* Блок, чтобы работали размеры */
background: no-repeat left; background: no-repeat left;
background-size: contain; background-size: contain;
@@ -96,6 +102,74 @@ body {
opacity: 0; opacity: 0;
} }
/* --- Бургер меню --- */
#main-navbar > #logo > .navbar-toggler {
position: absolute;
right: 0.75rem; /* Отступ справа как у контейнера */
transition: top 0.4s ease; /* Анимация позиции */
z-index: 1001;
}
#main-navbar > #logo.logo-big > .navbar-toggler {
top: 32px; /* Центрируем для высоты 105px */
}
/* При скролле меняем позицию бургера */
#main-navbar > #logo:not(.logo-big) > .navbar-toggler {
top: 10px; /* Центрируем для высоты 60px */
}
/* --- Стили для ссылок в меню --- */
.nav-item {
color: var(--bs-body-bg);
padding: 10px 15px;
text-decoration: none;
position: relative;
}
/* Фикс для мобильных версий: ширина по контенту, прижатие вправо, логотипы */
@media (max-width: 991.98px) {
.nav-item {
width: fit-content;
margin-left: auto;
}
.navbar-collapse {
margin-top: -10px;
}
}
@media (max-width: 767.98px) {
#main-navbar > #logo > .navbar-brand {
background: none !important;
}
}
@media (max-width: 456.98px) {
#main-navbar > .container {
background: no-repeat left;
background-size: 105px 500px !important;
}
}
.nav-item:hover {
background-color: var(--bs-navbar-bg);
transition: background-color 0.8s;
}
.nav-item::after {
content: '';
position: absolute;
width: 100%;
height: 3px;
background: var(--bs-linkcolor);
bottom: 0;
left: 0;
transform: scaleX(0);
transition: transform 0.3s;
}
.nav-item:hover::after {
transform: scaleX(1);
}
/* Контент растягивается, чтобы прижать футер */ /* Контент растягивается, чтобы прижать футер */
#content-container { #content-container {
flex: 1 0 auto; flex: 1 0 auto;
@@ -154,10 +228,8 @@ body {
color: var(--bs-body-color); color: var(--bs-body-color);
border: 1px solid var(--bs-border-color); border: 1px solid var(--bs-border-color);
border-radius: 0.375rem; border-radius: 0.375rem;
padding: 1rem;
min-height: 300px; min-height: 300px;
padding-left: 1.5rem; padding: 1rem 1.5rem;
padding-right: 1.5rem;
white-space: pre-wrap; white-space: pre-wrap;
font-family: inherit; font-family: inherit;
} }
@@ -249,4 +321,61 @@ body {
#cookie-accept:hover { #cookie-accept:hover {
background: rgba(var(--bs-primary-rgb), 0.1); background: rgba(var(--bs-primary-rgb), 0.1);
} }
/* --- Стили для контента блога (Typography) --- */
.post-page-content {
padding-bottom: 2rem;
margin-bottom: 6rem;
}
.post-page-content h1, .post-page-content h2, .post-page-content h3,
.post-page-content h4, .post-page-content h5, .post-page-content h6 {
color: var(--bs-body-color);
opacity: 90%;
font-weight: 300;
padding-top: 1rem;
padding-bottom: 0.5rem;
}
.post-page-content p, .post-page-content li {
margin-bottom: 0.5rem;
line-height: 1.6;
font-size: 1.1rem;
}
.post-page-content > div.lead {
font-size: 1.25rem;
font-weight: 400;
opacity: 95%;
border: 1px dashed var(--bs-border-color);
}
.post-page-content > div.lead > p {
margin-bottom: 0;
padding: 0.5rem;
}
.post-content ul, .page-content ul,
.post-content ol, .page-content ol {
margin-bottom: 1.5rem;
}
.post-content blockquote, .page-content blockquote {
border-left: 4px solid var(--bs-primary);
padding-left: 1rem;
margin: 1.5rem 0;
font-style: italic;
/*color: var(--bs-secondary-color);*/
}
.post-page-content a {
color: var(--bs-linkcolor);
text-decoration: none;
border-bottom: 1px dotted var(--bs-linkcolor);
}
.post-page-content a:hover {
color: var(--bs-linkclolor-hover);
border-bottom: 1px solid var(--bs-linkclolor-hover);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -14,11 +14,11 @@
// Слушать изменения // Слушать изменения
darkModeMediaQuery.addEventListener('change', updateTheme); darkModeMediaQuery.addEventListener('change', updateTheme);
// --- ЛОГОТИП И СКРОЛЛ --- // --- ЛОГОТИП И СКРОЛЛ ---
function updateLogo() { function updateLogo() {
const navbar = document.getElementById('logo'); const navbar = document.getElementById('logo');
if (!navbar) return; if (!navbar) return;
const scrollY = window.scrollY; const scrollY = window.scrollY;
// Гистерезис: включаем после 60px, выключаем до 10px // Гистерезис: включаем после 60px, выключаем до 10px
@@ -30,17 +30,32 @@
} }
} }
// Инициализация логотипа при загрузке и скролле // Инициализация логотипа при загрузке и скролле
// document.addEventListener('DOMContentLoaded', updateLogo);
window.addEventListener('scroll', updateLogo, { passive: true }); window.addEventListener('scroll', updateLogo, { passive: true });
// --- МОБИЛЬНОЕ МЕНЮ (Скрытие логотипа при открытии) ---
document.addEventListener('DOMContentLoaded', function() {
const navbarNav = document.getElementById('navbarNav');
const navbarBrand = document.querySelector('.navbar-brand');
if (navbarNav && navbarBrand) {
navbarNav.addEventListener('show.bs.collapse', function () {
navbarBrand.style.opacity = '0';
navbarBrand.style.transition = 'opacity 0.3s ease';
});
navbarNav.addEventListener('hide.bs.collapse', function () {
navbarBrand.style.opacity = '1';
});
}
});
// --- КУКИ И СЧЕТЧИКИ --- // --- КУКИ И СЧЕТЧИКИ ---
const COOKIE_KEY = 'cookie_consent'; const COOKIE_KEY = 'cookie_consent';
const TTL_MS = 90 * 24 * 60 * 60 * 1000; // 90 дней const TTL_MS = 90 * 24 * 60 * 60 * 1000; // 90 дней
const MAILRU_ID = "3734603"; const MAILRU_ID = "3734603";
const YANDEX_ID = "106310834"; const YANDEX_ID = "106310834";
const GOOGLE_ID = "G-03WY2S9FXB";
function loadCounters() { function loadCounters() {
// console.log("Загрузка счетчиков..."); // console.log("Загрузка счетчиков...");
@@ -67,6 +82,22 @@
trackLinks:true, trackLinks:true,
accurateTrackBounce:true accurateTrackBounce:true
}); });
// Google Analytics
(function() {
var script = document.createElement('script');
script.async = true;
script.src = 'https://www.googletagmanager.com/gtag/js?id=' + GOOGLE_ID;
document.head.appendChild(script);
})();
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
// Делаем gtag глобальной, чтобы вызывать из sendGoal
window.gtag = gtag;
gtag('js', new Date());
gtag('config', '\'' + GOOGLE_ID + '\'');
} catch (e) { } catch (e) {
console.error("Ошибка загрузки счетчиков:", e); console.error("Ошибка загрузки счетчиков:", e);
} }
@@ -115,12 +146,18 @@
// console.log("Sending goal:", goalName); // console.log("Sending goal:", goalName);
try { try {
// Mail.ru
if (window._tmr) { if (window._tmr) {
window._tmr.push({ id: MAILRU_ID, type: "reachGoal", goal: goalName, value: 1 }); window._tmr.push({ id: MAILRU_ID, type: "reachGoal", goal: goalName, value: 1 });
} }
// Яндекс.Метрика
if (typeof window.ym === 'function') { if (typeof window.ym === 'function') {
window.ym(YANDEX_ID, 'reachGoal', goalName); window.ym(YANDEX_ID, 'reachGoal', goalName);
} }
// Google Analytics
if (typeof window.gtag === 'function') {
window.gtag('event', goalName);
}
} catch (e) { } catch (e) {
console.error("Ошибка отправки цели:", e); console.error("Ошибка отправки цели:", e);
} }

5
public/static/robots.txt Normal file
View File

@@ -0,0 +1,5 @@
User-agent: *
Allow: /
Host: https://typograph.cube2.ru
Sitemap: https://typograph.cube2.ru/sitemap.xml

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 45 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 45 KiB

View File

@@ -15,6 +15,7 @@ etpgrf = "^0.1.3"
# lxml = "^5.1" # etpgrf подтянет как зависимость # lxml = "^5.1" # etpgrf подтянет как зависимость
# regex = "^2023.12" # etpgrf подтянет как зависимость # regex = "^2023.12" # etpgrf подтянет как зависимость
# beautifulsoup4 = "^4.10.0" # etpgrf подтянет как зависимость # beautifulsoup4 = "^4.10.0" # etpgrf подтянет как зависимость
pillow = "^12.1.0"
[build-system] [build-system]
requires = ["poetry-core"] requires = ["poetry-core"]