Marketing strategist site and CMS
A consultant needed a portfolio and services site they could run alone. I built it as a fixed set of page sections on a Filament admin, with search metadata that is never blank and a manual inside the panel.
A case study by Arif Hossen
Source: https://arifhossen.dev/work/marketing-strategist-site-and-cms
- Role: Sole designer and engineer
- Team: Me, for the site's owner
- Duration: About 6 weeks
- Stack: Laravel 13, Filament 5, Livewire, MySQL
- Period: Aug to Oct 2026
- Engagement: Unpaid build
- 110 Commits, all mine, over 10 active days
- 10 Section types a page can be composed from
- 11 Admin resources, one per business object
- 65 Test files covering the site and the panel
A site the owner has to run
Context
The owner is a consultant whose website is their sales channel. A site goes stale the moment changing it needs someone else. But a free-form page editor in the hands of a non-technical owner produces pages that render badly and look worse than the ones they replaced. I wanted full control over content, no way to break the presentation, and an owner who still knows how to use it a month after handover.
I built it in phases: a data spine, the public front end, the admin panel, then search metadata and deployment. Each phase has a written spec and plan, and a decision log that records every ruling made along the way.
Pages are compositions, not documents
Composition
Each page stores an ordered list of sections chosen from ten fixed types: hero, trust bar, about, services, skills, portfolio, pricing, testimonials, questions and contact. One enum owns three facts about each type. It knows its label, whether it can be dropped onto a page, and which component renders it. The admin's section builder and the front end read the same source, so they cannot drift.
<!-- diagram:page_composition -->
Diagram: Page composition
One enum owns what the admin can drop onto a page and which view renders it, so the builder and the front end cannot disagree about a section type.
A composer resolves a page's sections into readonly objects that carry their data, and one Blade component renders each type. Everything the business sells is its own admin resource: services, pricing categories with their packages edited in place, portfolio items, skills, testimonials, questions and posts. Six kinds of small proof item, such as statistics and certifications, share one table with a group column instead of six tables.
No visible string is hardcoded in a template, so a price changed in the panel is the price on the page. Publishing and manual ordering are two shared query scopes, each backed by an index, so no list can accidentally show a draft.
Metadata that cannot be blank
Search
Optional fields get left empty, and mandatory ones get filled with whatever makes the form submit. So search metadata is optional to write and impossible to leave empty. Any model can carry one metadata record: title, description, canonical address, robots directive, social cards, a focus keyword and structured-data overrides. Nothing reads those columns directly. Every tag resolves through a service that falls back to the site's defaults, so a page nobody filled in still ships a complete title, description and share card.
The robots directive is an enum that knows whether it indexes. The sitemap asks the value itself, which is what stops the sitemap listing a page the same system told search engines to ignore. Structured data is built as a graph from settings plus the current record, not pasted as a snippet that can contradict what is on screen.
Readable by AI assistants, on the owner's terms
Assistants
Buyers increasingly ask an assistant before they open a search engine. Alongside the sitemap, the feed and the robots file, the site serves two plain-text files that describe the business, its services and its prices. They are assembled from the same published content the pages render, so the two cannot disagree.
The robots file addresses the major AI crawlers one by one. Each crawler's allow-or-block decision is a setting the owner can change in the panel, and the default is allowed. Withdrawing access from one assistant is a toggle, not a deployment.
A contact form that dropped real enquiries
A defect
The most instructive fix is the last one before this write-up. The contact form carried an off-screen honeypot field, and a submission that filled it was rejected as spam. Autofill and password-manager extensions fill hidden fields, so a real visitor's first submission could be rejected as spam. Failed submissions also sent the visitor back to the top of the page, with the errors out of sight.
The fix hides the honeypot properly, renames it and tells password managers to ignore it. Success, validation errors and expired sessions now return to the form with the message at the top of it. An expired CSRF token returns the visitor's input instead of a bare 419 page. Validation is tighter: no links in the name, a phone format, a minimum message length, lowercased email and string-only fields. Each path has a test. I cannot say how many enquiries were affected before the fix.
Shipping it, and handing it over
Delivery
Deployment is an atomic release to shared hosting through GitHub Actions, with a health-check call after each deploy. Production seeding is guarded so the shared-password test user is never created there. Branded error pages link home, to the portfolio and to contact, and fall back to a lean page if the site layout itself cannot render.
The panel carries a written user manual as a page inside it, mirrored by a document in the repository. The instructions live where the owner works, not in an email they will lose. Handover is part of the product, not a conversation after it.
Flag it, then fix it in its own change
What I would do differently
At the end of the public front-end phase I found two problems in earlier code and flagged them instead of changing code outside that phase's scope. One was a settings lookup that went to the cache store about fifty times per page. The other was a resolved section that held one collection of content when three section types needed several, so the hero statistics and the skills items were queried inside their Blade components.
Both were fixed the next day, in their own change: an in-request memo for settings, and keyed extras on the resolved section. I would keep working that way. The contact form defect above is a different lesson. Autofill is invisible to a server-side test, so I would now add a browser test for it.
I have no traffic, enquiry or conversion figures, so I cannot say whether the site works as a sales channel. I can say how it is built.