A personal portfolio website built with Astro, featuring multilingual support (Japanese/English) and dark mode. Ships zero JavaScript bundles — only a few KB of inline script for the theme toggle and animations.
- Framework: Astro 7 (static output)
- Language: TypeScript 5
- Styling: Tailwind CSS 4 (via
@tailwindcss/vite) - Internationalization:
src/pages/[locale]/dynamic routes +getStaticPaths() - Content: Astro Content Collections (
globloader) - Icons: astro-icon + Iconify (
heroicons,fa6-brands), inlined at build time - Fonts: Fontsource Geist / Geist Mono (self-hosted)
- Linting/Formatting: Biome 2.4.16
- Hosting: Cloudflare Workers (static assets)
- 🌐 Multilingual support (Japanese/English)
- 🌓 Dark mode support (no flash on load)
- 📱 Responsive design
- 📝 Blog functionality
- 👤 Profile page
- 🏢 Experience/Resume page
- ⚡ Zero client-side JavaScript bundles
Optimized for Lighthouse / PageSpeed Insights:
- No render-blocking CSS — all stylesheets are inlined into each page's
<head>(build.inlineStylesheets: "always"inastro.config.mjs) - Font preloading — pages that use Geist / Geist Mono preload only the font subset they need, via a
headslot inBase.astro(avoids the HTML → CSS → font request chain) - Optimized assets — the noise background texture is a palette PNG (~15KB) and the home page icon is a right-sized WebP (~14KB)
- SEO — every page ships a locale-aware
<meta name="description">(seesite.descriptioninmessages/)
- Node.js 24.7.0 (can be automatically installed using mise)
- pnpm 10.14.0 (required — npm/yarn are blocked via
engines)
- Clone the repository
git clone <repository-url>
cd portfolio- Install dependencies
pnpm install- Start the development server
pnpm devOpen http://localhost:4321 in your browser to view the site.
# Start development server
pnpm dev
# Type-check and build for production (outputs to dist/)
pnpm build
# Serve the built output locally
pnpm preview
# Run linting
pnpm lint
# Run linting with auto-fix
pnpm lint:fix
# Format code
pnpm format
# Run E2E tests (Playwright — builds and serves dist/ automatically)
pnpm test:e2eportfolio/
├── src/
│ ├── pages/
│ │ ├── [locale]/ # Multilingual routes (getStaticPaths)
│ │ │ ├── blog/
│ │ │ │ ├── index.astro # Blog list
│ │ │ │ └── [slug].astro# Blog detail
│ │ │ ├── experience.astro
│ │ │ ├── profile.astro
│ │ │ └── index.astro # Home page
│ │ └── 404.astro
│ ├── layouts/
│ │ └── Base.astro # html/head/body, theme script, head slot, Header, Footer
│ ├── components/
│ │ ├── Header.astro # Nav, theme toggle, language switcher
│ │ ├── Footer.astro
│ │ ├── Tilt.astro # 3D hover effect
│ │ ├── TypingText.astro # Typing animation
│ │ └── AboutCard.astro
│ ├── i18n/
│ │ └── ui.ts # Locales, typed messages, helpers
│ ├── styles/
│ │ └── global.css # Tailwind entry + custom CSS
│ ├── content/blog/{locale}/{year}/{slug}.md
│ └── content.config.ts # Blog content collection
├── e2e/ # Playwright E2E tests
├── messages/ # Translation files
│ ├── en.json
│ └── ja.json
├── public/ # Static assets
├── astro.config.mjs
├── wrangler.jsonc # Cloudflare Workers config
├── biome.json
├── mise.toml
└── package.json
- Supported languages: Japanese (
ja), English (en) - Translation files:
messages/ja.json,messages/en.json - URL structure:
/{locale}/...(e.g.,/ja/profile,/en/profile) /shows the Japanese home page without redirecting (the URL stays/; seesrc/pages/index.astro)./jaredirects to/viapublic/_redirects(308)
When adding new translation keys, add them to both messages/ja.json and messages/en.json, and update the Messages interface in src/i18n/ui.ts.