Оглавление


Первый старт — с чего начать

Прежде чем публиковать записи — настройте три вещи в src/config/theme.ts. Всё остальное можно менять постепенно.

Шаг 1 — Название и описание

export const site = {
  name: 'Название вашего блога',    // ← поменять
  description: 'Краткое описание',  // ← поменять
  version: 'v0.1.0',                // ← можно оставить или убрать ('')
  foundedYear: 2026 as number | undefined, // ← ваш год основания или undefined
  umamiWebsiteId: undefined as string | undefined, // пока оставить
} as const;

Шаг 2 — Ваши ссылки в подвале

Найдите footer.links и замените на свои:

links: [
  {
    href:  'https://t.me/ваш_канал',
    icon:  'telegram',
    label: 'Telegram',
  },
  {
    href:  '/rss.xml',
    icon:  'rss',
    label: 'RSS лента',
  },
],

Иконки — SVG-файлы из папки src/icons/. Укажите имя файла без расширения.

Шаг 3 — Навигация в сайдбаре

Найдите navigation и поправьте ссылку на страницу «О блоге»:

export const navigation: NavLink[] = [
  { href: '/p/about',  label: 'Моя страница', labelEn: 'About',  icon: 'about'  },
  { href: '/',         label: 'Лента',         labelEn: 'Feed',   icon: 'feed'   },
  { href: '/search',   label: 'Поиск',         labelEn: 'Search', icon: 'search' },
];

Отредактируйте src/content/ru/pages/about.md — это ваша страница «О себе».

Что НЕ трогать при первом запуске

Эти блоки уже настроены правильно — не удаляйте и не меняйте без необходимости:

ЧтоПочему
fonts.faces[]Шрифты Roboto и Fira Code подключены и кешируются
colorsТёмная/светлая тема уже работает
categories[]Все категории (post, video, book, game, photo) настроены
ui (ru/en)Строки интерфейса локализованы
Helpers в конце файлаСлужебные функции — не менять

Структура файла theme.ts

1.  site          — название, версия, год, Umami ID
2.  typography    — размеры шрифтов статей и параграфов
3.  spacing       — отступы между блоками
4.  colors        — цвета тёмной и светлой темы
5.  textColors    — цвета отдельных элементов (лента, статья, навигация)
6.  layout        — ширина страницы и сайдбара
7.  sidebar       — иконки, отступы, кнопка темы
8.  header        — шапка, размер логотипа
9.  langToggle    — переключатель языков (по умолчанию скрыт)
10. categories    — рубрики: post, video, book, game, photo
11. navigation    — ссылки в сайдбаре
12. shortNotes    — поведение коротких заметок
13. stats         — страница статистики
14. feed          — лента: количество постов, типографика строки
15. shortNote     — оформление карточки заметки
16. feedTags      — теги в ленте и статьях
17. fonts         — шрифты: семейства, файлы woff2
18. footer        — подвал: иконки, ссылки
19. ui            — строки интерфейса (ru/en)
20. faqLink       — ссылка FAQ в подвале
21. gdprLink      — ссылка «Конфиденциальность» в подвале (по умолчанию скрыта)

Настройка сайта

Всё настраивается в одном файле: src/config/theme.ts.

Название, описание, версия

export const site = {
  name: 'The Seventy Eight',   // название блога в шапке и тегах <title>
  description: 'Дневник настоящего времени',
  version: 'v0.5.0',           // версия — отображается в подвале
  foundedYear: 2023 as number | undefined,
  umamiWebsiteId: undefined as string | undefined,
} as const;

name — появляется в шапке сайта и в <title> каждой страницы.

version — строка, которую вы сами назначаете. Показывается в подвале справа. Можно убрать, если не нужна — установите пустую строку ''.

Год основания

Поле foundedYear управляет диапазоном лет в подвале:

// Показывает «2023 – 2026» (если текущий год ≠ 2023)
foundedYear: 2023 as number | undefined,

// Показывает только текущий год «2026»
foundedYear: undefined,

Обновлять не нужно — правый год берётся автоматически из new Date().getFullYear().

Umami Website ID

После настройки Umami (см. раздел Umami — аналитика) вставьте ID из дашборда:

