My personal webpages powered by Next.js, TypeScript and CSS Modules.
A single Next.js app serves all three domains. proxy.ts inspects the
request host and rewrites /{pathname} to /{domain}/{pathname}, so each domain
resolves to its own route tree under app/. Per-domain settings (title,
links, locales, theme) live in app/<domain>/config.ts.
- Next.js 16 (App Router, Turbopack) with TypeScript.
- Content in Markdown via
next-mdx-remote+gray-matter, read throughlib/content.ts. - Internationalization with a small custom setup in
lib/i18n/— edge-safe locale negotiation (used byproxy.ts) plus aserver-onlydictionary loader; JSON translations live underpublic/<domain>/translations/. - Command bar with
kbar(⌘K/Ctrl+K). - Dark mode with
next-themes. - RSS feed generation with
rss. - "Recent activity" widgets that pull from Discogs, Letterboxd, Unsplash, Hardcover, GitHub, Last.fm, Setlist.fm and Deezer (
lib/services/). - ESLint, Prettier, stylelint, Husky and lint-staged — and more.
app/
<domain>/ route tree for one domain (e.g. diegocosta.com.br)
config.ts per-domain config (title, links, locales, theme)
[...page]/ catch-all for Markdown pages; notFound() when missing
not-found.tsx 404 boundary (single-locale domains)
[locale]/ localized routes (multi-locale domains only)
not-found.tsx localized 404 boundary (diegocoxta.com)
config.ts shared config defaults (theme)
components/ React components (one folder each, co-located CSS Modules)
lib/
config.ts shared types for the per-domain config
content.ts Markdown reading, front-matter, locale fallback (contentFor)
public-path.ts helper for building public/<domain>/… paths
i18n/
locale.ts locale list + Accept-Language / cookie negotiation (edge-safe)
translator.ts createTranslator: lookup, {param} interpolation, dates
messages.ts loads public/<domain>/translations/<locale>.json (server-only)
services/ third-party integrations for the activity widgets
proxy.ts host-based rewrite + locale negotiation (Next middleware)
public/<domain>/ Markdown content (posts/, pages/), translations/ and assets
The ~/* import alias maps to the repo root (see tsconfig.json).
- Node.js 24 (see
.nvmrc) - Yarn 1.x as the package manager
-
Clone this repo with git.
-
Run
yarn installinside the project directory. -
Copy the env file:
cp .env.example .env. Every key is optional — without a given token the matching activity widget just renders empty.SITE_ACCENT_COLORandSITE_TEXT_COLORcontrol the theme colors. -
Start the dev server for one domain:
yarn dev:br→ diegocosta.com.bryarn dev:me→ diegocosta.meyarn dev:com→ diegocoxta.com
(or the full form, e.g.
yarn dev:diegocosta.com.br) -
Open
http://localhost:3000. TheDEV_SITEenv var set by these scripts tellsproxy.tswhich domainlocalhostmaps to.
| Script | Description |
|---|---|
yarn build |
Production build |
yarn start |
Serve the production build |
yarn lint |
Run ESLint |
yarn prettier |
Format the codebase |
yarn stylelint |
Lint & fix CSS Modules |
yarn upgrade-latest |
Interactive dependency upgrade |
Content is plain Markdown under public/<domain>/, split into posts/ and
pages/ (one folder per slug). Localized files use an index.<locale>.md suffix
(e.g. index.en.md) and fall back to the default locale, then to index.md.
Relative image references () are rewritten to servable paths at
read time.
UI strings are flat-key JSON dictionaries at
public/<domain>/translations/<locale>.json. A requested locale is merged over
the default-locale dictionary, so partial translations are fine. Keys prefixed
client. / config. / components. / page. go through the translator;
anything else is treated as a literal (brand and proper names). Keys prefixed
client. are the subset shipped to Client Components, via getClientMessages
in lib/i18n/messages.ts.
diegocoxta.com ships in pt, en and es; the other domains are
single-locale (and skip the /<locale> path prefix entirely).
Unmatched URLs are funneled through each domain's catch-all [...page] route,
which calls notFound(). There is no root app/not-found.tsx — it would need the
request host to choose a domain, forcing every catch-all to render dynamically —
so each domain carries its own boundary:
- Single-locale domains render
app/<domain>/not-found.tsx(a Server Component). diegocoxta.complaces the boundary atapp/diegocoxta.com/[locale]/not-found.tsxso it renders inside the[locale]layout and can localize: a Client Component that reads the translator context. Its copy is stored under aclient.key (client.components.notFound.message) sogetClientMessagesships it.
The catch-all routes are dynamic, so the styled body streams in on hydration; the
response status is 404 either way.
Deployed on Vercel. Each domain is attached as a custom
domain to the same project; the host-based rewrite in proxy.ts does
the routing.
See CONTRIBUTING.md for details.
Say hello on Bluesky, Mastodon or email.
Source code is MIT. The publications under public/** are
© Diego Costa, all rights reserved.