@cooperco/nuxt-layer-base

Base Nuxt layer for cooperco projects
7,945
Install
npm install @cooperco/nuxt-layer-base
Latest Version:2.1.0
Vue:3
License:MIT
Last Updated:Jul 21, 2026
Links: npm
Maintainer: krysta.hunt

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/eslint with 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

  1. 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
  1. 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 window errors, unhandled rejections)
  • Provide a useLogger() composable for custom logs (and a deprecated useLoggly() 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=true
  • NUXT_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 a NODE_ENV guess, 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/log endpoint (with /api/loggly kept 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 (default debug) is the minimum severity forwarded to Loggly. The default sends everything; per the logging standard, set it to warn (or higher) in idle lower environments to reduce noise. Entries below the threshold return { ok: false, skipped: true }.
  • Back‑compat alias: /api/loggly remains 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. warn records don't trip error alerting at any LOG_LEVEL, so a bot sweeping for /wp-admin and /.env no longer pages anyone regardless of how you configure the threshold.
  • Loggly volume/cost is the only reason to go further. NUXT_PUBLIC_LOG_LEVEL=error drops 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.status comes from the thrown error, not the response object. Nitro's captureError runs before its error handler writes the response status, so event.node.res.statusCode is still unset inside the hook — reading it there reports 200 for a genuine 500.
  • meta.referer and meta.userAgent are 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: your LOGGLY_TAGS come first, the LOGGLY_ENV environment 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 error events: client, window
    • Unhandled rejections: client, promise
    • Server‑side errors: server

Logging payload

  • Logs are normalized server-side to the standard schema: level, message, tags, optional meta, error ({ name, message } only — no stack), data (always a JSON‑stringified string), and an ISO‑8601 ts.
  • useLogger() accepts an optional 4th data argument (any value); it is JSON‑stringified server-side. Captured‑error stack traces are relocated into data (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, window error 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, and data, and redacts sensitive key names (passwords, tokens, SSN, card, medical/DOB, etc.). Apps may add domain‑specific keys via NUXT_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:

  1. 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
  1. Start your app and trigger a test error.
  2. Check the network tab for POST /api/log and then confirm the entry in Loggly.

Troubleshooting:

  • No logs? Ensure LOGGLY_ENABLED=true and LOGGLY_TOKEN is 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.

  1. Create eslint.config.mjs in 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']
  }
})
  1. 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-tabs rule 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]
    • @stylistic/no-tabs with { allowIndentationTabs: true } — Tabs are allowed for indentation but tabs elsewhere are flagged.
    • @typescript-eslint/no-unused-vars with { 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
        }
        
    • 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" />
        
    • vue/html-closing-bracket-spacing: { selfClosingTag: 'never' } — No space before a self‑closing />.
      • Bad: <my-comp />
      • Good: <my-comp/>
    • vue/html-indent: ['tab', { baseIndent: 0 }] — Use tabs for indentation in Vue templates.
      • Example:
        <template>
        	<div>
        		<span>Text</span>
        	</div>
        </template>
        
    • vue/multi-word-component-names — Enforce multi‑word component names (no single generic names like Header conflicts). 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, the name should 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:

  1. Copy workflow files into your app’s repo
  • Create .github/workflows/lint.yml with a job that checks out your code, sets up Node, installs dependencies, and runs npm run lint.
  • (Optional) Create .github/workflows/typecheck.yml to run npm run typecheck if 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
  1. 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.
  1. 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/eslint with stylistic rules enabled and custom rules in eslint.config.mjs

Logging implementation (overview)

  • Client plugin: app/plugins/loggly.client.ts hooks into Nuxt/Vue error streams and window events and posts to /api/log
  • Composable: app/composables/useLogger.ts exposes error|warn|info|debug helpers posting to /api/log (deprecated wrapper useLoggly.ts delegates to useLogger())
  • Server plugin: server/plugins/loggly.ts hooks Nitro error events, derives severity/status via resolveErrorLevel()/resolveErrorStatus(), and posts to /api/log
  • API endpoint (canonical): server/api/log.post.ts scrubs/normalizes payloads and forwards to Loggly using LOGGLY_TOKEN
  • API endpoint (alias, deprecated): server/api/loggly.post.ts re-exports the canonical handler so /api/loggly continues to work
  • Runtime config (see nuxt.config.ts):
    • Server-only: logglyToken, logglyEndpoint
    • Public: logglyEnabled, logglyTags, logLevel
    • Supports both LOGGLY_* and NUXT_PUBLIC_LOG_* env vars where appropriate

Environment files

  • layers/base/.env is only for developing the base layer directly (not used by consuming apps)
  • For the repo playground, use playground/.env
  • For downstream apps, use .env or .env.local in 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.Z matching the version
  • CI checks whether the version exists on npm and publishes if not

Important notes:

  • Do NOT rely on npm version to create the tag (it creates vX.Y.Z). Create base-vX.Y.Z yourself.
  • publishConfig.access is set to public; publishes are public to npmjs.
  • The workflow skips if the exact version already exists.

Step-by-step

  1. Bump version without creating a tag:
cd layers/base
npm version patch --no-git-tag-version  # or minor | major
  1. Commit and push:
git add layers/base/package.json
git commit -m "chore(base): bump version"
git push origin main
  1. 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"
  1. CI will publish
  • Workflow: .github/workflows/publish-base.yml
  • Auth: NPM_TOKEN GitHub secret

Troubleshooting

  • Version exists: bump again and retag
  • 401/403: ensure NPM_TOKEN is configured and has access

Related Packages

@cooperco/nuxt-layer-seo

SEO Nuxt layer for cooperco projects

2,848
@cooperco/component-library-mcp-server

MCP server for the Cooper Component Library. Provides component documentation, p...

11,387

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