My Current Design State: Features and Learnings
My Current Design State: Features and Learnings
After two intense days of development (February 3-4, 2026), my Phoenix-based personal website platform is fully functional. Let’s walk through what I’ve built, the challenges I faced, and the decisions that shaped the current architecture.
What I’ve Built
Public Website Features
Homepage
- Personal profile with avatar (circular, with ring border)
- Professional headline and bio
- Social media links (GitHub, LinkedIn, Mastodon)
- Recent work experience showcase
- Latest blog articles with excerpts
- Newsletter signup form
- Fully responsive, mobile-first design
About Page
- Large portrait image with characteristic Spotlight rotation effect
- Comprehensive biography
- Complete work history with date ranges
- Education and certifications
- Skills categorized by type (languages, frameworks, tools, etc.)
- All content pulled dynamically from database
Articles (Blog)
- Article listing with categories and tags
- Individual article pages with markdown rendering
- SEO-optimized metadata
- Related articles suggestions
- Dark mode prose styling with @tailwindcss/typography
Custom Pages
- Dynamic page creation via admin
- Markdown content support
- SEO metadata per page
- Reorderable via display_order field
Admin Dashboard
Profile Management
- Edit personal information (name, headline, bio)
- Contact details (email, phone, location, website)
-
Avatar picker modal with two modes:
- Select from uploaded media library
- Enter external image URL
- Circular avatar preview
- Social media links (JSONB field for flexibility)
- SEO metadata (meta title, description)
- Availability flags (for hire, consulting)
Media Library
- Drag-and-drop file upload
- Real-time upload progress bars
-
Image optimization with Mogrify/ImageMagick:
- Auto-orientation
- EXIF metadata stripping
- Resize to max 2000x2000px
- 85% quality compression
- Grid view with hover actions
- Edit modal for alt text and titles
- Delete with confirmation
-
All images served from
/uploads/
Content Management
- Articles with markdown editor
- Categories and tags
- Publish/unpublish workflow
- Custom pages with slug management
- Work experience tracking
- Education records
- Certifications with expiration dates
- Skills with proficiency levels
Authentication
- Secure admin login with bcrypt
- Session management
- CSRF protection
- Remember me functionality
Technical Features
Dark Mode
- Default to dark theme
- Manual toggle with sun/moon icons
- Persisted in localStorage
- Synced via Alpine.js
- Every component has dark mode variants
- Modal backgrounds adapt to theme
Internationalization
- German (default) and English
- Language switcher in admin
-
Separate translation domains:
-
default.po- Public website -
admin.po- Admin interface -
errors.po- Validation errors
-
- Preference stored in session
LiveView Enhancements
- Real-time form validation
- Instant feedback on user actions
- No page refreshes for CRUD operations
- WebSocket-based updates
- Optimistic UI updates
Architectural Decisions
Database Schema
I designed my schema around flexibility and future expansion:
Content Contexts:
-
profiles- Single row for site owner (no user_id relation) -
articles- Blog posts with SEO fields -
pages- Custom pages with markdown -
work_experiences- Employment history linked to profile -
education- Academic background -
certifications- Professional credentials -
skills- Categorized abilities with proficiency
Media Context:
-
media- Uploaded files with metadata - UUID filenames prevent conflicts
- JSONB metadata field for extensibility
- Belongs to user who uploaded
Key Design Choices:
- Used JSONB for flexible data (social_links, metadata)
- Timestamps on everything for audit trails
- Boolean flags over enum strings (available_for_hire)
- Separate SEO fields per content type
- Display order for manual sorting
Component Architecture
Three-Tier Component System:
-
Core Components (
core_components.ex)- Phoenix framework essentials
- Modal, flash messages, icons
- Base form inputs
- Table helpers
-
Admin Components (
admin_components.ex)- Catalyst-inspired UI elements
- Page headers, form containers
- Badges, buttons, cards
- Empty states, alerts
- Description lists
-
Public Components (in layouts)
- Spotlight-inspired design
- Container, Card, Section
- Social icons
- Newsletter form
Component Patterns:
-
Use
attr/3for props with validation -
Use
slot/3for flexible content areas -
Support both
:patchand:navigatefor LiveView -
Include
@restfor arbitrary HTML attributes - Dark mode classes on everything
File Organization
lib/spotlight/ # Business Logic
├── accounts/ # Authentication
├── content/ # Articles, Pages, Profiles
└── media/ # File uploads
lib/spotlight_web/ # Web Interface
├── components/
│ ├── core_components.ex
│ ├── admin_components.ex
│ └── layouts/
├── controllers/ # HTTP endpoints
├── live/admin/ # Admin LiveViews
└── router.ex
priv/
├── static/uploads/ # User uploads
├── repo/migrations/ # DB schema
└── gettext/ # Translations
Asset Pipeline
- Tailwind CSS v3 with JIT compiler
- esbuild for JavaScript bundling
- Alpine.js for minimal interactivity
- Heroicons embedded in CSS
- Watchers for hot reloading in dev
Challenges and Solutions
Challenge 1: Modal Dark Mode
Problem: Core modal component didn’t support dark mode Solution: Added dark mode classes to background overlay and modal container:
class="bg-zinc-50/90 dark:bg-zinc-950/90" # Backdrop
class="bg-white dark:bg-zinc-900" # Modal
Challenge 2: Media Display
Problem: Uploaded images weren’t showing (404s)
Solution: Added "uploads" to SpotlightWeb.static_paths() to serve files via Plug.Static
Challenge 3: Avatar Selection UX
Problem: Text input for avatar URL was clunky Solution: Built modal with dual interface:
- Media library grid (click to select)
- URL input field (for external images)
- Preview shows current avatar
- Remove button to clear selection
Challenge 4: Form Field Syntax in Templates
Problem: Using atoms like field={:title} in inputs
Solution: Use Phoenix.HTML.FormField syntax: field={@form[:title]}
Challenge 5: Image Optimization
Problem: Large image uploads consuming bandwidth Solution: Mogrify pipeline:
- Auto-orient based on EXIF
- Strip all metadata
- Resize to 2000x2000 max
- 85% JPEG quality Result: 70%+ file size reduction
Performance Metrics
Current performance benchmarks:
- Homepage load: ~40ms (server) + assets
- Article page: ~50ms (includes markdown parsing)
- Media upload: Real-time progress, <2s processing
- Admin navigation: Instant (LiveView patch)
- CSS bundle: 12KB gzipped
- JavaScript: ~8KB (mostly Alpine.js)
What’s Next?
Features I’m planning:
Short Term:
- [ ] Article categories page
- [ ] Tag cloud and filtering
- [ ] Search functionality
- [ ] RSS feed generation
- [ ] Social share buttons
Medium Term:
- [ ] Projects showcase
- [ ] Speaking engagements
- [ ] Contact form with anti-spam
- [ ] Analytics dashboard
- [ ] Sitemap generation
Long Term:
- [ ] Newsletter management (subscribers, campaigns)
- [ ] Comment system (maybe)
- [ ] Webmentions support
- [ ] API for external integrations
- [ ] Multi-author support
Key Learnings
What Worked Well:
- Phoenix LiveView: Eliminated JavaScript complexity while maintaining interactivity
- Tailwind Utility Classes: Rapid UI development without context switching
- Catalyst Component Patterns: Professional look with minimal custom CSS
- Ecto Migrations: Database changes are versioned and reversible
- Pattern Matching: Cleaner code with fewer bugs
What I’d Do Differently:
- Test Earlier: Should have written tests alongside features
- Design System First: Would establish spacing/color conventions before building
- Mobile Preview: Should test mobile layouts continuously, not at the end
- Accessibility Audit: Need to run automated accessibility checks
- Documentation: Inline docs for complex functions would help
Unexpected Delights:
- LiveView’s form validation is magical
- Dark mode with Tailwind is almost too easy
- Ecto’s changesets catch bugs before hitting the database
- Hot reloading makes development addictive
- The BEAM VM’s error handling is comforting
Conclusion
Building a personal website platform from scratch with Phoenix has been incredibly rewarding. I now have:
-
✅ A performant, modern web application
-
✅ Full control over every feature
-
✅ Clean, maintainable codebase
-
✅ Beautiful admin interface
-
✅ Mobile-responsive design
-
✅ Dark mode throughout
-
✅ Multi-language support
The journey from WordPress to Phoenix taught me that sometimes the best tool isn’t the most popular one—it’s the one that matches your needs and values. For me, that meant choosing developer experience, performance, and maintainability over plugin ecosystems and familiar territory.
If you’re considering building something similar, my advice: Go for it. The learning experience alone is worth it, and you’ll end up with something uniquely yours.
Tech Stack Summary
- Backend: Phoenix 1.7, Elixir 1.18, Erlang/OTP 26
- Database: PostgreSQL with Ecto
- Frontend: Phoenix LiveView, Alpine.js, Tailwind CSS v3
- Deployment: Nix shell for reproducible environments
- Media: Mogrify + ImageMagick for image processing
- Content: Markdown with Earmark parser
- i18n: Gettext (German/English)
Want to see more? Check out my GitHub repository or get in touch.
Previous articles: