Table of Contents
- First Start — Where to Begin
- Site Settings
- Appearance — Colors
- Appearance — Typography & Fonts
- Creating Posts
- Short Notes
- Pages
- Search & Drafts
- Nginx — Server Setup
- Umami — Analytics
- Deploying to Server
- Browser Compatibility
First Start — Where to Begin
Before publishing posts — configure three things in src/config/theme.ts. Everything else can be changed gradually.
Step 1 — Name and Description
export const site = {
name: 'Your Blog Name', // ← change this
description: 'Short description', // ← change this
version: 'v0.1.0', // ← keep or remove ('')
foundedYear: 2026 as number | undefined, // ← your founding year or undefined
umamiWebsiteId: undefined as string | undefined, // leave for now
} as const;
Step 2 — Your Footer Links
Find footer.links and replace with your own:
links: [
{
href: 'https://t.me/your_channel',
icon: 'telegram',
label: 'Telegram',
},
{
href: '/rss.xml',
icon: 'rss',
label: 'RSS Feed',
},
],
Icons are SVG files from the src/icons/ folder. Specify the filename without extension.
Step 3 — Sidebar Navigation
Find navigation and update the “About” page link:
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' },
];
Edit src/content/en/pages/about.md — this is your “About” page.
What NOT to Touch on First Launch
These blocks are already configured correctly — do not delete or change them without a reason:
| What | Why |
|---|---|
fonts.faces[] | Roboto and Fira Code fonts are connected and cached |
colors | Dark/light theme already works |
categories[] | All categories (post, video, book, game, photo) are configured |
ui (ru/en) | Interface strings are localized |
| Helpers at the end of the file | Utility functions — do not change |
Structure of theme.ts
1. site — name, version, year, Umami ID
2. typography — font sizes for articles and paragraphs
3. spacing — gaps between blocks
4. colors — dark and light theme colors
5. textColors — colors for individual elements (feed, article, navigation)
6. layout — page and sidebar width
7. sidebar — icons, padding, theme button
8. header — header, logo size
9. langToggle — language toggle (hidden by default)
10. categories — categories: post, video, book, game, photo
11. navigation — sidebar links
12. shortNotes — short notes behavior
13. stats — statistics page
14. feed — feed: post count, row typography
15. shortNote — note card styling
16. feedTags — tags in feed and articles
17. fonts — fonts: families, woff2 files
18. footer — footer: icons, links
19. ui — interface strings (ru/en)
20. faqLink — FAQ link in footer
21. gdprLink — Privacy link in footer (hidden by default)
Site Settings
Everything is configured in one file: src/config/theme.ts.
Name, Description, Version
export const site = {
name: 'The Seventy Eight', // blog name in the header and <title> tags
description: 'A diary of the present',
version: 'v0.5.0', // version string — shown in the footer
foundedYear: 2023 as number | undefined,
umamiWebsiteId: undefined as string | undefined,
} as const;
name — appears in the site header and in the <title> of every page.
version — a string you assign yourself. Shown in the footer on the right. Remove it by setting an empty string ''.
Founded Year
The foundedYear field controls the year range in the footer:
// Shows «2023 – 2026» (when current year ≠ 2023)
foundedYear: 2023 as number | undefined,
// Shows only the current year «2026»
foundedYear: undefined,
No manual updates needed — the right year is taken automatically from new Date().getFullYear().
Umami Website ID
After setting up Umami (see Umami — Analytics), paste the ID from the dashboard:
// No analytics (default)
umamiWebsiteId: undefined as string | undefined,
// With analytics — paste the real ID from Umami Settings → Websites
umamiWebsiteId: 'a13c1aa1-7e44-41a6-9844-1d0bebbc4111' as string | undefined,
Copy the ID from Umami dashboard → Settings → Websites → select site → Website ID.
The Umami script is not loaded on
noindexpages (search, categories, tags, pagination) — only on real publications. Personal pages and drafts are not tracked.
Appearance — Colors
Dark and Light Theme Colors
The colors section in theme.ts:
export const colors = {
// ── Dark theme (default) ──
bg: '#0f0f0f', // page background
surface: '#1a1a1a', // card backgrounds, code, inputs
border: '#2e2e2e', // borders, dividers
text: '#e2e2e2', // main text
textMuted: '#888888', // secondary text (dates, meta)
accent: '#c5c5c6', // accent color (hover links, icons)
// ── Light theme ──
bgLight: '#f5f4f8',
surfaceLight: '#eceaf2',
borderLight: '#d8d5e6',
textLight: '#111111',
textMutedLight: '#4a4a4a',
accentLight: '#171717',
} as const;
Example: make the accent color bright blue
// Before
accent: '#c5c5c6',
// After
accent: '#5b8dee',
Example: warm white background for light theme
bgLight: '#faf9f7',
surfaceLight: '#f0ede8',
borderLight: '#dedad2',
Context Colors
The textColors section lets you set colors for specific elements independently of the theme. By default they reference theme variables (var(--text), var(--accent)).
export const textColors = {
// Feed
feedTitle: 'var(--text)', // post title
feedTitleHover: 'var(--accent)', // post title on hover
feedDate: 'var(--text-muted)', // date on the left
feedDateHover: 'var(--accent)',
// Article
articleTitle: 'var(--text)',
articleLink: 'var(--accent)', // links in the text
// Navigation
navLink: 'var(--text-muted)',
navLinkHover: 'var(--text)',
navLinkActive: 'var(--accent)',
// Header and footer
logo: 'var(--accent)',
footer: 'var(--text-muted)',
} as const;
Example: fix link color regardless of theme
// Before — color changes when switching themes
articleLink: 'var(--accent)',
// After — always blue, in any theme
articleLink: '#4a90e2',
Category Colors
Each category has its own color used in the navigation:
{
id: 'post',
label: 'Posts',
icon: 'post',
color: '#7b9cff', // ← color for this category's icon
...
},
Appearance — Typography & Fonts
Font Sizes
The typography section in theme.ts:
export const typography = {
base: '16px', // base size — used as html font-size
// Headings inside articles
h2: '1.3rem',
// Article page
articleTitle: '1.75rem',
articleBody: '1rem',
articleMeta: '0.875rem', // date, meta information
codeInline: '0.9em',
codeBlock: '0.875rem',
// Paragraphs
p: '1rem',
pLineHeight: 1.7,
pMarginBottom: '1rem',
lineHeight: {
normal: 1.5, // header, sidebar
relaxed: 1.75, // articles
},
} as const;
Example: make article text slightly larger
// Before
articleBody: '1rem',
// After — slightly larger, especially comfortable on mobile
articleBody: '1.05rem',
Adding a Custom Font
Steps:
- Download a
.woff2file (e.g., from gwfh.mranftl.com) - Place it in
public/fonts/inter-regular.woff2 - Add an entry to
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' },
]
- Set the font in
fonts.families:
families: {
body: "'Inter', system-ui, sans-serif",
heading: "'Inter', system-ui, sans-serif",
code: "'Fira Code', ui-monospace, monospace",
...
}
Different Fonts for RU and EN
If you want different fonts for Russian and English pages:
families: {
body: "'Roboto', system-ui, sans-serif", // for RU
bodyEn: "'Inter', system-ui, sans-serif", // for EN (if undefined — uses body)
heading: "'Roboto', system-ui, sans-serif",
headingEn: undefined, // EN uses heading (Roboto)
}
Current Fonts
| Role | Font | File |
|---|---|---|
| Body text | Roboto | public/fonts/roboto-v51-*.woff2 |
| Code, dates | Fira Code | public/fonts/fira-code-v27-*.woff2 |
Creating Posts
File Structure
All posts are stored in src/content/ru/ (Russian) and src/content/en/ (English).
src/content/en/
├── post-slug.md ← post in "post" category (Posts)
├── video-slug.md ← video
├── book-slug.md ← books
├── game-slug.md ← games
├── photo-slug.md ← gallery
├── short/
│ └── 2026-03-18.md ← short note
└── pages/
└── about.md ← "About" page
The post URL matches the filename: my-post.md → /en/article/my-post
All Frontmatter Fields
---
title: Post Title
date: 2026-03-18
description: Short description for search and meta tags
draft: false # optional (default: false)
searchable: true # optional (default: true)
category: post
tags: [anime, review, 2026]
cover: /images/my-post-cover.jpg
background: /images/my-post-bg.jpg
---
Article text starts here.
Minimal version — only required fields:
---
title: Post Title
date: 2026-03-18
---
Article text.
Field Descriptions
title — post title. Required.
date — publication date in YYYY-MM-DD format. Determines order in the feed.
description — short description. Shown in search and in <meta description>. If not set — the beginning of the text is used.
draft: true — draft. The post does not appear in the feed, search, or RSS. Accessible via direct link if you know the URL.
# Draft — hidden everywhere
draft: true
# Published (default)
draft: false
searchable: false — the post is visible in the feed and RSS, but does not appear in search (/search). Useful for posts that are better read in the context of the feed.
# Exclude from search, but keep in feed
searchable: false
category — post category. Available values:
| Value | Section | URL |
|---|---|---|
post | Posts | /en/post/ |
video | Video | /en/video/ |
book | Books | /en/book/ |
game | Games | /en/game/ |
photo | Gallery | /en/photo/ |
# Category «Books»
category: book
tags — list of tags. Syntax:
# Correct — square brackets
tags: [anime, review, 2026]
# Correct — with spaces in quotes
tags: ["my thoughts", anime, 2026]
# Correct — block style
tags:
- anime
- review
# WRONG — unclosed quote (Astro will not load the file!)
tags: ["tram, "bus"]
Post Cover
cover — image shown at the start of the article and in Open Graph (link preview).
cover: /images/my-post-cover.jpg
Place the file in public/images/my-post-cover.jpg. Recommended size: 1200×630 px (OG standard), JPEG or WebP.
Feed Background Image
background — a subtle background image for the row in the feed. Visible as an atmospheric background to the right of the title.
background: /images/my-post-bg.jpg
Recommended size: 1600×400 px, wide horizontal image. Format: JPEG 80–85% or WebP.
Background behavior is configured in theme.ts → feed:
rowBgSize: 'cover', // 'cover' | 'auto 100%' | 'auto 150%'
rowBgPosition: 'right center', // which edge is visible
Short Notes
Short notes are small posts shown in the main feed as cards.
Creating a Note
File: src/content/en/short/2026-03-18.md
---
date: 2026-03-18
title: Short note title
---
Note text. Thoughts, observations, links.
title is recommended — search works by it, and it shows up in results. If not set — the first 60 characters of text are used in search, which is harder to read.
Without a title (minimum):
---
date: 2026-03-18
---
Note text.
Exclude from search:
---
date: 2026-03-18
searchable: false
---
Short Notes Settings
In theme.ts → shortNotes:
export const shortNotes = {
enabled: true, // enable / disable
showInNav: true, // show in menu
showInFeed: true, // include in main feed
label: 'Мысли', // name in menu (Russian)
labelEn: 'Notes',
icon: 'short', // icon from src/icons/
path: '/short',
} as const;
Individual note URL: /en/short/2026-03-18
All notes index: /en/short (not indexed by search engines)
Pages
Pages are standalone content: “About”, “Contacts”, this FAQ.
Files: src/content/en/pages/about.md, src/content/en/pages/faq.md
Creating a Page
---
title: About the Blog
date: 2024-01-01
description: Short description for meta tags
summary: Subtitle shown under h1 on the page
draft: false
searchable: true
cover: /images/about-cover.jpg
---
Page text...
URL: /en/p/about, /en/p/faq
Adding a Link to Navigation
In theme.ts → 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' },
];
To add a link to your page — add a line to the array. Icon is the SVG filename from src/icons/ (without extension).
FAQ Link in Footer
In theme.ts → faqLink:
export const faqLink = {
enabled: true, // true — show FAQ link in footer
href: '/p/faq',
icon: 'faq',
label: 'FAQ',
labelEn: 'FAQ',
} as const;
Search & Drafts
How Search Works
Search is available at /en/search. It runs on the client via Fuse.js — no server requests.
Search index: /en/search.json (generated at build time).
What’s included in the index:
- Posts (
postEn): non-drafts +searchable: true - Pages (
pagesEn): non-drafts +searchable: true - Short notes (
shortEn): onlysearchable: true
Controlling Indexation
| Goal | Frontmatter |
|---|---|
| Visible everywhere (default) | nothing to specify |
| Hide from search, keep in feed | searchable: false |
| Hide everywhere (draft) | draft: true |
How robots.txt Works
The file public/robots.txt is already correctly configured:
User-agent: *
Allow: /
Disallow: /ru/search
Disallow: /en/search
Disallow: /ru/page/
Disallow: /en/page/
...
Disallow: /ru/short/$
Disallow: /en/short/$
Sitemap: https://theseventyeight.org/sitemap-index.xml
Individual articles (/en/article/…), pages (/en/p/…), and notes (/en/short/slug) are indexed.
Utility pages (search, tags, pagination, category lists) are not indexed.
Nginx — Server Setup
The ready-to-use config is in templates_nginx.conf in the repository root.
Quick Application
# Step 1. Copy to server
scp templates_nginx.conf project@your-server:/tmp/
# Step 2. Apply (Section 2 of the file — the ready config)
sudo cp /tmp/templates_nginx.conf /etc/nginx/conf.d/theseventyeight.conf
# Step 3. Check syntax — REQUIRED
sudo nginx -t
# Expected output:
# nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
# nginx: configuration file /etc/nginx/nginx.conf test is successful
# Step 4. Apply without stopping the site
sudo systemctl reload nginx
Full Server Config
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;
# Cache — _astro/ and fonts/ forever (hash in filename)
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 redirects: old URLs → /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 (tracking public, dashboard only via 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 (managed by Certbot — do not edit manually)
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 redirect
server {
if ($host = theseventyeight.org) {
return 301 https://$host$request_uri;
}
listen 80;
server_name theseventyeight.org www.theseventyeight.org;
return 404;
}
Umami — Analytics
Umami is self-hosted analytics. It runs in Docker on the server.
What’s Installed
| Component | Location |
|---|---|
docker-compose.yml | umami/docker-compose.yml |
.env (passwords) | umami/.env ← not in git |
.env.example | umami/.env.example |
First Launch
# 1. Go to the umami folder on the server
cd ~/nana/umami
# 2. Copy the environment template
cp .env.example .env
# 3. Generate passwords (always generate fresh ones!)
openssl rand -hex 16 # for POSTGRES_PASSWORD
openssl rand -hex 16 # for APP_SECRET
# 4. Insert passwords into .env
nano .env
# 5. Start
docker compose up -d
# 6. Verify it's running
docker compose ps
docker-compose.yml
services:
umami:
image: ghcr.io/umami-software/umami:postgresql-latest
ports:
- "127.0.0.1:3000:3000" # localhost only — not exposed publicly!
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 (template)
POSTGRES_PASSWORD=paste_generated_password_here
APP_SECRET=paste_generated_secret_here
Dashboard Access
The dashboard is not publicly accessible — only via SSH tunnel.
Command (replace with your own values):
ssh -L 3000:localhost:3000 YOUR_USER@SERVER_IP -p YOUR_PORT
Example:
# project — your server username (could be root or anything else)
# 1.2.3.4 — your server IP address (or domain — both work)
# 6543 — your SSH port (standard: 22)
ssh -L 3000:localhost:3000 project@1.2.3.4 -p 6543
To keep the tunnel in the background and return to the terminal — add &:
ssh -L 3000:localhost:3000 project@1.2.3.4 -p 6543 &
Then open in browser: http://localhost:3000
Default login: admin / umami — change immediately after first login!
Connecting to the Blog
- Go to Umami → Settings → Websites → Add website
- Copy the Website ID
- Paste in
theme.ts:
umamiWebsiteId: 'your-id-here' as string | undefined,
- Rebuild and deploy the blog
Management Commands
# Stop
docker compose down
# Restart
docker compose up -d
# Logs
docker compose logs -f umami
# Update to new version
docker compose pull && docker compose up -d
Deploying to Server
Deployment Structure
The repository lives in ~/nana/ on the server. Deployment = git pull + npm run build.
# Connect to server
ssh project@1.2.3.4 -p 6543
# Go to project folder
cd ~/nana
# Pull latest changes from GitHub
git pull
# Rebuild
npm run build
Automated Deploy via deploy.sh
The deploy.sh script in the repository root automates the process:
./deploy.sh
If the server has local changes and git pull fails — the script will offer to reset them.
Branches
| Branch | Purpose |
|---|---|
main | primary — deploy to prod from here |
release | stable release |
# Make sure you're on main
git status
# All new commits go to main
git checkout main
git add .
git commit -m "feat: new post"
git push
Browser Compatibility
The blog was tested on modern browsers. Known issues:
Jelly (LineageOS Browser)
No support — especially on older LineageOS versions. Possible issues with:
- CSS variables (
var(--...)) color-mix()(color mixing in themes)- JavaScript islands (Preact)
This browser uses an outdated WebKit engine — not worth fixing.
Browser Extensions
Errors like moz-extension://... or chrome-extension://... in the console are extension bugs, not site bugs. Ignore them.
Browser Cache
After deploying a new version, users may have old cached content. HTML pages are intentionally cached with Cache-Control: no-cache — the browser checks freshness on each visit. JavaScript and CSS in /_astro/ are cached forever (the filename contains a hash — it changes on update).
HTTP 304
A 304 Not Modified server response is normal. The browser asks “has the file changed?”, the server replies “no” — the browser uses cache. Not an error.
Swipe Back in Browser
Swipe left (iOS Safari, Chrome Android) works as the browser’s standard back button — supported natively without any code. The “Back to feed” button at the end of articles always returns to the main feed page — regardless of navigation history.