- Vue 69.2%
- TypeScript 15.2%
- JavaScript 6.9%
- CSS 4.7%
- Dockerfile 4%
|
|
||
|---|---|---|
| .forgejo/workflows | ||
| .github | ||
| app | ||
| i18n/locales | ||
| public | ||
| server | ||
| shared/domain | ||
| .env.example | ||
| .gitignore | ||
| bun.lock | ||
| content.config.ts | ||
| docker-compose.yml | ||
| Dockerfile | ||
| eslint.config.mjs | ||
| nuxt.config.ts | ||
| package.json | ||
| README.md | ||
| sonar-project.properties | ||
| tailwind.config.js | ||
| tsconfig.json | ||
Pontus Göth — Portfolio
Personal portfolio site built with Nuxt 4, featuring a blog, GitHub projects feed, and a resume page with server-side PDF generation.
Tech stack
- Nuxt 4 — full-stack Vue meta-framework
- Tailwind CSS — utility-first styling
- @nuxt/content — Markdown-based blog
- @nuxtjs/i18n — localisation (en, sv)
- Playwright — server-side PDF generation for the resume page
- Bun — package manager and runtime
Prerequisites
- Bun v1.0+
- A GitHub personal access token (optional — without one the GitHub API falls back to 60 req/h)
Setup
1. Install dependencies
bun install
2. Download the Playwright browser
The resume page generates a downloadable PDF server-side using a headless Chromium browser managed by Playwright. Download it once with:
bun run setup:browsers
This saves Chromium to ~/.cache/ms-playwright/ and only needs to be done once per machine (or after a major Playwright version bump).
3. Configure environment variables
Copy the example env file and fill in your values:
cp .env.example .env
| Variable | Required | Description |
|---|---|---|
NUXT_GITHUB_TOKEN |
No | GitHub fine-grained token with "Public Repositories (read-only)" permission. Without it the API is limited to 60 requests/hour. |
NUXT_GITHUB_USERNAME |
No | GitHub username to fetch public repos for (defaults to redines). |
CHROMIUM_PATH |
No | Path to a system Chromium binary. Leave unset locally — Playwright uses its own downloaded browser. Only set this in Docker/CI. |
Development
Start the development server on http://localhost:3000:
bun run dev
Production
Build for production:
bun run build
Preview the production build locally:
bun run preview
Docker
Build and run with Docker:
docker build -t portfolio .
docker run -p 3000:3000 portfolio
The Docker image uses Bun for both build and runtime. For this Nuxt app, the final image also installs the full production dependency tree into server/node_modules, because Bun resolves packages against the standalone server's local module tree and Nitro's traced copy can omit Bun-specific runtime files from transitive packages such as ofetch.
Docker installs also use --ignore-scripts to avoid compiling optional native modules such as better-sqlite3 in CI. This project's Bun-targeted server output uses bun:sqlite, so those native install scripts are not required for the container build.
The Docker image installs system Chromium via apt-get and sets CHROMIUM_PATH=/usr/bin/chromium automatically — no extra configuration needed.
To pass a GitHub token at runtime:
docker run -p 3000:3000 -e NUXT_GITHUB_TOKEN=your_token portfolio
CI/CD
On every push to main, the pipeline runs three jobs:
build ─┐
├─→ publish
analyze┘
- build — installs dependencies and runs
nuxt buildto validate the project compiles - analyze — runs a SonarQube scan against the source code (runs in parallel with build)
- publish — builds and pushes the Docker image; only runs if both
buildandanalyzepass
The following secrets must be set in your Forgejo user account (globally available to all repositories):
| Secret | Description |
|---|---|
REGISTRY_URL |
Hostname of the private Docker registry |
REGISTRY_USERNAME |
Username for the registry |
REGISTRY_PASSWORD |
Password for the registry |
SONAR_HOST_URL |
URL of your SonarQube instance |
SONAR_TOKEN |
SonarQube authentication token |
NUXT_GITHUB_TOKEN |
GitHub token — passed at container runtime, not needed at build time |
The published image is tagged with both latest and the commit SHA:
$REGISTRY_URL/portfolio:latest
$REGISTRY_URL/portfolio:<commit-sha>
To deploy, pull and run the latest image:
docker pull $REGISTRY_URL/portfolio:latest
docker run -p 3000:3000 \
-e NUXT_GITHUB_TOKEN=your_token \
$REGISTRY_URL/portfolio:latest
Resume page
The resume is available at /resume. It renders as a normal HTML page using the site layout.
Clicking Download PDF on that page triggers a server-side request to GET /resume.pdf. The Nitro server route launches a headless Chromium, renders /resume?pdf=1 (a stripped layout with no header or footer), and streams back the generated PDF.