Personal Project
PlainlySmart
Overview
PlainlySmart started as a simple marketing website for a custom software business, but I deliberately treated it as a production application rather than a collection of static pages. The goal was to keep the frontend completely statically deployable while still supporting localization, theme persistence, interactive consultation flows, campaign attribution and secure communication with an external backend. The result is a lightweight Next.js site that does not require a Node.js server at runtime, while still behaving like a modern application in the browser.
My Role
- I designed and built the site end to end, from the visual structure and responsive components to localization, browser-side state, accessibility and the contact workflow. I used Next.js App Router with TypeScript and Tailwind CSS 4, then added Motion selectively to make the interface feel more dynamic without turning animation into a dependency for usability.
- As the project evolved, I moved the contact flow away from simple email links into a reusable consultation dialog. React Hook Form now owns form state, Zod owns validation and normalization, and next-intl provides localized validation messages, keeping each responsibility explicit instead of accumulating form logic in one component.
- I also designed the integration boundary for a separate contact API. The static frontend collects the lead, attaches locale and campaign attribution, obtains a Cloudflare Turnstile token and sends the request to an external endpoint, allowing the frontend and backend infrastructure to evolve independently.
Architecture & Technology
- The application is exported as static HTML, CSS and JavaScript, so it can be hosted without a Next.js server. Dynamic operations such as contact submission are pushed behind external APIs instead of relying on Next.js server routes.
- Localization is handled with next-intl using symmetrical /en and /cs routes. Explicit URLs take precedence over browser detection, language preferences are persisted client-side and theme state is preserved across locale switches without introducing hydration mismatches.
- The consultation experience is shared across the page through React Context. Individual CTAs can open the same native dialog without prop drilling, while the dialog lifecycle remains separate from the form implementation.
- The contact form uses React Hook Form together with a Zod schema. Zod trims and validates submitted values, React Hook Form handles field registration, validation errors and submission state, and accessible error relationships are provided through aria-invalid and aria-describedby.
- Cloudflare Turnstile is integrated through its explicit JavaScript API. A small React wrapper loads the external script, renders the widget through window.turnstile.render, propagates verification tokens to the parent form and recreates the widget when a failed submission requires a fresh token.
- Campaign attribution is captured from standard UTM parameters and persisted in sessionStorage with an in-memory fallback. The values follow the visitor through navigation and are attached to the eventual contact request together with the current locale.
- Playwright browser tests cover the static export rather than only the development server, including localization, theme persistence, blocked storage scenarios, navigation behaviour and browser-level JavaScript errors.
Engineering Challenges
- One of the main design decisions was keeping the site truly static without giving up application-like behaviour. Instead of introducing a Next.js runtime just for one form, I separated the frontend from the future contact backend and designed the browser to communicate with a dedicated API endpoint.
- Localization introduced more complexity than translating strings. The site had to choose the correct language on the first visit, preserve explicit locale URLs, remember user preferences, survive unavailable browser storage and keep dark mode stable across full locale navigation without hydration flashes.
- The consultation form gradually accumulated validation, duplicate-submission protection, status handling and data extraction logic. Refactoring it around React Hook Form and Zod simplified the component by giving form state and validation clear owners while leaving Turnstile, tracking and API submission as application concerns.
- Integrating Turnstile required bridging React's declarative lifecycle with an imperative third-party API. The component has to wait for the external script, render into a referenced DOM element, clean up the previous widget and deliberately recreate it when a verification token becomes unusable.
- UTM tracking also needed to work defensively. Browser storage may be unavailable, stored JSON cannot be trusted and campaign values need to survive client-side navigation, so the implementation validates restored data and falls back to in-memory state instead of allowing tracking code to interfere with the actual contact flow.
Results & Impact
- The project evolved from a static company landing page into a production-oriented marketing application while retaining a fully static deployment model.
- Visitors can move between English and Czech versions, preserve their theme preferences and open the same consultation experience from multiple areas of the site without duplicating dialog logic.
- The contact workflow now has typed form state, schema-driven validation, localized errors, Turnstile verification, UTM attribution and a clean boundary for an external API rather than relying on mailto-based submission.
- The architecture leaves room for the backend to be introduced independently, for example through AWS API Gateway and Lambda, without changing the static hosting model of the frontend.
- The project also gave me a practical environment for working through issues that tend to be hidden in simpler marketing sites: hydration behaviour, accessibility, browser storage failures, third-party script lifecycles, static-export testing and client-to-server trust boundaries.