Skip to content

09 — Coding conventions

This is a short doc on house style — what the codebase actually does, not what an idealised Vue 2 project might do. Following these conventions keeps PRs predictable and reduces review churn.

Vue 2 patterns

This project is on Vue 2.7.16 — the last 2.x release. It supports the Composition API (via defineComponent / <script setup> is not available because the build doesn't include the relevant compiler flags), but the codebase is overwhelmingly Options API. Stick with Options API for new components unless you have a specific reason not to.

A typical page component:

<template>
  <div></div>
</template>

<script>
export default {
  name: 'MyPage',
  middleware: 'auth',                       // optional — re-runs middleware/redirect.js on direct loads
  layout: 'default',                        // omit for the default layout
  data() { return { loading: false, items: [] } },
  async mounted() {
    this.items = await this.$store.dispatch('api/fetchSettings')
  },
  computed: {
    accountId() { return this.$auth.user?.accountId }
  },
  methods: {
    async refresh() {
      this.items = await this.$store.dispatch('api/fetchSettings', { force: true })
    }
  }
}
</script>

<style scoped>

</style>

Things to know:

  • Template syntax: BootstrapVue is in heavy use (<b-row>, <b-col>, <b-spinner>, <b-modal>, etc.). Mix freely with HTML elements.
  • @click.once is used liberally on form-submit buttons to prevent double-submits. This is a project convention; use it for any button that initiates an irreversible action.
  • bootstrap-vue is registered with { css: false } — its styles are not auto-injected. CSS comes from assets/css/style.css, assets/css/appstyle.css, and the CDN Bootstrap 5 stylesheet loaded in nuxt.config.js → head.link.
  • <style scoped> is preferred for component-local styles. Global styles go to assets/css/ or assets/scss/.
  • components: true in nuxt.config.js means components in components/ are auto-imported. Don't write import Foo from '@/components/Foo.vue' for components in that tree — just use <Foo />.

Filenames and folders

  • Components: PascalCase .vue files (e.g. AccountSwitcher.vue, ContentItem.vue). Multi-word is preferred (Vue style guide) but the existing tree is mixed (accountSearch.vue, approvalModal.vue). Don't rename existing components for the sake of style — too many imports break.
  • Pages: lowercase, sometimes camelCase (contentplanner, userSetting, Organisationprofile). The path becomes the URL, so URL-readable lowercase wins for new pages. Existing camelCase paths are grandfathered — Organisationprofile is intentional.
  • Component subdirectories group related components (Calender/, Charts/, Dashboard/, Buttons/, Cards/, Sidebars/, …). Place new components in the matching subdirectory rather than at the top level of components/.

Imports and aliases

The build supports the standard Nuxt aliases:

  • @/ and ~/ → repo root.
  • ~assets/ and @/assets/ → the assets/ directory.

For the polotno bundle, the import is literal relative path (../../../../polotno-bundle) — not @/polotno-bundle. This is because the bundle is at the repo root and resolved as a regular ES module by webpack. Don't change the import style; existing pages share the convention.

Plugin registration patterns

Look at nuxt.config.js → plugins for the existing patterns:

plugins: [
  '@/plugins/empire-admin',          // both server and client
  '@/plugins/vue-bar',
  '@/plugins/vue-moment',
  '@/plugins/gtm.js',
  { src: './plugins/vue-snotify', ssr: false },
  { src: './plugins/snowfall', ssr: false },
  { src: './plugins/auth.js', ssr: false },
  { src: './plugins/konva', ssr: false },
  { src: './plugins/plyr.client.js', ssr: false },
  { src: './plugins/clarity.js', mode: 'client' },
  { src: './plugins/flagmeister.client.js', ssr: false },
]

Conventions:

  • ssr: false and mode: 'client' are equivalent here (the app has ssr: false globally). The codebase uses both forms; either is fine.
  • Plugins that use browser-only globals (window, document, localStorage) must be client-only — Nuxt still evaluates plugins in node during build. Without the flag the build fails.
  • Plugins suffixed .client.js are conventionally client-only by Nuxt's automatic suffix detection. The repo declares them explicitly and uses the suffix; redundant but harmless.

Auth opt-in / opt-out

Pages are auth-required by default (the @nuxtjs/auth strategy is enabled globally in nuxt.config.js). To make a page accessible to logged-out users, set auth: false in the page component's script:

export default {
  auth: false,
  // …
}

This is the only canonical way to expose a public page. Do not work around it via routing tricks.

Toast notifications

The codebase uses two Vue toast libraries:

  • vue-snotify — the older one, accessed as this.$snotify.success(...)/error(...)/warning(...). Registered as plugins/vue-snotify.js.
  • vue-toastification — the newer one, accessed as this.$toast.success(...)/error(...). Configured via helpers/helper.js → TOAST_OPTIONS and TOAST_LIB_OPTIONS.

For new code, prefer vue-toastification with the shared options:

import { TOAST_OPTIONS } from '@/helpers/helper'
this.$toast.success('Saved!', TOAST_OPTIONS)

(The Polotno editor uses react-toastify — that's a separate, isolated concern. Don't try to bridge the two.)

Post-status helpers

Anywhere you display a post's status to the user, route the value through helpers/helper.js → renderPostStatus(pData) (label) or calStatus(item) (calendar status code). They encode the partial-publish edge cases and the approval gate. See 13-post-lifecycle.md for the full state machine.

Mixins

mixins/onboardingNavigation.js and helpers/mixins/ are the only mixin locations. The codebase uses mixins sparingly — most cross-component logic is duplicated rather than extracted. Don't introduce a new mixin without a clear pattern of three or more components needing the same behaviour.

State management conventions

See 05-state-management.md for the full picture. The short version:

  • Prefer cached api/fetch* actions over direct $axios calls.
  • Prefer dispatching actions over committing mutations from components.
  • Don't read from store.state.user — use $auth.user.
  • Always invalidate the matching cache after a mutation.

Async patterns

  • async/await is preferred over .then() chains for new code.
  • Existing code uses both interchangeably; don't refactor to async/await for stylistic reasons alone.
  • Errors are typically swallowed. The pattern in store/api.js is:
try {
  const { data } = await this.$axios.get(...)
  commit('setFooCache', data)
  return data
} catch (error) {
  return state.fooCache    // silently return cached data on failure
}

This is intentional for cache-fallback behaviour, but it means failed requests are invisible without devtools open. When debugging "the data isn't showing," check the Network tab before assuming the action ran successfully. - The postData/getData helper functions in store/index.js follow a similar swallow pattern, returning { status: 400, data: null } on any error. Several actions in the same file then check if (status && status === 200) — be careful when adding new ones to follow the convention rather than throwing.

  • Read identity from cookies via app.$cookies.get(...) (server-side or middleware) or this.$cookies.get(...) (in components).
  • localStorage is fine to use directly (no SSR concerns, since ssr: false). Be explicit about cleanup — localStorage.clear() is called in several auth flows.
  • Don't add a new identity-shaped cookie without a strong reason. The cookie surface is already large (see 03-auth-and-sessions.md). Prefer extending the usedetail cookie object via the backend.

Comments and documentation

Look at the codebase to see how it's done — the codebase is sparsely commented. Prefer self-explanatory names over comments. When a comment is needed, it usually documents why, not what. Do not add JSDoc-style block comments to every function; that's not the house style.

// CLEAN and similar markers appear in some files (api/index.js, nuxt.config.js → serverMiddleware). They appear to mark sections being prepared for cleanup. Verify with the team whether to remove them when working in those areas.

What not to do

A non-exhaustive list of things that would surprise a reviewer:

  • Don't import React in the Nuxt app. React only lives in polotno-editor/. The Nuxt app talks to the editor through the bundle's createEditor / window.store surface.
  • Don't add SSR-aware code. process.server will always be false in this app at runtime. Code that branches on it is confusing.
  • Don't deduplicate <script> tags in nuxt.config.js. Some libraries (Bootstrap, jQuery, Popper) are loaded multiple times intentionally. Removing duplicates can break specific UI surfaces. If something looks redundant, ask first.
  • Don't introduce a new global state library (Pinia, Composition API ref-stores, etc.). Vuex 3 is the convention, even if it's awkward.
  • Don't switch to npm. Yarn 1 is the project manager. Mixing produces lockfile drift.
  • Don't add TypeScript files. The codebase is JS only. There's a @types/sortablejs dep but no tsconfig.json in either project — TypeScript is partially declared but never adopted, and adding it now is a project-scale decision.

When in doubt

Find a similar existing file and copy its shape. The codebase is internally consistent in its quirks; matching them is more important than following a Vue style guide written elsewhere.