A personal website built with Astro.
See the site in action:
- https://jarema.me
- https://z.is-a.dev
- https://jarema.vercel.app (backup)
- https://jarema.netlify.app (backup)
This is an Astro website using islands architecture, static HTML by default and React for some specific interactive components.
- Static-first, most components are
.astro - Interactive islands are implemented with React (
.tsx) through Preactclient:only="preact"is used for client-only UI (for example, command bar)client:loadis used where hydration after SSR is wanted (for example, games)
- Locale is inferred in middleware and injected into
Astro.locals.lang - Blog HTML is post-processed in middleware to wrap standalone images into
<figure><figcaption>using image alt text
The project supports both Vercel and Netlify adapters. The adapter is selected based on environment variables:
- Default adapter:
@astrojs/vercel - If
NETLIFYenvironment variable is set:@astrojs/netlify
Build output is static (output: "static") with dynamic runtime endpoints only where explicitly configured.
- Static site generation with Astro
- Tailwind CSS for styling
- Content collections for blog posts, now, projects, uses, scrapbook, and webrings
- Dark/light theme support
- Multilingual support
- Accessible (WCAG 2.1 AA)
- Responsive design
- RSS, Atom, JSON feeds per language, sitemap
- IndieWeb conventions (h-card, IndieAuth, Webmention)
- Global command bar and keyboard navigation
- Games: 2048, Tetris
- Tools: text counter
- Node.js 18+
- pnpm (preferred) or npm
# Install dependencies
pnpm install
# Start development server
pnpm dev
# Build for production
pnpm build
# Preview production build
pnpm preview| Command | Action |
|---|---|
pnpm dev |
Start dev server at localhost:4321 |
pnpm build |
Build production output to ./dist/ |
pnpm preview |
Preview production build |
pnpm check |
Run Astro + TypeScript checks |
public/
├── fonts/ # Self-hosted web fonts
├── keys/ # Public PGP and SSH keys
├── cursor/ # Custom cursor GIFs
├── robots.txt # Ask bad robots to go away politely :D
├── site.webmanifest # Manifest file
└── ... # Other static assets
src/
├── content.config.ts # Content collections config
├── middleware.ts # Injects lang into Astro.locals from URL
├── env.d.ts # TypeScript types
├── components/ # .astro (static) and .tsx (React islands)
│ ├── blog/ # Blog components
│ ├── home/ # Homepage components
│ ├── game/ # Game components
│ ├── layout/ # Site layout components
│ ├── KeyboardShortcut.astro # Keyboard shortcut helper component
│ └── T.astro # Translation component
├── content/ # Astro content collections data
│ ├── blog/ # Blog posts, in Markdown
│ ├── now/ # Now entries, in JSON
│ ├── projects/ # Project entries, in JSON
│ ├── scrapbook/ # Scrapbook entries, in JSON
│ ├── uses/ # Uses page categories, in JSON
│ └── webrings/ # Webring membership data, in JSON
├── hooks/ # Hooks
│ ├── game/ # Game hooks
│ └── index.ts # Other hooks
├── i18n/ # Translations and language utilities
│ ├── translations/ # JSON translation files (one per locale)
│ ├── client.ts # Client-side language utilities
│ ├── index.ts # Server-side i18n utilities
│ └── routing.ts # Locale routing helpers
├── layouts/
│ ├── BaseLayout.astro # Main site layout
│ └── RetroLayout.astro # Layout for /retro/* (old style) pages
├── lib/ # Shared utilities and constants
│ ├── utils/ # General utility functions
│ ├── constants.ts # Site metadata, author info, service config
│ ├── feed.ts # RSS/Atom/JSON feed helpers
│ └── now-utils.ts # Helpers for the /now page
├── pages/ # File-based routing
│ ├── [locale]/ # Locale catch-all
│ ├── blog/ # Blog pages
│ ├── api/ # API endpoints
│ ├── ... # Other pages
│ ├── atom.xml.ts # Atom feed
│ ├── feed.json.ts # JSON feed
│ ├── rss.xml.ts # RSS feed
│ ├── sitemap.xml.ts # Sitemap
│ ├── sitemap-index.xml.ts
│ └── index.astro # Homepage
└── styles/ # Global styling
---
title: "Post title" # Required
date: "2025-01-08" # Required
updated: "2025-01-10" # Optional
excerpt: "Brief description" # Optional
language: "en" # Default: "en"
mood: "optimistic" # Default: "contemplative"
catApproved: true # Default: true
readingTime: 5 # Optional, in minutes
tags: ["tag1", "tag2"] # Optional array
categories: ["coding", "guides"] # Optional array
draft: false # Default: false
alternates: # Optional, for multilingual posts
- language: vi
slug: vietnamese-article
---
Your content goes here.File naming: src/content/blog/YYYY/MM/slug.md or src/content/blog/slug.md
The site supports dark and light modes:
- Dark mode is the default
- Theme preference stored in
localStorage - CSS variables in
:root/.light/.darkselectors (seesrc/styles/globals.css) - Theme applied via
classon<html>element - Respects user's system preference on first visit
12 languages supported: English, Tiếng Việt, Русский, Eesti, Dansk, 中文, Türkçe, Polski, Svenska, Suomi, toki pona, 漢喃
- Translations in
src/i18n/translations/*.json - Use the
T.astrocomponent for static translations - Language state managed via
languageStorenanostore - Language detection from browser or localStorage
GET /api/lastfm: fetches recent Last.fm tracksGET /api/premidandPOST /api/premid: PreMID activity bridgeGET /.well-known/discord: host-based Discord verification token (because I have multiple domains)
- RSS endpoint:
src/pages/rss.xml.ts - Atom endpoint:
src/pages/atom.xml.ts - JSON feed endpoint:
src/pages/feed.json.ts - Sitemap:
src/pages/sitemap.xml.ts - XML stylesheets are provided in
public/*.xsl
When to use .astro vs .tsx:
- Use
.astrofor static content, layouts, pages - Use
.tsxwithclient:only="react"for interactive features (command bar, language toggle, games)
Island hydration examples:
<!-- Static component -->
<StaticComponent />
<!-- Client-only interactive island -->
<CommandBar client:only="react" />
<!-- Hydrated interactive island -->
<Game2048 client:load />- Deployment: Vercel (default) and Netlify
- IndieAuth: indieauth.com
- Webmentions: webmention.io
- Last.fm: music listening data via API
- PreMID: Discord activity for the Now section
- imood and status.cafe: mood and status widgets
- Analytics: Umami script proxied under
/stats/*
TL;DR: You are free to use, modify, redistribute, and sell the source code and the content on this website, provided proper attribution is given back to me. Attribution should include a link to my website (https://jarema.me).
This repository is dual-licensed.
All source code is licensed under the MIT License.
All non-code content, such as text, posts, essays, documentation, photos, videos, and other materials, is licensed under the Creative Commons Attribution 4.0 International License (CC BY 4.0).
These licenses do not apply to third-party libraries, assets, or content included in the project. Third-party components and assets are subject to their own respective licenses.