// Без аналитики (по умолчанию)
umamiWebsiteId: undefined as string | undefined,

// С аналитикой — вставить реальный ID из Umami Settings → Websites
umamiWebsiteId: 'a13c1aa1-7e44-41a6-9844-1d0bebbc4111' as string | undefined,

ID копируется из Umami dashboard → Settings → Websites → выбрать сайт → Website ID.

Скрипт Umami не подгружается на страницах с noindex (поиск, категории, теги, пагинация) — только на реальных публикациях. Личные страницы и черновики не трекируются.


Внешний вид — цвета

Цвета тёмной и светлой темы

Секция colors в theme.ts:

export const colors = {
  // ── Тёмная тема (по умолчанию) ──
  bg:          '#0f0f0f',   // фон страницы
  surface:     '#1a1a1a',   // фон карточек, кода, инпутов
  border:      '#2e2e2e',   // границы, разделители
  text:        '#e2e2e2',   // основной текст
  textMuted:   '#888888',   // второстепенный текст (даты, мета)
  accent:      '#c5c5c6',   // акцентный цвет (ссылки при наведении, иконки)

  // ── Светлая тема ──
  bgLight:          '#f5f4f8',
  surfaceLight:     '#eceaf2',
  borderLight:      '#d8d5e6',
  textLight:        '#111111',
  textMutedLight:   '#4a4a4a',
  accentLight:      '#171717',
} as const;

Пример: сделать акцент ярко-синим

// До
accent: '#c5c5c6',

// После
accent: '#5b8dee',

Пример: светлая тема с тёплым белым фоном

bgLight:      '#faf9f7',
surfaceLight: '#f0ede8',
borderLight:  '#dedad2',

Цвета по контексту

Секция textColors позволяет задать цвета для конкретных элементов независимо от темы. По умолчанию они ссылаются на переменные темы (var(--text), var(--accent)).

export const textColors = {
  // Лента
  feedTitle:       'var(--text)',        // заголовок поста
  feedTitleHover:  'var(--accent)',      // заголовок при наведении
  feedDate:        'var(--text-muted)',  // дата слева
  feedDateHover:   'var(--accent)',

  // Статья
  articleTitle:    'var(--text)',
  articleLink:     'var(--accent)',      // ссылки в тексте

  // Навигация
  navLink:         'var(--text-muted)',
  navLinkHover:    'var(--text)',
  navLinkActive:   'var(--accent)',

  // Шапка и подвал
  logo:            'var(--accent)',
  footer:          'var(--text-muted)',
} as const;

Пример: зафиксировать цвет ссылок независимо от темы

// До — цвет меняется при переключении тем
articleLink: 'var(--accent)',

// После — всегда синий, в любой теме
articleLink: '#4a90e2',

Добавление цвета для категории

У каждой категории есть свой color, который используется в навигации:

{
  id: 'post',
  label: 'Записи',
  icon: 'post',
  color: '#7b9cff',   // ← цвет иконки этой категории
  ...
},

Внешний вид — типографика и шрифты

Размеры шрифтов

Секция typography в theme.ts:

export const typography = {
  base: '16px',            // базовый размер — используется как html font-size

  // Заголовки в статьях
  h2: '1.3rem',

  // Страница статьи
  articleTitle: '1.75rem',
  articleBody:  '1rem',
  articleMeta:  '0.875rem',  // дата, мета-информация
  codeInline:   '0.9em',
  codeBlock:    '0.875rem',

  // Параграфы
  p:             '1rem',
  pLineHeight:   1.7,
  pMarginBottom: '1rem',

  lineHeight: {
    normal:  1.5,    // шапка, сайдбар
    relaxed: 1.75,   // статьи
  },
} as const;

Пример: сделать текст статьи крупнее

// До
articleBody: '1rem',

// После — чуть крупнее, особенно удобно на мобильном
articleBody: '1.05rem',

Подключение собственного шрифта

Шаги:

  1. Скачайте файл .woff2 (например, на gwfh.mranftl.com)
  2. Положите в public/fonts/inter-regular.woff2
  3. Добавьте запись в fonts.faces:
