├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ ├── config.yml │ └── feature-request.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .nuxtrc ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── docs ├── .vitepress │ ├── config.ts │ ├── meta.ts │ └── theme │ │ ├── index.ts │ │ ├── main.css │ │ └── vars.css ├── api │ ├── index.md │ ├── kirby.md │ ├── kql.md │ ├── types-query-request.md │ ├── types-query-response.md │ ├── use-kirby-data.md │ └── use-kql.md ├── config │ └── index.md ├── faq │ ├── cors-issues.md │ ├── how-does-it-work.md │ └── what-is-kql.md ├── guide │ ├── getting-started.md │ ├── starters.md │ └── what-is-nuxt-kql.md ├── index.md ├── package.json ├── public │ ├── _headers │ ├── logo-shadow.svg │ ├── logo.svg │ ├── og.jpg │ └── robots.txt └── usage │ ├── authentication-methods.md │ ├── batching-queries.md │ ├── caching.md │ ├── error-handling.md │ ├── multi-language-sites.md │ ├── prefetching-queries.md │ └── typed-query-results.md ├── eslint.config.mjs ├── netlify.toml ├── package.json ├── playground ├── .env.example ├── app.vue ├── layouts │ └── default.vue ├── nuxt.config.ts ├── package.json ├── pages │ ├── blocks.vue │ ├── client-query.vue │ ├── index.vue │ └── prefetched-queries.vue ├── server │ ├── api │ │ └── kql.ts │ └── tsconfig.json └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── src ├── kit.ts ├── module.ts ├── prefetch.ts └── runtime │ ├── composables │ ├── $kirby.ts │ ├── $kql.ts │ ├── useKirbyData.ts │ └── useKql.ts │ ├── server │ ├── handler.ts │ └── imports.ts │ ├── types.ts │ └── utils.ts ├── test ├── __snapshots__ │ └── e2e.test.ts.snap ├── e2e.test.ts └── fixture │ ├── app.vue │ ├── composables │ └── test-result.ts │ ├── nuxt.config.ts │ ├── package.json │ ├── pages │ ├── $kql.vue │ ├── prefetch.vue │ ├── useKirbyData.vue │ └── useKql.vue │ └── tsconfig.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ['https://paypal.me/jschopplich'] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug report 2 | description: Write a report to help improve Nuxt KQL 3 | labels: [pending triage] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Please use a template below to create a minimal reproduction: 9 | 👉 https://stackblitz.com/github/nuxt/starter/tree/v3-stackblitz 10 | 👉 https://codesandbox.io/p/github/nuxt/starter/v3-codesandbox 11 | - type: textarea 12 | id: bug-env 13 | attributes: 14 | label: Environment 15 | description: You can use `npx nuxi info` to fill this section. 16 | placeholder: Environment 17 | validations: 18 | required: true 19 | - type: textarea 20 | id: reproduction 21 | attributes: 22 | label: Reproduction 23 | description: Please provide a link to a repo that can reproduce the problem you ran into. A [**minimal reproduction**](https://nuxt.com/docs/community/reporting-bugs#create-a-minimal-reproduction) is required unless you are absolutely sure that the issue is obvious and the provided information is enough to understand the problem. 24 | placeholder: Reproduction 25 | validations: 26 | required: true 27 | - type: textarea 28 | id: bug-description 29 | attributes: 30 | label: Describe the bug 31 | description: A clear and concise description of what the bug is. If you intend to file a PR for this issue, please let us know in the description. Thanks a lot! 32 | placeholder: Bug description 33 | validations: 34 | required: true 35 | - type: textarea 36 | id: additonal 37 | attributes: 38 | label: Additional context 39 | description: If applicable, add any other context about the problem here. 40 | - type: textarea 41 | id: logs 42 | attributes: 43 | label: Logs 44 | description: | 45 | Optional if reproduction is provided. Please try not to insert an image, but copy and paste the log text. 46 | render: shell-script 47 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: 📚 Nuxt KQL Documentation 4 | url: https://github.com/johannschopplich/nuxt-kql#readme 5 | about: Check the documentation for usage of Nuxt KQL 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Feature request 2 | description: Suggest a feature that will improve Nuxt KQL 3 | labels: [pending triage] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thank you for taking the time to fill out this feature request! 9 | 10 | Please check that there is not an existing issue covering the scope of the feature you have in mind. 11 | - type: textarea 12 | id: feature-description 13 | attributes: 14 | label: Describe the feature 15 | description: A clear and concise description of what you think would be a helpful addition to Nuxt KQL, including the possible use cases and alternatives you have considered. 16 | placeholder: Feature description 17 | validations: 18 | required: true 19 | - type: checkboxes 20 | id: additional-info 21 | attributes: 22 | label: Additional information 23 | description: Additional information that helps us decide how to proceed. 24 | options: 25 | - label: Would you be willing to help implement this feature? 26 | - label: Can you think of other implementations of this feature? 27 | - type: checkboxes 28 | id: required-info 29 | attributes: 30 | label: Final checks 31 | description: 'Before submitting, please make sure you do the following:' 32 | options: 33 | - label: Check existing [issues](https://github.com/johannschopplich/nuxt-kql/issues). 34 | required: true 35 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | ### 🔗 Linked issue 6 | 7 | 8 | 9 | ### ❓ Type of change 10 | 11 | 12 | 13 | - [ ] 📖 Documentation (updates to the documentation, readme or JSDoc annotations) 14 | - [ ] 🐞 Bug fix (a non-breaking change that fixes an issue) 15 | - [ ] 👌 Enhancement (improving an existing functionality like performance) 16 | - [ ] ✨ New feature (a non-breaking change that adds functionality) 17 | - [ ] 🧹 Chore (updates to the build process or auxiliary tools and libraries) 18 | - [ ] ⚠️ Breaking change (fix or feature that would cause existing functionality to change) 19 | 20 | ### 📚 Description 21 | 22 | 23 | 24 | 25 | 26 | ### 📝 Checklist 27 | 28 | 29 | 30 | 31 | 32 | - [ ] I have linked an issue or discussion. 33 | - [ ] I have updated the documentation accordingly. 34 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: pnpm/action-setup@v3 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: 20 21 | cache: pnpm 22 | - run: pnpm install 23 | - run: pnpm run dev:prepare 24 | - run: pnpm run lint 25 | 26 | typecheck: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v4 30 | - uses: pnpm/action-setup@v3 31 | - uses: actions/setup-node@v4 32 | with: 33 | node-version: 20 34 | cache: pnpm 35 | - run: pnpm install 36 | - run: pnpm run dev:prepare 37 | - run: pnpm run test:types 38 | 39 | test: 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/checkout@v4 43 | - uses: pnpm/action-setup@v3 44 | - uses: actions/setup-node@v4 45 | with: 46 | node-version: 20 47 | cache: pnpm 48 | - run: cp playground/.env.example playground/.env 49 | - run: pnpm install 50 | - run: pnpm run dev:prepare 51 | - run: pnpm run test 52 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - 'v*' 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | - uses: pnpm/action-setup@v3 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version: 20 22 | registry-url: https://registry.npmjs.org/ 23 | cache: pnpm 24 | 25 | - name: Publish changelog 26 | run: npx changelogithub 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | 30 | - name: Install 31 | run: pnpm install 32 | 33 | - name: Build type stubs 34 | run: pnpm run dev:prepare 35 | 36 | - name: Publish to npm 37 | run: npm publish --access public 38 | env: 39 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Logs 5 | *.log* 6 | 7 | # Temp directories 8 | .temp 9 | .tmp 10 | .cache 11 | **/.vitepress/cache 12 | 13 | # Yarn 14 | **/.yarn/cache 15 | **/.yarn/*state* 16 | 17 | # Generated dirs 18 | dist 19 | 20 | # Nuxt 21 | .nuxt 22 | .output 23 | .vercel_build_output 24 | .build-* 25 | .env 26 | .netlify 27 | 28 | # Env 29 | .env 30 | 31 | # Testing 32 | reports 33 | coverage 34 | *.lcov 35 | .nyc_output 36 | 37 | # VSCode 38 | # .vscode 39 | 40 | # Intellij idea 41 | *.iml 42 | .idea 43 | 44 | # OSX 45 | .DS_Store 46 | .AppleDouble 47 | .LSOverride 48 | .AppleDB 49 | .AppleDesktop 50 | Network Trash Folder 51 | Temporary Items 52 | .apdisk 53 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | -------------------------------------------------------------------------------- /.nuxtrc: -------------------------------------------------------------------------------- 1 | imports.autoImport=false 2 | typescript.includeWorkspace=true 3 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "Vue.volar" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Enable the ESLint flat config support 3 | "eslint.useFlatConfig": true, 4 | 5 | // Disable the default formatter, use ESLint instead 6 | "prettier.enable": false, 7 | "editor.formatOnSave": false, 8 | 9 | // Auto-fix 10 | "editor.codeActionsOnSave": { 11 | "source.fixAll.eslint": "explicit", 12 | "source.organizeImports": "never" 13 | }, 14 | 15 | // Silent the stylistic rules in you IDE, but still auto-fix them 16 | "eslint.rules.customizations": [ 17 | { "rule": "style/*", "severity": "off" }, 18 | { "rule": "format/*", "severity": "off" }, 19 | { "rule": "*-indent", "severity": "off" }, 20 | { "rule": "*-spacing", "severity": "off" }, 21 | { "rule": "*-spaces", "severity": "off" }, 22 | { "rule": "*-order", "severity": "off" }, 23 | { "rule": "*-dangle", "severity": "off" }, 24 | { "rule": "*-newline", "severity": "off" }, 25 | { "rule": "*quotes", "severity": "off" }, 26 | { "rule": "*semi", "severity": "off" } 27 | ], 28 | 29 | // Enable ESLint for all supported languages 30 | "eslint.validate": [ 31 | "javascript", 32 | "javascriptreact", 33 | "typescript", 34 | "typescriptreact", 35 | "vue", 36 | "html", 37 | "markdown", 38 | "json", 39 | "jsonc", 40 | "yaml" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-PRESENT Johann Schopplich 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Nuxt KQL module](./docs/public/og.jpg)](https://nuxt-kql.byjohann.dev) 2 | 3 | # Nuxt KQL 4 | 5 | [Nuxt](https://nuxt.com) module for [Kirby's Query Language](https://github.com/getkirby/kql) API. 6 | 7 | - [✨  Release Notes](https://github.com/johannschopplich/nuxt-kql/releases) 8 | - [📖  Read the documentation](https://nuxt-kql.byjohann.dev) 9 | 10 | ## Features 11 | 12 | - 🔒 Protected Kirby credentials when sending queries 13 | - 🪢 Supports token-based authentication with the [Kirby Headless plugin](https://github.com/johannschopplich/kirby-headless) (recommended) 14 | - 🤹 No CORS issues! 15 | - 🍱 Handle request just like with the [`useFetch`](https://nuxt.com/docs/getting-started/data-fetching/#usefetch) composable 16 | - 🗃 Cached query responses 17 | - 🦦 [Multiple starter kits](https://nuxt-kql.byjohann.dev/guide/starters) available 18 | - 🦾 Strongly typed 19 | 20 | ## Setup 21 | 22 | > [!TIP] 23 | > [📖 Read the documentation](https://nuxt-kql.byjohann.dev) 24 | 25 | ```bash 26 | npx nuxi@latest module add kql 27 | ``` 28 | 29 | ## Basic Usage 30 | 31 | > [!TIP] 32 | > [📖 Read the documentation](https://nuxt-kql.byjohann.dev) 33 | 34 | Add the Nuxt KQL module to your Nuxt config: 35 | 36 | ```ts 37 | // `nuxt.config.ts` 38 | export default defineNuxtConfig({ 39 | modules: ['nuxt-kql'] 40 | }) 41 | ``` 42 | 43 | And send queries in your template: 44 | 45 | ```vue 46 | 51 | 52 | 58 | ``` 59 | 60 | ## 💻 Development 61 | 62 | 1. Clone this repository 63 | 2. Enable [Corepack](https://github.com/nodejs/corepack) using `corepack enable` 64 | 3. Install dependencies using `pnpm install` 65 | 4. Run `pnpm run dev:prepare` 66 | 5. Start development server using `pnpm run dev` 67 | 68 | ## License 69 | 70 | [MIT](./LICENSE) License © 2022-PRESENT [Johann Schopplich](https://github.com/johannschopplich) 71 | -------------------------------------------------------------------------------- /docs/.vitepress/config.ts: -------------------------------------------------------------------------------- 1 | import type { DefaultTheme } from 'vitepress' 2 | import { defineConfig } from 'vitepress' 3 | import { description, version } from '../../package.json' 4 | import { 5 | github, 6 | name, 7 | ogImage, 8 | ogUrl, 9 | releases, 10 | } from './meta' 11 | 12 | export default defineConfig({ 13 | lang: 'en-US', 14 | title: name, 15 | description, 16 | head: [ 17 | ['link', { rel: 'icon', href: '/logo.svg', type: 'image/svg+xml' }], 18 | ['meta', { name: 'author', content: 'Johann Schopplich' }], 19 | ['meta', { property: 'og:type', content: 'website' }], 20 | ['meta', { property: 'og:url', content: ogUrl }], 21 | ['meta', { property: 'og:title', content: name }], 22 | ['meta', { property: 'og:description', content: description }], 23 | ['meta', { property: 'og:image', content: ogImage }], 24 | ['meta', { name: 'twitter:title', content: name }], 25 | ['meta', { name: 'twitter:description', content: description }], 26 | ['meta', { name: 'twitter:image', content: ogImage }], 27 | ['meta', { name: 'twitter:site', content: '@jschopplich' }], 28 | ['meta', { name: 'twitter:card', content: 'summary_large_image' }], 29 | ], 30 | 31 | lastUpdated: true, 32 | appearance: 'dark', 33 | 34 | themeConfig: { 35 | logo: '/logo.svg', 36 | 37 | editLink: { 38 | pattern: 'https://github.com/johannschopplich/nuxt-kql/edit/main/docs/:path', 39 | text: 'Suggest changes to this page', 40 | }, 41 | 42 | nav: nav(), 43 | 44 | sidebar: { 45 | '/guide/': sidebarGuide(), 46 | '/config/': sidebarGuide(), 47 | '/usage/': sidebarGuide(), 48 | '/faq/': sidebarGuide(), 49 | '/api/': sidebarApi(), 50 | }, 51 | 52 | socialLinks: [ 53 | { icon: 'github', link: github }, 54 | ], 55 | 56 | footer: { 57 | message: 'Released under the MIT License.', 58 | copyright: 'Copyright © 2022-PRESENT Johann Schopplich', 59 | }, 60 | 61 | search: { 62 | provider: 'local', 63 | }, 64 | }, 65 | }) 66 | 67 | function nav(): DefaultTheme.NavItem[] { 68 | return [ 69 | { 70 | text: 'Guide', 71 | activeMatch: '^/guide/', 72 | items: [ 73 | { text: 'What is Nuxt KQL?', link: '/guide/what-is-nuxt-kql' }, 74 | { text: 'Getting Started', link: '/guide/getting-started' }, 75 | { text: 'Starter Kits', link: '/guide/starters' }, 76 | ], 77 | }, 78 | { 79 | text: 'Configuration', 80 | link: '/config/', 81 | }, 82 | { 83 | text: 'Usage', 84 | activeMatch: '^/usage/', 85 | items: [ 86 | { text: 'Authentication', link: '/usage/authentication-methods' }, 87 | { text: 'Caching', link: '/usage/caching' }, 88 | { text: 'Error Handling', link: '/usage/error-handling' }, 89 | { text: 'Typed Responses', link: '/usage/typed-query-results' }, 90 | { text: 'Prefetching Queries', link: '/usage/prefetching-queries' }, 91 | { text: 'Multi-Language Sites', link: '/usage/multi-language-sites' }, 92 | { text: 'Batching Queries', link: '/usage/batching-queries' }, 93 | ], 94 | }, 95 | { 96 | text: 'API', 97 | activeMatch: '^/api/', 98 | items: [ 99 | { 100 | text: 'Overview', 101 | link: '/api/', 102 | }, 103 | { 104 | text: 'Composables', 105 | items: [ 106 | { text: 'useKql', link: '/api/use-kql' }, 107 | { text: 'useKirbyData', link: '/api/use-kirby-data' }, 108 | { text: '$kql', link: '/api/kql' }, 109 | { text: '$kirby', link: '/api/kirby' }, 110 | ], 111 | }, 112 | ], 113 | }, 114 | { 115 | text: `v${version}`, 116 | items: [ 117 | { 118 | text: 'Release Notes ', 119 | link: releases, 120 | }, 121 | ], 122 | }, 123 | ] 124 | } 125 | 126 | function sidebarGuide(): DefaultTheme.SidebarItem[] { 127 | return [ 128 | { 129 | text: 'Guides', 130 | items: [ 131 | { text: 'What is Nuxt KQL?', link: '/guide/what-is-nuxt-kql' }, 132 | { text: 'Getting Started', link: '/guide/getting-started' }, 133 | { text: 'Starter Kits', link: '/guide/starters' }, 134 | ], 135 | }, 136 | { 137 | text: 'Configuration', 138 | items: [ 139 | { text: 'Module', link: '/config/' }, 140 | ], 141 | }, 142 | { 143 | text: 'Usage', 144 | items: [ 145 | { text: 'Authentication', link: '/usage/authentication-methods' }, 146 | { text: 'Caching', link: '/usage/caching' }, 147 | { text: 'Error Handling', link: '/usage/error-handling' }, 148 | { text: 'Typed Responses', link: '/usage/typed-query-results' }, 149 | { text: 'Prefetching Queries', link: '/usage/prefetching-queries' }, 150 | { text: 'Multi-Language Sites', link: '/usage/multi-language-sites' }, 151 | { text: 'Batching Queries', link: '/usage/batching-queries' }, 152 | ], 153 | }, 154 | { 155 | text: 'FAQ', 156 | items: [ 157 | { text: 'How Does It Work?', link: '/faq/how-does-it-work' }, 158 | { text: 'What Is KQL?', link: '/faq/what-is-kql' }, 159 | { text: 'Can I Encounter CORS Issues?', link: '/faq/cors-issues' }, 160 | ], 161 | }, 162 | ] 163 | } 164 | 165 | function sidebarApi(): DefaultTheme.SidebarItem[] { 166 | return [ 167 | { 168 | text: 'Overview', 169 | link: '/api/', 170 | }, 171 | { 172 | text: 'Composables', 173 | items: [ 174 | { text: 'useKql', link: '/api/use-kql' }, 175 | { text: 'useKirbyData', link: '/api/use-kirby-data' }, 176 | { text: '$kql', link: '/api/kql' }, 177 | { text: '$kirby', link: '/api/kirby' }, 178 | ], 179 | }, 180 | { 181 | text: 'Type Declarations', 182 | items: [ 183 | { text: 'KirbyQueryRequest', link: '/api/types-query-request' }, 184 | { text: 'KirbyQueryResponse', link: '/api/types-query-response' }, 185 | ], 186 | }, 187 | ] 188 | } 189 | -------------------------------------------------------------------------------- /docs/.vitepress/meta.ts: -------------------------------------------------------------------------------- 1 | /* VitePress head */ 2 | export const name = 'Nuxt KQL' 3 | export const ogUrl = 'https://nuxt-kql.byjohann.dev/' 4 | export const ogImage = `${ogUrl}og.jpg` 5 | 6 | /* GitHub and social links */ 7 | export const github = 'https://github.com/johannschopplich/nuxt-kql' 8 | export const releases = 'https://github.com/johannschopplich/nuxt-kql/releases' 9 | export const twitter = 'https://twitter.com/jschopplich' 10 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import Theme from 'vitepress/theme' 2 | import './main.css' 3 | import './vars.css' 4 | 5 | export default { 6 | ...Theme, 7 | } 8 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/main.css: -------------------------------------------------------------------------------- 1 | .dark [img-light] { 2 | display: none; 3 | } 4 | 5 | html:not(.dark) [img-dark] { 6 | display: none; 7 | } 8 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/vars.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Colors Theme 3 | * -------------------------------------------------------------------------- */ 4 | 5 | :root { 6 | --vp-c-brand-1: #FF9320; 7 | --vp-c-brand-2: #FF8401; 8 | --vp-c-brand-3: #E27400; 9 | } 10 | 11 | /** 12 | * Component: Home 13 | * -------------------------------------------------------------------------- */ 14 | :root { 15 | --vp-home-hero-name-color: transparent; 16 | --vp-home-hero-name-background: -webkit-linear-gradient( 17 | 120deg, 18 | #FF9320 15%, 19 | #ECF937 20 | ); 21 | --vp-home-hero-image-background-image: linear-gradient( 22 | -45deg, 23 | #FF932060 30%, 24 | #ECF93760 25 | ); 26 | --vp-home-hero-image-filter: blur(30px); 27 | } 28 | 29 | @media (min-width: 640px) { 30 | :root { 31 | --vp-home-hero-image-filter: blur(56px); 32 | } 33 | } 34 | 35 | @media (min-width: 960px) { 36 | :root { 37 | --vp-home-hero-image-filter: blur(72px); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /docs/api/index.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | ## Composables 4 | 5 | Nuxt KQL offers intuitive composables to return data for KQL queries or Kirby API requests. All composables are [auto-imported](https://nuxt.com/docs/guide/concepts/auto-imports) and globally available inside your ` 28 | 29 | 38 | ``` 39 | 40 | ## Allow Client Requests 41 | 42 | ::: warning 43 | Authorization credentials will be publicly visible. Also, possible CORS issues ahead if the backend is not configured properly. 44 | ::: 45 | 46 | To fetch data directly from your Kirby instance without the Nuxt proxy, set the module option `client` to `true`: 47 | 48 | ```ts{6} 49 | // `nuxt.config.ts` 50 | export default defineNuxtConfig({ 51 | modules: ['nuxt-kql'], 52 | 53 | kql: { 54 | client: true 55 | } 56 | }) 57 | ``` 58 | 59 | Now, every `$kirby` call will be directly use the Kirby instance by sending requests from the client: 60 | 61 | ```ts{3} 62 | const data = await $kirby('api/my-path') 63 | ``` 64 | 65 | ## Type Declarations 66 | 67 | ```ts 68 | function $kirby( 69 | path: string, 70 | opts: KirbyFetchOptions = {}, 71 | ): Promise 72 | 73 | type KirbyFetchOptions = Pick< 74 | NitroFetchOptions, 75 | | 'onRequest' 76 | | 'onRequestError' 77 | | 'onResponse' 78 | | 'onResponseError' 79 | | 'query' 80 | | 'headers' 81 | | 'method' 82 | | 'body' 83 | | 'retry' 84 | | 'retryDelay' 85 | | 'retryStatusCodes' 86 | | 'timeout' 87 | > & { 88 | /** 89 | * Language code to fetch data for in multi-language Kirby setups. 90 | */ 91 | language?: string 92 | /** 93 | * Cache the response between function calls for the same query. 94 | * @default true 95 | */ 96 | cache?: boolean 97 | } 98 | ``` 99 | -------------------------------------------------------------------------------- /docs/api/kql.md: -------------------------------------------------------------------------------- 1 | # `$kql` 2 | 3 | Returns raw KQL query data. Uses an internal server route to proxy requests. 4 | 5 | Query responses are cached by default between function calls for the same query based on a calculated hash. 6 | 7 | ## Example 8 | 9 | ```vue 10 | 32 | 33 | 38 | ``` 39 | 40 | ## Allow Client Requests 41 | 42 | ::: warning 43 | Authorization credentials will be publicly visible. Also, possible CORS issues ahead if the backend is not configured properly. 44 | ::: 45 | 46 | To fetch data directly from your Kirby instance without the Nuxt proxy, set the module option `client` to `true`: 47 | 48 | ```ts{6} 49 | // `nuxt.config.ts` 50 | export default defineNuxtConfig({ 51 | modules: ['nuxt-kql'], 52 | 53 | kql: { 54 | client: true 55 | } 56 | }) 57 | ``` 58 | 59 | Now, every `$kql` call will be directly use the Kirby instance by sending requests from the client: 60 | 61 | ```ts{3} 62 | const data = await $kql(query) 63 | ``` 64 | 65 | ## Type Declarations 66 | 67 | ```ts 68 | function $kql = KirbyQueryResponse>( 69 | query: KirbyQueryRequest, 70 | opts: KqlOptions = {} 71 | ): Promise 72 | 73 | type KqlOptions = Pick< 74 | NitroFetchOptions, 75 | | 'onRequest' 76 | | 'onRequestError' 77 | | 'onResponse' 78 | | 'onResponseError' 79 | | 'headers' 80 | | 'retry' 81 | | 'retryDelay' 82 | | 'retryStatusCodes' 83 | | 'timeout' 84 | > & { 85 | /** 86 | * Language code to fetch data for in multi-language Kirby setups. 87 | */ 88 | language?: string 89 | /** 90 | * Cache the response between function calls for the same query. 91 | * @default true 92 | */ 93 | cache?: boolean 94 | } 95 | ``` 96 | -------------------------------------------------------------------------------- /docs/api/types-query-request.md: -------------------------------------------------------------------------------- 1 | # `KirbyQueryRequest` 2 | 3 | Importable from `#nuxt-kql`. 4 | 5 | ```ts 6 | import type { KirbyQueryRequest } from '#nuxt-kql' 7 | ``` 8 | 9 | ## Type Declarations 10 | 11 | ::: info 12 | Types are re-exported from the [`kirby-types`](https://github.com/johannschopplich/kirby-types) package. 13 | ::: 14 | 15 | ```ts 16 | type KirbyQueryModel = 17 | | 'collection' 18 | | 'kirby' 19 | | 'site' 20 | | 'page' 21 | | 'user' 22 | | 'file' 23 | | 'content' 24 | | 'item' 25 | | 'arrayItem' 26 | | 'structureItem' 27 | | 'block' 28 | | CustomModel 29 | 30 | // For simple dot-based queries like `site.title`, `page.images`, etc. 31 | type DotNotationQuery = 32 | `${KirbyQueryModel}.${string}` 33 | 34 | // For function-based queries like `page("id")`, `user("name")`, etc. 35 | type FunctionNotationQuery = 36 | `${KirbyQueryModel}(${string})${string}` 37 | 38 | // Combines the two types above to allow for either dot or function notation 39 | type KirbyQueryChain = 40 | | DotNotationQuery 41 | | FunctionNotationQuery 42 | 43 | type KirbyQuery = 44 | | KirbyQueryModel 45 | // Ensures that it must match the pattern exactly, but not more broadly 46 | | (string extends KirbyQueryChain 47 | ? never 48 | : KirbyQueryChain) 49 | 50 | interface KirbyQuerySchema { 51 | query: KirbyQuery 52 | select?: 53 | | string[] 54 | | Record 55 | } 56 | 57 | interface KirbyQueryRequest extends KirbyQuerySchema { 58 | pagination?: { 59 | /** @default 100 */ 60 | limit?: number 61 | page?: number 62 | } 63 | } 64 | ``` 65 | -------------------------------------------------------------------------------- /docs/api/types-query-response.md: -------------------------------------------------------------------------------- 1 | # `KirbyQueryResponse` 2 | 3 | Importable from `#nuxt-kql`. 4 | 5 | ```ts 6 | import type { KirbyQueryResponse } from '#nuxt-kql' 7 | ``` 8 | 9 | ## Type Declarations 10 | 11 | ::: info 12 | Types are re-exported from the [`kirby-types`](https://github.com/johannschopplich/kirby-types) package. 13 | ::: 14 | 15 | ```ts 16 | interface KirbyApiResponse { 17 | code: number 18 | status: string 19 | result?: T 20 | } 21 | 22 | type KirbyQueryResponse< 23 | T = any, 24 | Pagination extends boolean = false 25 | > = KirbyApiResponse< 26 | Pagination extends true 27 | ? { 28 | data: T 29 | pagination: { 30 | page: number 31 | pages: number 32 | offset: number 33 | limit: number 34 | total: number 35 | } 36 | } 37 | : T 38 | > 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/api/use-kirby-data.md: -------------------------------------------------------------------------------- 1 | # `useKirbyData` 2 | 3 | Returns raw data from a Kirby instance for the given path. 4 | 5 | Responses are cached by default between function calls for the same path based on a calculated hash of the path and fetch options. 6 | 7 | ## Return Values 8 | 9 | - `data`: the result of the asynchronous function that is passed in. 10 | - `refresh`/`execute`: a function that can be used to refresh the data returned by the `handler` function. 11 | - `error`: an error object if the data fetching failed. 12 | - `status`: a string indicating the status of the data request (`'idle'`, `'pending'`, `'success'`, `'error'`). 13 | - `clear`: a function which will set `data` to `undefined`, set `error` to `null`, set `status` to `'idle'`, and mark any currently pending requests as cancelled. 14 | 15 | By default, Nuxt waits until a `refresh` is finished before it can be executed again. 16 | 17 | ## Caching 18 | 19 | By default, a [unique key is generated](/usage/caching) based in input parameters for each request to ensure that data fetching can be properly de-duplicated across requests. To disable caching, set the `cache` option to `false`: 20 | 21 | ```ts 22 | const { data } = await useKirbyData('api/my-path', { 23 | cache: false 24 | }) 25 | ``` 26 | 27 | Clear the cache for a specific path by calling the `clear` function. This will remove the cached data for the path and allow the next request to fetch the data from the server: 28 | 29 | ```ts 30 | const { data, refresh, clear } = await useKirbyData('api/my-path') 31 | 32 | async function invalidateAndRefresh() { 33 | clear() 34 | await refresh() 35 | } 36 | ``` 37 | 38 | ## Example 39 | 40 | ```vue 41 | 55 | 56 | 69 | ``` 70 | 71 | ## Allow Client Requests 72 | 73 | ::: warning 74 | Authorization credentials will be publicly visible. Also, possible CORS issues ahead if the backend is not configured properly. 75 | ::: 76 | 77 | To fetch data directly from your Kirby instance without the Nuxt proxy, set the module option `client` to `true`: 78 | 79 | ```ts{6} 80 | // `nuxt.config.ts` 81 | export default defineNuxtConfig({ 82 | modules: ['nuxt-kql'], 83 | 84 | kql: { 85 | client: true 86 | } 87 | }) 88 | ``` 89 | 90 | Now, every `useKirbyData` call will be directly use the Kirby instance by sending requests from the client: 91 | 92 | ```ts{3} 93 | const { data } = await useKirbyData('api/my-path') 94 | ``` 95 | 96 | ## Type Declarations 97 | 98 | ```ts 99 | export function useKirbyData( 100 | path: MaybeRefOrGetter, 101 | opts: UseKirbyDataOptions = {}, 102 | ): AsyncData 103 | 104 | type UseKirbyDataOptions = Omit, 'watch'> & Pick< 105 | NitroFetchOptions, 106 | | 'onRequest' 107 | | 'onRequestError' 108 | | 'onResponse' 109 | | 'onResponseError' 110 | | 'query' 111 | | 'headers' 112 | | 'method' 113 | | 'body' 114 | | 'retry' 115 | | 'retryDelay' 116 | | 'retryStatusCodes' 117 | | 'timeout' 118 | > & { 119 | /** 120 | * Language code to fetch data for in multi-language Kirby setups. 121 | */ 122 | language?: MaybeRefOrGetter 123 | /** 124 | * Cache the response between function calls for the same path. 125 | * @default true 126 | */ 127 | cache?: boolean 128 | /** 129 | * Watch an array of reactive sources and auto-refresh the fetch result when they change. 130 | * Path and language are watched by default. You can completely ignore reactive sources by using `watch: false`. 131 | * @default undefined 132 | */ 133 | watch?: MultiWatchSources | false 134 | } 135 | ``` 136 | 137 | `useKirbyData` infers all of Nuxt's [`useAsyncData` options](https://nuxt.com/docs/api/composables/use-async-data#params). 138 | -------------------------------------------------------------------------------- /docs/api/use-kql.md: -------------------------------------------------------------------------------- 1 | # `useKql` 2 | 3 | Returns KQL query data. Uses an internal server route to proxy requests. 4 | 5 | Query responses are cached by default between function calls for the same query based on a calculated hash. 6 | 7 | ## Return Values 8 | 9 | - `data`: the result of the asynchronous function that is passed in. 10 | - `refresh`/`execute`: a function that can be used to refresh the data returned by the `handler` function. 11 | - `error`: an error object if the data fetching failed. 12 | - `status`: a string indicating the status of the data request (`'idle'`, `'pending'`, `'success'`, `'error'`). 13 | - `clear`: a function which will set `data` to `undefined`, set `error` to `null`, set `status` to `'idle'`, and mark any currently pending requests as cancelled. 14 | 15 | By default, Nuxt waits until a `refresh` is finished before it can be executed again. 16 | 17 | ## Caching 18 | 19 | By default, a [unique key is generated](/usage/caching) based in input parameters for each request to ensure that data fetching can be properly de-duplicated across requests. To disable caching, set the `cache` option to `false`: 20 | 21 | ```ts 22 | const { data } = await useKql({ query: 'site' }, { 23 | cache: false 24 | }) 25 | ``` 26 | 27 | Clear the cache for a specific query by calling the `clear` function. This will remove the cached data for the query and allow the next request to fetch the data from the server: 28 | 29 | ```ts 30 | const { data, refresh, clear } = await useKql({ query: 'site' }) 31 | 32 | async function invalidateAndRefresh() { 33 | clear() 34 | await refresh() 35 | } 36 | ``` 37 | 38 | ## Example 39 | 40 | ```vue 41 | 47 | 48 | 56 | ``` 57 | 58 | ## Allow Client Requests 59 | 60 | ::: warning 61 | Authorization credentials will be publicly visible. Also, possible CORS issues ahead if the backend is not configured properly. 62 | ::: 63 | 64 | To fetch data directly from your Kirby instance without the Nuxt proxy, set the module option `client` to `true`: 65 | 66 | ```ts{6} 67 | // `nuxt.config.ts` 68 | export default defineNuxtConfig({ 69 | modules: ['nuxt-kql'], 70 | 71 | kql: { 72 | client: true 73 | } 74 | }) 75 | ``` 76 | 77 | Now, every `useKql` call will be directly use the Kirby instance by sending requests from the client: 78 | 79 | ```ts{3} 80 | const { data } = await useKql(query) 81 | ``` 82 | 83 | ## Type Declarations 84 | 85 | ```ts 86 | function useKql< 87 | ResT extends KirbyQueryResponse = KirbyQueryResponse, 88 | ReqT extends KirbyQueryRequest = KirbyQueryRequest 89 | >( 90 | query: MaybeRefOrGetter, 91 | opts?: UseKqlOptions 92 | ): AsyncData 93 | 94 | type UseKqlOptions = AsyncDataOptions & Pick< 95 | NitroFetchOptions, 96 | | 'onRequest' 97 | | 'onRequestError' 98 | | 'onResponse' 99 | | 'onResponseError' 100 | | 'headers' 101 | | 'retry' 102 | | 'retryDelay' 103 | | 'retryStatusCodes' 104 | | 'timeout' 105 | > & { 106 | /** 107 | * Language code to fetch data for in multi-language Kirby setups. 108 | */ 109 | language?: MaybeRefOrGetter 110 | /** 111 | * Cache the response between function calls for the same query. 112 | * @default true 113 | */ 114 | cache?: boolean 115 | /** 116 | * Watch an array of reactive sources and auto-refresh the fetch result when they change. 117 | * Query and language are watched by default. You can completely ignore reactive sources by using `watch: false`. 118 | * @default undefined 119 | */ 120 | watch?: MultiWatchSources | false 121 | } 122 | ``` 123 | 124 | `useKql` infers all of Nuxt's [`useAsyncData` options](https://nuxt.com/docs/api/composables/use-async-data#params). 125 | -------------------------------------------------------------------------------- /docs/config/index.md: -------------------------------------------------------------------------------- 1 | # Module Configuration 2 | 3 | ## Usage 4 | 5 | Adapt Nuxt KQL to your needs by setting module options in your `nuxt.config.ts`: 6 | 7 | ```ts 8 | // `nuxt.config.ts` 9 | export default defineNuxtConfig({ 10 | modules: ['nuxt-kql'], 11 | 12 | kql: { 13 | // ... Your options here 14 | } 15 | }) 16 | ``` 17 | 18 | ## Type Declarations 19 | 20 | See the types below for a complete list of options. 21 | 22 | ```ts 23 | interface ModuleOptions { 24 | /** 25 | * Kirby base URL, like `https://kirby.example.com` 26 | * 27 | * @default process.env.KIRBY_BASE_URL 28 | */ 29 | url?: string 30 | 31 | /** 32 | * Kirby KQL API route path 33 | * 34 | * @default 'api/query' // for `basic` authentication 35 | * @default 'api/kql' // for `bearer` authentication 36 | */ 37 | prefix?: string 38 | 39 | /** 40 | * Kirby API authentication method 41 | * 42 | * @remarks 43 | * Set to `none` to disable authentication 44 | * 45 | * @default 'basic' 46 | */ 47 | auth?: 'basic' | 'bearer' | 'none' 48 | 49 | /** 50 | * Token for bearer authentication 51 | * 52 | * @default process.env.KIRBY_API_TOKEN 53 | */ 54 | token?: string 55 | 56 | /** 57 | * Username/password pair for basic authentication 58 | * 59 | * @default { username: process.env.KIRBY_API_USERNAME, password: process.env.KIRBY_API_PASSWORD } 60 | */ 61 | credentials?: { 62 | username: string 63 | password: string 64 | } 65 | 66 | /** 67 | * Send client-side requests instead of using the server-side proxy 68 | * 69 | * @remarks 70 | * By default, KQL data is fetched safely with a server-side proxy. 71 | * If enabled, query requests will be be sent directly from the client. 72 | * Note: This means your token or user credentials will be publicly visible. 73 | * If Nuxt SSR is disabled, this option is enabled by default. 74 | * 75 | * @default false 76 | */ 77 | client?: boolean 78 | 79 | /** 80 | * Prefetch custom KQL queries at build-time 81 | * 82 | * @remarks 83 | * The queries will be fully typed and importable from `#nuxt-kql`. 84 | * 85 | * @default {} 86 | */ 87 | prefetch?: Record< 88 | string, 89 | KirbyQueryRequest | { query: KirbyQueryRequest, language: string } 90 | > 91 | 92 | /** 93 | * Server-side features 94 | */ 95 | server?: { 96 | /** 97 | * Enable server-side caching of queries using the Nitro cache API 98 | * 99 | * @see https://nitro.unjs.io/guide/cache 100 | */ 101 | cache?: boolean 102 | 103 | /** 104 | * Name of the storage mountpoint to use for caching 105 | * 106 | * @see https://nitro.unjs.io/guide/cache 107 | * @default 'cache' 108 | */ 109 | storage?: string 110 | 111 | /** 112 | * Enable stale-while-revalidate behavior (cache is returned while it is being updated) 113 | * 114 | * @see https://nitro.unjs.io/guide/cache#options 115 | * @default false 116 | */ 117 | swr?: boolean 118 | 119 | /** 120 | * Number of seconds to cache the query response 121 | * 122 | * @see https://nitro.unjs.io/guide/cache#options 123 | * @default 1 124 | */ 125 | maxAge?: number 126 | 127 | /** 128 | * Log verbose errors to the console if a query fails 129 | * 130 | * @remarks 131 | * This will log the full query to the console. Depending on the content of the query, this could be a security risk. 132 | * 133 | * @default false 134 | */ 135 | verboseErrors?: boolean 136 | } 137 | } 138 | ``` 139 | -------------------------------------------------------------------------------- /docs/faq/cors-issues.md: -------------------------------------------------------------------------------- 1 | # Can I Encounter CORS Issues? 2 | 3 | ## tl;dr 4 | 5 | No. 6 | 7 | ## Detailed Answer 8 | 9 | With the default module configuration, you will not have to deal with CORS issues. [`useKql`](/api/use-kql) and [`$kql`](/api/kql) pass given queries to the Nuxt server route `/api/__kirby__`. The query is fetched from the Kirby instance on the server-side and then passed back to the client. Thus, no data is fetched from the Kirby instance on the client-side. It is proxied by the Nuxt server and no CORS issues will occur. 10 | -------------------------------------------------------------------------------- /docs/faq/how-does-it-work.md: -------------------------------------------------------------------------------- 1 | # How It Works 2 | 3 | ## tl;dr 4 | 5 | The internal `/api/__kirby__` server route proxies KQL requests to the Kirby instance. 6 | 7 | ::: tip 8 | The proxy layer will not only pass through your API's response body to the response on the client, but also HTTP status code, HTTP status message and headers. This way, you can handle errors just like you would with a direct API call. 9 | ::: 10 | 11 | ## Detailed Answer 12 | 13 | The [`useKql`](/api/use-kql) and [`$kql`](/api/kql) composables will initiate a POST request to the Nuxt server route `/api/__kirby__` defined by this module. The KQL query will be encoded in the request body. 14 | 15 | The internal server route then fetches the actual data for a given query from the Kirby instance and passes the response back to the client. Thus, no KQL requests to the Kirby instance are initiated on the client-side. This proxy behavior has the benefit of omitting CORS issues, since data is sent from server to server. 16 | 17 | During server-side rendering, calls to the Nuxt server route will directly call the relevant function (emulating the request), saving an additional API call. Thus, only the fetch request from Nuxt to Kirby will be performed. 18 | 19 | ::: info 20 | Query responses are cached (with a key calculated by hashing the query) and hydrated to the client. Subsequent calls will return cached responses, saving duplicated requests. Head over to the [caching](/usage/caching) section to learn more. 21 | ::: 22 | -------------------------------------------------------------------------------- /docs/faq/what-is-kql.md: -------------------------------------------------------------------------------- 1 | # What Is KQL? 2 | 3 | ::: info 4 | You may visit the [KQL documentation](https://github.com/getkirby/kql) for a comprehensive introduction. 5 | ::: 6 | 7 | Kirby's Query Language API combines the flexibility of Kirby's data structures, the power of GraphQL and the simplicity of REST. 8 | 9 | The Kirby QL API takes POST requests with standard JSON objects and returns highly customized results that fit your application. 10 | 11 | Example request to the `/api/query` endpoint: 12 | 13 | ```json 14 | { 15 | "query": "page('photography').children", 16 | "select": { 17 | "url": true, 18 | "title": true, 19 | "text": "page.text.markdown", 20 | "images": { 21 | "query": "page.images", 22 | "select": { 23 | "url": true 24 | } 25 | } 26 | }, 27 | "pagination": { 28 | "limit": 10 29 | } 30 | } 31 | ``` 32 | 33 | **Response**: 34 | 35 | ```json 36 | { 37 | "code": 200, 38 | "status": "ok", 39 | "result": { 40 | "data": [ 41 | { 42 | "url": "https://example.com/photography/trees", 43 | "title": "Trees", 44 | "text": "Lorem ipsum …", 45 | "images": [ 46 | { "url": "https://example.com/media/pages/photography/trees/1353177920-1579007734/cheesy-autumn.jpg" }, 47 | { "url": "https://example.com/media/pages/photography/trees/1940579124-1579007734/last-tree-standing.jpg" }, 48 | { "url": "https://example.com/media/pages/photography/trees/3506294441-1579007734/monster-trees-in-the-fog.jpg" } 49 | ] 50 | }, 51 | { 52 | "url": "https://example.com/photography/sky", 53 | "title": "Sky", 54 | "text": "

