|
| 1 | +# Project Overview |
| 2 | + |
| 3 | +## What Is This |
| 4 | + |
| 5 | +Personal website and blog for Leszek Pomianowski (lepoco), deployed at **<https://lepo.co>**. |
| 6 | +Repository: `lepoco/.github`, branch: `main`. |
| 7 | + |
| 8 | +## Tech Stack |
| 9 | + |
| 10 | +- **Astro 5** (v5.13.2) — static site generator (SSG mode) |
| 11 | +- **@astrojs/sitemap** — auto-generated sitemap |
| 12 | +- **@astrojs/rss** — RSS feed generation |
| 13 | +- No UI framework (pure Astro components, zero client JS except theme toggle + menu) |
| 14 | + |
| 15 | +## Commands |
| 16 | + |
| 17 | +```bash |
| 18 | +npm run dev # Dev server at localhost:4321 |
| 19 | +npm run build # Production build to dist/ |
| 20 | +npm run preview # Preview production build |
| 21 | +``` |
| 22 | + |
| 23 | +## Project Structure |
| 24 | + |
| 25 | +``` |
| 26 | +src/ |
| 27 | +├── components/ |
| 28 | +│ ├── FormattedDate.astro # Locale-aware date (en-US / pl-PL) |
| 29 | +│ └── PostCard.astro # Blog post card, derives slug from post.id |
| 30 | +├── content/ |
| 31 | +│ ├── posts/en/ # English blog posts (markdown) |
| 32 | +│ └── posts/pl/ # Polish blog posts (markdown) |
| 33 | +├── i18n/ |
| 34 | +│ ├── ui.ts # Translation dictionary + Lang type |
| 35 | +│ └── utils.ts # getLangFromUrl(), useTranslations() |
| 36 | +├── layouts/ |
| 37 | +│ ├── BaseLayout.astro # Master layout (head, header, nav, footer, theme toggle) |
| 38 | +│ └── PostLayout.astro # Blog post wrapper (title, date, tags, reading time) |
| 39 | +├── pages/ |
| 40 | +│ ├── index.astro # EN homepage |
| 41 | +│ ├── about.astro # EN about |
| 42 | +│ ├── contact.astro # EN contact |
| 43 | +│ ├── support.astro # EN support/consulting |
| 44 | +│ ├── privacy.astro # Privacy policy (shared, uses fallback for PL) |
| 45 | +│ ├── 404.astro # EN 404 |
| 46 | +│ ├── rss.xml.js # EN RSS feed |
| 47 | +│ ├── posts/ |
| 48 | +│ │ ├── index.astro # EN posts listing |
| 49 | +│ │ └── [...slug].astro # EN single post (dynamic route) |
| 50 | +│ ├── tags/ |
| 51 | +│ │ ├── index.astro # EN tags listing |
| 52 | +│ │ └── [tag].astro # EN posts by tag |
| 53 | +│ └── pl/ # Polish pages (mirror of EN structure) |
| 54 | +│ ├── index.astro |
| 55 | +│ ├── about.astro |
| 56 | +│ ├── contact.astro |
| 57 | +│ ├── support.astro |
| 58 | +│ ├── 404.astro |
| 59 | +│ ├── rss.xml.js |
| 60 | +│ ├── posts/ |
| 61 | +│ │ ├── index.astro |
| 62 | +│ │ └── [...slug].astro |
| 63 | +│ └── tags/ |
| 64 | +│ ├── index.astro |
| 65 | +│ └── [tag].astro |
| 66 | +├── styles/ # CSS modules (no preprocessor) |
| 67 | +│ ├── terminal.css # Theme variables (:root), body reset |
| 68 | +│ ├── main.css # Layout, container, links |
| 69 | +│ ├── header.css # Site header |
| 70 | +│ ├── menu.css # Mobile/desktop menu |
| 71 | +│ ├── footer.css # Footer |
| 72 | +│ ├── post.css # Post content styles |
| 73 | +│ ├── code.css # Inline/block code |
| 74 | +│ ├── syntax.css # Shiki syntax highlighting CSS variables |
| 75 | +│ ├── buttons.css # Button components |
| 76 | +│ ├── pagination.css # Post pagination |
| 77 | +│ ├── gist.css # GitHub Gist embeds |
| 78 | +│ ├── fonts.css # Font declarations |
| 79 | +│ └── terms.css # Legal/terms pages |
| 80 | +└── content.config.ts # Content collection (glob loader, zod schema) |
| 81 | +``` |
| 82 | + |
| 83 | +## i18n Architecture |
| 84 | + |
| 85 | +### Routing |
| 86 | + |
| 87 | +Configured in `astro.config.mjs`: |
| 88 | + |
| 89 | +- **Default locale**: `en` (no prefix — pages at `/`, `/about/`, `/posts/...`) |
| 90 | +- **Polish**: `pl` (prefix — pages at `/pl/`, `/pl/about/`, `/pl/posts/...`) |
| 91 | +- **Fallback**: `pl` → `en` with `fallbackType: 'rewrite'` (if PL page doesn't exist, EN is served at `/pl/` URL) |
| 92 | +- `prefixDefaultLocale: false` |
| 93 | + |
| 94 | +### Translations |
| 95 | + |
| 96 | +- **Dictionary**: `src/i18n/ui.ts` — exports `ui` object with `en` and `pl` keys, ~120 translation strings |
| 97 | +- **Type**: `Lang = 'en' | 'pl'` |
| 98 | +- **Helper**: `useTranslations(lang)` returns `t(key)` function with EN fallback |
| 99 | + |
| 100 | +### Patterns Used in Pages |
| 101 | + |
| 102 | +Every page follows this boilerplate: |
| 103 | + |
| 104 | +```astro |
| 105 | +--- |
| 106 | +import { defaultLang, type Lang } from '../../i18n/ui'; |
| 107 | +import { useTranslations } from '../../i18n/utils'; |
| 108 | +
|
| 109 | +const lang = (Astro.currentLocale || defaultLang) as Lang; |
| 110 | +const t = useTranslations(lang); |
| 111 | +--- |
| 112 | +``` |
| 113 | + |
| 114 | +- `Astro.currentLocale` — built-in Astro 5 API, automatically set based on route |
| 115 | +- `getRelativeLocaleUrl(lang, 'path')` from `astro:i18n` — used for all internal links |
| 116 | +- `BaseLayout` does NOT take a `lang` prop (reads `Astro.currentLocale` internally) |
| 117 | +- `PostCard` derives slug from `post.id` internally: `post.id.replace(/^(en|pl)\//, '')` |
| 118 | + |
| 119 | +### Content Collections |
| 120 | + |
| 121 | +- Posts stored in `src/content/posts/en/` and `src/content/posts/pl/` |
| 122 | +- Loader: `glob({ base: './src/content/posts', pattern: '**/*.{md,mdx}' })` |
| 123 | +- `post.id` format: `en/hello-friend` or `pl/hello-friend` (no `.md` extension) |
| 124 | +- `render()` imported from `astro:content` (Astro 5 API): `const { Content } = await render(post)` |
| 125 | +- Filtering by language: `post.id.startsWith('en/')` or `post.id.startsWith('pl/')` |
| 126 | + |
| 127 | +### Language Switcher |
| 128 | + |
| 129 | +In `BaseLayout.astro` header — computes alternate URL by stripping locale prefix from current path and using `getRelativeLocaleUrl(altLang, pathSegment)`. |
| 130 | + |
| 131 | +### Browser Auto-Detection |
| 132 | + |
| 133 | +Homepage includes client-side `navigator.language` check — redirects to `/pl/` if browser language starts with `pl`. |
| 134 | + |
| 135 | +## Theming |
| 136 | + |
| 137 | +### CSS Variables (in `terminal.css`) |
| 138 | + |
| 139 | +| Variable | Light | Dark | |
| 140 | +|---|---|---| |
| 141 | +| `--background` | `#ffffff` | `#1b1b1f` | |
| 142 | +| `--foreground` | `#000000` | `#e3e3e3` | |
| 143 | +| `--accent` | `#6b7f5c` | `#8fa882` | |
| 144 | + |
| 145 | +Dark theme inspired by Microsoft Learn dark mode (soft grays instead of pure black/white). |
| 146 | + |
| 147 | +### Theme Toggle |
| 148 | + |
| 149 | +Three states: `auto` → `light` → `dark` → `auto` (cycled on click). |
| 150 | +Persisted in `localStorage.theme`. JS in `BaseLayout.astro` sets CSS variables on `:root`. |
| 151 | + |
| 152 | +### Syntax Highlighting |
| 153 | + |
| 154 | +Uses Shiki `css-variables` theme. Token colors defined in `syntax.css` using `--astro-code-*` variables — monochrome approach where keywords/tags use `--accent`, rest uses `--foreground`. |
| 155 | + |
| 156 | +## Post Schema (Zod) |
| 157 | + |
| 158 | +```ts |
| 159 | +z.object({ |
| 160 | + title: z.string(), |
| 161 | + description: z.string().optional(), |
| 162 | + pubDate: z.coerce.date(), |
| 163 | + updatedDate: z.coerce.date().optional(), |
| 164 | + author: z.string().optional(), |
| 165 | + image: z.string().optional(), |
| 166 | + avifImage: z.string().optional(), |
| 167 | + externalLink: z.string().optional(), |
| 168 | + tags: z.array(z.string()).default([]), |
| 169 | + draft: z.boolean().default(false), |
| 170 | +}) |
| 171 | +``` |
| 172 | + |
| 173 | +## Build Output |
| 174 | + |
| 175 | +61 pages (EN + PL + fallbacks). Build completes in ~1s. |
| 176 | + |
| 177 | +## Conventions |
| 178 | + |
| 179 | +- **No client-side JS frameworks** — static HTML with minimal vanilla JS (theme toggle, menu) |
| 180 | +- **All links use `getRelativeLocaleUrl()`** — never hardcode `/pl/` or `/en/` prefixes |
| 181 | +- **No `lang` prop on BaseLayout** — it reads `Astro.currentLocale` automatically |
| 182 | +- **PostCard receives `post` directly** — no need to strip slug prefixes externally |
| 183 | +- **Font**: system sans-serif stack (no custom font files loaded) |
| 184 | +- **Container width**: 1200px max |
0 commit comments