"A clean, fast, server-rendered personal portfolio built for performance and clarity."
I'm Alessandro Kuz — AI/ML Engineer, Data Scientist, and Builder. This project is my own digital place, a personal website and professional portfolio. It serves as a central hub for showcasing projects, skills, and career background, and as a direct point of contact for recruiters, collaborators, and clients; That is the reason why I called it "PersonalHub".
The site is built on a deliberately minimal but slick stack: server-rendered Django templates, HTMX for targeted partial-page updates, and Bootstrap 5 for layout — no JavaScript framework, no build pipeline, no client-side routing. The result is a fast, accessible, and fully indexable website with a small operational footprint.
Live site: alessandrokuz.com
- Light / dark theme with flash-free initialization and
localStoragepersistence - Fully responsive layout across mobile, tablet, and desktop
- Internationalisation (i18n) with English (default) and Italian (more to come), via Django's i18n framework with clean URL prefixes (
/it/about/) - HTMX-powered interactions — partial page updates without a single-page application
- Contact form with server-side validation and HTMX submission (no full page reload)
- Sticky frosted-glass navbar with true-centred navigation links and scroll transition
- Fully responsive layout built on Bootstrap 5's grid, with mobile-first breakpoints
- Accessibility-first — semantic HTML,
ariaattributes, skip-to-content link, keyboard navigability (via Vim-like motions) - Dockerised deployment with separate development and production Compose configurations
- Zero build step — no webpack, no Node, no asset pipeline
- MkDocs documentation for the project itself, available at
docs.alessandrokuz.com - Max Security Scores on both Security Headers and Mozilla observatory
| Layer | Technology | Purpose |
|---|---|---|
| Language | Python 3.14 | Runtime |
| Framework | Django 6.0 | Routing, views, templating, ORM |
| Interactivity | HTMX 2.0 | Partial DOM updates without a JS framework |
| CSS framework | Bootstrap 5.3 | Responsive layout and components |
| ASGI server | Uvicorn | Async request handling |
| Static files | WhiteNoise | Zero-config static file serving |
| Package manager | uv | Dependency management and virtual environments |
| Linter / formatter | Ruff | Code quality |
| Testing | pytest + pytest-django + pytest-asyncio | TDD test suite |
| Reverse proxy | Caddy | TLS termination and automatic HTTPS |
| Containerisation | Docker + Compose | Reproducible environments |
| Documentation | MkDocs Material | Project reference docs |
"The tree below is sorted by logical grouping, not alphabetically — directories and files are ordered by purpose and dependency to make the architecture easier to read at a glance."
personalhub/
│
├── config/ # Project config — NOT an app
│ ├── settings/
│ │ ├── __init__.py
│ │ ├── base.py # Shared settings across all environments
│ │ ├── dev.py # Development overrides (SQLite, console email, debug toolbar)
│ │ └── prod.py # Production overrides (PostgreSQL, HTTPS headers, security settings)
│ ├── urls.py # Root URL dispatcher with i18n_patterns
│ ├── asgi.py # ASGI entry point (uvicorn target)
│ └── wsgi.py
│
├── apps/
│ ├── core/ # Phase 1: home, about, work, contact
│ ├── projects/ # Phase 2: Project + Tag models
│ ├── blog/ # Phase 3: Post model, writing interface
│ └── chat/ # Phase 5: LLM-base custom chatbot
│
├── templates/
│ ├── base.html # Master layout — extended by all pages
│ ├── components/ # _nav.html, _footer.html, _cta.html
│ └── partials/ # HTMX response fragments
│
├── static/
│ ├── css/ # Design tokens and custom styles
│ ├── js/ # Navbar, Theme & language switches, tooltips, ...
│ └── img/
│
├── locale/ # i18n .po / .mo translation files
├── docs/ # MkDocs source — architecture and decisions
├── docs/ # MkDocs source (you are here)
├── site/ # MkDocs build output (gitignored)
├── scripts/
│ └── dev.sh # Launches Django + MkDocs simultaneously
│
├── manage.py # Django CLI entry point
├── mkdocs.yml # MkDocs configuration
├── pyproject.toml # uv: dependencies + tool config (ruff, etc.)
├── uv.lock # Committed lockfile — guarantees reproducible installs
├── .python-version # Pins Python 3.14 — read by uv and mise
├── Dockerfile # Production image definition
├── docker-compose.yml # Dev environment
├── docker-compose.prod.yml # Production environment
├── .env # Secret values — gitignored
└── .env.example # Committed template for .env
- uv — Python version and package management
- Docker (optional, for running the full production stack locally)
git clone https://github.com/AlessandroKuz/personalhub.git
cd personalhubcp .env.example .env
# Edit .env and set at minimum:
# SECRET_KEY=
# DJANGO_SETTINGS_MODULE=config.settings.devuv syncGenerating a secret key:
uv run python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"uv run python manage.py migrateThe included script starts both the Django/Uvicorn server and the MkDocs documentation server simultaneously. A single Ctrl+C shuts both down cleanly.
./scripts/dev.shAlternatively, run each separately:
# Application — http://127.0.0.1:8080
uv run uvicorn config.asgi:application --reload --port 8080
# Documentation — http://127.0.0.1:8001
uv run mkdocs serve --dev-addr 127.0.0.1:8001uv run pytest -vWith coverage:
uv run pytest --cov=apps --cov-report=term-missingTwo parallel command runners are provided — just (preferred) and make
(universally available fallback). They expose identical functionality. Install
just on Arch with sudo pacman -S just or on Debian/Ubuntu with
sudo apt install just.
just # list all available recipes
make # same, for Makejust accepts extra arguments naturally — append them after the recipe name:
just check --deploy
just migrate --run-syncdb
just test apps/core/tests.py -xmake requires the ARGS variable:
make check ARGS="--deploy"
make migrate ARGS="--run-syncdb"
make test ARGS="apps/core/tests.py -x"just install # set up the environment from uv.lock
just run # start the ASGI dev server (preferred — mirrors production)
just runserver # start Django's WSGI dev server (use for browser error debugger)
just ci # lint + format check + full test suite — run before pushing
just deploy # full production deployment cycle inside Docker
just reset-db # wipe and recreate local SQLite (dev only — irreversible)Copy .env.example to .env and populate the following:
# Django
SECRET_KEY=<your-secret-key>
DJANGO_SETTINGS_MODULE=config.settings.dev
DEBUG=True
ALLOWED_HOSTS=127.0.0.1,localhost
# Database (production only — dev uses SQLite)
POSTGRES_DB=personalhub
POSTGRES_USER=personalhub
POSTGRES_PASSWORD=<your-db-password>
# Email (production)
EMAIL_HOST=<smtp-host>
EMAIL_PORT=587
EMAIL_HOST_USER=<your-email>
EMAIL_HOST_PASSWORD=<your-email-password>The production stack runs as three Docker services — the Django application, PostgreSQL, and Caddy — defined in docker-compose.prod.yml. Caddy handles TLS certificate provisioning and renewal automatically via Let's Encrypt.
# On the production host
git clone https://github.com/AlessandroKuz/personalhub.git
cd personalhub
cp .env.example .env
# Fill in all production values, set DJANGO_SETTINGS_MODULE=config.settings.prod
docker compose -f docker-compose.prod.yml up -d --build
docker compose -f docker-compose.prod.yml exec web uv run python manage.py migrate
docker compose -f docker-compose.prod.yml exec web uv run python manage.py createsuperusergit pull
docker compose -f docker-compose.prod.yml build web
docker compose -f docker-compose.prod.yml up -d
# Run migrations if the update includes model changes:
docker compose -f docker-compose.prod.yml exec web uv run python manage.py migrateThe site is hosted on a VPS behind Caddy. An alternative zero-cost configuration using a home server and Cloudflare Tunnel is documented in docs/infrastructure/hosting.md — no port forwarding required, the home IP never appears in DNS.
Content lives directly in the templates under templates/ and apps/core/templates/core/ (Django convention tbh). Update the following to make the site your own:
templates/base.html— brand name, social linkstemplates/components/_nav.html— brand name, social linkstemplates/components/_footer.html— name, social links (GitHub, LinkedIn, YouTube), copyrighttemplates/components/_cta.html— email, scheduling link, social handles components
And inside the core app:
-
templates/core/home.html— TLDR version of all the pages -
templates/core/about.html— bio, background, languages, interests -
templates/core/work.html— skills, career timeline, CV download -
templates/core/contact.html— email, scheduling link, social handles
All colours are defined globally inside static/scss/custom.scss and as CSS custom properties in static/css/main.css. Light and dark variants are set in a single place:
:root {
--accent
--color-bg
--color-bg2
--color-surface
--color-text
--color-muted
--color-border
/* ... */
}
[data-bs-theme="dark"] {
/* same as above */
/* ... */
}To add a language, add its code to LANGUAGES in config/settings/base.py, then run:
uv run python manage.py makemessages -l <LANG_CODE>
# for example for English: uv run python manage.py makemessages -l en
# Edit the generated .po file in locale//LC_MESSAGES/django.po
uv run python manage.py compilemessages| Light mode | Dark mode |
|---|---|
![]() |
![]() |
The apps/projects application will introduce dynamic project cards driven by Django models. Each project will have a title, description, tags, GitHub link, and optional live URL. Tag-based filtering will be implemented with HTMX — clicking a tag updates the project grid without a page reload. Content will be managed through Django's admin interface.
The apps/blog application will add a writing and publishing interface. Posts will be written in Markdown with a live server-side preview powered by a WebSocket connection — the ASGI foundation is already in place for this. Posts will support draft and published states, managed through a lightweight custom editor or the Django admin.
With core functionality in place, this phase focuses on elevating the production quality of the site across three dimensions: visual refinement, performance, and SEO. Animations and micro-interactions will be tightened, the design system will be audited for consistency across all pages and themes, and accessibility will be validated. On the performance side, static assets will be audited with Lighthouse, images will be served in modern formats with appropriate sizing, and Core Web Vitals will be measured and optimised. SEO groundwork — canonical URLs, structured data and a generated sitemap — will also be completed in this phase.
The apps/chat application will introduce a context-aware AI assistant capable of answering questions about my background, projects, skills and work. The assistant will be powered by a Retrieval-Augmented Generation (RAG) pipeline: site content, project descriptions, and CV data will be embedded and stored in a vector database, retrieved at query time to ground the model's responses in accurate, up-to-date information. The interface will be a minimal chat widget, streamed over a WebSocket connection using the ASGI infrastructure already in place. The goal is not a generic chatbot, but a focused, honest assistant that represents me accurately — and declines gracefully when it doesn't know the answer.
Partially inspired by Ignacio Figueroa's Portfolio.
Full architecture documentation — settings split, URL structure, ASGI rationale, HTMX patterns, deployment guide — is available at:
uv run mkdocs serve --dev-addr 127.0.0.1:8001Or browse the docs/ directory directly.
This project follows a Test-Driven Development approach — tests are written before implementation, not after. The discipline of writing a failing test first ensures that every piece of production code has a clear, testable purpose and that the entire codebase remains refactorable with confidence.
| Package | Role |
|---|---|
pytest |
Test runner — plain assert, rich failure output |
pytest-django |
Django integration — async_client, db fixtures, settings wiring |
pytest-asyncio |
Native async def test support (required for async views) |
pytest-cov |
Coverage reporting |
asyncio_mode = "auto" is set in pyproject.toml, meaning every async def test_* function runs on the event loop without any decorator. Given that all views in this project are async, this eliminates significant boilerplate.
# Run all tests
uv run pytest
# Verbose output (show individual test names)
uv run pytest -v
# Run a specific app
uv run pytest apps/core/
# Run with coverage report
uv run pytest --cov=apps --cov-report=term-missingTests live inside each app, co-located with the code they test. This is the Django convention: when an app is removed, its tests go with it — no orphaned test files pointing at deleted code.
apps/
└── core/
└── tests/
├── __init__.py
├── test_forms.py # Navbar rendered on all pages, controls present
├── test_urls.py # URL resolution + i18n prefix correctness
├── test_views.py # HTTP status codes + template assertions
└── test_*.py # For everything else that might need it
No production code in this repository was written without a failing test that justified it. The workflow is:
- Red — write a test that describes the desired behaviour; confirm it fails
- Green — write the minimum code to make it pass
- Refactor — improve the implementation; tests confirm nothing broke
For detailed rationale, patterns, and the full test inventory see the Testing & TDD documentation.
This is a personal portfolio project and is not open to general feature contributions. That said, the following are welcome and appreciated:
- Bug reports — if you spot a broken layout, a template rendering issue, or unexpected behaviour, please open an issue with your browser, OS, and a clear description of what you expected vs. what you saw.
- Accessibility feedback — if you encounter any barrier navigating the site with assistive technology, please open an issue. Accessibility is treated as a first-class concern in this project.
- Security disclosures — please do not open a public issue for security vulnerabilities. Contact me directly at contact@alessandrokuz.com with a description of the issue.
- If you are forking this project as a base for your own portfolio, you are welcome to do so under the terms of the licence below. A mention or backlink is appreciated but not required.
This project is licensed under the MIT Licence. You are free to use, copy, modify, and distribute it, provided the original copyright notice is retained.
Alessandro Kuz — AI/ML Engineer, Data Scientist & Builder
| Channel | Link |
|---|---|
| Website | alessandrokuz.com |
| contact@alessandrokuz.com | |
| linkedin.com/in/alessandrokuz | |
| GitHub | github.com/alessandrokuz |
| Schedule a call | cal.com/alessandrokuz |
This project makes use of the following open-source tools and would not exist without the work of their respective communities.
- Django — the web framework doing the heavy lifting
- HTMX — hypermedia-driven interactivity without a JavaScript framework
- Bootstrap 5 — layout, components, and responsive utilities
- uv — fast Python package and project management
- Caddy — automatic HTTPS and reverse proxy
- MkDocs Material — developer documentation
- Cloudflare — DNS, DDoS protection, and CDN layer in front of the stack + Docs hosting
- Docker — containerised services for consistent dev and production environments

