# Imago Brand Sites

*Neither site may state a figure the client did not supply, so a missing spec reads "Available on request".*

**Category:** WEB PLATFORM / B2B AGRICULTURE
**Status:** in progress
**Period:** July – September 2026
**Built by:** HM Ammar, Digital Architect & Head of IT, Kuala Lumpur

**Stack:** Next.js 16 (static export), React 19, TypeScript (strict), Tailwind CSS v4, Framer Motion, Lenis, JSON-LD, GitHub Actions

## The challenge

Imago sells insect protein to feed formulators and organic fertilizer to commercial farms. Neither site has a cart. Every conversion is a request for pricing or a technical document, so I was writing for technical buyers who want composition ranges, not adjectives. Both briefs forbade me from stating any number the client had not supplied, and both sites had to run on shared hosting with no Node runtime.

## What I built

I built both as Next.js 16 App Router sites that static-export to plain HTML. Each keeps its product, spec and application facts in one typed content layer that feeds the rendered page and the JSON-LD from the same value, so the two cannot disagree. For the fertilizer site I reused the protein site's planning package and design system, recoloured. Deployment was the harder half: shared hosting clones a branch into the web root and runs nothing, so what I push has to be the finished HTML, not the source.

## The detail

### A content layer that cannot invent a number

Every product, spec and application fact lives in one typed layer, cited back to the brief by section. I write absent values as `null` plus a `[TBD]` marker and render them as "Available on request" instead of a plausible-sounding figure; the fertilizer codebase still carries over 140 such markers. I locked a CANON document that fixes the hex palette, route slugs and CTA strings so parallel work could not quietly diverge.

### Ten clips, one palette

Ten pages, ten stock clips, ten colour casts fighting the brand. I strip each clip to luminance with `grayscale(1)` and recolour it toward brand gold through a `mix-blend-mode: color` layer, so a blue lab shot and a green forest canopy resolve to the same world. The still poster paints first, which keeps the video out of the LCP path, and under reduced motion I never mount the video element.

### Scroll-pinning without wheel interception

The applications showcase pins six industry slides: a 700svh wrapper with a `sticky top-0 h-svh` stage inside, so native scroll drives Framer's `scrollYProgress` and nothing intercepts the wheel, which is what lets it coexist with Lenis. I left a comment in the file recording why I could not reuse the shared `.hero-band` class: it sets `position: relative`, which overrides Tailwind's `sticky` at the same specificity and silently breaks the pin. Below `lg` and under reduced motion the parent switches to a plain grid.

### Shipping HTML, not source

Hostinger's Git integration clones a branch into the web root and runs nothing. Pointed at `main`, the site served `package.json` and TypeScript source with no `index.html`, because `out/` is generated at build time and gitignored. A GitHub Action I wrote now builds the export, checks that `out/index.html`, `out/404.html` and `out/.htaccess` exist, then force-pushes the contents of `out/` to a `deploy` branch whose root is the finished site.

### An .htaccess for a server that fails quietly

Shared hosting runs LiteSpeed, which reads .htaccess like Apache but ignores directives it does not support instead of erroring, so a file that works locally can 500 in production. I wrap every module-dependent block in `<IfModule>`, and never use `Options` or `DirectorySlash`. My cache policy splits on a real distinction: hashed build output gets a year and `immutable`, while `/assets/**` filenames are stable, so replacing a hero photo must still be able to expire.

## The outcome

Both sites build clean from a cold clone: no database, no API key, no environment file. The protein site serves from a temporary Hostinger preview subdomain, with the production domain still unset. The fertilizer site's CI publishes a built export to its `deploy` branch; its staging host still returns 500, and my DEPLOYMENT.md diagnoses the remaining step as a branch setting in the client's hosting panel. Of a 16-item client change list, I applied 14, left one partial and left one blocked on a client decision rather than quietly guessing.

---

[All work](https://hmammar.world/) · [HTML version](https://hmammar.world/work/imago-products.html)
