@cooperco/nuxt-layer-base
| Install | |
|---|---|
npm install @cooperco/nuxt-layer-base |
|
| Latest Version: | 2.1.0 |
| Vue: | 3 |
| License: | MIT |
| Last Updated: | Jul 21, 2026 |
| Links: | npm |
Base Layer
A foundational layer that provides essential configuration and tooling for Nuxt 4 projects.
Compatibility Date: 2025-07-15
—
About Nuxt Layers
Nuxt Layers are a way to share configuration, modules, and app/server files across multiple Nuxt projects. An app “extends” a layer, and Nuxt will merge the layer’s modules, runtime configuration, and directory contents (like app/, server/, and plugins/) with the app’s own files. This lets teams centralize common tooling and conventions while each app remains free to add its own code.
Learn more in the official Nuxt Layers documentation: https://nuxt.com/docs/guide/going-further/layers
For app developers: Using this layer
What this layer provides
- TypeScript with strict mode enabled
- ESLint via
@nuxt/eslintwith stylistic Vue template rules - Internationalization (i18n) via
@nuxtjs/i18n - Accessibility checks via
@nuxt/a11y - Nuxt DevTools enabled in development
- Optional error and event logging to Loggly (client and server), disabled by default
Install in your app
- Add the layer to your project as a dev dependency
# npm
npm i -D @cooperco/nuxt-layer-base
# pnpm
pnpm add -D @cooperco/nuxt-layer-base
# yarn
yarn add -D @cooperco/nuxt-layer-base
- Extend the layer in your app’s Nuxt config
// nuxt.config.ts (in your app)
export default defineNuxtConfig({
extends: ['@cooperco/nuxt-layer-base']
})
TypeScript strict mode and DevTools are active immediately. For ESLint, add a config in your app (see "ESLint in consumer apps").
Internationalization (i18n)
This layer integrates the Nuxt i18n module. Use it directly in your components; preferred usage is an SFC <i18n> block. You can also call useI18n() in scripts.
Basic usage in a component:
<script setup lang="ts">
const { t, locale } = useI18n()
</script>
<template>
<p>{{ t('hello') }}</p>
<small>Current locale: {{ locale }}</small>
<!-- Provide your own translation files in your app (e.g., locales/en.json) -->
</template>
Preferred: single‑file component local messages using an
<script setup lang="ts">
const { t } = useI18n()
</script>
<template>
<h1>{{ t('welcome') }}</h1>
<p>{{ t('cta') }}</p>
</template>
<i18n lang="json5">
{
en: {
welcome: 'Welcome',
cta: 'Click to continue'
},
fr: {
welcome: 'Bienvenue',
cta: 'Cliquez pour continuer'
}
}
</i18n>
Both global files (e.g., locales/en.json) and per‑component <i18n> blocks are supported.
If an app uses a custom app/router.options.ts route table instead of Nuxt's
file-based page routes, disable i18n route rewriting unless the app explicitly
maps every localized route. Newer @nuxtjs/i18n versions derive route resources
from page files by default, which can conflict with custom router paths:
export default defineNuxtConfig({
extends: ['@cooperco/nuxt-layer-base'],
i18n: {
strategy: 'no_prefix'
}
})
Accessibility (@nuxt/a11y)
This layer includes @nuxt/a11y, which integrates accessibility checks and hints directly into your development workflow. It helps you identify and fix accessibility issues as you build your application.
No additional configuration is required to use the basic features of @nuxt/a11y once the layer is extended.
Logging (optional, Loggly‑backed)
When enabled via environment variables, the base layer will:
- Capture server-side errors (Nitro request lifecycle)
- Capture client-side errors (Nuxt app errors, Vue component errors, global
windowerrors, unhandled rejections) - Provide a
useLogger()composable for custom logs (and a deprecateduseLoggly()wrapper) - Proxy all logs through the canonical route
/api/log, where payloads are normalized and sensitive fields are scrubbed before forwarding to Loggly
Enable in your app with environment variables:
NUXT_PUBLIC_LOGGLY_ENABLED=trueNUXT_LOGGLY_TOKEN=<your Loggly customer token>(server-only)NUXT_PUBLIC_LOGGLY_TAGS=app-name,env(optional, comma-separated)NUXT_PUBLIC_LOG_LEVEL=debug(optional; minimum severity sent to Loggly — see note below)NUXT_PUBLIC_LOGGLY_ENV=prod(optional; environment tag —local|dev|staging|sandbox|qa|stage|prod; falls back to aNODE_ENVguess, omitted if neither resolves)NUXT_LOGGLY_REDACT_KEYS=donorId,policyNumber(optional, server-only; comma-separated extra sensitive key names, added to the built-in redaction list)NUXT_LOGGLY_ENDPOINT=https://logs-01.loggly.com/inputs(optional override, server-only)
Notes
- The token is never exposed to the client. Browsers only post to your app’s
/api/logendpoint (with/api/logglykept as a deprecated alias). - If logging is disabled or a token is not provided, the logging code is effectively a no-op and the API returns
{ ok: false, skipped: true }. LOG_LEVEL(defaultdebug) is the minimum severity forwarded to Loggly. The default sends everything; per the logging standard, set it towarn(or higher) in idle lower environments to reduce noise. Entries below the threshold return{ ok: false, skipped: true }.- Back‑compat alias:
/api/logglyremains available but is deprecated. Prefer/api/log.
Server error severity (4xx vs 5xx)
Nitro fires its error hook for every thrown error, including deliberate createError({ statusCode: 404 }). The layer maps status to severity rather than logging all of them as errors:
| Thrown error | Level |
|---|---|
5xx, or no statusCode (an uncaught TypeError, etc.) |
error |
| 4xx (404, 401, 403, 422, …) | warn |
Any status, but unhandled/fatal |
error |
unhandled/fatal are Nitro's own markers for an error that escaped rather than being thrown on purpose, so those stay at error regardless of status.
Choosing LOG_LEVEL in production — the two problems 4xx noise causes have different answers:
- Error alerting noise is solved unconditionally by this mapping.
warnrecords don't trip error alerting at anyLOG_LEVEL, so a bot sweeping for/wp-adminand/.envno longer pages anyone regardless of how you configure the threshold. - Loggly volume/cost is the only reason to go further.
NUXT_PUBLIC_LOG_LEVEL=errordrops 4xx entirely at the threshold check.
warn is the better default for most apps. It keeps error alerting clean and keeps 404s queryable for broken-link triage. Reserve error for when Loggly volume is the binding constraint, and know what you give up: a broken internal link hit via a hard load or shared URL is server-rendered, so it logs at warn and disappears — while the same link followed by in-app client navigation raises a Nuxt app:error and still logs at error. At LOG_LEVEL=error you see half your broken links.
Two details behind the meta on server errors:
meta.statuscomes from the thrown error, not the response object. Nitro'scaptureErrorruns before its error handler writes the response status, soevent.node.res.statusCodeis still unset inside the hook — reading it there reports200for a genuine 500.meta.refererandmeta.userAgentare captured for triage: a 404 with a same-origin referer is a broken link in your own app, while an absent or external referer is almost always a scanner. These are metadata only and never influence severity — both headers are client-controlled, so letting them drive the level would let anyone forge a header to force error-level logs and re-create the flooding problem.
Tags behavior
- The canonical tag order is
[app, environment, layer, …extras]. Tags are assembled server-side: yourLOGGLY_TAGScome first, theLOGGLY_ENVenvironment tag is inserted next (when resolvable), then the per‑source/per‑call tags. Duplicates are removed, values lowercased, and the list is capped at 10. - Automatic, source‑specific tags for captured errors:
- Nuxt app‑level errors:
client,nuxt - Vue component errors:
client,vue - Global window
errorevents:client,window - Unhandled rejections:
client,promise - Server‑side errors:
server
- Nuxt app‑level errors:
Logging payload
- Logs are normalized server-side to the standard schema:
level,message,tags, optionalmeta,error({ name, message }only — no stack),data(always a JSON‑stringified string), and an ISO‑8601ts. useLogger()accepts an optional 4thdataargument (any value); it is JSON‑stringified server-side. Captured‑error stack traces are relocated intodata(kept out of Loggly's indexed fields per the standard).
Composable usage:
// inside a component or any composable
const log = useLogger()
await log.error('Checkout failed', { orderId, step: 'place-order' }, ['checkout'])
await log.warn('Slow response', { endpoint: '/api/orders', ms: 850 })
await log.info('User clicked CTA', { campaign: 'summer' }, ['marketing'])
// Optional 4th arg: arbitrary debug payload, JSON-stringified into `data`
await log.debug('State snapshot', undefined, ['debug'], { state, retryCount })
Automatically captured errors (when enabled):
- Server: request-time exceptions via a Nitro plugin (severity derived from the error's status — see "Server error severity" above)
- Client: Nuxt app errors, Vue component errors,
windowerror events, unhandled promise rejections
Security and privacy:
- The server proxy masks sensitive values (SSN, Luhn‑valid card numbers, bearer tokens, long hex/base64) across
message,meta, anddata, and redacts sensitive key names (passwords, tokens, SSN, card, medical/DOB, etc.). Apps may add domain‑specific keys viaNUXT_LOGGLY_REDACT_KEYS. Masking is a safety net, not a substitute for not logging secrets. - Logging is fire-and-forget; failures to log are swallowed to avoid breaking UX. The token is never exposed to the client.
Verify locally:
- Add env vars in your app (e.g.,
.env.local):
NUXT_PUBLIC_LOGGLY_ENABLED=true
NUXT_LOGGLY_TOKEN=...your token...
NUXT_PUBLIC_LOGGLY_TAGS=app-name,local
- Start your app and trigger a test error.
- Check the network tab for
POST /api/logand then confirm the entry in Loggly.
Troubleshooting:
- No logs? Ensure
LOGGLY_ENABLED=trueandLOGGLY_TOKENis present in the server environment. - Browser CORS or network errors? The browser never talks to Loggly directly; it only posts to
/api/log. - High volume? Consider adding sampling or batching at the proxy later if needed.
ESLint in consumer apps
This layer integrates ESLint via the @nuxt/eslint module. When you extend this layer, ESLint is available to your app; you only need to add a config file and scripts in your project.
- Create
eslint.config.mjsin your app root (copy from this layer and adjust only if necessary)
// eslint.config.mjs (in your app)
// Nuxt generates a flat config at ./.nuxt/eslint.config.mjs
// We enable stylistic rules and add a few custom rules matching the base layer.
// @ts-check
import withNuxt from './.nuxt/eslint.config.mjs'
export default withNuxt({
rules: {
/* Stylistic */
'@stylistic/comma-dangle': [
'error',
'only-multiline'
],
'@stylistic/no-tabs': [
'error',
{ allowIndentationTabs: true }
],
/* TS */
'@typescript-eslint/no-unused-vars': [
'error',
{ caughtErrorsIgnorePattern: '^_' }
],
/* Vue */
'vue/html-closing-bracket-newline': [
'error',
{ multiline: 'never', selfClosingTag: { multiline: 'never' } }
],
'vue/html-closing-bracket-spacing': [
'error',
{ selfClosingTag: 'never' }
],
'vue/html-indent': [
'error', 'tab',
{ baseIndent: 0 }
],
'vue/multi-word-component-names': ['error', {
ignores: []
}],
'vue/component-name-in-template-casing': [
'error',
'kebab-case',
{ registeredComponentsOnly: false, ignores: [] }
],
'vue/component-options-name-casing': ['error', 'kebab-case'],
'vue/component-definition-name-casing': ['error', 'kebab-case']
}
})
- Add scripts and run ESLint
{
"scripts": {
"lint": "nuxt prepare && eslint .",
"lint:fix": "nuxt prepare && eslint . --fix"
}
}
Notes
- The base layer enables stylistic mode via Nuxt (
eslint.config.stylistic: true). - Do not use Prettier alongside stylistic rules; remove Prettier configs/plugins from your app to avoid conflicts.
- Tabs are allowed for indentation (including in Vue templates). The
no-tabsrule is enabled but permits indentation tabs.
What this ESLint config enforces (in plain English)
- Base: It starts from Nuxt’s generated, project‑aware flat config (via
@nuxt/eslint), then turns on stylistic mode for consistent formatting in JS/TS/Vue files. - This layer adds a few focused rules to keep things consistent and practical:
@stylistic/comma-dangle: 'only-multiline'— Allow trailing commas only when the list spans multiple lines; disallow them on single‑line lists.- Good (multiline, trailing comma ok):
const user = { id: 1, name: 'Ada', } - Good (single‑line, no trailing comma):
const arr = [1, 2, 3]
- Good (multiline, trailing comma ok):
@stylistic/no-tabswith{ allowIndentationTabs: true }— Tabs are allowed for indentation but tabs elsewhere are flagged.@typescript-eslint/no-unused-varswith{ caughtErrorsIgnorePattern: '^_' }— Flag unused variables, but allow a try/catch parameter that starts with_when you don’t use it.- Good (explicitly ignored):
try { risky() } catch (_err) { // intentionally ignored }
- Good (explicitly ignored):
vue/html-closing-bracket-newline: { multiline: 'never', selfClosingTag: { multiline: 'never' } }— Don’t put a newline before a closing bracket, even for multi‑line attribute lists.- Bad:
<my-comp a="1" b="2" /> - Good:
<my-comp a="1" b="2" />
- Bad:
vue/html-closing-bracket-spacing: { selfClosingTag: 'never' }— No space before a self‑closing/>.- Bad:
<my-comp /> - Good:
<my-comp/>
- Bad:
vue/html-indent: ['tab', { baseIndent: 0 }]— Use tabs for indentation in Vue templates.- Example:
<template> <div> <span>Text</span> </div> </template>
- Example:
vue/multi-word-component-names— Enforce multi‑word component names (no single generic names likeHeaderconflicts). Configure ignores if needed.vue/component-name-in-template-casing: 'kebab-case'— In templates, component tags must be kebab‑case (e.g.,<my-widget/>, not<MyWidget/>).vue/component-options-name-casing: 'kebab-case'— In SFC component options, thenameshould be kebab‑case.vue/component-definition-name-casing: 'kebab-case'— For component definitions in script, enforce kebab‑case names.
TypeScript in consumer apps
Strict mode is enabled by default. No configuration is required in your app.
Optional type checking script (install vue-tsc in your app):
npm i -D vue-tsc
{
"scripts": {
"typecheck": "nuxt prepare && vue-tsc -b --noEmit"
}
}
GitHub Actions in your app (linting and type checks)
To run the same checks in your app’s GitHub repository:
- Copy workflow files into your app’s repo
- Create
.github/workflows/lint.ymlwith a job that checks out your code, sets up Node, installs dependencies, and runsnpm run lint. - (Optional) Create
.github/workflows/typecheck.ymlto runnpm run typecheckif you added the script.
Minimal examples you can adapt:
.github/workflows/lint.yml
name: Lint
on:
pull_request:
branches: ['main']
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run lint
.github/workflows/typecheck.yml
name: Typecheck
on:
pull_request:
branches: ['main']
jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run typecheck
- Configure GitHub as needed
- Enable Actions in your repository settings (if disabled).
- If your app installs private packages, set appropriate repository secrets (e.g.,
NPM_TOKEN, organization PATs) and reference them in the workflow. For public packages only, no extra secrets are required.
- Open a pull request to see the checks run.
—
Claude Code Skills
This repo includes Claude Code skills for common tasks in apps that use these layers. To use them, copy the relevant skill files from the skills/ directory into your app's .claude/skills/ directory.
Skills provided by the base layer:
| Skill | File | Description |
|---|---|---|
/setup-layers |
skills/setup-layers.md |
Bootstrap a new Nuxt app to extend CooperCo layers — generates nuxt.config.ts, installs dependencies, configures ESLint and i18n |
/add-i18n |
skills/add-i18n.md |
Add <i18n> blocks to SFCs, create/update global locale files, or scaffold a new locale |
/add-api-route |
skills/add-api-route.md |
Scaffold a Nitro server route with useLogger(), TypeScript types, and sensitive field scrubbing |
# Copy base layer skills into your app
mkdir -p .claude/skills
cp node_modules/@cooperco/nuxt-layer-base/../../skills/setup-layers.md .claude/skills/
cp node_modules/@cooperco/nuxt-layer-base/../../skills/add-i18n.md .claude/skills/
cp node_modules/@cooperco/nuxt-layer-base/../../skills/add-api-route.md .claude/skills/
Or copy them directly from the nuxt-layers repository.
—
For maintainers: Working on this layer
Repository layout and Nuxt 4 directories
- Layer root:
layers/base - Nuxt 4 app directories are used inside the layer:
app/plugins/...(e.g.,app/plugins/loggly.client.ts)app/composables/...(e.g.,app/composables/useLogger.ts; deprecated wrapper:app/composables/useLoggly.ts)
Development scripts
# From layers/base
npm install
npm run dev # start a dev server for the layer (via Nuxt)
npm run lint # ESLint (with nuxt prepare)
npm run lint:fix # ESLint --fix
npm run typecheck # vue-tsc
Linting and TypeScript
- TypeScript strict mode is enforced via
nuxt.config.ts - ESLint is provided by
@nuxt/eslintwith stylistic rules enabled and custom rules ineslint.config.mjs
Logging implementation (overview)
- Client plugin:
app/plugins/loggly.client.tshooks into Nuxt/Vue error streams and window events and posts to/api/log - Composable:
app/composables/useLogger.tsexposeserror|warn|info|debughelpers posting to/api/log(deprecated wrapperuseLoggly.tsdelegates touseLogger()) - Server plugin:
server/plugins/loggly.tshooks Nitroerrorevents, derives severity/status viaresolveErrorLevel()/resolveErrorStatus(), and posts to/api/log - API endpoint (canonical):
server/api/log.post.tsscrubs/normalizes payloads and forwards to Loggly usingLOGGLY_TOKEN - API endpoint (alias, deprecated):
server/api/loggly.post.tsre-exports the canonical handler so/api/logglycontinues to work - Runtime config (see
nuxt.config.ts):- Server-only:
logglyToken,logglyEndpoint - Public:
logglyEnabled,logglyTags,logLevel - Supports both
LOGGLY_*andNUXT_PUBLIC_LOG_*env vars where appropriate
- Server-only:
Environment files
layers/base/.envis only for developing the base layer directly (not used by consuming apps)- For the repo playground, use
playground/.env - For downstream apps, use
.envor.env.localin the app’s root
Playground
There is a simple playground app at playground that extends the base layer and exposes UI to trigger logging. This is useful for end‑to‑end verification during development.
Run from repo root:
cd playground
npm run dev
Publishing to npm (tag-based)
This package is published via GitHub Actions when you push a tag that matches base-vX.Y.Z.
High-level flow:
- Bump the version in
layers/base/package.json(SemVer) - Commit and push to
main - Create and push a tag
base-vX.Y.Zmatching the version - CI checks whether the version exists on npm and publishes if not
Important notes:
- Do NOT rely on
npm versionto create the tag (it createsvX.Y.Z). Createbase-vX.Y.Zyourself. publishConfig.accessis set topublic; publishes are public to npmjs.- The workflow skips if the exact version already exists.
Step-by-step
- Bump version without creating a tag:
cd layers/base
npm version patch --no-git-tag-version # or minor | major
- Commit and push:
git add layers/base/package.json
git commit -m "chore(base): bump version"
git push origin main
- Tag and push:
cd layers/base
VERSION=$(node -p "require('./package.json').version")
cd ../..
git tag "base-v$VERSION"
git push origin "base-v$VERSION"
- CI will publish
- Workflow:
.github/workflows/publish-base.yml - Auth:
NPM_TOKENGitHub secret
Troubleshooting
- Version exists: bump again and retag
- 401/403: ensure
NPM_TOKENis configured and has access
Related Packages
MCP server for the Cooper Component Library. Provides component documentation, p...
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 2.1.0 | MIT | ||
| 2.0.0 | MIT | ||
| 1.3.8 | MIT | ||
| 1.3.7 | ^3.0 | MIT | |
| 1.3.5 | ^3.0 | MIT | |
| 1.3.4 | ^3.0 | MIT | |
| 1.3.3 | ^3.0 | MIT | |
| 1.3.2 | ^3.0 | MIT | |
| 1.3.1 | ^3.0 | MIT | |
| 1.3.0 | ^3.0 | MIT | |
| 1.2.2 | ^3.0 | MIT | |
| 1.2.1 | ^3.0 | MIT | |
| 1.1.1 | ^3.0 | MIT | |
| 1.1.0 | ^3.0 | MIT | |
| 1.0.5 | ^3.0 | MIT | |
| 1.0.4 | ^3.0 | MIT | |
| 1.0.3 | ^3.0 | MIT | |
| 1.0.2 | ^3.0 | MIT | |
| 1.0.1 | ^3.0 | MIT | |
| 1.0.0 | ^3.0 | MIT |