Post

A Cinematic Portfolio on Infrastructure I Own

The architecture behind denmotion.com. Three independent layout engines in one Jekyll repo, self-hosted zero-compression video delivery, and a branded share system, all on a single S3 bucket fronted by CloudFront.

A Cinematic Portfolio on Infrastructure I Own

I shoot film and photography under a brand called DenMotion. The work is hosted on infrastructure I built and own, no Squarespace, no Vimeo, no YouTube embeds. A visitor lands on a dark particle animation, picks Films or Photos, and drops into an edge-to-edge cinematic grid where video thumbnails play on hover. Clicking one opens a full-screen player streaming the master file at the exact quality it left Premiere Pro. The whole thing runs on a single S3 bucket and CloudFront, costs under a pound a month, and deploys from a private repo on git push.

This post is the architecture. The brand story and the full build logs live across three posts on my personal site, linked at the end. This one covers what was engineered and why it holds together.

The Problem With Portfolio Platforms

Filmmakers spend thousands on a camera, hours colour grading, then upload to a platform that compresses the result. YouTube crushes dark shadows and shifts grades. Squarespace recompresses uploaded video natively. The standard fix is paying for Vimeo on top of a site builder, two subscriptions to show one reel. I wanted the client to see the exact file that came out of the edit, served fast, with no third-party logos and no recommended videos at the end. That requirement drove every decision below.

Three Layout Engines, One Repo

The constraint that shaped the build is that a portfolio has three kinds of page with three completely different needs. The landing page wants to be cinematic. The galleries want to be edge-to-edge and immersive. The case studies and contact pages want to be readable. No single template does all three well, so the site runs three independent layouts inside one Jekyll project.

graph TB
    A[denmotion.com] --> B[landing.html<br/>HTML5UP Dimension + particles.js]
    A --> C[cinematic.html<br/>Edge-to-edge dark grid]
    A --> D[Chirpy default<br/>Sidebar, typography, blog UI]
    B --> E[Landing page]
    C --> F[Photos grid]
    C --> G[Films grid]
    D --> H[Case studies]
    D --> I[About, Contact]

The trick that makes this work without the three systems fighting is asset isolation plus standalone layouts. The landing and cinematic layouts are self-contained HTML documents that do not extend Chirpy’s templates, each with its own head and its own stylesheet, and the HTML5UP assets live isolated under their own directory so the two design systems never load on the same page. The result is zero CSS bleed, the landing page shows no Chirpy styling, the galleries show no blog UI, and the visitor moves between all three without noticing they are different engines.

Self-Hosted Video, the 3-File Strategy

Hosting video properly is the part most portfolios get wrong. A single master file is too heavy to autoplay in a grid and too slow to load on hover. So every film exists as three files on S3, each with one job.

FilePurposeWhen it loads
Poster PNGStatic first frame, prevents the empty black boxOn page load
Thumbnail MP4Five-second silent loop, 720p, low bitrateOn hover, or autoplay on mobile
Master MP4Full resolution with audioOnly when opened in the lightbox

The grid loads posters first, swaps to the silent loop on hover, and only fetches the heavy master when a visitor chooses to watch. Desktop plays on hover, mobile autoplays the muted loops since there is no cursor, and the layout detects which to use. The viewer never downloads a large file unless they commit to watching it, and when they do, it is the uncompressed master straight from CloudFront.

Clean URLs With a CloudFront Function

Alongside the portfolio sits a share system at a subdomain, for sending clients a single edited video as a branded page rather than a raw file link. It produced the build’s favourite piece of engineering.

I wanted to send share.denmotion.com/client/project, no file extension, a clean route rather than a link to a file in a folder. The problem is there is no file called project in S3, the file is project.mp4, so CloudFront looks for the extensionless path, fails, and errors. The fix is a CloudFront Function, a lightweight JavaScript snippet that runs at the edge in under a millisecond before the request reaches S3.

1
2
3
4
5
6
7
8
9
10
11
12
function handler(event) {
    var request = event.request;
    var uri = request.uri;

    // Real files pass through untouched
    if (uri.match(/\.\w+$/)) {
        return request;
    }
    // Clean URLs serve the branded viewer page
    request.uri = '/index.html';
    return request;
}

