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
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
views/
components/ Blade components, organised by domain + atomic-design tier (see §5)
product/
atoms/ Smallest reusable pieces (badge, price, stock-status, colors)
molecules/ Combinations of atoms (card, pdp-title-bar)
organism/ Larger sections built from molecules/atoms (product-slider, pdp-main-section)
custom/
organism/ One-off, non-domain organisms (hero-homepage-parallax, ...)
brandguide.blade.php Living design-token reference (see §9)
playground.blade.php / playground/ Shared demo/experiment area (see §9)
layout/, partials/ Page shell (header, footer, layout)
*.blade.php Pages
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/UI/SliderContainerFull.vue → <ui-slider-container-full>
Components/Custom/HeroHomepageParallax.vue → <custom-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 correctly 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 nesting shows up as extra prefixes (
Components/UI/Tabs/Component.vue→<ui-tabs-component>). Keep folders shallow and meaningful (Product/,UI/,Custom/).
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. — seehero-homepage-parallax.blade.phpandproduct-slider.blade.php). - Compound components (e.g.
Tabs) usedefineSlotson the child (Tab.vue) and inspectuseSlots()/vnode types on the parent (Component.vue) to build a structured list — this pattern is used when a component 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 multi-segment names combined with the folder for clarity.
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. 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 then atomic-design tier:
components/
product/
atoms/ badge.blade.php, price.blade.php, stock-status.blade.php, colors.blade.php
molecules/ card.blade.php, pdp-title-bar.blade.php
organism/ product-slider.blade.php, pdp-main-section.blade.php
custom/
organism/ hero-homepage-parallax.blade.php, hero-vibe-code-parallax.blade.php
When does something become a component (atom/molecule/organism), 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). Atomic Design is used deliberately here because it forces that reusability question by default.
- Used from pages as
<x-product.atoms.badge :label="..." />,<x-product.molecules.card :product="$product" />, 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 (seebadge.blade.php). - File & folder naming convention: all-lowercase kebab-case, no capitals — anywhere under
components/. This includes the domain/tier folders (product,atoms,molecules,organism,custom), not just the file name. 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-product.atoms.stock-status>must matchatoms/stock-status.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:
<custom-hero-homepage-parallax class="hidden lg:grid lg:h-[730px]">
<template #content>
...
</template>
</custom-hero-homepage-parallax>
<ui-slider-container-full>
<template #slides>
@foreach ($products as $product)
<x-product.molecules.card :product="$product" />
@endforeach
</template>
</ui-slider-container-full>
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 />inproduct-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/brandguide.blade.php) — 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/playground.blade.phpis a shared demo/experiment page, rendered as a set of tabs (viaui-tabs-component/ui-tabs-tab). Each tab's content lives in its own file underresources/views/playground/(product.blade.php,cart.blade.php,parallax.blade.php,container.blade.php,reviews.blade.php, ...) and is pulled in with@include('playground.<name>').
When trying out a new component or experiment, add your own file under playground/ and wire it into a new tab in playground.blade.php, rather than editing an existing tab's file. This keeps everyone's work-in-progress in separate files so simultaneous work on the playground 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 top level (not under a domain folder) — confirm with the team whether it should be removed before/at launch or gated behind an env flag.
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.