Glaser Mills has been supplying fabric (flags, banners, tactical and outdoor textiles) since the late 1940s. Their public website hadn't kept pace: a Bootstrap template from another era, copy that hadn't been touched in years, and no one on staff who could safely make a change without calling a developer. (This is a separate engagement from our long-running work on Glaser Mills' internal Rails inventory system, covered in that case study. This one is about glasermills.com, the public-facing marketing site.) The brief from Mike, the owner, was simple to say and hard to do well: "make it look like a real company," and "let me not be the bottleneck."
This became as much an exercise in treating AI-assisted delivery as an engineering discipline as it was a website project. We applied the same rigor around knowledge management, permissions, and audit trails that we'd apply to any production system. This post walks through the process and the decisions that mattered, including a few we got wrong on the first pass and had to fix live.
What we were working with
This wasn't a green-field site. It was a legacy Bootstrap/jQuery site that needed retiring page by page without a visual regression, a non-technical owner who needed to review copy and occasionally make small edits himself, and a brand voice that lived in nobody's head consistently: some in old call recordings, some in Mike's own phrasing, some just implied by what had shipped before. There was also a future chat/intake feature that had to never misrepresent the product, because a wrong fabric recommendation is a real customer-relationship problem, and it doesn't read to a customer as a software bug.
None of that is unusual on its own. What ate the calendar on this project was the scaffolding we built around the AI agents doing the work, more than the site itself.
Decision 1: knowledge lives in three layers, outside the agent's head
Early on we adopted a structure based on Karpathy's llm-wiki pattern:
raw/ # immutable sources (call transcripts, the customer list, company history)
wiki/ # compiled knowledge (brand voice, vocabulary, CTAs, design patterns)
AGENTS.md # the schema: workflows, conventions, decision discipline
raw/ is read-only, ever. Agents never edit a transcript. When a new sales call came in, an explicit ingest workflow ran: read every file in the call folder, classify each atomic fact into the right wiki page (company facts, tone, vocabulary, CTA wording, content pillars...), and write it with an inline citation back to the source. If a new fact contradicted something already in the wiki, the rule was to flag it as [CONFLICT ...] inline and surface it, not silently overwrite it.
That last rule mattered more than we expected. Two real examples surfaced this way and are still sitting in the wiki as open questions rather than papered over: the company's founding year (1949 in one source, "late 1940s" in another), and whether Glaser Mills is "third-generation" or "eight generations of textile expertise" depending on which document you trust. An agent with no memory of a contradiction will happily pick one and move on. A wiki with provenance forces the contradiction to stay visible until a human resolves it.
Copy generated months apart, by different sessions, stayed consistent, because "consistent" was written down and citable rather than reconstructed from vibes each time.
Decision 2: the design system is a contract
We wrote DESIGN.md as the human-readable source of truth for color, typography, shape, and component recipes: a "Heritage Industrial" design language with a defined palette (navy chrome, red for primary CTAs only, a separate brighter heading-navy for legible display type on light backgrounds), Material-Design-3-derived tokens, and explicit rules like "touch targets are minimum 48px on both axes."
We enforced one rule: DESIGN.md and the Tailwind @theme block had to stay mirrors of the same contract. A new visual token isn't sanctioned until it exists in both places. A new component pattern goes in DESIGN.md first, then becomes a Handlebars partial once it's used three or more times (below that threshold, it stays inline; we didn't want premature abstraction). When the two drifted, DESIGN.md won and @theme got fixed to match, because the human-readable spec is the one a non-engineer (or a new agent session with no memory of prior conversations) can audit.
This is what let us migrate the legacy Bootstrap pages (about, contact, small_cuts, color_card, 404) one at a time onto Vite + Tailwind CSS v4 + Handlebars partials without the site ever looking like two different products stitched together mid-migration.
Decision 3: make the chat agent refuse by default
The riskiest new feature was a live chat/intake widget. A generic chatbot for a fabric manufacturer is a liability: if it recommends the wrong SKU, quotes a lead time it doesn't know, or names a fabric by a common name Mike doesn't want used publicly, that's a conversation with a real customer gone wrong, and there's no rollback for that.
So the architecture decisions here were deliberately conservative:
| Decision | Why |
|---|---|
| Small Node/TypeScript service, separate from the static site, its own deploy lifecycle | Keeps the marketing site's build simple; the chat service can iterate and redeploy independently |
| Per-turn Server-Sent Events instead of WebSockets | The interaction is basically request/response; SSE gets streaming tokens without a stateful socket server |
| Server-side guardrail pass on every assistant turn, before it reaches the visitor | Blocks/rewrites three categories: fabric recommendations, specific specs/availability/lead-times, and prohibited common product names. Each gets replaced with a consistent handoff line rather than a refusal that reads as broken |
| Discord as the human-takeover console | Any thread member can !takeover and drive the conversation directly; the bot mirrors both sides into the thread either way |
| JSONL append-only transcript logging, including every guardrail rewrite (both versions) | Full audit trail: you can see exactly what the model wanted to say and what shipped instead |
| Quantity-based routing to email vs. urgent-phone handoff | Small orders redirect to the self-serve small-cuts page; mid-size orders get a same-day-SLA handoff; large orders (10,000+ yd) get a flagged, phone-first escalation |
In practice, the guardrail rewrite log became the most valuable part of this design. The model rarely misbehaved; what mattered was that the log let us tune the system prompt against evidence, real transcripts and real rewrites, instead of guessing at edge cases.
Decision 4: give the owner real write access, safely
Mike isn't a developer, but he needed to say "change this headline" over Discord and see it live before it shipped, without waiting on us for every small copy tweak.
For exactly this, we deployed Hermes Agent (Nous Research) on a dedicated, locked-down service account, rather than routing his requests through a general-purpose agent running as the deploy user. We installed and configured it, wired up the Discord integration, and built the custom skills, scripts, and guardrails around it. Specifically:
- Its own unprivileged Linux system user, no login shell, only reachable via the Discord gateway process
- Filesystem access split cleanly: read the whole repo (so it has context: brand voice, design system, prior decisions), write only inside the site's own source tree (and later, on request, the chat service's source, but never its build or deploy pipeline)
- Local git commits allowed (so tested changes get a durable, revertible checkpoint) but no push, and no
.git/hooksor.git/configwrite access. A writable hook is a way to get arbitrary code executed the next time anyone else runs an ordinary git command in that repo - On-demand build+preview, scoped so it can never fight the primary developer for the same port
- No AWS credentials, no SSH keys, no access to anything outside the project
We treated this the way we'd treat any production access-control problem: build the boundary, then try to break it rather than assume the design held. That testing found a real issue: a secrets file was briefly more permissive than intended and got read out during a verification pass. We tightened it and rotated the exposed credentials the same day. We'd rather report that than pretend the first draft of a permission boundary was perfect. Testing it live is what caught it before anyone with bad intentions could.
We also learned to write the recipe down once. Partway through, we found three different, slightly diverging copies of the same "rebuild and restart the preview" procedure: one in a doc, one tracked in scripts, one untracked and forgotten. Three copies of one truth is itself a bug, so we collapsed it to a single script, referenced everywhere else, so it can't silently drift again.
Decision 5: keep hosting boring
For the marketing site itself, there was no reason to reach for anything exotic. Static HTML built by Vite, synced to an S3 bucket, served through CloudFront with a real TLS cert. Deploy is one script: build, sync with --delete, invalidate the CDN. Monthly hosting cost for a small marketing site like this is usually under two dollars. The chat service, which does need a live process, lives on its own small VPS behind a separate CloudFront distribution with its own certificate, isolated from the static site's origin, so an incident in one doesn't touch the other.
What carried across the whole project
A few habits kept showing up, across subsystems that otherwise had little to do with each other. When a fix required undoing what the last fix did, that was a sign the earlier fix was wrong. We ran into this constantly while debugging permission issues and build quirks, and the fix that removed a constraint always beat the one that bolted a special case on top of it.
The wiki's change log and the chat service's transcript log ended up working the same way, without us planning it that way: append, never overwrite, and write each entry so a future session, human or agent, can reconstruct why without asking. And the durable stuff, DESIGN.md, the brand playbook, the guardrail categories, all got written for a human to read first and a machine to consume second.
Where it landed
Glaser Mills now has a site that looks like the company it is, a chat intake flow that routes real inquiries without overstepping into product advice it shouldn't give, and an owner who can ship a copy change himself on a Tuesday night without waiting on a developer. Every one of those self-service edits still lands as a reviewable, revertible git commit. None of it took exotic technology, just treating "let AI agents do the work" as something to design carefully instead of a shortcut past the real work.