A request with an extension passes through to S3. A clean request gets rewritten to the viewer page, whose JavaScript reads the original path, appends the extension, and streams the matching video into a branded player. The visitor sees a dark DenMotion page with the work in it, never a raw file, never an extension. The same edge-function pattern later solved subdirectory routing when the main site moved to CloudFront, since CloudFront, unlike GitHub Pages, does not serve index.html from a directory path on its own.

Two Distributions, One Bucket

The entire system, portfolio and share, runs on a single S3 bucket partitioned by prefix, with two CloudFront distributions scoped to their own paths.

graph TB
    A[denmotion.com] --> B[CloudFront<br/>portfolio]
    C[share.denmotion.com] --> D[CloudFront<br/>share]
    B --> E[denmotion bucket<br/>website prefix]
    D --> F[denmotion bucket<br/>share prefix]

Each distribution uses an origin path scoping it to one prefix, so the portfolio distribution physically cannot read the share files and vice versa, one bucket with hard internal walls. A single wildcard certificate covers every subdomain, and the bucket blocks all public access so every byte is served through CloudFront via Origin Access Control. Nothing is reachable directly.

Private Repo, Automated Deploy

The site code is the intellectual property, the three layouts and the grid logic, so the repo is private. That breaks GitHub Pages on a free account, so the site deploys to S3 and CloudFront through GitHub Actions instead. On every push, the Action builds the Jekyll site, syncs it to the bucket’s website prefix, and invalidates the CloudFront cache, live in about forty-five seconds.

The deploy authenticates as an IAM user scoped to exactly what it needs and nothing more.

1
2
3
4
5
6
7
8
{
    "Effect": "Allow",
    "Action": ["s3:PutObject", "s3:DeleteObject", "s3:ListBucket"],
    "Resource": [
        "arn:aws:s3:::denmotion",
        "arn:aws:s3:::denmotion/website/*"
    ]
}

The deployer can write to the website prefix and invalidate one distribution. It cannot touch the share, films, or photos prefixes, cannot reach the other distribution, and has no console access. If the credentials leaked, the blast radius is the website files alone. Least privilege scoped to a prefix.

The Full Architecture

LayerTechnologyPurpose
DomainRoute 53DNS for apex and subdomains
Portfolio CDNCloudFrontServes the Jekyll site from the website prefix
Share CDNCloudFrontServes the branded viewer from the share prefix
RoutingCloudFront FunctionsSubdirectory routing and clean URLs at the edge
StorageS3, single bucketAll files, partitioned by prefix
TLSACM wildcard certEvery subdomain, one certificate
BuildGitHub ActionsJekyll build, S3 sync, cache invalidation on push
Deploy authIAM, prefix-scopedLeast-privilege programmatic access
MediaS3 + CloudFrontSelf-hosted, zero-compression video and photos

Two distributions, one bucket, one certificate, one hosted zone, one IAM deployer. Zero databases, zero servers, zero monthly platform fees beyond storage and requests. The whole thing runs for less than the cost of a coffee and delivers video at a quality the hosted platforms compress away.

What It Demonstrates

The reason this build is worth documenting is the intersection it sits on. Self-hosting cinematic video needs S3, CloudFront, edge functions, IAM, and DNS, the cloud engineering. Making a portfolio that actually feels premium needs custom layouts, a video grid, and hover-to-play logic, the frontend. Cloud engineers do not usually shoot and grade film, and videographers do not usually configure CloudFront distributions, and the build lives in the gap between the two. Adding a new piece of work is a few lines of config and a git push, which is the payoff of treating a creative portfolio as infrastructure.

The Full Build Logs

The complete story, the brand naming, every layout decision, the cinematic grid CSS, the share system, and the GitHub-Pages-to-S3 migration, is documented in three posts on my personal site. Building the brand and the portfolio site covers the three layouts and the cinematic grid. Building the share system covers the CloudFront Function and the branded viewer. Moving to S3 and CloudFront covers the migration, the IAM setup, and the deploy pipeline. The work itself is at denmotion.com.

This post is licensed under CC BY 4.0 by the author.