All projects

ProfileWebNORM

★ 0 stars↓ 0 downloads
Open on GitHub ↗

README.md

normno.com — portfolio website

A production portfolio website with a liquid-glass aesthetic: animated blob backgrounds, backdrop blur with a glass effect, and a ripple effect on clickable elements. Dark and light themes switch automatically based on the device’s theme (prefers-color-scheme). The navigation bar features a signature smiley face; clicking it triggers a blinking animation (Easter egg). Screenshot 2026-08-14 at 01 21 09

Stack: Astro + Svelte islands · Node 22 · SQLite (Drizzle ORM) · Caddy · Docker Compose. Designed for a low-spec VPS (512 MB RAM): everything is as static and cached as possible, a single process, the site never accesses GitHub or Telegram at the time of a user’s request.

Features

  • Homepage — “About Me” section (Markdown text and links are edited in the admin panel, photos are uploaded there as well), minimalist social media buttons, the last 3 releases across all projects, a Spotify widget (current track; if nothing is playing—the last track played), the last 3 posts, and an activity heatmap with a GitHub / Claude toggle ✨.
  • Projects—Hard Code / Vibe Code tabs, search and sorting (new releases, downloads, stars; the default option is set in the admin panel). Card: cover art, title, description, stars, total downloads. Project page: rendered README, releases with dates and download counts for each asset, list of open issues.
  • Download menu — “Download the latest version” and a button next to each release expand to show a list of files: a platform icon based on the file extension (Windows, macOS, Linux, Android, iOS, Java, archive), size, and number of downloads. The file for the visitor’s OS is highlighted at the top and labeled “for your OS.” If there is only one file, it’s a regular link.
  • Publications—Markdown posts with images from the admin panel and imported from the Telegram channel (along with photos and albums), source badge, sorted by date. Site posts are also published to the channel (rich-markdown, Bot API 10.1); edits are synchronized in both directions without duplicates; synchronized posts have a blue airplane icon and a link to the channel post. Details: docs/TELEGRAM.md.
  • Project cover images—by default, GitHub’s og-image; in the admin panel, you can select an image from the repository’s README (only the URL is stored) or upload your own (only in this case is the file stored on the server, in data/uploads).
  • RSS — a feed of posts on /rss.xml (+ auto-detection in <head>).
  • Backups in Telegram—every day at 4:30 AM, the bot sends an archive of the database and uploaded files to your private chat (TELEGRAM_BACKUP_CHAT_ID), plus a manual backup button in the admin panel.
  • Two language versions—Russian on “clean” URLs, English at /en (/en/publications/12). The language is determined by Accept-Language: if the browser isn’t set to Russian, the English version opens; the visitor’s selection is stored in a cookie, and the round button in the corner switches the language on the same page. We do not redirect bots based on language—each one receives exactly the URL it requested.
  • Automatic translation via API—the interface is translated manually (no quota is used), while content is translated using Google Cloud Translation (or DeepL/Azure, as desired) with a cache in SQLite. New posts are translated immediately after publication; older pages are translated lazily on the first visit. Editing a post (in the admin panel or on the Telegram channel) marks the translation as outdated based on the source’s hash and triggers a new translation; until it’s ready, the previous English text is displayed instead of the Russian version. The README, which is already in English, is identified based on the percentage of Cyrillic characters and is not translated at all. Nothing runs locally: for 2 GB of RAM, this is the only viable option. Details: docs/TRANSLATE.md.
  • SEO — canonical and hreflang on every page, sitemap.xml with both language versions, dynamic robots.txt (on the admin host—completely prohibited), Open Graph and Twitter Card, JSON-LD (Person, WebSite, BlogPosting, SoftwareSourceCode, breadcrumbs), RSS in two languages, and IndexNow ping to Bing/Yandex upon publication. Checklist for migrating to a new domain: docs/SEO.md.
  • Data sync—an in-app cron job (set to run every 30 minutes by default) fetches GitHub (repos, releases + download_count, stargazers, issues, README, contribution calendar, commits involving Claude) and Telegram channel posts and writes them to SQLite. README and posts are rendered markdown-it + sanitize-html during the sync phase—the site never makes calls to external APIs at the time of a user request.
  • The admin panel includes the tabs “GitHub Projects” (visibility, Hard/Vibe, covers, default sorting, manual sync), “About Me and Links” (Markdown with preview, photos, to Spotify), “Posts” (editor with preview and photos, sending to Telegram, import, bulk deletion via checkboxes, bot diagnostics, manual backup), and “Translations” (active provider, monthly character limit usage with a progress bar, comparison of free service limits, API error analysis, translation status for each post, and manual translation). Uploaded files are stored in data/uploads, the same volume as the database.

