Skip to content

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

@VictorioBerra

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions