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.onceis 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-vueis registered with{ css: false }— its styles are not auto-injected. CSS comes fromassets/css/style.css,assets/css/appstyle.css, and the CDN Bootstrap 5 stylesheet loaded innuxt.config.js → head.link.<style scoped>is preferred for component-local styles. Global styles go toassets/css/orassets/scss/.components: trueinnuxt.config.jsmeans components incomponents/are auto-imported. Don't writeimport Foo from '@/components/Foo.vue'for components in that tree — just use<Foo />.
Filenames and folders¶
- Components: PascalCase
.vuefiles (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 —Organisationprofileis 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 ofcomponents/.
Imports and aliases¶
The build supports the standard Nuxt aliases:
@/and~/→ repo root.~assets/and@/assets/→ theassets/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: falseandmode: 'client'are equivalent here (the app hasssr: falseglobally). 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.jsare 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:
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 asplugins/vue-snotify.js. - vue-toastification — the newer one, accessed as
this.$toast.success(...)/error(...). Configured viahelpers/helper.js → TOAST_OPTIONSandTOAST_LIB_OPTIONS.
For new code, prefer vue-toastification with the shared 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$axioscalls. - 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/awaitis 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.jsis:
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.
Cookie & localStorage usage¶
- Read identity from cookies via
app.$cookies.get(...)(server-side or middleware) orthis.$cookies.get(...)(in components). localStorageis fine to use directly (no SSR concerns, sincessr: 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 theusedetailcookie 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'screateEditor/window.storesurface. - Don't add SSR-aware code.
process.serverwill always be false in this app at runtime. Code that branches on it is confusing. - Don't deduplicate
<script>tags innuxt.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/sortablejsdep but notsconfig.jsonin 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.