xalonious

Case study

Portfolio Platform

A self-hosted portfolio platform with a custom SQLite CMS, structured case-study editing, persistent media, revision history, and a shared data layer used by both the Next.js website and Go SSH interface.

My Portfolio Website project preview

Technology

  • TypeScript
  • React
  • Next.js
  • Tailwind CSS
  • SQLite
  • Framer Motion
Role
Sole developer
Year
2026
Status
In active use

Context

When content updates became code changes

My portfolio originally stored every project and case study in a single TypeScript file. This worked while the content was relatively static, but longer case studies gradually turned routine writing changes into application changes.

Updating a paragraph, changing an image, or documenting a new feature meant editing source code, creating a commit, and deploying the entire site. A growing portion of the repository history consisted of content-only commits, and the same project information was also hardcoded separately in my Go SSH portfolio.

I built a private content system so projects could be edited independently from the application while still supporting the custom layouts used throughout the site.

The main design problem

Rich enough for case studies, constrained enough to edit

The difficult part was not storing project metadata in SQLite. Case studies contain paragraphs, highlighted observations, ordered flows, structured comparisons, image groups, captions, and layout-specific presentation choices. A basic collection of text fields would not be expressive enough, while unrestricted HTML would make the editor fragile and tightly couple stored content to the current frontend.

I designed the case-study format as a small collection of structured content blocks. Each block describes the meaning of the content rather than its final HTML, allowing the public site to remain responsible for presentation.

Paragraphs
Long-form narrative content separated into readable passages.
Highlights
Short observations or constraints that need stronger visual emphasis.
Steps
Ordered processes such as search, validation, publishing, or request lifecycles.
Details
Repeated title-and-description pairs for comparisons and technical breakdowns.
Images
Media references with alternative text, captions, dimensions, layout, and lightbox preferences.
Case studies are assembled from typed sections and reusable content blocks rather than unrestricted HTML.

The system

One source of truth for two portfolio interfaces

The Next.js application now stores projects in a SQLite database outside the deployed repository. Published metadata remains available through ordinary columns, while each case study is stored as validated structured JSON. Drafts and revision snapshots are kept separately from the currently published version.

The public website reads published projects through a repository layer. A dedicated published_projects database view exposes the same content contract to my Go SSH portfolio, which opens the database read-only and adapts the rows into its terminal UI model.

Publishing once therefore updates both interfaces. The Next.js site renders the full visual case study, while the SSH portfolio uses the fields relevant to its keyboard-driven terminal experience.

One publishing workflow provides shared project data to both the web and SSH portfolio interfaces.

Content workflow

Separating editing from publishing

Editing and publishing are separate operations. A draft can be changed privately without affecting either public portfolio. Publishing validates the complete document, updates the public project record, and creates an immutable revision snapshot before the new content becomes visible.

  1. 01

    Edit

    Build project metadata and case-study sections through the private editor.

  2. 02

    Validate

    Show readable field-level errors for missing images, alternative text, headings, malformed slugs, and invalid nested content.

  3. 03

    Save

    Store unfinished changes as a private draft without changing the public website.

  4. 04

    Publish

    Update the published record and preserve the document as a revision.

  5. 05

    Consume

    Let the Next.js and Go applications read the same newly published project.

Persistent media

Keeping uploaded content outside deployments

Images were originally stored in the repository’s public directory. That was appropriate for versioned assets, but not for files uploaded through an editor: an automated deployment could replace the application directory and remove anything created at runtime.

Uploaded images now live under persistent application data on the Raspberry Pi. The CMS validates each upload, processes it into WebP through Sharp, records its metadata in SQLite, and exposes it through the /media public path.

Project deletion also collects every media reference from the published document, current draft, and revision history. Because media is owned by one project, the corresponding database records and physical files can be removed together without leaving unused uploads behind.

Administration

A private editor without a public login system

The editor is intended for one administrator on a self-hosted server, so I did not build a multi-user authentication system. Nginx restricts /admin and its descendants to my public IP before the request reaches Next.js.

After the IP check, Nginx replaces a trusted internal header that the application verifies on every admin page, server action, and media endpoint. Client-supplied versions of that header are stripped from ordinary requests, and the application port is blocked from direct public access by the host firewall.

This keeps the authorization boundary at the reverse proxy while still requiring the application to verify that each administrative operation passed through it.

Operations

Developing against real content without committing it

Moving content out of Git created a new development problem: a fresh local database no longer contained the projects visible in production. I added a one-way synchronization command that connects to the Raspberry Pi over SSH, creates a consistent SQLite snapshot, and downloads it together with the persistent media directory.

Both local applications can read that ignored snapshot. The Next.js editor uses it directly, while the Go portfolio points its database environment variable at the same file. Production remains the source of truth, and local development never writes back to the live database.

Remaining limitations

Purpose-built rather than general-purpose

Single administrator
IP-based access fits my current deployment but would not be appropriate for multiple editors or frequently changing networks.
Structured layouts
The editor supports the layouts used by my portfolio, but adding a fundamentally new presentation still requires a new block type and renderer.
Local storage
SQLite and filesystem media keep deployment simple, but both must be backed up and remain available to every consuming application.
Revision model
Revisions preserve complete published documents rather than presenting field-level diffs or collaborative editing.

What I learned

Content becomes infrastructure once more than one interface depends on it

This project began as an attempt to avoid editing a TypeScript file and grew into a lesson about content modeling, persistence, trust boundaries, and shared data contracts. The most important decision was not choosing SQLite; it was deciding which parts of a case study were content and which parts should remain presentation.

Separating those responsibilities made it possible for two very different interfaces to share the same project data without forcing either one to understand the other’s rendering system.

It also reinforced that small workflow problems can justify dedicated tooling once they occur frequently enough and begin affecting multiple systems. The result is intentionally specific to my portfolio, but it gives me a publishing workflow I can continue using as the projects and case studies grow.