- TypeScript 92.9%
- JavaScript 3.3%
- MDX 2.5%
- CSS 0.8%
- Dockerfile 0.5%
Bumps the minor-and-patch group with 6 updates: | Package | From | To | | --- | --- | --- | | [fuse.js](https://github.com/krisk/Fuse) | `7.3.0` | `7.4.0` | | [js-cookie](https://github.com/js-cookie/js-cookie) | `3.0.7` | `3.0.8` | | [js-yaml](https://github.com/nodeca/js-yaml) | `4.1.1` | `4.2.0` | | [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.16.0` | `1.17.0` | | [resend](https://github.com/resend/resend-node) | `6.12.3` | `6.12.4` | | [tsx](https://github.com/privatenumber/tsx) | `4.22.3` | `4.22.4` | Updates `fuse.js` from 7.3.0 to 7.4.0 - [Release notes](https://github.com/krisk/Fuse/releases) - [Changelog](https://github.com/krisk/Fuse/blob/main/CHANGELOG.md) - [Commits](https://github.com/krisk/Fuse/compare/v7.3.0...v7.4.0) Updates `js-cookie` from 3.0.7 to 3.0.8 - [Release notes](https://github.com/js-cookie/js-cookie/releases) - [Commits](https://github.com/js-cookie/js-cookie/compare/v3.0.7...v3.0.8) Updates `js-yaml` from 4.1.1 to 4.2.0 - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/commits) Updates `lucide-react` from 1.16.0 to 1.17.0 - [Release notes](https://github.com/lucide-icons/lucide/releases) - [Commits](https://github.com/lucide-icons/lucide/commits/1.17.0/packages/lucide-react) Updates `resend` from 6.12.3 to 6.12.4 - [Release notes](https://github.com/resend/resend-node/releases) - [Commits](https://github.com/resend/resend-node/compare/v6.12.3...v6.12.4) Updates `tsx` from 4.22.3 to 4.22.4 - [Release notes](https://github.com/privatenumber/tsx/releases) - [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs) - [Commits](https://github.com/privatenumber/tsx/compare/v4.22.3...v4.22.4) --- updated-dependencies: - dependency-name: fuse.js dependency-version: 7.4.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-and-patch - dependency-name: js-cookie dependency-version: 3.0.8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-and-patch - dependency-name: js-yaml dependency-version: 4.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-and-patch - dependency-name: lucide-react dependency-version: 1.17.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-and-patch - dependency-name: resend dependency-version: 6.12.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-and-patch - dependency-name: tsx dependency-version: 4.22.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-and-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|---|---|---|
| .github | ||
| config | ||
| content | ||
| docs | ||
| drizzle | ||
| public | ||
| scripts | ||
| src | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| .nvmrc | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| drizzle.config.ts | ||
| eslint.config.mjs | ||
| LICENSE | ||
| middleware.ts | ||
| next.config.ts | ||
| nextjs_admin_prompt.md | ||
| package-lock.json | ||
| package.json | ||
| postcss.config.mjs | ||
| README.md | ||
| ROADMAP.md | ||
| tsconfig.json | ||
Portfolio
A modern, self-hosted portfolio website built with Next.js 16, featuring a full admin dashboard, MDX-based content management, and comprehensive SEO optimization.
✨ Features
🎨 Public Portfolio
- Responsive Design — Mobile-first, accessible UI with dark mode support
- MDX-Powered Content — Write blog posts, projects, and experiences in Markdown with React components
- Advanced Search — Fuzzy search across all content with keyboard shortcuts (
Cmd/Ctrl+K) - SEO Optimized — OpenGraph images, JSON-LD structured data, dynamic sitemap, robots.txt
- GitHub Integration — Contribution calendar visualization
- Contact Form — Email submissions with file attachments via Resend API
🛠️ Admin Dashboard (/admin)
- Content Management — Create, edit, and manage all content types through a visual editor
- Markdown Editor — Live preview with syntax highlighting and drag-and-drop image upload
- Database Viewer — SQL editor and table inspector for SQLite database
- Session Management — View and revoke active admin sessions
- SSO/OIDC Support — Configure multiple identity providers (Google, Microsoft, Okta, PocketID, Authentik, Keycloak, etc.)
- OpenAPI Documentation — Interactive API docs for all admin endpoints
🔐 Authentication
- Local Auth — Argon2-based password hashing, session cookies
- SSO/OIDC — Database-backed provider configuration with env var fallback
- Session Management — SQLite-backed sessions with configurable expiry
🐳 Production-Ready
- Docker — Optimized amd64 images on Docker Hub and GitHub Container Registry
- CI/CD — Automated linting, type-checking, builds, and releases via GitHub Actions
- CodeQL Security Scanning — Automated vulnerability detection
- Dependabot — Auto-merge for minor/patch dependency updates
🚀 Quick Start
Option 1: Docker (Recommended)
# Pull the latest image
docker pull ftmahringer/portfolio:latest
# Run with required environment variables
docker run -d \
-p 3000:3000 \
-v portfolio-data:/app/data \
-e ADMIN_EMAIL=admin@example.com \
-e ADMIN_PASSWORD=secure-password \
-e API_SECRET=random-secret-key \
--name portfolio \
ftmahringer/portfolio:latest
Or use docker-compose.yml:
version: '3.8'
services:
portfolio:
image: ftmahringer/portfolio:latest
ports:
- "3000:3000"
environment:
ADMIN_EMAIL: admin@example.com
ADMIN_PASSWORD: secure-password
API_SECRET: random-secret-key
# Optional: Spotify Integration
# SPOTIFY_CLIENT_ID: your-spotify-client-id
# SPOTIFY_CLIENT_SECRET: your-spotify-client-secret
# SPOTIFY_REFRESH_TOKEN: your-spotify-refresh-token
# SPOTIFY_REDIRECT_URI: https://your-domain.com/api/spotify/callback
# Optional: SSO via env vars
# OIDC_ISSUER: https://accounts.google.com
# OIDC_CLIENT_ID: your-client-id
# OIDC_CLIENT_SECRET: your-client-secret
volumes:
- portfolio-data:/app/data
restart: unless-stopped
volumes:
portfolio-data:
Option 2: Local Development
# Clone the repository
git clone https://github.com/FTMahringer/Portfolio.git
cd Portfolio
# Install dependencies
npm install
# Set up environment variables
cp .env.local.example .env.local
# Edit .env.local with your values
# Initialize the database
npm run db:push
# Run the development server
npm run dev
Visit http://localhost:3000 for the public site and http://localhost:3000/admin for the admin dashboard.
📋 Environment Variables
Required
| Variable | Description | Example |
|---|---|---|
ADMIN_EMAIL |
Default admin email (auto-seeded on first start) | admin@example.com |
ADMIN_PASSWORD |
Default admin password | secure-password |
API_SECRET |
Secret for API authentication | random-secret-key |
Optional: Spotify Integration
To enable the Spotify "Now Playing" widget, configure the following environment variables:
| Variable | Description | Example |
|---|---|---|
SPOTIFY_CLIENT_ID |
Spotify app client ID | 829beffbf6fe... |
SPOTIFY_CLIENT_SECRET |
Spotify app client secret | 19c278a670... |
SPOTIFY_REFRESH_TOKEN |
OAuth refresh token | Get via authorization flow (see below) |
SPOTIFY_REDIRECT_URI |
Public OAuth callback URL | https://portfolio.ftmahringer.com/api/spotify/callback |
Getting the Spotify refresh token:
- Create a Spotify app at https://developer.spotify.com/dashboard
- Add redirect URI:
https://your-domain.com/api/spotify/callback - Set
SPOTIFY_CLIENT_ID,SPOTIFY_CLIENT_SECRET, andSPOTIFY_REDIRECT_URIin your environment - Visit this URL (replace
YOUR_CLIENT_IDand use the same domain asSPOTIFY_REDIRECT_URI):https://accounts.spotify.com/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=https://your-domain.com/api/spotify/callback&scope=user-read-currently-playing+user-read-recently-played+user-top-read - Authorize the app
- Copy the refresh token from the success page
- Add it to your environment as
SPOTIFY_REFRESH_TOKEN
Important: The SPOTIFY_REDIRECT_URI must match exactly what's configured in your Spotify app settings. Use your public domain (e.g., https://portfolio.ftmahringer.com/api/spotify/callback), not internal/VPN domains.
Note: If these variables are not set, Spotify features will be automatically disabled.
Optional: Analytics
| Variable | Description | Example |
|---|---|---|
NEXT_PUBLIC_UMAMI_WEBSITE_ID |
Umami website ID | abc-123-def |
NEXT_PUBLIC_UMAMI_URL |
Umami analytics server URL | https://analytics.example.com |
NEXT_PUBLIC_PLAUSIBLE_DOMAIN |
Plausible domain | your-domain.com |
Analytics are only loaded in production and require configuration in config/site.yaml.
Optional: Other
| Variable | Description | Default |
|---|---|---|
DATABASE_URL |
SQLite database path | file:./data/portfolio.db |
NEXT_PUBLIC_BASE_URL |
Public URL for OG images and canonical links | http://localhost:3000 |
RESEND_API_KEY |
API key for email submissions | - |
CONTACT_EMAIL |
Email address to receive contact form submissions | - |
OIDC_ISSUER |
OIDC provider issuer URL (env var fallback) | - |
OIDC_CLIENT_ID |
OIDC client ID (env var fallback) | - |
OIDC_CLIENT_SECRET |
OIDC client secret (env var fallback) | - |
🐳 Docker Tags
We publish amd64 images to both Docker Hub and GitHub Container Registry:
| Tag | Description | Example |
|---|---|---|
latest |
Latest stable release | ftmahringer/portfolio:latest |
X.Y.Z |
Exact version | ftmahringer/portfolio:2.1.0 |
X.Y |
Latest patch in X.Y.* | ftmahringer/portfolio:2.1 |
X |
Latest minor/patch in X.* | ftmahringer/portfolio:2 |
Recommendation: Use X.Y for production (e.g., 2.1) to get patch updates automatically while avoiding breaking changes.
🛠️ Tech Stack
- Framework: Next.js 16 (App Router, Turbopack)
- Styling: Tailwind CSS 4
- Database: SQLite + Drizzle ORM
- Auth: Argon2 + jose (OIDC)
- Content: MDX + gray-matter
- Search: Fuse.js
- Email: Resend
🐛 Troubleshooting
Port 3000 already in use
# Change port in docker run
docker run -p 8080:3000 ...
# Or in docker-compose.yml
ports:
- "8080:3000"
Database locked errors
The SQLite database is in /app/data/portfolio.db inside the container. Make sure it''s persisted in a volume and not shared across multiple container instances.
Admin login not working
If you changed ADMIN_EMAIL or ADMIN_PASSWORD after the first start, the admin user was already seeded. Either:
- Delete the volume and restart (resets database), or
- Use the existing credentials, or
- Manually update the password via SQL:
npm run db:studio→ users table
📄 License
MIT — Feel free to use this template for your own portfolio!
Need help? Open an issue on GitHub.