WLE Website
A public website and self-hosted CMS for an independent wireline and well-testing company, built on a strict contract between the people who publish content and the code that renders it.
- Public routes
- 22 approved
- Content model
- 16 collections · 13 globals
- Animation libraries
- None
- Pinned scroll sequences
- Two
- Context
- Well Logging Energy · Professional work
- Client
- Independent wireline and well-testing services, Pakistan onshore fields
- Period
- June 2026 — Present
- Role
- Requirements, architecture, constraints, direction and review
- Shape
- One Next.js application carrying both the public site and its CMS
- Status
- In development · CMS connected, content migration in progress
- Visibility
- Private repository · no source, screens or client data shown
Covers
Stack
- Next.js
- React
- TypeScript
- Payload CMS
- PostgreSQL
- Tailwind CSS
- Railway
- Cloudflare Turnstile
The problem
WLE is an independent wireline and well-testing contractor working Pakistan's onshore oil and gas fields. Its public presence had to do two unrelated jobs at once: present the company to operators evaluating a service provider, and give the people inside it a way to keep that presentation current.
Those two audiences pull in opposite directions. A visitor wants a site that reads as considered and specific — real photography, real evidence of how the company works safely, a clear account of what it actually does. An editor wants to correct a phone number without waiting on a deployment.
So the brief was never a set of pages. It was a system in which the public surface and the publishing surface could each move at their own speed without breaking the other.
Public website
People finding information
CMS
Structure and publishing
Content editors
People maintaining it
One contract, two sides
The decision the rest of the project rests on is that no component knows where its content comes from. Every section and card is presentational and receives its data as props. Pages are the composition layer, and the only place aware that an origin exists.
Between the two sits a single typed contract module — the declared shape of every piece of content the site can render. A mapping layer converts CMS documents into those contracts, and is the only runtime code permitted to import generated CMS types. Let components read CMS types directly and they are coupled to the CMS exactly as they were once coupled to placeholder files.
The payoff arrived when the CMS was connected: it was a change in the page files and nothing else. Before the boundary existed, roughly two dozen components imported placeholder content directly, and swapping in real data would have meant editing every one of them.
One rule keeps it from eroding. Contracts are declared explicitly, never inferred from sample data. A type derived from a fixture only describes that fixture, and silently changes shape the moment the fixture does — so typing a prop as "whatever this placeholder happens to be" is banned outright.
The same discipline covers imagery. The media reference type treats a photograph that does not exist yet as a first-class case: the source is optional, the alt text is not. A picture nobody has taken is still described, still laid out and still accessible — it renders as an intentional placeholder rather than a broken image or a silent gap.
Public visitors
Read
Content editors
Publish
Next.js routes
Composition layer
Content contracts
Declared types
Mapping layer
CMS → contracts
Payload CMS
Collections & globals
PostgreSQL
Migration-driven schema
Presentational components
Props only
Private object storage
Proxied media
The content model
Payload runs self-hosted inside the same Next application rather than as a separate service, so the site and its administration share one deployment, one type system and one build. The trade is that the admin brings its own document shell — which is why the application has no single root layout, and instead gives the public site and the CMS a root each.
The model splits along a line that matters. Reusable business entities are collections; page-specific editorial structure lives in globals. People, locations, departments, policies, standards, news, job openings and enquiries are things the organization has. A page's headings and section order are things a page has.
Nothing is stored as a catch-all block of page JSON. Repeating structures are real relational child tables, which is what makes them queryable, migratable and safe to change deliberately rather than by overwriting a blob.
Everything public carries a draft, published and archived lifecycle plus revision history. An anonymous reader sees published records; a signed-in editor sees the whole workflow. Media and documents are separate taxonomies with separate access rules, so a private CV never inherits the behaviour of a public photograph.
- MEDIA
- DOCUMENTS
- PEOPLE
- NEWS
- POLICIES
- ROLES
- JOBS
- STANDARDS
- DEPARTMENTS
- LOCATIONS
- ENQUIRIES
Motion without a library
There is no animation library on the public site, and adding one would be a regression. Motion is CSS transitions and keyframes triggered by a single IntersectionObserver wrapper with a forward or pop variant and a delay for stagger. That covers the entrance reveals carrying most of the site, at no dependency cost.
Two sequences go further and are built as pinned, scrubbed experiences: the company mark unwinding act by act over a field of formation strata with a depth-log readout, and a head-to-toe inspection of protective equipment annotated across eight acts.
Both follow the same shape. A pure function maps scroll progress, zero to one, onto opacity, transform and state. Because it is pure, the timeline can be verified numerically without a browser — and both were. The component that plays it writes results straight to the DOM through refs on each animation frame, so scrolling never triggers a React render.
Reduced motion is not a degraded version of that. When the preference is set, a static summary renders instead: the same information, arranged to be read rather than played. The preference is honoured live, not only at first paint.
- Entrance reveals
- Pinned scroll sequences
- Pure-function timelines
- Per-frame DOM writes
- Reduced-motion alternates
- Keyboard-accessible gallery
When to take the scroll
The two pinned sequences answer the same question in opposite ways, and the difference is the point. The homepage sequence captures the scroll. It is the first thing a visitor meets, it is the company's own mark being explained, and it earns a moment that advances act by act rather than by distance.
The safety-equipment sequence does not capture anything. It sits on a page someone may have reached looking for one specific fact, and an information page should not fight a reader who wants to leave it. The same engine drives both; only the decision about whose gesture wins is different.
Scroll captured — the homepage
- The first thing a visitor meets
- The company's own mark, explained
- Advances by act, not by distance
- A held moment is the whole point
Scroll left alone — the safety page
- Reached by people after one fact
- Pinned and scrubbed, never hijacked
- Leaves at the reader's pace
- Same engine, different authority
- Attention
- or
- Autonomy
Invariants
A design system on a real project erodes through small exceptions, so a few things are held as invariants rather than conventions — enforced by removing the escape hatch, not by asking people not to use it.
Every public hero shares one typographic contract. A route may vary its photography, crop, height, motion and copy; it cannot quietly change a hero's font, size, weight, leading or tracking. The per-page typography overrides that once made that possible were deleted rather than deprecated.
Header height is a single derived token instead of a number repeated in a dozen places. It is computed from a viewport unit, bounded, and read by everything that depends on it — viewport-fitted heroes, pinned stage padding, section snap offsets. The rule is capability-based: nothing sniffs an operating system or a browser. Display scaling and extra browser chrome shrink the usable height, and the geometry follows instead of diverging from it.
The brand values the animations paint with — strand colours, stage background — stay in code and are explicitly not editable through the CMS. An editor changing a background would break the contrast the sequence is tuned against, and it would surface as a bug rather than as a content change.
- 01One typographic contract for every public hero
- 02Per-page typography overrides removed, not deprecated
- 03Header height derived once and consumed everywhere
- 04Capability-based sizing — no operating system or browser sniffing
- 05Animation brand values held in code, outside the CMS
- 06Schema reachable only through committed migrations
Known-data KPI test
- Viewport unit
- Bounded range
- Header token
- Hero height
- Stage padding
- Snap offsets
The chain is how one measurement propagates, named at stage level only. No values, breakpoints or thresholds are published here.
How it grew
The order was deliberate. The content boundary was extracted and every component moved onto props before any CMS existed, which meant the integration later had somewhere to land instead of somewhere to invade.
Work that is finished but not currently on the site is parked rather than deleted: kept restorable, excluded from the type-check, lint and build, and held outside the application source. A long-form leadership sequence lives there now — one crew photograph panned and zoomed person to person across roughly fifteen screens of pinned, scrubbed scroll.
Parking it paid for itself. Two bugs found while building it apply to the two live sequences, and are written down beside it where they will be found again: a viewport height reading as zero in a hidden tab, and a scrubbed timeline taking its duration from its last step rather than from its intended runway.
- 01
Route hierarchy
The approved sitemap built out against placeholder content, with responsive navigation.
- 02
The boundary
Content contracts declared and every component moved onto props — before any CMS existed.
- 03
CMS foundation
Payload self-hosted in the same application, with auth, lifecycle, revisions and migration-driven schema.
- 04
Connection
Page files switched to CMS reads through the mapping layer. No component changed.
- 05
Real content
Company photography, contact details and page copy migrated in. The remaining pages still carry placeholders.
Delivery discipline
Schema changes are committed migrations in every environment, with the CMS's development-mode auto-push disabled everywhere including locally. The build compiles without database access at all; migrations run afterwards as a separate pre-deploy step on the private network, and the release is promoted only if they succeed.
That arrangement is fail-closed without coupling compilation to a database. A bad migration stops a deployment instead of half-applying itself to a live schema, and a database that is briefly unreachable cannot break a build.
Uploads go to a private object store and reach the browser through server-side proxies that apply access checks and immutable caching, rather than being exposed directly. Public imagery and access-controlled documents travel separate paths on purpose. Retired routes are permanent redirects, so old bookmarks keep working without unapproved pages staying in the sitemap.
Client
Railway
Build and runtime
Build
Compiles without database access
Pre-deploy migrations
Private network
Next.js + Payload
One deployment
PostgreSQL
My responsibility
I gathered requirements from the people who would use the site on both sides, shaped the architecture and the constraints it had to hold, directed AI-assisted implementation against them, tested the working system, reviewed changes and iterated on weak behaviour.
The constraints are the part I would point at. That components may not import content. That contracts are declared rather than inferred. That schema moves only through committed migrations. That an information page does not take the reader's scroll. Those decisions are what the codebase is still holding, and they are why a section can be added now without renegotiating the whole system.
- 01Requirements
- 02Architecture
- 03Constraints
- 04AI-assisted Development
- 05Testing
- 06Review
- 07Iteration
I do not claim that I manually wrote every line.
What I can show
The repository is private and the site is a client's, so this page describes the system rather than displaying it. Everything above is architecture and reasoning: decisions, boundaries, the shape of the model and why each one is there.
What is not here is the operational surface — no source, no credentials or secret names, no database or storage identifiers, no admin or API paths, no schema definitions, and no client records or contact details. Approved screenshots would extend the first list; they would not shorten the second.
Can show
- Client and project name
- Technology stack
- Architecture and its rationale
- Content model at concept level
- Motion and interaction techniques
- Delivery and access discipline
- Approved screenshots when available
Private
- Source code
- Credentials and secret names
- Database and storage identifiers
- Admin and API paths
- Schema definitions
- Internal and client records
Current status
The route hierarchy and responsive navigation are complete, and the content routes read through the mapping layer from a connected CMS. The committed migrations are applied, and the production asset library is in place behind its proxy.
Real company content is partially migrated: the homepage and the company overview and vision pages carry supplied photography and copy, while the remaining pages still render approved placeholders. That is the honest reason this case study describes a system rather than a finished website.
- Route hierarchy
- Complete
- Responsive navigation
- Complete
- CMS connected to content routes
- Complete
- Content model & migrations
- Applied
- Production asset library
- In place
- Real company content
- Partially migrated
- Public launch
- Pending client sign-off
Publishing and access
- Anonymous visitors read published records only
- Signed-in editors see draft, published and archived states
- Writes require authentication; destructive administration is separated again
- Public enquiries are never written straight to a collection — a validated handler stands in front
- A honeypot field, a per-address rate window and optional bot verification guard that handler
- Uploaded documents carry their own access checks, independent of public media
Concept level only. No roles, permissions, routes, endpoints or field names are published here.
Evidence boundary
Supported
I can account for the architecture in detail: the content and presentation contract and why it exists, the shape of the CMS model, the hand-written motion system and its two pinned sequences, and the migration, access and delivery discipline around them.
Not overstated
The repository is private and the site has not launched publicly, so none of this is independently verifiable from outside. I show no source, screens, internal content or client records, and I claim no traffic, business or commercial outcome.
Technical notes
The public site and its CMS ship as one Next.js application, backed by managed PostgreSQL and a private S3-compatible bucket whose objects are served through access-checked proxies. Schema moves only through committed migrations, applied in a pre-deploy step before a release is promoted.
Core stack
- Next.js (App Router)
- React
- TypeScript (strict)
- Payload CMS
- PostgreSQL
Interface
- Tailwind CSS
- CSS keyframes + IntersectionObserver
- No animation library
Data & media
- Migration-driven schema
- Private S3-compatible object storage
- Access-checked media proxies
Deployment & services
- Railway
- Pre-deploy migration step
- Permanent redirects for retired routes
Security & integrations
- Cloudflare Turnstile
- Honeypot and rate-limited submissions
- Server-side validation of every public form
- Repository
- Private
- Public artifacts
- Approved screenshots and diagrams will be added after redaction and review.