├── .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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 31 | -------------------------------------------------------------------------------- /playground/app/components/LatestPost.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 29 | -------------------------------------------------------------------------------- /src/runtime/components/WPContent.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 27 | -------------------------------------------------------------------------------- /src/runtime/components/WPNuxtLogo.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 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 | 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 | * Featured image 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | --------------------------------------------------------------------------------