Skip to content

What is the proper way to use nuxt/icon with a SPA? #492

Description

@VictorioBerra

I am making a nuxt app where my deployment target is nuxt generate with ssr false on most pages, and true on like /pricing and seo pages. Pocketbase will serve my static files.

In nuxt dev, icons wont load, but i found a working solution I want to verify this is OK and im not bloating my bundle, or creating future issues.

nuxt.config.ts

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  compatibilityDate: '2026-05-21',

  modules: [
    '@nuxt/eslint',
    '@nuxt/ui'
  ],

  devtools: {
    enabled: true
  },

  css: ['~/assets/css/main.css'],

  // PocketBase SDK base URL — consumed by `plugins/pocketbase.ts`.
  // In production the Nuxt static files are served by PocketBase on the same
  // origin, so '/' works as a relative base URL (PB SDK strips the trailing
  // slash and builds relative paths like /api/...). The devProxy above handles
  // forwarding in dev. Nuxt automatically overrides this from the
  // `NUXT_PUBLIC_PB_URL` env var at runtime.
  runtimeConfig: {
    public: {
      pbUrl: '/'
    }
  },

  // SPA mode for all authenticated/dynamic app routes.
  // The marketing pages (/ and /pricing) remain SSR-pre-rendered via @nuxt/content.
  // PocketBase serves the static output and handles SPA fallback via apis.Static with
  // indexFallback=true (see main.go OnServe hook).
  routeRules: {
    '/auth/**': { ssr: false },
    '/portal/**': { ssr: false },
    '/book/**': { ssr: false },
    '/forms/**': { ssr: false }
  },

  // Dev-only: proxy PocketBase API and admin routes to avoid CORS issues
  // (browser fetches to /api/* and /_/* are forwarded to PB on :8090)
  nitro: {
    devProxy: {
      '/api': { target: 'http://localhost:8090/api', changeOrigin: true },
      '/_/': { target: 'http://localhost:8090/_/', changeOrigin: true }
    },
    prerender: {
      // Don't fail the build if a dynamic route (e.g. /book/[slug]) can't be crawled
      failOnError: false
    }
  },

  eslint: {
    config: {
      stylistic: {
        commaDangle: 'never',
        braceStyle: '1tbs'
      }
    }
  },

  // @nuxt/icon: In production, PocketBase serves static output with no Nitro
  // server, so the /_nuxt_icon server endpoint is unreachable from SPA pages.
  // Use provider: 'none' + client bundle to pre-bundle all icons at build time.
  //
  // - icons: explicit list of every icon hardcoded in @nuxt/ui's theme defaults
  //   (from node_modules/@nuxt/ui/dist/shared/ui.*.mjs). More reliable than
  //   scanning node_modules since those files are hashed and not .vue files.
  // - scan: true picks up any icon literals in your own app/** source files.
  icon: {
    provider: 'none',
    clientBundle: {
      scan: true,
      icons: [
        'lucide:arrow-down',
        'lucide:arrow-left',
        'lucide:arrow-right',
        'lucide:arrow-up',
        'lucide:arrow-up-right',
        'lucide:check',
        'lucide:chevron-down',
        'lucide:chevron-left',
        'lucide:chevron-right',
        'lucide:chevrons-left',
        'lucide:chevrons-right',
        'lucide:chevron-up',
        'lucide:circle-alert',
        'lucide:circle-check',
        'lucide:circle-x',
        'lucide:copy',
        'lucide:copy-check',
        'lucide:ellipsis',
        'lucide:eye',
        'lucide:eye-off',
        'lucide:file',
        'lucide:folder',
        'lucide:folder-open',
        'lucide:grip-vertical',
        'lucide:hash',
        'lucide:info',
        'lucide:lightbulb',
        'lucide:loader-circle',
        'lucide:menu',
        'lucide:minus',
        'lucide:monitor',
        'lucide:moon',
        'lucide:panel-left-close',
        'lucide:panel-left-open',
        'lucide:plus',
        'lucide:rotate-ccw',
        'lucide:search',
        'lucide:square',
        'lucide:sun',
        'lucide:terminal',
        'lucide:triangle-alert',
        'lucide:upload',
        'lucide:x'
      ]
    }
  }
})

app.config.ts

export default defineAppConfig({
  ui: {
    colors: {
      primary: 'violet',
      neutral: 'slate'
    },
  }
})

package.json

{
  "name": "frontend",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "nuxt build",
    "dev": "nuxt dev",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare",
    "lint": "eslint .",
    "typecheck": "nuxt typecheck"
  },
  "dependencies": {
    "@iconify-json/lucide": "^1.2.108",
    "@iconify-json/simple-icons": "^1.2.83",
    "@nuxt/ui": "^4.7.1",
    "nuxt": "^4.4.6",
    "pocketbase": "^0.26.2",
    "tailwindcss": "^4.3.0"
  },
  "devDependencies": {
    "@nuxt/eslint": "^1.15.2",
    "eslint": "^10.4.0",
    "typescript": "^6.0.3",
    "vue-tsc": "^3.3.0"
  },
  "packageManager": "pnpm@11.1.3"
}

I hate listing every single icon out. I wish I could just drop in nuxt/ui (comes with nuxt/icon), have defaults in my config and be on my happy way.

Did I configure this right?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions