personal linktree alternative. https://nghia.link
  • TypeScript 45%
  • Astro 36.3%
  • HTML 10.2%
  • CSS 5.8%
  • Dockerfile 2.2%
  • Other 0.5%
Find a file
nghialele 3e8e96c25e docs: update README for SSR mode, live Spotify, and Node.js server
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
2026-06-14 18:00:31 +07:00
.commandcode feat: Docker deployment, component polish, and repo cleanup (#4) 2026-06-14 01:28:43 -07:00
docker feat: Docker deployment, component polish, and repo cleanup (#4) 2026-06-14 01:28:43 -07:00
public feat: Docker deployment, component polish, and repo cleanup (#4) 2026-06-14 01:28:43 -07:00
src feat: live Spotify polling with React island and /api/spotify endpoint 2026-06-14 17:49:32 +07:00
.dockerignore feat: Docker deployment, component polish, and repo cleanup (#4) 2026-06-14 01:28:43 -07:00
.env.example fix: pass env vars as Docker build args for static site generation 2026-06-14 15:58:14 +07:00
.gitignore fix: pass env vars as Docker build args for static site generation 2026-06-14 15:58:14 +07:00
AGENTS.md feat: Docker deployment, component polish, and repo cleanup (#4) 2026-06-14 01:28:43 -07:00
astro.config.mjs feat: switch to SSR for live data fetching on every request 2026-06-14 16:07:19 +07:00
CLAUDE.md feat: Docker deployment, component polish, and repo cleanup (#4) 2026-06-14 01:28:43 -07:00
docker-compose.yml feat: switch to SSR for live data fetching on every request 2026-06-14 16:07:19 +07:00
Dockerfile fix: copy settings.json to production container for SSR runtime 2026-06-14 16:24:14 +07:00
package.json feat: switch to SSR for live data fetching on every request 2026-06-14 16:07:19 +07:00
pnpm-lock.yaml feat: switch to SSR for live data fetching on every request 2026-06-14 16:07:19 +07:00
pnpm-workspace.yaml feat: Docker deployment, component polish, and repo cleanup (#4) 2026-06-14 01:28:43 -07:00
README.md docs: update README for SSR mode, live Spotify, and Node.js server 2026-06-14 18:00:31 +07:00
tsconfig.json feat: Docker deployment, component polish, and repo cleanup (#4) 2026-06-14 01:28:43 -07:00

nghia.link

"It's not a bug, it's a feature. It's not a crash, it's an unscheduled downtime."

A personal link hub that's prettier than your average terminal output. Think of it as the digital equivalent of those business cards that just say "I make things".

what's this magician doing?

A single-page link aggregator that looks like it was designed by someone who has strong opinions about font rendering and checks Steam stats more than their email. Dark mode only.

the stack (nerd stuff)

  • Astro 6 — SSR mode, ships less JavaScript than your React app, and that's a compliment
  • React — only where interactivity actually needs to happen (I have principles)
  • TypeScript — catch my typos before I ship them to prod
  • Tailwind CSS v4 — I'm still finding utility classes I forgot existed
  • Lucide React — icons that don't look like they survived Y2K
  • @astrojs/node — standalone Node.js server for SSR, no nginx needed

features (the flex)

  • Dark/light mode — toggle in the top-right corner because some people have preferences
  • Green accent — is it terminal green? Is it matrix? Is it just easier on the eyes? Yes.
  • Staggered animations — links don't just appear, they arrive like they have somewhere to be
  • Responsive design — works on phone, tablet, desktop, and that one smart fridge that somehow has a browser
  • SSR with live data — every page visit fetches fresh data from APIs, no stale builds
  • Now Playing — what I'm vibing to on Spotify, live-polled every 30 seconds (clickable so you can suffer with me)
  • CS2 Stats — K/D, kills, headshots, win rate, matches, wins — yes I'm that guy who checks after every session
  • Dev Stats — top language, active repos, and self-hosted count across GitHub and Forgejo
  • Age counter — how long I've been alive, displayed so you can do the math and feel old
  • Random footer quotes — phrases that change so you never get bored of me
  • Service icons — Discord, Steam, SoundCloud, GitHub — little logos where they belong
  • One config filesettings.json because scattered configs are a cry for help

getting started (if you want to clone my homework)

# install dependencies (pnpm recommended — fight me)
pnpm install

# spin up the dev server
pnpm dev

# build for production
pnpm build

# preview what you just built
pnpm preview

project structure (the org chart nobody asked for)

nghia.link/
├── src/
│   ├── components/
│   │   ├── astro/          # server-rendered components, no JavaScript overhead
│   │   │   ├── ProfileHeader.astro  # avatar, name, tagline, the whole show
│   │   │   ├── LinkList.astro       # link cards in a grid, fancy
│   │   │   ├── LinkCard.astro       # individual links with brand icons
│   │   │   ├── StatSection.astro    # grouped stat cards
│   │   │   ├── StatCard.astro       # single stat display, small and proud
│   │   │   └── CS2Stats.astro       # CS2 stats grid, the real reason I'm online
│   │   └── react/          # client-side components, limited edition
│   │       ├── SpotifyNowPlaying.tsx # live-polled Spotify widget (30s interval)
│   │       ├── Footer.tsx            # quotes, year, copyright
│   │       └── ThemeToggle.tsx       # toggle button, groundbreaking
│   ├── layouts/
│   │   └── Layout.astro    # base HTML, fonts, metadata, the boring stuff that works
│   ├── lib/
│   │   ├── settings.ts     # loads settings.json with env var interpolation + .env loading
│   │   ├── stats.ts        # fetches and aggregates stats from everywhere
│   │   ├── age.ts          # calculates age because I'm curious
│   │   ├── quotes.json     # footer quotes, probably change these
│   │   └── apis/           # API integrations, the magic happens here
│   │       ├── steam.ts    # Steam Web API, CS2 stats extractor
│   │       ├── spotify.ts  # Spotify OAuth, what I'm listening to
│   │       ├── github.ts   # GitHub API, language stats, active repos
│   │       └── forgejo.ts  # Forgejo API, self-hosted git stats
│   ├── pages/
│   │   ├── index.astro     # main page, where everything comes together
│   │   └── api/
│   │       └── spotify.ts  # live Spotify data endpoint (polled by React island)
│   ├── styles/
│   │   └── global.css      # theme tokens, animations, the visual sauce
│   └── settings.json       # the one ring to rule them all
├── Dockerfile              # multi-stage build: Node.js SSR server
├── docker-compose.yml      # one-command deploy with env vars
└── public/
    └── favicon.ico         # the favicon, probably a terminal icon or something

configuration (tweak at your own risk)

All content lives in src/settings.json. Change it, break it, make it yours:

  • profile — name, tagline, avatar emoji, tags (be honest)
  • birthdate — for the age/uptime counter (yes I know my age is public now)
  • links — your links with icons and descriptions
  • stats — fallback static values when APIs are being difficult
  • apis — API configs (Steam, Spotify, GitHub, Forgejo)

API keys use ${ENV_VAR} syntax and get swapped out at runtime. Environment variables are your friend.

API setup

API Env Vars What It Does
Steam STEAM_API_KEY, STEAM_ID CS2 stats — K/D, kills, headshots, wins, matches
Spotify SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET, SPOTIFY_REFRESH_TOKEN Now playing + recent tracks
GitHub GITHUB_TOKEN Commits, repos, stars — git commit counter
Forgejo FORGEJO_TOKEN Self-hosted git stats for the homelabbers

Links get brand icons when the service is recognized:

  • discord — Discord logo (ping me)
  • steam — Steam logo (add me)
  • soundcloud — SoundCloud waveform (listen to my vibes)
  • github — GitHub logo (watch my code)
  • forgejo — Git branch icon (self-hosted gang)
  • globe — generic fallback (we don't know everything)

why does this exist?

I got tired of Linktree. I wanted something that looked like me, not a SaaS landing page with my links duct-taped to it. Also I needed an excuse to use Astro and pretend I understand CSS animations.

This site loads fast, looks clean, and doesn't track you. It's a personal page in an era of personal brands. No analytics, no cookies, no newsletter popups. Just vibes and links.

🐋 deploying with docker

Forget "works on my machine." Now it's "works in my container" and that's significantly more professional.

quick start (the TL;DR for the impatient)

# Clone and deploy — finally, a one-liner that actually works
git clone https://git.nghia.app/nghialele/nghia.link.git
cd nghia.link

# Copy the env template and fill in your secrets
cp .env.example .env

# Fire it up
docker-compose up -d

# Visit http://localhost:8080 — you're welcome

prerequisites

  • Docker (obviously — we aren't doing this the hard way)
  • Docker Compose (v2+ recommended, but v1 works too if you're vintage)

configuration (env vars are your friends)

Create a .env file in the project root with your API keys:

# Steam — so you can flex your CS2 stats publicly
STEAM_API_KEY=your_steam_web_api_key
STEAM_ID=your_steam_id_64

# Spotify — share your music taste (or lack thereof)
SPOTIFY_CLIENT_ID=your_spotify_client_id
SPOTIFY_CLIENT_SECRET=your_spotify_client_secret
SPOTIFY_REFRESH_TOKEN=your_spotify_refresh_token

# GitHub — because your commit count matters
GITHUB_TOKEN=your_github_token

# Forgejo — for the homelab squad
FORGEJO_TOKEN=your_forgejo_token

# Optional: change the port (default is 8080)
PORT=3000

docker commands (for when things go sideways)

# Start the container — the easy part
docker-compose up -d

# Watch the logs — for when you need to pretend you're debugging
docker-compose logs -f

# Restart after changes — because why not
docker-compose restart

# Stop everything — cleanup is important, kids
docker-compose down

# Full rebuild — nuking from orbit style
docker-compose down && docker-compose build --no-cache && docker-compose up -d

# Shell into the container — for the brave
docker exec -it nghia-link /bin/sh

how it works

The Docker image builds an Astro SSR site and runs it as a standalone Node.js server on port 4321. Docker Compose maps it to port 8080 (or your custom PORT). Env vars are passed at runtime so the server can fetch live data on every request.

port forwarding (for the network-savvy)

# Default: http://localhost:8080
PORT=80 docker-compose up -d          # changes external port to 80

# Custom port
PORT=3000 docker-compose up -d        # http://localhost:3000

# HTTPS? Put a reverse proxy in front (Caddy, Traefik, etc.)

building without docker-compose

# For the minimalists
docker build -t nghia.link .
docker run -p 8080:4321 --env-file .env nghia.link

# Check if it's alive
curl http://localhost:8080

🤖 deploying on cool hosting platforms

This site works great with:

Platform How Notes
Railway Connect repo + set env vars Auto-deploys on push
Fly.io fly launch + fly deploy Edge deployment, my beloved
Render Connect repo + set env vars Free tier is surprisingly good
Coolify Self-hosted GitOps For the "I have too many VMs" crew
Docker Swarm docker stack deploy When you want to feel important
Kubernetes Apply the manifests When you're solving problems you don't have

license

Do whatever. Fork it, break it, make it yours. If you build something cool, @ me. If you build something cursed, definitely @ me


Built with caffeine, curiosity, and an unhealthy obsession with dark mode toggles.

Containerized with love and a standalone Node.js server.