Zwerfkei Website — Front-End Code Conventions
This document describes the front-end architecture and coding conventions of the Zwerfkei Website project, for handover purposes. It covers the Blade templating layer and the Vue/TypeScript layer, and how the two interact.
Naming policy at a glance: Vue → PascalCase (files and folders, capitals allowed). Blade → all-lowercase kebab-case (files and folders, no capitals). Keeping the two visually distinct makes it obvious at a glance which layer a given file belongs to. Details in §4.2 and §5.
1. Stack overview
- Templating: Laravel Blade (server-rendered pages and components)
- Interactive components: Vue 3 (
<script setup lang="ts">), auto-registered as global custom elements usable directly from Blade - Language: TypeScript, strict mode
- State: Pinia (Option Store syntax)
- Styling: Tailwind CSS v4 (CSS-first config via
@theme), plusfluid-tailwindcssfor viewport-based fluid scaling - Animation: GSAP (with ScrollTrigger) for scroll/parallax effects
- Build: Vite 7, Yarn package manager
- HTTP: Axios, wrapped in a small service layer (Saloon is used on the Laravel/PHP side, not in the front-end)
- i18n: vue-i18n, JSON translation files per locale
- Icons: raw SVGs, auto-registered as global components via
vite-svg-loader
Local dev runs via Docker/Traefik at https://website.zwerfkei.localhost. Frontend-only work (yarn dev, yarn build) runs on the host.
Getting started:
- Repo: https://github.com/zwerfkei-flooris/zwerfkei-website
- Current website: https://zwerfkei.nl/
- Key routes to know:
/brandguideand/playground(see §9) — these are where front-end work happens day to day.
New dependencies: don't silently add a new tool or library outside the current stack. If something's missing, check first — there may already be a project reason for the current choice, or it's worth deciding on together.
2. Figma fidelity & responsiveness
- Building 1:1 from Figma is non-negotiable. Spacing, padding, colors, typography — everything should match the design exactly. This is one of the most heavily checked aspects of a PR.
- Figma is also where you check for reusability: before building something, check whether the same element appears elsewhere in Figma (or plausibly could later). That's the signal for whether something should become a shared component rather than a one-off (see also §5's componentization criteria).
- Responsiveness is tested across a wide range of sizes, not just the breakpoints Figma happens to define: ultrawide (watch for content that should break out of the
container), large desktop, laptop, down to mobile. Don't just check the handful of sizes Figma provides — actually drag your browser window smaller and larger and watch every size in between. Every size should look intentional, not just "not broken." Find the point where something stops looking right, and add a breakpoint for that specific element if needed — it's fine to add breakpoints beyond what Figma defines. - For type sizes, padding, and similar values, prefer the fluid
fl-utilities (§7) over hard breakpoint jumps. When implementing a component, compare the desktop and mobile Figma frames and check that the fluid scaling looks right in between as well, not just at the two extremes.
3. Directory structure
Components are organised by domain / feature, not by an abstraction tier. There is no Atom/Molecule/Organism split — a folder name says what area of the site a component belongs to.
resources/
js/
Components/ Vue single-file components (see §4)
app.ts App bootstrap: component auto-registration, i18n, Pinia
svg/ Icon source files, auto-registered as <icon-*> components
css/
app.css Tailwind entry point + design tokens (@theme) + custom utilities
data/ JSON fixtures / demo data (products, brands, categories, compare-products).
Stand-in for a CMS/DB; read server-side via resource_path('data/<name>.json').
views/
layout/ Page shell: app.blade.php (the <html> wrapper), header, footer
templates/ Route-facing pages, one folder per page type
pdp/
show.blade.php (add index.blade.php etc. per page type when needed)
components/ Blade components, organised by domain / feature (see §5)
global/ Cross-cutting, non-domain components
breadcrumbs.blade.php
category/ category card + slider
custom/ genuinely one-off, site-specific pieces (hero-homepage-parallax)
brands/ letter-group, list, logo-card, slider
pdp/
sections/ the large chunks of the PDP (top-section, info-tabs, product-reviews, product-slider)
components/ sub-parts used only inside those sections (top-section-usp)
docs/
brandguide.blade.php / brandguide/ Living design-token reference (see §9)
playground.blade.php / playground/ Shared demo/experiment area (see §9)
lang/ vue-i18n translation JSON files, one per locale
4. Vue components (resources/js/Components)
4.1 Auto-registration
Every .vue file under resources/js/Components/** is registered as a global component in app.ts via import.meta.glob. The component's global tag name is derived automatically from its file path:
Components/Product/CompareButton.vue → <product-compare-button>
Components/Product/Card/Default.vue → <product-card-default>
Components/Ui/SliderContainerFull.vue → <ui-slider-container-full>
Components/Global/HeroHomepageParallax.vue → <global-hero-homepage-parallax>
The name is built by stripping Components/ and the extension, then converting the remaining path to PascalCase (via lodash camelCase + upperFirst, which folds an all-caps segment like UI into Ui) before Vue kebab-cases it for template usage. Practical implications:
- There is no manual import/registration step — dropping a
.vuefile in the right folder is enough to use it anywhere, including from Blade. - The folder path becomes part of the tag name, so folder names matter and every nesting level shows up as an extra prefix. Keep folders one level deep and named by domain/feature (
Product/,Cart/,Pdp/,Compare/,Brands/,Ui/,Global/);Product/Card/is the one deliberate second level (the three card variants).
4.1a Which folder
| Folder | Holds | Knows about |
|---|---|---|
Product/, Cart/ |
Domain components | one domain (a product, the cart) |
Pdp/, Compare/, Brands/ |
Feature components | one page / feature |
Ui/ |
Generic primitives — Tabs, Popover, SliderContainerFull, HoverIconButton, ParallaxBg |
nothing; pure mechanics + slots (see §4.5) |
Global/ |
Cross-cutting, site-wide pieces that aren't a bare primitive (HeroHomepageParallax) |
the site, not a domain |
ProductCompareDebug.vue sits at the Components/ root — a dev/debug component, see §10.
4.2 File naming
PascalCase file names (e.g. CompareButton.vue, HeroHomepageParallax.vue), in line with the official Vue style guide. All components in the codebase follow this.
4.3 Component internals
- Always
<script setup lang="ts">. - Named slots are the standard way to pass templated content from Blade into a Vue component (
<template #content>,<template #slides>, etc. — seeglobal/custom/hero-homepage-parallax.blade.phpandpdp/sections/product-slider.blade.php). - Compound components are a single file.
Ui/Tabs.vuerenders<ui-tabs>and reads its default slot withuseSlots(), picking up each child<tab>element's#label/#contentsub-slots to build a structured tab list. The child<tab>is just a marker — there is noTab.vue. This pattern is for when a parent needs to know about its slotted children's sub-slots. vue/multi-word-component-namesis disabled in ESLint, so single-word component file names are allowed, but prefer descriptive names — the file name plus its folder is what a reader sees as the tag.
4.4 Third-party UI primitives
Headless/unstyled primitives come from reka-ui (TabsRoot, TabsList, etc.), styled with Tailwind utility classes directly in the template. Don't introduce a second headless-UI library without discussing it first (see the dependency note in §1).
4.5 Reference example: what a reusable component should look like
Components/Ui/SliderContainerFull.vue is the model to follow for structural/reusable components: it's a generic slider (drag, touch, snap, prev/next, progress bar) that takes itemWidth and gap as props and receives its content through a slides slot. It has zero knowledge of what it's displaying — that's the pattern, and it's why it lives in Ui/. If a design element repeats across the site with different content, build it once this way rather than copy-pasting a variant per page.
5. Blade components (resources/views/components)
Blade components are organised by domain / feature, mirroring the Vue side (§3, §4.1a) — no atomic-design tier:
components/
global/ breadcrumbs.blade.php, category/{card,slider}.blade.php, custom/hero-homepage-parallax.blade.php
brands/ letter-group.blade.php, list.blade.php, logo-card.blade.php, slider.blade.php
pdp/
sections/ top-section.blade.php, info-tabs.blade.php, product-reviews.blade.php, product-slider.blade.php
components/ top-section-usp.blade.php (only used inside a pdp section)
The Blade side has a single global/ bucket for anything non-domain (the Vue side splits Ui/ primitives from Global/) — so breadcrumbs, a primitive, lives in Blade components/global/.
Page sections live in components/<page>/sections/ and are rendered from the page's template with <x-<page>.sections.<name> />. templates/pdp/show.blade.php is just the ordered list of <x-pdp.sections.*> calls.
When does something become a component, instead of staying one-off markup? Not everything needs breaking down — a one-off heading doesn't need its own component. Something should become a reusable component when it: appears in multiple places, is dynamic, and shares the same underlying data across those places (e.g. color swatches and price on a product card). Check Figma for whether the same element shows up elsewhere (§2).
- Used from pages/sections as
<x-sections.breadcrumbs :crumbs="..." />,<x-brands.logo-card :brand="$brand" />,<x-pdp.sections.top-section />, matching Laravel's default folder-based component auto-discovery — no manual namespace registration needed. - Each component declares its accepted data via
@props([...])at the top, with default values. - Use
{{ $attributes->merge(['class' => ...]) }}to allow the parent to extend/override classes. - File & folder naming convention: all-lowercase kebab-case, no capitals — anywhere under
components/,layout/,templates/. This is the deliberate opposite of the Vue convention (§4.2): Vue →PascalCase, Blade → lowercasekebab-case. Never mix capitals into a Blade path. - Case-sensitivity warning: unlike the Vue side (§4.1), Blade does not normalize component tag casing —
<x-pdp.sections.top-section>must matchpdp/sections/top-section.blade.phpcharacter-for-character. macOS's default filesystem (APFS) is case-insensitive, so a mismatched rename can look fine locally (git statusshows no diff) and only break once checked out on a case-sensitive filesystem (Linux, e.g. Docker/CI/production). When renaming a Blade file or folder, always grep for every<x-...>tag and@include/@extendscall referencing it and update them in the same change.
5.1 Blade ↔ Vue interaction
Interactive pieces (sliders, parallax, compare/wishlist buttons) are plain Vue custom elements dropped straight into Blade templates, passing content via named slots:
<global-hero-homepage-parallax class="hidden lg:grid lg:h-[730px]">
<template #content>
...
</template>
</global-hero-homepage-parallax>
<ui-slider-container-full>
<template #slides>
@foreach ($products as $product)
<product-card-default :product='@json($product)'></product-card-default>
@endforeach
</template>
</ui-slider-container-full>
(Product cards are themselves Vue components here — <product-card-default> — because they carry client-side behaviour like swatch → image swapping.)
Rule of thumb: static markup and server-side data (products, pricing, translations) stay in Blade/PHP; only genuinely interactive behaviour (dragging, scroll-driven animation, client-side state like the compare list) becomes a Vue component.
6. TypeScript conventions
strict: true; no implicitany.- Import alias
@→resources/js; never use relative../../chains across top-level folders. - No file extensions in imports (
.ts,.js,.json,.vueare all omitted).
7. Styling (Tailwind v4)
- Tailwind only. No SCSS, no inline
style="...". - Tailwind config is CSS-first: design tokens live in
resources/css/app.cssunder@theme(colors, radii, custom font sizes) rather than atailwind.config.js. - Colors follow the design (see
@themeinresources/css/app.css) and generally shouldn't need new ones added — if a design calls for a color that isn't in there, check with design first before introducing a one-off value. - Custom heading utilities
text-h1…text-h10provide fluidfont-size/line-heightpairs, generated from the styleguide. They're available to use, but they don't always match the Figma design exactly — check how a text actually looks in both desktop and responsive in Figma, and base the markup on that rather than forcing it onto the closesttext-h*token if the design says otherwise. button-large/button-medium/button-small/button-iconutilities were put together to show what's possible for button styling, not as the definitive button system — it's likely worth eventually replacing these with the actual buttons used elsewhere on the live website instead.fluid-tailwindcssplugin adds thefl-prefix for viewport-fluid utilities, e.g.fl-w-7/10,fl-px-2.5/5,fl-text-2xs/base(fluid between the configured 600–1300px viewport range). Usefl-utilities for anything that should scale smoothly across breakpoints instead of hand-rolling multiplesm:/lg:overrides — see §2 for how to check this against Figma.containeris a custom utility (max-width 1350px, centered,1reminline padding) — use it instead ofmax-w-*+mx-autocombinations for page sections.
8. Icons
The same resources/svg/** folder feeds two independent registration mechanisms, one per layer:
- Vue:
resources/js/plugins/icons.tsauto-registers every SVG as a global component namedicon-<path-with-dashes>(folder separators become dashes) — drop an SVG inresources/svg/and use<icon-your-name />in any Vue template, no import needed. - Blade: the
blade-ui-kit/blade-iconspackage (config/blade-icons.php) is configured with the sameresources/svgpath and aniconprefix, exposing<x-icon-name />(see<x-icon-arrow-right />inpdp/sections/product-slider.blade.php).
Both point at the same source folder and use the same icon-/icon prefix, so in practice an SVG added once is usable from both Blade and Vue — but they are two separate packages/configs, so if you rename or restructure resources/svg/, update both.
9. Brandguide & Playground workflow
Front-end work happens primarily out of two routes:
/brandguide(resources/views/docs/brandguide.blade.php+docs/brandguide/) — the living reference for design tokens: colors, type, spacing, radii. Check here first, not just inapp.css, when in doubt about a token./playground— where components get built and demoed in isolation before/alongside being wired into real pages.resources/views/docs/playground.blade.phpis the shell (sidebar + content); each topic is its own file underresources/views/docs/playground/(product.blade.php,cart.blade.php,ui-components.blade.php,custom.blade.php,brands.blade.php,tryouts.blade.php, ...) served via the/playground/{topic}route. Within a topic page, group by what a thing is (e.g. "Losse elementen", "Productkaarten", "PDP-secties") — not by an atomic tier.
When trying out a new component or experiment, add your own topic file under docs/playground/ (and a sidebar link in docs/partials/sidebar.blade.php) rather than editing an existing one, so simultaneous work doesn't cause merge conflicts.
Definition of "done" for a component landing in the playground: a back-ender should be able to pick it up and use it without friction — no guessing about props, no missing states. Functional/data-driven behavior should already work too, not just the visuals (e.g. hovering a color swatch on a product card swaps the product image; on a PDP, clicking a swatch updates the gallery).
10. Known inconsistencies (flagged for follow-up, not blockers)
ProductCompareDebug.vueis a debug/dev component committed at theComponents/root (not under a domain folder) — confirm with the team whether it should be removed before/at launch or gated behind an env flag.- Vue
Ui/vsGlobal/: the Vue side splits generic primitives (Ui/) from other cross-cutting pieces (Global/); the Blade side has onlycomponents/global/. Sobreadcrumbs— a primitive — lives in Bladecomponents/global/, not acomponents/ui/. Fine for now; unify later if a second Blade primitive appears. components/global/custom/andcomponents/pdp/components/are extra nesting levels kept deliberately (site-specific one-offs; sub-parts of a section). They can be flattened if they stay small.components/plp/is an empty folder reserved for the product-list page.- Git tracks the Vue folder as
UI/(uppercase); the auto-registration folds it to<ui-*>tags (§4.1). macOS's case-insensitive filesystem may show it asUi/locally — git is the source of truth. - The PDP is served from
/static/pdp(routes/web.php) as a placeholder route; wiring to real data/routing is still open (see theTODOthere).
11. Git & review process
- Working/main branch:
development— feature branches are created offdevelopment. - Branch naming:
feature/ZWE-[ticket]-[description] - Commit title:
ZWE-[ticket] - [description] - Commit body: grouped under Added / Changed / Deprecated / Removed / Fixed / Security
- PRs are written using the Keep a Changelog format (grouped under the same categories) to keep them clear and easy to scan.
- Team members review each other's PRs first, to encourage collaboration and catch things early — but Mike is always the last reviewer before merge, keeping final control over what lands in
development. - If you're blocked or unsure about something, raise it proactively rather than guessing and waiting for review feedback.