Dolor sit amet

…", 55 | "images": [ 56 | { "url": "https://example.com/media/pages/photography/sky/183363500-1579007734/blood-moon.jpg" }, 57 | { "url": "https://example.com/media/pages/photography/sky/3904851178-1579007734/coconut-milkyway.jpg" } 58 | ] 59 | } 60 | ], 61 | "pagination": { 62 | "page": 1, 63 | "pages": 1, 64 | "offset": 0, 65 | "limit": 10, 66 | "total": 2 67 | } 68 | } 69 | } 70 | ``` 71 | -------------------------------------------------------------------------------- /docs/guide/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | This guide will walk you through the steps to get started with Nuxt KQL. 4 | 5 | ::: tip 6 | Choose on of the [starter kits](/guide/starters) and get started with Nuxt KQL in no time instead of starting from scratch. 7 | ::: 8 | 9 | ## Step 1: Install Nuxt KQL 10 | 11 | ```bash 12 | npx nuxi@latest module add kql 13 | ``` 14 | 15 | ## Step 2: Use the `nuxt-kql` Module 16 | 17 | Add `nuxt-kql` to your Nuxt config: 18 | 19 | ```ts 20 | // `nuxt.config.ts` 21 | export default defineNuxtConfig({ 22 | modules: ['nuxt-kql'] 23 | }) 24 | ``` 25 | 26 | ## Step 3: Set up the Environment 27 | 28 | Without a backend, Nuxt KQL will not be able to fetch queries. In order to do so, you have to point to a Kirby instance with the official [Kirby KQL](https://github.com/getkirby/kql) plugin installed. 29 | 30 | It is recommended to use the [Kirby Headless Starter](/guide/what-is-nuxt-kql#kirby-headless-plugin), which is a customized Kirby project template that includes the KQL plugin and a custom KQL endpoint `api/kql` that supports **token authentication**. 31 | 32 | Enable the `bearer` authentication method in your Nuxt config: 33 | 34 | ```ts 35 | // `nuxt.config.ts` 36 | export default defineNuxtConfig({ 37 | modules: ['nuxt-kql'], 38 | 39 | kql: { 40 | // Enable token-based authentication for the Kirby Headless Starter, 41 | // which includes a custom KQL endpoint `api/kql` 42 | auth: 'bearer' 43 | } 44 | }) 45 | ``` 46 | 47 | Nuxt KQL automatically reads your environment variables. Set the following environment variables in your project's `.env` file: 48 | 49 | ```ini 50 | # Base URL of your Kirby instance (without a path) 51 | KIRBY_BASE_URL=https://kirby.example.com 52 | KIRBY_API_TOKEN=your-token 53 | ``` 54 | 55 | ::: tip 56 | Although not recommended, you can also use basic authentication, follow the [basic authentication method](/usage/authentication-methods#basic-authentication) guide. 57 | ::: 58 | 59 | ## Step 4: Send Queries 60 | 61 | Use the globally available [`useKql`](/api/use-kql) composable to send queries: 62 | 63 | ```vue 64 | 73 | 74 | 80 | ``` 81 | 82 | ## Step. 5: Your Turn 83 | 84 | Create your own Nuxt KQL project and start building your website. 85 | 86 | I'm curious to see what you've built. [Drop me a line at](mailto:hello@johannschopplich.com) if you like! 87 | -------------------------------------------------------------------------------- /docs/guide/starters.md: -------------------------------------------------------------------------------- 1 | # Starter Kits 2 | 3 | Instead of starting from scratch, you can use one of the starter kits provided to get up and running quickly. Choose the one that best suits your needs. 4 | 5 | ## 🍫 Cacao Kit 6 | 7 | ::: info 8 | That's the one I use for my own projects. 9 | ::: 10 | 11 | The [🍫 Cacao Kit](https://github.com/johannschopplich/cacao-kit-frontend) provides a minimal but feature-rich Nuxt starter kit. It fetches content from the [🍫 Cacao Kit backend](https://github.com/johannschopplich/cacao-kit-backend), a headless Kirby instance. It is the evolved version of the Kirby Nuxt Starterkit (see below) and my best practice solution for building a Nuxt-based frontend on top of Kirby CMS. 12 | 13 | You can harness every feature Nuxt provides to build a server-side rendered application or even pre-render the content using [Nuxt's static generation](https://nuxt.com/docs/getting-started/deployment#static-hosting). 14 | 15 | Kirby's page structure is used as the source routing – you do not need to replicate the page structure in Nuxt. This makes it easy to add new pages in Kirby without touching the frontend. All pages are rendered by the [catch-all route](https://github.com/johannschopplich/cacao-kit-frontend/blob/main/pages/%5B...slug%5D.vue). 16 | 17 | A key design decision is: _Everything is a block_. All Kirby templates are designed to be block-based. This allows you to create complex pages in Kirby with a simple drag-and-drop interface. The frontend then renders these blocks in a flexible way. Of course, you do not have to stick to the block-first architecture. 18 | 19 | If this does not appeal to you, or if you need custom Kirby page templates with custom fields, you can always create Nuxt pages and query the content using KQL. See the [`pages/about.vue`](https://github.com/johannschopplich/cacao-kit-frontend/blob/main/pages/about.vue) page for an example. 20 | 21 | ## Kirby Nuxt Starter Kit 22 | 23 | The [Kirby Nuxt Starter Kit](https://github.com/johannschopplich/kirby-nuxt-starterkit) is a rewrite of the official Kirby Starter Kit with Nuxt and Nuxt KQL. It is configured to use **token-based authentication**, but can be used with **basic authentication** as well. 24 | 25 | ::: warning 26 | This starter kit is for educational purposes, to show how the official Kirby starter kit you know translates to the headless world. For production use, please use the [Cacao Kit](https://github.com/johannschopplich/cacao-kit-frontend) instead. 27 | ::: 28 | 29 | ## Playground 30 | 31 | Technically not a starter, but a way to show and test all the features of this module for development. A good place to start if you want to look at the code. 32 | 33 | Check out the [playground](https://github.com/johannschopplich/nuxt-kql/tree/main/playground) of this module for an example Nuxt project setup. To spin up your playground: 34 | 35 | 1. Duplicate the local `playground/.env.example` file as `playground/.env`. 36 | 2. Run `pnpm install` and `pnpm dev` to start the development server. 37 | -------------------------------------------------------------------------------- /docs/guide/what-is-nuxt-kql.md: -------------------------------------------------------------------------------- 1 | # What is Nuxt KQL? 2 | 3 | Nuxt KQL is a lightweight [Nuxt](https://nuxt.com) module to reliably retrieve data from your Kirby instance using the **Kirby Query Language API**. It keeps your authentication credentials safe and works on the server and client. 4 | 5 | ## Motivation 6 | 7 | Kirby lends itself well to a headless CMS. Setting up [KQL](https://github.com/getkirby/kql) is fairly easy, but fetching queries can be cumbersome at times. Not to mention CORS issues. This module solves these common problems by providing easy-to-use composables to query your Kirby instance with KQL. 8 | 9 | With provided composables like [`useKql`](/api/use-kql), your KQL responses are cached and authorization is handled for you right out of the box: 10 | 11 | ```ts 12 | const { data, error } = await useKql({ 13 | query: 'site', 14 | select: ['title', 'children'] 15 | }) 16 | ``` 17 | 18 | Most importantly, your Kirby authentication credentials are protected when fetching data, even on the client. 19 | 20 | ## Kirby Headless Plugin 21 | 22 | Setting up Kirby to support headless mode is a bit of a hassle. To make your life easier, you can use the [Kirby Headless plugin](https://github.com/johannschopplich/kirby-headless). It provides a custom KQL endpoint with token-based authentication and other useful features. You do not have to use it, but it is the best way to use Kirby as a headless CMS and avoids common pitfalls like CORS issues. This Nuxt module is designed to work with it. 23 | 24 | With the Kirby Headless plugin you are ready to go: 25 | 26 | - 🧩 Optional bearer token authentication for [KQL](https://kirby.tools/docs/headless/usage#kirby-query-language-kql) and custom API endpoints 27 | - 🧱 Resolve fields in blocks: [UUIDs to file and page objects](https://kirby.tools/docs/headless/field-methods) or [any other field](https://kirby.tools/docs/headless/field-methods) 28 | - ⚡️ Cached KQL queries 29 | - 🌐 Multi-language support for KQL queries 30 | - 😵 Built-in CORS handling 31 | - 🍢 Express-esque [API builder](https://kirby.tools/docs/headless/api-builder) with middleware support 32 | - 🗂 Return [JSON from templates](https://kirby.tools/docs/headless/usage#json-templates) instead of HTML 33 | 34 | ## Nuxt Starter Kits 35 | 36 | Instead of building your Nuxt project from scratch, you can use one of the provided [starter kits](/guide/starters) to get up and running quickly. Choose the one that best suits your needs. 37 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | titleTemplate: Kirby's Query Language API for Nuxt 4 | hero: 5 | name: Nuxt KQL 6 | text: Kirby's Query Language API for Nuxt 7 | tagline: Fetch Kirby CMS data with KQL queries 8 | image: 9 | src: /logo-shadow.svg 10 | alt: Nuxt KQL Logo 11 | actions: 12 | - theme: brand 13 | text: Get Started 14 | link: /guide/what-is-nuxt-kql 15 | - theme: alt 16 | text: API 17 | link: /api/ 18 | - theme: alt 19 | text: View on GitHub 20 | link: https://github.com/johannschopplich/nuxt-kql 21 | 22 | features: 23 | - title: Protected Kirby Credentials 24 | icon: 🔒 25 | details: A Nuxt server route proxies your queries. No CORS issues! 26 | link: /faq/how-does-it-work 27 | linkText: How It Works 28 | - title: Authentication Methods 29 | icon: 🪢 30 | details: Bearer token or basic authentication with the Kirby Headless plugin. 31 | link: /usage/authentication-methods 32 | linkText: Authentication Methods 33 | - title: Familiar Data Handling 34 | icon: 🤹 35 | details: Handle query requests just like with Nuxt's useFetch composable. Caching included. 36 | link: /api/use-kql 37 | linkText: useKql 38 | - title: Multiple Starter Kits 39 | icon: 🦦 40 | details: Not sure where to start? Choose from three starter kits. 41 | link: /guide/starters 42 | linkText: Starter Kits 43 | --- 44 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vitepress dev", 7 | "build": "vitepress build", 8 | "preview": "vitepress preview" 9 | }, 10 | "devDependencies": { 11 | "vitepress": "^1.6.3" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /docs/public/_headers: -------------------------------------------------------------------------------- 1 | /assets/* 2 | cache-control: max-age=31536000 3 | cache-control: immutable 4 | -------------------------------------------------------------------------------- /docs/public/logo-shadow.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/public/og.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johannschopplich/nuxt-kql/e6f65b4eea838d6dd50afedb8c7d6b0284f127b9/docs/public/og.jpg -------------------------------------------------------------------------------- /docs/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | -------------------------------------------------------------------------------- /docs/usage/authentication-methods.md: -------------------------------------------------------------------------------- 1 | # Authentication 2 | 3 | ::: tip 4 | Nuxt KQL is designed to be used with the [Kirby Headless plugin](/guide/what-is-nuxt-kql#kirby-headless-plugin), a collection of best practices and tools for building headless Kirby projects. It includes a custom KQL endpoint that supports token-based authentication out of the box. 5 | ::: 6 | 7 | Depending on your Kirby setup and project requirements, you can use one of the following authentication methods: 8 | 9 | ## Token-Based Authentication 10 | 11 | Nuxt KQL supports the use of a bearer token for authentication when coupled with the [Kirby Headless plugin](https://github.com/johannschopplich/kirby-headless). It supports KQL with token authentication out of the box, unlike the default Kirby API which requires basic authentication for API endpoints. 12 | 13 | Enable token-based authentication in your Nuxt project's `nuxt.config.ts` file: 14 | 15 | ```ts 16 | // `nuxt.config.ts` 17 | export default defineNuxtConfig({ 18 | modules: ['nuxt-kql'], 19 | 20 | kql: { 21 | // Enable token-based authentication for the Kirby Headless plugin, 22 | // which includes a custom KQL endpoint `api/kql` 23 | auth: 'bearer' 24 | } 25 | }) 26 | ``` 27 | 28 | Set the following environment variables in your project's `.env` file: 29 | 30 | ```ini 31 | KIRBY_BASE_URL=https://kirby.example.com 32 | KIRBY_API_TOKEN=your-token 33 | ``` 34 | 35 | ::: info 36 | Make sure to set the same token as the `KIRBY_HEADLESS_API_TOKEN` environment variable in your headless Kirby project's `.env` file. 37 | ::: 38 | 39 | ## Basic Authentication 40 | 41 | If you do not want to build your KQL API using [Kirby Headless plugin](https://github.com/johannschopplich/kirby-headless), you can use basic authentication. However, this method is not recommended for production environments due to security concerns. 42 | 43 | ::: tip 44 | The default KQL endpoint `/api/query` [requires authentication](https://getkirby.com/docs/guide/api/authentication). You have to enable HTTP basic authentication in your Kirby project's `config.php` file: 45 | 46 | ```php 47 | // `site/config/config.php` 48 | return [ 49 | 'api' => [ 50 | 'basicAuth' => true, 51 | // For local development, you may want to disable SSL verification 52 | 'allowInsecure' => true 53 | ] 54 | ]; 55 | ``` 56 | 57 | ::: 58 | 59 | Nuxt KQL will automatically read your environment variables. Create an `.env` file in your project (or edit the existing one) and add the following environment variables: 60 | 61 | ```ini 62 | KIRBY_BASE_URL=https://kirby.example.com 63 | KIRBY_API_USERNAME=your-username 64 | KIRBY_API_PASSWORD=your-password 65 | ``` 66 | -------------------------------------------------------------------------------- /docs/usage/batching-queries.md: -------------------------------------------------------------------------------- 1 | # Batching Queries 2 | 3 | If multiple query requests are needed, they can be combined into a single request. This is useful for reducing the number of requests to the server and improving performance. 4 | 5 | The Kirby Query Language supports nested queries by defining a query object with multiple keys. Each key represents a query request that can be executed in parallel: 6 | 7 | ```ts 8 | // Optional: Queries can be imported from a separate directory for cleaner code 9 | import { articlesQuery, navigationQuery, siteQuery } from '~/queries' 10 | 11 | // Combine the queries in a single nested request 12 | const { data } = await useKql({ 13 | query: 'site', 14 | select: { 15 | site: siteQuery, 16 | articles: articlesQuery, 17 | navigation: navigationQuery 18 | } 19 | }) 20 | 21 | // Extract the data from the response 22 | const site = computed(() => data.value?.result.site) 23 | const articles = computed(() => data.value?.result.articles) 24 | const navigation = computed(() => data.value?.result.navigation) 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/usage/caching.md: -------------------------------------------------------------------------------- 1 | # Caching 2 | 3 | ## Client-Side Caching 4 | 5 | Query responses using the built-in [composables](/api/#composables) are cached by default. This means that if you call the same query multiple times, the response will be cached and returned from the cache on subsequent calls. 6 | 7 | For each query, a hash is calculated based on the query string and the requested language (in multi-language setups). 8 | 9 | If you want to disable caching, you can do so by setting the `cache` option to `false`: 10 | 11 | ```ts 12 | const { data } = await useKql( 13 | query, 14 | { 15 | // Disable in-memory caching 16 | cache: false 17 | } 18 | ) 19 | ``` 20 | 21 | ## Clearing the Cache 22 | 23 | ```ts 24 | const { data, refresh, clear } = await useKql({ query: 'site' }) 25 | 26 | async function invalidateAndRefresh() { 27 | clear() 28 | await refresh() 29 | } 30 | ``` 31 | 32 | ## Server-Side Caching 33 | 34 | Nuxt KQL allows you opt-in to server-side caching of query responses. It does this by using the [cache API](https://nitro.build/guide/cache) of Nuxt's underlying server engine, [Nitro](https://nitro.build). Query responses are cached in-memory by default, but you can use any storage mountpoint supported by Nitro. The full list of built-in storage mountpoints can be found in the [unstorage documentation](https://unstorage.unjs.io). 35 | 36 | For short, concurrent requests on your site, caching will make a big difference in performance because the query response will be served from the cache rather than fetched from the server. The default expiration time is set to 60 minutes. 37 | 38 | You can enable server-side caching by setting the `server.cache` module option to `true`. You can also set a custom expiration time in seconds by setting the `server.maxAge` option: 39 | 40 | ```ts 41 | // `nuxt.config.ts` 42 | export default defineNuxtConfig({ 43 | modules: ['nuxt-kql'], 44 | 45 | kql: { 46 | server: { 47 | // Enable server-side caching 48 | // @default false 49 | cache: true, 50 | // Number of seconds to cache the query response 51 | // @default 1 52 | maxAge: 1 53 | } 54 | } 55 | }) 56 | ``` 57 | 58 | A custom storage mountpoint is appropriate for production environments. For example, if you are deploying to Cloudflare, the Clouflare KV storage mountpoint is a good choice. For development, you can use the built-in `fs` storage mountpoint. 59 | 60 | To define a custom storage mountpoint, use the `storage` option of the Nuxt KQL module. In the example above, we use the `kql` storage mountpoint to store the query responses. 61 | 62 | However, this custom storage mountpoint is not yet defined. To make it available, we need to mount it in the `nitro` section of our `nuxt.config.ts`: 63 | 64 | ```ts 65 | // `nuxt.config.ts` 66 | export default defineNuxtConfig({ 67 | nitro: { 68 | storage: { 69 | kql: { 70 | // https://unstorage.unjs.io/drivers/cloudflare 71 | driver: 'cloudflareKVBinding', 72 | // Make sure to link the namespace in your worker settings 73 | binding: 'KV_BINDING' 74 | } 75 | }, 76 | // Make sure to define a fallback storage mountpoint for 77 | // local development, since the Cloudflare KV binding is 78 | // not available locally 79 | devStorage: { 80 | kql: { 81 | driver: 'fs', 82 | base: '.data', 83 | }, 84 | }, 85 | } 86 | }) 87 | ``` 88 | -------------------------------------------------------------------------------- /docs/usage/error-handling.md: -------------------------------------------------------------------------------- 1 | # Error Handling 2 | 3 | While the idea of this Nuxt module is to mask your Kirby API (and credentials) inside the [server proxy](/faq/how-does-it-work), Nuxt KQL will minimize the hassle of handling errors by passing through the following properties to the response on the client: 4 | 5 | - Response Body 6 | - HTTP Status Code 7 | - HTTP Status Message 8 | - Headers 9 | 10 | So if a request to Kirby fails, you can still handle the error response in your Nuxt application just as you would with a direct API call. In this case, both [`useKql`](/api/use-kql) and [`$kql`](/api/kql) will throw a `NuxtError`. 11 | 12 | Logging the available error properties will give you insight into what went wrong: 13 | 14 | ```ts 15 | // `data` will be of type `KirbyQueryResponse` if the request to Kirby itself succeeded 16 | const { data, error } = await useKql({ query: 'site' }) 17 | 18 | // Log the error if the request to Kirby failed 19 | console.log( 20 | 'Request failed with:', 21 | error.value.statusCode, 22 | error.value.statusMessage, 23 | // Response body 24 | error.value.data 25 | ) 26 | ``` 27 | 28 | ## `NuxtError` Type Declaration 29 | 30 | ```ts 31 | interface NuxtError extends H3Error {} 32 | 33 | // See https://github.com/unjs/h3 34 | class H3Error extends Error { 35 | static __h3_error__: boolean 36 | statusCode: number 37 | fatal: boolean 38 | unhandled: boolean 39 | statusMessage?: string 40 | data?: DataT 41 | cause?: unknown 42 | constructor(message: string, opts?: { 43 | cause?: unknown 44 | }) 45 | toJSON(): Pick, 'data' | 'statusCode' | 'statusMessage' | 'message'> 46 | } 47 | ``` 48 | -------------------------------------------------------------------------------- /docs/usage/multi-language-sites.md: -------------------------------------------------------------------------------- 1 | # Multi-Language Sites 2 | 3 | Nuxt KQL provides seamless integration with multi-language Kirby sites using [Nuxt i18n](https://i18n.nuxtjs.org). This guide shows you how to set up and handle translated content effectively. 4 | 5 | ## Prerequisites 6 | 7 | Install the official Nuxt i18n module before proceeding: 8 | 9 | ```bash 10 | npx nuxi@latest module add i18n 11 | ``` 12 | 13 | ## Basic Usage 14 | 15 | To fetch language-specific content, pass the `language` option with your query request. You can get the current locale from the `useI18n` composable: 16 | 17 | ```ts 18 | const { locale } = useI18n() 19 | 20 | // Get the German translation of the about page 21 | const { data } = await useKql( 22 | { query: 'page("about")' }, 23 | { language: locale.value } 24 | ) 25 | ``` 26 | 27 | ## Handling Dynamic Routes 28 | 29 | For dynamic routes like `pages/[...slug].vue`, you will need to handle both the language code and the page slug. 30 | 31 | ### Path Slug Without Locale Prefix 32 | 33 | First, create a utility function in `utils/locale.ts` to extract the non-localized slug: 34 | 35 | ```ts 36 | export function getNonLocalizedSlug( 37 | param: string | string[], 38 | locales: string[] = [] 39 | ) { 40 | if (Array.isArray(param)) { 41 | param = param.filter(Boolean) 42 | 43 | // Remove locale prefix if present 44 | if (param.length > 0 && locales.includes(param[0])) { 45 | param = param.slice(1) 46 | } 47 | 48 | return param.join('/') 49 | } 50 | 51 | return param 52 | } 53 | ``` 54 | 55 | ### Dynamic Page Component 56 | 57 | A dynamic page component should handle translations and slug mismatches: 58 | 59 | ```vue 60 | 81 | ``` 82 | 83 | ## Error Handling & Redirects 84 | 85 | When working with translated content, you may want to handle pages that do not exist in the requested language. You can do this by loading an error page in the current language and setting the appropriate HTTP status code: 86 | 87 | ```ts 88 | // Handle missing content 89 | if (!pageData.value?.result) { 90 | // Load error page in current language 91 | const { data } = await useKql( 92 | { query: 'page("error")' }, 93 | { language: locale.value } 94 | ) 95 | 96 | const event = useRequestEvent() 97 | if (event) 98 | setResponseStatus(event, 404) 99 | 100 | // Use error page data for rendering... 101 | } 102 | ``` 103 | -------------------------------------------------------------------------------- /docs/usage/prefetching-queries.md: -------------------------------------------------------------------------------- 1 | # Prefetching Queries 2 | 3 | Nuxt KQL allows you to prefetch custom queries **at build time** (during development and in production). This is recommended for infrequently changing data to improve performance in production. You can prefetch as many queries as you like. The query results as well as their TypeScript type can be imported from `#nuxt-kql`. 4 | 5 | To get started, define a **key** for your query as well as the actual query request. Let's use `site` as an example key and just get the site title: 6 | 7 | ```ts 8 | // `nuxt.config.ts` 9 | export default defineNuxtConfig({ 10 | modules: ['nuxt-kql'], 11 | 12 | kql: { 13 | // Prefetch queries at build-time 14 | prefetch: { 15 | // Define key and query 16 | site: { 17 | query: 'site', 18 | select: ['title'] 19 | } 20 | } 21 | } 22 | }) 23 | ``` 24 | 25 | When you run your dev or build command, the result of the `site` query is fetched, stored locally, and the time spent on the query(s) is logged. 26 | 27 | ``` 28 | ℹ Prefetched site KQL query in 170ms 29 | ``` 30 | 31 | Now you can import the query result and its TypeScript type in your application: 32 | 33 | ```ts 34 | // Import the type for the query result below 35 | import type { Site } from '#nuxt-kql' 36 | 37 | // Import the query result following the query's given name in `nuxt.config.ts` 38 | import { site } from '#nuxt-kql' 39 | ``` 40 | 41 | ::: info 42 | The actual **type** for the key will be pascal-cased, e.g. the key `somePageKey` will result in the type `SomePageKey`. 43 | ::: 44 | 45 | ## Multi-Language Queries 46 | 47 | If you have a [multi-language](/usage/multi-language-sites) Kirby instance, you can specify the language for each query individually. Let's use `site` as an example key and get the site title in German: 48 | 49 | ```ts 50 | // `nuxt.config.ts` 51 | export default defineNuxtConfig({ 52 | modules: ['nuxt-kql'], 53 | 54 | kql: { 55 | prefetch: { 56 | site: { 57 | // Define a query and its language 58 | query: { 59 | query: 'site', 60 | select: ['title'] 61 | }, 62 | language: 'de' 63 | } 64 | } 65 | } 66 | }) 67 | ``` 68 | 69 | ::: info 70 | Note that the actual `query` to be fetched is nested under the `query` key. 71 | ::: 72 | -------------------------------------------------------------------------------- /docs/usage/typed-query-results.md: -------------------------------------------------------------------------------- 1 | # Typed Query Results 2 | 3 | For the best TypeScript experience, you may want to define your own response types for [`useKql`](/api/use-kql), which will help catch errors in your template. 4 | 5 | The [`KirbyQueryResponse`](/api/types-query-response) accepts the generic type parameter `T` used for the query result type. 6 | 7 | ```ts 8 | // Extend the default response type with the result we expect from the query response 9 | await useKql>({ 10 | query: 'site', 11 | select: ['title'], 12 | }) 13 | ``` 14 | 15 | ## Example 16 | 17 | By creating a custom `KirbySite` type for the expected response result and passed to the `KirbyQueryResponse` as its first type parameter, the `data` reactive variable will be provided with typings: 18 | 19 | ```vue 20 | 49 | 50 | 56 | ``` 57 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import antfu from '@antfu/eslint-config' 3 | 4 | export default antfu({ 5 | vue: true, 6 | }) 7 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "docs/.vitepress/dist" 3 | command = "pnpm run dev:prepare && pnpm run docs:build" 4 | 5 | [[redirects]] 6 | from = "/*" 7 | to = "/index.html" 8 | status = 200 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-kql", 3 | "type": "module", 4 | "version": "1.5.5", 5 | "packageManager": "pnpm@10.11.0", 6 | "description": "Kirby's Query Language API for Nuxt", 7 | "author": "Johann Schopplich ", 8 | "license": "MIT", 9 | "homepage": "https://nuxt-kql.byjohann.dev", 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/johannschopplich/nuxt-kql.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/johannschopplich/nuxt-kql/issues" 16 | }, 17 | "keywords": [ 18 | "getkirby", 19 | "kirby", 20 | "kql", 21 | "nuxt", 22 | "query" 23 | ], 24 | "sideEffects": false, 25 | "exports": { 26 | ".": { 27 | "types": "./dist/types.d.mts", 28 | "default": "./dist/module.mjs" 29 | } 30 | }, 31 | "main": "./dist/module.mjs", 32 | "types": "./dist/types.d.mts", 33 | "files": [ 34 | "dist" 35 | ], 36 | "scripts": { 37 | "prepack": "nuxt-module-build build", 38 | "dev": "nuxi dev playground", 39 | "dev:build": "nuxi build playground", 40 | "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground && nuxi prepare test/fixture", 41 | "docs:dev": "vitepress dev docs", 42 | "docs:build": "vitepress build docs", 43 | "docs:preview": "vitepress preview docs", 44 | "lint": "eslint .", 45 | "lint:fix": "eslint . --fix", 46 | "test": "vitest", 47 | "test:types": "tsc --noEmit", 48 | "release": "bumpp" 49 | }, 50 | "dependencies": { 51 | "@nuxt/kit": "^3.17.4", 52 | "@vueuse/core": "^13.3.0", 53 | "defu": "^6.1.4", 54 | "kirby-types": "^1.0.0", 55 | "ofetch": "^1.4.1", 56 | "ohash": "^2.0.11", 57 | "pathe": "^2.0.3", 58 | "scule": "^1.3.0", 59 | "ufo": "^1.6.1", 60 | "uint8array-extras": "^1.4.0" 61 | }, 62 | "devDependencies": { 63 | "@antfu/eslint-config": "^4.13.2", 64 | "@nuxt/module-builder": "^1.0.1", 65 | "@nuxt/test-utils": "^3.19.1", 66 | "@types/node": "^22.15.24", 67 | "bumpp": "^10.1.1", 68 | "destr": "^2.0.5", 69 | "eslint": "^9.27.0", 70 | "nuxt": "^3.17.4", 71 | "typescript": "^5.8.3", 72 | "vitest": "^3.1.4" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /playground/.env.example: -------------------------------------------------------------------------------- 1 | KIRBY_BASE_URL=https://kirby-headless-starter.byjohann.dev 2 | KIRBY_API_TOKEN=test 3 | -------------------------------------------------------------------------------- /playground/app.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /playground/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 28 | -------------------------------------------------------------------------------- /playground/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtConfig } from 'nuxt/config' 2 | import NuxtKql from '../src/module' 3 | 4 | export default defineNuxtConfig({ 5 | modules: [NuxtKql], 6 | 7 | compatibilityDate: '2025-01-01', 8 | 9 | kql: { 10 | // Enable token-based authentication 11 | auth: 'bearer', 12 | 13 | // Send client-side query requests to Kirby instead of the KQL proxy 14 | // client: true, 15 | 16 | // Prefetch queries at build-time 17 | prefetch: { 18 | site: { 19 | query: 'site', 20 | select: { 21 | title: true, 22 | children: { 23 | query: 'site.children', 24 | select: { 25 | id: true, 26 | title: true, 27 | isListed: true, 28 | }, 29 | }, 30 | }, 31 | }, 32 | }, 33 | 34 | server: { 35 | cache: true, 36 | }, 37 | }, 38 | }) 39 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "dev": "nuxi dev", 7 | "build": "nuxi build", 8 | "generate": "nuxi generate" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /playground/pages/blocks.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 47 | -------------------------------------------------------------------------------- /playground/pages/client-query.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 42 | -------------------------------------------------------------------------------- /playground/pages/index.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 66 | -------------------------------------------------------------------------------- /playground/pages/prefetched-queries.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /playground/server/api/kql.ts: -------------------------------------------------------------------------------- 1 | import { $kql, defineEventHandler } from '#imports' 2 | 3 | export default defineEventHandler(() => { 4 | return $kql({ 5 | query: 'site', 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /playground/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - docs 3 | - playground 4 | onlyBuiltDependencies: 5 | - '@parcel/watcher' 6 | - esbuild 7 | - unrs-resolver 8 | -------------------------------------------------------------------------------- /src/kit.ts: -------------------------------------------------------------------------------- 1 | import type { ConsolaInstance } from 'consola' 2 | import { useLogger } from '@nuxt/kit' 3 | 4 | export const logger: ConsolaInstance = useLogger('nuxt-kql') 5 | -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | import type { KirbyQueryRequest } from 'kirby-types' 2 | import process from 'node:process' 3 | import { addImports, addServerHandler, addTemplate, createResolver, defineNuxtModule } from '@nuxt/kit' 4 | import { defu } from 'defu' 5 | import { join } from 'pathe' 6 | import { pascalCase } from 'scule' 7 | import { withLeadingSlash } from 'ufo' 8 | import { name } from '../package.json' 9 | import { logger } from './kit' 10 | import { prefetchQueries } from './prefetch' 11 | 12 | const KIRBY_TYPES_PKG_EXPORT_NAMES = [ 13 | 'KirbyApiResponse', 14 | 'KirbyBlock', 15 | 'KirbyDefaultBlockType', 16 | 'KirbyDefaultBlocks', 17 | 'KirbyLayout', 18 | 'KirbyLayoutColumn', 19 | 'KirbyQuery', 20 | 'KirbyQueryChain', 21 | 'KirbyQueryModel', 22 | 'KirbyQueryRequest', 23 | 'KirbyQueryResponse', 24 | 'KirbyQuerySchema', 25 | ] 26 | 27 | export interface ModuleOptions { 28 | /** 29 | * Kirby base URL, like `https://kirby.example.com` 30 | * 31 | * @default process.env.KIRBY_BASE_URL 32 | */ 33 | url?: string 34 | 35 | /** 36 | * Kirby KQL API route path 37 | * 38 | * @default 'api/query' // for `basic` authentication 39 | * @default 'api/kql' // for `bearer` authentication 40 | */ 41 | prefix?: string 42 | 43 | /** 44 | * Kirby API authentication method 45 | * 46 | * @remarks 47 | * Set to `none` to disable authentication 48 | * 49 | * @default 'basic' 50 | */ 51 | auth?: 'basic' | 'bearer' | 'none' 52 | 53 | /** 54 | * Token for bearer authentication 55 | * 56 | * @default process.env.KIRBY_API_TOKEN 57 | */ 58 | token?: string 59 | 60 | /** 61 | * Username/password pair for basic authentication 62 | * 63 | * @default { username: process.env.KIRBY_API_USERNAME, password: process.env.KIRBY_API_PASSWORD } 64 | */ 65 | credentials?: { 66 | username: string 67 | password: string 68 | } 69 | 70 | /** 71 | * Send client-side requests instead of using the server-side proxy 72 | * 73 | * @remarks 74 | * By default, KQL data is fetched safely with a server-side proxy. 75 | * If enabled, query requests will be be sent directly from the client. 76 | * Note: This means your token or user credentials will be publicly visible. 77 | * If Nuxt SSR is disabled, this option is enabled by default. 78 | * 79 | * @default false 80 | */ 81 | client?: boolean 82 | 83 | /** 84 | * Prefetch custom KQL queries at build-time 85 | * 86 | * @remarks 87 | * The queries will be fully typed and importable from `#nuxt-kql`. 88 | * 89 | * @default {} 90 | */ 91 | prefetch?: Record< 92 | string, 93 | KirbyQueryRequest | { query: KirbyQueryRequest, language: string } 94 | > 95 | 96 | /** 97 | * Server-side features 98 | */ 99 | server?: { 100 | /** 101 | * Enable server-side caching of queries using the Nitro cache API 102 | * 103 | * @see https://nitro.unjs.io/guide/cache 104 | */ 105 | cache?: boolean 106 | 107 | /** 108 | * Name of the storage mountpoint to use for caching 109 | * 110 | * @see https://nitro.unjs.io/guide/cache 111 | * @default 'cache' 112 | */ 113 | storage?: string 114 | 115 | /** 116 | * Enable stale-while-revalidate behavior (cache is returned while it is being updated) 117 | * 118 | * @see https://nitro.unjs.io/guide/cache#options 119 | * @default false 120 | */ 121 | swr?: boolean 122 | 123 | /** 124 | * Number of seconds to cache the query response 125 | * 126 | * @see https://nitro.unjs.io/guide/cache#options 127 | * @default 1 128 | */ 129 | maxAge?: number 130 | 131 | /** 132 | * Log verbose errors to the console if a query fails 133 | * 134 | * @remarks 135 | * This will log the full query to the console. Depending on the content of the query, this could be a security risk. 136 | * 137 | * @default false 138 | */ 139 | verboseErrors?: boolean 140 | } 141 | } 142 | 143 | declare module '@nuxt/schema' { 144 | interface RuntimeConfig { 145 | kql: ModuleOptions 146 | } 147 | } 148 | 149 | export default defineNuxtModule({ 150 | meta: { 151 | name, 152 | configKey: 'kql', 153 | compatibility: { 154 | nuxt: '>=3.7', 155 | }, 156 | }, 157 | defaults: { 158 | url: process.env.KIRBY_BASE_URL || '', 159 | prefix: '', 160 | auth: 'basic', 161 | token: process.env.KIRBY_API_TOKEN || '', 162 | credentials: { 163 | username: process.env.KIRBY_API_USERNAME || '', 164 | password: process.env.KIRBY_API_PASSWORD || '', 165 | }, 166 | client: false, 167 | prefetch: {}, 168 | server: { 169 | cache: false, 170 | storage: 'cache', 171 | swr: false, 172 | maxAge: 1, 173 | verboseErrors: false, 174 | }, 175 | }, 176 | async setup(options, nuxt) { 177 | const moduleName = name 178 | 179 | // Make sure Kirby URL and KQL endpoint are set 180 | if (!options.url) 181 | logger.error('Missing `KIRBY_BASE_URL` environment variable') 182 | 183 | // Make sure authentication credentials are set 184 | if (options.auth === 'basic' && (!options.credentials || !options.credentials.username || !options.credentials.password)) 185 | logger.error('Missing `KIRBY_API_USERNAME` and `KIRBY_API_PASSWORD` environment variable for basic authentication') 186 | 187 | if (options.auth === 'bearer' && !options.token) 188 | logger.error('Missing `KIRBY_API_TOKEN` environment variable for bearer authentication') 189 | 190 | if (!options.prefix) { 191 | if (options.auth === 'basic') 192 | options.prefix = 'api/query' 193 | else if (options.auth === 'bearer') 194 | options.prefix = 'api/kql' 195 | } 196 | 197 | if (!nuxt.options.ssr) { 198 | logger.info('KQL requests are client-only because SSR is disabled') 199 | options.client = true 200 | } 201 | 202 | if (options.server) { 203 | // The Nitro storage mountpoint requires a leading slash 204 | options.server.storage ||= 'cache' 205 | options.server.storage = withLeadingSlash(options.server.storage) 206 | } 207 | 208 | // Private runtime config 209 | nuxt.options.runtimeConfig.kql = defu( 210 | nuxt.options.runtimeConfig.kql, 211 | options, 212 | ) 213 | 214 | // Write data to public runtime config if client requests are enabled 215 | nuxt.options.runtimeConfig.public.kql = defu( 216 | nuxt.options.runtimeConfig.public.kql as Required, 217 | options.client 218 | ? options 219 | : { client: false }, 220 | ) 221 | 222 | // Transpile runtime 223 | const { resolve } = createResolver(import.meta.url) 224 | nuxt.options.build.transpile.push(resolve('runtime')) 225 | 226 | // Add KQL proxy endpoint to send queries server-side 227 | addServerHandler({ 228 | route: '/api/__kirby__/:key', 229 | handler: resolve('runtime/server/handler'), 230 | method: 'post', 231 | }) 232 | 233 | // Add KQL composables 234 | addImports( 235 | ['$kirby', '$kql', 'useKirbyData', 'useKql'].map(name => ({ 236 | name, 237 | as: name, 238 | from: resolve(`runtime/composables/${name}`), 239 | })), 240 | ) 241 | 242 | nuxt.hooks.hook('nitro:config', (config) => { 243 | // Inline local server handler dependencies into Nitro bundle 244 | // Needed to circumvent "cannot find module" error in `server.ts` for the `utils` import 245 | config.externals ||= {} 246 | config.externals.inline ||= [] 247 | config.externals.inline.push(resolve('runtime/utils')) 248 | 249 | // Add Nitro auto-imports for composables 250 | config.imports = defu(config.imports, { 251 | presets: [{ 252 | from: resolve('runtime/server/imports'), 253 | imports: ['$kirby', '$kql'], 254 | }], 255 | }) 256 | }) 257 | 258 | // Add `#nuxt-kql` module alias 259 | nuxt.options.alias[`#${moduleName}`] = join(nuxt.options.buildDir, `module/${moduleName}`) 260 | 261 | // Prefetch custom KQL queries at build-time 262 | const prefetchedQueries = await prefetchQueries(options) 263 | 264 | // Add `#nuxt-kql` module template 265 | addTemplate({ 266 | filename: `module/${moduleName}.ts`, 267 | write: true, 268 | getContents() { 269 | return ` 270 | // Generated by ${moduleName} 271 | export type { ${KIRBY_TYPES_PKG_EXPORT_NAMES.join(', ')} } from 'kirby-types' 272 | 273 | ${[...prefetchedQueries.entries()].map(([key, response]) => ` 274 | export const ${key} = ${JSON.stringify(response?.result || null, undefined, 2)} 275 | export type ${pascalCase(key)} = typeof ${key} 276 | `.trimStart()).join('') || `export {}\n`}`.trimStart() 277 | }, 278 | }) 279 | }, 280 | }) 281 | -------------------------------------------------------------------------------- /src/prefetch.ts: -------------------------------------------------------------------------------- 1 | import type { KirbyQueryResponse } from 'kirby-types' 2 | import type { FetchError } from 'ofetch' 3 | import type { ModuleOptions } from './module' 4 | import { ofetch } from 'ofetch' 5 | import { logger } from './kit' 6 | import { createAuthHeader } from './runtime/utils' 7 | 8 | export async function prefetchQueries( 9 | options: ModuleOptions, 10 | ): Promise> { 11 | const results = new Map() 12 | 13 | if (!options.prefetch || Object.keys(options.prefetch).length === 0) 14 | return results 15 | 16 | if (!options.url) { 17 | logger.error('Skipping KQL prefetch, since no Kirby base URL is provided') 18 | return results 19 | } 20 | 21 | const start = Date.now() 22 | 23 | for (const [key, query] of Object.entries(options.prefetch)) { 24 | const language = 'language' in query ? query.language : undefined 25 | 26 | if (language && !query.query) { 27 | logger.error(`Prefetch KQL query "${key}" requires the "query" property in multi-language mode`) 28 | continue 29 | } 30 | 31 | try { 32 | results.set( 33 | key, 34 | await ofetch(options.prefix!, { 35 | baseURL: options.url, 36 | method: 'POST', 37 | body: language ? query.query : query, 38 | headers: { 39 | ...createAuthHeader(options), 40 | ...(language && { 'X-Language': language }), 41 | }, 42 | }), 43 | ) 44 | } 45 | catch (error) { 46 | const _error = error as FetchError 47 | logger.error( 48 | `Prefetch KQL query "${key}" failed${ 49 | _error.status 50 | ? ` with status code ${_error.status}:\n${JSON.stringify(_error.data, undefined, 2)}` 51 | : `: ${_error}` 52 | }`, 53 | ) 54 | } 55 | } 56 | 57 | if (results.size > 0) { 58 | const firstKey = results.keys().next().value as string 59 | 60 | logger.info( 61 | `Prefetched ${results.size === 1 ? '' : `${results.size} `}KQL ${ 62 | results.size === 1 ? `query "${firstKey}"` : 'queries' 63 | } in ${Date.now() - start}ms`, 64 | ) 65 | } 66 | 67 | return results 68 | } 69 | -------------------------------------------------------------------------------- /src/runtime/composables/$kirby.ts: -------------------------------------------------------------------------------- 1 | import type { NitroFetchOptions } from 'nitropack' 2 | import type { ModuleOptions } from '../../module' 3 | import type { ServerFetchOptions } from '../types' 4 | import { useNuxtApp, useRuntimeConfig } from '#imports' 5 | import { hash } from 'ohash' 6 | import { joinURL } from 'ufo' 7 | import { createAuthHeader, getProxyPath, headersToObject } from '../utils' 8 | 9 | export type KirbyFetchOptions = Pick< 10 | NitroFetchOptions, 11 | | 'onRequest' 12 | | 'onRequestError' 13 | | 'onResponse' 14 | | 'onResponseError' 15 | | 'query' 16 | | 'headers' 17 | | 'method' 18 | | 'body' 19 | | 'retry' 20 | | 'retryDelay' 21 | | 'retryStatusCodes' 22 | | 'timeout' 23 | > & { 24 | /** 25 | * Language code to fetch data for in multi-language Kirby setups. 26 | */ 27 | language?: string 28 | /** 29 | * Cache the response between function calls for the same path. 30 | * @default true 31 | */ 32 | cache?: boolean 33 | } 34 | 35 | export function $kirby( 36 | path: string, 37 | opts: KirbyFetchOptions = {}, 38 | ): Promise { 39 | const nuxt = useNuxtApp() 40 | const promiseMap = (nuxt._pendingRequests ||= new Map()) as Map> 41 | const { 42 | query, 43 | headers, 44 | method, 45 | body, 46 | language, 47 | cache = true, 48 | ...fetchOptions 49 | } = opts 50 | const kql = useRuntimeConfig().public.kql as Required 51 | 52 | if (language) 53 | path = joinURL(language, path) 54 | 55 | const key = `$kirby${hash([ 56 | path, 57 | query, 58 | method, 59 | body, 60 | language, 61 | ])}` 62 | 63 | if ((nuxt.isHydrating || cache) && nuxt.payload.data[key]) 64 | return Promise.resolve(nuxt.payload.data[key]) 65 | 66 | if (promiseMap.has(key)) 67 | return promiseMap.get(key)! 68 | 69 | const baseHeaders = headersToObject(headers) 70 | 71 | const _serverFetchOptions: NitroFetchOptions = { 72 | method: 'POST', 73 | body: { 74 | path, 75 | query, 76 | headers: Object.keys(baseHeaders).length ? baseHeaders : undefined, 77 | method, 78 | body, 79 | cache, 80 | } satisfies ServerFetchOptions, 81 | } 82 | 83 | const _clientFetchOptions: NitroFetchOptions = { 84 | baseURL: kql.url, 85 | query, 86 | headers: { 87 | ...baseHeaders, 88 | ...createAuthHeader(kql), 89 | }, 90 | method, 91 | body, 92 | } 93 | 94 | const request = globalThis.$fetch(kql.client ? path : getProxyPath(key), { 95 | ...fetchOptions, 96 | ...(kql.client ? _clientFetchOptions : _serverFetchOptions), 97 | }) 98 | .then((response) => { 99 | if (import.meta.server || cache) 100 | nuxt.payload.data[key] = response 101 | promiseMap.delete(key) 102 | return response 103 | }) 104 | // Invalidate cache if request fails 105 | .catch((error) => { 106 | nuxt.payload.data[key] = undefined 107 | promiseMap.delete(key) 108 | throw error 109 | }) as Promise 110 | 111 | promiseMap.set(key, request) 112 | 113 | return request 114 | } 115 | -------------------------------------------------------------------------------- /src/runtime/composables/$kql.ts: -------------------------------------------------------------------------------- 1 | import type { KirbyQueryRequest, KirbyQueryResponse } from 'kirby-types' 2 | import type { NitroFetchOptions } from 'nitropack' 3 | import type { ModuleOptions } from '../../module' 4 | import type { ServerFetchOptions } from '../types' 5 | import { useNuxtApp, useRuntimeConfig } from '#imports' 6 | import { hash } from 'ohash' 7 | import { createAuthHeader, getProxyPath, headersToObject } from '../utils' 8 | 9 | export type KqlOptions = Pick< 10 | NitroFetchOptions, 11 | | 'onRequest' 12 | | 'onRequestError' 13 | | 'onResponse' 14 | | 'onResponseError' 15 | | 'headers' 16 | | 'retry' 17 | | 'retryDelay' 18 | | 'retryStatusCodes' 19 | | 'timeout' 20 | > & { 21 | /** 22 | * Language code to fetch data for in multi-language Kirby setups. 23 | */ 24 | language?: string 25 | /** 26 | * Cache the response between function calls for the same query. 27 | * @default true 28 | */ 29 | cache?: boolean 30 | } 31 | 32 | export function $kql = KirbyQueryResponse>( 33 | query: KirbyQueryRequest, 34 | opts: KqlOptions = {}, 35 | ): Promise { 36 | const nuxt = useNuxtApp() 37 | const promiseMap = (nuxt._kirbyPromises ||= new Map()) as Map> 38 | const { headers, language, cache = true, ...fetchOptions } = opts 39 | const kql = useRuntimeConfig().public.kql as Required 40 | const key = `$kql${hash([query, language])}` 41 | 42 | if ((nuxt.isHydrating || cache) && nuxt.payload.data[key]) 43 | return Promise.resolve(nuxt.payload.data[key]) 44 | 45 | if (promiseMap.has(key)) 46 | return promiseMap.get(key)! 47 | 48 | const baseHeaders = { 49 | ...headersToObject(headers), 50 | ...(language && { 'X-Language': language }), 51 | } 52 | 53 | const _serverFetchOptions: NitroFetchOptions = { 54 | method: 'POST', 55 | body: { 56 | query, 57 | cache, 58 | headers: Object.keys(baseHeaders).length ? baseHeaders : undefined, 59 | } satisfies ServerFetchOptions, 60 | } 61 | 62 | const _clientFetchOptions: NitroFetchOptions = { 63 | baseURL: kql.url, 64 | method: 'POST', 65 | body: query, 66 | headers: { 67 | ...baseHeaders, 68 | ...createAuthHeader(kql), 69 | }, 70 | } 71 | 72 | const request = globalThis.$fetch(kql.client ? kql.prefix : getProxyPath(key), { 73 | ...fetchOptions, 74 | ...(kql.client ? _clientFetchOptions : _serverFetchOptions), 75 | }) 76 | .then((response) => { 77 | if (import.meta.server || cache) 78 | nuxt.payload.data[key] = response 79 | promiseMap.delete(key) 80 | return response 81 | }) 82 | // Invalidate cache if request fails 83 | .catch((error) => { 84 | nuxt.payload.data[key] = undefined 85 | promiseMap.delete(key) 86 | throw error 87 | }) as Promise 88 | 89 | promiseMap.set(key, request) 90 | 91 | return request 92 | } 93 | -------------------------------------------------------------------------------- /src/runtime/composables/useKirbyData.ts: -------------------------------------------------------------------------------- 1 | import type { NitroFetchOptions } from 'nitropack' 2 | import type { AsyncData, AsyncDataOptions, NuxtError } from 'nuxt/app' 3 | import type { MaybeRefOrGetter, MultiWatchSources } from 'vue' 4 | import type { ModuleOptions } from '../../module' 5 | import { useAsyncData, useRuntimeConfig } from '#imports' 6 | import { hash } from 'ohash' 7 | import { joinURL } from 'ufo' 8 | import { computed, toValue } from 'vue' 9 | import { createAuthHeader, getProxyPath, headersToObject } from '../utils' 10 | 11 | type UseKirbyDataOptions = Omit, 'watch'> & Pick< 12 | NitroFetchOptions, 13 | | 'onRequest' 14 | | 'onRequestError' 15 | | 'onResponse' 16 | | 'onResponseError' 17 | | 'query' 18 | | 'headers' 19 | | 'method' 20 | | 'body' 21 | | 'retry' 22 | | 'retryDelay' 23 | | 'retryStatusCodes' 24 | | 'timeout' 25 | > & { 26 | /** 27 | * Language code to fetch data for in multi-language Kirby setups. 28 | */ 29 | language?: MaybeRefOrGetter 30 | /** 31 | * Cache the response between function calls for the same path. 32 | * @default true 33 | */ 34 | cache?: boolean 35 | /** 36 | * Watch an array of reactive sources and auto-refresh the fetch result when they change. 37 | * Path and language are watched by default. You can completely ignore reactive sources by using `watch: false`. 38 | * @default undefined 39 | */ 40 | watch?: MultiWatchSources | false 41 | } 42 | 43 | export function useKirbyData( 44 | path: MaybeRefOrGetter, 45 | opts: UseKirbyDataOptions = {}, 46 | ) { 47 | const { 48 | server, 49 | lazy, 50 | default: defaultFn, 51 | transform, 52 | pick, 53 | watch, 54 | immediate, 55 | query, 56 | headers, 57 | method, 58 | body, 59 | language, 60 | cache = true, 61 | ...fetchOptions 62 | } = opts 63 | 64 | const kql = useRuntimeConfig().public.kql as Required 65 | const _language = computed(() => toValue(language)) 66 | const _path = computed(() => { 67 | const value = toValue(path).replace(/^\//, '') 68 | return _language.value ? joinURL(_language.value, value) : value 69 | }) 70 | const key = computed(() => `$kirby${hash([ 71 | _path.value, 72 | query, 73 | method, 74 | ])}`) 75 | 76 | if (!_path.value || (_language.value && !_path.value.replace(new RegExp(`^${_language.value}/`), ''))) 77 | console.warn('[useKirbyData] Empty Kirby path') 78 | 79 | const asyncDataOptions: AsyncDataOptions = { 80 | server, 81 | lazy, 82 | default: defaultFn, 83 | transform, 84 | pick, 85 | watch: watch === false 86 | ? [] 87 | : [ 88 | _path, 89 | ...(watch || []), 90 | ], 91 | immediate, 92 | } 93 | 94 | let controller: AbortController | undefined 95 | 96 | return useAsyncData( 97 | key.value, 98 | async (nuxt) => { 99 | controller?.abort?.() 100 | 101 | if (nuxt && (nuxt.isHydrating || cache) && nuxt.payload.data[key.value]) 102 | return nuxt.payload.data[key.value] 103 | 104 | controller = new AbortController() 105 | 106 | try { 107 | let result: T | undefined 108 | 109 | if (kql.client) { 110 | result = (await globalThis.$fetch(_path.value, { 111 | ...fetchOptions, 112 | signal: controller.signal, 113 | baseURL: kql.url, 114 | query, 115 | headers: { 116 | ...headersToObject(headers), 117 | ...createAuthHeader(kql), 118 | }, 119 | method, 120 | body, 121 | })) as T 122 | } 123 | else { 124 | result = (await globalThis.$fetch(getProxyPath(key.value), { 125 | ...fetchOptions, 126 | signal: controller.signal, 127 | method: 'POST', 128 | body: { 129 | path: _path.value, 130 | query, 131 | headers: headersToObject(headers), 132 | method, 133 | body, 134 | cache, 135 | }, 136 | })) as T 137 | } 138 | 139 | if (nuxt && cache) 140 | nuxt.payload.data[key.value] = result 141 | 142 | return result 143 | } 144 | catch (error) { 145 | // Invalidate cache if request fails 146 | if (nuxt) 147 | nuxt.payload.data[key.value] = undefined 148 | 149 | throw error 150 | } 151 | }, 152 | asyncDataOptions, 153 | ) as AsyncData 154 | } 155 | -------------------------------------------------------------------------------- /src/runtime/composables/useKql.ts: -------------------------------------------------------------------------------- 1 | import type { KirbyQueryRequest, KirbyQueryResponse } from 'kirby-types' 2 | import type { NitroFetchOptions } from 'nitropack' 3 | import type { AsyncData, AsyncDataOptions, NuxtError } from 'nuxt/app' 4 | import type { MaybeRefOrGetter, MultiWatchSources } from 'vue' 5 | import type { ModuleOptions } from '../../module' 6 | import { useAsyncData, useRuntimeConfig } from '#imports' 7 | import { hash } from 'ohash' 8 | import { computed, toValue } from 'vue' 9 | import { createAuthHeader, getProxyPath, headersToObject } from '../utils' 10 | 11 | export type UseKqlOptions = Omit, 'watch'> & Pick< 12 | NitroFetchOptions, 13 | | 'onRequest' 14 | | 'onRequestError' 15 | | 'onResponse' 16 | | 'onResponseError' 17 | | 'headers' 18 | | 'retry' 19 | | 'retryDelay' 20 | | 'retryStatusCodes' 21 | | 'timeout' 22 | > & { 23 | /** 24 | * Language code to fetch data for in multi-language Kirby setups. 25 | */ 26 | language?: MaybeRefOrGetter 27 | /** 28 | * Cache the response between function calls for the same query. 29 | * @default true 30 | */ 31 | cache?: boolean 32 | /** 33 | * Watch an array of reactive sources and auto-refresh the fetch result when they change. 34 | * Query and language are watched by default. You can completely ignore reactive sources by using `watch: false`. 35 | * @default undefined 36 | */ 37 | watch?: MultiWatchSources | false 38 | } 39 | 40 | export function useKql< 41 | ResT extends KirbyQueryResponse = KirbyQueryResponse, 42 | ReqT extends KirbyQueryRequest = KirbyQueryRequest, 43 | >(query: MaybeRefOrGetter, opts: UseKqlOptions = {}) { 44 | const { 45 | server, 46 | lazy, 47 | default: defaultFn, 48 | transform, 49 | pick, 50 | watch, 51 | immediate, 52 | headers, 53 | language, 54 | cache = true, 55 | ...fetchOptions 56 | } = opts 57 | 58 | const kql = useRuntimeConfig().public.kql as Required 59 | const _query = computed(() => toValue(query)) 60 | const _language = computed(() => toValue(language)) 61 | const key = computed(() => `$kql${hash([_query.value, _language.value])}`) 62 | 63 | if (Object.keys(_query.value).length === 0 || !_query.value.query) 64 | console.error('[useKql] Empty KQL query') 65 | 66 | const asyncDataOptions: AsyncDataOptions = { 67 | server, 68 | lazy, 69 | default: defaultFn, 70 | transform, 71 | pick, 72 | watch: watch === false 73 | ? [] 74 | : [ 75 | // Key contains query and language 76 | key, 77 | ...(watch || []), 78 | ], 79 | immediate, 80 | } 81 | 82 | let controller: AbortController | undefined 83 | 84 | return useAsyncData( 85 | key.value, 86 | async (nuxt) => { 87 | controller?.abort?.() 88 | 89 | if (nuxt && (nuxt.isHydrating || cache) && nuxt.payload.data[key.value]) 90 | return nuxt.payload.data[key.value] 91 | 92 | controller = new AbortController() 93 | 94 | try { 95 | let result: ResT | undefined 96 | 97 | if (kql.client) { 98 | result = (await globalThis.$fetch(kql.prefix, { 99 | ...fetchOptions, 100 | signal: controller.signal, 101 | baseURL: kql.url, 102 | method: 'POST', 103 | body: _query.value, 104 | headers: { 105 | ...headersToObject(headers), 106 | ...createAuthHeader(kql), 107 | ...(_language.value && { 'X-Language': _language.value }), 108 | }, 109 | })) as ResT 110 | } 111 | else { 112 | result = (await globalThis.$fetch(getProxyPath(key.value), { 113 | ...fetchOptions, 114 | signal: controller.signal, 115 | method: 'POST', 116 | body: { 117 | query: _query.value, 118 | cache, 119 | headers: { 120 | ...headersToObject(headers), 121 | ...(_language.value && { 'X-Language': _language.value }), 122 | }, 123 | }, 124 | })) as ResT 125 | } 126 | 127 | if (nuxt && cache) 128 | nuxt.payload.data[key.value] = result 129 | 130 | return result 131 | } 132 | catch (error) { 133 | // Invalidate cache if request fails 134 | if (nuxt) 135 | nuxt.payload.data[key.value] = undefined 136 | 137 | throw error 138 | } 139 | }, 140 | asyncDataOptions, 141 | ) as AsyncData 142 | } 143 | -------------------------------------------------------------------------------- /src/runtime/server/handler.ts: -------------------------------------------------------------------------------- 1 | import type { H3Event } from 'h3' 2 | import type { ModuleOptions } from '../../module' 3 | import type { ServerFetchOptions } from '../types' 4 | // @ts-expect-error: `tsconfig.server` has the types 5 | import { defineCachedFunction, useRuntimeConfig } from '#imports' 6 | import { consola } from 'consola' 7 | import { destr } from 'destr' 8 | import { createError, defineEventHandler, getRouterParam, readBody, setResponseHeader, setResponseStatus, splitCookiesString } from 'h3' 9 | import { base64ToUint8Array, uint8ArrayToBase64, uint8ArrayToString } from 'uint8array-extras' 10 | import { createAuthHeader } from '../utils' 11 | 12 | const ignoredResponseHeaders = new Set([ 13 | // https://github.com/h3js/h3/blob/fe9800bbbe9bda2972cc5d11db7353f4ab70f0ba/src/utils/proxy.ts#L97 14 | 'content-encoding', 15 | 'content-length', 16 | // Reduce information leakage 17 | 'server', 18 | 'x-powered-by', 19 | ]) 20 | 21 | export default defineEventHandler(async (event) => { 22 | const kql = useRuntimeConfig(event).kql as Required 23 | const body = await readBody(event) 24 | const key = decodeURIComponent(getRouterParam(event, 'key')!) 25 | const isQueryRequest = key.startsWith('$kql') 26 | 27 | // Always give `event` as first argument to make sure cached functions 28 | // are working as expected in edge workers 29 | const fetcher = async (event: H3Event, { 30 | key, 31 | query, 32 | path, 33 | headers, 34 | method, 35 | body, 36 | }: { key: string } & ServerFetchOptions) => { 37 | const isQueryRequest = key.startsWith('$kql') 38 | 39 | const response = await globalThis.$fetch.raw(isQueryRequest ? kql.prefix : path!, { 40 | responseType: 'arrayBuffer', 41 | ignoreResponseError: true, 42 | baseURL: kql.url, 43 | ...(isQueryRequest 44 | ? { 45 | method: 'POST', 46 | body: query, 47 | } 48 | : { 49 | query, 50 | method, 51 | body, 52 | }), 53 | headers: { 54 | ...headers, 55 | ...createAuthHeader(kql), 56 | }, 57 | }) 58 | 59 | // Serialize the response data 60 | const dataArray = new Uint8Array(response._data ?? ([] as unknown as ArrayBuffer)) 61 | const data = uint8ArrayToBase64(dataArray) 62 | 63 | return { 64 | status: response.status, 65 | statusText: response.statusText, 66 | headers: [...response.headers.entries()], 67 | data, 68 | } 69 | } 70 | 71 | const cachedFetcher = defineCachedFunction(fetcher, { 72 | name: 'nuxt-kql', 73 | base: kql.server.storage, 74 | swr: kql.server.swr, 75 | maxAge: kql.server.maxAge, 76 | getKey: (event: H3Event, { key }: { key: string } & ServerFetchOptions) => key, 77 | }) 78 | 79 | if (isQueryRequest) { 80 | if (!body.query?.query) { 81 | throw createError({ 82 | statusCode: 400, 83 | statusMessage: 'KQL query is empty', 84 | }) 85 | } 86 | } 87 | else { 88 | // Check if the path is an absolute URL 89 | if (body.path && new URL(body.path, 'http://localhost').origin !== 'http://localhost') { 90 | throw createError({ 91 | statusCode: 400, 92 | statusMessage: 'Absolute URLs are not allowed', 93 | }) 94 | } 95 | } 96 | 97 | try { 98 | const response = kql.server.cache && body.cache 99 | ? await cachedFetcher(event, { key, ...body }) 100 | : await fetcher(event, { key, ...body }) 101 | 102 | const dataArray = base64ToUint8Array(response.data) 103 | 104 | if (response.status >= 400 && response.status < 600) { 105 | if (isQueryRequest) { 106 | consola.error(`Failed KQL query "${body.query?.query}" (...) with status code ${response.status}:\n`, destr( 107 | uint8ArrayToString(dataArray), 108 | )) 109 | if (kql.server.verboseErrors) 110 | consola.log('Full KQL query request:', body.query) 111 | } 112 | else { 113 | consola.error(`Failed ${(body.method || 'get').toUpperCase()} request to "${body.path}"`) 114 | } 115 | } 116 | 117 | const cookies: string[] = [] 118 | 119 | for (const [key, value] of response.headers) { 120 | if (ignoredResponseHeaders.has(key)) 121 | continue 122 | 123 | if (key === 'set-cookie') { 124 | cookies.push(...splitCookiesString(value)) 125 | continue 126 | } 127 | 128 | setResponseHeader(event, key, value) 129 | } 130 | 131 | if (cookies.length > 0) 132 | setResponseHeader(event, 'set-cookie', cookies) 133 | 134 | setResponseStatus(event, response.status, response.statusText) 135 | return dataArray 136 | } 137 | catch (error) { 138 | consola.error(error) 139 | 140 | throw createError({ 141 | statusCode: 503, 142 | statusMessage: 'Service Unavailable', 143 | }) 144 | } 145 | }) 146 | -------------------------------------------------------------------------------- /src/runtime/server/imports.ts: -------------------------------------------------------------------------------- 1 | import type { KirbyQueryRequest, KirbyQueryResponse } from 'kirby-types' 2 | import type { NitroFetchOptions } from 'nitropack' 3 | import type { ModuleOptions } from '../../module' 4 | import { useRuntimeConfig } from '#imports' 5 | import { joinURL } from 'ufo' 6 | import { createAuthHeader, headersToObject } from '../utils' 7 | 8 | export type KirbyFetchOptions = Omit< 9 | NitroFetchOptions, 10 | 'baseURL' 11 | > & { 12 | /** 13 | * Language code to fetch data for in multi-language Kirby setups. 14 | */ 15 | language?: string 16 | } 17 | 18 | export type KqlFetchOptions = Pick< 19 | NitroFetchOptions, 20 | | 'onRequest' 21 | | 'onRequestError' 22 | | 'onResponse' 23 | | 'onResponseError' 24 | | 'headers' 25 | | 'retry' 26 | | 'retryDelay' 27 | | 'retryStatusCodes' 28 | | 'timeout' 29 | > & { 30 | /** 31 | * Language code to fetch data for in multi-language Kirby setups. 32 | */ 33 | language?: string 34 | } 35 | 36 | export function $kirby( 37 | path: string, 38 | opts: KirbyFetchOptions = {}, 39 | ): Promise { 40 | const { headers, language, ...fetchOptions } = opts 41 | const kql = useRuntimeConfig().kql as Required 42 | 43 | if (language) 44 | path = joinURL(language, path) 45 | 46 | return globalThis.$fetch(path, { 47 | ...fetchOptions, 48 | baseURL: kql.url, 49 | headers: { 50 | ...headersToObject(headers), 51 | ...createAuthHeader(kql), 52 | }, 53 | }) as Promise 54 | } 55 | 56 | export function $kql = KirbyQueryResponse>( 57 | query: KirbyQueryRequest, 58 | opts: KqlFetchOptions = {}, 59 | ): Promise { 60 | const { headers, language, ...fetchOptions } = opts 61 | const kql = useRuntimeConfig().kql as Required 62 | 63 | return globalThis.$fetch(kql.prefix, { 64 | ...fetchOptions, 65 | baseURL: kql.url, 66 | method: 'POST', 67 | body: query, 68 | headers: { 69 | ...headersToObject(headers), 70 | ...createAuthHeader(kql), 71 | ...(language && { 'X-Language': language }), 72 | }, 73 | }) as Promise 74 | } 75 | -------------------------------------------------------------------------------- /src/runtime/types.ts: -------------------------------------------------------------------------------- 1 | import type { KirbyQueryRequest } from 'kirby-types' 2 | import type { NitroFetchOptions } from 'nitropack' 3 | 4 | export type ServerFetchOptions = Pick< 5 | NitroFetchOptions, 6 | 'query' | 'headers' | 'method' | 'body' 7 | > & { 8 | // Either fetch a KQL query 9 | query?: Partial 10 | // … or from a Kirby path 11 | path?: string 12 | cache?: boolean 13 | } 14 | -------------------------------------------------------------------------------- /src/runtime/utils.ts: -------------------------------------------------------------------------------- 1 | import type { ModuleOptions } from '../module' 2 | 3 | export function getProxyPath(key: string) { 4 | return `/api/__kirby__/${encodeURIComponent(key)}` 5 | } 6 | 7 | export function headersToObject(headers: HeadersInit = {}): Record { 8 | return Object.fromEntries(new Headers(headers)) 9 | } 10 | 11 | export function createAuthHeader({ 12 | auth, 13 | token, 14 | credentials, 15 | }: Pick & { auth?: string }) { 16 | if (auth === 'basic' && credentials) { 17 | const { username, password } = credentials 18 | const encoded = globalThis.btoa(`${username}:${password}`) 19 | 20 | return { Authorization: `Basic ${encoded}` } 21 | } 22 | 23 | if (auth === 'bearer') 24 | return { Authorization: `Bearer ${token}` } 25 | } 26 | -------------------------------------------------------------------------------- /test/__snapshots__/e2e.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`nuxt-kql > can prefetch KQL queries 1`] = ` 4 | { 5 | "children": [ 6 | { 7 | "id": "photography", 8 | "isListed": true, 9 | "title": "Photography", 10 | }, 11 | { 12 | "id": "notes", 13 | "isListed": true, 14 | "title": "Notes", 15 | }, 16 | { 17 | "id": "about", 18 | "isListed": true, 19 | "title": "About us", 20 | }, 21 | { 22 | "id": "error", 23 | "isListed": false, 24 | "title": "Error", 25 | }, 26 | { 27 | "id": "home", 28 | "isListed": false, 29 | "title": "Home", 30 | }, 31 | { 32 | "id": "sandbox", 33 | "isListed": false, 34 | "title": "Sandbox", 35 | }, 36 | ], 37 | "title": "Mægazine", 38 | } 39 | `; 40 | 41 | exports[`nuxt-kql > fetches Kirby data useKirbyData 1`] = ` 42 | { 43 | "code": 201, 44 | "result": { 45 | "children": [ 46 | { 47 | "id": "photography", 48 | "isListed": true, 49 | "title": "Photography", 50 | }, 51 | { 52 | "id": "notes", 53 | "isListed": true, 54 | "title": "Notes", 55 | }, 56 | { 57 | "id": "about", 58 | "isListed": true, 59 | "title": "About us", 60 | }, 61 | { 62 | "id": "error", 63 | "isListed": false, 64 | "title": "Error", 65 | }, 66 | { 67 | "id": "home", 68 | "isListed": false, 69 | "title": "Home", 70 | }, 71 | { 72 | "id": "sandbox", 73 | "isListed": false, 74 | "title": "Sandbox", 75 | }, 76 | ], 77 | "title": "Mægazine", 78 | }, 79 | "status": "Created", 80 | } 81 | `; 82 | 83 | exports[`nuxt-kql > fetches queries with $kql 1`] = ` 84 | { 85 | "code": 200, 86 | "result": { 87 | "children": [ 88 | { 89 | "id": "photography", 90 | "isListed": true, 91 | "title": "Photography", 92 | }, 93 | { 94 | "id": "notes", 95 | "isListed": true, 96 | "title": "Notes", 97 | }, 98 | { 99 | "id": "about", 100 | "isListed": true, 101 | "title": "About us", 102 | }, 103 | { 104 | "id": "error", 105 | "isListed": false, 106 | "title": "Error", 107 | }, 108 | { 109 | "id": "home", 110 | "isListed": false, 111 | "title": "Home", 112 | }, 113 | { 114 | "id": "sandbox", 115 | "isListed": false, 116 | "title": "Sandbox", 117 | }, 118 | ], 119 | "title": "Mægazine", 120 | }, 121 | "status": "OK", 122 | } 123 | `; 124 | -------------------------------------------------------------------------------- /test/e2e.test.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url' 2 | import { $fetch, setup } from '@nuxt/test-utils/e2e' 3 | import { destr } from 'destr' 4 | import { describe, expect, it } from 'vitest' 5 | 6 | describe('nuxt-kql', async () => { 7 | await setup({ 8 | server: true, 9 | rootDir: fileURLToPath(new URL('./fixture', import.meta.url)), 10 | }) 11 | 12 | it('fetches queries with $kql', async () => { 13 | const html = await $fetch('/$kql') 14 | expect(getTestResult(html)).toMatchSnapshot() 15 | }) 16 | 17 | it('fetches Kirby data useKirbyData', async () => { 18 | const html = await $fetch('/useKirbyData') 19 | expect(getTestResult(html)).toMatchSnapshot() 20 | }) 21 | 22 | it('can prefetch KQL queries', async () => { 23 | const html = await $fetch('/prefetch') 24 | expect(getTestResult(html)).toMatchSnapshot() 25 | }) 26 | }) 27 | 28 | function getTestResult(html: string) { 29 | const content = html.match(/(.*?)<\/script>/s)?.[1] 30 | return destr(content) 31 | } 32 | -------------------------------------------------------------------------------- /test/fixture/app.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /test/fixture/composables/test-result.ts: -------------------------------------------------------------------------------- 1 | import { useHead } from '#imports' 2 | 3 | export function useTestResult(data: unknown) { 4 | useHead({ 5 | script: [ 6 | { 7 | children: JSON.stringify(data), 8 | type: 'text/test-result', 9 | }, 10 | ], 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /test/fixture/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtConfig } from 'nuxt/config' 2 | import NuxtKql from '../../src/module' 3 | 4 | export default defineNuxtConfig({ 5 | modules: [NuxtKql], 6 | 7 | compatibilityDate: '2025-01-01', 8 | 9 | kql: { 10 | url: 'https://kirby-headless-starter.byjohann.dev', 11 | token: 'test', 12 | auth: 'bearer', 13 | prefetch: { 14 | site: { 15 | query: 'site', 16 | select: { 17 | title: true, 18 | children: { 19 | query: 'site.children', 20 | select: { 21 | id: true, 22 | title: true, 23 | isListed: true, 24 | }, 25 | }, 26 | }, 27 | }, 28 | }, 29 | }, 30 | }) 31 | -------------------------------------------------------------------------------- /test/fixture/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "dev": "nuxi dev", 7 | "build": "nuxi build", 8 | "prepare": "nuxi prepare" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/fixture/pages/$kql.vue: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /test/fixture/pages/prefetch.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /test/fixture/pages/useKirbyData.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /test/fixture/pages/useKql.vue: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /test/fixture/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json", 3 | "exclude": ["dist", "playground", "test"] 4 | } 5 | --------------------------------------------------------------------------------