
Corporate Catalogue
A catalogue site and admin panel for an İzmir-based LED lighting manufacturer. Cart, payment and accounts are deliberately out of scope: the site's only job is to turn a visitor into a WhatsApp or phone conversation. Product pages are read from the database and generated statically at build time.
The business is pure B2B manufacturing: an electrician or a contracting firm supplies the measurements, production happens, and the product goes to the electrician's own customer. There is no direct sale to end consumers. In a business like that the default answer — "let's add e-commerce to the corporate site" — is not only unnecessary but harmful: it loads stock, payment, returns and a price list onto a flow that does not sell, and by making the numbers public it damages the commercial ground as well. The real problem is not selling but establishing trust and starting a conversation. The second issue was timing: the domain was in hand but the content was not ready, and the site still had to show something.
Three visitor types, one conversion goal. The electrician and the contracting firm: order repeatedly, look for a product type and technical detail in the catalogue, and get pricing by talking anyway. The architect and the project contractor: look less at standard products and more at custom-manufacturing capacity. The electrician's own customer: sees the site as a trust reference — the electrician shows it to them. The panel has exactly one user: the owner, managing products and incoming requests.
Single developer. The scope decision itself, data model, catalogue and static generation structure, admin panel, request flow and deployment.
Next.js 16 App Router, Neon Postgres and Drizzle. The application is split into two parts: the public site and the panel under `/admin`. Product detail and category pages are generated at build time through `generateStaticParams`, and the parameter list is read from the database — the static output derives from the panel's content. In that design a visitor request never reaches the database; only the contact form writes to the server. The panel side is fully dynamic: product and variant definitions, customer records, sales and sale items, incoming requests. Images are kept in object storage and their dimensions are read at upload time and written to the record, so image ratios are known while the page is rendered. The session is carried in a signed token. The launch switch is a single environment variable: when it is on, the application reduces to one screen while the rest of the structure stays in place.
The reasoning is commercial rather than technical: in this business an order is taken with measurements and a conversation, not with a product on a shelf. Adding a cart would have loaded stock synchronisation, payment integration, a returns process and price transparency onto a flow that contributes nothing to the sale. The narrower scope simplified the architecture directly: no session on the visitor side, no cart state, no stock locking.
Trade-offThe site is not a sales channel on its own; conversion is measurable only up to the click, and everything after it happens inside a WhatsApp conversation with no trace in the system. Whether a request became an order cannot be read from the panel and is handled by hand.
The catalogue changes rarely and visitor traffic is entirely reads. Generating pages at build time removes the runtime database connection: a visitor request never goes to the database, so connection pooling, cold starts and query latency are not visitor-side problems.
Trade-offCatalogue freshness is tied to deployment: a typo fixed in the panel does not reach the storefront until a new build. That is acceptable because the catalogue changes rarely; for a catalogue that changes several times a day the same decision would be wrong.
The domain should not sit empty while the content was unfinished. Instead of standing up a separate "coming soon" site, a switch was put inside the application: when it is on, the site reduces to one screen — brand, a short description, direct contact — and when it is off the full site opens. There is no second project to maintain.
Trade-offThe application carries two different appearances, and one of them is never opened during day-to-day development; a broken coming-soon mode would only be noticed when it is turned on. Because the switch is a client-visible variable, it is not a security boundary either — only a display switch.
For a lighting product the image does more work than the text, and the page is mostly image. Because dimension information lives on the record, the ratio is known while rendering and nothing shifts before the image loads. The alternative — measuring the image on every page build — would have grown build time in direct proportion to the number of images.
Trade-offThe dimension relies on a single measurement at upload; if an image in object storage is replaced externally, the stored dimension stays silently wrong. There is no job that re-measures.
Drizzle and Neon keep the relational model the panel needs — product, customer, sale, sale item — readable alongside its migration history. Object storage keeps images out of both the repository and the database without inflating the build output. For sessions, a signed token was enough instead of a full authentication framework: the panel has one class of user and needs none of registration, password reset or provider sign-in — there is no point carrying a dependency that will not be used. Turnstile protects the contact form because it is the single conversion point. Zod works both on form input and on environment-variable validation.
The panel is protected by a signed session token and the panel routes are entirely separate from the site. The contact form sits behind Turnstile and verification happens on the server — a verification result coming from the client is not accepted on its own. The write path is Server Action → Zod → Drizzle; there is no other write path on the visitor side. Customer and sales records are visible only inside the panel; no public page reads those tables and static generation does not touch them at all. Environment variables are validated with Zod.
There is no stored, date-stamped Lighthouse output for this project, so no page-speed score is written here. The structural measure is this: there is no runtime database query on the visitor side, pages are served as build output. Because image ratios are read from the record, no layout shift occurs during loading. Bundle analysis is available as a ready command in the repository.
There are no automated tests in this repository, and the gap is felt most in the link between static generation and the panel: nothing verifies whether a product unpublished in the panel is still on the site until the next build. Today I would at least make the function returning the generation parameters pure and test it — exactly what I did with the availability calculation in Ustura, where a pure function made edge cases testable in a single call. The second point is scope: I stand behind leaving cart and payment out, but whether a request turned into an order is invisible in the system. Today I would add a simple status field to the request; that is not building a CRM, it is recording the outcome of the conversation — the booking status history in VAP Turizm does exactly this job.
Tell me what you want to build; I'll tell you up front how long it takes and where to start.