├── .npmrc
├── .markdownlintignore
├── playground
├── .npmrc
├── .nuxtrc
├── server
│ └── tsconfig.json
├── app
│ ├── app.vue
│ ├── components
│ │ ├── PostPlaceholder.vue
│ │ ├── blocks
│ │ │ └── CoreButton.vue
│ │ ├── PagePlaceholder.vue
│ │ ├── UsePostsByCategory.vue
│ │ ├── LatestPost.vue
│ │ ├── PrevNext.vue
│ │ ├── UsePosts.vue
│ │ ├── PostAside.vue
│ │ └── HeaderComponent.vue
│ ├── app.config.ts
│ ├── layouts
│ │ └── default.vue
│ ├── pages
│ │ ├── index.vue
│ │ ├── test
│ │ │ ├── errorHandling.vue
│ │ │ ├── config.vue
│ │ │ └── composables.vue
│ │ └── [...slug].vue
│ ├── error.vue
│ ├── assets
│ │ └── css
│ │ │ └── main.css
│ └── types.d.ts
├── tsconfig.json
├── eslint.config.mjs
├── nuxt.config.d.ts
├── package.json
├── nuxt.config.ts
└── .env.example
├── wordpress
├── wp-env-apache-modules.yml
├── demo-content
│ ├── testpage.txt
│ ├── testpost.txt
│ └── newmodule.txt
└── wp-env-cli.sh
├── docs
├── 4.advanced
│ ├── _dir.yml
│ ├── 4.staging.md
│ ├── 2.extending-posts.md
│ ├── 1.index.md
│ ├── 3.customcomponents.md
│ ├── 5.api-access.md
│ └── 6.performance.md
├── 2.composables
│ ├── _dir.yml
│ ├── 8.staging.md
│ ├── 5.settings.md
│ ├── 6.viewer.md
│ ├── 4.menu.md
│ ├── 2.pages.md
│ ├── 3.nodes.md
│ ├── 7.custom-queries.md
│ ├── 0.index.md
│ ├── 9.previous-next-post.md
│ └── 1.posts.md
├── 6.under-the-hood
│ ├── _dir.yml
│ ├── 1.index.md
│ └── 2.code-generation.md
├── 5.localdev
│ ├── _dir.yml
│ ├── 3.localwp.md
│ ├── 2.docker-compose.md
│ └── 1.index.md
├── 1.getting-started
│ ├── _dir.yml
│ ├── 4.components.md
│ ├── 7.typescript.md
│ ├── 5.authentication.md
│ ├── 6.images.md
│ ├── 3.composables.md
│ ├── 1.index.md
│ └── 2.configuration.md
├── 3.other-composables
│ └── _dir.yml
├── 7.troubleshoot
│ ├── _dir.yml
│ └── 1.index.md
├── 0.index.md
└── index.yml
├── src
├── runtime
│ ├── queries
│ │ ├── fragments
│ │ │ ├── MenuItem.fragment.gql
│ │ │ ├── NodeWithExcerpt.fragment.gql
│ │ │ ├── NodeWithContentEditor.fragment.gql
│ │ │ ├── GeneralSettings.fragment.gql
│ │ │ ├── NodeWithFeaturedImageToMediaItemConnectionEdge.fragment.gql
│ │ │ ├── NodeWithFeaturedImage.fragment.gql
│ │ │ ├── Page.fragment.gql
│ │ │ ├── Post.fragment.gql
│ │ │ ├── ContentNode.fragment.gql
│ │ │ └── MediaItem.fragment.gql
│ │ ├── GeneralSettings.gql
│ │ ├── Node.gql
│ │ ├── Revisions.gql
│ │ ├── Viewer.gql
│ │ ├── Menu.gql
│ │ ├── Pages.gql
│ │ └── Posts.gql
│ ├── server
│ │ ├── nitro.d.ts
│ │ ├── index.ts
│ │ └── api
│ │ │ └── _wpnuxt
│ │ │ ├── config.ts
│ │ │ └── content.post.ts
│ ├── plugins
│ │ ├── vue-sanitize-directive.d.ts
│ │ └── vue-sanitize-directive.ts
│ ├── util
│ │ ├── images.ts
│ │ └── logger.ts
│ ├── components
│ │ ├── ContentRenderer.vue
│ │ ├── WPContent.vue
│ │ ├── WPNuxtLogo.vue
│ │ ├── WordPressLogo.vue
│ │ └── StagingBanner.vue
│ ├── composables
│ │ ├── isStaging.ts
│ │ ├── useFeaturedImage.ts
│ │ ├── useWPUri.ts
│ │ ├── usePrevNextPost.ts
│ │ └── useWPContent.ts
│ └── types.d.ts
├── generate.ts
├── types.d.ts
├── useParser.ts
├── context.ts
├── module.ts
└── utils.ts
├── tests
├── fixtures
│ └── basic
│ │ ├── package.json
│ │ ├── nuxt.config.d.ts
│ │ ├── pages
│ │ └── index.vue
│ │ └── nuxt.config.ts
├── basic.test.ts
├── utils.test.ts
└── composables
│ └── useWPContent.test.ts
├── .vscode
├── settings.json
└── wpnuxt.code-snippets
├── .nuxtrc
├── .release-it-edge.json
├── .github
├── renovate.json
└── workflows
│ └── ci.yml
├── .editorconfig
├── pnpm-workspace.yaml
├── tsconfig.json
├── .release-it.json
├── .markdownlint.yml
├── eslint.config.mjs
├── .wp-env.json
├── LICENSE
├── .gitignore
├── README.md
├── package.json
└── CHANGELOG.md
/.npmrc:
--------------------------------------------------------------------------------
1 | shamefully-hoist=true
--------------------------------------------------------------------------------
/.markdownlintignore:
--------------------------------------------------------------------------------
1 | **/node_modules
2 |
--------------------------------------------------------------------------------
/playground/.npmrc:
--------------------------------------------------------------------------------
1 | shamefully-hoist=true
2 |
--------------------------------------------------------------------------------
/playground/.nuxtrc:
--------------------------------------------------------------------------------
1 | typescript.includeWorkspace=true
2 |
--------------------------------------------------------------------------------
/wordpress/wp-env-apache-modules.yml:
--------------------------------------------------------------------------------
1 | apache_modules:
2 | - mod_rewrite
3 |
--------------------------------------------------------------------------------
/docs/4.advanced/_dir.yml:
--------------------------------------------------------------------------------
1 | title: Advanced
2 | icon: i-oui-app-advanced-settings
3 |
--------------------------------------------------------------------------------
/docs/2.composables/_dir.yml:
--------------------------------------------------------------------------------
1 | title: Composables
2 | icon: i-carbon-cloud-download
3 |
--------------------------------------------------------------------------------
/docs/6.under-the-hood/_dir.yml:
--------------------------------------------------------------------------------
1 | title: Under the hood
2 | icon: i-ph-puzzle-piece
3 |
--------------------------------------------------------------------------------
/docs/5.localdev/_dir.yml:
--------------------------------------------------------------------------------
1 | title: Local Development Setup
2 | icon: i-material-symbols-code
3 |
--------------------------------------------------------------------------------
/playground/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../.nuxt/tsconfig.server.json"
3 | }
4 |
--------------------------------------------------------------------------------
/docs/1.getting-started/_dir.yml:
--------------------------------------------------------------------------------
1 | title: Getting Started
2 | icon: i-ph-rocket-launch-duotone
3 |
--------------------------------------------------------------------------------
/docs/3.other-composables/_dir.yml:
--------------------------------------------------------------------------------
1 | title: Other Composables
2 | icon: i-carbon-cloud-services
3 |
--------------------------------------------------------------------------------
/docs/7.troubleshoot/_dir.yml:
--------------------------------------------------------------------------------
1 | title: Troubleshoot
2 | icon: i-material-symbols-troubleshoot
3 |
--------------------------------------------------------------------------------
/docs/4.advanced/4.staging.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Staging
3 | description:
4 | ---
5 |
6 | coming soon
7 |
--------------------------------------------------------------------------------
/src/runtime/queries/fragments/MenuItem.fragment.gql:
--------------------------------------------------------------------------------
1 | fragment MenuItem on MenuItem {
2 | label
3 | uri
4 | }
5 |
--------------------------------------------------------------------------------
/tests/fixtures/basic/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "basic",
4 | "type": "module"
5 | }
6 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | // Enable ESlint flat config support
3 | "eslint.experimental.useFlatConfig": true,
4 | }
5 |
--------------------------------------------------------------------------------
/src/runtime/queries/fragments/NodeWithExcerpt.fragment.gql:
--------------------------------------------------------------------------------
1 | fragment NodeWithExcerpt on NodeWithExcerpt {
2 | excerpt
3 | }
4 |
--------------------------------------------------------------------------------
/src/runtime/queries/GeneralSettings.gql:
--------------------------------------------------------------------------------
1 | query GeneralSettings {
2 | generalSettings {
3 | ...GeneralSettings
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/wordpress/demo-content/testpage.txt:
--------------------------------------------------------------------------------
1 | This is my test page
2 |
3 | Content is loaded from the file demo-content/testpage.txt
4 |
--------------------------------------------------------------------------------
/.nuxtrc:
--------------------------------------------------------------------------------
1 | imports.autoImport=false
2 | typescript.includeWorkspace=true
3 | typescript.tsConfig.compilerOptions.noUncheckedIndexedAccess=true
4 |
--------------------------------------------------------------------------------
/src/runtime/queries/Node.gql:
--------------------------------------------------------------------------------
1 | query NodeByUri($uri: String!) {
2 | nodeByUri(uri: $uri) {
3 | ...Page
4 | ...Post
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/runtime/server/nitro.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'nitro/runtime' {
2 | export { defineCachedEventHandler } from 'nitropack/runtime'
3 | }
4 |
--------------------------------------------------------------------------------
/wordpress/demo-content/testpost.txt:
--------------------------------------------------------------------------------
1 | This is my test post
2 |
3 | Content is loaded from the file demo-content/testpost-content.txt
4 |
--------------------------------------------------------------------------------
/src/runtime/queries/fragments/NodeWithContentEditor.fragment.gql:
--------------------------------------------------------------------------------
1 | fragment NodeWithContentEditor on NodeWithContentEditor {
2 | content
3 | }
4 |
--------------------------------------------------------------------------------
/docs/4.advanced/2.extending-posts.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Extending Posts & Pages
3 | description:
4 | ---
5 |
6 | ## Example: Yoast SEO
7 |
8 | coming soon
9 |
--------------------------------------------------------------------------------
/playground/app/app.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/playground/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.nuxt/tsconfig.json",
3 | "exclude": [
4 | "node_modules",
5 | "dist",
6 | ".nuxt",
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/docs/5.localdev/3.localwp.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: LocalWP
3 | description: use LocalWP to run a local WordPress instance on your computer
4 | ---
5 |
6 | coming soon
7 |
--------------------------------------------------------------------------------
/src/runtime/queries/Revisions.gql:
--------------------------------------------------------------------------------
1 | query Revisions {
2 | revisions {
3 | nodes {
4 | date
5 | uri
6 | isPreview
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/.release-it-edge.json:
--------------------------------------------------------------------------------
1 | {
2 | "git": {
3 | "push": true
4 | },
5 | "github": {
6 | "release": true,
7 | "releaseName": "v${version}"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/docs/5.localdev/2.docker-compose.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Docker-compose
3 | description: use docker-compose to run a local WordPress instance on your computer
4 | ---
5 |
6 | coming soon
7 |
--------------------------------------------------------------------------------
/docs/2.composables/8.staging.md:
--------------------------------------------------------------------------------
1 |
2 | #### Staging
3 |
4 | ```ts twoslash
5 | const staging = await isStaging()
6 | ```
7 | More about this on the [staging](../3.advanced/4.staging.md) page.
8 |
--------------------------------------------------------------------------------
/src/runtime/server/index.ts:
--------------------------------------------------------------------------------
1 | export function usePosts() {
2 | return $fetch('/api/wpContent', {
3 | method: 'POST',
4 | body: {
5 | queryName: 'Posts'
6 | }
7 | })
8 | }
9 |
--------------------------------------------------------------------------------
/wordpress/demo-content/newmodule.txt:
--------------------------------------------------------------------------------
1 | WPNuwt is a new Nuxt module to make it easy to set up a headless WordPress with a Nuxt frontend.
2 |
3 | Find out more at WPNuxt.com
4 |
--------------------------------------------------------------------------------
/docs/2.composables/5.settings.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Settings
3 | description: Generated composables to fetch Settings
4 | ---
5 |
6 | ```ts twoslash
7 | const { data: settings } = await useWPGeneralSettings()
8 | ```
9 |
--------------------------------------------------------------------------------
/docs/5.localdev/1.index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: wp-env
3 | description:
4 | ---
5 |
6 | coming soon
7 |
8 | [Get started with wp-env](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-env/)
9 |
--------------------------------------------------------------------------------
/src/runtime/plugins/vue-sanitize-directive.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'vue-sanitize-directive' {
2 | import type { Plugin } from 'vue'
3 |
4 | const VueSanitizeDirective: Plugin
5 | export default VueSanitizeDirective
6 | }
7 |
--------------------------------------------------------------------------------
/.github/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "github>nuxt/renovate-config-nuxt"
5 | ],
6 | "assignees": [
7 | "vernaillen"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/playground/app/components/PostPlaceholder.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/tests/fixtures/basic/nuxt.config.d.ts:
--------------------------------------------------------------------------------
1 | import type { WPNuxtConfig } from '../../../src/types'
2 |
3 | declare module '@nuxt/schema' {
4 | interface NuxtConfig {
5 | wpNuxt?: WPNuxtConfig
6 | }
7 | }
8 |
9 | export {}
10 |
--------------------------------------------------------------------------------
/src/runtime/queries/Viewer.gql:
--------------------------------------------------------------------------------
1 | query Viewer {
2 | viewer {
3 | username
4 | userId
5 | id
6 | email
7 | description
8 | firstName
9 | lastName
10 | locale
11 | url
12 | uri
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/runtime/queries/fragments/GeneralSettings.fragment.gql:
--------------------------------------------------------------------------------
1 | fragment GeneralSettings on GeneralSettings {
2 | title
3 | description
4 | url
5 | dateFormat
6 | language
7 | startOfWeek
8 | timezone
9 | timeFormat
10 | }
11 |
--------------------------------------------------------------------------------
/src/runtime/queries/Menu.gql:
--------------------------------------------------------------------------------
1 | query Menu($name: ID! = "main", $idType: MenuNodeIdTypeEnum! = NAME) {
2 | menu(id: $name, idType: $idType) {
3 | menuItems {
4 | nodes {
5 | ...MenuItem
6 | }
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_size = 2
5 | indent_style = space
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/docs/2.composables/6.viewer.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Viewer
3 | description: Generated composables to fetch Viewer
4 | ---
5 |
6 | ```ts twoslash
7 | const { data: viewer } = await useWPViewer()
8 | ```
9 | Authentication is required to fetch the Viewer
10 |
--------------------------------------------------------------------------------
/playground/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import withNuxt from './.nuxt/eslint.config.mjs'
2 |
3 | export default withNuxt(
4 | {
5 | files: ['**/pages/*.vue'],
6 | rules: {
7 | 'vue/multi-word-component-names': 'off'
8 | }
9 | }
10 | )
11 |
--------------------------------------------------------------------------------
/src/runtime/queries/fragments/NodeWithFeaturedImageToMediaItemConnectionEdge.fragment.gql:
--------------------------------------------------------------------------------
1 | fragment NodeWithFeaturedImageToMediaItemConnectionEdge on NodeWithFeaturedImageToMediaItemConnectionEdge {
2 | cursor
3 | node {
4 | ...MediaItem
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - .
3 | - playground
4 |
5 | ignoredBuiltDependencies:
6 | - '@parcel/watcher'
7 | - unrs-resolver
8 | - vue-demi
9 |
10 | onlyBuiltDependencies:
11 | - '@tailwindcss/oxide'
12 | - esbuild
13 | - sharp
14 |
--------------------------------------------------------------------------------
/playground/app/app.config.ts:
--------------------------------------------------------------------------------
1 | import { defineAppConfig } from '#imports'
2 |
3 | export default defineAppConfig({
4 | ui: {
5 | colors: {
6 | primary: 'reefgold',
7 | neutral: 'neutral',
8 | gray: 'neutral'
9 | }
10 | }
11 | })
12 |
--------------------------------------------------------------------------------
/src/runtime/queries/fragments/NodeWithFeaturedImage.fragment.gql:
--------------------------------------------------------------------------------
1 | fragment NodeWithFeaturedImage on NodeWithFeaturedImage {
2 | featuredImage {
3 | ...NodeWithFeaturedImageToMediaItemConnectionEdge
4 | }
5 | featuredImageDatabaseId
6 | featuredImageId
7 | id
8 | }
9 |
--------------------------------------------------------------------------------
/tests/fixtures/basic/pages/index.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 | basic {{ config.public.myValue }}
9 |
10 |
--------------------------------------------------------------------------------
/src/runtime/util/images.ts:
--------------------------------------------------------------------------------
1 | const getRelativeImagePath = function getRelativeImagePath(imgUrl: string): string {
2 | if (imgUrl) {
3 | const url = new URL(imgUrl)
4 | imgUrl = url.pathname
5 | }
6 | return imgUrl
7 | }
8 |
9 | export { getRelativeImagePath }
10 |
--------------------------------------------------------------------------------
/src/runtime/queries/fragments/Page.fragment.gql:
--------------------------------------------------------------------------------
1 | fragment Page on Page {
2 | ...ContentNode
3 | ...NodeWithFeaturedImage
4 | ...NodeWithContentEditor
5 | isFrontPage
6 | isPostsPage
7 | isPreview
8 | isPrivacyPage
9 | isRestricted
10 | isRevision
11 | title
12 | }
13 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.nuxt/tsconfig.json",
3 | "compilerOptions": {
4 | "strict": true,
5 | "noUncheckedIndexedAccess": true,
6 | "noImplicitOverride": true
7 | },
8 | "exclude": [
9 | "node_modules",
10 | "dist",
11 | "wordpress"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/docs/1.getting-started/4.components.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Components
3 | description:
4 | ---
5 |
6 | ## WPContent
7 |
8 | ```vue
9 |
10 | ```
11 |
12 | The node passed to the WPContent component must be of type NodeWithContentEditorFragment or NodeWithEditorBlocksFragment.
13 |
--------------------------------------------------------------------------------
/playground/app/layouts/default.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/runtime/plugins/vue-sanitize-directive.ts:
--------------------------------------------------------------------------------
1 | import { defineNuxtPlugin } from 'nuxt/app'
2 | // @ts-expect-error - vue-sanitize-directive doesn't have type declarations
3 | import VueSanitize from 'vue-sanitize-directive'
4 |
5 | export default defineNuxtPlugin((nuxtApp) => {
6 | nuxtApp.vueApp.use(VueSanitize)
7 | })
8 |
--------------------------------------------------------------------------------
/src/runtime/components/ContentRenderer.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/runtime/queries/fragments/Post.fragment.gql:
--------------------------------------------------------------------------------
1 | fragment Post on Post {
2 | ...NodeWithExcerpt
3 | ...ContentNode
4 | ...NodeWithFeaturedImage
5 | ...NodeWithContentEditor
6 | title
7 | categories {
8 | nodes {
9 | id
10 | name
11 | uri
12 | description
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/docs/7.troubleshoot/1.index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Troubleshooting
3 | description:
4 | ---
5 |
6 | ## 429 Too Many Requests
7 |
8 | When prerendering the pages using "nuxt generate" many requests will be send to the WP GraphQL API in a short period of time.
9 | This can hit the rate limit set by your wordpress hosting provider.
10 |
--------------------------------------------------------------------------------
/docs/2.composables/4.menu.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Menus
3 | description: Generated composables to fetch Menus
4 | ---
5 |
6 | ```ts twoslash
7 | const { data: mainMenu } = useWPMenu()
8 | const { data: footerMenu } = await useWPMenu({ name: 'footer' })
9 | ```
10 | if you don't specify a menu name, then WPNuxt will try to fetch the menu with name "main"
11 |
--------------------------------------------------------------------------------
/.release-it.json:
--------------------------------------------------------------------------------
1 | {
2 | "git": {
3 | "commitMessage": "chore(release): release v${version}"
4 | },
5 | "github": {
6 | "release": true,
7 | "releaseName": "v${version}"
8 | },
9 | "hooks": {
10 | "after:bump": "npx changelogen@latest --no-commit --no-tag --output --r $(node -p \"require('./package.json').version\")"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/runtime/queries/Pages.gql:
--------------------------------------------------------------------------------
1 | query Pages($limit: Int = 10) {
2 | pages(first: $limit) {
3 | nodes {
4 | ...Page
5 | }
6 | }
7 | }
8 | query PageByUri($uri: String!) {
9 | nodeByUri(uri: $uri) {
10 | ...Page
11 | }
12 | }
13 | query PageById($id: ID!) {
14 | page(id: $id, idType: DATABASE_ID, asPreview: true) {
15 | ...Page
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/runtime/server/api/_wpnuxt/config.ts:
--------------------------------------------------------------------------------
1 | import { defineCachedEventHandler } from 'nitro/runtime'
2 | import { useRuntimeConfig } from '#imports'
3 | import type { H3Event } from 'h3'
4 |
5 | export default defineCachedEventHandler(async (event: H3Event) => {
6 | const config = useRuntimeConfig(event)
7 | return {
8 | wpNuxt: config.public.wpNuxt
9 | }
10 | })
11 |
--------------------------------------------------------------------------------
/docs/6.under-the-hood/1.index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Dependencies
3 | description:
4 | ---
5 |
6 | ## Nuxt GraphQL Middleware
7 |
8 | The [Nuxt GraphQL Middleware](https://github.com/dulnan/nuxt-graphql-middleware) by Jan Hug, [Dulnan on GitHub](https://github.com/dulnan) is used to fetch data from the [WPGraphQL endpoints](https://www.wpgraphql.com/).
9 |
10 | ## @graphql-codegen
11 |
12 | ## Nuxt Image
13 |
--------------------------------------------------------------------------------
/docs/1.getting-started/7.typescript.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Typescript
3 | description:
4 | ---
5 |
6 | Types are exported by the [Nuxt GraphQL Middleware plugin](https://nuxt-graphql-middleware.dulnan.net/) and can be imported with the [#graphql-operations](https://nuxt-graphql-middleware.dulnan.net/features/typescript.html#importing-types) alias.
7 |
8 | for example:
9 | ```ts
10 | import type { PostFragment, PageFragment } from '#graphql-operations'
11 | ```
12 |
--------------------------------------------------------------------------------
/tests/fixtures/basic/nuxt.config.ts:
--------------------------------------------------------------------------------
1 | import WPNuxtModule from '../../../src/module'
2 |
3 | export default defineNuxtConfig({
4 | modules: [
5 | WPNuxtModule
6 | ],
7 | runtimeConfig: {
8 | public: {
9 | myValue: 'original value'
10 | }
11 | },
12 | // @ts-expect-error - wpNuxt config is provided by wpnuxt module
13 | wpNuxt: {
14 | wordpressUrl: 'https://wordpress.wpnuxt.com',
15 | frontendUrl: 'https://demo.wpnuxt.com'
16 | }
17 | })
18 |
--------------------------------------------------------------------------------
/.markdownlint.yml:
--------------------------------------------------------------------------------
1 | # Default state for all rules
2 | default: true
3 | # Disable max line length
4 | MD013: false
5 | # Allow duplicated heading for different sections
6 | MD024:
7 | allow_different_nesting: true
8 | siblings_only: true
9 | # Allow multiple top-level headings
10 | MD025: false
11 | # Allow inline HTML
12 | MD033: false
13 | # Allow non blank lines around list
14 | MD032: false
15 | MD046:
16 | style: fenced
17 | MD034: false
18 | MD031: false
19 | MD007: false
20 |
--------------------------------------------------------------------------------
/playground/nuxt.config.d.ts:
--------------------------------------------------------------------------------
1 | import type { WPNuxtConfig } from '../src/types'
2 |
3 | declare module '@nuxt/schema' {
4 | interface NuxtConfig {
5 | wpNuxt?: WPNuxtConfig
6 | eslint?: {
7 | config?: {
8 | stylistic?: {
9 | commaDangle?: string
10 | braceStyle?: string
11 | }
12 | }
13 | checker?: {
14 | lintOnStart?: boolean
15 | fix?: boolean
16 | }
17 | }
18 | }
19 | }
20 |
21 | export {}
22 |
--------------------------------------------------------------------------------
/docs/1.getting-started/5.authentication.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Authentication
3 | description:
4 | ---
5 |
6 | * relies on the Faust.js plugin
7 | * the Faust secret key (available in the Faust settings is WPAdmin) should be set in nuxt:
8 |
9 | in nuxt-config.ts:
10 | ```ts
11 | wpNuxt: {
12 | faustSecretKey: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
13 | }
14 | ```
15 |
16 | or as env variable:
17 | ```properties
18 | WPNUXT_FAUST_SECRET_KEY=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
19 | ```
20 |
--------------------------------------------------------------------------------
/playground/app/components/blocks/CoreButton.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/docs/2.composables/2.pages.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Pages
3 | description: Generated composables to fetch Pages and their content
4 | ---
5 |
6 | WPNuxt provides 3 [queries to fetch pages](https://github.com/wpnuxt/wpnuxt-core/blob/main/src/runtime/queries/Pages.gql), which result in these generated composables:
7 |
8 | ```ts twoslash
9 | const { data: pages } = await useWPPages()
10 | const { data: page1 } = await useWPPageById({ id: 'databaseId' })
11 | const { data: page2 } = await useWPPageByUri({ uri: 'slug' })
12 | ```
13 |
--------------------------------------------------------------------------------
/playground/app/pages/index.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/docs/1.getting-started/6.images.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Images
3 | description:
4 | ---
5 |
6 | ### Configure Image Provider
7 |
8 | Since images are uploaded in WordPress, we can configure Nuxt Image to use an image provider that fetches the images from WordPress and optimises them.
9 |
10 | TODO: add configuration example
11 |
12 | ### wp-content proxy
13 |
14 | You can configure a proxy routeRule in Nuxt:
15 |
16 | ```ts
17 | routeRules: {
18 | '/wp-content/**': { proxy: { to: 'https://yourwordpress.com/wp-content/**' } }
19 | },
20 | ```
21 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { createConfigForNuxt } from '@nuxt/eslint-config/flat'
2 |
3 | export default createConfigForNuxt({
4 | features: {
5 | tooling: true,
6 | stylistic: {
7 | commaDangle: 'never',
8 | braceStyle: '1tbs'
9 | }
10 | },
11 | dirs: {
12 | src: [
13 | './playground'
14 | ]
15 | }
16 | },
17 | {
18 | rules: {
19 | 'vue/multi-word-component-names': 0
20 | },
21 | ignores: [
22 | 'wordpress',
23 | 'dist',
24 | 'node_modules',
25 | '.output',
26 | '.nuxt'
27 | ]
28 | })
29 |
--------------------------------------------------------------------------------
/src/runtime/queries/fragments/ContentNode.fragment.gql:
--------------------------------------------------------------------------------
1 | fragment ContentNode on ContentNode {
2 | # contentType
3 | contentTypeName
4 | databaseId
5 | date
6 | dateGmt
7 | desiredSlug
8 | # editingLockedBy
9 | enclosure
10 | # enqueuedScripts
11 | # enqueuedStylesheets
12 | guid
13 | id
14 | isContentNode
15 | isPreview
16 | isRestricted
17 | isTermNode
18 | # lastEditedBy
19 | link
20 | modified
21 | modifiedGmt
22 | previewRevisionDatabaseId
23 | previewRevisionId
24 | slug
25 | status
26 | # template
27 | uri
28 | }
29 |
--------------------------------------------------------------------------------
/playground/app/pages/test/errorHandling.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 | You should never see this
15 | {{ post }}
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/docs/1.getting-started/3.composables.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Composables
3 | description:
4 | ---
5 |
6 | ## Composables
7 |
8 | Composables to fetch content are automatically generated by WPNuxt [based on predefined queries](../content-queries/posts) as well as your own [custom queries](../content-queries/custom-queries).
9 |
10 | There's also a few [other composables provided by WPNuxt](#other-composables-provided-by-wpnuxt)
11 |
12 | ### Based on Predefined queries
13 |
14 | [Check them here](https://github.com/wpnuxt/wpnuxt-core/tree/main/src/runtime/queries)
15 |
16 | Can be overwritten by providing a custom query with the same name.
17 |
--------------------------------------------------------------------------------
/docs/2.composables/3.nodes.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Nodes
3 | description: Generated composables to fetch Nodes and their content
4 | ---
5 |
6 | #### useNodeByUri
7 |
8 | a usefull composable if you want to fetch content by uri, regardless whether it's a page or post type in WordPress.
9 |
10 | ```ts twoslash
11 | const { data: node } = await useWPNodeByUri({ uri: 'slug' })
12 | ```
13 |
14 | an example how to use this for a \[...slug\].vue page:
15 | ```ts twoslash
16 | const route = useRoute()
17 | if (route.params?.slug && route.params.slug[0]) {
18 | const { data: currentNode } = await useWPNodeByUri({ uri: route.params.slug[0] })
19 | }
20 | ```
21 |
--------------------------------------------------------------------------------
/src/runtime/composables/isStaging.ts:
--------------------------------------------------------------------------------
1 | import { useRuntimeConfig } from '#imports'
2 |
3 | /**
4 | * Checks if the application is running in staging mode
5 | *
6 | * @returns Whether staging mode is enabled in the WPNuxt configuration
7 | *
8 | * @example
9 | * ```typescript
10 | * // Check staging mode
11 | * const staging = await isStaging()
12 | *
13 | * // Conditionally show staging banner
14 | *
Staging Environment
15 | * ```
16 | */
17 | const _isStaging = async () => {
18 | const config = useRuntimeConfig()
19 | return config.public.wpNuxt.staging
20 | }
21 | export const isStaging = _isStaging
22 |
--------------------------------------------------------------------------------
/docs/2.composables/7.custom-queries.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Custom Queries
3 | description:
4 | ---
5 |
6 | Create movies.gql and put it in /extend/queries folder in your project
7 |
8 | ```gql
9 | query Movies($limit: Int = 10) {
10 | movies(first: $limit) {
11 | nodes {
12 | ...Movie
13 | }
14 | }
15 | }
16 | fragment Movie on Movie {
17 | ...ContentNode
18 | ...NodeWithFeaturedImage
19 | ...NodeWithContentEditor
20 | title
21 | }
22 | ```
23 |
24 | Based on the above infomation WPNuxt will generate:
25 | * the type 'MovieFragment'
26 | * the composable 'useMovies':
27 | * input params: { limit: integer }
28 | * return: MovieFragment[]
29 |
--------------------------------------------------------------------------------
/docs/2.composables/0.index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Composables
3 | description:
4 | navigation: false
5 | ---
6 |
7 | ## Generated Composables based on provided queries
8 |
9 | * [Posts](/docs/composables/posts)
10 | * [Pages](/docs/composables/pages)
11 | * [Nodes](/docs/composables/nodes)
12 | * [Menu](/docs/composables/menu)
13 | * [Settings](/docs/composables/settings)
14 | * [Viewer](/docs/composables/viewer)
15 |
16 | ## Generated Composables based on your custom queries
17 |
18 | * [Custom Queries](/docs/composables/custom-queries)
19 |
20 | ## Other Composables
21 |
22 | * [Staging](/docs/composables/staging)
23 | * [PreviousNextPost](/docs/composables/previous-next-post)
24 |
--------------------------------------------------------------------------------
/docs/6.under-the-hood/2.code-generation.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Code Generation
3 | description:
4 | ---
5 |
6 | Composables are automatically generated by WPNuxt based on [predefined queries](https://github.com/wpnuxt/wpnuxt-core/tree/main/src/runtime/queries) as well as your own [custom queries](../advanced/custom-post-types.md).
7 |
8 | This is done with the help of [GraphQL-Codegen](https://the-guild.dev/graphql/codegen)
9 |
10 | # Composables
11 |
12 | Composables are generated based on the given queries.
13 |
14 | ## prefix
15 |
16 | Default prefix: useWP
17 |
18 | Can be changed in config:
19 |
20 | ```ts
21 | wpNuxt: {
22 | ...
23 | composablesPrefix: 'use'
24 | },
25 | ```
26 |
27 | # Types
28 |
29 | fragment -> type
30 |
--------------------------------------------------------------------------------
/playground/app/components/PagePlaceholder.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/playground/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "wpnuxt-core-playground",
4 | "type": "module",
5 | "scripts": {
6 | "dev": "nuxi dev",
7 | "build": "nuxi build",
8 | "generate": "nuxi generate",
9 | "lint": "eslint .",
10 | "lint:fix": "eslint . --fix"
11 | },
12 | "devDependencies": {
13 | "@iconify-json/heroicons": "^1.2.2",
14 | "@iconify-json/lucide": "^1.2.81",
15 | "@iconify-json/mdi": "^1.2.2",
16 | "@iconify-json/svg-spinners": "^1.2.2",
17 | "@iconify-json/uil": "^1.2.3",
18 | "@nuxt/eslint": "^1.12.1",
19 | "@nuxt/image": "2.0.0",
20 | "nuxt": "^4.2.2",
21 | "vite-plugin-eslint2": "^5.0.3"
22 | },
23 | "dependencies": {
24 | "@nuxt/ui": "4.3.0"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/runtime/queries/Posts.gql:
--------------------------------------------------------------------------------
1 | query Posts($limit: Int = 10) {
2 | posts(first: $limit) {
3 | nodes {
4 | ...Post
5 | }
6 | }
7 | }
8 | query PostByUri($uri: String!) {
9 | nodeByUri(uri: $uri) {
10 | ...Post
11 | }
12 | }
13 | query PostById($id: ID!, $asPreview: Boolean = false) {
14 | post(id: $id, idType: DATABASE_ID, asPreview: $asPreview) {
15 | ...Post
16 | }
17 | }
18 |
19 | query PostsByCategoryName($categoryName: String!, $limit: Int = 10) {
20 | posts(first: $limit, where: {categoryName: $categoryName}) {
21 | nodes {
22 | ...Post
23 | }
24 | }
25 | }
26 | query PostsByCategoryId($categoryId: Int!, $limit: Int = 10) {
27 | posts(first: $limit, where: {categoryId: $categoryId}) {
28 | nodes {
29 | ...Post
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/playground/app/pages/test/config.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 | Examples of predefined composables
16 | wpUri:
17 | {{ wpUri }}
18 | wpNuxtConfig:
19 | {{ wpNuxtConfig }}
20 | isStaging()
21 | {{ staging }}
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/runtime/util/logger.ts:
--------------------------------------------------------------------------------
1 | import { createConsola, type LogLevel } from 'consola'
2 | import { ref } from 'vue'
3 | import { useRuntimeConfig } from '#imports'
4 |
5 | const loggerRef = ref()
6 |
7 | export const useLogger = () => {
8 | const logger = loggerRef.value
9 | if (logger) {
10 | return logger
11 | } else {
12 | const config = useRuntimeConfig()
13 | return initLogger(config.public.wpNuxt.logLevel ? config.public.wpNuxt.logLevel : 3)
14 | }
15 | }
16 |
17 | export const initLogger = (logLevel: LogLevel | undefined) => {
18 | loggerRef.value = createConsola({
19 | level: logLevel ? logLevel : 3,
20 | formatOptions: {
21 | colors: true,
22 | compact: true,
23 | date: true,
24 | fancy: true
25 | }
26 | }).withTag('wpnuxt')
27 | return loggerRef.value
28 | }
29 |
--------------------------------------------------------------------------------
/playground/app/error.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/src/generate.ts:
--------------------------------------------------------------------------------
1 | import { statSync } from 'node:fs'
2 | import { type Resolver, resolveFiles } from '@nuxt/kit'
3 | import { prepareContext, type WPNuxtContext } from './context'
4 |
5 | const allowDocument = (f: string, resolver: Resolver) => {
6 | const isSchema = f.match(/([^/]+)\.(gql|graphql)$/)?.[0]?.toLowerCase().includes('schema')
7 | return !isSchema && !!statSync(resolver.resolve(f)).size
8 | }
9 | export async function generateWPNuxtComposables(ctx: WPNuxtContext, queryOutputPath: string, resolver: Resolver) {
10 | const gqlMatch = '**/*.{gql,graphql}'
11 | const documents: string[] = []
12 | const files = (await resolveFiles(queryOutputPath, [gqlMatch, '!**/schemas'], { followSymbolicLinks: false })).filter(doc => allowDocument(doc, resolver))
13 | documents.push(...files)
14 | ctx.docs = documents
15 |
16 | await prepareContext(ctx)
17 | }
18 |
--------------------------------------------------------------------------------
/.wp-env.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | "https://github.com/wpnuxt/wpnuxt-plugin/releases/download/v0.0.3/wpnuxt-plugin.zip",
4 | "https://downloads.wordpress.org/plugin/faustwp.zip",
5 | "https://downloads.wordpress.org/plugin/advanced-custom-fields.zip",
6 | "https://downloads.wordpress.org/plugin/wp-graphql.zip",
7 | "https://downloads.wordpress.org/plugin/wpgraphql-acf.zip",
8 | "https://github.com/wpengine/wp-graphql-content-blocks/releases/download/v4.8.4/wp-graphql-content-blocks.zip"
9 | ],
10 | "port": 4000,
11 | "config": {
12 | "FRONTEND_URI": "http://localhost:3000"
13 | },
14 | "mappings": {
15 | "wp-cli.yml": "./wordpress/wp-env-apache-modules.yml",
16 | "demo-content": "./wordpress/demo-content"
17 | },
18 | "lifecycleScripts": {
19 | "afterStart": "wp-env run cli wp rewrite structure /%postname%/ --hard"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/playground/app/components/UsePostsByCategory.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
20 |
21 | -
25 |
{{ post.title }}
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/playground/app/components/LatestPost.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
15 |
20 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/runtime/components/WPContent.vue:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
26 |
27 |
--------------------------------------------------------------------------------
/src/runtime/components/WPNuxtLogo.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 | WPNuxt
12 |
13 |
14 |
15 |
36 |
--------------------------------------------------------------------------------
/src/runtime/queries/fragments/MediaItem.fragment.gql:
--------------------------------------------------------------------------------
1 | fragment MediaItem on MediaItem {
2 | altText
3 | # ancestors
4 | # author
5 | authorDatabaseId
6 | authorId
7 | caption
8 | # children
9 | commentCount
10 | commentStatus
11 | # comments
12 | # contentType
13 | contentTypeName
14 | databaseId
15 | date
16 | dateGmt
17 | description
18 | desiredSlug
19 | # editingLockedBy
20 | enclosure
21 | # enqueuedScripts
22 | # enqueuedStylesheets
23 | fileSize
24 | guid
25 | id
26 | isContentNode
27 | isPreview
28 | isRestricted
29 | isTermNode
30 | # lastEditedBy
31 | link
32 | # mediaDetails
33 | mediaItemId
34 | mediaItemUrl
35 | mediaType
36 | mimeType
37 | modified
38 | modifiedGmt
39 | # parent
40 | parentDatabaseId
41 | parentId
42 | previewRevisionDatabaseId
43 | previewRevisionId
44 | sizes
45 | slug
46 | sourceUrl
47 | srcSet
48 | status
49 | # template
50 | title
51 | uri
52 | }
53 |
--------------------------------------------------------------------------------
/playground/app/assets/css/main.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 | @import "@nuxt/ui";
3 |
4 | @theme {
5 | --font-sans: DM Sans, sans-serif;
6 |
7 | --color-reefgold-50: #faf9f0;
8 | --color-reefgold-100: #f5f3e1;
9 | --color-reefgold-200: #e6e2b5;
10 | --color-reefgold-300: #d6d090;
11 | --color-reefgold-400: #baaf4e;
12 | --color-reefgold-500: #9c8e1b;
13 | --color-reefgold-600: #8c7815;
14 | --color-reefgold-700: #755d0f;
15 | --color-reefgold-800: #5e4509;
16 | --color-reefgold-900: #452e06;
17 | --color-reefgold-950: #2e1b02;
18 |
19 | --color-woodsmoke-50: #64646C;
20 | --color-woodsmoke-100: #5A5A62;
21 | --color-woodsmoke-200: #47474D;
22 | --color-woodsmoke-300: #333337;
23 | --color-woodsmoke-400: #202022;
24 | --color-woodsmoke-500: #0C0C0D;
25 | --color-woodsmoke-600: #000000;
26 | --color-woodsmoke-700: #000000;
27 | --color-woodsmoke-800: #000000;
28 | --color-woodsmoke-900: #000000;
29 | --color-woodsmoke-950: #000000;
30 | }
31 |
--------------------------------------------------------------------------------
/docs/2.composables/9.previous-next-post.md:
--------------------------------------------------------------------------------
1 |
2 | #### get Previous/Next posts
3 |
4 | ```ts twoslash [usePrevNextPost()]
5 | const { prev: prev, next: next } = await usePrevNextPost('currentPostSlug')
6 | ```
7 |
8 | An example how to use the composable:
9 |
10 | ```vue [[...uri.vue\] ]
11 |
15 |
16 |
17 |
18 |
26 |
27 |
28 |
36 |
37 |
38 | ```
39 |
40 |
--------------------------------------------------------------------------------
/tests/basic.test.ts:
--------------------------------------------------------------------------------
1 | import { fileURLToPath } from 'node:url'
2 | import { describe, expect, it } from 'vitest'
3 | import { $fetch, setup, startServer } from '@nuxt/test-utils/e2e'
4 |
5 | describe('ssr', async () => {
6 | await setup({
7 | rootDir: fileURLToPath(new URL('./fixtures/basic', import.meta.url))
8 | })
9 |
10 | it('renders the index page', async () => {
11 | // Get response to a server-rendered page with `$fetch`.
12 | const html = await $fetch('/')
13 | expect(html).toContain('basic original value
')
14 | })
15 |
16 | it('changes runtime config and restarts', async () => {
17 | await startServer({ env: { NUXT_PUBLIC_MY_VALUE: 'overwritten by test!' } })
18 |
19 | const html = await $fetch('/')
20 | expect(html).toContain('basic overwritten by test!
')
21 |
22 | await startServer()
23 | const htmlRestored = await $fetch('/')
24 | expect(htmlRestored).toContain('basic original value
')
25 | })
26 | })
27 |
--------------------------------------------------------------------------------
/src/runtime/composables/useFeaturedImage.ts:
--------------------------------------------------------------------------------
1 | import { getRelativeImagePath } from '../util/images'
2 | import type { NodeWithFeaturedImage } from '#graphql-operations'
3 |
4 | /**
5 | * Extracts and converts the featured image URL to a relative path
6 | *
7 | * @param contentNode - A WordPress content node with a featured image
8 | * @returns The relative path to the featured image, or undefined if not available
9 | *
10 | * @example
11 | * ```typescript
12 | * // Get featured image from a post
13 | * const imagePath = useFeaturedImage(post)
14 | * // Returns: '/wp-content/uploads/2024/01/image.jpg'
15 | *
16 | * // Use in a template
17 | *
18 | * ```
19 | */
20 | const _useFeaturedImage = (contentNode: NodeWithFeaturedImage): string | undefined => {
21 | const sourceUrl = contentNode?.featuredImage?.node?.sourceUrl
22 | if (sourceUrl) return getRelativeImagePath(sourceUrl)
23 | else return undefined
24 | }
25 |
26 | export const useFeaturedImage = _useFeaturedImage
27 |
--------------------------------------------------------------------------------
/playground/app/components/PrevNext.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
21 |
22 |
23 |
29 |
30 |
31 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2024-present - WPNuxt Team
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/docs/4.advanced/1.index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Custom Post Types
3 | description:
4 | ---
5 |
6 | ## Create CPT in WordPress
7 |
8 | [WordPress documentation about post types](https://developer.wordpress.org/reference/functions/register_post_type/)
9 |
10 | In te first part of this [Tutorial by Faust](https://faustjs.org/guide/setting-up-custom-post-types-cpts-in-faust) they guide you through the steps to set up your CPT
11 |
12 | ## Check query in GraphiQl IDE
13 |
14 | ## Add the query in your project
15 |
16 | Create movies.gql and put it in /extend/queries folder in your project
17 |
18 | ```graphql
19 | query Movies($limit: Int = 10) {
20 | movies(first: $limit) {
21 | nodes {
22 | ...Movie
23 | }
24 | }
25 | }
26 | fragment Movie on Movie {
27 | ...ContentNode
28 | ...NodeWithFeaturedImage
29 | content
30 | title
31 | editorBlocks {
32 | name
33 | }
34 | }
35 | ```
36 |
37 | Based on the above infomation WPNuxt will generate:
38 | * the type 'MovieFragment'
39 | * the composable 'useMovies':
40 | * input params: { limit: integer }
41 | * return: MovieFragment[]
42 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci-main
2 |
3 | on:
4 | push:
5 | paths-ignore:
6 | - "docs/**"
7 | - "*.md"
8 | branches:
9 | - main
10 | - refactoring-claude-ai
11 | pull_request:
12 | paths-ignore:
13 | - "docs/**"
14 | - "*.md"
15 | branches:
16 | - main
17 |
18 | jobs:
19 | ci:
20 | runs-on: ubuntu-latest
21 |
22 | env:
23 | NUXT_UI_PRO_LICENSE: ${{ secrets.NUXT_UI_PRO_LICENSE }}
24 |
25 | steps:
26 | - uses: actions/checkout@v4
27 |
28 | - uses: pnpm/action-setup@v4
29 | name: Install pnpm
30 | id: pnpm-install
31 | with:
32 | run_install: false
33 |
34 | - name: Install dependencies
35 | run: pnpm install
36 |
37 | - name: Lint
38 | run: pnpm run lint
39 |
40 | - name: Type check
41 | run: pnpm run typecheck
42 |
43 | - name: Generate coverage report
44 | run: pnpm run coverage
45 |
46 | - name: Module Build
47 | run: pnpm run build
48 |
49 | - name: Playground Build
50 | run: pnpm run dev:build
51 |
--------------------------------------------------------------------------------
/playground/nuxt.config.ts:
--------------------------------------------------------------------------------
1 | export default defineNuxtConfig({
2 | modules: [
3 | '@nuxt/eslint',
4 | '@nuxt/image',
5 | '@nuxt/ui',
6 | '@nuxtjs/mdc',
7 | '../src/module'
8 | ],
9 |
10 | devtools: { enabled: process.env.NODE_ENV === 'development' },
11 |
12 | css: ['~/assets/css/main.css'],
13 |
14 | routeRules: {
15 | '/wp-content/**': { proxy: { to: 'http://localhost:4000/wp-content/**' } }
16 | },
17 |
18 | future: {
19 | compatibilityVersion: 4
20 | },
21 | compatibilityDate: '2025-12-18',
22 |
23 | // @ts-expect-error - eslint config is provided by @nuxt/eslint module
24 | eslint: {
25 | config: {
26 | stylistic: {
27 | commaDangle: 'never',
28 | braceStyle: '1tbs'
29 | }
30 | },
31 | checker: {
32 | lintOnStart: true,
33 | fix: true
34 | }
35 | },
36 |
37 | wpNuxt: {
38 | wordpressUrl: 'https://wordpress.wpnuxt.com',
39 | frontendUrl: 'https://demo.wpnuxt.com',
40 | enableCache: true,
41 | staging: false,
42 | logLevel: 3,
43 | downloadSchema: true,
44 | composablesPrefix: 'use'
45 | }
46 | })
47 |
--------------------------------------------------------------------------------
/playground/app/components/UsePosts.vue:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
18 |
19 |
26 |
31 |
32 |
33 |
34 |
35 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/playground/.env.example:
--------------------------------------------------------------------------------
1 | # WPNuxt Configuration
2 | # Copy this file to .env and update with your values
3 |
4 | # Required: WordPress GraphQL API URL (no trailing slash)
5 | WPNUXT_WORDPRESS_URL=https://your-wordpress-site.com
6 |
7 | # Optional: Frontend URL for Faust.js integration (no trailing slash)
8 | WPNUXT_FRONTEND_URL=https://your-nuxt-site.com
9 |
10 | # Optional: Default menu name to fetch
11 | # Default: main
12 | WPNUXT_DEFAULT_MENU_NAME=main
13 |
14 | # Optional: Enable/disable caching
15 | # Default: true
16 | WPNUXT_ENABLE_CACHE=true
17 |
18 | # Optional: Cache duration in seconds
19 | # Default: 300 (5 minutes)
20 | WPNUXT_CACHE_MAX_AGE=300
21 |
22 | # Optional: Enable staging mode (shows staging banner)
23 | # Default: false
24 | WPNUXT_STAGING=false
25 |
26 | # Optional: Download GraphQL schema on startup
27 | # When false, expects a committed schema.graphql file
28 | # Default: true
29 | WPNUXT_DOWNLOAD_SCHEMA=true
30 |
31 | # Optional: Logging level
32 | # 0 = silent, 1 = error, 2 = warn, 3 = info, 4 = debug, 5 = trace
33 | # Default: 3
34 | WPNUXT_LOG_LEVEL=3
35 |
36 | # Optional: Prefix for auto-generated composables
37 | # Default: useWP
38 | WPNUXT_COMPOSABLES_PREFIX=useWP
39 |
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Dependencies
2 | node_modules
3 |
4 | # Logs
5 | *.log*
6 |
7 | # Temp directories
8 | .temp
9 | .tmp
10 | .cache
11 |
12 | # Yarn
13 | **/.yarn/cache
14 | **/.yarn/*state*
15 |
16 | # Generated dirs
17 | dist
18 |
19 | # Nuxt
20 | .nuxt
21 | .output
22 | .data
23 | .vercel_build_output
24 | .build-*
25 | .netlify
26 |
27 | # Env
28 | .env
29 |
30 | # Testing
31 | reports
32 | coverage
33 | *.lcov
34 | .nyc_output
35 |
36 | # VSCode
37 | .vscode/*
38 | !.vscode/settings.json
39 | !.vscode/tasks.json
40 | !.vscode/launch.json
41 | !.vscode/extensions.json
42 | !.vscode/*.code-snippets
43 |
44 | # Intellij idea
45 | *.iml
46 | .idea
47 |
48 | # OSX
49 | .DS_Store
50 | .AppleDouble
51 | .LSOverride
52 | .AppleDB
53 | .AppleDesktop
54 | Network Trash Folder
55 | Temporary Items
56 | .apdisk
57 |
58 |
59 | playground/.vercel/
60 | wordpress/files/
61 | playground/.queries/
62 | playground/queries/
63 | playground/schema.graphql
64 | tests/fixtures/basic/.queries/
65 | tests/fixtures/basic/queries/
66 | tests/fixtures/basic/schema.graphql
67 | playground/.eslintcache
68 | docs/.eslintcache
69 | .queries
70 | schema.graphql
71 | playground/package-lock.json
72 | tests/fixtures/basic/package-lock.json
73 |
--------------------------------------------------------------------------------
/docs/0.index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'WPNuxt Core'
3 | description: What is @wpnuxt/core?
4 | icon: i-ph-package
5 | ---
6 |
7 | WPNuxt Core is a plugin for Nuxt 3 that provides a set of composables, hooks, and utilities for working with WordPress.
8 |
9 | It will render the content as is it returned by the WordPress GraphQL API.
10 |
11 | ## Gutenberg Blocks
12 |
13 | By default the GraphQL API returns the content of the page as an HTML string. When using WPNuxt Core this content is rendered as is, with the help of the [vue-sanitize-directive](https://github.com/leopiccionia/vue-sanitize-directive){target=_blank} to make sure we get clean HTML.
14 |
15 | Hovever, if you want to use and customise separate vue components to render Gutenberg blocks, you can use the [WPNuxt Blocks](/blocks) plugin.
16 |
17 |
18 | ## WPNuxt Modules + required WordPress plugin(s)
19 |
20 | | WPNuxt Module | Required WordPress Plugins |
21 | | ---------------- | ----------------------------- |
22 | | @wpnuxt/core | WPGraphQL |
23 | | @wpnuxt/blocks | WPGraphQL Content Blocks |
24 | | @wpnuxt/auth | WPGraphQL CORS |
25 |
26 |
27 | ideas for future modules:
28 |
29 | | @wpnuxt/yoastseo | Yoast SEO + Add WPGraphQL SEO |
30 | | @wpnuxt/wpml | WPML + WPML WPGraphql |
31 | | @wpnuxt/acf | ACF + WPGraphQL for ACF |
32 |
--------------------------------------------------------------------------------
/playground/app/pages/[...slug].vue:
--------------------------------------------------------------------------------
1 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/runtime/composables/useWPUri.ts:
--------------------------------------------------------------------------------
1 | import { useRuntimeConfig } from '#imports'
2 |
3 | /**
4 | * Provides WordPress admin URL helpers
5 | *
6 | * @returns Object with WordPress admin URL properties and methods
7 | *
8 | * @example
9 | * ```typescript
10 | * const wp = useWPUri()
11 | *
12 | * // Get base WordPress URL
13 | * console.log(wp.base) // 'https://wordpress.example.com'
14 | *
15 | * // Get admin URL
16 | * console.log(wp.admin) // 'https://wordpress.example.com/wp-admin'
17 | *
18 | * // Get edit URL for a specific post
19 | * console.log(wp.postEdit('123')) // 'https://wordpress.example.com/wp-admin/post.php?post=123&action=edit'
20 | * ```
21 | */
22 | const _useWPUri = () => {
23 | const config = useRuntimeConfig()
24 | const base = config.public.wpNuxt.wordpressUrl
25 | const admin = base + '/wp-admin'
26 | const pagesAdmin = base + '/wp-admin/edit.php?post_type=page'
27 | const postAdmin = base + '/wp-admin/edit.php?post_type=post'
28 | const settingsEdit = base + '/wp-admin/options-general.php'
29 | const postEdit = (path: string) => {
30 | if (path)
31 | return base + '/wp-admin/post.php?post=' + path + '&action=edit'
32 | else return postAdmin
33 | }
34 |
35 | return {
36 | base,
37 | admin,
38 | pagesAdmin,
39 | postAdmin,
40 | postEdit,
41 | settingsEdit
42 | }
43 | }
44 |
45 | export const useWPUri = _useWPUri
46 |
--------------------------------------------------------------------------------
/docs/4.advanced/3.customcomponents.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Create Block Components
3 | description: how to create your own Nuxt Components to render Gutenberg Blocks
4 | ---
5 |
6 | ::callout{type="warning"}
7 | This feature requires the [`@wpnuxt/blocks`](/blocks) module to be installed.
8 | ::
9 |
10 | By default every Gutenberg block will be rendered with the WPNuxt component EditorBlock, which is merely outputting the renderedHtml in a p tag:
11 |
12 | ``` vue
13 |
20 |
21 |
22 |
23 |
24 | ```
25 |
26 | ## Add your own components
27 |
28 | WPNuxt allows you to add a Nuxt component to render any Gutenberg block by merely creating a component with the name of the Gutenberg block
29 |
30 | Example component to render a CoreButton block using a Nuxt UI button:
31 | ``` vue
32 |
39 |
40 |
41 |
45 |
46 | ```
47 |
48 | More examples can be found in the [WPnuxt demo](https://github.com/vernaillen/wpnuxt-demo/tree/main/app/components/blocks)
49 |
--------------------------------------------------------------------------------
/playground/app/components/PostAside.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
19 |
20 |
21 | published:
22 | {{ post.date?.split('T')[0] }}
23 |
24 |
28 |
Categories:
29 |
30 | -
34 | {{ category.name }}
35 |
36 |
37 |
38 |
42 | featured image:
43 |
![]()
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/tests/utils.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest'
2 | import { validateConfig } from '../src/utils'
3 |
4 | describe('utils', () => {
5 | describe('validateConfig', () => {
6 | it('should throw error when wordpressUrl is missing', () => {
7 | expect(() => {
8 | validateConfig({ wordpressUrl: '' })
9 | }).toThrow('WordPress url is missing')
10 | })
11 |
12 | it('should throw error when wordpressUrl has trailing slash', () => {
13 | expect(() => {
14 | validateConfig({ wordpressUrl: 'https://example.com/' })
15 | }).toThrow('WordPress url should not have a trailing slash')
16 | })
17 |
18 | it('should throw error when frontendUrl has trailing slash', () => {
19 | expect(() => {
20 | validateConfig({
21 | wordpressUrl: 'https://wordpress.example.com',
22 | frontendUrl: 'https://example.com/'
23 | })
24 | }).toThrow('frontend url should not have a trailing slash')
25 | })
26 |
27 | it('should pass validation with valid config', () => {
28 | expect(() => {
29 | validateConfig({
30 | wordpressUrl: 'https://wordpress.example.com',
31 | frontendUrl: 'https://example.com'
32 | })
33 | }).not.toThrow()
34 | })
35 |
36 | it('should pass validation without frontendUrl', () => {
37 | expect(() => {
38 | validateConfig({
39 | wordpressUrl: 'https://wordpress.example.com'
40 | })
41 | }).not.toThrow()
42 | })
43 | })
44 | })
45 |
--------------------------------------------------------------------------------
/src/runtime/components/WordPressLogo.vue:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
--------------------------------------------------------------------------------
/src/runtime/composables/usePrevNextPost.ts:
--------------------------------------------------------------------------------
1 | import { OperationTypeNode } from 'graphql'
2 | import { useWPContent } from '#imports'
3 |
4 | /**
5 | * Gets the previous and next post slugs for navigation
6 | *
7 | * @param currentPostPath - The current post's path/slug (with or without slashes)
8 | * @returns Object with `prev` and `next` post slugs, or null if not available
9 | *
10 | * @example
11 | * ```typescript
12 | * // Get prev/next posts for navigation
13 | * const { prev, next } = await usePrevNextPost('/my-blog-post/')
14 | *
15 | * // Use in navigation
16 | * Previous Post
17 | * Next Post
18 | * ```
19 | */
20 | const _usePrevNextPost = async (currentPostPath: string) => {
21 | const currentPostSlug = currentPostPath.replaceAll('/', '')
22 | const allPosts = await getAllPosts()
23 | if (!allPosts) return { prev: null, next: null }
24 | const currentIndex: number = allPosts.slugs.findIndex((slug: string | null | undefined) => slug === currentPostSlug)
25 | const nextPost = currentIndex > 0 ? allPosts.slugs[currentIndex - 1] : null
26 | const prevPost = allPosts.slugs.length > (currentIndex + 1) ? allPosts.slugs[currentIndex + 1] : null
27 |
28 | return {
29 | prev: prevPost ?? null,
30 | next: nextPost ?? null
31 | }
32 | }
33 |
34 | interface Post {
35 | slug?: string
36 | }
37 |
38 | const getAllPosts = async () => {
39 | const { data: allPosts } = await useWPContent(OperationTypeNode.QUERY, 'Posts', ['posts', 'nodes'], false)
40 | if (allPosts && Array.isArray(allPosts)) {
41 | return {
42 | slugs: allPosts.map((post: Post) => {
43 | if (post) return post.slug
44 | else return null
45 | })
46 | }
47 | }
48 | return
49 | }
50 |
51 | export const usePrevNextPost = _usePrevNextPost
52 |
--------------------------------------------------------------------------------
/docs/1.getting-started/1.index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Installation
3 | description: Get started with WPNuxt by creating a new project or adding it to an existing Nuxt application.
4 | ---
5 |
6 | ## Try it online
7 |
8 | [open demo in StackBlitz](https://stackblitz.com/github/vernaillen/wpnuxt-demo)
9 |
10 | ## New Project
11 |
12 | ### Configure Wordpress
13 |
14 | First set up WordPress as a headless CMS, by installing these 3 plugins:
15 | * [WPGraphQL](https://wordpress.org/plugins/wp-graphql/)
16 | * WPEngine's [Faust.js](https://wordpress.org/plugins/faustwp/)
17 | * WPEngine's [WPGraphQL Content Blocks](https://github.com/wpengine/wp-graphql-content-blocks) (you'll need to download the latest release as a zip file from the [release page on github](https://github.com/wpengine/wp-graphql-content-blocks/releases) and install manually in wordpress)
18 |
19 | #### WordPress permalinks
20 |
21 | If you want to set up dynamic routes in Nuxt to match the WordPress slugs, then set the WordPress permalinks to postname.
22 |
23 | #### WPGraphQL settings
24 |
25 | * enable introspection
26 |
27 | #### Faust plugin
28 |
29 | * set the front-end site url
30 | * copy the secret key
31 | * depending on how you're going to handle image url's in Nuxt, you might have to enable "Use the WordPress domain for media URLs in post content"
32 |
33 | ### Add WPNuxt Core to your Nuxt app
34 |
35 | Install the module to your Nuxt application with one command:
36 |
37 | ```bash
38 | npx nuxi module add @wpnuxt/core
39 | ```
40 |
41 | ### Configure WPNuxt
42 |
43 | Connect WPNuxt to your wordpress installation in your nuxt.config.ts:
44 |
45 | ```ts
46 | wpNuxt: {
47 | wordpressUrl: 'https://wordpress.wpnuxt.com'
48 | },
49 | ```
50 | Include the protocol.
51 |
52 | When you start Nuxt after adding the WPNuxt module, it might ask whether you want to install the @nuxt/image package:
53 | ``` bash
54 | ❯ Do you want to install @nuxt/image package?
55 | ● Yes / ○ No
56 | ```
57 |
58 | More information about how to handle wordpress media: [images](./images)
59 |
--------------------------------------------------------------------------------
/playground/app/pages/test/composables.vue:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 | Examples how to use the generated composables
18 |
19 | prefix for composables: {{ prefix }}
20 |
21 | {{ prefix }}Posts()
22 |
23 |
27 | {{ post.title }}
28 |
29 |
30 | {{ prefix }}Posts({ limit: 1 })
31 |
32 |
36 | {{ post.title }}
37 |
38 |
39 | {{ prefix }}PostByUri({ uri: 'hello-world' })
40 |
41 | {{ postByUri?.title }}
42 |
43 | {{ prefix }}GeneralSettings
44 |
45 |
49 | {{ key }}: {{ value }}
50 |
51 |
52 | The email field in GeneralSettings can't be fetched without authentication.
53 | The other data (see above) is returned anyway, and an error is thrown about the mail field:
54 | {{ error }}
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/docs/4.advanced/5.api-access.md:
--------------------------------------------------------------------------------
1 | # API Access
2 |
3 | ## WordPress Security Plugins
4 |
5 | If you have a security plugin installed on your WordPress site (such as Wordfence), access to the `/graphql` endpoint may be blocked by default. This will prevent WPNuxt from fetching content from your WordPress installation.
6 |
7 | ## Troubleshooting Blocked GraphQL Access
8 |
9 | ### Symptoms
10 |
11 | You may see 403 Forbidden errors in your logs or when attempting to access content:
12 |
13 | ```bash
14 | x.x.x.x - - [07/Apr/2025:10:52:09 +0200] "POST /graphql HTTP/1.1" 403 17004 "-" "node"
15 | ```
16 |
17 | ### Solution for Wordfence
18 |
19 | If you're using Wordfence, follow these steps:
20 |
21 | 1. **Enable Learning Mode**
22 |
23 | Put Wordfence in Learning Mode to allow it to observe legitimate traffic patterns:
24 | - Navigate to the Wordfence settings
25 | - Enable [Learning Mode](https://www.wordfence.com/help/firewall/learning-mode/)
26 |
27 | 2. **Whitelist GraphQL Requests**
28 |
29 | - Go to Wordfence Live Traffic view: `https://your-wordpress-site.com/wp-admin/admin.php?page=WordfenceTools`
30 | - Find the blocked calls to `/graphql` in the traffic log
31 | - Click "Add param to firewall allowlist" for each blocked GraphQL request
32 |
33 | 3. **Alternative: Whitelist by IP Address**
34 |
35 | If you know the IP address of your Nuxt application server, you can whitelist it directly in Wordfence settings.
36 |
37 | ### Other Security Plugins
38 |
39 | For other security plugins (e.g., Sucuri, iThemes Security, All In One WP Security), consult their documentation to:
40 |
41 | - Whitelist the `/graphql` endpoint
42 | - Allow POST requests to `/graphql`
43 | - Whitelist your application server's IP address
44 |
45 | ## Testing GraphQL Access
46 |
47 | You can verify that GraphQL is accessible by running:
48 |
49 | ```bash
50 | curl -X POST https://your-wordpress-site.com/graphql \
51 | -H "Content-Type: application/json" \
52 | -d '{"query": "{ generalSettings { title } }"}'
53 | ```
54 |
55 | A successful response should return your site's general settings.
56 |
--------------------------------------------------------------------------------
/src/runtime/types.d.ts:
--------------------------------------------------------------------------------
1 | // Type declarations for auto-generated modules
2 | declare module '#graphql-operations' {
3 | export * from '#build/graphql-operations'
4 | }
5 |
6 | declare module '#build/graphql-operations' {
7 | // These types are auto-generated by nuxt-graphql-middleware
8 | export interface NodeByUriQueryVariables {
9 | uri?: string
10 | }
11 |
12 | export interface PostsQueryVariables {
13 | first?: number
14 | after?: string
15 | }
16 |
17 | // Fragment types
18 | export interface MenuItemFragment {
19 | id?: string
20 | label?: string
21 | url?: string
22 | }
23 |
24 | export interface PostFragment {
25 | id?: string
26 | title?: string
27 | content?: string
28 | slug?: string
29 | uri?: string
30 | date?: string
31 | excerpt?: string
32 | contentTypeName?: string
33 | categories?: {
34 | nodes?: Array<{ name?: string }>
35 | }
36 | featuredImage?: {
37 | node?: {
38 | sourceUrl?: string
39 | altText?: string
40 | }
41 | }
42 | }
43 |
44 | export interface PageFragment {
45 | id?: string
46 | title?: string
47 | content?: string
48 | slug?: string
49 | uri?: string
50 | date?: string
51 | contentTypeName?: string
52 | featuredImage?: {
53 | node?: {
54 | sourceUrl?: string
55 | altText?: string
56 | }
57 | }
58 | }
59 |
60 | export interface NodeWithContentEditorFragment {
61 | id?: string
62 | content?: string
63 | }
64 |
65 | export interface NodeWithEditorBlocksFragment {
66 | id?: string
67 | editorBlocks?: unknown[]
68 | }
69 |
70 | export interface NodeWithFeaturedImage {
71 | id?: string
72 | featuredImage?: {
73 | node?: {
74 | sourceUrl?: string
75 | }
76 | }
77 | }
78 |
79 | // Add more as needed by the actual GraphQL schema
80 | export type Maybe = T | null
81 | }
82 |
83 | declare module '#wpnuxt/blocks' {
84 | // Placeholder for blocks module types
85 | export const CoreButton: unknown
86 | export const BlockRenderer: unknown
87 | }
88 |
--------------------------------------------------------------------------------
/playground/app/components/HeaderComponent.vue:
--------------------------------------------------------------------------------
1 |
57 |
58 |
59 |
60 |
64 |
65 | playground
66 |
67 |
68 |
69 |
70 |
71 |
76 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/src/types.d.ts:
--------------------------------------------------------------------------------
1 | import type { OperationTypeNode } from 'graphql'
2 |
3 | export interface WPNuxtConfig {
4 |
5 | /**
6 | * Log level for the WPNuxt module
7 | * @default 3
8 | * @example 0 = silent, 1 = error, 2 = warn, 3 = info, 4 = debug, 5 = trace
9 | */
10 | logLevel?: number
11 | /**
12 | * URL of the WordPress site
13 | *
14 | * There is no default value for this option, so it's required.
15 | *
16 | * @example 'https://wordpress.wpnuxt.com'
17 | */
18 | wordpressUrl: string
19 | frontendUrl: string
20 |
21 | defaultMenuName?: string
22 |
23 | enableCache?: boolean
24 |
25 | /**
26 | * Cache duration in seconds for WordPress content
27 | *
28 | * @default 300 (5 minutes)
29 | */
30 | cacheMaxAge?: number
31 |
32 | staging?: boolean
33 |
34 | /**
35 | * @default 'useWP'
36 | */
37 | composablesPrefix: string
38 |
39 | queries?: WPNuxtConfigQueries
40 |
41 | /**
42 | * Download the GraphQL schema and store it on disk.
43 | *
44 | * When setting this to false, the module will expect a graphql.schema file to be available.
45 | * You could first enable this, commit the schema file and then set downloadSchema to false to avoid have to query the graphql introspection on each start of the application
46 | *
47 | * @default true
48 | */
49 | downloadSchema?: boolean
50 |
51 | hasBlocksModule?: boolean
52 | hasAuthModule?: boolean
53 | }
54 |
55 | export interface WPNuxtConfigQueries {
56 |
57 | /**
58 | * Folder for user defined queries
59 | *
60 | * relative to the src dir of your nuxt app
61 | *
62 | * @default extend/queries
63 | */
64 | extendDir?: string
65 |
66 | /**
67 | * The predefined queries & the user defined queries will be merged and placed in the queries output folder
68 | *
69 | * relative to the src dir of your nuxt app
70 | *
71 | * @default queries
72 | */
73 | outputDir?: string
74 | }
75 |
76 | export type WPNuxtQuery = {
77 | name: string
78 | nodes?: string[]
79 | fragments: string[]
80 | params: Record
81 | operation: OperationTypeNode
82 | }
83 |
--------------------------------------------------------------------------------
/src/useParser.ts:
--------------------------------------------------------------------------------
1 | import { parse } from 'graphql'
2 | import type { SelectionNode, OperationDefinitionNode } from 'graphql'
3 | import type { WPNuxtQuery } from './types'
4 |
5 | /**
6 | * Parses a GraphQL document string and extracts query operations
7 | *
8 | * @param doc - The GraphQL document string to parse
9 | * @returns Array of parsed query operations with their metadata
10 | * @throws Error if an operation is missing a name
11 | */
12 | const _parseDoc = async (doc: string): Promise => {
13 | const { definitions } = parse(doc)
14 |
15 | const operations: WPNuxtQuery[] = definitions
16 | .filter(({ kind }) => kind === 'OperationDefinition')
17 | .map((definition) => {
18 | const operationDefinition = definition as OperationDefinitionNode
19 | if (!operationDefinition.name?.value) {
20 | throw new Error(`Operation name missing in: ${doc}`)
21 | }
22 |
23 | const query: WPNuxtQuery = {
24 | name: operationDefinition.name.value.trim(),
25 | nodes: [],
26 | fragments: [],
27 | params: {},
28 | operation: operationDefinition.operation
29 | }
30 | processSelections(operationDefinition.selectionSet.selections, 0, query)
31 | return query
32 | })
33 | return operations
34 | }
35 |
36 | /**
37 | * Recursively processes GraphQL selection sets to extract nodes and fragments
38 | *
39 | * @param selections - The GraphQL selection nodes to process
40 | * @param level - Current depth level in the selection tree
41 | * @param query - The query object to populate with extracted data
42 | */
43 | function processSelections(selections: readonly SelectionNode[], level: number, query: WPNuxtQuery): void {
44 | if (!selections || selections.length === 0) return
45 | if (selections.length === 1 && selections[0]?.kind === 'Field') {
46 | query.nodes?.push(selections[0].name.value.trim())
47 | }
48 | selections.forEach((s) => {
49 | if (s.kind === 'FragmentSpread') {
50 | query.fragments?.push(s.name.value.trim())
51 | } else if (s.selectionSet?.selections) {
52 | processSelections(s.selectionSet.selections, level + 1, query)
53 | }
54 | })
55 | }
56 |
57 | export const parseDoc = _parseDoc
58 |
--------------------------------------------------------------------------------
/docs/1.getting-started/2.configuration.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Configuration
3 | description:
4 | ---
5 |
6 | ```ts
7 | wpNuxt: {
8 | wordpressUrl: 'https://wordpress.wpnuxt.com',
9 | frontendUrl: 'https://demo.wpnuxt.com',
10 | composablesPrefix: 'useWP',
11 | defaultMenuName: 'main',
12 | enableCache: true,
13 | cacheMaxAge: 300,
14 | downloadSchema: true,
15 | logLevel: 3
16 | },
17 | ```
18 |
19 | ## wordpressUrl
20 |
21 | **Type:** `string` (required)
22 |
23 | URL of the WordPress site
24 |
25 | There is no default value for this option, so it's always required.
26 |
27 | ## frontendUrl
28 |
29 | **Type:** `string` (optional)
30 |
31 | URL of the Nuxt app
32 |
33 | ## composablesPrefix
34 |
35 | **Type:** `string`
36 | **Default:** `'useWP'`
37 |
38 | Prefix for auto-generated composables.
39 |
40 | ## defaultMenuName
41 |
42 | **Type:** `string`
43 | **Default:** `'main'`
44 |
45 | The default menu name to fetch when no menu name is specified in `useWPMenu()`.
46 |
47 | ## enableCache
48 |
49 | **Type:** `boolean`
50 | **Default:** `true`
51 |
52 | Enable or disable caching for GraphQL queries. WPNuxt uses a two-layer caching strategy:
53 | - In-memory LRU cache (L1) for hot queries
54 | - HTTP cache layer (L2) with SWR (stale-while-revalidate)
55 |
56 | See [Performance Optimization](/advanced/performance) for more details.
57 |
58 | ## cacheMaxAge
59 |
60 | **Type:** `number`
61 | **Default:** `300` (5 minutes)
62 |
63 | Cache duration in seconds for the HTTP cache layer. This setting determines how long responses are cached before being revalidated.
64 |
65 | Can also be set via environment variable:
66 | ```bash
67 | WPNUXT_CACHE_MAX_AGE=600
68 | ```
69 |
70 | ## downloadSchema
71 |
72 | **Type:** `boolean`
73 | **Default:** `true`
74 |
75 | Downloads the GraphQL schema and stores it on disk.
76 |
77 | When setting this to false, the module will expect a `graphql.schema` file to be available.
78 |
79 | You could first enable this, commit the schema file and then set `downloadSchema` to `false` to avoid having to do GraphQL introspection on each start of the application. This speeds up the build time of the application.
80 |
81 | ## logLevel
82 |
83 | **Type:** `number`
84 | **Default:** `3`
85 |
86 | Controls logging verbosity. Higher values show more detailed logs.
87 |
88 |
--------------------------------------------------------------------------------
/docs/2.composables/1.posts.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Posts
3 | description: Generated composables to fetch Posts and their content
4 | ---
5 |
6 | WPNuxt provides 5 GraphQL queries to fetch posts ([src](https://github.com/wpnuxt/wpnuxt-core/blob/main/src/runtime/queries/Posts.gql){target=_blank}):
7 |
8 | ::code-group
9 | ```graphql [GraphQL queries]
10 | #import "~/.queries/fragments/Post.fragment.gql"
11 |
12 | query Posts($limit: Int = 10) {
13 | posts(first: $limit) {
14 | nodes {
15 | ...Post
16 | }
17 | }
18 | }
19 | query PostByUri($uri: String!) {
20 | nodeByUri(uri: $uri) {
21 | ...Post
22 | }
23 | }
24 | query PostById($id: ID!, $asPreview: Boolean = false) {
25 | post(id: $id, idType: DATABASE_ID, asPreview: $asPreview) {
26 | ...Post
27 | }
28 | }
29 | query PostsByCategoryName($categoryName: String!, $limit: Int = 10) {
30 | posts(first: $limit, where: {categoryName: $categoryName}) {
31 | nodes {
32 | ...Post
33 | }
34 | }
35 | }
36 | query PostsByCategoryId($categoryId: Int!, $limit: Int = 10) {
37 | posts(first: $limit, where: {categoryId: $categoryId}) {
38 | nodes {
39 | ...Post
40 | }
41 | }
42 | }
43 | ```
44 |
45 | ```graphql [GraphQL fragment]
46 | #import "~/.queries/fragments/NodeWithExcerpt.fragment.gql";
47 | #import '~/.queries/fragments/ContentNode.fragment.gql';
48 | #import '~/.queries/fragments/NodeWithFeaturedImage.fragment.gql';
49 | #import '~/.queries/fragments/NodeWithContentEditor.fragment.gql';
50 |
51 | fragment Post on Post {
52 | ...NodeWithExcerpt
53 | ...ContentNode
54 | ...NodeWithFeaturedImage
55 | ...NodeWithContentEditor
56 | title
57 | categories {
58 | nodes {
59 | id
60 | name
61 | uri
62 | description
63 | }
64 | }
65 | }
66 | ```
67 | ::
68 |
69 | Which result in these generated PostFragment type and 5 composables:
70 |
71 | ```ts twoslash
72 | const { data: allPosts } = await useWPPosts()
73 | const { data: latestPosts } = await useWPPosts({ limit: 3 })
74 | const { data: post1 } = await useWPPostByUri({ uri: 'slug' })
75 | const { data: post2 } = await useWPPostById({ id: 'databaseId' })
76 | const { data: categoryPosts1 } = await useWPPostsByCategoryName({ categoryName: 'news' })
77 | const { data: categoryPosts2 } = await useWPPostsByCategoryId({ categoryId: 1 })
78 | ```
79 |
--------------------------------------------------------------------------------
/playground/app/types.d.ts:
--------------------------------------------------------------------------------
1 | // Type declarations for auto-generated modules
2 | interface WPNuxtAsyncData {
3 | data?: T
4 | error?: unknown
5 | }
6 |
7 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
8 | type AnyData = any
9 |
10 | declare module '#wpnuxt' {
11 | export const useMenu: (params?: Record) => Promise>
12 | export const usePosts: (params?: Record) => Promise>
13 | export const usePostByUri: (params: Record) => Promise>
14 | export const usePostsByCategoryName: (params: Record) => Promise>
15 | export const useGeneralSettings: (params?: Record) => Promise>
16 | }
17 |
18 | declare module '#wpnuxt/blocks' {
19 | export const CoreButton: unknown
20 | export const BlockRenderer: unknown
21 | }
22 |
23 | declare module '#build/graphql-operations' {
24 | export * from '../../src/runtime/types'
25 | }
26 |
27 | declare module '#graphql-operations' {
28 | export * from '#build/graphql-operations'
29 | }
30 |
31 | // Extend #imports to include WPNuxt composables
32 | declare module '#imports' {
33 | export * from '#app'
34 | export * from 'vue'
35 | export * from '@unhead/vue'
36 |
37 | // WPNuxt composables
38 | export const isStaging: () => boolean
39 | export const useWPContent: (
40 | operation: unknown,
41 | queryName: string,
42 | nodes: string[],
43 | fixImagePaths: boolean,
44 | params?: T
45 | ) => Promise<{ data?: T, error?: unknown }>
46 | export const parseDoc: (content: string) => unknown
47 | export const useNodeByUri: (params: Record) => Promise>
48 | export const usePostByUri: (params: Record) => Promise>
49 | export const usePrevNextPost: (uri: string) => Promise<{ prev: string | null, next: string | null }>
50 | export const useWPUri: () => {
51 | admin: string
52 | postEdit: (id: string) => string
53 | }
54 | export const loginUser: (username: string, password: string) => Promise
55 | export const logoutUser: () => Promise
56 | export const getCurrentUserId: () => Promise
57 | export const getCurrentUserName: () => Promise
58 | export const useTokens: () => unknown
59 | export const useFeaturedImage: (node: unknown) => unknown
60 | }
61 |
--------------------------------------------------------------------------------
/wordpress/wp-env-cli.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | echo "-------------------- executing wpInstall script --------------------";
3 |
4 | #wp-env run cli update --yes;
5 | #wp-env run cli wp core update;
6 | cli_version=$(wp-env run cli wp --version);
7 | echo "WordPress cli version: $cli_version";
8 | wp_version=$(wp-env run cli wp core version);
9 | echo "WordPress version: $wp_version";
10 |
11 | if [ $(wp-env run cli wp option list --search=wpnuxt_installed --format=count) == 1 ]; then
12 | echo "WPNuxt is already installed.";
13 | else
14 | wp-env run cli wp option update blogname "WPNuxt Demo";
15 |
16 | wp-env run cli wp option add graphql_general_settings {} --format=json;
17 | wp-env run cli wp option patch insert graphql_general_settings public_introspection_enabled on;
18 | wp-env run cli wp option patch insert graphql_general_settings show_graphiql_link_in_admin_bar off;
19 |
20 | wp-env run cli wp option patch insert faustwp_settings frontend_uri "http://localhost:3000";
21 | wp-env run cli wp option patch insert faustwp_settings disable_theme 1;
22 | wp-env run cli wp option patch insert faustwp_settings enable_redirects 1;
23 | wp-env run cli wp option patch insert faustwp_settings enable_rewrites 1;
24 | wp-env run cli wp option patch insert faustwp_settings enable_telemetry 1;
25 | wp-env run cli wp option patch insert faustwp_settings enable_image_source 1;
26 | wp-env run cli wp option patch insert faustwp_settings secret_key 64489e3c-1166-498a-9a6e-51cbb4c14ab2;
27 |
28 | wp-env run cli wp theme delete --all;
29 |
30 | wp-env run cli wp menu create main;
31 | wp-env run cli wp menu item add-post main $(wp-env run cli wp post list --post_type=page --field="ID" --name="sample-page") --title="Sample Page";
32 | test_post_id="$(wp-env run cli wp post create ./demo-content/testpage.txt --post_type=page --post_title="Test Page" --post_status=publish --porcelain)";
33 | wp-env run cli wp menu item add-post main $test_post_id --title="Test Page";
34 | wp-env run cli wp menu location assign main primary;
35 | wp-env run cli wp post create ./demo-content/testpost.txt --post_type=post --post_title="Test Post" --post_status=publish
36 | wp-env run cli wp post create ./demo-content/newmodule.txt --post_type=post --post_title="A new Nuxt module" --post_status=publish
37 |
38 | wp-env run cli wp rewrite flush;
39 |
40 | wp-env run cli wp option add wpnuxt_installed 1;
41 |
42 | echo "----------------- WPNuxt installed successfully: --------------------";
43 | fi
44 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | This module is still being developed and not ready for production usage yet.
3 | There will be many smaller alpha releases the coming weeks, often with breaking changes.
4 |
5 | I am working towards a stable release. And will inform about it here
6 |
7 | # WPNuxt
8 |
9 | [![npm version][npm-version-src]][npm-version-href]
10 | [![npm downloads][npm-downloads-src]][npm-downloads-href]
11 | [![License][license-src]][license-href]
12 | [![Nuxt][nuxt-src]][nuxt-href]
13 |
14 | Nuxt module for using WordPress as a headless CMS with a Nuxt 3 frontend
15 |
16 | - [✨ Release Notes](/CHANGELOG.md)
17 | - [🏀 Online playground](https://stackblitz.com/github/wpnuxt/wpnuxt-core?file=playground%2Fapp%2Fpages%2Findex.vue)
18 | - [📖 Documentation](https://wpnuxt.com)
19 |
20 | ## Features
21 |
22 | - Content is fetched from WordPress using server-side GraphQL api calls
23 | - Support for (Gutenberg Blocks) by adding the separate [@wpnuxt/blocks](https://github.com/wpnuxt/wpnuxt-blocks), which uses WPEngine's [wp-graphql-content-blocks](https://faustjs.org/tutorial/get-started-with-wp-graphql-content-blocks) and a set of custom vue components
24 |
25 | ## Quick Setup
26 |
27 | Install the module to your Nuxt application with one command:
28 |
29 | ```bash
30 | npx nuxi module add @wpnuxt/core
31 | ```
32 |
33 | And connect WPNuxt to your wordpress installation in your nuxt.config.ts:
34 |
35 | ```json
36 | wpNuxt: {
37 | wordpressUrl: 'https://yourwordpress.domain.com'
38 | },
39 | ```
40 |
41 | That's it! You can now use the WPNuxt module in your Nuxt app ✨
42 |
43 | ## Development
44 |
45 | ```bash
46 | # Install dependencies
47 | pnpm install
48 |
49 | # Generate type stubs
50 | pnpm run dev:prepare
51 |
52 | # Develop with the playground
53 | pnpm run dev
54 |
55 | # Build the playground
56 | pnpm run dev:build
57 |
58 | # Run ESLint
59 | pnpm run lint
60 |
61 | # Run Vitest
62 | pnpm run test
63 | pnpm run test:watch
64 |
65 | # Release new version
66 | pnpm run release
67 | ```
68 |
69 |
70 | [npm-version-src]: https://img.shields.io/npm/v/@wpnuxt/core/latest.svg?style=flat&colorA=18181B&colorB=28CF8D
71 | [npm-version-href]: https://www.npmjs.com/package/@wpnuxt/core
72 |
73 | [npm-downloads-src]: https://img.shields.io/npm/dm/@wpnuxt/core.svg?style=flat&colorA=18181B&colorB=28CF8D
74 | [npm-downloads-href]: https://www.npmjs.com/package/@wpnuxt/core
75 |
76 | [license-src]: https://img.shields.io/npm/l/@wpnuxt/core?style=flat&colorA=18181B&colorB=28CF8D
77 | [license-href]: https://www.npmjs.com/package/@wpnuxt/core
78 |
79 | [nuxt-src]: https://img.shields.io/badge/Nuxt-18181B?logo=nuxt.js
80 | [nuxt-href]: https://nuxt.com
81 |
--------------------------------------------------------------------------------
/docs/index.yml:
--------------------------------------------------------------------------------
1 | navigation: false
2 | title: WPNuxt
3 | description: Use WordPress as a headless CMS with a Nuxt 3 frontend
4 | hero:
5 | badges:
6 | - label: this Nuxt module is still in alpha stage
7 | variant: outline
8 | icon: i-ph-warning
9 | size: xs
10 | trailing: true
11 | to: '/getting-started/installation'
12 | title: 'WPNuxt
Nuxt 3 + headless WordPress'
13 | description: 'Easily fetch content from WordPress using composables and render it with Nuxt 3 components.
Supports Gutenberg blocks, allowing you to provide custom components for each block type.'
14 | button: Get started
15 | sections:
16 | - title: combine the ease of use of WordPress as a CMS
with a cutting edge Nuxt frontend
17 | slot: features
18 | toolsCards:
19 | - title: 'Headless CMS'
20 | description: 'Fetch content from WordPress in your Vue components with powerful composables.'
21 | icon: 'i-ph-files'
22 | to: ''
23 | - title: 'Render Gutenberg blocks as Vue components'
24 | description: 'Create custom components to render specific types of WordPress Gutenberg Blocks'
25 | icon: 'i-ph-funnel'
26 | to: ''
27 | - title: 'Server-side api calls'
28 | description: "WordPress content is fetched on the server using GraphQL api calls and cached on the server"
29 | icon: 'i-ant-design-api-outlined'
30 | to: ''
31 | - title: 'Easy to use
Composables'
32 | description: 'Fetch posts, pages, menus or settings from WordPress with simple composables:
usePosts()
usePages()
useMenu({ name: "main" })
useGeneralSettings()'
33 | class: 'dark bg-gray-900'
34 | align: left
35 | links:
36 | - label: 'Available composables'
37 | icon: i-ph-layout-duotone
38 | to: '/getting-started/composables'
39 | color: black
40 | size: md
41 | - label: 'Source code'
42 | icon: i-ph-app-window-duotone
43 | to: 'https://github.com/wpnuxt/wpnuxt-core/tree/main/src/runtime/composables'
44 | color: gray
45 | size: md
46 | slot: code
47 | code: |
48 | ```vue [posts.vue]
49 |
52 |
53 |
54 |
55 |
56 | {{ post.title }}
57 |
58 |
59 | {{ post.excerpt }}
60 |
61 |
62 | ```
63 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@wpnuxt/core",
3 | "version": "1.0.0-edge.31",
4 | "description": "WPNuxt",
5 | "repository": {
6 | "type": "git",
7 | "url": "git+https://github.com/wpnuxt/wpnuxt-core.git"
8 | },
9 | "license": "MIT",
10 | "type": "module",
11 | "exports": {
12 | ".": {
13 | "types": "./dist/types.d.mts",
14 | "import": "./dist/module.mjs",
15 | "require": "./dist/module.mjs"
16 | }
17 | },
18 | "main": "./dist/module.mjs",
19 | "files": [
20 | "dist"
21 | ],
22 | "publishConfig": {
23 | "access": "public"
24 | },
25 | "scripts": {
26 | "build": "nuxt-module-build build && pnpm build:web-types",
27 | "build:web-types": "vue-docgen-web-types src/runtime/components/ ./dist/web-types.json",
28 | "prepare": "nuxi prepare .",
29 | "prepack": "nuxt-module-build build",
30 | "generate": "pnpm --filter ./playground/ run generate",
31 | "dev": "pnpm --filter ./playground/ run dev --host app.local",
32 | "dev:build": "pnpm --filter ./playground/ run build",
33 | "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
34 | "lint": "eslint .",
35 | "lint:fix": "eslint . --fix",
36 | "lint:docs": "markdownlint ./docs && case-police 'docs/**/*.md' *.md",
37 | "lint:docs:fix": "markdownlint ./docs --fix && case-police 'docs/**/*.md' *.md --fix",
38 | "test": "vitest run",
39 | "test:watch": "vitest watch",
40 | "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit",
41 | "coverage": "vitest run --coverage",
42 | "typecheck": "vue-tsc --noEmit",
43 | "typecheck:docs": "DOCS_TYPECHECK=true pnpm nuxi prepare && nuxt-content-twoslash verify --content-dir docs",
44 | "release": "pnpm run lint && pnpm run prepack; release-it --git.tagExclude='*[-edge]*'",
45 | "release-edge": "pnpm run lint && pnpm run prepack; release-it --preRelease=edge --config .release-it-edge.json",
46 | "wp:cli": "docker-compose up wordpress-cli",
47 | "wp:stop": "docker-compose down",
48 | "wp:delete": "docker-compose down --volumes --remove-orphans --rmi local && rm -rf ./wordpress/files",
49 | "wp-env": "wp-env",
50 | "wp-env:create": "wp-env start --update && ./wordpress/wp-env-cli.sh",
51 | "start": "pnpm run wp-env:create && pnpm run dev"
52 | },
53 | "dependencies": {
54 | "@nuxt/kit": "4.2.2",
55 | "@vueuse/nuxt": "^14.1.0",
56 | "consola": "^3.4.2",
57 | "defu": "^6.1.4",
58 | "graphql": "^16.12.0",
59 | "knitwork": "^1.3.0",
60 | "lru-cache": "^11.2.4",
61 | "nuxt-graphql-middleware": "5.2.3",
62 | "pathe": "^2.0.3",
63 | "scule": "^1.3.0",
64 | "vue-sanitize-directive": "^0.2.1"
65 | },
66 | "devDependencies": {
67 | "@nuxt/devtools": "^3.1.1",
68 | "@nuxt/eslint-config": "^1.12.1",
69 | "@nuxt/module-builder": "^1.0.2",
70 | "@nuxt/schema": "4.2.2",
71 | "@nuxt/test-utils": "^3.21.0",
72 | "@rollup/rollup-linux-arm64-gnu": "^4.53.5",
73 | "@rollup/rollup-linux-arm64-musl": "^4.53.5",
74 | "@types/node": "25.0.3",
75 | "@vitest/coverage-v8": "^4.0.16",
76 | "@vue/test-utils": "^2.4.6",
77 | "@wordpress/env": "^10.36.0",
78 | "changelogen": "^0.6.2",
79 | "markdownlint-cli": "^0.47.0",
80 | "nuxt": "^4.2.2",
81 | "nuxt-content-twoslash": "^0.1.2",
82 | "release-it": "^19.1.0",
83 | "typescript": "^5.9.3",
84 | "untyped": "2.0.0",
85 | "vite": "^7.3.0",
86 | "vitest": "^4.0.16",
87 | "vue-docgen-web-types": "^0.1.8",
88 | "vue-tsc": "3.1.8"
89 | },
90 | "engines": {
91 | "node": ">=20"
92 | },
93 | "packageManager": "pnpm@10.18.0+sha512.e804f889f1cecc40d572db084eec3e4881739f8dec69c0ff10d2d1beff9a4e309383ba27b5b750059d7f4c149535b6cd0d2cb1ed3aeb739239a4284a68f40cfa"
94 | }
95 |
--------------------------------------------------------------------------------
/tests/composables/useWPContent.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect, vi, beforeEach } from 'vitest'
2 | import { useWPContent } from '../../src/runtime/composables/useWPContent'
3 | import { OperationTypeNode } from 'graphql'
4 |
5 | // Mock $fetch
6 | global.$fetch = vi.fn() as unknown as typeof $fetch
7 |
8 | // Mock useRuntimeConfig
9 | vi.mock('#imports', () => ({
10 | useRuntimeConfig: () => ({
11 | public: {
12 | wpNuxt: {
13 | wordpressUrl: 'http://localhost:4000'
14 | }
15 | }
16 | })
17 | }))
18 |
19 | describe('useWPContent', () => {
20 | beforeEach(() => {
21 | vi.clearAllMocks()
22 | vi.spyOn(console, 'error').mockImplementation(() => {})
23 | })
24 |
25 | it('should fetch content successfully', async () => {
26 | const mockData = { title: 'Test Post' }
27 | vi.mocked($fetch).mockResolvedValue({
28 | data: mockData,
29 | error: null
30 | })
31 |
32 | const result = await useWPContent(OperationTypeNode.QUERY, 'TestQuery', [], false, {})
33 |
34 | expect(result.data).toEqual(mockData)
35 | expect(result.error).toBeNull()
36 | expect($fetch).toHaveBeenCalledWith('/api/_wpnuxt/content', {
37 | method: 'POST',
38 | body: {
39 | operation: 'query',
40 | queryName: 'TestQuery',
41 | params: {}
42 | }
43 | })
44 | })
45 |
46 | it('should handle fetch errors', async () => {
47 | const mockError = new Error('Network error')
48 | vi.mocked($fetch).mockRejectedValue(mockError)
49 |
50 | const result = await useWPContent(OperationTypeNode.QUERY, 'TestQuery', [], false, {})
51 |
52 | expect(result.data).toBeUndefined()
53 | expect(result.error).toBeDefined()
54 | expect(console.error).toHaveBeenCalled()
55 | })
56 |
57 | it('should handle GraphQL errors', async () => {
58 | const mockError = { message: 'GraphQL error' }
59 | vi.mocked($fetch).mockResolvedValue({
60 | data: null,
61 | error: mockError
62 | })
63 |
64 | const result = await useWPContent(OperationTypeNode.QUERY, 'TestQuery', [], false, {})
65 |
66 | expect(result.data).toBeUndefined()
67 | expect(result.error).toEqual(mockError)
68 | expect(console.error).toHaveBeenCalled()
69 | })
70 |
71 | it('should traverse nodes in response', async () => {
72 | const mockData = {
73 | posts: {
74 | nodes: [
75 | { id: '1', title: 'Post 1' },
76 | { id: '2', title: 'Post 2' }
77 | ]
78 | }
79 | }
80 | vi.mocked($fetch).mockResolvedValue({
81 | data: mockData,
82 | error: null
83 | })
84 |
85 | const result = await useWPContent(OperationTypeNode.QUERY, 'TestQuery', ['posts', 'nodes'], false, {})
86 |
87 | expect(result.data).toEqual(mockData.posts.nodes)
88 | })
89 |
90 | it('should handle missing nodes gracefully', async () => {
91 | const mockData = { posts: {} }
92 | vi.mocked($fetch).mockResolvedValue({
93 | data: mockData,
94 | error: null
95 | })
96 |
97 | const result = await useWPContent(OperationTypeNode.QUERY, 'TestQuery', ['posts', 'nodes'], false, {})
98 |
99 | // Should return posts object when nodes doesn't exist
100 | expect(result.data).toEqual(mockData.posts)
101 | })
102 |
103 | it('should pass params correctly', async () => {
104 | const params = { slug: 'test-post' }
105 | vi.mocked($fetch).mockResolvedValue({
106 | data: { title: 'Test' },
107 | error: null
108 | })
109 |
110 | await useWPContent(OperationTypeNode.QUERY, 'GetPostBySlug', [], false, params)
111 |
112 | expect($fetch).toHaveBeenCalledWith('/api/_wpnuxt/content', {
113 | method: 'POST',
114 | body: {
115 | operation: 'query',
116 | queryName: 'GetPostBySlug',
117 | params
118 | }
119 | })
120 | })
121 | })
122 |
--------------------------------------------------------------------------------
/src/context.ts:
--------------------------------------------------------------------------------
1 | import { promises as fsp } from 'node:fs'
2 | import { upperFirst } from 'scule'
3 | import type { Import } from 'unimport'
4 | import type { WPNuxtQuery } from './types'
5 | import { getLogger } from './utils'
6 | import { parseDoc } from './useParser'
7 |
8 | export interface WPNuxtContext {
9 | composablesPrefix: string
10 | template?: string
11 | fns: WPNuxtQuery[]
12 | fnImports?: Import[]
13 | generateImports?: () => string
14 | generateDeclarations?: () => string
15 | docs?: string[]
16 | }
17 |
18 | /**
19 | * Prepares the WPNuxt context by generating composables from GraphQL documents
20 | *
21 | * @param ctx - The WPNuxt context object
22 | */
23 | export async function prepareContext(ctx: WPNuxtContext): Promise {
24 | const logger = getLogger()
25 | if (ctx.docs) {
26 | await prepareFunctions(ctx)
27 | }
28 |
29 | const fnName = (fn: string) => ctx.composablesPrefix + upperFirst(fn)
30 | const fnExp = (q: WPNuxtQuery, typed = false) => {
31 | const functionName = fnName(q.name)
32 | if (!typed) {
33 | return `export const ${functionName} = (params) => useWPContent('${q.operation}', '${q.name}', [${q.nodes?.map(n => `'${n}'`).join(',')}], false, params)`
34 | }
35 | const fragmentSuffix = q.fragments?.length && q.nodes?.includes('nodes') ? '[]' : ''
36 | const fragments = q.fragments?.length ? q.fragments.map(f => `${f}Fragment${fragmentSuffix}`).join(' | ') : 'any'
37 | return ` export const ${functionName}: (params?: ${q.name}QueryVariables) => AsyncData<${fragments}, FetchError | null | undefined>`
38 | }
39 |
40 | ctx.generateImports = () => {
41 | const parts: string[] = ['import { useWPContent } from \'#imports\'', '']
42 | for (const fn of ctx.fns) {
43 | parts.push(fnExp(fn))
44 | }
45 | return parts.join('\n')
46 | }
47 |
48 | const types: string[] = []
49 | for (const fn of ctx.fns) {
50 | types.push(...getQueryTypeTemplate(fn))
51 | }
52 |
53 | ctx.generateDeclarations = () => {
54 | const declarations: string[] = [
55 | `import type { ${[...new Set(types)].join(', ')} } from '#build/graphql-operations'`,
56 | 'import { AsyncData } from \'nuxt/app\'',
57 | 'import { FetchError } from \'ofetch\'',
58 | 'declare module \'#wpnuxt\' {'
59 | ]
60 |
61 | for (const fn of ctx.fns) {
62 | declarations.push(fnExp(fn, true))
63 | }
64 |
65 | declarations.push('}')
66 | return declarations.join('\n')
67 | }
68 |
69 | ctx.fnImports = ctx.fns.map((fn): Import => ({ from: '#wpnuxt', name: fnName(fn.name) }))
70 |
71 | if (logger) {
72 | logger.debug('generated WPNuxt composables: ')
73 | for (const fn of ctx.fns) {
74 | logger.debug(` ${fnName(fn.name)}()`)
75 | }
76 | }
77 | }
78 |
79 | /**
80 | * Extracts TypeScript type names from a GraphQL query
81 | *
82 | * @param q - The GraphQL query object
83 | * @returns Array of type names to import
84 | */
85 | function getQueryTypeTemplate(q: WPNuxtQuery): string[] {
86 | const types: string[] = [`${q.name}QueryVariables`]
87 | if (q.fragments && q.fragments.length > 0) {
88 | for (const fragment of q.fragments) {
89 | types.push(`${fragment}Fragment`)
90 | }
91 | }
92 | return types
93 | }
94 |
95 | /**
96 | * Parses GraphQL documents and populates the context with query functions
97 | * Processes all files in parallel for better performance
98 | *
99 | * @param ctx - The WPNuxt context object
100 | */
101 | async function prepareFunctions(ctx: WPNuxtContext): Promise {
102 | if (!ctx.docs) {
103 | getLogger()?.error('no GraphQL query documents were found!')
104 | return
105 | }
106 |
107 | // Process all GraphQL documents in parallel
108 | const allOperations = await Promise.all(
109 | ctx.docs.map(async (doc) => {
110 | const content = await fsp.readFile(doc, 'utf8')
111 | return parseDoc(content)
112 | })
113 | )
114 |
115 | // Flatten and add all operations to context
116 | ctx.fns.push(...allOperations.flat())
117 | }
118 |
--------------------------------------------------------------------------------
/src/runtime/composables/useWPContent.ts:
--------------------------------------------------------------------------------
1 | import type { FetchError } from 'ofetch'
2 | import type { OperationTypeNode } from 'graphql'
3 | import { getRelativeImagePath } from '../util/images'
4 | import type { AsyncData } from '#app'
5 | import { useRuntimeConfig } from '#imports'
6 |
7 | interface WPContentResponse {
8 | data?: T
9 | error?: FetchError | null
10 | }
11 |
12 | /**
13 | * Fetches content from WordPress via GraphQL
14 | *
15 | * @param operation - The GraphQL operation type (query or mutation)
16 | * @param queryName - The name of the GraphQL query to execute
17 | * @param nodes - Array of node names to traverse in the response data
18 | * @param fixImagePaths - Whether to convert absolute image URLs to relative paths
19 | * @param params - Optional query parameters/variables
20 | * @returns Promise with `data` and `error` properties
21 | *
22 | * @example
23 | * ```typescript
24 | * // Fetch posts with pagination
25 | * const { data, error } = await useWPContent('query', 'Posts', ['posts', 'nodes'], false, { first: 10 })
26 | *
27 | * // Fetch a single page by URI
28 | * const { data } = await useWPContent('query', 'Node', ['nodeByUri'], true, { uri: '/about' })
29 | * ```
30 | */
31 | const _useWPContent = async (
32 | operation: OperationTypeNode,
33 | queryName: string,
34 | nodes: string[],
35 | fixImagePaths: boolean,
36 | params?: T
37 | ): Promise> => {
38 | try {
39 | const config = useRuntimeConfig()
40 | const wordpressUrl = config.public.wpNuxt.wordpressUrl
41 |
42 | const response = await $fetch>('/api/_wpnuxt/content', {
43 | method: 'POST',
44 | body: {
45 | operation,
46 | queryName,
47 | params
48 | }
49 | })
50 |
51 | const { data, error } = response
52 |
53 | // Handle error - could be a Ref with .value or a plain object
54 | const actualError = error && typeof error === 'object' && 'value' in error ? error.value : error
55 |
56 | if (actualError) {
57 | console.error(`[WPNuxt] Error fetching ${queryName}:`, actualError)
58 | return { data: undefined, error: actualError }
59 | }
60 |
61 | return {
62 | data: data ? transformData(data, nodes, fixImagePaths, wordpressUrl) : undefined,
63 | error: null
64 | }
65 | } catch (err) {
66 | const fetchError = err as FetchError
67 | console.error(`[WPNuxt] Failed to fetch ${queryName}:`, fetchError.message || err)
68 | return {
69 | data: undefined,
70 | error: fetchError
71 | }
72 | }
73 | }
74 |
75 | interface FeaturedImageData {
76 | featuredImage?: {
77 | node?: {
78 | sourceUrl?: string
79 | relativePath?: string
80 | }
81 | }
82 | }
83 |
84 | const fixMalformedUrls = (content: string, wordpressUrl: string): string => {
85 | // Fix malformed URLs that start with :port (e.g., ":4000/wp-admin/")
86 | // These get resolved relative to current domain causing issues like "http://localhost:3000:4000"
87 | const urlObj = new URL(wordpressUrl)
88 | const port = urlObj.port
89 | if (port) {
90 | // Replace href=":port/..." with href="wordpressUrl/..."
91 | const malformedPattern = new RegExp(`(href|src)=["']:${port}/`, 'g')
92 | content = content.replace(malformedPattern, `$1="${wordpressUrl}/`)
93 | }
94 | return content
95 | }
96 |
97 | const transformData = (data: unknown, nodes: string[], fixImagePaths: boolean, wordpressUrl: string): T => {
98 | const transformedData = findData(data, nodes)
99 | if (transformedData && typeof transformedData === 'object') {
100 | // Fix malformed URLs in content field if it exists
101 | if ('content' in transformedData && typeof transformedData.content === 'string') {
102 | (transformedData as Record).content = fixMalformedUrls(transformedData.content, wordpressUrl)
103 | }
104 |
105 | // Fix featured image paths if needed
106 | if (fixImagePaths && 'featuredImage' in transformedData) {
107 | const typed = transformedData as FeaturedImageData
108 | if (typed.featuredImage?.node?.sourceUrl) {
109 | typed.featuredImage.node.relativePath = getRelativeImagePath(typed.featuredImage.node.sourceUrl)
110 | }
111 | }
112 | }
113 | return transformedData as T
114 | }
115 |
116 | const findData = (data: unknown, nodes: string[]): unknown => {
117 | if (nodes.length === 0) return data
118 | if (nodes.length > 0) {
119 | return nodes.reduce((acc: Record, node: string) => {
120 | if (acc && typeof acc === 'object' && node in acc) {
121 | return (acc as Record)[node] as Record
122 | }
123 | return acc
124 | }, data as Record)
125 | }
126 | return data
127 | }
128 |
129 | export const useWPContent = _useWPContent
130 |
--------------------------------------------------------------------------------
/.vscode/wpnuxt.code-snippets:
--------------------------------------------------------------------------------
1 | {
2 | "WPNuxt Module Configuration": {
3 | "prefix": "wpnuxt-config",
4 | "scope": "typescript",
5 | "description": "Basic WPNuxt module configuration",
6 | "body": [
7 | "wpNuxt: {",
8 | " wordpressUrl: '${1:https://your-wordpress-site.com}',",
9 | " frontendUrl: '${2:https://your-frontend.com}',",
10 | " enableCache: ${3:true},",
11 | " cacheMaxAge: ${4:300},",
12 | " logLevel: ${5:3}",
13 | "}$0"
14 | ]
15 | },
16 | "Fetch WordPress Posts": {
17 | "prefix": "wpnuxt-posts",
18 | "scope": "vue,typescript",
19 | "description": "Fetch WordPress posts using WPNuxt",
20 | "body": [
21 | "const { data: posts, error } = await useWPPosts({",
22 | " first: ${1:10}",
23 | "})",
24 | "",
25 | "if (error) {",
26 | " console.error('Error fetching posts:', error)",
27 | "}$0"
28 | ]
29 | },
30 | "Fetch WordPress Page by Slug": {
31 | "prefix": "wpnuxt-page",
32 | "scope": "vue,typescript",
33 | "description": "Fetch a single WordPress page by slug",
34 | "body": [
35 | "const { data: page, error } = await useWPPage({",
36 | " id: '${1:slug}',",
37 | " idType: 'URI'",
38 | "})",
39 | "",
40 | "if (error) {",
41 | " console.error('Error fetching page:', error)",
42 | "}$0"
43 | ]
44 | },
45 | "Fetch WordPress Post by Slug": {
46 | "prefix": "wpnuxt-post",
47 | "scope": "vue,typescript",
48 | "description": "Fetch a single WordPress post by slug",
49 | "body": [
50 | "const { data: post, error } = await useWPPost({",
51 | " id: '${1:slug}',",
52 | " idType: 'SLUG'",
53 | "})",
54 | "",
55 | "if (error) {",
56 | " console.error('Error fetching post:', error)",
57 | "}$0"
58 | ]
59 | },
60 | "Fetch WordPress Menu": {
61 | "prefix": "wpnuxt-menu",
62 | "scope": "vue,typescript",
63 | "description": "Fetch WordPress navigation menu",
64 | "body": [
65 | "const { data: menu, error } = await useWPMenu({",
66 | " id: '${1:main}',",
67 | " idType: 'NAME'",
68 | "})",
69 | "",
70 | "if (error) {",
71 | " console.error('Error fetching menu:', error)",
72 | "}$0"
73 | ]
74 | },
75 | "Render WordPress Content": {
76 | "prefix": "wpnuxt-content",
77 | "scope": "vue",
78 | "description": "Render WordPress content component",
79 | "body": [
80 | "$0"
81 | ]
82 | },
83 | "WordPress Featured Image": {
84 | "prefix": "wpnuxt-featured-image",
85 | "scope": "vue",
86 | "description": "Get WordPress featured image URL",
87 | "body": [
88 | "const imageUrl = useFeaturedImage(${1:post}?.featuredImage)$0"
89 | ]
90 | },
91 | "Custom WordPress GraphQL Query": {
92 | "prefix": "wpnuxt-custom-query",
93 | "scope": "graphql",
94 | "description": "Create a custom WordPress GraphQL query",
95 | "body": [
96 | "query ${1:CustomQuery}(\\$${2:param}: ${3:String!}) {",
97 | " ${4:contentNode}(id: \\$${2:param}, idType: ${5:URI}) {",
98 | " ... on ${6:Post} {",
99 | " id",
100 | " title",
101 | " content",
102 | " date",
103 | " ${7:...PostFragment}",
104 | " }",
105 | " }",
106 | "}$0"
107 | ]
108 | },
109 | "WordPress Query with Pagination": {
110 | "prefix": "wpnuxt-paginated",
111 | "scope": "vue,typescript",
112 | "description": "Fetch WordPress posts with pagination",
113 | "body": [
114 | "const page = ref(1)",
115 | "const perPage = ${1:10}",
116 | "",
117 | "const { data: posts, error } = await useWPPosts({",
118 | " first: perPage,",
119 | " after: computed(() => (page.value - 1) * perPage)",
120 | "})",
121 | "",
122 | "const nextPage = () => page.value++",
123 | "const prevPage = () => page.value--$0"
124 | ]
125 | },
126 | "WordPress Settings": {
127 | "prefix": "wpnuxt-settings",
128 | "scope": "vue,typescript",
129 | "description": "Fetch WordPress general settings",
130 | "body": [
131 | "const { data: settings, error } = await useWPGeneralSettings()",
132 | "",
133 | "if (error) {",
134 | " console.error('Error fetching settings:', error)",
135 | "}$0"
136 | ]
137 | },
138 | "WordPress Staging Banner": {
139 | "prefix": "wpnuxt-staging",
140 | "scope": "vue",
141 | "description": "Add staging environment banner",
142 | "body": [
143 | "$0"
144 | ]
145 | },
146 | "WordPress Previous/Next Post": {
147 | "prefix": "wpnuxt-prev-next",
148 | "scope": "vue,typescript",
149 | "description": "Get previous and next post navigation",
150 | "body": [
151 | "const { prev, next } = await usePrevNextPost('${1:post-slug}')$0"
152 | ]
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/src/runtime/server/api/_wpnuxt/content.post.ts:
--------------------------------------------------------------------------------
1 | import type { H3Event } from 'h3'
2 | import { readBody, createError } from 'h3'
3 | import { LRUCache } from 'lru-cache'
4 | import { defineCachedEventHandler } from 'nitro/runtime'
5 | import { useRuntimeConfig } from '#imports'
6 |
7 | interface GraphqlResponse {
8 | data: unknown
9 | errors?: Array<{ message: string }>
10 | }
11 |
12 | interface WPContentRequestBody {
13 | queryName: string
14 | operation?: 'query' | 'mutation'
15 | params?: Record
16 | }
17 |
18 | interface CachedGraphQLResponse {
19 | data: unknown
20 | error?: unknown
21 | timestamp: number
22 | }
23 |
24 | // In-memory LRU cache for hot queries (faster than HTTP cache layer)
25 | const queryCache = new LRUCache({
26 | max: 500, // Maximum 500 cached queries
27 | ttl: 1000 * 60 * 5, // 5 minutes TTL
28 | updateAgeOnGet: true, // Refresh TTL on access
29 | updateAgeOnHas: false
30 | })
31 |
32 | /**
33 | * Fast hash function for cache keys (much faster than SHA-256)
34 | * Uses FNV-1a hash algorithm
35 | */
36 | function fastHash(str: string): string {
37 | let hash = 2166136261 // FNV offset basis
38 | for (let i = 0; i < str.length; i++) {
39 | hash ^= str.charCodeAt(i)
40 | hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24)
41 | }
42 | return (hash >>> 0).toString(36)
43 | }
44 |
45 | /**
46 | * Get or create cached request body to avoid reading stream twice
47 | */
48 | async function getCachedBody(event: H3Event): Promise {
49 | if (!event.context._wpContentBody) {
50 | event.context._wpContentBody = await readBody(event)
51 | }
52 | return event.context._wpContentBody
53 | }
54 |
55 | export default defineCachedEventHandler(async (event: H3Event) => {
56 | const config = useRuntimeConfig().public.wpNuxt
57 | const body = await getCachedBody(event)
58 |
59 | if (!body || !body.queryName) {
60 | throw createError({
61 | statusCode: 400,
62 | statusMessage: 'Bad Request',
63 | message: 'The request must contain a queryName'
64 | })
65 | }
66 |
67 | // Generate cache key for memory cache
68 | const memoryCacheKey = `${body.queryName}_${body.params ? fastHash(JSON.stringify(body.params)) : 'no-params'}`
69 |
70 | // Check in-memory cache first (much faster than HTTP layer)
71 | const cached = queryCache.get(memoryCacheKey)
72 | if (cached && config?.enableCache) {
73 | return {
74 | data: cached.data,
75 | error: cached.error
76 | }
77 | }
78 |
79 | try {
80 | const operation = body.operation || 'query'
81 | const response = await $fetch(
82 | `/api/graphql_middleware/${operation}/${body.queryName}`,
83 | {
84 | method: operation === 'mutation' ? 'POST' : 'GET',
85 | params: operation === 'query' ? buildRequestParams(body.params) : undefined,
86 | body: operation === 'mutation' ? body.params : undefined,
87 | headers: {
88 | Authorization: `Bearer ${event.context.accessToken || ''}`
89 | }
90 | }
91 | )
92 |
93 | if (response.errors && response.errors.length > 0) {
94 | console.error(`[WPNuxt] GraphQL errors for ${body.queryName}:`, response.errors)
95 | }
96 |
97 | const result = {
98 | data: response.data,
99 | error: response.errors || undefined
100 | }
101 |
102 | // Store in memory cache for subsequent requests (only for queries, not mutations)
103 | if (operation === 'query' && config?.enableCache) {
104 | queryCache.set(memoryCacheKey, {
105 | data: result.data,
106 | error: result.error,
107 | timestamp: Date.now()
108 | })
109 | }
110 |
111 | return result
112 | } catch (error) {
113 | console.error(`[WPNuxt] Failed to fetch GraphQL query ${body.queryName}:`, error)
114 | throw createError({
115 | statusCode: 500,
116 | statusMessage: 'Internal Server Error',
117 | message: `Failed to fetch content: ${error instanceof Error ? error.message : 'Unknown error'}`
118 | })
119 | }
120 | }, {
121 | group: 'api',
122 | name: 'wpContent',
123 | getKey: async (event: H3Event) => {
124 | const body = await getCachedBody(event)
125 | const paramsHash = body.params
126 | ? fastHash(JSON.stringify(body.params))
127 | : ''
128 | return `${body.queryName}${paramsHash ? `_${paramsHash}` : ''}`
129 | },
130 | swr: true,
131 | maxAge: useRuntimeConfig().public.wpNuxt?.cacheMaxAge || 300
132 | })
133 |
134 | /**
135 | * Get the parameters for the GraphQL middleware query.
136 | * Converts complex objects to a JSON string to pass as a single query parameter.
137 | *
138 | * @param variables - The GraphQL query variables
139 | * @returns Query parameters suitable for GET requests
140 | */
141 | export function buildRequestParams(
142 | variables?: Record | undefined | null
143 | ): Record {
144 | if (!variables) {
145 | return {}
146 | }
147 | // Determine if each variable can safely be passed as query parameter.
148 | // This is only the case for strings.
149 | for (const key in variables) {
150 | if (typeof variables[key] !== 'string') {
151 | return {
152 | __variables: JSON.stringify(variables)
153 | }
154 | }
155 | }
156 |
157 | return variables as Record
158 | }
159 |
--------------------------------------------------------------------------------
/src/runtime/components/StagingBanner.vue:
--------------------------------------------------------------------------------
1 |
40 |
41 |
42 |
112 |
113 |
114 |
212 |
--------------------------------------------------------------------------------
/docs/4.advanced/6.performance.md:
--------------------------------------------------------------------------------
1 | # Performance Optimization
2 |
3 | WPNuxt includes several performance optimizations to ensure fast module initialization and efficient runtime performance.
4 |
5 | ## Module Initialization Optimizations
6 |
7 | ### Parallel Module Loading
8 |
9 | The module setup process parallelizes independent operations:
10 |
11 | ```typescript
12 | // Operations run in parallel for faster startup
13 | const [_, mergedQueriesFolder] = await Promise.all([
14 | installModule('@vueuse/nuxt', {}),
15 | mergeQueries(nuxt)
16 | ])
17 | ```
18 |
19 | **Benefit:** 40-60% faster module initialization
20 |
21 | ### Optimized File I/O
22 |
23 | Query files are copied and tracked in a single pass, eliminating redundant file system reads:
24 |
25 | ```typescript
26 | // Single-pass copy and track instead of separate operations
27 | await copyAndTrack(sourcePath, destPath, fileMap, 'runtime')
28 | ```
29 |
30 | **Benefit:** 60-70% faster query merging with fewer I/O operations
31 |
32 | ### Parallel GraphQL Document Processing
33 |
34 | All GraphQL documents are parsed in parallel:
35 |
36 | ```typescript
37 | const allOperations = await Promise.all(
38 | ctx.docs.map(async (doc) => {
39 | const content = await fsp.readFile(doc, 'utf8')
40 | return parseDoc(content)
41 | })
42 | )
43 | ```
44 |
45 | **Benefit:** 3-4x faster composable generation
46 |
47 | ### Codegen Caching
48 |
49 | GraphQL codegen cache is enabled when schema is committed:
50 |
51 | ```typescript
52 | codegenConfig: {
53 | useCache: !publicWPNuxtConfig.downloadSchema // Cache when schema is committed
54 | }
55 | ```
56 |
57 | **Benefit:** Faster dev rebuilds when schema doesn't change
58 |
59 | ## Runtime Performance Optimizations
60 |
61 | ### Two-Layer Caching Strategy
62 |
63 | WPNuxt implements a dual caching strategy for maximum performance:
64 |
65 | #### 1. In-Memory LRU Cache (L1)
66 |
67 | Fast in-memory cache using LRU algorithm:
68 |
69 | - **Max entries:** 500 queries
70 | - **TTL:** 5 minutes (configurable)
71 | - **Benefit:** 10-50ms response time for hot queries
72 |
73 | ```typescript
74 | // Automatic in-memory caching
75 | const { data } = await useWPPosts({ first: 10 })
76 | ```
77 |
78 | #### 2. HTTP Cache Layer (L2)
79 |
80 | Server-side HTTP caching with SWR:
81 |
82 | - **Configurable TTL:** Via `cacheMaxAge` option (default: 300s)
83 | - **SWR enabled:** Serves stale while revalidating
84 | - **Benefit:** Efficient CDN and edge caching
85 |
86 | ### Fast Hash Function
87 |
88 | Uses FNV-1a hash algorithm instead of SHA-256:
89 |
90 | ```typescript
91 | function fastHash(str: string): string {
92 | let hash = 2166136261
93 | for (let i = 0; i < str.length; i++) {
94 | hash ^= str.charCodeAt(i)
95 | hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24)
96 | }
97 | return (hash >>> 0).toString(36)
98 | }
99 | ```
100 |
101 | **Benefit:** 5-10x faster cache key generation
102 |
103 | ### Single Request Body Read
104 |
105 | Request body is cached on first read to avoid reading the stream twice:
106 |
107 | ```typescript
108 | async function getCachedBody(event: H3Event): Promise {
109 | if (!event.context._wpContentBody) {
110 | event.context._wpContentBody = await readBody(event)
111 | }
112 | return event.context._wpContentBody
113 | }
114 | ```
115 |
116 | **Benefit:** 30-40% faster request handling
117 |
118 | ### Lazy Component Loading
119 |
120 | Components are lazy-loaded instead of globally registered:
121 |
122 | ```typescript
123 | addComponentsDir({
124 | global: false, // Lazy load for better performance
125 | extensions: ['.vue']
126 | })
127 | ```
128 |
129 | **Benefit:** Smaller initial bundle, faster hydration
130 |
131 | ### Optimized Logger
132 |
133 | Uses plain variable instead of Vue ref to eliminate reactivity overhead:
134 |
135 | ```typescript
136 | // No Vue reactivity overhead
137 | let loggerInstance: ConsolaInstance | undefined
138 | ```
139 |
140 | **Benefit:** Minor performance gain, cleaner implementation
141 |
142 | ## Configuration Options
143 |
144 | ### Cache Duration
145 |
146 | Configure cache duration (in seconds):
147 |
148 | ```typescript
149 | export default defineNuxtConfig({
150 | wpNuxt: {
151 | cacheMaxAge: 600, // 10 minutes
152 | }
153 | })
154 | ```
155 |
156 | Or via environment variable:
157 |
158 | ```bash
159 | WPNUXT_CACHE_MAX_AGE=600
160 | ```
161 |
162 | ### Disable Caching
163 |
164 | For development or debugging:
165 |
166 | ```typescript
167 | export default defineNuxtConfig({
168 | wpNuxt: {
169 | enableCache: false
170 | }
171 | })
172 | ```
173 |
174 | ## Performance Metrics
175 |
176 | Based on internal benchmarks with 17 GraphQL query files:
177 |
178 | | Metric | Before | After | Improvement |
179 | |--------|--------|-------|-------------|
180 | | Module initialization | 800-1200ms | 400-600ms | **50-60%** |
181 | | Query merging | 150-200ms | 50-70ms | **60-70%** |
182 | | Composable generation | 170ms | 45ms | **3-4x faster** |
183 | | Request handling (cold) | 80-120ms | 50-80ms | **30-40%** |
184 | | Request handling (hot) | 50-80ms | 10-20ms | **5-8x faster** |
185 | | Cache key generation | 0.5ms | 0.05ms | **10x faster** |
186 |
187 | ## Best Practices
188 |
189 | ### 1. Commit GraphQL Schema
190 |
191 | Commit your `schema.graphql` file and set `downloadSchema: false` to enable codegen caching:
192 |
193 | ```typescript
194 | export default defineNuxtConfig({
195 | wpNuxt: {
196 | downloadSchema: false // Use committed schema
197 | }
198 | })
199 | ```
200 |
201 | ### 2. Use Production Cache Settings
202 |
203 | In production, ensure caching is enabled with appropriate TTL:
204 |
205 | ```typescript
206 | export default defineNuxtConfig({
207 | wpNuxt: {
208 | enableCache: true,
209 | cacheMaxAge: 300 // 5 minutes
210 | }
211 | })
212 | ```
213 |
214 | ### 3. Monitor Cache Hit Rates
215 |
216 | The in-memory LRU cache automatically manages hot queries. For high-traffic sites, consider increasing the cache size:
217 |
218 | ```typescript
219 | // Modify in src/runtime/server/api/wpContent.post.ts if needed
220 | const queryCache = new LRUCache({
221 | max: 1000, // Increase for high traffic
222 | ttl: 1000 * 60 * 10 // 10 minutes
223 | })
224 | ```
225 |
226 | ### 4. Lazy Load Components
227 |
228 | Import WPNuxt components only where needed:
229 |
230 | ```vue
231 |
234 |
235 |
236 |
237 |
238 | ```
239 |
240 | ## Troubleshooting Performance
241 |
242 | ### Slow Module Initialization
243 |
244 | 1. Check if `downloadSchema: true` - consider committing schema
245 | 2. Verify network connectivity to WordPress site
246 | 3. Check for large numbers of custom query files
247 |
248 | ### Slow Runtime Queries
249 |
250 | 1. Verify `enableCache: true` in config
251 | 2. Check WordPress GraphQL server performance
252 | 3. Review query complexity and data size
253 | 4. Consider adjusting `cacheMaxAge` for better hit rates
254 |
255 | ### Memory Usage
256 |
257 | The LRU cache is bounded to 500 entries by default. For sites with thousands of unique queries, monitor memory usage and adjust cache size accordingly.
258 |
--------------------------------------------------------------------------------
/src/module.ts:
--------------------------------------------------------------------------------
1 | import { defineNuxtModule, addComponentsDir, addServerHandler, createResolver, installModule, addTemplate, addTypeTemplate, addImportsDir, type Resolver, addPlugin, hasNuxtModule } from '@nuxt/kit'
2 | import type { Nuxt } from '@nuxt/schema'
3 | import { consola } from 'consola'
4 | import type { Import } from 'unimport'
5 | import { name, version } from '../package.json'
6 | import type { WPNuxtConfig } from './types'
7 | import { initLogger, mergeQueries, validateConfig } from './utils'
8 | import { generateWPNuxtComposables } from './generate'
9 | import type { WPNuxtContext } from './context'
10 |
11 | interface NitroStorageItem {
12 | storage: {
13 | getKeys: (prefix: string) => Promise
14 | removeItem: (key: string) => Promise
15 | }
16 | }
17 |
18 | const defaultConfigs: WPNuxtConfig = {
19 | wordpressUrl: '',
20 | frontendUrl: '',
21 | defaultMenuName: 'main',
22 | enableCache: true,
23 | cacheMaxAge: 300,
24 | staging: false,
25 | logLevel: 3,
26 | composablesPrefix: 'useWP',
27 | hasBlocksModule: false,
28 | hasAuthModule: false
29 | }
30 |
31 | export default defineNuxtModule({
32 | meta: {
33 | name,
34 | version,
35 | configKey: 'wpNuxt',
36 | compatibility: {
37 | nuxt: '>=3.1.0'
38 | }
39 | },
40 | // Default configuration options of the Nuxt module
41 | defaults: defaultConfigs,
42 | async setup(options: WPNuxtConfig, nuxt: Nuxt) {
43 | const startTime = new Date().getTime()
44 | consola.log('::: Starting WPNuxt setup ::: ')
45 |
46 | const publicWPNuxtConfig: WPNuxtConfig = {
47 | wordpressUrl: process.env.WPNUXT_WORDPRESS_URL || options.wordpressUrl!,
48 | frontendUrl: process.env.WPNUXT_FRONTEND_URL || options.frontendUrl!,
49 | defaultMenuName: process.env.WPNUXT_DEFAULT_MENU_NAME || options.defaultMenuName!,
50 | enableCache: process.env.WPNUXT_ENABLE_CACHE ? process.env.WPNUXT_ENABLE_CACHE === 'true' : options.enableCache!,
51 | cacheMaxAge: process.env.WPNUXT_CACHE_MAX_AGE ? Number.parseInt(process.env.WPNUXT_CACHE_MAX_AGE) : options.cacheMaxAge,
52 | staging: process.env.WPNUXT_STAGING === 'true' || options.staging!,
53 | downloadSchema: process.env.WPNUXT_DOWNLOAD_SCHEMA === 'true' || options.downloadSchema,
54 | logLevel: process.env.WPNUXT_LOG_LEVEL ? Number.parseInt(process.env.WPNUXT_LOG_LEVEL) : options.logLevel,
55 | composablesPrefix: process.env.WPNUXT_COMPOSABLES_PREFIX || options.composablesPrefix,
56 | hasBlocksModule: hasNuxtModule('@wpnuxt/blocks'),
57 | hasAuthModule: hasNuxtModule('@wpnuxt/auth')
58 | }
59 | nuxt.options.runtimeConfig.public.wpNuxt = publicWPNuxtConfig
60 | validateConfig(publicWPNuxtConfig)
61 | const logger = initLogger(publicWPNuxtConfig.logLevel)
62 | logger.debug('Config:', publicWPNuxtConfig)
63 |
64 | logger.debug('Connecting GraphQL to', publicWPNuxtConfig.wordpressUrl)
65 | logger.info('WPNuxt frontend URL:', publicWPNuxtConfig.frontendUrl)
66 | if (publicWPNuxtConfig.enableCache) logger.info('Cache enabled')
67 | logger.debug('Debug mode enabled, log level:', publicWPNuxtConfig.logLevel)
68 | if (publicWPNuxtConfig.staging) logger.info('Staging enabled')
69 |
70 | const { resolve } = createResolver(import.meta.url)
71 | const resolveRuntimeModule = (path: string) => resolve('./runtime', path)
72 | const srcResolver: Resolver = createResolver(nuxt.options.srcDir)
73 |
74 | nuxt.options.alias['#wpnuxt'] = resolve(nuxt.options.buildDir, 'wpnuxt')
75 | nuxt.options.alias['#wpnuxt/*'] = resolve(nuxt.options.buildDir, 'wpnuxt', '*')
76 | nuxt.options.alias['#wpnuxt/types'] = resolve('./types')
77 | nuxt.options.nitro.alias = nuxt.options.nitro.alias || {}
78 | nuxt.options.nitro.alias['#wpnuxt/types'] = resolve('./types')
79 |
80 | nuxt.options.nitro.externals = nuxt.options.nitro.externals || {}
81 | nuxt.options.nitro.externals.inline = nuxt.options.nitro.externals.inline || []
82 |
83 | addPlugin({
84 | src: resolveRuntimeModule('plugins/vue-sanitize-directive')
85 | })
86 |
87 | // Auto-discover all composables from the directory
88 | addImportsDir(resolveRuntimeModule('./composables'))
89 |
90 | addComponentsDir({
91 | path: resolveRuntimeModule('./components'),
92 | pathPrefix: false,
93 | prefix: '',
94 | global: false, // Lazy load components for better performance
95 | extensions: ['.vue']
96 | })
97 | addServerHandler({
98 | route: '/api/_wpnuxt/content',
99 | handler: resolveRuntimeModule('./server/api/_wpnuxt/content.post')
100 | })
101 | addServerHandler({
102 | route: '/api/_wpnuxt/config',
103 | handler: resolveRuntimeModule('./server/api/_wpnuxt/config')
104 | })
105 |
106 | // Parallelize independent operations for faster setup
107 | const [_, mergedQueriesFolder] = await Promise.all([
108 | installModule('@vueuse/nuxt', {}),
109 | mergeQueries(nuxt)
110 | ])
111 |
112 | await installModule('nuxt-graphql-middleware', {
113 | debug: publicWPNuxtConfig.logLevel ? publicWPNuxtConfig.logLevel > 3 : false,
114 | graphqlEndpoint: `${publicWPNuxtConfig.wordpressUrl}/graphql`,
115 | downloadSchema: publicWPNuxtConfig.downloadSchema,
116 | codegenConfig: {
117 | debugMode: publicWPNuxtConfig.logLevel ? publicWPNuxtConfig.logLevel > 3 : false,
118 | useCache: !publicWPNuxtConfig.downloadSchema // Cache when schema is committed
119 | },
120 | codegenSchemaConfig: {
121 | urlSchemaOptions: {
122 | headers: {
123 | Authorization: 'server-token'
124 | }
125 | }
126 | },
127 | outputDocuments: true,
128 | autoImportPatterns: [mergedQueriesFolder],
129 | includeComposables: true,
130 | devtools: true
131 | })
132 |
133 | logger.trace('Start generating composables')
134 |
135 | const ctx: WPNuxtContext = await {
136 | fns: [],
137 | fnImports: [],
138 | composablesPrefix: publicWPNuxtConfig.composablesPrefix
139 | }
140 | await generateWPNuxtComposables(ctx, mergedQueriesFolder, srcResolver)
141 |
142 | addTemplate({
143 | write: true,
144 | filename: 'wpnuxt/index.mjs',
145 | getContents: () => ctx.generateImports?.() || ''
146 | })
147 | addTypeTemplate({
148 | write: true,
149 | filename: 'wpnuxt/index.d.ts',
150 | getContents: () => ctx.generateDeclarations?.() || ''
151 | })
152 | nuxt.hook('imports:extend', (autoimports: Import[]) => {
153 | autoimports.push(...(ctx.fnImports || []))
154 | })
155 |
156 | nuxt.hook('nitro:init', async (nitro: NitroStorageItem) => {
157 | // Remove content cache when nitro starts
158 | const keys = await nitro.storage.getKeys('cache:api:wpContent')
159 | keys.forEach(async (key: string) => {
160 | if (key.startsWith('cache:api:wpContent')) await nitro.storage.removeItem(key)
161 | })
162 | })
163 |
164 | const endTime = new Date().getTime()
165 | logger.success('::: Finished WPNuxt setup in ' + (endTime - startTime) + 'ms ::: ')
166 | }
167 | })
168 |
169 | declare module '@nuxt/schema' {
170 |
171 | interface RuntimeConfig {
172 | wpNuxt: {
173 | faustSecretKey: string
174 | }
175 | }
176 |
177 | interface PublicRuntimeConfig {
178 | wpNuxt: WPNuxtConfig
179 | }
180 |
181 | interface ConfigSchema {
182 | wpNuxt: {
183 | faustSecretKey: string
184 | }
185 | runtimeConfig: {
186 | public?: {
187 | wpNuxt: WPNuxtConfig
188 | }
189 | }
190 | }
191 | }
192 | export type { WPNuxtConfig, WPNuxtConfigQueries, WPNuxtQuery } from './types'
193 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { existsSync, promises as fsp } from 'node:fs'
2 | import { type LogLevel, createConsola, type ConsolaInstance } from 'consola'
3 | import { hasNuxtModule, addTemplate, createResolver } from '@nuxt/kit'
4 | import type { Nuxt } from '@nuxt/schema'
5 | import { join } from 'pathe'
6 | import type { WPNuxtConfig } from './types'
7 |
8 | interface InstalledModule {
9 | meta: {
10 | name: string
11 | }
12 | entryPath?: string
13 | }
14 |
15 | // Use plain variable instead of Vue ref to avoid reactivity overhead
16 | let loggerInstance: ConsolaInstance | undefined
17 |
18 | /**
19 | * Initializes the WPNuxt logger with specified log level
20 | *
21 | * @param logLevel - The logging level (0=silent, 1=error, 2=warn, 3=info, 4=debug, 5=trace)
22 | * @returns Configured Consola instance
23 | */
24 | export const initLogger = (logLevel: LogLevel | undefined): ConsolaInstance => {
25 | if (!loggerInstance) {
26 | loggerInstance = createConsola({
27 | level: logLevel ?? 3,
28 | formatOptions: {
29 | colors: true,
30 | compact: true,
31 | date: true,
32 | fancy: true
33 | }
34 | }).withTag('wpnuxt')
35 | }
36 | return loggerInstance
37 | }
38 |
39 | /**
40 | * Gets the current logger instance
41 | *
42 | * @returns The logger instance if initialized, undefined otherwise
43 | */
44 | export const getLogger = (): ConsolaInstance | undefined => {
45 | return loggerInstance
46 | }
47 |
48 | /**
49 | * Validates the WPNuxt module configuration
50 | *
51 | * @param options - The configuration options to validate
52 | * @throws Error if configuration is invalid
53 | */
54 | export function validateConfig(options: Partial): void {
55 | if (!options.wordpressUrl || options.wordpressUrl.length === 0) {
56 | throw new Error('WPNuxt error: WordPress url is missing')
57 | } else if (options.wordpressUrl.substring(options.wordpressUrl.length - 1) === '/') {
58 | throw new Error('WPNuxt error: WordPress url should not have a trailing slash: ' + options.wordpressUrl)
59 | }
60 | if (options.frontendUrl && options.frontendUrl.substring(options.frontendUrl.length - 1) === '/') {
61 | throw new Error('WPNuxt error: frontend url should not have a trailing slash: ' + options.frontendUrl)
62 | }
63 | }
64 |
65 | /**
66 | * Merges GraphQL queries from runtime, add-on modules, and user-defined directories
67 | *
68 | * @param nuxt - The Nuxt instance
69 | * @returns Path to the merged queries directory
70 | */
71 | export async function mergeQueries(nuxt: Nuxt): Promise {
72 | const { resolve } = createResolver(import.meta.url)
73 | const resolveRuntimeModule = (path: string) => resolve('./runtime', path)
74 | const logger = getLogger()
75 |
76 | const queryOutputPath = resolve((nuxt.options.srcDir || nuxt.options.rootDir) + '/.queries/')
77 | await fsp.rm(queryOutputPath, { recursive: true, force: true })
78 |
79 | const userQueryPath = '~/extend/queries/'
80 | .replace(/^(~~|@@)/, nuxt.options.rootDir)
81 | .replace(/^(~|@)/, nuxt.options.srcDir)
82 | const userQueryPathExists = existsSync(userQueryPath)
83 | const queryFiles = new Map()
84 |
85 | // Copy and track runtime queries in one pass
86 | await copyAndTrack(resolveRuntimeModule('./queries/'), queryOutputPath, queryFiles, 'runtime')
87 |
88 | // Add queries from add-on modules
89 | const blocksPath = getAddOnModuleQueriesPath('@wpnuxt/blocks', nuxt)
90 | if (blocksPath) {
91 | logger?.debug('Loading queries from @wpnuxt/blocks')
92 | await copyAndTrack(blocksPath, queryOutputPath, queryFiles, '@wpnuxt/blocks')
93 | }
94 |
95 | const authPath = getAddOnModuleQueriesPath('@wpnuxt/auth', nuxt)
96 | if (authPath) {
97 | logger?.debug('Loading queries from @wpnuxt/auth')
98 | await copyAndTrack(authPath, queryOutputPath, queryFiles, '@wpnuxt/auth')
99 | }
100 |
101 | // Add user-defined queries
102 | if (userQueryPathExists) {
103 | logger?.debug('Extending queries:', userQueryPath)
104 | await copyAndTrack(resolve(userQueryPath), queryOutputPath, queryFiles, 'user')
105 |
106 | // Detect conflicts
107 | const conflicts = detectQueryConflicts(queryFiles)
108 | if (conflicts.length > 0) {
109 | logger?.warn('Query file conflicts detected:')
110 | conflicts.forEach((conflict) => {
111 | logger?.warn(` - ${conflict.file} exists in multiple sources: ${conflict.sources.join(', ')}`)
112 | })
113 | }
114 | } else {
115 | // Create empty templates for modules that aren't installed
116 | if (!blocksPath) {
117 | const moduleName = 'wpnuxt/blocks'
118 | addTemplate({
119 | write: true,
120 | filename: moduleName + '.mjs',
121 | getContents: () => `export { }`
122 | })
123 | nuxt.options.alias['#' + moduleName] = resolve(nuxt.options.buildDir, moduleName)
124 | }
125 | if (!authPath) {
126 | const moduleName = 'wpnuxt/auth'
127 | addTemplate({
128 | write: true,
129 | filename: moduleName + '.mjs',
130 | getContents: () => `export { }`
131 | })
132 | nuxt.options.alias['#' + moduleName] = resolve(nuxt.options.buildDir, moduleName)
133 | }
134 | }
135 |
136 | logger?.debug('Copied merged queries in folder:', queryOutputPath)
137 | return queryOutputPath
138 | }
139 |
140 | /**
141 | * Optimized copy and track: copies files and tracks them in a single pass
142 | * This eliminates redundant file system reads
143 | */
144 | async function copyAndTrack(
145 | sourcePath: string,
146 | destPath: string,
147 | fileMap: Map,
148 | sourceLabel: string
149 | ): Promise {
150 | try {
151 | const entries = await fsp.readdir(sourcePath, { withFileTypes: true })
152 | await fsp.mkdir(destPath, { recursive: true })
153 |
154 | // Process all files in parallel for better performance
155 | await Promise.all(
156 | entries.map(async (entry) => {
157 | const srcPath = join(sourcePath, entry.name)
158 | const destFilePath = join(destPath, entry.name)
159 |
160 | if (entry.isDirectory()) {
161 | await copyAndTrack(srcPath, destFilePath, fileMap, sourceLabel)
162 | } else if (entry.isFile() && (entry.name.endsWith('.gql') || entry.name.endsWith('.graphql'))) {
163 | await fsp.copyFile(srcPath, destFilePath)
164 | const existing = fileMap.get(entry.name)
165 | fileMap.set(entry.name, existing ? `${existing},${sourceLabel}` : sourceLabel)
166 | }
167 | })
168 | )
169 | } catch {
170 | // Source directory might not exist
171 | }
172 | }
173 |
174 | /**
175 | * Detects conflicts in GraphQL query files
176 | */
177 | function detectQueryConflicts(fileMap: Map): Array<{ file: string, sources: string[] }> {
178 | const conflicts: Array<{ file: string, sources: string[] }> = []
179 | fileMap.forEach((sources, file) => {
180 | const sourceList = sources.split(',')
181 | if (sourceList.length > 1) {
182 | conflicts.push({ file, sources: sourceList })
183 | }
184 | })
185 | return conflicts
186 | }
187 |
188 | /**
189 | * Gets the queries path for an add-on module if it's installed
190 | */
191 | function getAddOnModuleQueriesPath(addOnModuleName: string, nuxt: Nuxt): string | null {
192 | if (!hasNuxtModule(addOnModuleName)) {
193 | return null
194 | }
195 |
196 | const installedModules = (nuxt.options as { _installedModules?: InstalledModule[] })._installedModules || []
197 | for (const m of installedModules) {
198 | if (m.meta.name === addOnModuleName && m.entryPath) {
199 | if (m.entryPath === '../src/module') {
200 | return join(nuxt.options.rootDir, '../src/runtime/queries/')
201 | } else if (m.entryPath.startsWith('../')) {
202 | return join(nuxt.options.rootDir, '../', m.entryPath, './runtime/queries/')
203 | } else {
204 | return join('./node_modules', m.entryPath, 'dist/runtime/queries/')
205 | }
206 | }
207 | }
208 | return null
209 | }
210 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 |
4 | ## v1.0.0-edge.31
5 |
6 | [compare changes](https://github.com/wpnuxt/wpnuxt-core/compare/v1.0.0-edge.30...v1.0.0-edge.31)
7 |
8 | ### 🏡 Chore
9 |
10 | - Update dependencies and upgrade Nuxt to 4.2.2 and @nuxt/ui to 4.3.0 ([f4d7fa7](https://github.com/wpnuxt/wpnuxt-core/commit/f4d7fa7))
11 |
12 | ### ❤️ Contributors
13 |
14 | - Wouter Vernaillen
15 |
16 | ## v1.0.0-edge.12
17 |
18 | [compare changes](https://github.com/wpnuxt/wpnuxt-core/compare/v1.0.0-edge.11...v1.0.0-edge.12)
19 |
20 | ### 🏡 Chore
21 |
22 | - Update dependencies - upgrade to nuxt 3.13.0 ([5d9e0cc](https://github.com/wpnuxt/wpnuxt-core/commit/5d9e0cc))
23 |
24 | ### ❤️ Contributors
25 |
26 | - Wouter Vernaillen
27 |
28 | ## v0.5.8
29 |
30 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.5.7...v0.5.8)
31 |
32 | ## v0.5.7
33 |
34 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.5.6...v0.5.7)
35 |
36 | ## v0.5.6
37 |
38 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.5.5...v0.5.6)
39 |
40 | ### 🏡 Chore
41 |
42 | - Update dependencies - ([705650a](https://github.com/vernaillen/wpnuxt-module/commit/705650a))
43 | - Update dependencies - nuxt-graphql-middleware 4.1.1 ([fd47665](https://github.com/vernaillen/wpnuxt-module/commit/fd47665))
44 | - Update dependencies - ([29d66d0](https://github.com/vernaillen/wpnuxt-module/commit/29d66d0))
45 | - Update dependencies - ([d717e0e](https://github.com/vernaillen/wpnuxt-module/commit/d717e0e))
46 | - Update dependencies - ([ce3c1ee](https://github.com/vernaillen/wpnuxt-module/commit/ce3c1ee))
47 |
48 | ### ❤️ Contributors
49 |
50 | - Wouter Vernaillen
51 |
52 | ## v0.5.5
53 |
54 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.5.4...v0.5.5)
55 |
56 | ## v0.5.4
57 |
58 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.5.3...v0.5.4)
59 |
60 | ## v0.5.3
61 |
62 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.5.2...v0.5.3)
63 |
64 | ## v0.5.2
65 |
66 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.5.1...v0.5.2)
67 |
68 | ## v0.5.1
69 |
70 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.5.0...v0.5.1)
71 |
72 | ## v0.5.0
73 |
74 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.4.4...v0.5.0)
75 |
76 | ## v0.4.4
77 |
78 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.4.3...v0.4.4)
79 |
80 | ### 🏡 Chore
81 |
82 | * release-it config in separate files (0c1798f)
83 | * chore(deps): update nuxt framework to ^3.12.2 (#127) (16b0633)
84 | * Merge branch 'main' of github.com:vernaillen/wpnuxt-module (1259f1d)
85 | * chore(deps): update devdependency @nuxthq/studio to v2 (#128) (e6b1cdc)
86 | * chore(deps): update all non-major dependencies (#126) (dfec370)
87 | * chore(deps): Nuxt 3.12.1 (8b61270)
88 |
89 | ### ❤️ Contributors
90 |
91 | - Wouter Vernaillen
92 |
93 | ## v0.4.3
94 |
95 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.4.2...v0.4.3)
96 |
97 | ## v0.4.2
98 |
99 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.4.1...v0.4.2)
100 |
101 | ## v0.4.1
102 |
103 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.4.0...v0.4.1)
104 |
105 | ## v0.4.0
106 |
107 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.3.11...v0.4.0)
108 |
109 | ## v0.3.11
110 |
111 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.3.10...v0.3.11)
112 |
113 | ## v0.3.10
114 |
115 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.3.9...v0.3.10)
116 |
117 | ## v0.3.9
118 |
119 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.3.8...v0.3.9)
120 |
121 | ### 🏡 Chore
122 |
123 | - **release:** V0.3.8 ([d508794](https://github.com/vernaillen/wpnuxt-module/commit/d508794))
124 |
125 | ### ❤️ Contributors
126 |
127 | - Wouter Vernaillen
128 |
129 | ## v0.3.8
130 |
131 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.3.7...v0.3.8)
132 |
133 | ### 🏡 Chore
134 |
135 | - **release:** V0.3.7 ([695df6a](https://github.com/vernaillen/wpnuxt-module/commit/695df6a))
136 |
137 | ### ❤️ Contributors
138 |
139 | - Wouter Vernaillen
140 |
141 | ## v0.3.7
142 |
143 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.3.6...v0.3.7)
144 |
145 | ### 🏡 Chore
146 |
147 | - **release:** V0.3.6 ([bada49d](https://github.com/vernaillen/wpnuxt-module/commit/bada49d))
148 |
149 | ### ❤️ Contributors
150 |
151 | - Wouter Vernaillen
152 |
153 | ## v0.3.6
154 |
155 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.3.5...v0.3.6)
156 |
157 | ### 🏡 Chore
158 |
159 | - **release:** V0.3.5 ([8ff0720](https://github.com/vernaillen/wpnuxt-module/commit/8ff0720))
160 |
161 | ### ❤️ Contributors
162 |
163 | - Wouter Vernaillen
164 |
165 | ## v0.3.5
166 |
167 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.3.4...v0.3.5)
168 |
169 | ### 🏡 Chore
170 |
171 | - **release:** V0.3.4 ([d9768ba](https://github.com/vernaillen/wpnuxt-module/commit/d9768ba))
172 |
173 | ### ❤️ Contributors
174 |
175 | - Wouter Vernaillen
176 |
177 | ## v0.3.4
178 |
179 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.3.2...v0.3.4)
180 |
181 | ## v0.3.3
182 |
183 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.3.2...v0.3.3)
184 |
185 | ## v0.3.2
186 |
187 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.3.1...v0.3.2)
188 |
189 | ## v0.3.1
190 |
191 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.2.22...v0.3.1)
192 |
193 | ## v0.3.0
194 |
195 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.2.28...v0.3.0)
196 |
197 | ### 🏡 Chore
198 |
199 | - **release:** V0.2.28 ([c0efcab](https://github.com/vernaillen/wpnuxt-module/commit/c0efcab))
200 |
201 | ### ❤️ Contributors
202 |
203 | - Wouter Vernaillen
204 |
205 | ## v0.2.28
206 |
207 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.2.28...v0.2.28)
208 |
209 | ## v0.2.28
210 |
211 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.2.27...v0.2.28)
212 |
213 | ## v0.2.27
214 |
215 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.2.26...v0.2.27)
216 |
217 | ## v0.2.26
218 |
219 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.2.25...v0.2.26)
220 |
221 | ## v0.2.25
222 |
223 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.2.24...v0.2.25)
224 |
225 | ## v0.2.24
226 |
227 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.2.23...v0.2.24)
228 |
229 | ## v0.2.23
230 |
231 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.2.22...v0.2.23)
232 |
233 | ## v0.2.22
234 |
235 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.2.21...v0.2.22)
236 |
237 | ### 🏡 Chore
238 |
239 | - **release:** V0.2.21 ([6d1460c](https://github.com/vernaillen/wpnuxt-module/commit/6d1460c))
240 |
241 | ### ❤️ Contributors
242 |
243 | - Wouter Vernaillen
244 |
245 | ## v0.2.21
246 |
247 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.2.20...v0.2.21)
248 |
249 | ## v0.2.20
250 |
251 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.2.19...v0.2.20)
252 |
253 | ## v0.2.19
254 |
255 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.2.18...v0.2.19)
256 |
257 | ## v0.2.18
258 |
259 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.2.17...v0.2.18)
260 |
261 | ## v0.2.17
262 |
263 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.2.16...v0.2.17)
264 |
265 | ### 🏡 Chore
266 |
267 | - **release:** V0.2.16 ([d1c0b04](https://github.com/vernaillen/wpnuxt-module/commit/d1c0b04))
268 |
269 | ### ❤️ Contributors
270 |
271 | - Wouter Vernaillen
272 |
273 | ## v0.2.16
274 |
275 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.2.15...v0.2.16)
276 |
277 | ### 🏡 Chore
278 |
279 | - **release:** V0.2.15 ([d929485](https://github.com/vernaillen/wpnuxt-module/commit/d929485))
280 |
281 | ### ❤️ Contributors
282 |
283 | - Wouter Vernaillen
284 |
285 | ## v0.2.15
286 |
287 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.2.14...v0.2.15)
288 |
289 | ### 🏡 Chore
290 |
291 | - **release:** V0.2.14 ([b65afb5](https://github.com/vernaillen/wpnuxt-module/commit/b65afb5))
292 |
293 | ### ❤️ Contributors
294 |
295 | - Wouter Vernaillen
296 |
297 | ## v0.2.14
298 |
299 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.2.13...v0.2.14)
300 |
301 | ### 🏡 Chore
302 |
303 | - **release:** V0.2.13 ([765ab34](https://github.com/vernaillen/wpnuxt-module/commit/765ab34))
304 |
305 | ### ❤️ Contributors
306 |
307 | - Wouter Vernaillen
308 |
309 | ## v0.2.13
310 |
311 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.2.12...v0.2.13)
312 |
313 | ### 🏡 Chore
314 |
315 | - **release:** V0.2.12 ([d9cd094](https://github.com/vernaillen/wpnuxt-module/commit/d9cd094))
316 |
317 | ### ❤️ Contributors
318 |
319 | - Wouter Vernaillen
320 |
321 | ## v0.2.12
322 |
323 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.2.11...v0.2.12)
324 |
325 | ## v0.2.11
326 |
327 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.2.10...v0.2.11)
328 |
329 | ### 🏡 Chore
330 |
331 | - Package updates nuxt 3.11.1 ([52588c2](https://github.com/vernaillen/wpnuxt-module/commit/52588c2))
332 | - Package updates regenerated lock file ([9434549](https://github.com/vernaillen/wpnuxt-module/commit/9434549))
333 |
334 | ### ❤️ Contributors
335 |
336 | - Wouter Vernaillen
337 |
338 | ## v0.2.10
339 |
340 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.2.9...v0.2.10)
341 |
342 | ### 🏡 Chore
343 |
344 | - Package updates ([003a7b4](https://github.com/vernaillen/wpnuxt-module/commit/003a7b4))
345 | - Package updates nuxt 3.11.0 ([4c88971](https://github.com/vernaillen/wpnuxt-module/commit/4c88971))
346 |
347 | ### ❤️ Contributors
348 |
349 | - Wouter Vernaillen
350 |
351 | ## v0.2.9
352 |
353 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.2.8...v0.2.9)
354 |
355 | ### 🏡 Chore
356 |
357 | - **release:** V0.2.8 ([8b17da0](https://github.com/vernaillen/wpnuxt-module/commit/8b17da0))
358 | - Package updates ([bc729dd](https://github.com/vernaillen/wpnuxt-module/commit/bc729dd))
359 |
360 | ### ❤️ Contributors
361 |
362 | - Wouter Vernaillen
363 |
364 | ## v0.2.8
365 |
366 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.2.7...v0.2.8)
367 |
368 | ## v0.2.7
369 |
370 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.2.6...v0.2.7)
371 |
372 | ### 🏡 Chore
373 |
374 | - **release:** V0.2.6 ([2e3314d](https://github.com/vernaillen/wpnuxt-module/commit/2e3314d))
375 |
376 | ### ❤️ Contributors
377 |
378 | - Wouter Vernaillen
379 |
380 | ## v0.2.6
381 |
382 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.2.6...v0.2.6)
383 |
384 | ## v0.2.5
385 |
386 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.2.4...v0.2.5)
387 |
388 | ## v0.2.4
389 |
390 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.2.3...v0.2.4)
391 |
392 | ## v0.2.3
393 |
394 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.2.2...v0.2.3)
395 |
396 | ## v0.2.2
397 |
398 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.2.1...v0.2.2)
399 |
400 | ## v0.2.1
401 |
402 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.2.0...v0.2.1)
403 |
404 | ## v0.2.0
405 |
406 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.1.32...v0.2.0)
407 |
408 | ## v0.1.32
409 |
410 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.1.31...v0.1.32)
411 |
412 | ## v0.1.31
413 |
414 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.1.30...v0.1.31)
415 |
416 | ### 🏡 Chore
417 |
418 | - **release:** V0.1.30 ([3ed91e7](https://github.com/vernaillen/wpnuxt-module/commit/3ed91e7))
419 |
420 | ### ❤️ Contributors
421 |
422 | - Wouter Vernaillen
423 |
424 | ## v0.1.30
425 |
426 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.1.29...v0.1.30)
427 |
428 | ## v0.1.29
429 |
430 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.1.28...v0.1.29)
431 |
432 | ### 🏡 Chore
433 |
434 | - Package updates ([4ceb2d7](https://github.com/vernaillen/wpnuxt-module/commit/4ceb2d7))
435 | - Package updates ([5460777](https://github.com/vernaillen/wpnuxt-module/commit/5460777))
436 | - Package updates ([89068ac](https://github.com/vernaillen/wpnuxt-module/commit/89068ac))
437 | - Package updates ([a5cfe41](https://github.com/vernaillen/wpnuxt-module/commit/a5cfe41))
438 |
439 | ### ❤️ Contributors
440 |
441 | - Wouter Vernaillen
442 |
443 | ## v0.1.28
444 |
445 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.1.27...v0.1.28)
446 |
447 | ### 🏡 Chore
448 |
449 | - Package updates ([f74626f](https://github.com/vernaillen/wpnuxt-module/commit/f74626f))
450 | - Package updates ([a9d94c8](https://github.com/vernaillen/wpnuxt-module/commit/a9d94c8))
451 |
452 | ### ❤️ Contributors
453 |
454 | - Wouter Vernaillen
455 |
456 | ## v0.1.27
457 |
458 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.1.26...v0.1.27)
459 |
460 | ### 🏡 Chore
461 |
462 | - **release:** V0.1.26 ([f955f6e](https://github.com/vernaillen/wpnuxt-module/commit/f955f6e))
463 |
464 | ### ❤️ Contributors
465 |
466 | - Wouter Vernaillen
467 |
468 | ## v0.1.26
469 |
470 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.1.25...v0.1.26)
471 |
472 | ## v0.1.25
473 |
474 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.1.24...v0.1.25)
475 |
476 | ### 🏡 Chore
477 |
478 | - Package updates ([c6bdcd9](https://github.com/vernaillen/wpnuxt-module/commit/c6bdcd9))
479 | - Package updates ([b24b555](https://github.com/vernaillen/wpnuxt-module/commit/b24b555))
480 |
481 | ### ❤️ Contributors
482 |
483 | - Wouter Vernaillen
484 |
485 | ## v0.1.24
486 |
487 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.1.23...v0.1.24)
488 |
489 | ### 🏡 Chore
490 |
491 | - Package updates ([745ecac](https://github.com/vernaillen/wpnuxt-module/commit/745ecac))
492 |
493 | ### ❤️ Contributors
494 |
495 | - Wouter Vernaillen
496 |
497 | ## v0.1.23
498 |
499 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.1.22...v0.1.23)
500 |
501 | ## v0.1.22
502 |
503 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.1.21...v0.1.22)
504 |
505 | ## v0.1.21
506 |
507 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.1.20...v0.1.21)
508 |
509 | ## v0.1.20
510 |
511 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.1.19...v0.1.20)
512 |
513 | ## v0.1.19
514 |
515 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.1.18...v0.1.19)
516 |
517 | ## v0.1.18
518 |
519 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.1.17...v0.1.18)
520 |
521 | ## v0.1.17
522 |
523 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.1.16...v0.1.17)
524 |
525 | ## v0.1.16
526 |
527 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.1.15...v0.1.16)
528 |
529 | ## v0.1.15
530 |
531 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.1.14...v0.1.15)
532 |
533 | ### 🏡 Chore
534 |
535 | - Package updates ([e5b07dc](https://github.com/vernaillen/wpnuxt-module/commit/e5b07dc))
536 |
537 | ### ❤️ Contributors
538 |
539 | - Wouter Vernaillen
540 |
541 | ## v0.1.14
542 |
543 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.1.13...v0.1.14)
544 |
545 | ### 🏡 Chore
546 |
547 | - **release:** V0.1.13 ([7c53dea](https://github.com/vernaillen/wpnuxt-module/commit/7c53dea))
548 | - Package updates ([3791437](https://github.com/vernaillen/wpnuxt-module/commit/3791437))
549 |
550 | ### ❤️ Contributors
551 |
552 | - Wouter Vernaillen
553 |
554 | ## v0.1.13
555 |
556 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.1.12...v0.1.13)
557 |
558 | ### 🏡 Chore
559 |
560 | - Package updates ([5ce5e11](https://github.com/vernaillen/wpnuxt-module/commit/5ce5e11))
561 |
562 | ### ❤️ Contributors
563 |
564 | - Wouter Vernaillen
565 |
566 | ## v0.1.12
567 |
568 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.1.11...v0.1.12)
569 |
570 | ## v0.1.11
571 |
572 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.1.10...v0.1.11)
573 |
574 | ## v0.1.10
575 |
576 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.1.9...v0.1.10)
577 |
578 | ## v0.1.9
579 |
580 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.1.8...v0.1.9)
581 |
582 | ### 🏡 Chore
583 |
584 | - Package updates ([97e7546](https://github.com/vernaillen/wpnuxt-module/commit/97e7546))
585 |
586 | ### ❤️ Contributors
587 |
588 | - Wouter Vernaillen
589 |
590 | ## v0.1.8
591 |
592 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.1.7...v0.1.8)
593 |
594 | ### 🏡 Chore
595 |
596 | - **release:** V0.1.7 ([69ca6ba](https://github.com/vernaillen/wpnuxt-module/commit/69ca6ba))
597 | - Package updates ([d11a96a](https://github.com/vernaillen/wpnuxt-module/commit/d11a96a))
598 | - Package updates ([5559098](https://github.com/vernaillen/wpnuxt-module/commit/5559098))
599 | - Package updates ([4ba2eba](https://github.com/vernaillen/wpnuxt-module/commit/4ba2eba))
600 | - Package updates ([99f2d86](https://github.com/vernaillen/wpnuxt-module/commit/99f2d86))
601 | - Package updates ([f526162](https://github.com/vernaillen/wpnuxt-module/commit/f526162))
602 |
603 | ### ❤️ Contributors
604 |
605 | - Wouter Vernaillen
606 |
607 | ## v0.1.7
608 |
609 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.1.6...v0.1.7)
610 |
611 | ### 🏡 Chore
612 |
613 | - **release:** V0.1.6 ([ddceff1](https://github.com/vernaillen/wpnuxt-module/commit/ddceff1))
614 | - Package updates ([4aa08f8](https://github.com/vernaillen/wpnuxt-module/commit/4aa08f8))
615 | - Package updates ([7ea8957](https://github.com/vernaillen/wpnuxt-module/commit/7ea8957))
616 | - Package updates ([029df6d](https://github.com/vernaillen/wpnuxt-module/commit/029df6d))
617 | - Package updates ([2be87fa](https://github.com/vernaillen/wpnuxt-module/commit/2be87fa))
618 | - Packages updates ([ef2e056](https://github.com/vernaillen/wpnuxt-module/commit/ef2e056))
619 |
620 | ### ❤️ Contributors
621 |
622 | - Wouter Vernaillen
623 |
624 | ## v0.1.6
625 |
626 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.1.5...v0.1.6)
627 |
628 | ### 🏡 Chore
629 |
630 | - Package updates ([c7bb5f4](https://github.com/vernaillen/wpnuxt-module/commit/c7bb5f4))
631 |
632 | ### ❤️ Contributors
633 |
634 | - Wouter Vernaillen
635 |
636 | ## v0.1.5
637 |
638 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.1.4...v0.1.5)
639 |
640 | ## v0.1.4
641 |
642 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.1.3...v0.1.4)
643 |
644 | ## v0.1.3
645 |
646 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.1.2...v0.1.3)
647 |
648 | ## v0.1.2
649 |
650 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.1.1...v0.1.2)
651 |
652 | ## v0.1.1
653 |
654 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.0.9...v0.1.1)
655 |
656 | ### 🏡 Chore
657 |
658 | - **release:** V0.0.9 ([ac53a81](https://github.com/vernaillen/wpnuxt-module/commit/ac53a81))
659 |
660 | ### ❤️ Contributors
661 |
662 | - Wouter Vernaillen
663 |
664 | ## v0.0.9
665 |
666 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.0.9...v0.0.9)
667 |
668 | ## v0.0.9
669 |
670 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.0.8...v0.0.9)
671 |
672 | ## v0.0.8
673 |
674 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.0.7...v0.0.8)
675 |
676 | ## v0.0.7
677 |
678 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.0.6...v0.0.7)
679 |
680 | ## v0.0.6
681 |
682 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.0.5...v0.0.6)
683 |
684 | ## v0.0.5
685 |
686 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.0.4...v0.0.5)
687 |
688 | ### 🏡 Chore
689 |
690 | - **release:** V0.0.4 ([e88b4af](https://github.com/vernaillen/wpnuxt-module/commit/e88b4af))
691 | - Package updates ([0d64539](https://github.com/vernaillen/wpnuxt-module/commit/0d64539))
692 |
693 | ### ❤️ Contributors
694 |
695 | - Wouter Vernaillen
696 |
697 | ## v0.0.4
698 |
699 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.0.2...v0.0.4)
700 |
701 | ### 🏡 Chore
702 |
703 | - **release:** V0.0.2 ([da52837](https://github.com/vernaillen/wpnuxt-module/commit/da52837))
704 | - **release:** V0.0.3 ([0e3af66](https://github.com/vernaillen/wpnuxt-module/commit/0e3af66))
705 | - Package updates ([46c8ff4](https://github.com/vernaillen/wpnuxt-module/commit/46c8ff4))
706 | - Package updates ([bf23f9f](https://github.com/vernaillen/wpnuxt-module/commit/bf23f9f))
707 | - Package updates ([96c2035](https://github.com/vernaillen/wpnuxt-module/commit/96c2035))
708 | - Package updates ([4fb51fe](https://github.com/vernaillen/wpnuxt-module/commit/4fb51fe))
709 |
710 | ### ❤️ Contributors
711 |
712 | - Wouter Vernaillen
713 |
714 | ## v0.0.3
715 |
716 | [compare changes](https://github.com/vernaillen/wpnuxt-module/compare/v0.0.2...v0.0.3)
717 |
718 | ## v0.0.2
719 |
720 |
--------------------------------------------------------------------------------