faces: [
  { family: 'Inter', src: '/fonts/inter-regular.woff2', weight: '400', style: 'normal', preload: true },
  { family: 'Inter', src: '/fonts/inter-bold.woff2',    weight: '700', style: 'normal' },
]
  1. Укажите шрифт в fonts.families:
families: {
  body:    "'Inter', system-ui, sans-serif",
  heading: "'Inter', system-ui, sans-serif",
  code:    "'Fira Code', ui-monospace, monospace",
  ...
}

Разные шрифты для RU и EN

Если хотите использовать разные шрифты для русских и английских страниц:

families: {
  body:      "'Roboto', system-ui, sans-serif",    // для RU
  bodyEn:    "'Inter', system-ui, sans-serif",      // для EN (если undefined — используется body)
  heading:   "'Roboto', system-ui, sans-serif",
  headingEn: undefined,  // EN использует heading (Roboto)
}

Текущие шрифты

РольШрифтФайл
Основной текстRobotopublic/fonts/roboto-v51-*.woff2
Код, датыFira Codepublic/fonts/fira-code-v27-*.woff2

Создание записей

Структура файлов

Все записи хранятся в src/content/ru/ (русские) и src/content/en/ (английские).

src/content/ru/
├── post-slug.md       ← запись категории "post" (Записи)
├── video-slug.md      ← видео
├── book-slug.md       ← книги
├── game-slug.md       ← игры
├── photo-slug.md      ← галерея
├── short/
│   └── 2026-03-18.md  ← короткая заметка
└── pages/
    └── about.md       ← страница "О блоге"

URL записи совпадает с именем файла: my-post.md/ru/article/my-post

Все поля frontmatter

---
title: Название записи
date: 2026-03-18
description: Краткое описание для поиска и мета-тегов
draft: false         # не обязательно (по умолчанию false)
searchable: true     # не обязательно (по умолчанию true)
category: post
tags: [anime, review, 2026]
cover: /images/my-post-cover.jpg
background: /images/my-post-bg.jpg
---

Текст статьи начинается здесь.

Минимальный вариант — только обязательные поля:

---
title: Название записи
date: 2026-03-18
---

Текст статьи.

Описание полей

title — заголовок записи. Обязательное поле.

date — дата публикации в формате YYYY-MM-DD. Влияет на порядок в ленте.

description — краткое описание. Показывается в поиске и в <meta description>. Если не задан — используется начало текста.

draft: true — черновик. Запись не появляется в ленте, поиске и RSS. Доступна по прямой ссылке если знать URL.

# Черновик — скрыт везде
draft: true

# Опубликована (по умолчанию)
draft: false

searchable: false — запись видна в ленте и RSS, но не попадает в поиск (/search). Полезно для записей, которые лучше читать в контексте ленты.

# Исключить из поиска, но оставить в ленте
searchable: false

category — рубрика записи. Доступные значения:

ЗначениеРазделURL
postЗаписи/ru/post/
videoВидео/ru/video/
bookКниги/ru/book/
gameИгры/ru/game/
photoГалерея/ru/photo/
# Рубрика «Книги»
category: book

tags — список тегов. Синтаксис:

# Правильно — квадратные скобки
tags: [anime, review, 2026]

# Правильно — с пробелами в кавычках
tags: ["мои мысли", anime, 2026]

# Правильно — блочный стиль
tags:
  - anime
  - review