Admin panel security: a separate subdomain

The admin panel is hidden from site visitors:

  1. /admin* returns a 404 on the main domain—and in the application (middleware checks Host), and in Caddy (requests to /admin* the main domain are not proxied at all);
  2. the admin panel can only be accessed from a subdomain within ADMIN_HOST (for example admin.normno.com) and is protected by Basic Auth (ADMIN_USER / ADMIN_PASS there .env);
  3. there is no link to the admin panel in the public navigation, robots.txt and X-Robots-Tag indexing is disabled.

Local development

npm install
cp .env.example .env   # заполнить минимум ADMIN_PASS; токены — по мере надобности
npm run dev

The first sync with GitHub runs automatically a few seconds after startup (or by clicking the “Sync with GitHub” button in the admin panel).

Deployment to a VPS (Docker Compose)

Complete guide — docs/DEPLOY.md: setting up integrations, backups and restoration, updates, troubleshooting common issues. In short:

  1. Point your DNS A records normno.com and admin.normno.com to the server’s IP address.

  2. Install Docker (curl -fsSL https://get.docker.com | sh), open ports 80 and 443.

  3. Deploy:

    git clone https://github.com/NORMss/ProfileWebNORM.git
    cd ProfileWebNORM
    cp .env.example .env
    nano .env   # SITE_DOMAIN, ADMIN_DOMAIN, ADMIN_HOST, ADMIN_PASS, TZ, токены
    docker compose up -d --build
    

    Caddy will automatically issue Let's Encrypt HTTPS certificates for both domains.

  4. Update: git pull && docker compose up -d --build (schema migrations are applied automatically; data is preserved).

The entire site’s status is stored in ./data: site.db (SQLite) and uploads/ (profile photos, cover images, post images). A backup is an archive of this directory; when configured, TELEGRAM_BACKUP_CHAT_ID the bot sends it automatically every day.

Environment variables

Variable Description
SITE_URL Public website address (for links)
SITE_DOMAIN / ADMIN_DOMAIN Domains for Caddy
ALIAS_DOMAINS Additional domains separated by spaces: a separate certificate for each, 301 redirect to SITE_DOMAIN (DEPLOY 2.1)
ADMIN_HOST Admin panel host; on other hosts /admin → 404
ADMIN_USER / ADMIN_PASS Basic Auth for the admin panel (the admin panel is disabled without a password)
GITHUB_USERNAME Whose repositories to display
GITHUB_TOKEN Classic PAT with scope public_repo + read:user: API limits and heatmap
SPOTIFY_CLIENT_ID / SPOTIFY_CLIENT_SECRET Spotify app (the widget is hidden without it)
SPOTIFY_REFRESH_TOKEN Optional: manually obtained token (connected via the admin panel takes priority)
SPOTIFY_REDIRECT_URI Optional: override the redirect URI (for local development)
TELEGRAM_BOT_TOKEN Bot for importing and publishing channel posts (the bot is the channel admin; see docs/TELEGRAM.md)
TELEGRAM_CHANNEL @username channel (or -100…); required for publishing website posts to the channel
TELEGRAM_BACKUP_CHAT_ID Personal chat_id for daily backups (empty means disabled)
TRANSLATE_PROVIDER Auto-translation provider: google / deepl / azure / none (empty — the first one with a key)
GOOGLE_TRANSLATE_API_KEY Google Cloud Translation API key (500,000 characters/month free)
DEEPL_API_KEY / AZURE_TRANSLATOR_KEY Alternative translation services, see docs/TRANSLATE.md
TRANSLATE_MONTHLY_LIMIT Your monthly character limit (0 — provider’s free limit)
INDEXNOW_KEY IndexNow key: the site automatically notifies Bing and Yandex of new posts
GOOGLE_SITE_VERIFICATION / YANDEX_VERIFICATION content= Meta tags for rights confirmation in Search Console and Webmaster Tools
DB_PATH Path to the SQLite file
SYNC_INTERVAL_MIN Sync interval, in minutes (default is 30)
TZ Container time zone — the daily backup time depends on this (4:30 a.m.)

How to connect Spotify

Everything is done via a browser (including on a phone); no terminal is required:

  1. Create an app at https://developer.spotify.com/dashboard.
  2. In its settings, add a Redirect URI—the admin panel URL: https://admin.normno.com/admin/spotify/callback (replace with your own ADMIN_DOMAIN; the exact value is shown in the admin panel).
  3. Enter SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET in .env, then restart: docker compose up -d --build.
  4. Open the admin panel → “About Me and Links” tab → “♫ Connect Spotify” → confirm access. The refresh token will be saved to the database automatically, SPOTIFY_REFRESH_TOKEN you .env you don't need to enter it.

Spotify only accepts HTTPS addresses and loopback (127.0.0.1) — so the redirect leads to the admin panel domain, where HTTPS is already configured via Caddy. For local development, set SPOTIFY_REDIRECT_URI=http://127.0.0.1:4321/admin/spotify/callback and add the same address to the Spotify app.

Manual method (if you need a token in .env)

Open this in your browser (replace with your own client_id and redirect_uri from the app), authorize access, and copy code from the address bar—the page may display an error, but this is normal:

https://accounts.spotify.com/authorize?client_id=CLIENT_ID&response_type=code&redirect_uri=REDIRECT_URI&scope=user-read-currently-playing%20user-read-recently-played
curl -X POST https://accounts.spotify.com/api/token \
  -u "CLIENT_ID:CLIENT_SECRET" \
  -d grant_type=authorization_code -d code=CODE \
  -d redirect_uri=REDIRECT_URI

refresh_token from the response → to .env as SPOTIFY_REFRESH_TOKEN (the value from .env takes precedence over the one received via the admin panel).

Structure

src/
  middleware.ts        # host-gating админки + Basic Auth + CSRF + языковой
                       # роутинг (/en, Accept-Language, куки) + запуск cron
  lib/
    config.ts          # доступ к env
    db/                # better-sqlite3 + Drizzle, DDL, миграции, дефолты
    sync/              # github.ts (репозитории, контрибуции, Claude-коммиты),
                       # telegram.ts (импорт постов и фото), планировщик
    telegram.ts        # публикация и правка постов в канале (rich-markdown)
    backup.ts          # снапшот БД + uploads → архив в Telegram
    spotify.ts         # now-playing с кешем 30 с
    markdown.ts        # markdown-it + sanitize-html
    i18n/              # языки, разбор Accept-Language, словарь строк интерфейса
    translate/         # провайдеры API, кеш переводов в SQLite, учёт лимита
    seo.ts             # canonical, hreflang, Open Graph, JSON-LD
    indexnow.ts        # пинг Bing/Яндекса при публикации
  pages/
    index/projects/publications   # публичные страницы
    rss.xml.ts                    # RSS-лента (на языке версии сайта)
    sitemap.xml.ts                # карта сайта с hreflang-альтернативами
    robots.txt.ts                 # robots: публичный хост и админ-хост по-разному
    [key].txt.ts                  # файл-подтверждение ключа IndexNow
    api/now-playing.ts            # JSON для виджета
    media/**                      # отдача загруженных изображений
    admin/**                      # админка, её API и OAuth Spotify
  components/          # Icon, Heatmap, SpotifyWidget, admin/AdminApp
docs/DEPLOY.md         # руководство по развертыванию
docs/TELEGRAM.md       # бот: посты, синхронизация, бэкапы
docs/TRANSLATE.md      # автоперевод: выбор API, лимиты, кеш, ошибки
docs/SEO.md            # индексация, hreflang, переезд на новый домен
deploy/Caddyfile       # домены: сайт, админка, алиасы с 301
deploy/local/          # свои блоки Caddy для этого сервера (не в git)

Releases

No releases yet.

Open issues

No open issues 🎉

This page was machine-translated from Russian

RU