# НЕПРАВИЛЬНО — незакрытая кавычка (Astro не загрузит файл!)
tags: ["трамвай, "автобус"]

Обложка статьи

cover — изображение, которое показывается в начале статьи и в Open Graph (превью ссылки).

cover: /images/my-post-cover.jpg

Положите файл в public/images/my-post-cover.jpg. Рекомендуемые размеры: 1200×630 px (стандарт OG), формат JPEG или WebP.

Фоновое изображение в ленте

background — тонкое фоновое изображение для строки в ленте. Видно как атмосферный фон справа от заголовка.

background: /images/my-post-bg.jpg

Рекомендуемые размеры: 1600×400 px, широкое горизонтальное изображение. Формат: JPEG 80–85% или WebP.

Поведение фона настраивается в theme.tsfeed:

rowBgSize:     'cover',          // 'cover' | 'auto 100%' | 'auto 150%'
rowBgPosition: 'right center',   // откуда виден фон

Короткие заметки

Короткие заметки — это небольшие записи, которые показываются в общей ленте в виде карточек.

Создание заметки

Файл: src/content/ru/short/2026-03-18.md

---
date: 2026-03-18
title: Короткий заголовок заметки
---

Текст заметки. Мысли, наблюдения, ссылки.

title рекомендуется — по нему работает поиск, и его видно в результатах. Если не задан — в поиске используются первые 60 символов текста, что хуже читается.

Без заголовка (минимум):

---
date: 2026-03-18
---

Текст заметки.

Исключить из поиска:

---
date: 2026-03-18
searchable: false
---

Настройки коротких заметок

В theme.tsshortNotes:

export const shortNotes = {
  enabled:    true,    // включить / выключить
  showInNav:  true,    // показывать в меню
  showInFeed: true,    // включать в общую ленту
  label:    'Мысли',   // название в меню
  labelEn:  'Notes',
  icon: 'short',       // иконка из src/icons/
  path: '/short',
} as const;

URL отдельной заметки: /ru/short/2026-03-18 Индекс всех заметок: /ru/short (не индексируется поисковиками)


Страницы

Страницы — это отдельные материалы: «О блоге», «Контакты», этот FAQ.

Файлы: src/content/ru/pages/about.md, src/content/ru/pages/faq.md

Создание страницы

---
title: О блоге
date: 2024-01-01
description: Краткое описание для мета-тегов
summary: Подзаголовок под h1 (показывается на странице)
draft: false
searchable: true
cover: /images/about-cover.jpg
---

Текст страницы...

URL: /ru/p/about, /ru/p/faq

Добавить ссылку в навигацию

В theme.tsnavigation:

export const navigation: NavLink[] = [
  { href: '/p/about',  label: 'Моя страница', labelEn: 'About',  icon: 'about'  },
  { href: '/',         label: 'Лента',         labelEn: 'Feed',   icon: 'feed'   },
  { href: '/search',   label: 'Поиск',         labelEn: 'Search', icon: 'search' },
];

Чтобы добавить ссылку на свою страницу — добавьте строку в массив. Иконка — имя SVG-файла из src/icons/ (без расширения).

FAQ-ссылка в подвале

В theme.tsfaqLink:

export const faqLink = {
  enabled:  false,        // true — показывать ссылку FAQ в подвале
  href:     '/p/faq',
  icon:     'faq',
  label:    'FAQ',
  labelEn:  'FAQ',
} as const;

Поиск и черновики

Как работает поиск

Поиск доступен по адресу /ru/search. Он работает на клиенте через библиотеку Fuse.js — без серверных запросов.

Индекс поиска: /ru/search.json (генерируется при сборке).

В индекс попадают:

  • Записи (postRu): не черновики + searchable: true
  • Страницы (pagesRu): не черновики + searchable: true
  • Короткие заметки (shortRu): только searchable: true

Управление индексацией

НужноFrontmatter
Виден везде (стандарт)ничего не указывать
Скрыть из поиска, но оставить в лентеsearchable: false
Скрыть везде (черновик)draft: true

Как работает robots.txt

Файл public/robots.txt уже настроен правильно:

User-agent: *
Allow: /

Disallow: /ru/search
Disallow: /en/search
Disallow: /ru/page/
Disallow: /en/page/
Disallow: /ru/tag/
Disallow: /en/tag/
Disallow: /ru/post/
...
Disallow: /ru/short/$
Disallow: /en/short/$

Sitemap: https://theseventyeight.org/sitemap-index.xml

Одиночные статьи (/ru/article/…), страницы (/ru/p/…) и заметки (/ru/short/slug) — индексируются. Служебные страницы (поиск, теги, пагинация, категорийные списки) — не индексируются.


Nginx — настройка сервера

Готовый конфиг находится в templates_nginx.conf в корне репозитория.

Быстрое применение

# Шаг 1. Скопировать на сервер
scp templates_nginx.conf project@ваш-сервер:/tmp/

# Шаг 2. Применить (Раздел 2 файла — готовый конфиг)
sudo cp /tmp/templates_nginx.conf /etc/nginx/conf.d/theseventyeight.conf

# Шаг 3. Проверить синтаксис — ОБЯЗАТЕЛЬНО
sudo nginx -t
# Ожидаемый ответ:
# nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
# nginx: configuration file /etc/nginx/nginx.conf test is successful

# Шаг 4. Применить без остановки сайта
sudo systemctl reload nginx

Полный конфиг сервера

server {
    server_name theseventyeight.org www.theseventyeight.org;

    root /home/project/nana/dist;
    index index.html;

    charset utf-8;

    gzip              on;
    gzip_vary         on;
    gzip_proxied      any;
    gzip_comp_level   6;
    gzip_min_length   1024;
    gzip_types
        text/html text/css text/plain
        application/javascript application/json
        application/xml application/rss+xml image/svg+xml;

    # Кеш — _astro/ и fonts/ навсегда (хэш в имени файла)
    location /_astro/ {
        add_header Cache-Control "public, max-age=31536000, immutable";
        expires 1y;
    }
    location /images/ {
        add_header Cache-Control "public, max-age=604800";
        expires 7d;
    }
    location /fonts/ {
        add_header Cache-Control "public, max-age=31536000, immutable";
        add_header Access-Control-Allow-Origin "*";
        expires 1y;
    }
    location ~* \.html$ {
        add_header Cache-Control "no-cache";
    }

    # Security headers
    add_header X-Frame-Options             "SAMEORIGIN"                        always;
    add_header X-Content-Type-Options      "nosniff"                           always;
    add_header Referrer-Policy             "strict-origin-when-cross-origin"   always;
    add_header Strict-Transport-Security   "max-age=31536000; includeSubDomains" always;

    # i18n редиректы: старые URL → /ru/...
    rewrite ^/article/(.*)$                      /ru/article/$1  permanent;
    rewrite ^/short/(.*)$                        /ru/short/$1    permanent;
    rewrite ^/p/(.*)$                            /ru/p/$1        permanent;
    rewrite ^/tag/(.*)$                          /ru/tag/$1      permanent;
    rewrite ^/page/(.*)$                         /ru/page/$1     permanent;
    rewrite ^/(post|video|book|game|photo)(/.*)?$ /ru/$1$2       permanent;
    rewrite ^/search$                            /ru/search      permanent;
    rewrite ^/stats$                             /ru/stats       permanent;

    location / {
        try_files $uri $uri.html $uri/index.html =404;
    }

    location = /rss.xml {
        add_header Content-Type  "application/rss+xml; charset=utf-8";
        add_header Cache-Control "public, max-age=3600";
    }

    # Umami Analytics (трекинг публично, дашборд только через SSH)
    location = /analytics/script.js {
        proxy_pass         http://127.0.0.1:3000/script.js;
        proxy_set_header   Host $host;
        add_header         Cache-Control "public, max-age=86400";
    }
    location /analytics/api/ {
        proxy_pass         http://127.0.0.1:3000/api/;
        proxy_set_header   Host              $host;
        proxy_set_header   X-Real-IP         $remote_addr;
        proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $scheme;
    }

    location = /site.webmanifest {
        add_header Cache-Control "public, max-age=86400";
        add_header Content-Type  "application/manifest+json";
    }

    error_page 404 /404.html;
    location = /404.html { internal; }

    access_log /var/log/nginx/theseventyeight.access.log;
    error_log  /var/log/nginx/theseventyeight.error.log warn;

    # SSL (управляется Certbot — не менять вручную)
    listen 443 ssl;
    ssl_certificate /etc/letsencrypt/live/theseventyeight.org/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/theseventyeight.org/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}

# HTTP → HTTPS редирект
server {
    if ($host = theseventyeight.org) {
        return 301 https://$host$request_uri;
    }
    listen 80;
    server_name theseventyeight.org www.theseventyeight.org;
    return 404;
}

Umami — аналитика

Umami — это self-hosted аналитика. Работает в Docker на сервере.

Что установлено

КомпонентРасположение
docker-compose.ymlumami/docker-compose.yml
.env (пароли)umami/.envне в git
.env.exampleumami/.env.example

Первый запуск

# 1. Зайти в папку umami на сервере
cd ~/nana/umami

# 2. Скопировать шаблон окружения
cp .env.example .env

# 3. Сгенерировать пароли (каждый раз — новые!)
openssl rand -hex 16   # для POSTGRES_PASSWORD
openssl rand -hex 16   # для APP_SECRET

# 4. Вставить пароли в .env
nano .env

# 5. Запустить
docker compose up -d

# 6. Проверить что работает
docker compose ps

docker-compose.yml

services:
  umami:
    image: ghcr.io/umami-software/umami:postgresql-latest
    ports:
      - "127.0.0.1:3000:3000"   # только localhost — наружу не открыт!
    environment:
      DATABASE_URL: postgresql://umami:${POSTGRES_PASSWORD}@db:5432/umami
      APP_SECRET: ${APP_SECRET}
      DISABLE_TELEMETRY: 1
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: umami
      POSTGRES_USER: umami
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - umami-db:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U umami"]
      interval: 5s
      timeout: 5s
      retries: 10
    restart: unless-stopped

volumes:
  umami-db:

.env (шаблон)

POSTGRES_PASSWORD=сюда_вставить_сгенерированный_пароль
APP_SECRET=сюда_вставить_сгенерированный_секрет

Доступ к дашборду

Дашборд не открыт публично — только через SSH-туннель.

Команда (заменить на свои значения):

ssh -L 3000:localhost:3000 ВАШ_ПОЛЬЗОВАТЕЛЬ@IP_СЕРВЕРА -p ВАШ_ПОРТ

Пример:

# project — имя пользователя на сервере (у вас может быть root или другое)
# 1.2.3.4 — IP-адрес сервера (или домен — оба варианта работают)
# 6543    — ваш SSH-порт (стандартный: 22)
ssh -L 3000:localhost:3000 project@1.2.3.4 -p 6543

Если хотите оставить туннель в фоне и вернуться в терминал — добавьте &:

ssh -L 3000:localhost:3000 project@1.2.3.4 -p 6543 &

После подключения открыть в браузере: http://localhost:3000

Логин по умолчанию: admin / umamiсменить сразу после первого входа!

Подключить к блогу

  1. Зайти в Umami → Settings → Websites → Add website
  2. Скопировать Website ID
  3. Вставить в theme.ts:
umamiWebsiteId: 'ваш-id-здесь' as string | undefined,
  1. Пересобрать и задеплоить блог

Управление

# Остановить
docker compose down

# Перезапустить
docker compose up -d

# Логи
docker compose logs -f umami

# Обновить до новой версии
docker compose pull && docker compose up -d

Совместимость браузеров

Блог тестировался на современных браузерах. Известные проблемы:

Jelly (LineageOS Browser)

Поддержка отсутствует — особенно на старых версиях LineageOS. Возможны проблемы с:

  • CSS-переменными (var(--...))
  • color-mix() (смешивание цветов в теме)
  • JavaScript-островами (Preact)

Это браузер на устаревшем движке WebKit — фиксить нецелесообразно.

Браузерные расширения

Ошибки вида moz-extension://... или chrome-extension://... в консоли — это баги расширений браузера, не сайта. Игнорировать.

Кеш браузера

После деплоя новой версии у пользователей может остаться старый кеш. HTML-страницы намеренно кешируются с Cache-Control: no-cache — браузер проверяет свежесть при каждом визите. JavaScript и CSS в /_astro/ кешируются навсегда (имя файла содержит хеш — при обновлении меняется).

HTTP 304

Ответ сервера 304 Not Modified — это норма. Браузер спрашивает «файл изменился?», сервер отвечает «нет» — браузер использует кеш. Не ошибка.

Свайп «назад» в браузере

Свайп влево (iOS Safari, Chrome Android) работает как стандартная кнопка «назад» браузера — поддерживается нативно, без кода. Кнопка «Назад к ленте» в конце статьи всегда возвращает на главную страницу ленты — независимо от истории навигации.