From c1b4eb035f04fc8395f14df8213c8e0e3b5047cf Mon Sep 17 00:00:00 2001 From: Baptiste Leproux Date: Wed, 18 Jun 2025 10:37:04 +0200 Subject: [PATCH 01/23] docs: init with docus --- docs/content/index.md | 31 +- docus/.gitignore | 40 ++ docus/app/components/content/BrowserFrame.vue | 21 + docus/content/blog.yml | 2 + docus/content/blog/docus-v3.md | 122 ++++ docus/content/blog/studio-v2.md | 83 +++ docus/content/blog/ui-pro-docs-migration.md | 678 ++++++++++++++++++ docus/content/blog/v3.md | 195 +++++ docus/content/blog/visual-editor.md | 83 +++ docus/content/changelog.yml | 3 + docus/content/changelog/frontmatter-form.md | 113 +++ .../content/changelog/studio-customisation.md | 109 +++ docus/content/changelog/yaml-json-form.md | 46 ++ docus/content/docs/.navigation.yml | 1 + .../docs/1.getting-started/.navigation.yml | 2 + .../content/docs/1.getting-started/1.index.md | 53 ++ .../docs/1.getting-started/2.installation.md | 149 ++++ .../docs/1.getting-started/3.configuration.md | 515 +++++++++++++ .../docs/1.getting-started/4.migration.md | 194 +++++ .../docs/2.collections/.navigation.yml | 3 + docus/content/docs/2.collections/1.define.md | 155 ++++ docus/content/docs/2.collections/2.types.md | 138 ++++ docus/content/docs/2.collections/3.sources.md | 127 ++++ docus/content/docs/3.files/.navigation.yml | 3 + docus/content/docs/3.files/1.markdown.md | 552 ++++++++++++++ docus/content/docs/3.files/2.yaml.md | 65 ++ docus/content/docs/3.files/3.json.md | 73 ++ docus/content/docs/3.files/4.csv.md | 142 ++++ docus/content/docs/4.utils/.navigation.yml | 2 + .../docs/4.utils/1.query-collection.md | 301 ++++++++ .../4.utils/2.query-collection-navigation.md | 202 ++++++ .../3.query-collection-item-surroundings.md | 133 ++++ .../4.query-collection-search-sections.md | 88 +++ .../content/docs/5.components/.navigation.yml | 3 + .../docs/5.components/0.content-renderer.md | 67 ++ docus/content/docs/5.components/1.slot.md | 104 +++ docus/content/docs/5.components/2.prose.md | 339 +++++++++ docus/content/docs/6.deploy/.navigation.yml | 1 + docus/content/docs/6.deploy/1.server.md | 39 + docus/content/docs/6.deploy/2.serverless.md | 142 ++++ docus/content/docs/6.deploy/3.nuxthub.md | 41 ++ .../docs/6.deploy/4.cloudflare-pages.md | 41 ++ docus/content/docs/6.deploy/5.vercel.md | 29 + docus/content/docs/6.deploy/6.netlify.md | 29 + docus/content/docs/6.deploy/7.aws-amplify.md | 28 + docus/content/docs/6.deploy/8.docker.md | 79 ++ docus/content/docs/6.deploy/9.static.md | 34 + docus/content/docs/7.advanced/.navigation.yml | 2 + .../docs/7.advanced/1.fulltext-search.md | 128 ++++ .../content/docs/7.advanced/2.raw-content.md | 54 ++ docus/content/docs/7.advanced/3.database.md | 31 + docus/content/docs/7.advanced/4.tools.md | 89 +++ docus/content/docs/7.advanced/5.hooks.md | 81 +++ .../docs/7.advanced/6.custom-source.md | 63 ++ docus/content/docs/7.advanced/7.llms.md | 96 +++ .../content/docs/7.advanced/8.transformers.md | 139 ++++ docus/content/docs/8.studio/.navigation.yml | 2 + docus/content/docs/8.studio/1.setup.md | 117 +++ docus/content/docs/8.studio/2.github.md | 44 ++ docus/content/docs/8.studio/3.content.md | 251 +++++++ docus/content/docs/8.studio/4.medias.md | 27 + docus/content/docs/8.studio/5.config.md | 157 ++++ docus/content/docs/8.studio/6.debug.md | 62 ++ docus/content/index.md | 445 ++++++++++++ docus/content/studio/index.md | 328 +++++++++ docus/content/studio/pricing.yml | 221 ++++++ docus/content/templates.yml | 2 + docus/content/templates/canvas.md | 56 ++ docus/content/templates/content-wind.md | 52 ++ docus/content/templates/docs.md | 50 ++ docus/content/templates/docus.md | 79 ++ docus/content/templates/landing.md | 46 ++ docus/content/templates/minimal-starter.md | 43 ++ docus/content/templates/minted-directory.md | 47 ++ docus/content/templates/portfolio.md | 50 ++ docus/content/templates/saas.md | 51 ++ docus/nuxt.config.ts | 23 + docus/package.json | 12 + docus/public/favicon.ico | Bin 0 -> 4286 bytes docus/public/mountains/everest.jpg | Bin 0 -> 114786 bytes package.json | 4 +- pnpm-lock.yaml | 178 +++++ pnpm-workspace.yaml | 1 + 83 files changed, 8415 insertions(+), 16 deletions(-) create mode 100644 docus/.gitignore create mode 100644 docus/app/components/content/BrowserFrame.vue create mode 100644 docus/content/blog.yml create mode 100644 docus/content/blog/docus-v3.md create mode 100644 docus/content/blog/studio-v2.md create mode 100644 docus/content/blog/ui-pro-docs-migration.md create mode 100644 docus/content/blog/v3.md create mode 100644 docus/content/blog/visual-editor.md create mode 100644 docus/content/changelog.yml create mode 100644 docus/content/changelog/frontmatter-form.md create mode 100644 docus/content/changelog/studio-customisation.md create mode 100644 docus/content/changelog/yaml-json-form.md create mode 100644 docus/content/docs/.navigation.yml create mode 100644 docus/content/docs/1.getting-started/.navigation.yml create mode 100644 docus/content/docs/1.getting-started/1.index.md create mode 100644 docus/content/docs/1.getting-started/2.installation.md create mode 100644 docus/content/docs/1.getting-started/3.configuration.md create mode 100644 docus/content/docs/1.getting-started/4.migration.md create mode 100644 docus/content/docs/2.collections/.navigation.yml create mode 100644 docus/content/docs/2.collections/1.define.md create mode 100644 docus/content/docs/2.collections/2.types.md create mode 100644 docus/content/docs/2.collections/3.sources.md create mode 100644 docus/content/docs/3.files/.navigation.yml create mode 100644 docus/content/docs/3.files/1.markdown.md create mode 100644 docus/content/docs/3.files/2.yaml.md create mode 100644 docus/content/docs/3.files/3.json.md create mode 100644 docus/content/docs/3.files/4.csv.md create mode 100644 docus/content/docs/4.utils/.navigation.yml create mode 100644 docus/content/docs/4.utils/1.query-collection.md create mode 100644 docus/content/docs/4.utils/2.query-collection-navigation.md create mode 100644 docus/content/docs/4.utils/3.query-collection-item-surroundings.md create mode 100644 docus/content/docs/4.utils/4.query-collection-search-sections.md create mode 100644 docus/content/docs/5.components/.navigation.yml create mode 100644 docus/content/docs/5.components/0.content-renderer.md create mode 100644 docus/content/docs/5.components/1.slot.md create mode 100644 docus/content/docs/5.components/2.prose.md create mode 100644 docus/content/docs/6.deploy/.navigation.yml create mode 100644 docus/content/docs/6.deploy/1.server.md create mode 100644 docus/content/docs/6.deploy/2.serverless.md create mode 100644 docus/content/docs/6.deploy/3.nuxthub.md create mode 100644 docus/content/docs/6.deploy/4.cloudflare-pages.md create mode 100644 docus/content/docs/6.deploy/5.vercel.md create mode 100644 docus/content/docs/6.deploy/6.netlify.md create mode 100644 docus/content/docs/6.deploy/7.aws-amplify.md create mode 100644 docus/content/docs/6.deploy/8.docker.md create mode 100644 docus/content/docs/6.deploy/9.static.md create mode 100644 docus/content/docs/7.advanced/.navigation.yml create mode 100644 docus/content/docs/7.advanced/1.fulltext-search.md create mode 100644 docus/content/docs/7.advanced/2.raw-content.md create mode 100644 docus/content/docs/7.advanced/3.database.md create mode 100644 docus/content/docs/7.advanced/4.tools.md create mode 100644 docus/content/docs/7.advanced/5.hooks.md create mode 100644 docus/content/docs/7.advanced/6.custom-source.md create mode 100644 docus/content/docs/7.advanced/7.llms.md create mode 100644 docus/content/docs/7.advanced/8.transformers.md create mode 100644 docus/content/docs/8.studio/.navigation.yml create mode 100644 docus/content/docs/8.studio/1.setup.md create mode 100644 docus/content/docs/8.studio/2.github.md create mode 100644 docus/content/docs/8.studio/3.content.md create mode 100644 docus/content/docs/8.studio/4.medias.md create mode 100644 docus/content/docs/8.studio/5.config.md create mode 100644 docus/content/docs/8.studio/6.debug.md create mode 100644 docus/content/index.md create mode 100644 docus/content/studio/index.md create mode 100644 docus/content/studio/pricing.yml create mode 100644 docus/content/templates.yml create mode 100644 docus/content/templates/canvas.md create mode 100644 docus/content/templates/content-wind.md create mode 100644 docus/content/templates/docs.md create mode 100644 docus/content/templates/docus.md create mode 100644 docus/content/templates/landing.md create mode 100644 docus/content/templates/minimal-starter.md create mode 100644 docus/content/templates/minted-directory.md create mode 100644 docus/content/templates/portfolio.md create mode 100644 docus/content/templates/saas.md create mode 100644 docus/nuxt.config.ts create mode 100644 docus/package.json create mode 100644 docus/public/favicon.ico create mode 100644 docus/public/mountains/everest.jpg diff --git a/docs/content/index.md b/docs/content/index.md index 00399b427..7b5af9a81 100644 --- a/docs/content/index.md +++ b/docs/content/index.md @@ -171,20 +171,23 @@ Combine file-based simplicity with Vue component power. Build content-rich websi reverse: true orientation: horizontal --- - :::code-group - ::::preview-card{.!h-[458px] icon="i-lucide-eye" label="Preview"} - :::::example-landing-hero - --- - class: "!h-[458px]" - image: /mountains/everest.jpg - --- - #title - The Everest. - - #description - The Everest is the highest mountain in the world, standing at 8,848 meters above sea level. - ::::: - :::: +:::code-group + ::::preview-card{.!h-[458px] icon="i-lucide-eye" label="Preview"} + :::::example-landing-hero + --- + class: "!h-[458px]" + image: /mountains/everest.jpg + --- + #default + + + #title + The Everest. + + #description + The Everest is the highest mountain in the world, standing at 8,848 meters above sea level. + ::::: + :::: ```mdc [content/index.md] --- diff --git a/docus/.gitignore b/docus/.gitignore new file mode 100644 index 000000000..fc624192f --- /dev/null +++ b/docus/.gitignore @@ -0,0 +1,40 @@ +# Nuxt dev/build outputs +.output +.data +.nuxt +.nitro +.cache +dist + +# Node dependencies +node_modules + +# Logs +logs +*.log + +# Misc +.DS_Store +.fleet +.idea +.eslintcache + +# Local env files +.env +.env.* +!.env.example + +# Template +template/pnpm-lock.yaml + +# npm pack +*.tgz + +# Temp files +.tmp +.profile +*.0x + +#VSC +.history +.wrangler \ No newline at end of file diff --git a/docus/app/components/content/BrowserFrame.vue b/docus/app/components/content/BrowserFrame.vue new file mode 100644 index 000000000..a8d6a4a32 --- /dev/null +++ b/docus/app/components/content/BrowserFrame.vue @@ -0,0 +1,21 @@ + + + diff --git a/docus/content/blog.yml b/docus/content/blog.yml new file mode 100644 index 000000000..0b4f64508 --- /dev/null +++ b/docus/content/blog.yml @@ -0,0 +1,2 @@ +title: Blog +description: Read the latest news about Nuxt Content or articles about the Studio platform. diff --git a/docus/content/blog/docus-v3.md b/docus/content/blog/docus-v3.md new file mode 100644 index 000000000..ff8554784 --- /dev/null +++ b/docus/content/blog/docus-v3.md @@ -0,0 +1,122 @@ +--- +title: Docus, the Comeback +description: The Nuxt documentation theme and CLI is back with version 3 + rewritten from the ground up. +seo: + title: Docus v3 — The Return of the Nuxt Docs Theme +date: 2025-06-13T00:00:00.000Z +category: content +image: + src: https://docus.dev/__og-image__/static/og.png + alt: Docus Landing Page +authors: + - name: Baptiste Leproux + avatar: + src: https://avatars.githubusercontent.com/u/7290030?v=4 + to: https://x.com/_larbish + username: larbish +--- + +We’ve completely rewritten the [Docus](https://docus.dev) theme. Reviving it with a fresh and modern foundation powered by the Nuxt ecosystem and designed by Nuxt UI to offer the best documentation experience. + +The goal was simple: take **the best parts of the Nuxt ecosystem** and deliver a documentation theme that’s powerful, elegant and easy to maintain. + +## **What’s New in Docus v3?** + +### **📦 A real** [Nuxt]{.text-primary} **app with just one dependency** + +Docus is built on top of [Nuxt 3](https://nuxt.com) (version 4 compatibility mode is enabled so we're already ready for Nuxt 4). That means your documentation is a full Nuxt application with access to the entire Nuxt features: components, modules, plugins, runtime config, and more. + +**But**, **the best part is**... You only need the **docus** package. It bundles all the necessary officials Nuxt modules, so you can start writing documentation in seconds. All you need in your app is a `package.json` file and a `content/` folder with your Markdown in it. Then you’re good to go. + +::prose-tip{to="https://docus.dev/concepts/nuxt"} +Learn more about Nuxt layer in Docus dedicated section. +:: + +### **✨ Designed by** [Nuxt]{.text-primary} **UI Pro** + +Docus v2 is powered by **Nuxt UI Pro**, giving you a beautiful, responsive, and accessible theme out of the box. With **Tailwind CSS v4**, **CSS variables**, and the **Tailwind Variants API**, your docs look great by default but stays fully customizable. + +You can tweak colors, update typography or adjust component styles globally or per component with simple updates in your `app.config.ts`. + +::prose-tip{to="https://docus.dev/concepts/theme"} +Learn more about UI theming in Docus dedicated section. +:: + +::prose-note +A UI Pro license is currently required, but we’re working to make it free for everyone soon. Also, if you're currently building an OSS documentation, you can ask for the OSS license at `ui-pro@nuxt.com` . +:: + +### **✍️ Markdown with superpowers (MDC syntax by** [Nuxt]{.text-primary} **Content)** + +Writing docs has never been more simple. You're one Markdown folder away from it. Furthermore with Nuxt Content and the MDC syntax, you can embed interactive Vue components in Markdown and use any Nuxt UI components or your own custom ones. + +::prose-tip{to="https://docus.dev/concepts/edition"} +Learn more about MDC syntax in Docus dedicated section. +:: + +### 🖥️ [Nuxt]{.text-primary} Studio ready + +Docus works perfectly with **Nuxt Studio**, allowing you to manage and edit your docs entirely from the browser. No terminal, no local setup. It’s the ideal way to collaborate with non-technical contributors or manage docs centrally for your team. + +::prose-tip{to="https://docus.dev/getting-started/studio"} +Learn more about Studio editor in Docus dedicated section. +:: + +### **🔍 SEO out of the box** + +Technical SEO is tricky and boring. Docus offers a solid, opt-in default setup that works out of the box while giving you full control to customize your SEO metadata, from pages metas to social sharing images. + +::prose-tip{to="https://docus.dev/concepts/configuration"} +Learn more about app configuration in Docus dedicated section. +:: + +### **🔧 Full customization via component overrides** + +Need to replace parts of the layout or UI? Docus uses **Nuxt Layers** to let you override core components we've defined. Just create a new component in your project’s `components/` directory using the same name, and Docus will automatically use it. + +::prose-tip{to="https://docus.dev/concepts/customization"} +Learn more about components override in Docus dedicated section. +:: + +### **🤖** LLMs integration by default + +Docus integrates `nuxt-llms` by default to prepare your content for Large Language Models (LLMs). All your documentation pages are injected and `/llms.txt` file is automatically generated and pre-rendered. + +::prose-tip{to="https://docus.dev/concepts/llms"} +Learn more about LLMs integration in Docus dedicated section. +:: + +### **🧠 Smart defaults for a ready docs** + +Docus includes thoughtful defaults that save you time: + +- ✅ Auto-generated sidebar navigation from your folder structure +- 🔍 Full-text search using Fuse.js +- ✨ Optimized typography and layout +- 🌙 Dark mode support out of the box +- 🖼️ Nuxt Image integration for responsive, optimized images + +### **🔁** Easy migration + +Moving from any Markdown-based is straightforward: drop your `.md` files into your `content/` folder and you’re live. + +## **What’s Next?** + +### **🔧 Try Docus Today** + +```bash +npx docus init docs +``` + +That's it 🚀 You're ready to edit your `content/` folder and start writing your doc. + +::prose-tip{to="https://docus.dev"} +Visit the documentation to learn everything about Docus. +:: + +### **🤝 Contribute** + +We’ve moved the repository to the **NuxtLabs** GitHub organization and cleaned up the issue tracker to start fresh. + +Whether you’re fixing bugs, suggesting features, or writing docs, we’d love your help. Feedback, contributions, and discussions about the future of Docus are all welcome! diff --git a/docus/content/blog/studio-v2.md b/docus/content/blog/studio-v2.md new file mode 100644 index 000000000..13252a62d --- /dev/null +++ b/docus/content/blog/studio-v2.md @@ -0,0 +1,83 @@ +--- +title: Introducing Nuxt Studio v2 +description: We are excited to announce the v2 release of Nuxt Studio, the new + editing experience for your Nuxt Content website +image: + src: /blog/nuxt-studio-v2.png +authors: + - name: Baptiste Leproux + avatar: + src: https://avatars.githubusercontent.com/u/7290030?v=4 + to: https://x.com/_larbish + username: larbish +date: 2024-06-13T00:00:00.000Z +category: studio +--- + +::warning +This article was published before the merge of the [Content](https://github.com/nuxt/content) and [Studio](https://github.com/nuxtlabs/studio-module) modules on January 6, 2025. As a result, it may contain some inconsistencies. The Studio module is now deprecated and available as an opt-in feature of the Content module. Learn how to enable it in [this guide](/docs/getting-started). +:: + +We are excited to announce the release of Nuxt Studio v2, a major update bringing a brand new interface designed specifically for our users, based on their feedback. + +::tip +Studio is optimized for **Nuxt Content** project but the only real requirement is to have a *content* folder with Markdown files. This simple setup is enough to start editing and publishing your files with the platform. +:: + +### **A more intuitive interface** + +![Nuxt studio v2 interface](/blog/v2-interface.webp) + +The main improvement in Version 2 is a **complete rework of the interface**. We have designed it to be more intuitive and user-friendly, especially for non-technical users. Our goal was to simplify the user experience, making it easier to create and set up projects with minimal hassle. The new interface is light, straightforward, and designed to streamline your workflow. + +### **Google authentication** + +![Google and GitHub authentication](/blog/google-github.webp) + +We now have two different authentication methods. You can either login with **GitHub** or with **Google**. Both methods give you the same edition rights but since Studio is synchronized with GitHub, some features are specific to GitHub users, especially project creation. + +::warning +Since a Google user can not create a project, he has to **join a team** with existing projects to edit them. +:: + +### **Minimal setup to edit your files** + +You can now edit your content **without any setup**, just import your repository and this is it. You can navigate through your files and medias, edit your content and publish on GitHub. + +Collaboration is available for teams. + +![Notion-like editor with collaboration](/blog/collaborate.webp) + +::warning +Medias in the editor are not displayed until you set up the live preview (see section below). +:: + +### Simplified setup for live preview + +![preview enable between notion like editor and website](/blog/preview.webp) + +As the live preview feature requires a deployed URL, we made it as simple as possible to set it up. + +While GitHub pages deployment remains available and still does not require any configuration on your end, requirements have been simplified for self-hosted project as we removed the token verification. [Enabling the Studio module](https://nuxt.studio/docs/get-started/setup#enable-the-live-preview) is the **only remaining requirement.** + +::warning{to="https://github.com/nuxtlabs/studio-module"} +It's crucial to use the latest version of the **Studio module** to ensure compatibility and access to new features. +:: + +### New documentation + +With a revamped platform comes a [new documentation](https://nuxt.studio/docs/get-started/introduction). Don't hesitate to check it out to learn everything about the new Studio. + +Whether you are an [editor](https://nuxt.studio/docs/editors/introduction) or a [developer](https://nuxt.studio/docs/developers/introduction) you now have your dedicated section in the docs. + +### A new direction for Studio + +Most available CMS solutions have to choose between being very customizable for developers or highly user friendly for content editors, with Studio we want to do both. + +**The developer provides the tools for the editors to focus on content, without requiring any technical knowledge**. + +::tip +Our Notion-like editor has a bright future ahead, and we want to develop it collaboratively with the community. +:: + +### diff --git a/docus/content/blog/ui-pro-docs-migration.md b/docus/content/blog/ui-pro-docs-migration.md new file mode 100644 index 000000000..a1d6c21b8 --- /dev/null +++ b/docus/content/blog/ui-pro-docs-migration.md @@ -0,0 +1,678 @@ +--- +title: Migrate Nuxt UI Pro Documentation Starter +description: How to upgrade your Nuxt UI Pro documentation to Content and UI v3 +image: + src: /blog/migrate-docs-starter.png +authors: + - name: Baptiste Leproux + avatar: + src: https://avatars.githubusercontent.com/u/7290030?v=4 + to: https://x.com/_larbish + username: larbish +date: 2025-01-21T01:00:00.000Z +category: Migration +--- + +# How to upgrade your Nuxt documentation website to Content x UI v3 + +**2025 kicks off with the power of 3!** + +This start of year is marked by major updates to our favorite tools. The UI team is about to launch **version 3** of the **UI / UI Pro libraries** (currently in alpha), while the Content team has already released **Nuxt Content v3**. + +These updates mean that all our starter templates combining **Content** and **UI** will need to be updated to align with the latest versions. To help you make the transition, this guide walks through migrating the **Nuxt UI Pro Docs Starter** to the new **Content v3 and Nuxt UI v3** packages. + +::prose-tip{to="https://github.com/nuxt-ui-pro/docs/tree/v3"} +Check the UI Pro documentation starter repository source code. +:: + +## Content migration (v2 → v3) + +### 1. Update package to v3 + +::code-group +```bash [pnpm] +pnpm add @nuxt/content@^3 +``` + +```bash [yarn] +yarn add @nuxt/content@^3 +``` + +```bash [npm] +npm install @nuxt/content@^3 +``` + +```bash [bun] +bun add @nuxt/content@^3 +``` +:: + +### 2. Create `content.config.ts` file + +This configuration file defines your data structure. A collection represents a set of related items. In the case of the docs starter, there are two different collections, the `landing` collection representing the home page and another `docs` collection for the documentation pages. + +```js [content.config.ts] +import { defineContentConfig, defineCollection, z } from '@nuxt/content' + +export default defineContentConfig({ + collections: { + landing: defineCollection({ + type: 'page', + source: 'index.yml' + }), + docs: defineCollection({ + type: 'page', + source: { + include: '**', + exclude: ['index.yml'] + }, + schema: z.object({ + links: z.array(z.object({ + label: z.string(), + icon: z.string(), + to: z.string(), + target: z.string().optional() + })).optional() + }) + }) + } +}) +``` + +On top of the built-in fields provided by the [`page`](/docs/collections/types#page-type) type, we added the extra field `links` to the `docs` collection so we can optionally display them in the docs [page header](https://ui3.nuxt.dev/components/page-header). + +::prose-tip +The `type: page` means there is a 1-to-1 relationship between the content file and a page on your site. +:: + +### 3. Migrate `app.vue` + +::prose-steps{level="4"} +#### Navigation fetch can be updated by moving from `fetchContentNavigation` to `queryCollectionNavigation` method + + :::prose-code-group + ```ts [app.vue (v3)] + const { data: navigation } = await useAsyncData('navigation', () => queryCollectionNavigation('docs')) + + ``` + + ```ts [app.vue (v2)] + const { data: navigation } = await useAsyncData('navigation', () => fetchContentNavigation()) + ``` + ::: + +#### Content search command palette data can use the new `queryCollectionSearchSections` method + + :::prose-code-group + ```ts [app.vue (v3)] + const { data: files } = useLazyAsyncData('search', () => queryCollectionSearchSections('docs'), { + server: false, + }) + ``` + + ```ts [app.vue (v2)] + const { data: files } = useLazyFetch('/api/search.json', { + default: () => [], + server: false + }) + ``` + ::: +:: + +### 4. Migrate landing page + +::prose-steps{level="4"} +#### Home page data fetching can be updated by moving from `queryContent` to `queryCollection` method + + :::prose-code-group + ```ts [index.vue (v3)] + const { data: page } = await useAsyncData('index', () => queryCollection('landing').path('/').first()) + ``` + + ```ts [index.vue (v2)] + const { data: page } = await useAsyncData('index', () => queryContent('/').findOne()) + ``` + ::: + +#### `useSeoMeta` can be populated using the `seo` field provided by the [page](/docs/collections/types#page-type) type + +```ts [index.vue] +useSeoMeta({ + title: page.value.seo.title, + ogTitle: page.value.seo.title, + description: page.value.seo.description, + ogDescription: page.value.seo.description +}) +``` + + :::prose-note + Please note that the `seo` field is automatically overridden by the root `title` and `description` if not set. + ::: +:: + +### 5. Migrate catch-all docs page + +::prose-steps{level="4"} +#### Docs page data and surround fetching can be updated and mutualised by moving from `queryContent` to `queryCollection` and `queryCollectionItemSurroundings` methods + + :::prose-code-group + ```ts [docs/[...slug\\].vue (v3)] + const { data } = await useAsyncData(route.path, () => Promise.all([ + queryCollection('docs').path(route.path).first(), + queryCollectionItemSurroundings('docs', route.path, { + fields: ['title', 'description'], + }), + ]), { + transform: ([page, surround]) => ({ page, surround }), + }) + + const page = computed(() => data.value?.page) + const surround = computed(() => data.value?.surround) + ``` + + ```ts [docs/[...slug\\].vue (v2)] + const { data: page } = await useAsyncData(route.path, () => queryContent(route.path).findOne()) + + const { data: surround } = await useAsyncData(`${route.path}-surround`, () => queryContent() + .where({ _extension: 'md', navigation: { $ne: false } }) + .only(['title', 'description', '_path']) + .findSurround(withoutTrailingSlash(route.path)) + ) + ``` + ::: + +#### Populate `useSeoMeta` with the `seo` field provided by the [page](/docs/collections/types#page-type) type + +```ts [index.vue] +useSeoMeta({ + title: page.value.seo.title, + ogTitle: `${page.value.seo.title} - ${seo?.siteName}`, + description: page.value.seo.description, + ogDescription: page.value.seo.description +}) +``` + + :::prose-note + Please note that the `seo` field is automatically overridden by the root `title` and `description` if not set. + ::: +:: + +### 6. Update types + +Types have been significantly enhanced in Content v3, eliminating the need for most manual typings, as they are now directly provided by the Nuxt Content APIs. + +Concerning the documentation starter, the only typing needed concerns the navigation items where `NavItem` can be replaced by `ContentNavigationItem` . + +```ts +import type { ContentNavigationItem } from '@nuxt/content' + +const navigation = inject>('navigation') +``` + +### 7. Replace folder metadata files + +All `_dir.yml` files become `.navigation.yml` + +### 8. Migrate Studio activation + +Since the [studio module](https://nuxt.studio) has been deprecated and a new generic `Preview API` has been implemented directly into Nuxt Content, we can remove the `@nuxthq/studio` package from our dependencies and from the `nuxt.config.ts` modules. + +Instead we just need to enable the preview mode in the Nuxt configuration file by binding the Studio API. + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + content: { + preview: { + api: 'https://api.nuxt.studio' + } + }, +}) +``` + +Finally, in order to keep the [app config file updatable](/docs/studio/config) from Studio, we just need to update the helper import of the `nuxt.schema.ts` file from `@nuxthq/studio/theme` to `@nuxt/content/preview`. + +::prose-tip +That's it, content v3 is now powering the starter. Let's now migrate to version 3 of [Nuxt UI / UI Pro](https://ui3.nuxt.dev). +:: + +## Nuxt UI Pro Migration (v1 → v3) + +::prose-caution +This is a migration case, it won't cover all breaking changes introduced by the version upgrade. You should check each component you're using in the documentation to know if you need updates concerning props, slots or styles. +:: + +### 1. Setup package to v3 + +::prose-note +To maintain consistency with the UI versioning, which transitioned from v1 to v2. The Nuxt UI Pro version 2 is being skipped, and the update jumps directly to v3. +:: + +::prose-steps{level="4"} +#### Install the Nuxt UI v3 alpha package + + :::code-group{sync="pm"} + ```bash [pnpm] + pnpm add @nuxt/ui-pro@next + ``` + + ```bash [yarn] + yarn add @nuxt/ui-pro@next + ``` + + ```bash [npm] + npm install @nuxt/ui-pro@next + ``` + + ```bash [bun] + bun add @nuxt/ui-pro@next + ``` + ::: + +#### Add the module in the Nuxt configuration file + +It's no longer required to add `@nuxt/ui` in modules as it is automatically imported by `@nuxt/ui-pro` . + + :::prose-code-group + ```ts [nuxt.config.ts (v3)] + export default defineNuxtConfig({ + modules: ['@nuxt/ui-pro'] + }) + ``` + + ```ts [nuxt.config.ts (v1)] + export default defineNuxtConfig({ + extends: ['@nuxt/ui-pro'], + modules: ['@nuxt/ui'] + }) + ``` + ::: + + :::prose-note + **Nuxt UIPro V3** is now considered as a module and no longer as a layer. + ::: + +#### Import Tailwind CSS and Nuxt UI Pro in your CSS + +```css [assets/css/main.css] +@import "tailwindcss" theme(static); +@import "@nuxt/ui-pro"; +``` + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + modules: ['@nuxt/ui-pro'], + css: ['~/assets/css/main.css'] +}) +``` + +#### Remove tailwind config file and use CSS-first theming + +Nuxt UI v3 uses Tailwind CSS v4 that follows a CSS-first configuration approach. You can now customize your theme with CSS variables inside a `@theme` directive. + +- Delete the `tailwind.config.ts` file +- Use the `@theme` directive to apply your theme in `main.css` file +- Use the `@source` directive in order for Tailwind to detect classes in `markdown` files. + +```css [assets/css/main.css] +@import "tailwindcss" theme(static); +@import "@nuxt/ui-pro"; + +@source "../content/**/*"; + +@theme { + --font-sans: 'DM Sans', sans-serif; + + --color-green-50: #EFFDF5; + --color-green-100: #D9FBE8; + --color-green-200: #B3F5D1; + --color-green-300: #75EDAE; + --color-green-400: #00DC82; + --color-green-500: #00C16A; + --color-green-600: #00A155; + --color-green-700: #007F45; + --color-green-800: #016538; + --color-green-900: #0A5331; + --color-green-950: #052E16; +} + +``` +:: + +### 2. Update `ui` overloads in `app.config.ts` + +::prose-caution{to="https://ui3.nuxt.dev/getting-started/theme#customize-theme"} +All overloads using the `ui` props in a component or the `ui` key in the `app.config.ts` are obsolete and need to be checked in the **UI / UI Pro** documentation. +:: + +::prose-code-group +```ts [app.config.ts (v3)] +export default defineAppConfig({ + ui: { + colors: { + primary: 'green', + neutral: 'slate' + } + }, + uiPro: { + footer: { + slots: { + root: 'border-t border-gray-200 dark:border-gray-800', + left: 'text-sm text-gray-500 dark:text-gray-400' + } + } + }, +} +``` + +```ts [app.config.ts (v1)] +export default defineAppConfig({ + ui: { + primary: 'green', + gray: 'slate', + footer: { + bottom: { + left: 'text-sm text-gray-500 dark:text-gray-400', + wrapper: 'border-t border-gray-200 dark:border-gray-800' + } + } + }, +}) +``` +:: + +### 3. Migrate `error.vue` page + +New `UError` component can be used as full page structure. + +::prose-code-group +```vue [error.vue (v3)] + +``` + +```vue [error.vue (v1)] + +``` +:: + +### 4. Migrate `app.vue` page + +- `Main`, `Footer` and `LazyUContentSearch` components do not need any updates in our case. +- `Notification` component can be removed since `Toast` components are directly handled by the `App` component. +- Instead of the `NavigationTree` component you can use the `NavigationMenu` component or the `ContentNavigation` component to display content navigation. + +::prose-code-group +```vue [Header.vue (v3)] + + + +``` + +```vue [Header.vue (v1)] + + + +``` +:: + +### 5. Update landing page + +We've decided to move the landing content from `YML` to `Markdown` . + +::prose-tip +This decision was made because components used in Markdown no longer need to be exposed globally (nor do they need to be created in the `components/content` folder). Content v3 handles it under the hood. +:: + +::prose-steps{level="4"} +#### Update content configuration + +```ts [content.config.ts] +export default defineContentConfig({ + collections: { + landing: defineCollection({ + type: 'page', + source: 'index.md' + }), + docs: defineCollection({ + type: 'page', + source: { + include: '**', + exclude: ['index.md'] + }, + ... + }) + } +}) +``` + +#### Use `ContentRenderer` to render `Markdown` + + :::prose-note + `prose` property must be set to `false` in `ContentRendered` as we don't want `Mardown` to be applied with prose styling in the case of a landing page integrating non prose Vue components. + ::: + + :::prose-code-group + ```vue [index.vue (v3)] + + ``` + + ```vue [index.vue (v1)] + + ``` + ::: + +#### Migrate Vue components to MDC + +Move all components in `index.md` following the [MDC syntax](/docs/files/markdown). + +Landing components have been reorganised and standardised as generic `Page` components. + +- `LandingHero` => `PageHero` +- `LandingSection` => `PageSection` +- `LandingCard` => `PageCard` (we'll use the `PageFeature` instead) + + :::prose-tip{to="https://github.com/nuxt-ui-pro/docs/blob/v3/content/index.md"} + Have a look at the final `Markdown` result on GitHub. + ::: +:: + +### 6. Migrate docs page + +::prose-steps{level="4"} +#### Layout + +- `Aside` component has been renamed to `PageAside` . +- `ContentNavigation` component can be used (instead of `NavigationTree`) to display the content navigation returned by `queryCollectionNavigation`. + + :::prose-code-group + ```vue [layout/docs.vue (v3)] + + ``` + + ```vue [layout/docs.vue (v1)] + + ``` + ::: + +#### Catch-all pages + +- `Divider` has been renamed to `Separator` +- `findPageHeadline` must be imported from `#ui-pro/utils/content` +- `prose` property does not exist no more on `PageBody` component. +:: + +::prose-tip{to="https://github.com/nuxt-ui-pro/docs/tree/v3"} +That's it! The docs starter is now fully running on both UI and Content v3 🎉 +:: + +## Edit on Studio + +If you're using Nuxt Studio to edit your documentation you also need to migrate the related code. + +The Studio module has been deprecated and a new generic `Preview API` has been implemented directly into Nuxt Content, you can remove the `@nuxthq/studio` package from your dependencies and from the`nuxt.config.ts` modules. Instead you just need to enable the preview mode in the Nuxt configuration file by binding the Studio API. + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + content: { + preview: { + api: 'https://api.nuxt.studio' + } + }, +}) +``` + +In order to keep the app config file updatable from Studio you need to update the helper import of the `nuxt.schema.ts` file from `@nuxthq/studio/theme` to `@nuxt/content/preview`. + +:video{autoplay controls loop poster="https://res.cloudinary.com/nuxt/video/upload/v1737458923/studio/docs-v3_lqfasl.png" src="https://res.cloudinary.com/nuxt/video/upload/v1737458923/studio/docs-v3_lqfasl.mp4"} diff --git a/docus/content/blog/v3.md b/docus/content/blog/v3.md new file mode 100644 index 000000000..7d5e73683 --- /dev/null +++ b/docus/content/blog/v3.md @@ -0,0 +1,195 @@ +--- +title: Nuxt Content v3 +description: "Content version 3 is out - introducing SQL based storage, + collections, preview API for a better Studio integration and tons of + performance improvements. " +image: + src: /blog/nuxt-content-v3.png + alt: Version 3 of Nuxt Content +authors: + - name: Baptiste Leproux + avatar: + src: https://avatars.githubusercontent.com/u/7290030?v=4 + to: https://x.com/_larbish + username: larbish + - name: Ahad Birang + avatar: + src: https://avatars.githubusercontent.com/u/2047945?v=4 + to: https://x.com/farnabaz + username: farnabaz + - name: Sébastien Chopin + avatar: + src: https://avatars.githubusercontent.com/u/904724?v=4 + to: https://x.com/atinux + username: atinux +date: 2025-01-16T01:00:00.000Z +category: Release +seo: + title: Announcing Nuxt Content version 3 +--- + +# **Announcing Nuxt Content 3.0** + +We are thrilled to announce the first stable version of Nuxt Content 3.0.0 ✨ + +## 🚀 Performance Improvements + +Nuxt Content v3 moves away from a file-based storage approach to an SQL database system. Using a database instead of the file-based storage reduces many I/O operations when querying large datasets. + +::prose-note +The new database system enhances the way your data files are stored and structured, ensuring better performance and scalability. This update is entirely behind the scenes and does not affect the file types you can use in Content (`yml`, `json`, and `markdown` ). +:: + +This switch is transparent to users and Nuxt Content still provides a zero config support for development mode, [server hosting](/docs/deploy/server) and [static generation](/docs/deploy/static). + +Furthermore, [serverless](/docs/deploy/serverless) hosting is now supported and client-side navigation performance has been improved. + +### Serverless Compatibility + +A key challenge with Nuxt Content v2 was the large bundle size required to store all content files. It was an issue when deploying to serverless or edge platforms like [Netlify](https://netlify.com), [NuxtHub](https://hub.nuxt.com) or [Vercel](https://vercel.com). + +In serverless environments, each user request triggers a fresh instance of your Nuxt server, it starts from scratch each time. This "stateless" nature means you can't store data in server memory or use file-based databases like SQLite. That's why we've implemented database adaptors that persist data independently of your server instances. + +::prose-note +We're manually switching to the appropriate provider (Vercel / Postgres, NuxtHub / D1...) according to the [database type](https://cfec52f9.content-f0q.pages.dev/docs/getting-started/configuration#database) you've set in your config. +:: + +### WASM SQLite in Browser + +For client-side navigation, the module uses a similar approach. When the application executes the first content query, it downloads the generated dump from the server and initializes a local SQLite database within the browser. From that point onward, all queries are executed locally without needing to call the server: significantly improving the responsiveness of the application and providing a seamless user experience. + +## 🗄️ Content Collections + +Collections are groups of related content items within your Nuxt Content project. They help organize and manage large datasets more efficiently. + +### **Define Collections** + +You can now define collections in the [`content.config.ts`](/docs/getting-started/configuration) file to configure the database structure, utility types, and methods for finding, parsing, and querying content. + +### **Collections Schema** + +Schemas enforce consistency within collections and improve TypeScript typings for better integration with Nuxt Content utilities. + +```ts [content.config.ts] +import { defineCollection, z } from '@nuxt/content' + +// Export collections +export const collections = { + // Define collection using `defineCollection` utility + posts: defineCollection({ + // Specify the type of content in this collection + type: 'page', + // Load every file matching this pattern + source: 'blog/**/*.md', + // Define custom schema for this collection + schema: z.object({ + date: z.date(), + image: z.object({ + src: z.string(), + alt: z.string() + }), + badge: z.object({ + label: z.string(), + color: z.string() + }) + }) + }), +} +``` + +::prose-tip{to="/docs/collections/define"} +Learn more about collections in the documentation. +:: + +## 🔧 Simplified Vue Utils + +We simplified the utils to now expose: + +- [queryCollection](/docs/utils/query-collection) to fetch your collections with our powerful query builder +- [queryCollectionNavigation](/docs/utils/query-collection-navigation) to fetch the generated navigation for a specific collection +- [queryCollectionItemSurroundings](/docs/utils/query-collection-item-surroundings) to fetch sibling content for a specific path +- [queryCollectionSearchSections](/docs/utils/query-collection-search-sections) to fetch searchable sections from a collection for enhanced content discovery + +These four utils allow your to efficiently fetch and query your content within your Vue pages and components: + +```vue [pages/blog.vue] + + + +``` + +## 📦 Built-in Components + +We've updated the components to include only the essentials: + +- [ContentRenderer](/docs/components/content-renderer) to render the parsed Markdown to HTML & Vue components +- [Slot](/docs/components/slot) replaced `ContentSlot` as we now support unwrapping using a directive, making your Vue components perfectly compatible to be used in both Vue & Markdown +- [Prose Components](/docs/components/prose) are pre-designed components tailored for MDC syntax, with integrated styling for a good appearance + +Here's an example of displaying the content of a Markdown file: + +```vue [pages/about.vue] + + + +``` + +## 🔷 TypeScript Integration + +The new collections system provides automatic TypeScript types for all your data. Every utility and API is strongly typed based on your collection definitions, ensuring robust type safety throughout development. + +## ⬆️ Migrating from V2 + +Migration should be as easy as possible, this is why we wrote the [migration guide](/docs/getting-started/migration). + +::prose-note +Note that we've decided to remove the document-driven mode to simplify the module usage. +:: + +## 🖼️ Studio Integration + +[Nuxt Studio](/studio) is a platform to visually edit your **Nuxt Content** projects in production. With support for `Markdown`, `YAML`, or `JSON` files, our editor ensures versatility and ease of use. + +### Preview API + +Previously an independent module, the [Studio module](https://github.com/nuxtlabs/studio-module) has been updated to be more generic and is now integrated directly into Nuxt Content as a `Preview API`. + +Enabling the preview functionality in Studio is easier than ever—simply configure the Studio API as your `Preview API` in your Nuxt Content settings: + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + content: { + preview: { + api: 'https://api.nuxt.studio' + } + } +}) +``` + +This simplification means no extra module is required for Studio, making setup faster. Furthermore, the Preview API is now generic, enabling other providers to deliver great editing experiences on top of Nuxt Content. + +### **Unified Documentation** + +In addition to this integration, we’ve unified the **Content** and **Studio** documentation and websites into a single comprehensive resource. Only the Studio platform (available once the user is logged-in) remains as a [standalone site](https://nuxt.studio). + +**We can now take advantage of data structures and collections in Studio**. The Studio platform supports and adapts its behaviour to **collections** and **user-defined schemas**. This enhancement will allow schema-generated forms for both `YAML` and `JSON` files as well as front-matter within Markdown files. diff --git a/docus/content/blog/visual-editor.md b/docus/content/blog/visual-editor.md new file mode 100644 index 000000000..1c6193024 --- /dev/null +++ b/docus/content/blog/visual-editor.md @@ -0,0 +1,83 @@ +--- +title: Behind the scenes of Nuxt Studio's visual editor +description: Discover the inner workings of Nuxt Studio's visual editor and how it interprets the Markdown syntax and generate it back. +image: + src: /blog/visual-editor.webp +authors: + - name: Baptiste Leproux + avatar: + src: https://avatars.githubusercontent.com/u/7290030?v=4 + to: https://x.com/_larbish + username: larbish + - name: Ferdinand Coumau + avatar: + src: https://avatars.githubusercontent.com/u/98885012?v=4 + to: https://x.com/CoumauFerdinand + username: CoumauFerdinand +date: 2024-09-04T00:00:00.000Z +category: studio +--- + +## **Introduction** + +Nuxt Studio offers a versatile workspace for both developers and content writers, giving them the freedom to choose between two distinct editors for content creation and management: the Markdown editor and the Visual editor. + +![Select your favorit editor from the project settings](/blog/favorite-editor.webp) + +Each editor serves its own purpose—some users are used to Markdown edition, while others prefer a non-technical, visual approach. + +At the end, **Markdown syntax is the final output** for both editors. + +This article explains the technical processes behind the visual editor, exploring how it interprets Markdown, converts it back, and why this process might occasionally lead to changes from the original content. + +## **Markdown Editor** + +![Edit directly markdown on Nuxt Studio](/blog/markdown-editor.webp) + +The Markdown editor in Nuxt Studio provides full control over your content, allowing you to write directly in [MDC](/docs/files/markdown) (an empowered Markdown syntax). This syntax enables integration of Vue components directly into your Markdown files, offering more flexibility to structure your pages. + +When your file is saved with the Markdown editor, the content is stored exactly as you've written it, preserving all specific syntax and formatting. This editor is ideal for users comfortable with Markdown who want precise control over the layout and structure of their content. + +## **Visual Editor** + +![Edit your content with a visual editor on Nuxt Studio](/blog/visual-editor.webp) + +The Visual Editor is a sort of WYSIWYG (What You See Is What You Get) tool built on top of [TipTap](https://tiptap.dev/) and [ProseMirror](https://prosemirror.net/), designed to abstract away the complexities of Markdown syntax and offer a more intuitive, visual editing experience. This editor is particularly user-friendly for those who prefer not to deal with raw Markdown code. + +### **How the visual editor processes files** + +When you open a Markdown file with the Visual Editor, Nuxt Studio first parses the original Markdown file. Using the [MDC module](https://github.com/nuxt-modules/mdc), it generates an Abstract Syntax Tree (AST). This AST is then converted into a TipTap-compatible format (TipTap AST), allowing the editor to accurately render the document visually. + +Once the Visual Editor displays the content, users can make updates in a visually intuitive way. Behind the scenes, the editor continuously transforms the TipTap AST back into MDC AST then MDC syntax, ensuring that your content remains in Markdown format. + +### **Why Changes might occur in the original markdown file without user modification** + +![Alert is displayed when automatic markdown parsing is detected](/blog/automatic-parsing-modal.webp) + +#### **Non-Critical Changes** + +As the Visual Editor translates the visual formatting back into Markdown, it applies a parsing algorithm that applies predefined Markdown standards. In some cases, these standards may differ slightly from the original content. These changes are typically non-impactful and are only another working syntax of the Markdown, the rendered website should remain consistent with the original. + +#### **Critical Changes** + +Ideally, every feature in Markdown has a direct and accurate equivalent in the Visual Editor. We've built custom TipTap extensions to support custom MDC syntax such as [Vue components](/docs/files/markdown#vue-components) edition or [front-matter](/docs/files/markdown#front-matter). However, in rare cases, particularly with complex or unconventional Markdown elements, the Visual Editor may not fully support or correctly interpret these elements. When this happens, the editor might approximate, simplify, or even omit these elements during the parsing process. + +Such discrepancies can result in data loss or regressions when converting back to Markdown. While these occurrences are rare, they can disrupt the intended display or functionality of your content. + +Our primary objective is to prevent any loss of content and to maintain the integrity of your Markdown files. If you encounter any issues where the transition from visual to Markdown isn’t perfect, we encourage you to report them on our Discord server. Your feedback is invaluable in helping us refine and improve the Visual Editor, ensuring it meets the needs of all users. + +## **Best practices to minimize unintended changes** + +To avoid losing crucial formatting or content, consider the following best practices: + +- **Avoid using complex HTML structures**. As the MDC syntax allows you to integrate Vue components, It's more effective to create reusable components that can be easily inserted into the Markdown and edited within the editor, rather than relying on intricate HTML code. +- **Use one editor consistently.** Whenever possible, select the editor that best suits your needs and stick with it for the entire page. +- **Review changes after switching from an editor to the other.** After switching editors, always review the Markdown (on the review page) and check the preview to ensure no important elements have been altered. + +## **Conclusion** + +Switching between the Markdown editor and the visual editor in Nuxt Studio offers flexibility, but it's important to be aware of the technical implications. + +Understanding how the visual editor processes and converts Markdown can help ensure that what you craft in Markdown is accurately displayed in the visual editor, allowing non-technical users to easily edit everything without altering the original Markdown file. + +### diff --git a/docus/content/changelog.yml b/docus/content/changelog.yml new file mode 100644 index 000000000..1de83648b --- /dev/null +++ b/docus/content/changelog.yml @@ -0,0 +1,3 @@ +title: Changelog +description: Follow the latest updates and improvements. + diff --git a/docus/content/changelog/frontmatter-form.md b/docus/content/changelog/frontmatter-form.md new file mode 100644 index 000000000..076eb9578 --- /dev/null +++ b/docus/content/changelog/frontmatter-form.md @@ -0,0 +1,113 @@ +--- +name: Visual front-matter edition +title: Visual Front-matter Edition +description: Your page metadata is now editable through a visual interface instead of YAML. +date: 2024-10-17T00:00:00.000Z +image: + src: /blog/frontmatters.png +authors: + - name: Baptiste Leproux + avatar: + src: https://avatars.githubusercontent.com/u/7290030?v=4 + to: https://x.com/_larbish +category: studio +--- + +::warning +This article was published before the merge of the [Content](https://github.com/nuxt/content) and [Studio](https://github.com/nuxtlabs/studio-module) modules on January 6, 2025. As a result, it may contain some inconsistencies. The Studio module is now deprecated and available as an opt-in feature of the Content module. Learn how to enable it in [this guide](/docs/getting-started). +:: + +## Visual Front-Matter editing + +You can now edit your markdown front-matter without writing in the `YAML` syntax. Instead, Nuxt Studio automatically generates a user-friendly form that simplifies metadata editing. + +:video{autoplay controls loop poster="https://res.cloudinary.com/nuxt/video/upload/v1729157955/frontmatterform2_rmh58v.jpg" src="https://res.cloudinary.com/nuxt/video/upload/v1729157955/frontmatterform2_rmh58v.mp4"} + +## What is the front-matter? + +Front-matter is a convention used in Markdown-based CMSs to provide metadata for pages, such as descriptions, titles, and more. In [Nuxt Content](/docs/files/markdown#front-matter), the front-matter uses the YAML syntax. + +::callout{icon="i-ph-info" to="/docs/files/markdown#front-matter"} +For more detailed information about front-matter syntax, visit the Nuxt Content documentation. +:: + +## The last piece of our non-technical editor + +Nuxt Studio has been designed with non-technical users in mind, mainly since our editor has been released. Our goal is to make markdown and content edition accessible to everyone. + +The automatic form generation for front-matter is the next logical step. By moving away from the complexities of YAML syntax, we’re simplifying the process for non-developers, offering dynamic input options like image pickers, date pickers, boolean toggles and more. This enhancement brings us to a fully visual, user-friendly content management experience. + +## Expanding to all YAML and JSON files + +Soon, the form generation feature will extend to all `YAML` and `JSON` files you edit within Nuxt Studio, making it easier than ever to work with structured data. + +## Looking ahead to Nuxt Content v3 + +::callout{icon="i-ph-lightbulb"} +This section is just a teaser of [Nuxt Content v3](https://github.com/nuxt/content/tree/v3). We will publish a more detailed blog post soon. +:: + +We're actively working on the next major update of Nuxt Content which will bring significant performance improvements and new features to further enhance your content management experience. + +### Improved Performance + +A key challenge with Nuxt Content v2 was the large bundle size required to store all content files. It was an issue when deploying to edge platforms like [NuxtHub](https://hub.nuxt.com/). + +To address this, Nuxt Content v3 moves away from the file based storing in production and leverage SQL database system. This switch is transparent to users. We're providing a zero config support for development mode, static generation, server rendering and edge deployments with NuxtHub. + +### Introducing Collections + +Collections are groups of related content items within your Nuxt Content project. They help organize and manage large datasets more efficiently. + +#### Define collections + +You'll be able to define collections in the `content.config.ts` file which is used by Nuxt Content to configure database structures, utility types, and methods for finding, parsing, and querying content. + +#### Collections schema + +Schemas enforce consistency within collections and improve TypeScript typings for better integration with Nuxt Content utilities. + +```ts [content.config.ts] +import { defineCollection, z } from '@nuxt/content' + +// Export collections +export const collections = { + // Define collection using `defineCollection` utility + posts: defineCollection({ + // Specify the type of content in this collection + type: 'page', + // Load every file matching this pattern + source: 'blog/**/*.md', + // Define custom schema for this collection + schema: z.object({ + date: z.date(), + image: z.object({ + src: z.string(), + alt: z.string() + }), + badge: z.object({ + label: z.string(), + color: z.string() + }) + }) + }), +} +``` + +### Built with Nuxt Studio in mind + +Nuxt Studio was originally developed alongside Nuxt Content v2, but with v3, we're building the module with Nuxt Studio experience in mind. Our goal is to create the best CMS platform for content editing, while still offering the best developers experience. + +For example, collection schemas will help us further enhance form generation in Studio. Among other things, you'll be able to set the editor type for a field directly in your schema. + +```ts [content.config.ts] +image: z.object({ + src: z.string().editor({ type: 'media' }) + alt: z.string() +}), +icon: z.string().editor({ type: 'icon' }) +``` + +::callout{icon="i-ph-lightbulb" to="https://github.com/nuxt/content/tree/main"} +Nuxt Content v3 has been officially released. Don't hesitate to try it out and give us feedback. +:: diff --git a/docus/content/changelog/studio-customisation.md b/docus/content/changelog/studio-customisation.md new file mode 100644 index 000000000..2f3148468 --- /dev/null +++ b/docus/content/changelog/studio-customisation.md @@ -0,0 +1,109 @@ +--- +name: Studio Frontmatter Customization +title: Studio Form Customisation +description: Studio forms are dynamically generated based on the collection + schema defined in your content configuration file. +date: 2025-02-20T01:00:00.000Z +image: + src: /blog/studio-form-generation.png + alt: Frontmatter form generation based on collection schema +authors: + - name: Baptiste Leproux + to: https://x.com/_larbish + avatar: + src: https://avatars.githubusercontent.com/u/7290030?v=4 +category: content +draft: false +--- + +The [Studio](https://nuxt.studio) forms are dynamically generated based on the collection schema defined in your content configuration file. This behaviour applies whether you’re editing the [frontmatter](/docs/files/markdown#frontmatter) of a `Markdown` file or a `JSON` / `YAML` file. + +:video{autoplay controls poster="https://res.cloudinary.com/nuxt/video/upload/v1739982761/frontmatterform_yjafgt.png" src="https://res.cloudinary.com/nuxt/video/upload/v1739982761/frontmatterform_yjafgt.mp4"} + +## **Defining your form with** `zod` Schema + +Nuxt Content leverages [zod](https://github.com/colinhacks/zod) to let you define a type-safe schema for your content. This schema not only validates your data but also powers the form generation in **Studio**. + +### **Built-in zod Helpers** + +You can define your Content schema by adding the `schema` property to your collection and by using a [zod](https://github.com/colinhacks/zod) schema. + +`@nuxt/content` exposes a `z` object that contains a set of [Zod](/) utilities for common data types. + +::prose-code-group +```ts [content.config.ts] +export default defineContentConfig({ + collections: { + posts: defineCollection({ + type: 'page', + source: 'blog/*.md', + schema: z.object({ + draft: z.boolean().default(false), + category: z.enum(['Alps', 'Himalaya', 'Pyrenees']).optional(), + date: z.date(), + image: z.object({ + src: z.string().editor({ input: 'media' }), + alt: z.string(), + }), + slug: z.string().editor({ hidden: true }), + icon: z.string().optional().editor({ input: 'icon' }), + authors: z.array(z.object({ + slug: z.string(), + username: z.string(), + name: z.string(), + to: z.string(), + avatar: z.object({ + src: z.string(), + alt: z.string(), + }), + })), + }), + }), + }, +}) +``` + + :::preview-card{icon="i-lucide-eye" label="Generated Form"} + ![Form preview](/docs/studio/preview-schema.png) + ::: +:: + +### **Native Inputs Mapping** + +Primitive Zod types are automatically mapped to appropriate form inputs in **Studio**: + +- **String** → Text input +- **Date** → Date picker +- **Number** → Number input (counter) +- **Boolean** → Toggle switch +- **Enum** → Select dropdown +- **Arrays of strings** → List of badge inputs +- **Arrays of objects** → Accordion of items with embedded form + +:video{autoplay controls loop poster="https://res.cloudinary.com/nuxt/video/upload/v1740679550/arrayobjectandstring_r1jpvz.jpg" src="https://res.cloudinary.com/nuxt/video/upload/v1740679550/arrayobjectandstring_r1jpvz.mp4"} + +### Custom Inputs Mapping + +Content goes beyond primitive types. You can customise form fields using the `editor` method, which extends Zod types with metadata to empower editor interface. + +This allows you to define custom inputs or hide fields. + +#### Usage + +```ts [content.config.ts] +mainScreen: z.string().editor({ input: 'media' }) +``` + +#### Options + +##### `input: 'media' | 'icon'` + +You can set the editor input type. Currently both icon and media are available since there are handled in Studio editor. + +##### `hidden: Boolean` + +This option can be set to avoid the display of a field in the Studio editor. + +::prose-tip +Studio inputs are fully extensible. We can create as many input as we want based on our users needs. +:: diff --git a/docus/content/changelog/yaml-json-form.md b/docus/content/changelog/yaml-json-form.md new file mode 100644 index 000000000..55e7c6a9d --- /dev/null +++ b/docus/content/changelog/yaml-json-form.md @@ -0,0 +1,46 @@ +--- +name: Visual YAML and JSON File Edition +title: Visual YAML and JSON File Edition +description: Edit YAML and JSON files with an automatically generated form. +date: 2024-10-28T01:00:00.000Z +image: + src: /docs/studio/json-yml-forms.png +authors: + - name: Baptiste Leproux + to: https://x.com/_larbish + avatar: + src: https://avatars.githubusercontent.com/u/7290030?v=4 +category: studio +--- + +::warning +This article was published before the merge of the [Content](https://github.com/nuxt/content) and [Studio](https://github.com/nuxtlabs/studio-module) modules on January 6, 2025. As a result, it may contain some inconsistencies. The Studio module is now deprecated and available as an opt-in feature of the Content module. Learn how to enable it in [this guide](/docs/getting-started). +:: + +## Auto-generated form for `YAML` and `JSON` files + +:video{controls loop src="https://res.cloudinary.com/nuxt/video/upload/v1730132248/yml-json-form_n9czcs.mp4"} + +Continuing our journey to make Nuxt Studio the tool for non-technical users to edit their content with Nuxt websites, we're excited to announce that `YAML` and `JSON` files can now be edited through a generated visual form. This update removes the need for users to interact directly with complex file syntax such as YAML or JSON. + +::callout{icon="i-ph-info"} +Arrays are not yet handled as form but we'll work on it once collections and user-defined schemas will be released with Nuxt Content v3. See the section below. +:: + +### Synchronized navigation + +Alongside this update, we’ve improved the synchronized navigation between the preview and selected files for non-Markdown formats (like YAML and JSON). To apply this fixe, please update the Studio module to the latest version `v2.2.0`. + +## On the Road to Nuxt Content v3 + +We’re excited to share that the fourth alpha version of Nuxt Content v3 has been released, with the [**draft documentation**](https://content.nuxt.com/) available. + +### What’s Next? + +In the coming months, we’ll focus on testing and refining Nuxt Content v3 to ensure a robust, production-ready release. Here’s a quick look at the Nuxt Studio related improvements ahead: + +- **Merging the Studio module**: Soon, the Studio module will be integrated directly into Nuxt Content. Once Nuxt Content v3 is released, activating Studio will be as simple as setting `content.editor: true` in your `nuxt.config.ts` file. This simplification means no extra module is required for Studio, making setup faster. +- **Unified documentation**: With the module integration, we’ll also merge the [Content](https://content.nuxt.com) and [Studio](https://nuxt.studio) documentation and websites into one comprehensive resource. Only the Studio platform (available once the user is logged) will remain as a standalone site. +- **Take advantage of data structures and collections in Studio**: With Nuxt Content v3, the Studio platform will support and adapt its behaviour to [collections](/docs/collections/define) and user-defined schemas. This enhancement will allow schema-generated forms for both YAML and JSON files as well as front-matter within Markdown files. + +These updates reflect our commitment to providing the best content editing platform for your Nuxt website. Stay tuned! diff --git a/docus/content/docs/.navigation.yml b/docus/content/docs/.navigation.yml new file mode 100644 index 000000000..d408b2086 --- /dev/null +++ b/docus/content/docs/.navigation.yml @@ -0,0 +1 @@ +title: Documentation diff --git a/docus/content/docs/1.getting-started/.navigation.yml b/docus/content/docs/1.getting-started/.navigation.yml new file mode 100644 index 000000000..f9de2eae3 --- /dev/null +++ b/docus/content/docs/1.getting-started/.navigation.yml @@ -0,0 +1,2 @@ +title: Getting Started +icon: i-lucide-square-play diff --git a/docus/content/docs/1.getting-started/1.index.md b/docus/content/docs/1.getting-started/1.index.md new file mode 100644 index 000000000..76a27cde6 --- /dev/null +++ b/docus/content/docs/1.getting-started/1.index.md @@ -0,0 +1,53 @@ +--- +navigation: + title: Introduction +title: Nuxt Content v3 +description: The powerful Git-based CMS designed specifically for Nuxt developers. +--- + +Welcome to Nuxt Content v3, a major upgrade that brings enhanced performance and innovative features to your Nuxt projects. This latest iteration of our Git-based CMS is optimized for modern application development. + +## What's New? + +### Content Collections + +Collections organize related items within your project, helping you manage large datasets more efficiently. Key benefits include: + +- **Structured Data**: Configure database architecture and define collections in [`content.config.ts`](/docs/collections/define#defining-collections) +- **Type-safe Queries**: Direct TypeScript integration across all utilities +- **Automatic Validation**: Ensure data consistency across frontmatter fields and data files (json, yml...) +- **Advanced Query Builder**: Filter, sort, and paginate your collections with ease +- **Studio Integration**: Enhanced form generation and optimal editing experience through [Studio](/studio) + +Learn more about [Content Collections](/docs/collections/define). + +### Improved Performance + +A significant challenge in v2 was the large bundle size needed for storing files, particularly affecting serverless deployments. + +V3 addresses this by transitioning to SQL-based storage in production. This switch requires zero configuration, supporting development mode, static generation, server hosting, serverless and edge deployments. + +::prose-note +The new database system enhances the way your data files are stored and structured, ensuring better performance and scalability. This update is entirely behind the scenes and does not affect the file types you can use in Content (`yml`, `json`, and `markdown` ). +:: + +Benefits include: + +- **Optimized Queries**: SQL storage enables ultra-fast data retrieval +- **Universal Compatibility**: Our adapter-based system integrates SQL databases across all deployment modes ([server](/docs/deploy/server), [serverless](/docs/deploy/serverless) and [static](/docs/deploy/static)). We welcome community contributions for additional adapters. + +### TypeScript Integration + +The new collections system provides automatic TypeScript types for all your data. Every utility and API is strongly typed based on your collection definitions, ensuring robust type safety throughout development. + +### Nuxt Studio Integration :badge[Soon]{color="neutral"} + +[Nuxt Studio](/docs/studio/setup) and v3 are designed to complement each other perfectly.. The [studio module](https://github.com/nuxtlabs/studio-module) is now integrated directly into Nuxt Content, creating an ideal environment where developers can focus on code while team members manage content through an intuitive interface. + +--- + +We're excited for you to explore these new capabilities. Dive into our documentation to learn more about integrating the module and implementing best practices in your next project. + +## Content V2 Migration + +Learn how to migrate from Content v2 to v3 in the [migration guide](/docs/getting-started/migration). diff --git a/docus/content/docs/1.getting-started/2.installation.md b/docus/content/docs/1.getting-started/2.installation.md new file mode 100644 index 000000000..494e90a28 --- /dev/null +++ b/docus/content/docs/1.getting-started/2.installation.md @@ -0,0 +1,149 @@ +--- +title: Installation +description: Get started with Nuxt Content v3 in your Nuxt application. +--- + +### Install the Package + +Choose your preferred package manager to install Nuxt Content v3: + +::code-group +```bash [pnpm] +pnpm add @nuxt/content +``` + +```bash [yarn] +yarn add @nuxt/content +``` + +```bash [npm] +npm install @nuxt/content +``` + +```bash [bun] +bun add @nuxt/content +``` +:: + +### Register the Module + +Add the Nuxt Content module to your `nuxt.config.ts`: + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + modules: ['@nuxt/content'] +}) +``` + +### Automatic Setup + +When starting a new Nuxt project with the `create-nuxt` CLI, you can simply select `@nuxt/content` from the interactive module selector. This will automatically install and register the module for you. + +::code-group +```bash [npm] +npm create nuxt +``` + +```bash [yarn] +yarn create nuxt +``` + +```bash [pnpm] +pnpm create nuxt +``` + +```bash [bun] +bun create nuxt +``` + +```bash [deno] +deno -A npm:create-nuxt@latest +``` +:: + +::note{color="warning"} +When you run your project in Node.js, Nuxt Content will ask you about the database connector to use. +You can choose to install `better-sqlite3` or `sqlite3` package. + +:br + + + +If you don't want to install any package, you can use native SQLite from Node.js\@v22.5.0 or newer. +Checkout [`experimental.nativeSqlite` configuration](/docs/getting-started/configuration#experimentalnativesqlite). +:: + +### Create your First Collection + +Create a `content.config.ts` file in your project root directory: + +```ts [content.config.ts] +import { defineContentConfig, defineCollection } from '@nuxt/content' + +export default defineContentConfig({ + collections: { + content: defineCollection({ + type: 'page', + source: '**/*.md' + }) + } +}) +``` + +This configuration creates a default `content` collection that processes all Markdown files located in the `content` folder of your project. You can customize the collection settings based on your needs. + +::tip +The `type: page` means there is a 1-to-1 relationship between content files and pages on your site. +:: + +::note{to="/docs/collections/define"} +Learn more in our **Collections guide**. +:: + +### Create your First Markdown Page + +Create a `content/index.md` file in your project root directory: + +```md [content/index.md] +# My First Page + +Here is some content. +``` + +Read more about writing [Markdown pages](/docs/files/markdown). + +### Display your Page + +Create a `pages/index.vue` file and display the page content: + +```vue [pages/index.vue] + + + +``` + +::note{icon="i-lucide-info"} +If you are installing Nuxt Content in a new Nuxt project and you didn't have `pages` directory, you also need to update the `app.vue` file to allow rendering the pages by adding the `NuxtPage` component. (If you already have some pages in your project, you are good to go.) + +```vue [app.vue] + +``` +:: + +::tip{icon="i-lucide-rocket"} +That's it! You've now created your first Nuxt Content page. +:: diff --git a/docus/content/docs/1.getting-started/3.configuration.md b/docus/content/docs/1.getting-started/3.configuration.md new file mode 100644 index 000000000..2ab90b164 --- /dev/null +++ b/docus/content/docs/1.getting-started/3.configuration.md @@ -0,0 +1,515 @@ +--- +title: Configuration +description: Nuxt Content is configured with sensible defaults. +--- + +To configure the content module and customize its behavior, you can use the `content` property in your `nuxt.config`: + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + content: { + // Options + } +}) +``` + +::note{to="https://github.com/nuxt-modules/mdc#configurations"} +In addition to configuring via `content.markdown`, you can use Markdown Components (MDC) to customize the rendering of Markdown elements with `mdc` property. +:: + +## `build` + +Nuxt Content read and parse all the available contents at build time. This option gives you control over parsing contents. + +### `markdown` + +Configure markdown parser. + +#### `toc` + +::code-group +```ts [Default] +toc: { + depth: 2, + searchDepth: 2 +} +``` + +```ts [Signature] +type Toc = { + depth: number + searchDepth: number +} +``` +:: + +Control behavior of Table of Contents generation. + +Value: + +- `depth`: Maximum heading depth to include in the table of contents. +- `searchDepth`: Maximum depth of nested tags to search for heading. + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + content: { + build: { + markdown: { + toc: { + depth: 3, // include h3 headings + } + } + } + } +}) +``` + +#### `remarkPlugins` + +::code-group +```ts [Default] +remarkPlugins: {} +``` + +```ts [Signature] +type RemarkPlugins = Record +``` +:: + +A list of [remark](https://github.com/remarkjs/remark) plugins to use. + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + content: { + build: { + markdown: { + // Object syntax can be used to override default options + remarkPlugins: { + // Override remark-emoji options + 'remark-emoji': { + options: { + emoticon: true + } + }, + // Disable remark-gfm + 'remark-gfm': false, + // Add remark-oembed + 'remark-oembed': { + // Options + } + }, + } + } + } +}) +``` + +#### `rehypePlugins` + +::code-group +```ts [Default] +rehypePlugins: {} +``` + +```ts [Signature] +type RehypePlugins = object +``` +:: + +A list of [rehype](https://github.com/remarkjs/remark-rehype) plugins to use. + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + content: { + build: { + markdown: { + // Object syntax can be used to override default options + rehypePlugins: { + 'rehype-figure': { + + } + }, + } + } + } +}) +``` + +#### `highlight` + +::code-group +```ts [Default] +highlight: false +``` + +```ts [Signature] +type Highlight = false | object +``` +:: + +Nuxt Content uses [Shiki](https://github.com/shikijs/shiki) to provide syntax highlighting for [`ProsePre`](/docs/components/prose#prosepre) and [`ProseCode`](/docs/components/prose#prosecode). + +| Option | Type | Description | +| - | - | - | +| `theme` | `ShikiTheme` or `Record` | The [color theme](https://github.com/shikijs/shiki/blob/main/docs/themes.md) to use. | +| `langs` | `ShikiLang[]` | The [loaded languages](https://github.com/shikijs/shiki/blob/main/docs/languages.md) available for highlighting. | + +- `highlight.theme` + +Theme can be specified by a single string but also supports an object with multiple themes. + +This option is compatible with [Color Mode module](https://color-mode.nuxtjs.org/). + +If you are using multiple themes, it's recommended to always have a `default` theme specified. + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + content: { + build: { + markdown: { + highlight: { + // Theme used in all color schemes. + theme: 'github-light', + // OR + theme: { + // Default theme (same as single string) + default: 'github-light', + // Theme used if `html.dark` + dark: 'github-dark', + // Theme used if `html.sepia` + sepia: 'monokai' + } + } + } + } + } +}) +``` + +- `highlight.langs` + +By default, the module loads a couple of languages for syntax highlighting: + +```ts [Default] +['json', 'js', 'ts', 'html', 'css', 'vue', 'shell', 'mdc', 'md', 'yaml'] +``` + +If you plan to use code samples of other languages, you need to define the language in these options. + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + content: { + build: { + markdown: { + highlight: { + langs: [ + 'c', + 'cpp', + 'java' + ] + } + } + } + } +}) +``` + +If you wish to add highlighting for an unsupported language, you can do so by loading the grammar file for the language. + +```ts [nuxt.config.ts] +import { readFileSync } from 'node:fs' + +export default defineNuxtConfig({ + content: { + build: { + markdown: { + highlight: { + langs: [ + // Read more about Shiki languages: https://shiki.style/guide/load-lang + JSON.parse( + readFileSync('./shiki/languages/gdscript.tmLanguage.json', 'utf-8'), + ), + ] + } + } + } + } +}) +``` + +Read more about adding languages in the [Shiki documentation](https://github.com/shikijs/shiki/blob/main/docs/languages.md#adding-grammar). + +### `pathMeta` + +Content module uses files path to generate the slug, default title and content order, you can customize this behavior with `pathMeta` option. + +#### `pathMeta.forceLeadingSlash` + +If set to `true`, the path will be prefixed with a leading slash. Default value is `true`. + +#### `pathMeta.slugifyOptions` + +Content module uses [slugify](https://github.com/simov/slugify) to generate the slug, you can customize the behavior of slugify with this option. + +Checkout [slugify options](https://github.com/simov/slugify#options) for more information. + +### `transformers` + +Nuxt Content has specific transformers for each content type to parse the raw content and prepare it for querying and rendering. Using this option you can define custom transformers to support new content types or improve functionalities of supported content types. + +::code-group +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + content: { + build: { + transformers: [ + '~/transformers/title-suffix', + ], + }, + }, +}) +``` + +```ts [~/transformers/title-suffix.ts] +import { defineTransformer } from '@nuxt/content' + +export default defineTransformer({ + name: 'title-suffix', + extensions: ['.md'], + transform(file) { + return { + ...file, + title: file.title + ' (suffix)', + } + }, +}) +``` + +:: + +Read more about transformers in the [Transformers](/docs/advanced/transformers) documentation. + +## `database` + +By default Nuxt Content uses a local SQLite database to store and query content. If you like to use another database or you plan to deploy on Cloudflare Workers, you can modify this option. + +Here is the list of supported database adapters: + +### `SQLite` + +If you want to change the default database location and move it to elsewhere you can use `sqlite` adapter to do so. This is the default value to the `database` option. Depending on your runtime-environment different sqlite adapters will be used (Node: better-sqlite-3, Bun: bun\:sqlite). + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + content: { + database: { + type: 'sqlite', + filename: 'SQLITE_DB_LOCATION' + } + } +}) +``` + +### `D1` + +If you plan to deploy your application to Cloudflare workers, you need to use the `d1` database adapter. Create a `d1` binding in the Cloudflare dashboard and fill in the `bindingName` field. + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + content: { + database: { + type: 'd1', + bindingName: 'CF_BINDING_NAME' + } + } +}) +``` + +### `Postgres` + +If you plan to deploy your application using PostgreSQL database you need to use the `postgres` database adapter. + +First, make sure to install the `pg` package: + +```bash [Terminal] +npx npm i pg +``` + +Then, configure the `postgres` adapter in your `nuxt.config.ts`: + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + content: { + database: { + type: 'postgres', + url: process.env.POSTGRES_URL, + /* Other options for `pg` */ + } + } +}) +``` + +### `LibSQL` + +If you plan to deploy your application using a LibSQL database you need to use the `libsql` database adapter. + +First, make sure to install the `@libsql/client` package: + +```bash [Terminal] +npx npm i @libsql/client +``` + +Then, configure the `libsql` adapter in your `nuxt.config.ts`: + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + content: { + database: { + type: 'libsql', + url: process.env.TURSO_DATABASE_URL, + authToken: process.env.TURSO_AUTH_TOKEN, + } + } +}) +``` + +::note +The most popular LibSQL hosting services is [Turso](https://turso.tech/). +:: + +## `renderer` + +Configure content renderer. + +### `anchorLinks` + +::code-group +```ts [Default] +{ h2: true, h3: true, h4: true } +``` + +```ts [Signature] +type AnchorLinks = boolean | Record<'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6', boolean> +``` +:: + +Control anchor link generation, by default it generates anchor links for `h2`, `h3` and `h4` heading + +Value: + +- `false`: will disable link generation. +- `true`: will enable link generation for all headings. + +### `alias` + +::code-group +```ts [Default] +alias: {} +``` + +```ts [Signature] +type Alias = Record +``` +:: + +Aliases will be used to replace markdown components and render custom components instead of default ones. + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + content: { + renderer: { + alias: { + p: 'MyCustomParagraph' + } + } + } +}) +``` + +## `watch` + +```ts [Default] +watch: { + enabled: true, + port: 4000, + showURL: false +} +``` + +Configure content hot reload in development. + +Value: + +- `enabled`: Enable/Disable hot reload. +- `port`: Select the port used for the WebSocket server. +- `showURL`: Toggle URL display in dev server boot message. + +Nuxt Content uses [listhen](https://github.com/unjs/listhen) to provide a local development server. Check out the [listhen documentation](https://github.com/unjs/listhen#options) for more information. + +::callout +The watcher is a development feature and will not be included in production. +:: + +::code-group +```ts [Enabled] +export default defineNuxtConfig({ + content: { + watch: { + port: 4000, + showURL: true + } + } +}) +``` + +```ts [Disabled] +export default defineNuxtConfig({ + content: { + watch: { + enabled: false + } + } +}) +``` +:: + +## `preview` + +Enable `Preview API` + +::prose-note +This is needed to enable live preview on [Nuxt Studio](/studio). +:: + +Value: + +- `dev`: Enable in development mode +- `api`: Activate the preview mode and set the `API` to be linked with. + +```ts [Enable Studio] +preview: { + api: 'https://api.nuxt.studio', +} +``` + +## `experimental` + +Experimental features that are not yet stable. + +### `experimental.nativeSqlite` + +As of Node.js v22.5.0, the `node:sqlite` module is available natively in Node.js. +This allows Nuxt Content to use SQLite as a database without the need for an external package. + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + content: { + experimental: { nativeSqlite: true }, + }, +}); +``` + +::prose-note +This feature is only available in Node.js v22.5.0 and above. Enabling this feature in older version will not do anything. +:: + diff --git a/docus/content/docs/1.getting-started/4.migration.md b/docus/content/docs/1.getting-started/4.migration.md new file mode 100644 index 000000000..945ad5a3e --- /dev/null +++ b/docus/content/docs/1.getting-started/4.migration.md @@ -0,0 +1,194 @@ +--- +title: Migration +description: How to migrate from v2 to v3 +--- + +Nuxt Content v3 has been rebuilt from the ground up, resulting in a new library with enhanced capabilities. While we've redesigned concepts and components in a similar way as Content v2, breaking changes are inevitable. + +Don't worry, you don't need to modify your content files. We made sure that Content v3 handles content in the same way as Content v2. + +## Changes + +### Vue utils + +- `queryContent()` API is replaced with new `queryCollection()` + +::prose-tip +The new API is backed by SQL and content queries happens within a specific collection. +:: + +- `fetchContentNavigation()` API is replaced with new `queryCollectionNavigation()` +- Surroundings now has its own separate API `queryCollectionItemSurroundings()` +- Document driven mode is dropped: `Markdown` files will not convert to Nuxt pages automatically, you need to create pages, [check this section to see how](/docs/components/content-renderer#example-usage). +- `useContent()` composable is removed +- `searchContent()` is dropped in favor of the new `queryCollectionSearchSections` API +- Full text search can easily be done using the `queryCollectionSearchSections` API, [check this section to see how](/docs/advanced/fulltext-search) + +### Components + +- All content should be rendered using `` component. ``, ``, `` and `` components are dropped in v3. +- `` and `` components are not supported in v3. Instead components can simply use Vue's native `` component + +::prose-note +`` and `` was initially pro to manipulate content before rendering and removing wrapping paragraphs from slot content. This unwrapping behavior is now supported via `mdc-unwrap` attribute in `` component. Example: `` +:: + +- Components created under the `components/content` directory are no longer automatically registered as global components. If you use [dynamic rendering](https://vuejs.org/guide/essentials/component-basics.html#dynamic-components) to render these components outside markdown files, you must manually register them in your Nuxt app. Check out the [Nuxt - Custom Components Directories](https://nuxt.com/docs/guide/directory-structure/components#custom-directories) documentation for more information on how to do so. + +### Types + +- `import type { NavItem } from '@nuxt/content/dist/runtime/types'` is replaced with `import type { ContentNavigationItem } from '@nuxt/content'` + +### General + +- `_dir.yml` files are renamed to `.navigation.yml` +- There is no source option in module options, instead you can define [multiple sources](/docs/collections/sources) for your collections in `content.config.ts`. +- Document `._path` is now renamed to `.path`, likewise all internal fields with `_` prefix are removed or renamed. +- `useContentHelpers()` is removed +- Module does not ignore dot files by default, you can ignore them by adding `ignore: ['**/.*']` in `exclude` options of your collection source. +- Due to SQL limitations, sort order now uses alphabetical order instead for numerical order. Check out the [Ordering Files](/docs/collections/types#ordering-files) section for more information. +- Module options have changed from v2. Check out [configuration page](/docs/getting-started/configuration) for details. + +### Nuxt Studio integration + +- The [studio module](https://nuxt.studio) has been deprecated and a new generic `Preview API` has been implemented directly into Nuxt Content, you can remove the `@nuxthq/studio` package from your dependencies and from the `nuxt.config.ts` modules. Instead we just need to enable the preview mode in the Nuxt configuration file by binding the Studio API. + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + content: { + preview: { + api: 'https://api.nuxt.studio' + } + }, +}) +``` + +- In order to keep the [app config file](/docs/studio/config) updatable from Studio we just need to update the helper import of the `nuxt.schema.ts` file from `@nuxthq/studio/theme` to `@nuxt/content/preview`. + +## Implement Document Driven mode in v3 + +Implementing document driven mode in Content v3 is quite easy. All you need is to create a catch-all page in Nuxt and fetch contents based on route path. + +```vue [pages/[...slug\\].vue] + + + +``` + +## Converting `queryContent` to `queryCollections` + +As we mentioned above, `queryContent` is dropped in favor of new collection based `queryCollection`. There are two main differences between these two: + +1. `queryCollection` is building a query for an SQL database. +2. `queryCollection` does the search only inside the specified collection. You should know the collection's name (key on config). + +```ts [Find content with path] +// Content v2 +const v2Query = await queryContent(route.path).findOne() +// Content v3 - don't forget to create `content` collection in `content.config.ts` +const v3Query = await queryCollection('content').path(route.path).first() +``` + +```ts [Find contents with custom filter] +// Content v2 +const v2Query = await queryContent() + .where({ path: /^\/hello\/.*/ }) + .find() +// Content v3 - don't forget to create `content` collection in `content.config.ts` +const v3Query = await queryCollection('content') + .where('path', 'LIKE', '/hello%') + .first() +``` + +::prose-note{to="/docs/collections/define"} +Check the dedicated section for more info about collections +:: + +## Convert `queryContent().findSurround()` + +Surround now has its own separate API. + +```ts +const targetPath = '/docs' + +// Content v2 +const v2Surround = await queryContent(targetPath) + .only(['title', 'description', 'navigation']) + .findSurround(withoutTrailingSlash(route.path)) + +// Content v3 - don't forget to create `content` collection in `content.config.ts` +const v3Surround = await queryCollectionItemSurroundings( + 'content', + targetPath, + { + fields: ['title', 'description', 'navigation'] + } +) +``` + +::prose-note +Check the dedicated section for more information about the +:: + +## Consolidate `ProsePre`, `ProseCode`, and `ProseCodeInline` components + +Many `ProsePre` components are thin wrappers around the `ProseCode` component. We've consolidated these three components into two components. There is now no difference between `ProsePre` and multi-line code blocks. + +1. MDC will now map and parse single backticks `` ` `` as `ProseCode` instead of `ProseCodeInline`. +2. MDC will now map and parse block code starting with three backticks` ``` ` as `ProsePre` component. + +**Suggested Changes:** + +1. Your current `ProseCode` logic should be moved to `ProsePre` +2. Rename your `ProseCodeInline` components to `ProseCode` + +## `_dir.yml` files are renamed to `.navigation.yml` + +In Content v3, we renamed `_dir.yml` to `.navigation.yml`. The new name better reflects the purpose of these files. +Module uses these files to gather information about directories for generating navigation. + +Note that in order to make these files available for Module, youe should define your collection's source in +a way that includes these files. For example `source: '**'` and `source: '**/*.{md|yml}` will include these files +in collection, but `source: '**/*.md'` will not include them. + + +## Ignore dot files + +By default, Content v3 does not ignore dot files. If you want to ignore them, you can add `ignore: ['**/.*']` in the `exclude` option of your collection source. + +```ts +defineCollection({ + source: { + include: '**', + exclude: ['**/.*'] + } +}) +``` + +Note that the above pattern will also exclude `.navigation.yml` file from collection. If you use `.navigation.yml` and want to keep them +you can use `**/.(!(navigation.yml))` pattern to exclude all dot files except `.navigation.yml`. + +```ts +defineCollection({ + source: { + include: '**', + exclude: ['**/.!(navigation.yml)'] + } +}) +``` + + + diff --git a/docus/content/docs/2.collections/.navigation.yml b/docus/content/docs/2.collections/.navigation.yml new file mode 100644 index 000000000..e4f762fdd --- /dev/null +++ b/docus/content/docs/2.collections/.navigation.yml @@ -0,0 +1,3 @@ + +title: Collections +icon: i-lucide-database diff --git a/docus/content/docs/2.collections/1.define.md b/docus/content/docs/2.collections/1.define.md new file mode 100644 index 000000000..2d922dd85 --- /dev/null +++ b/docus/content/docs/2.collections/1.define.md @@ -0,0 +1,155 @@ +--- +title: Define Content Collections +navigation: + title: Define +description: Learn how to define and configure content collections in your Nuxt application. +--- + +The Nuxt Content module automatically parses any content files within the `content/` directory located at the root of your Nuxt application. This setup allows you to freely structure the folder to suit your project's needs. + +For better organization, consider using Content Collections, which let you categorize and manage content more effectively. These collections are defined in a `content.config.ts` file. + +::warning +If no `content.config.ts` file is present, all files within the content folder are parsed and imported by default. However, once a config file is added, only files matching the specified path patterns defined in collections will be imported. +:: + +## What are Content Collections? + +Content Collections organize related items within your Nuxt Content project. They provide a structured way to manage your content, making it easier to query, display, and maintain your site's data. + +Key features include: + +- **Logical Grouping**: Group similar content together, such as blog posts, product pages, or documentation articles +- **Shared Configuration**: Apply common settings and validations across all items within a collection +- **Improved Querying**: Fetch and filter related content items efficiently +- **Automatic Type Inference**: Get type safety and autocompletion in your development environment +- **Flexible Structure**: Organize collections by content type, category, or any other logical grouping that suits your needs + +## Defining Collections + +Create a `content.config.ts` file in your project's root directory. This special file configures your collections database, utility types, and content handling. + +Here's a basic example: + +```ts [content.config.ts] +import { defineCollection, defineContentConfig } from '@nuxt/content' + +export default defineContentConfig({ + collections: { + docs: defineCollection({ + // Load every file inside the `content` directory + source: '**', + // Specify the type of content in this collection + type: 'page' + }) + } +}) +``` + +::warning +Currently, a document is designed to be present in only one collection at a time. If a file is referenced in multiple collections, live reload will not work correctly. To avoid this, it is recommended to use the `exclude` attribute to explicitly exclude a document from other collections using appropriate regex patterns. + +This topic is still under discussion in this issue: [nuxt/content#2966](https://github.com/nuxt/content/issues/2966). +:: + +### Collection Schema + +Schemas enforce data consistency within a collection and serve as the source of truth for TypeScript types. + +On top of the built-in fields, you can define a schema by adding the `schema` property to your collection by using a [`zod`](https://zod.dev) schema: + +```ts [content.config.ts] +import { defineCollection, defineContentConfig, z } from '@nuxt/content' + +export default defineContentConfig({ + collections: { + blog: defineCollection({ + source: 'blog/*.md', + type: 'page', + // Define custom schema for docs collection + schema: z.object({ + tags: z.array(z.string()), + image: z.string(), + date: z.date() + }) + }) + } +}) +``` + +::note +`@nuxt/content` exposes a `z` object that contains a set of Zod schemas for common data types. Check [Zod’s README](https://github.com/colinhacks/zod) for complete documentation on how Zod works and what features are available. +:: + +::tip +You can define as many collections as you want to organize different types of content. +:: + +## Querying Collections + +Use the [`queryCollection`](/docs/utils/query-collection) util to fetch one or all items from a collection: + +```vue [pages/blog.vue] + + + +``` + +::note{to="/docs/utils/query-collection"} +Learn more about the available query options in our `queryCollections` API documentation. +:: + +## defineCollection() + +The `defineCollection` function defines a collection in your content configuration. Here's its TypeScript signature: + +```ts +function defineCollection(collection: Collection): DefinedCollection + +type Collection = { + // Determines how content is processed + type: 'page' | 'data' + // Specifies content location + source?: string | CollectionSource + // Zod schema for content validation and typing + schema?: ZodObject +} +``` + +::note{to="/docs/collections/types"} +Learn more about collection types. +:: + +```ts +type CollectionSource = { + // Glob pattern for content matching + include: string + // .path prefix (only applies to 'page' type) + prefix?: string + // Glob patterns to exclude content + exclude?: string[] + // Root directory for content matching + cwd?: string + // Remote git repository URL (e.g., https://github.com/nuxt/content) + repository?: string + // Authentication token for private repositories (e.g., GitHub personal access token) + authToken?: string +} +``` + +::note{to="/docs/collections/sources"} +Learn more about collection sources. +:: + +The function returns the defined collection object. diff --git a/docus/content/docs/2.collections/2.types.md b/docus/content/docs/2.collections/2.types.md new file mode 100644 index 000000000..dfaa77ccf --- /dev/null +++ b/docus/content/docs/2.collections/2.types.md @@ -0,0 +1,138 @@ +--- +title: Collection Types +navigation: + title: Types +description: Learn about the two types of collections you can define in Nuxt Content. +--- + +In Nuxt Content, you can specify a type for each collection, depending on the intended purpose of the collection files. Collections can be defined as either **page** or **data** types. + +For both types, built-in fields are generated. Every collection includes these default fields: + +- `id`: Unique content identifier +- `stem`: File path without extension (used for sorting and location) +- `extension`: File extension +- `meta`: Custom fields not defined in the collection schema + +## Page type + +```ts +defineCollection({ + source: '**/*.md', + type: 'page' +}) +``` + +::tip +Use the **page** type if there is a 1-to-1 relationship between content files and pages on your site. +:: + +### Path generation + +Nuxt Content will automatically generate a path for each file in the collection, making it easy to create URL mappings. + +Here are examples of generated paths based on file structure: + +| | | +| -------------------------------- | --------------------- | +| | | +| File | Path | +| `content/index.md` | `/` | +| `content/about.md` | `/about` | +| `content/blog/index.md` | `/blog` | +| `content/blog/hello.md` | `/blog/hello` | +| `content/1.guide/2.installation` | `/guide/installation` | + +::note +You can use the helper [`queryCollection('COLLECTION').path('PATH')`](/docs/utils/query-collection) to retrieve content by a specific path. +:: + +### Schema Overrides + +When you use the **page** type, Nuxt Content generates several standard fields that are commonly used for web pages. These fields provide structure and are **automatically** applied to the collection’s schema: + +- `path`: Generated route path +- `title`: Page title +- `description`: Page description +- `seo`: SEO metadata (to be used with Nuxt's `useSeoMeta` composable) +- `body`: Page content parsed as AST +- `navigation`: Page navigation configuration (for [queryCollectionNavigation](/docs/utils/query-collection-navigation)) + +Here is the corresponding schema applied: + +```ts + path: z.string(), + title: z.string(), + description: z.string(), + seo: z.intersection( + z.object({ + title: z.string().optional(), + description: z.string().optional(), + meta: z.array(z.record(z.string(), z.any())).optional(), + link: z.array(z.record(z.string(), z.any())).optional(), + }), + z.record(z.string(), z.any()), + ).optional().default({}), + body: z.object({ + type: z.string(), + children: z.any(), + toc: z.any(), + }), + navigation: z.union([ + z.boolean(), + z.object({ + title: z.string(), + description: z.string(), + icon: z.string(), + }), + ]).default(true), +``` + +::note +You can override any of these fields by defining them in the collection’s schema. +:: + +## Data type + +```ts +defineCollection({ + source: 'authors/**.yml', + type: 'data' +}) +``` + +The data type is useful for content that doesn't directly correspond to a webpage but instead represents structured data you might want to query and display within your application. + +With data collections, you have complete control over the schema, allowing you to define custom structures. + +::note +There's no strict relationship between collection type and file extension. For instance, a **page** collection can use [Markdown](/docs/files/markdown) or [YAML](/docs/files/yaml) or [JSON](/docs/files/json) files, and **data** collections can use any of these formats as well. +:: + +## Ordering Files + +For both types, you may want to control the display order in lists. Use numeric prefixes in file and directory names to specify an order. Nuxt Content will use these numbers when ordering content lists. + +::note +Nuxt Content uses alphabetical order for sorting, so if you want to use numerical order, you need to prefix single digit numbers with `0`. For example, without the `0` prefix, `10.foo.md` would come before `2.bar.md`. +:: + +```text [Directory structure] +content/ + 1.frameworks/ + 1.vue.md + 2.nuxt.md + ... + 2.examples/ + 01.nuxthub.md + 02.vercel.md + 03.netlify.md + 04.heroku.md + ... + 10.cloudflare.md + index.md +``` + +::warning +Separate number from file name using `.` character. Using any other separator will not work. +:: diff --git a/docus/content/docs/2.collections/3.sources.md b/docus/content/docs/2.collections/3.sources.md new file mode 100644 index 000000000..20114ef01 --- /dev/null +++ b/docus/content/docs/2.collections/3.sources.md @@ -0,0 +1,127 @@ +--- +title: Collection Sources +navigation: + title: Sources +description: Learn how to import your files in Nuxt Content collections. +--- + +Nuxt Content provides several ways to import content files into your collection. You can configure the source by using the `source` property within `defineCollection`: + +```ts [content.config.ts] +import { defineCollection, defineContentConfig } from '@nuxt/content' + +export default defineContentConfig({ + collections: { + docs: defineCollection({ + source: '**', + type: 'page' + }) + } +}) +``` + +## `source` + +The `source` property can be defined as either a string (following a glob pattern) or an object, allowing more detailed source configuration for your target directory and files within the content folder. + +**Example:** + +- `source: '**'` includes all files within the content directory and its subdirectories. +- `source: '**/*.md'`includes all `Markdown` files within the content directory and its subdirectories. +- `source: 'docs/**/*.yml'` includes all `YML` files within the `content/docs` and its subdirectories. +- `source: '**/*.{json,yml}'` includes `JSON` or `YML` file within the content directory and all its subdirectories. +- `source: '*.json'` includes only `JSON` files located directly within the content directory, excluding any subdirectories. + +### `include` (required) + +Glob pattern of your target repository and files in the content folder. + +### `exclude` + +Glob patterns to exclude content from the import. + +### `prefix` + +This configuration only applied for **page** type with 1-to-1 relationship between content files and pages on your site. + +It represents the path prefix (base URL) of the corresponding page on the website. + +::prose-warning +The `prefix` must start by a leading `/`. +:: + +By default, module extracts the static prefix of `source`(or `source.include`) and uses it as a prefix for content paths. For example, if you define `/en/**` source, module will auto-fill the `prefix` with `/en`. You can manually provide a prefix to override this behavior. The prefix can be removed by setting `prefix: '/'` in the collection source. + +```ts +defineCollection({ + type: "page", + source: { + include: "en/**", + exclude: ["en/index.md"], + prefix: '/' + } +}) +``` + +### `cwd` + +Root directory for content matching. + +**Example:** + +If you want to include files from a folder outside the content directory, set the absolute path of that folder to the `cwd` property. + +```ts +source: { + cwd: path.resolve('packages/my-pkg/docs'), + include: '**/*.md', +} +``` + +### `repository` + +External source representing a remote git repository URL (e.g., ). + +When defining an external source you must also define the `include` option. +`include` pattern is essential for the module to know which files to use for the collection. + +```js +import { defineCollection, defineContentConfig } from '@nuxt/content' + +export default defineContentConfig({ + collections: { + docs: defineCollection({ + type: 'page', + source: { + repository: 'https://github.com/nuxt/content', + include: 'docs/content/**', + }, + }) + } +}) +``` + +### `authToken` + +Authentication token for private repositories (e.g., GitHub personal access token). + +::warning{icon="i-lucide-shield-alert"} +Never commit authentication tokens or credentials directly in your code. Use environment variables or other secure methods to provide these values at runtime. +:: + +### `authBasic` + +Basic authentication for private repositories (e.g., Bitbucket username and password). + +```ts +defineCollection({ + type: 'page', + source: { + repository: 'https://bitbucket.org/username/repo', + authBasic: { + username: 'username', + password: 'password', + }, + }, +}) +``` diff --git a/docus/content/docs/3.files/.navigation.yml b/docus/content/docs/3.files/.navigation.yml new file mode 100644 index 000000000..59e4fb8b7 --- /dev/null +++ b/docus/content/docs/3.files/.navigation.yml @@ -0,0 +1,3 @@ + +title: Files +icon: i-lucide-file diff --git a/docus/content/docs/3.files/1.markdown.md b/docus/content/docs/3.files/1.markdown.md new file mode 100644 index 000000000..3a0668965 --- /dev/null +++ b/docus/content/docs/3.files/1.markdown.md @@ -0,0 +1,552 @@ +--- +title: Markdown +description: Create and query Markdown files in your Nuxt applications and use + the MDC syntax to integrate Vue components. +--- + +## Usage + +### Define a Collection + +```ts [content.config.ts] +import { defineCollection, defineContentConfig, z } from '@nuxt/content' + +export default defineContentConfig({ + collections: { + blog: defineCollection({ + type: 'page', + source: 'blog/*.md', + schema: z.object({ + date: z.string() + }) + }) + } +}) +``` + +::note{to="/docs/collections/types#page-type"} +Learn more about the `page` collection type. +:: + +### Create `.md` files + +Create blog posts in `content/blog/` directory. + +::code-group +```md [foo.md] +--- +date: 2020-11-11 +--- + +# Foo + +This is Foo blog post. +``` + +```md [bar.md] +--- +date: 2024-12-12 +--- +Hello +I am bar. Nice to meet you. +``` +:: + +### Query Markdown Files + +Now we can query blog posts: + +```ts +// Get the foo post +const fooPost = await queryCollection('blog').path('/blog/foo').first() + +// Find all posts +const allPosts = await queryCollection('blog').order('date', 'DESC').all() +``` + +### Display Markdown + +To display the content of a markdown file, you can use the [``](/docs/components/content-renderer) component. + +```vue [blog/[slug\\].vue] + + + +``` + +::note +Read more about the [``](/docs/components/content-renderer) component and [`Prose Components`](/docs/components/prose). +:: + +## Frontmatter + +Frontmatter is a convention of Markdown-based CMS to provide meta-data to pages, like description or title. In Nuxt Content, the frontmatter uses the YAML syntax with `key: value` pairs. + +These data are available when rendering the content and can store any information that you would need. + +### Syntax + +You can declare a frontmatter block at the top of the Markdown files in the `content/` directory with the `---` identifier. + +```md [content/index.md] +--- +title: 'Title of the page' +description: 'meta description of the page' +--- + + +``` + +```ts [example.ts] +const home = await queryCollection('content').path('/').first() + +console.log(home.title) +// => 'Title of the page' +console.log(home.description) +// => 'meta description of the page' +console.log(home.body) +// => AST object of the page content +``` + +### Native parameters + +| | | | | +| ------------- | --------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------- | +| Key | Type | Default | Description | +| `title` | `string` | First `

` of the page | Title of the page, will also be injected in metas | +| `description` | `string` | First `

` of the page | Description of the page, will be shown below the title and injected into the metas | +| `navigation` | `boolean` | `true` | Define if the page is included in [`queryCollectionNavigation`](/docs/utils/query-collection-navigation) return value. | + +::warning +Additional parameters that you have defined in your frontmatter block need to be defined in your schema (see the date parameter in the example at top of this page) to be able to use them for querying. +:: + +## MDC Syntax + +We created the MDC syntax to supercharge Markdown and give you the ability to integrate Vue components with slots and props inside your Markdown. + +::callout +--- +icon: i-simple-icons-visualstudiocode +to: https://marketplace.visualstudio.com/items?itemName=Nuxt.mdc +--- +Install the **MDC VS Code extension** to get proper syntax highlighting for the MDC syntax. +:: + +## Vue Components + +You can use any Vue component in your Markdown files. + +We have a special syntax to make it easier to use components in your Markdown files. + +```mdc [content/index.md] +::component-name +Default slot content +:: +``` + +::warning +Components that are used in Markdown has to be marked as `global` in your Nuxt app if you don't use the `components/content/` directory, visit [Nuxt 3 docs](https://nuxt.com/docs/guide/directory-structure/components) to learn more about it. +:: + +### Block Components + +Block components are components that accept Markdown content or another component as a slot. + +The component must contain at least one `` component to accept formatted text. + +In a markdown file, use the component with the `::` identifier. + +::code-group +```mdc [index.md] +::card +The content of the card +:: +``` + +```html [Card.vue] + + +``` + + :::preview-card{icon="i-lucide-eye" label="Preview"} + ::::example-card + The content of the card + :::: + ::: +:: + +### Slots + +A component's slots can accept content or another components. + +- **Default slot** renders the top-level content inside the block component or with `#default` +- **Named slots** use the `#` identifier to render the corresponding content. + +::code-group +```mdc [index.md] +::hero +My Page Title + +#description +This will be rendered inside the `description` slot. +:: +``` + +```html [Hero.vue] + +``` + + :::preview-card{icon="i-lucide-eye" label="Preview"} + ::::example-hero + My Page Title + + #description + This will be rendered inside the `description` slot. + :::: + ::: +:: + +::note +Read more about the [``](/docs/components/slot) component. +:: + +::tip +You can use Markdown inside your components slots: + + :::code-group + ```mdc [index.md] + ::the-title + A [rich text](/) will be **rendered** by the component. + :: + ``` + + ```html [MyTitle.vue] + + ``` + + ::::preview-card{icon="i-lucide-eye" label="Preview"} + :::::example-title + A [rich text](/) will be **rendered** by the component. + ::::: + :::: + ::: +:: + +### Props + +There are two ways to pass props to components using MDC. + +#### Inline method + +The `{}` identifier passes props to components in a terse way by using a `key=value` syntax. + +::code-group +```mdc [index.md] +::alert{type="warning"} +The **alert** component. +:: +``` + +```vue [Alert.vue] + + + +``` + + :::preview-card{icon="i-lucide-eye" label="Preview"} + ::::example-alert{type="warning"} + The **alert** component. + :::: + ::: +:: + +Multiple props can be separated with a space: + +```mdc +::alert{type="warning" icon="exclamation-circle"} +Oops! An error occurred +:: +``` + +The `v-bind` shorthand `:` can be also be used to bind a prop to a value in the frontmatter. + +```mdc +--- +type: "warning" +--- + +::alert{:type="type"} +Your warning +:: +``` + +If you want to pass arrays or objects as props to components you can pass them as JSON string and prefix the prop key with a colon to automatically decode the JSON string. Note that in this case you should use single quotes for the value string so you can use double quotes to pass a valid JSON string: + +::code-group +```mdc [array.md] +::dropdown{:items='["Nuxt", "Vue", "React"]'} +:: +``` + +```mdc [number-array.md] +::dropdown{:items='[1,2,3.5]'} +:: +``` + +```mdc [object.md] +::chart{:options='{"responsive": true, "scales": {"y": {"beginAtZero": true}}}'} +:: +``` +:: + +#### YAML method + +The YAML method uses the `---` identifier to declare one prop per line, that can be useful for readability. + +::code-group +```mdc [index.md] +::icon-card +--- +icon: IconNuxt +description: Harness the full power of Nuxt and the Nuxt ecosystem. +title: Nuxt Architecture. +--- +:: +``` + +```html [IconCard.vue] + + + +``` + + :::preview-card{icon="i-lucide-eye" label="Preview"} + ::::example-icon-card + --- + description: Harness the full power of Nuxt and the Nuxt ecosystem. + icon: IconNuxt + title: Nuxt Architecture. + --- + :::: + ::: +:: + +### Attributes + +Attributes are useful for highlighting and modifying part of paragraph. The syntax is nearly similar to inline components and markdown links syntax. + +Possible values are all named attributes, classes with the notation `.class-name` and an ID with `#id-name`. + +::code-group +```mdc [index.md] +Hello [World]{style="color: green;" .custom-class #custom-id}! +``` + + :::preview-card{icon="i-lucide-eye" label="Preview"} + Hello [World]{#custom-id .custom-class style="color: green;"} ! + ::: +:: + +In addition to mdc components and `span`, attribute syntax will work on images, links, inline `code`, **bold** and _italic_ text. + +::code-group +```md [index.md] +Attributes work on: + +- [link](#attributes){style="background-color: pink;"}, `code`{style="color: cyan;"}, +- _italic_{style="background-color: yellow; color:black;"} and **bold**{style="background-color: lightgreen;"} texts. +``` + + :::preview-card{prose label="Preview"} + Attributes work on: + + - [link](#attributes){style="background-color: pink;"}, `code`, + - *italic* and **bold** texts. + ::: +:: + +## Binding Data in Markdown + +You can bind data within your Markdown document using the `{{ $doc.variable || 'defaultValue' }}` syntax. These values can be defined in the YAML frontmatter at the top of the document, within each MDC component, or injected using the `data` prop of the `` component. + +### Example 1: Define in YAML + +```mdc +--- +title: 'Title of the page' +description: 'meta description of the page' +customVariable: 'Custom Value' +--- + +# The Title is {{ $doc.title }} and customVariable is {{ $doc.customVariable || 'defaultValue' }} + +``` + +### Example 2: Define in external with `` + +```html [test.vue] + + + +``` + +```mdc [test.md] +# Hello {{ $doc.name || 'World' }} + +``` + +## Prose Components + +In Nuxt Content, the prose represents HTML tags generated by the Markdown syntax, such as heading levels and links. + +For each HTML tag, a Vue component is used, allowing you to override them if needed, for example `

` becomes ``. + +If you want to customize a Prose component, here are the recommended steps: + +- Check out the original [component sources](https://github.com/nuxt-modules/mdc/blob/main/src/runtime/components/prose). +- Use the exact same props. +- In your `components/content/` directory, give it the same name. +- Make it yours 🚀. + +::note{to="/docs/components/prose"} +Read the complete Prose reference in the Prose Components section. +:: + +## Code Highlighting + +Nuxt Content uses [Shiki](https://github.com/shikijs/shiki), which colors tokens with VSCode themes. + +Code highlighting works both on [`ProsePre`](/docs/components/prose#prosepre) and [`ProseCode`](/docs/components/prose#prosecodeinline). + +Each line of a code block gets its line number in the `line` attribute so lines can be labeled or individually styled. + +::callout +[Read the API reference to configure or entirely disable syntax highlighting.](/docs/getting-started/configuration) +:: + +## Images + +You can add images to your `public` directory: + +```bash [Directory structure] +content/ + index.md +public/ + image.png +nuxt.config.ts +package.json +``` + +And then use them in your markdown files in the `content` directory as such: + +```md [content/index.md] +![my image](/image.png) +``` + +## Excerpt + +Content excerpt or summary can be extracted from the content using `` as a divider. + +```md [content/index.md] +--- +title: Introduction +--- + +Learn how to use `@nuxt/content`. + + + +Full amount of content beyond the more divider. +``` + +Description property will contain the excerpt content unless defined within the frontmatter props. + +If there is no `` divider in the text then excerpt is undefined. + +::tip +You should define the `excerpt` field in the collection schema if you want to use the excerpt feature. +```ts [content.config.ts] +const content = defineCollection({ + type: 'page', + source: '**', + schema: z.object({ + excerpt: z.object({ + type: z.string(), + children: z.any(), + }), + }), +}) +``` +Read more about the [collection schema](/docs/collections/define#collection-schema). +:: + +Example variables will be injected into the document: + +```json +{ + "excerpt": Object + "body": Object + // ... other keys +} +``` diff --git a/docus/content/docs/3.files/2.yaml.md b/docus/content/docs/3.files/2.yaml.md new file mode 100644 index 000000000..ca81a4610 --- /dev/null +++ b/docus/content/docs/3.files/2.yaml.md @@ -0,0 +1,65 @@ +--- +title: YAML +description: How to define, write and query YAML data. +--- + +## Define Collection + +```ts [content.config.ts] +import { defineCollection, defineContentConfig, z } from '@nuxt/content' + +export default defineContentConfig({ + collections: { + authors: defineCollection({ + type: 'data', + source: 'authors/**.yml', + schema: z.object({ + name: z.string(), + avatar: z.string(), + url: z.string() + }) + }) + } +}) + +``` + +## Create `.yml` files + +Create authors files in `content/authors/` directory. + +::code-group +```yaml [farnabaz.yml] +name: Ahad Birang +avatar: https://avatars.githubusercontent.com/u/2047945?v=4 +url: https://github.com/farnabaz +``` + +```yaml [larbish.yml] +name: Baptiste Leproux +avatar: https://avatars.githubusercontent.com/u/7290030?v=4 +url: https://github.com/larbish +``` +:: + +## Query Data + +Now we can query authors: + +```vue + +``` diff --git a/docus/content/docs/3.files/3.json.md b/docus/content/docs/3.files/3.json.md new file mode 100644 index 000000000..520a2eb0d --- /dev/null +++ b/docus/content/docs/3.files/3.json.md @@ -0,0 +1,73 @@ +--- +title: JSON +description: How to define, write and query JSON data. +--- + +## Define Collection + +```ts [content.config.ts] +import { defineCollection, defineContentConfig, z } from '@nuxt/content' + +export default defineContentConfig({ + collections: { + authors: defineCollection({ + type: 'data', + source: 'authors/**.json', + schema: z.object({ + name: z.string(), + avatar: z.string(), + url: z.string() + }) + }) + } +}) + +``` + +## Create `.json` files + +Create authors files in `content/authors/` directory. + +::code-group +```json [farnabaz.json] +{ + "name": "Ahad Birang", + "avatar": "https://avatars.githubusercontent.com/u/2047945?v=4", + "url": "https://github.com/farnabaz" +} +``` + +```json [larbish.json] +{ + "name": "Baptiste Leproux", + "avatar": "https://avatars.githubusercontent.com/u/7290030?v=4", + "url": "https://github.com/larbish" +} +``` +:: + +::warning +Each file in `data` collection should contain only one object, therefore having top level array in a JSON file will cause invalid result in query time. +:: + +## Query Data + +Now we can query authors: + +```vue + +``` diff --git a/docus/content/docs/3.files/4.csv.md b/docus/content/docs/3.files/4.csv.md new file mode 100644 index 000000000..7b9042dea --- /dev/null +++ b/docus/content/docs/3.files/4.csv.md @@ -0,0 +1,142 @@ +--- +title: CSV +description: How to define, write and query CSV data. +--- + +## Define Collection + +```ts [content.config.ts] +import { defineCollection, defineContentConfig, z } from '@nuxt/content' + +export default defineContentConfig({ + collections: { + authors: defineCollection({ + type: 'data', + source: 'authors/**.csv', + schema: z.object({ + name: z.string(), + email: z.string(), + avatar: z.string() + }) + }) + } +}) + +``` + +## Create `.csv` files + +Create author files in `content/authors/` directory. + +::code-group +```csv [users.csv] +id,name,email +1,John Doe,john@example.com +2,Jane Smith,jane@example.com +3,Alice Johnson,alice@example.com +``` + +```csv [team.csv] +name,role,avatar +John Doe,Developer,https://avatars.githubusercontent.com/u/1?v=4 +Jane Smith,Designer,https://avatars.githubusercontent.com/u/2?v=4 +``` +:: + +::warning +Each CSV file should have a header row that defines the column names, which will be used as object keys when parsed. +:: + +## Query Data + +Now we can query authors: + +```vue + + + +``` + +## Configuration + +You can configure how CSV files are parsed in your `nuxt.config.ts`: + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + content: { + build: { + csv: { + // Convert CSV data to JSON objects + json: true, + // Specify custom delimiter (default is ',') + delimiter: ',' + } + } + } +}) +``` + +With `json: true` in the configuration, each row will be converted to a JavaScript object with the header row used as keys: + +```json +[ + { + "id": "1", + "name": "John Doe", + "email": "john@example.com" + }, + { + "id": "2", + "name": "Jane Smith", + "email": "jane@example.com" + } +] +``` + +## Custom Delimiters + +If your CSV files use a different delimiter, you can specify it in the configuration: + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + content: { + build: { + csv: { + delimiter: ';' // Use semicolon as delimiter + } + } + } +}) +``` + +This would parse CSV files like: + +```csv [semicolon-data.csv] +id;name;email +1;John Doe;john@example.com +2;Jane Smith;jane@example.com +``` + +::note +The CSV parser can be disabled by setting `csv: false` in the configuration if you don't need CSV support. +:: diff --git a/docus/content/docs/4.utils/.navigation.yml b/docus/content/docs/4.utils/.navigation.yml new file mode 100644 index 000000000..5d7785c95 --- /dev/null +++ b/docus/content/docs/4.utils/.navigation.yml @@ -0,0 +1,2 @@ +icon: i-lucide-square-function +title: Query Utils diff --git a/docus/content/docs/4.utils/1.query-collection.md b/docus/content/docs/4.utils/1.query-collection.md new file mode 100644 index 000000000..d833b9d5e --- /dev/null +++ b/docus/content/docs/4.utils/1.query-collection.md @@ -0,0 +1,301 @@ +--- +title: queryCollection +description: The queryCollection composable provides methods for querying and + fetching your collections. +--- + +## Usage + +Use the auto-imported `queryCollection` to find contents inside a collection. Here we assume that you have defined `docs` collection inside `content.config.ts`. + + +If you have not defined any collection, check [How to define a collection](/docs/collections/define#defining-collections). + + +```vue [pages/[...slug\\].vue] + +``` + +::tip +The `queryCollection` utility is available in both Vue and Nitro. Checkout [Server Usage](#server-usage) for more details on how to use it on the server side. +:: + +## API + +### Type + +```ts +function queryCollection(collection: T): CollectionQueryBuilder + +interface CollectionQueryBuilder { + where(field: keyof T | string, operator: SQLOperator, value?: unknown): CollectionQueryBuilder + andWhere(groupFactory: QueryGroupFunction): CollectionQueryBuilder + orWhere(groupFactory: QueryGroupFunction): CollectionQueryBuilder + order(field: keyof T, direction: 'ASC' | 'DESC'): CollectionQueryBuilder + // ... other methods +} +``` + +### `queryCollection(collection: CollectionName)` + +Create a query builder to search in the specific collection. + +- Parameters: +- - `collection`: The key of defined collection in `content.config.ts` + +### `path(path: string)` + +Search for contents that have specific `path`. (`path` is an special field in `page` collections which generates based on fs path and can be use as route to render the content) + +- Parameter: +- - `path`: The path string to match. + +```ts +const route = useRoute() +const { data } = await useAsyncData(route.path, () => { + return queryCollection('docs').path(route.path).first() +}) +``` + +### `select(...fields: keyof Collection)` + +Select specific fields from the collection to be returned in the query result. + +- Parameters: + - `...fields`: A list of field names to select from the collection. + +```ts +const route = useRoute() +const { data } = await useAsyncData(route.path, () => { + return queryCollection('docs') + .select('path', 'title', 'description') + .first() +}) +``` + +### `where(field: keyof Collection | string, operator: SqlOperator, value?: unknown)` + +Add a condition to the query to filter results based on a specific field. + +- Parameters: + - `field`: The field to filter on + - `operator`: The SQL operator to use for comparison. Possible values include: + - `'='`: Equal to + - `'>'`: Greater than + - `'<'`: Less than + - `'<>'`: Not equal to + - `'IN'`: In a list of values + - `'BETWEEN'`: Between two values + - `'NOT BETWEEN'`: Not between two values + - `'IS NULL'`: Is null + - `'IS NOT NULL'`: Is not null + - `'LIKE'`: Matches a pattern + - `'NOT LIKE'`: Does not match a pattern + - `value`: The value to compare against. The type depends on the operator used. + +```ts +const route = useRoute() +const { data } = await useAsyncData(route.path, () => { + return queryCollection('docs') + .where('date', '<', '2024-04-04') + .where('category', '=', 'news') + .all() +}) + +// Generated SQL +// SELECT * FROM docs WHERE date < '2024-04-04' AND category = 'news' +``` + +### `andWhere(groupFactory: QueryGroupFunction)` + +Add an AND condition group to the query. This allows for more complex query conditions. + +- Parameter: + - `groupFactory`: A function that receives a query builder and can add multiple conditions that will be grouped together with AND + +```ts +const { data } = await useAsyncData('recent-docs', () => { + return queryCollection('docs') + .where('published', '=', true) + .andWhere(query => query.where('date', '>', '2024-01-01').where('category', '=', 'news')) + .all() +}) + +// Generated SQL +// SELECT * FROM docs WHERE published = true AND (date > '2024-01-01' AND category = 'news') +``` + +### `orWhere(groupFactory: QueryGroupFunction)` + +Add an OR condition group to the query. This allows for alternative conditions. + +- Parameter: + - `groupFactory`: A function that receives a query builder and can add multiple conditions that will be grouped together with OR + +```ts +const { data } = await useAsyncData('featured-docs', () => { + return queryCollection('docs') + .where('published', '=', true) + .orWhere(query => query.where('featured', '=', true).where('priority', '>', 5)) + .all() +}) + +// Generated SQL +// SELECT * FROM docs WHERE published = true AND (featured = true OR priority > 5) +``` + +### `order(field: keyof Collection, direction: 'ASC' | 'DESC')` + +Order the query results based on a specific field. + +- Parameters: + - `field`: The field to order by. + - `direction`: The direction of ordering, either 'ASC' for ascending or 'DESC' for descending. + +```ts +const route = useRoute() +const { data } = await useAsyncData(route.path, () => { + return queryCollection('docs') + .order('date', 'DESC') + .all() +}) +``` + +### `limit(limit: number)` + +Limit the number of results returned by the query. + +- Parameter: + - `limit`: The maximum number of results to return. + +```ts +const route = useRoute() +const { data } = await useAsyncData(route.path, () => { + return queryCollection('docs') + .limit(10) + .all() +}) +``` + +### `skip(skip: number)` + +Skip a specified number of results in the query. + +- Parameter: + - `skip`: The number of results to skip. + +```ts +const route = useRoute() +const { data } = await useAsyncData(route.path, () => { + return queryCollection('docs') + // Skip first 5 items + .skip(5) + .all() +}) +``` + +### `all()` + +Execute the query and return all matching results. + +- Returns: A Promise that resolves to an array of all matching documents. + +```ts +const route = useRoute() +const { data } = await useAsyncData(route.path, () => { + return queryCollection('docs').all() +}) +``` + +### `first()` + +Execute the query and return the first matching result. + +- Returns: A Promise that resolves to the first matching document, or `null` if no documents match. + +```ts +const route = useRoute() +const { data } = await useAsyncData(route.path, () => { + return queryCollection('docs').first() +}) +``` + +### `count()` + +Count the number of matched collection entries based on the query. + +```ts +const route = useRoute() +const { data } = await useAsyncData(route.path, () => { + return queryCollection('docs') + // Count matches + .count() +}) + +// Returns +5 // number of matches +``` + +You can also use `count()` with other methods defined above such as `where()` in order to apply additional conditions within the collection query. + +```ts +const route = useRoute() +const { data } = await useAsyncData(route.path, () => { + return queryCollection('docs') + .where('date', '<', '2024-04-04') + // Count matches + .count() +}) + +// Returns +3 // number of matches for the provided query +``` + +## Examples + +Here is a complete example of how to fetch a list of documents in the `docs` collections. + +```vue [index.vue] + + + +``` + +## Server Usage + +Nuxt Content provides a similar utility to query collections on the server side. The only difference is that you need to pass `event` as the first argument to the `queryCollection` function. + +```ts [server/api/[slug\\].ts] +export default eventHandler(async (event) => { + const { slug } = getRouterParams(event) + const page = await queryCollection(event, 'docs').path(slug).first() + return page +}) +``` + +:::note +Make sure to create `server/tsconfig.json` file with the following content to avoid type error. + +```json +{ + "extends": "../.nuxt/tsconfig.server.json" +} +``` +::: diff --git a/docus/content/docs/4.utils/2.query-collection-navigation.md b/docus/content/docs/4.utils/2.query-collection-navigation.md new file mode 100644 index 000000000..50e1acce4 --- /dev/null +++ b/docus/content/docs/4.utils/2.query-collection-navigation.md @@ -0,0 +1,202 @@ +--- +title: queryCollectionNavigation +description: The queryCollectionNavigation composable generates the navigation + tree of given collection. +--- + +## Type + +```ts +function queryCollectionNavigation( + collection: T, + fields?: Array +): ChainablePromise + +interface ChainablePromise extends Promise { + where(field: keyof PageCollections[T] | string, operator: SQLOperator, value?: unknown): ChainablePromise + andWhere(groupFactory: QueryGroupFunction): ChainablePromise + orWhere(groupFactory: QueryGroupFunction): ChainablePromise + order(field: keyof PageCollections[T], direction: 'ASC' | 'DESC'): ChainablePromise +} +``` + +## Usage + +Use the auto-imported `queryCollectionNavigation` to generate a navigation tree for a specific collection. This is particularly useful for creating dynamic navigation menus or sidebars based on your content structure. + +The function returns a chainable promise that allows you to add additional query conditions: + +```vue [pages/[...slug\\].vue] + +``` + +::tip +The `queryCollectionNavigation` utility is available in both Vue and Nitro. Checkout [Server Usage](#server-usage) for more details on how to use it on the server side. +:: + +### Navigation metadata with `.navigation.yml` + +You can add metadata to a directory using a `.navigation.yml` file. + +```yml [.navigation.yml] +title: Getting Started +icon: i-lucide-square-play +``` + +## API + +### `queryCollectionNavigation(collection: CollectionName, extraField: keyof Collection)` + +Generate a navigation tree for the specified collection. + +- Parameters: + - `collection`: The key of the defined collection in `content.config.ts`. + - `extraFields`: (Optional) An array of additional fields to include in the navigation items. (By default `title` and `path` are included in the navigation items.) +- Returns: A chainable promise that resolves to a navigation tree structure. The promise includes methods for adding query conditions: + - `where(field, operator, value)`: Add a WHERE condition + - `andWhere(groupFactory)`: Add a grouped AND condition + - `orWhere(groupFactory)`: Add a grouped OR condition + - `order(field, direction)`: Add an ORDER BY clause + +The navigation tree is generated based on the directory structure and ordering happens based on files [ordering](/docs/collections/types#ordering-files) + +## Examples + +Basic usage without additional query conditions: + +```vue [pages/[...slug\\].vue] + + + +``` + +Example with additional query conditions and extra fields: + +```vue [pages/[...slug\\].vue] + + + +``` + + +## Server Usage + +Nuxt Content provides a similar utility to query collections on the server side. The only difference is that you need to pass `event` as the first argument to the `queryCollectionNavigation` function. + +```ts [server/api/navigation.ts] +export default eventHandler(async (event) => { + const navigation = await queryCollectionNavigation(event, 'docs') + return navigation +}) +``` + +:::note +Make sure to create `server/tsconfig.json` file with the following content to avoid type error. + +```json +{ + "extends": "../.nuxt/tsconfig.server.json" +} +``` +::: + +--- + +## Extra utilities to work with navigation + +Content module provides some extra utilities to simplify common use cases like building breadcrumb navigation. + +### `findPageBreadcrumb(navigation, path, options?)` + +Returns the breadcrumb trail (array of navigation items) for a given path within a navigation tree. Useful for building breadcrumb navigation components. + +- `navigation`: The navigation tree (array of ContentNavigationItem). +- `path`: The current page path. +- `options` (optional): + - `current`: Include the current page in the breadcrumb. + - `indexAsChild`: Treat index pages as children. + +**Example:** + +```ts +import { findPageBreadcrumb } from '@nuxt/content/utils' + +const breadcrumb = findPageBreadcrumb(navigation, '/docs/guide/getting-started') +// breadcrumb is an array of navigation items leading to the current page +``` + + +### `findPageChildren(navigation, path, options?)` + +Finds and returns the direct children of a given path in the navigation tree. + +- `navigation`: The navigation tree (array of ContentNavigationItem). +- `path`: The parent path to find children for. +- `options` (optional): + - `indexAsChild`: Treat index pages as children. + +**Example:** + +```ts +import { findPageChildren } from '@nuxt/content/utils' + +const children = findPageChildren(navigation, '/docs/guide') +// children is an array of navigation items under '/docs/guide' +``` + + + +### `findPageSiblings(navigation, path, options?)` + +Returns the sibling navigation items for a given path (i.e., other items with the same parent). + +- `navigation`: The navigation tree (array of ContentNavigationItem). +- `path`: The current page path. +- `options` (optional): + - `indexAsChild`: Treat index pages as children. + +**Example:** + +```ts +import { findPageSiblings } from '@nuxt/content/utils' + +const siblings = findPageSiblings(navigation, '/docs/guide/getting-started') +// siblings is an array of navigation items that share the same parent as the current page +``` diff --git a/docus/content/docs/4.utils/3.query-collection-item-surroundings.md b/docus/content/docs/4.utils/3.query-collection-item-surroundings.md new file mode 100644 index 000000000..50a8ac13e --- /dev/null +++ b/docus/content/docs/4.utils/3.query-collection-item-surroundings.md @@ -0,0 +1,133 @@ +--- +title: queryCollectionItemSurroundings +description: The queryCollectionItemSurroundings composable looks for sibling + contents of an specific path. +--- + +## Type + +```ts +function queryCollectionItemSurroundings( + collection: T, + path: string, + opts?: SurroundOptions +): ChainablePromise + +interface ChainablePromise extends Promise { + where(field: keyof PageCollections[T] | string, operator: SQLOperator, value?: unknown): ChainablePromise + andWhere(groupFactory: QueryGroupFunction): ChainablePromise + orWhere(groupFactory: QueryGroupFunction): ChainablePromise + order(field: keyof PageCollections[T], direction: 'ASC' | 'DESC'): ChainablePromise +} +``` + +## Usage + +Use the auto-imported `queryCollectionItemSurroundings` to find the previous and next items relative to a specific content item in a collection. This is particularly useful for creating navigation between related content pages. + +The function returns a chainable promise that allows you to add additional query conditions: + +```vue [pages/[...slug\\].vue] + +``` + +::tip +The `queryCollectionItemSurroundings` utility is available in both Vue and Nitro. Checkout [Server Usage](#server-usage) for more details on how to use it on the server side. +:: + + +## API + +### `queryCollectionItemSurroundings(collection: CollectionName, path: string, opts?: SurroundOptions)` + +Find the surrounding items (previous and next) for a specific content item in a collection. + +- Parameters: + - `collection`: The key of the defined collection in `content.config.ts`. + - `path`: The path of the current content item. + - `opts`: (Optional) An object with the following properties: + - `before`: (Optional) The number of items to fetch before the current item. Default is 1. + - `after`: (Optional) The number of items to fetch after the current item. Default is 1. + - `fields`: (Optional) An array of additional fields to include in the surrounding items. +- Returns: A chainable promise that resolves to an array containing the surrounding items. The promise includes methods for adding query conditions: + - `where(field, operator, value)`: Add a WHERE condition + - `andWhere(groupFactory)`: Add a grouped AND condition + - `orWhere(groupFactory)`: Add a grouped OR condition + - `order(field, direction)`: Add an ORDER BY clause + +The final result will be an array with the following structure: + +- `[previousItem, nextItem]` if using default options +- `[...previousItems, ...nextItems]` if using custom `before` and `after` values + +Each item in the array is of type `ContentNavigationItem` or `null` if there is no item in that position. + +## Examples + +Basic usage without additional query conditions: + +```vue [pages/[...slug\\].vue] + + + +``` + +Example with additional query conditions: + +```vue [pages/[...slug\\].vue] + +``` + + + +## Server Usage + +Nuxt Content provides a similar utility to query collections on the server side. The only difference is that you need to pass `event` as the first argument to the `queryCollectionItemSurroundings` function. + +```ts [server/api/surroundings.ts] +export default eventHandler(async (event) => { + const surroundings = await queryCollectionItemSurroundings(event, 'docs', '/foo') + return surroundings +}) +``` + +:::note +Make sure to create `server/tsconfig.json` file with the following content to avoid type error. + +```json +{ + "extends": "../.nuxt/tsconfig.server.json" +} +``` +::: + diff --git a/docus/content/docs/4.utils/4.query-collection-search-sections.md b/docus/content/docs/4.utils/4.query-collection-search-sections.md new file mode 100644 index 000000000..93724bf22 --- /dev/null +++ b/docus/content/docs/4.utils/4.query-collection-search-sections.md @@ -0,0 +1,88 @@ +--- +title: queryCollectionSearchSections +description: The queryCollectionSearchSections composable generates searchable + sections from a collection for enhanced content discovery. +--- + +## Type + +```ts +function queryCollectionSearchSections(collection: keyof Collections, opts?: { ignoredTags: string[] }): ChainablePromise + +interface ChainablePromise extends Promise { + where(field: keyof PageCollections[T] | string, operator: SQLOperator, value?: unknown): ChainablePromise + andWhere(groupFactory: QueryGroupFunction): ChainablePromise + orWhere(groupFactory: QueryGroupFunction): ChainablePromise + order(field: keyof PageCollections[T], direction: 'ASC' | 'DESC'): ChainablePromise +} +``` + +## Usage + +Use the auto-imported `queryCollectionSearchSections` to generate searchable sections from a specific collection. This is particularly useful for creating advanced search functionality or content discovery features in your application. + +```vue [app.vue] + +``` + +::tip +The `queryCollectionSearchSections` utility is available in both Vue and Nitro. Checkout [Server Usage](#server-usage) for more details on how to use it on the server side. +:: + + +## API + +### `queryCollectionSearchSections(collection: CollectionName, options?: SearchSectionsOptions)` + +Generate searchable sections from the specified collection. + +- Parameters: + - `collection`: The key of the defined collection in `content.config.ts`. + - `options`: (Optional) An object with the following properties: + - `ignoredTags`: An array of tag names to ignore when generating sections. Default is an empty array. +- Returns: A Promise that resolves to an array of searchable sections. Each section is an object with the following properties: + - `id`: A unique identifier for the section. + - `title`: The title of the section (usually the heading text). + - `titles`: An array of parent section titles, representing the hierarchy. + - `content`: The textual content of the section. + - `level`: The heading level (1-6) of the section, where 1 is the highest level. + +## Example + +Here's an example of how to use `queryCollectionSearchSections` to create searchable sections from the 'docs' collection: + +```vue [pages/[...slug\\].vue] + +``` + +## Server Usage + +Nuxt Content provides a similar utility to query collections on the server side. The only difference is that you need to pass `event` as the first argument to the `queryCollectionSearchSections` function. + +```ts [server/api/search-sections.ts] +export default eventHandler(async (event) => { + const sections = await queryCollectionSearchSections(event, 'docs') + return sections +}) +``` + +:::note +Make sure to create `server/tsconfig.json` file with the following content to avoid type error. + +```json +{ + "extends": "../.nuxt/tsconfig.server.json" +} +``` +::: + diff --git a/docus/content/docs/5.components/.navigation.yml b/docus/content/docs/5.components/.navigation.yml new file mode 100644 index 000000000..c9e7f3f38 --- /dev/null +++ b/docus/content/docs/5.components/.navigation.yml @@ -0,0 +1,3 @@ +title: Components +navigation: + icon: i-lucide-square-code diff --git a/docus/content/docs/5.components/0.content-renderer.md b/docus/content/docs/5.components/0.content-renderer.md new file mode 100644 index 000000000..16a8bd3c3 --- /dev/null +++ b/docus/content/docs/5.components/0.content-renderer.md @@ -0,0 +1,67 @@ +--- +title: ContentRenderer +description: Takes your component from an AST to a wonderful template. +--- + +The `` component renders a document coming from a query with [`queryCollection()`](/docs/utils/query-collection). + +::note +This component **only works** with `Markdown` files. +:: + +## Props + +| Prop | Default | Type | Description | +|------------|------------|----------------------------|-------------| +| `value` | `{}` | `ParsedContent` | The document to render. | +| `tag` | `'div'` | `string` | The tag to use for the renderer element if it is used. | +| `excerpt` | `false` | `boolean` | Whether to render the excerpt only without the rest of the content. | +| `components` | `{}` | `object` | The map of custom components to use for rendering. This prop will pass to the markdown renderer and will not affect other file types. | +| `data` | `{}` | `object` (required) | A map of variables to inject into the markdown content for later use in binding variables. | +| `prose` | `undefined` | `boolean` | Whether or not to render Prose components instead of HTML tags. | +| `class` | `undefined` | `string` or `object` | Root tag to use for rendering. | +| `unwrap` | `false` | `boolean` or `string` | Tags to unwrap separated by spaces. Example: `'ul li'`. | + + +## Example Usage + +```vue [pages/[...slug\\].vue] + + + +``` + +## Handling Missing Pages +If the queried content is **missing**, you can display a **custom fallback message**. + +```vue [pages/[...slug\\].vue] + + + +``` + +## Handling Empty Pages +If the queried content is **empty**, you can display a **custom fallback message**. diff --git a/docus/content/docs/5.components/1.slot.md b/docus/content/docs/5.components/1.slot.md new file mode 100644 index 000000000..551182b7b --- /dev/null +++ b/docus/content/docs/5.components/1.slot.md @@ -0,0 +1,104 @@ +--- +title: Slot +description: The fastest way to inject Markdown into your Vue components. +--- + +When you write contents and paragraphs inside a component with the MDC syntax, you can use Vue's `` component to render the content. + +## Usage + +If you don't want to modify the rendered content, simply use Vue's `` component. + +```vue [components/content/Callout.vue] + +``` + +Now let's use it in Markdown: + +```mdc [content/index.md] +::callout +This is a callout. +:: +``` + +The rendered HTML will be: + +```html +

+

This is a callout.

+
+``` + +This usage would be similar to using the native `` component. + +### Unwrapping + +The `mdc-unwrap` prop allows you to remove one or multiple wrapping elements from the rendered content. This is useful when you want to extract the content nested in native Markdown syntax. Each specified tag will get removed from AST. + +Let's unwrap the `

` element from the previous example: + +```vue [components/content/Callout.vue] + +``` + +Now the rendered HTML will be: + +```html +

+ This is a callout. +
+``` + +### Named Slots + +The `name` prop allows you to bind a slot by its name. This is useful when you want to render a slot that is not the default one. + +Let's improve our `Callout` component to have a `title` slot: + +```vue [components/content/Callout.vue] + +``` + +Now let's use it in Markdown: + +```mdc [content/index.md] +::callout +#title +Please be careful! +#default +Using MDC & Vue components is addictive. +:: +``` + +This will result into: + +```html +
+

Please be careful!

+

Using MDC & Vue components is addictive.

+
+``` + +When not using the `title` slot, the `h2` element will not be rendered. + +## Props + +- `mdc-unwrap`: Whether to unwrap the content or not. This is useful when you want to extract the content nested in native Markdown syntax. Each specified tag will get removed from AST. + - **Type:** `boolean` or `string` + - **Default:** `false` + - **Example:** `'p'` or `'ul li'` diff --git a/docus/content/docs/5.components/2.prose.md b/docus/content/docs/5.components/2.prose.md new file mode 100644 index 000000000..98fe8c6aa --- /dev/null +++ b/docus/content/docs/5.components/2.prose.md @@ -0,0 +1,339 @@ +--- +title: Prose Components +description: A list of Prose components. +links: + - label: Source + icon: i-simple-icons-github + size: sm + to: https://github.com/nuxt-modules/mdc/tree/main/src/runtime/components/prose +toc: + searchDepth: 2 +--- + +Prose components are replacements for HTML typography tags. Prose components provide a simple way to customize content UI. + +To overwrite a prose component, create a component with the same name in your project `components/content/` directory (ex: `components/content/ProseA.vue`). + +::note +Prose components are originally part of [`@nuxtjs/mdc`](https://github.com/nuxt-modules/mdc). +:: + +## `ProseA` + +::code-group +```md [Code] +[Link](/docs/components/prose) +``` + + :::preview-card{icon="i-lucide-eye" label="Preview"} + [Link](/docs/components/prose) + ::: +:: + +## `ProseBlockquote` + +::code-group +```md [Code] +> Block quote +``` + + :::preview-card{icon="i-lucide-eye" label="Preview"} + > Block quote + ::: +:: + +## `ProsePre` + +::code-group +````md [Code] + ```js [file.js]{2} meta-info=val + export default () => { + console.log('Code block') + } + ``` +```` + + :::preview-card{icon="i-lucide-eye" label="Preview"} + ```js [file.js] + export default () => { + console.log('Code block') + } + ``` + ::: +:: + +Component properties will be: + +```json +{ + code: "export default () => {\n console.log('Code block')\n}" + language: "js" + filename: "file.js" + highlights: [2] + meta: "meta-info=val" +} +``` + +Check out the [highlight options](/docs/getting-started/configuration#highlight) for more about the syntax highlighting. + +::callout{type="warning"} +If you want to use `]` in the filename, you need to escape it with 2 backslashes: `\\]`. This is necessary since JS will automatically escape the backslash in a string so `\]` will be resolved as `]` breaking our regex. +:: + +## `ProseCode` + +::code-group +```md [Code] +`code` + +`const code: string = 'highlighted code inline'`{lang="ts"} +``` + + :::preview-card{icon="i-lucide-eye" label="Preview"} + `code` + + `const code: string = 'highlighted code inline'` + ::: +:: + +## `ProseH1` + +::code-group +```md [Code] +# H1 Heading +``` + + :::preview-card{.pt-4 label="Preview"} + + # H1 Heading + ::: +:: + +## `ProseH2` + +::code-group +```md [Code] +## H2 Heading +``` + + :::preview-card{icon="i-lucide-eye" label="Preview"} + + ## H2 Heading + ::: +:: + +## `ProseH3` + +::code-group +```md [Code] +### H3 Heading +``` + + :::preview-card{icon="i-lucide-eye" label="Preview"} + + ### H3 Heading + ::: +:: + +## `ProseH4` + +::code-group +```md [Code] +#### H4 Heading +``` + + :::preview-card{icon="i-lucide-eye" label="Preview"} + + #### H4 Heading + ::: +:: + +## `ProseH5` + +::code-group +```md [Code] +##### H5 Heading +``` + + :::preview-card{icon="i-lucide-eye" label="Preview"} + + ##### H5 Heading + ::: +:: + +## `ProseH6` + +::code-group +```md [Code] +###### H6 Heading +``` + + :::preview-card{icon="i-lucide-eye" label="Preview"} + + ###### H6 Heading + ::: +:: + +## `ProseHr` + +::code-group +```md [Code] +Divider under. + +--- + +Divider above. +``` + + :::preview-card{icon="i-lucide-eye" label="Preview"} + Divider under. + + --- + + Divider above. + ::: +:: + +## `ProseImg` + +::code-group +```md [Code] +![A Cool Image](https://nuxt.com/assets/design-kit/icon-green.png) +``` + + :::preview-card{icon="i-lucide-eye" label="Preview"} + ![A Cool Image](https://nuxt.com/assets/design-kit/icon-green.png) + ::: +:: + +## `ProseUl` + +::code-group +```md [Code] +- Just +- An +- Unordered +- List +``` + + :::preview-card{icon="i-lucide-eye" label="Preview"} + - Just + - An + - Unordered + - List + ::: +:: + +## `ProseLi` + +::code-group +```md [Code] +- List element +``` + + :::preview-card{icon="i-lucide-eye" label="Preview"} + - List element + ::: +:: + +## `ProseOl` + +::code-group +```md [Code] +1. Foo +2. Bar +3. Baz +``` + + :::preview-card{icon="i-lucide-eye" label="Preview"} + 1. Foo + 2. Bar + 3. Baz + ::: +:: + +## `ProseP` + +::code-group +```md [Code] +Just a paragraph. +``` + + :::preview-card{icon="i-lucide-eye" label="Preview"} + Just a paragraph. + ::: +:: + +## `ProseStrong` + +::code-group +```md [Code] +**Just a strong paragraph.** +``` + + :::preview-card{icon="i-lucide-eye" label="Preview"} + **Just a strong paragraph.** + ::: +:: + +## `ProseEm` + +::code-group +```md [Code] +_Just an italic paragraph._ +``` + + :::preview-card{icon="i-lucide-eye" label="Preview"} + _Just an italic paragraph._ + ::: +:: + +## `ProseTable` + +::code-group +```md [Code] +| Key | Type | Description | +| --- | --------- | ----------- | +| 1 | Wonderful | Table | +| 2 | Wonderful | Data | +| 3 | Wonderful | Website | +``` + + :::preview-card{icon="i-lucide-eye" label="Preview"} + + | Key | Type | Description | + | --- | --------- | ----------- | + | 1 | Wonderful | Table | + | 2 | Wonderful | Data | + | 3 | Wonderful | Website | + + ::: +:: + +## `ProseTbody` + +Included in **ProseTable** example. + +## `ProseTd` + +Included in **ProseTable** example. + +## `ProseTh` + +Included in **ProseTable** example. + +## `ProseThead` + +Included in **ProseTable** example. + +## `ProseTr` + +Included in **ProseTable** example. + +::callout +--- +icon: i-simple-icons-github +to: https://github.com/nuxt-modules/mdc/tree/main/src/runtime/components/prose +--- +Checkout the source code for these components on GitHub. +:: diff --git a/docus/content/docs/6.deploy/.navigation.yml b/docus/content/docs/6.deploy/.navigation.yml new file mode 100644 index 000000000..00d9f5649 --- /dev/null +++ b/docus/content/docs/6.deploy/.navigation.yml @@ -0,0 +1 @@ +icon: i-lucide-cloud-upload diff --git a/docus/content/docs/6.deploy/1.server.md b/docus/content/docs/6.deploy/1.server.md new file mode 100644 index 000000000..bd861658c --- /dev/null +++ b/docus/content/docs/6.deploy/1.server.md @@ -0,0 +1,39 @@ +--- +title: Server Hosting +description: Node preset is the default preset for Nuxt and Nuxt Content. It is + used to build and run Nuxt applications on Node.js. +navigation: + title: Server +--- + +## What is Node.js preset? + +Node preset is the default preset for Nuxt, when building your project, Nuxt will output a Node.js server that you can run with `node .output/server/index.mjs`. + +## Environment requirement + +If you are using the default `better-sqlite3` module to operate the sqlite database, +then you have to deploy to an OS with Glibc version higher than 2.29, eg. Debian 11, Ubuntu 20.04. + +::note +You can use `ldd --version` to check the Glibc version. Checkout [issue #3248](https://github.com/nuxt/content/issues/3248) for more details. +:: + +## Building with Node.js preset + +Build project with Nuxt build command: + +```bash [Terminal] +nuxi build +``` + +When running `nuxi build` with the Node server preset, the result will be an entry point that launches a ready-to-run Node server. + +```bash [Terminal] +$ node .output/server/index.mjs +Listening on http://localhost:3000 +``` + +::note +The SQLite database will be loaded on the server side when booting the server as well as in the browser for client-side navigation or actions. +:: diff --git a/docus/content/docs/6.deploy/2.serverless.md b/docus/content/docs/6.deploy/2.serverless.md new file mode 100644 index 000000000..fdefd7a77 --- /dev/null +++ b/docus/content/docs/6.deploy/2.serverless.md @@ -0,0 +1,142 @@ +--- +title: Serverless Hosting +description: How to deploy Nuxt Content on various serverless platforms. +navigation: + title: Serverless +--- + +## What is Serverless Hosting? + +Serverless hosting lets you run code and applications without managing servers directly - you just upload your code and the cloud provider automatically handles all the infrastructure, scaling, and maintenance while charging you only for the actual compute resources you use. + +**In serverless environments, each user request triggers a fresh instance of your Nuxt server, meaning it starts from scratch every time.** This "stateless" nature means you can't store data in server memory or use file-based databases like SQLite. That's why we need to use external database services (like D1, Turso, or PostgreSQL) that persist data independently of your server instances. + +## Deploy with Serverless + +The module have built-in support for couple of famous serverless platforms. You can deploy your project to them with ease. Checkout the guides for each platform: + +- [NuxtHub](/docs/deploy/nuxthub) +- [Cloudflare Pages](/docs/deploy/cloudflare-pages) +- [Vercel](/docs/deploy/vercel) + +If you like to deploy to other platforms, you can follow below steps to deploy your project. + +### 1. Select a database service + +Before deploying your project, you need to select a database service: + +::code-group +```ts [PostgreSQL] +// 1. Create a PostgreSQL database +// 2. And add the `POSTGRES_URL` to the env variables +export default defineNuxtConfig({ + content: { + database: { + type: 'postgres', + url: process.env.POSTGRES_URL + } + } +}) +``` + +```ts [Cloudflare D1] +// 1. Create a D1 database in your CF account +// 2. Link it to your project with the same binding name +export default defineNuxtConfig({ + content: { + database: { + type: 'd1', + bindingName: '' + } + } +}) +``` + +```ts [LibSQL] +// 1. Create a LibSQL database on Turso.tech +// 2. And add the `TURSO_DATABASE_URL` and `TURSO_AUTH_TOKEN` env variables +export default defineNuxtConfig({ + modules: ['@nuxt/content'], + content: { + database: { + type: 'libsql', + url: process.env.TURSO_DATABASE_URL, + authToken: process.env.TURSO_AUTH_TOKEN, + } + } +}) +``` + +```ts [NuxtHub] +// Install the NuxtHub module (see hub.nuxt.com) +export default defineNuxtConfig({ + modules: ['@nuxt/content', '@nuxthub/core'], + content: { + database: { + type: 'd1', + binding: 'DB' + } + }, + hub: { + database: true + } +}) +``` +:: + +### 2. Deploy your project + +Nuxt Content uses Nuxt deployment presets to adjust the build process for different hosting platforms. + +Various serverless platform are supported with zero configuration: + +- [Cloudflare](https://nuxt.com/deploy/cloudflare) +- [NuxtHub](https://nuxt.com/deploy/nuxthub) +- [Vercel](https://nuxt.com/deploy/vercel) +- [Netlify](https://nuxt.com/deploy/netlify) + +All you need to do is to set the build command to: + +```bash [Terminal] +nuxi build +``` + +The generated output will be compatible with the selected platform. + +::note +The linked database will be loaded on the server side when booting the server. In the browser, a [WASM SQLite](/docs/advanced/database#wasm-sqlite-in-browser) database will be loaded for client-side navigation and actions. +:: + +::tip +If you wish to deploy to AWS Lambda, you need to make sure your sqlite file is in `/tmp` since this is the only writeable folder + +```ts +export default defineNuxtConfig({ + modules: ['@nuxt/content'], + content: { + database: { + type: 'sqlite', + filename: '/tmp/contents.sqlite' + } + } +}) +``` +:: + +### 3. Optimize with pre-rendering + +As each request trigger a fresh instance of your Nuxt server, the performance of your serverless application will be impacted if you don't pre-render some pages. + +To optimize your serverless application, you can pre-render your pages using the `routeRules` option: + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + routeRules: { + '/': { prerender: true } + } +}) +``` + +::tip{to="https://hub.nuxt.com/docs/recipes/pre-rendering"} +We recommend to checkout **NuxtHub's Pre-rendering guide** to learn more about the different strategies to optimize your serverless application, it applies to all serverless platforms. +:: diff --git a/docus/content/docs/6.deploy/3.nuxthub.md b/docus/content/docs/6.deploy/3.nuxthub.md new file mode 100644 index 000000000..892ec3442 --- /dev/null +++ b/docus/content/docs/6.deploy/3.nuxthub.md @@ -0,0 +1,41 @@ +--- +title: NuxtHub +description: Deploy your Content app to NuxtHub +--- + +::card +Quick Setup + +1. Install the `@nuxthub/core` module `nuxi module add hub` +2. Use `npx nuxthub deploy` to deploy your content to NuxtHub +:: + +:hr + +Nuxt Content module has a built-in integration with [NuxtHub](https://hub.nuxt.com) to deploy your content. + +To enable NuxtHub integration, you need to install the `@nuxthub/core` module and register it in your `nuxt.config.ts`. More efficiently, you can use `nuxi module` command to do both at once. + +```bash +npx nuxi module add hub +``` + +That's it :tada: + +Now you can use the `npx nuxthub deploy` command to deploy your content to NuxtHub. + +```bash +npx nuxthub deploy +``` + + +::note +Nuxt Content module automatically enables NuxtHub database. And update database configuration to use Cloudflare D1 with `DB` binding name. (This is default configuration for NuxtHub database.) + +:br + +You can override the database configuration by providing your own database configuration in `nuxt.config.ts`. +:: + + +Checkout the [NuxtHub documentation](https://hub.nuxt.com/docs/getting-started/deploy) for more information. diff --git a/docus/content/docs/6.deploy/4.cloudflare-pages.md b/docus/content/docs/6.deploy/4.cloudflare-pages.md new file mode 100644 index 000000000..428758657 --- /dev/null +++ b/docus/content/docs/6.deploy/4.cloudflare-pages.md @@ -0,0 +1,41 @@ +--- +title: Cloudflare Pages +description: Deploy your Content app to Cloudflare Pages +--- + +::card +Quick Setup + +1. Use `nuxi build --preset=cloudflare_pages` to build your app +2. Create D1 database and connect to your project in Cloudflare Dashboard under `DB` binding name +3. Deploy/Redeploy your app +:: + +:hr + +Nuxt Content module has a built-in integration with [Cloudflare Pages](https://pages.cloudflare.com) to deploy your content. + +Module will automatically detects the build target and prepare the necessary configuration for Cloudflare Pages. Content module currently only support [`cloudflare-pages` presets](https://nuxt.com/deploy/cloudflare). + +You can either use `--preset=cloudflare_pages` option on `nuxi build` command or use `nuxt.config.ts` to configure the preset. + +```ts +export default defineNuxtConfig({ + nitro: { + preset: 'cloudflare_pages', + }, +}); +``` + +The module requires a D1 database to be connected to the app in order to work. By default it will use the `DB` binding name. You can override the database configuration by providing your own database configuration in `nuxt.config.ts`. + +After creating a new Cloudflare Pages project, you need to create a new D1 database and connect it to the project. Make sure to use the same binding name as the module is using. (default is `DB`) + + +That's it :tada: + +Checkout: + +- [Nuxt Deploy documentation](https://nuxt.com/deploy/cloudflare) +- [Cloudflare D1 documentation](https://developers.cloudflare.com/d1/) +- [Cloudflare Pages documentation](https://developers.cloudflare.com/pages/) diff --git a/docus/content/docs/6.deploy/5.vercel.md b/docus/content/docs/6.deploy/5.vercel.md new file mode 100644 index 000000000..45623b76b --- /dev/null +++ b/docus/content/docs/6.deploy/5.vercel.md @@ -0,0 +1,29 @@ +--- +title: Vercel +description: Deploy your Content app to Vercel +--- + +::card +Quick Setup + +- Execute `npx vercel deploy` command or go to Vercel dashboard and create a new project using git repository. +:: + +:hr + +Nuxt Content projects can be deployed to Vercel with zero configuration. The module will automatically detect a Vercel environment and will prepare the necessary configuration for deployment. + +All you need to do is to execute `npx vercel deploy` command or go to Vercel dashboard and create a new project using git repository. + +That's it :tada: + +::note +By default module will use SQlite database in Vercel located at `/tmp` directory. You can override the database configuration by providing your own database configuration. +:br +There are a couple of database providers that are supported by Vercel. You can use any of them by providing the correct connection string in `nuxt.config.ts`. +:: + +Checkout: + +- [Nuxt Deploy documentation](https://nuxt.com/deploy/vercel) +- [Vercel documentation](https://vercel.com/docs/deployments/deployment-methods) diff --git a/docus/content/docs/6.deploy/6.netlify.md b/docus/content/docs/6.deploy/6.netlify.md new file mode 100644 index 000000000..96f0b31ad --- /dev/null +++ b/docus/content/docs/6.deploy/6.netlify.md @@ -0,0 +1,29 @@ +--- +title: Netlify +description: Deploy your Content app to Netlify +--- + +::card +Quick Setup + +- Go go Netlify dashboard and create a new project using git repository. +- Go to `Site Configuration` under `Dependency management` and change Node Version to `20.x` or higher. +- Go to `deploys` and retry last deployment. +:: + +:hr + +Nuxt Content projects can be deployed to Netlify with zero configuration. The module will automatically detects Netlify environment and prepare the necessary configuration for Netlify. + +All you need to do is to go to Netlify dashboard and create a new project using git repository. + +::note +By default Netlify uses Node.js 18.x which is not supported by the module. You need to change the Node.js version in `Site Configuration` under `Dependency management`. +:: + +That's it :tada: + +Checkout: + +- [Nuxt Deploy documentation](https://nuxt.com/deploy/netlify) +- [Netlify documentation](https://www.netlify.com/blog/2016/09/29/a-step-by-step-guide-deploying-on-netlify/) diff --git a/docus/content/docs/6.deploy/7.aws-amplify.md b/docus/content/docs/6.deploy/7.aws-amplify.md new file mode 100644 index 000000000..5de3f5fe7 --- /dev/null +++ b/docus/content/docs/6.deploy/7.aws-amplify.md @@ -0,0 +1,28 @@ +--- +title: AWS Amplify +description: Deploy your Content app to AWS Amplify +--- + +::card +Quick Setup + +- Install `sqlite3` package in your project. +- Go to AWS Amplify dashboard and create a new project using git repository and deploy the app. +:: + +:hr + +Nuxt Content projects can be deployed to AWS Amplify with zero configuration. +The module will automatically detect an AWS Amplify environment and will prepare the necessary configuration for deployment. + +All you need to do is to install `sqlite3` package in your project and go to AWS Amplify dashboard and create a new project using git repository. + +That's it :tada: + +::note +By default module will use SQlite database located at `/tmp` directory. You can override the database configuration by providing your own database configuration. +:: + +Checkout: + +- [Nuxt Deploy documentation](https://nuxt.com/deploy/aws-amplify) diff --git a/docus/content/docs/6.deploy/8.docker.md b/docus/content/docs/6.deploy/8.docker.md new file mode 100644 index 000000000..d2b9518a1 --- /dev/null +++ b/docus/content/docs/6.deploy/8.docker.md @@ -0,0 +1,79 @@ +--- +title: Docker +description: Deploy your Content app with Docker +--- + +Docker is a popular containerization platform that allows you to package your application with all its dependencies into a single container. This makes it easy to deploy your Content app on any platform that supports Docker. + +## With Node.js image + +Using Docker's Node.js image, you can deploy your Content app. All you need is to create a Dockerfile and build the image. Here is an example Dockerfile: + +```docker [Dockerfile] +# Build Stage 1 + +FROM node:22-alpine AS build +WORKDIR /app + +RUN corepack enable + +# Copy package.json and your lockfile, here we add pnpm-lock.yaml for illustration +COPY package.json pnpm-lock.yaml .npmrc ./ + +# Install dependencies +RUN pnpm i + +# Copy the entire project +COPY . ./ + +# Build the project +RUN pnpm run build + +# Build Stage 2 + +FROM node:22-alpine +WORKDIR /app + +# Only `.output` folder is needed from the build stage +COPY --from=build /app/.output/ ./ + +# Change the port and host +ENV PORT=80 +ENV HOST=0.0.0.0 + +EXPOSE 80 + +CMD ["node", "/app/server/index.mjs"] +``` + +## With Bun image + +If you like to use Bun, you can use the official Bun image. Here is an example Dockerfile: + +```docker [Dockerfile] +# use the official Bun image +# see all versions at https://hub.docker.com/r/oven/bun/tags +FROM oven/bun:1 AS build +WORKDIR /app + +COPY package.json bun.lockb ./ + +# use ignore-scripts to avoid builting node modules like better-sqlite3 +RUN bun install --frozen-lockfile --ignore-scripts + +# Copy the entire project +COPY . . + +RUN bun --bun run build + +# copy production dependencies and source code into final image +FROM oven/bun:1 AS production +WORKDIR /app + +# Only `.output` folder is needed from the build stage +COPY --from=build /app/.output /app + +# run the app +EXPOSE 3000/tcp +ENTRYPOINT [ "bun", "--bun", "run", "/app/server/index.mjs" ] +``` diff --git a/docus/content/docs/6.deploy/9.static.md b/docus/content/docs/6.deploy/9.static.md new file mode 100644 index 000000000..86113e4d3 --- /dev/null +++ b/docus/content/docs/6.deploy/9.static.md @@ -0,0 +1,34 @@ +--- +title: Static Hosting +description: How to deploy Nuxt Content to static hosting with static site generation. +navigation: + title: Static +--- + +## What is Static Hosting? + +Static hosting is a type of hosting where your website is built and served as static files (HTML, CSS, JS) that can be served by any static file server. + +Nuxt Content can be deployed to static hosting using Nuxt prerendering. + +## Building with SSG + +To build your app with static site generation, run the following command: + +```bash +npx nuxi generate +``` + +::tip{icon="i-lucide-check"} +This command will create a `dist/` directory with your static site. You can upload it to any static hosting service. +:: + +Nuxt will automatically pre-render all pages using an internal crawler, you can customize it's behavior with the `nitro.prerender` options. + +::note{to="https://nuxt.com/docs/getting-started/prerendering"} +Learn more about pre-rendering in Nuxt. +:: + +## What about the Database? + +Nuxt Content will load the database in the browser using [WASM SQLite](/docs/advanced/database#wasm-sqlite-in-browser), this way, the content queries happening on client-side navigation or actions will run in the browser. diff --git a/docus/content/docs/7.advanced/.navigation.yml b/docus/content/docs/7.advanced/.navigation.yml new file mode 100644 index 000000000..66182732a --- /dev/null +++ b/docus/content/docs/7.advanced/.navigation.yml @@ -0,0 +1,2 @@ +icon: i-lucide-code-xml +title: Advanced diff --git a/docus/content/docs/7.advanced/1.fulltext-search.md b/docus/content/docs/7.advanced/1.fulltext-search.md new file mode 100644 index 000000000..3035891e4 --- /dev/null +++ b/docus/content/docs/7.advanced/1.fulltext-search.md @@ -0,0 +1,128 @@ +--- +title: Full-Text Search +description: Implement full-text search in your website using Nuxt Content +--- + +Content module exposes a handy utility [`queryCollectionSearchSections`](/docs/utils/query-collection-search-sections) to break down content files into searchable sections. This is useful for implementing full-text search in your website. You can use the result of this utility in combination with [Nuxt UI Content Search](https://ui.nuxt.com/pro/components/content-search) or other search libraries like [Fuse.js](https://fusejs.io/), [minisearch](https://lucaong.github.io/minisearch), etc. + +## Nuxt UI Pro + +Nuxt UI Pro provides a ready to use component for full-text search. You can use it by passing the result of `queryCollectionSearchSections` to the `files` prop of the component. + +Read more about [Nuxt UI Content Search](https://ui.nuxt.com/pro/components/content-search). + +::code-group +```vue [UContentSearchExample.vue] + + + +``` + + :::preview-card{label="Live Preview"} + ::::example-fulltext-content-search + :::: + ::: +:: + +## MiniSearch example + +Read more about [minisearch](https://lucaong.github.io/minisearch). + +::code-group +```vue [MiniSearchExample.vue] + + + +``` + + :::preview-card{label="Live Preview"} + ::::example-fulltext-mini-search + :::: + ::: +:: + +## Fuse.js example + +Read more about [Fuse.js](https://fusejs.io). + +::code-group +```vue [FusejsExample.vue] + + + +``` + + :::preview-card{label="Live Preview"} + ::::example-fulltext-fusejs + :::: + ::: +:: diff --git a/docus/content/docs/7.advanced/2.raw-content.md b/docus/content/docs/7.advanced/2.raw-content.md new file mode 100644 index 000000000..74ba98f0b --- /dev/null +++ b/docus/content/docs/7.advanced/2.raw-content.md @@ -0,0 +1,54 @@ +--- +title: Raw Content +description: Access to contents raw data in appliction +--- + +There were lots of requests in Content version 2 about accessing contents raw data in production. In Content version 3 it is possible to ship contents raw data to production. + +In order to ship raw contents to production you need to define `rawbody` field in your collection's schema. That's it. + +Nuxt Content will detect this magical field in your schema and fill it with the raw content. + +```ts [content.config.ts] + +import { defineCollection, defineContentConfig, z } from '@nuxt/content' + +export default defineContentConfig({ + collections: { + docs: defineCollection({ + source: '**', + type: 'page', + schema: z.object({ + rawbody: z.string() + }) + }) + } +}) +``` + +And you can use `queryCollection()` to fetch the raw content. + +```vue [pages/index.vue] + + + +``` + +In case you don't want to ship raw content of a specific file you can add `rawbody: ''` to frontmatter of that file. The auto filled value of `rawbody` is acting like default value and when you define `rawbody` in the frontmatter it will overwritten. + +```md [content.md] +--- +title: My page +rawbody: '' +--- + +``` + +::callout +It is important to fill frontmatter fields with a same type of data that is defined in collection schema. In this case `rawbody` is a string, and you should consider passing empty string. Do not use `boolean` or other type of values. +:: diff --git a/docus/content/docs/7.advanced/3.database.md b/docus/content/docs/7.advanced/3.database.md new file mode 100644 index 000000000..112c1fb96 --- /dev/null +++ b/docus/content/docs/7.advanced/3.database.md @@ -0,0 +1,31 @@ +--- +title: Database +description: How Nuxt Content stores and retrieves content +navigation: + title: SQL Storage +--- + +In Content v3, we have introduced a robust storage layer based on SQLite, which offers a powerful and efficient method for managing content. This marks a significant enhancement over the previous file-based storage system, which was constrained by performance and scalability limitations. + +> In Content v2, the system read and parsed content during the Nitro runtime, creating a cache file for each content file to store the parsed data. This method introduced considerable overhead to the website's runtime. +> +> - The I/O time in production was substantial, as the module had to load all cache files to search through the content. +> - Additionally, the lack of optimization and compression for the content resulted in a large bundle size, particularly problematic in edge environments. + +Content management in Content v3 involves three key steps, which are designed to streamline the process and enhance performance. + +## Generating Database Dump + +For each collection in your project, the module reads the content from the defined source and parses it into an Abstract Syntax Tree (AST). It creates a specific table for each collection based on its schema. The parsed content is then inserted into the corresponding table, ensuring that the data structure aligns with the defined schema for optimal querying. Everything is then saved in a dump file. + +## Restoring Dump on Cold Start + +During runtime, when the application executes the first query to retrieve content, the module reads the generated dump from the previous step and restores it into the target database. This process is fast and optimized for each deployment mode and platform. + +The module employs a special integrity check mechanism to ensure that the database is updated with the latest content. This same integrity check also prevents duplicate imports, maintaining the integrity and accuracy of the data stored. + +## WASM SQLite in Browser + +For client-side navigation, the module utilizes a similar approach. When the application executes the first query for content, it downloads the generated dump from the server and initializes a local SQLite database within the browser. From that point onward, all queries are executed locally without needing to call the server, significantly improving the responsiveness of the application and providing a seamless user experience. + +This architecture not only enhances performance but also allows for offline capabilities, enabling users to access content even without an active internet connection. The combination of server-side and client-side processing ensures that Nuxt Content v3 is both powerful and flexible, catering to a wide range of use cases and environments. diff --git a/docus/content/docs/7.advanced/4.tools.md b/docus/content/docs/7.advanced/4.tools.md new file mode 100644 index 000000000..a184b2f22 --- /dev/null +++ b/docus/content/docs/7.advanced/4.tools.md @@ -0,0 +1,89 @@ +--- +title: Tools +description: Debugging tools +navigation: + title: Debugging tools +--- + +Nuxt Content uses an **SQLite database (`contents.sqlite`)** to store and query content efficiently. If you're running into **missing content, slow queries, or database issues**, debugging your SQLite database can help! + +::callout +--- +icon: i-simple-icons-visualstudiocode +to: https://marketplace.visualstudio.com/items?itemName=alexcvzz.vscode-sqlite +--- +A simple way to inspect it? **Use the SQLite VS Code extension!** +:: + +## Install SQLite VS Code Extension + +1. Open **Visual Studio Code**. +2. Go to the **Extensions** panel (`Ctrl+Shift+X` / `Cmd+Shift+X` on Mac). +3. Search for **"SQLite"** (by `alexcvzz`) and install it. +4. Open your Nuxt Content database (`.data/content/contents.sqlite`). + +::tip{icon="i-lucide-lightbulb"} +If you don't see `contents.sqlite`, start your Nuxt app first: + +```bash [Terminal] +npx nuxi dev +``` +:: + + +## Locate Your SQLite Database +Nuxt Content stores its database here: + +```bash +.data/content/contents.sqlite +``` + +::note{to="https://nuxt.com/docs/getting-started/prerendering"} +This file is automatically generated when you start your Nuxt app. No need to create it manually! +:: + +## Open & Explore the Database + +1. **Right-click** on `contents.sqlite` in VS Code. +2. Select **"Open Database"**. +3. Expand the **Database Explorer** panel to view tables & data. + +![SQLite Explorer in VS Code](https://github.com/user-attachments/assets/c9f22c4c-7a95-43e8-ab03-aa76f2e49c8e) + + +## Fixing Common Issues + +### Content Not Showing? + +1. **Check if the database exists** (`.data/content/contents.sqlite`). +2. **Run a cleanup & restart Nuxt**: + ```bash [Terminal] + npx nuxi cleanup && npx nuxi dev + ``` +3. **Check if content is inside the database** (run an SQL query). + +### Manually Reset the Database +If things seem **really broken**, try resetting it: + +1. **Delete the database file**: + ```bash [Terminal] + rm -rf .data/content/contents.sqlite + ``` +2. **Run cleanup** to remove old caches: + ```bash [Terminal] + npx nuxi cleanup + ``` +3. **Restart Nuxt** to generate a fresh database: + ```bash [Terminal] + npx nuxi dev + ``` + +::note{icon="i-lucide-triangle-alert"} +Cleaning up will remove cached data. Don't worry—it regenerates automatically! +:: + +## More Debugging Tools +If VS Code isn’t enough, check out: + +- 🖥️ [**DB Browser for SQLite**](https://sqlitebrowser.org/) – A visual tool for inspecting & modifying the database. +- 🛠️ **SQLite Command Line** – Use `sqlite3 contents.sqlite` to run SQL queries from your terminal. diff --git a/docus/content/docs/7.advanced/5.hooks.md b/docus/content/docs/7.advanced/5.hooks.md new file mode 100644 index 000000000..5c8d48769 --- /dev/null +++ b/docus/content/docs/7.advanced/5.hooks.md @@ -0,0 +1,81 @@ +--- +title: Hooks +description: Modify your content using Nuxt build time hooks +navigation: + title: Hooks +--- + +## `content:file:beforeParse`{lang="ts"} + +This hook is called before the content is parsed. + +It can be used to modify the raw content from a `file` before it is transformed +or modify the transform options. + +```ts +export default defineNuxtConfig({ + hooks: { + 'content:file:beforeParse'(ctx) { + // ... + } + } +}) +``` + +## `content:file:afterParse`{lang="ts"} + +This hook is called after the content is parsed and before it is saved to the database. + +```ts +export default defineNuxtConfig({ + hooks: { + 'content:file:afterParse'(ctx) { + // ... + } + } +}) +``` + +## Example Usage + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + // ... + hooks: { + 'content:file:beforeParse'(ctx) { + const { file } = ctx; + + if (file.id.endsWith(".md")) { + file.body = file.body.replace(/react/gi, "Vue"); + } + }, + 'content:file:afterParse'(ctx) { + const { file, content } = ctx; + + const wordsPerMinute = 180; + const text = typeof file.body === 'string' ? file.body : ''; + const wordCount = text.split(/\s+/).length; + + content.readingTime = Math.ceil(wordCount / wordsPerMinute); + } + } +}) +``` + +::note{icon="i-lucide-info"} +In the `content:file:afterParse` hook, we added a custom property to our content object. To be able to access that property within our pages using [`queryCollection()`](/docs/utils/query-collection), we first need to define it in our content schema. + +```ts [content.config.ts] +export default defineContentConfig({ + collections: { + content: defineCollection({ + type: 'page', + source: '**/*.md', + schema: z.object({ + readingTime: z.number().optional() + }) + }) + } +}); +``` +:: diff --git a/docus/content/docs/7.advanced/6.custom-source.md b/docus/content/docs/7.advanced/6.custom-source.md new file mode 100644 index 000000000..53120431a --- /dev/null +++ b/docus/content/docs/7.advanced/6.custom-source.md @@ -0,0 +1,63 @@ +--- +title: Custom Source +description: Define a custom source to retrive data. +--- + +By default, Nuxt Content provides some built-in sources like local files source and remote Github source. However this is not enough for some cases, for example, you want to fetch data from a remote API. In this case, you can define a custom source to fetch data and use it in your collections. + +Using `defineCollectionSource`, you can define a custom source. + +```ts +import { defineCollectionSource } from '@nuxt/content' + +const hackernewsSource = defineCollectionSource({ + getKeys: () => { + return fetch('https://hacker-news.firebaseio.com/v0/topstories.json') + .then(res => res.json()) + .then(data => data.map((key: string) => `${key}.json`)) + }, + getItem: (key: string) => { + const id = key.split('.')[0] + return fetch(`https://hacker-news.firebaseio.com/v0/item/${id}.json`) + .then(res => res.json()) + }, +}) +``` + +Then you can use this source in your collections. + +```ts [content.config.ts] +import { defineContentConfig, defineCollectionSource, defineCollection, z } from '@nuxt/content' + +const hackernewsSource = defineCollectionSource({ + getKeys: () => { + return fetch('https://hacker-news.firebaseio.com/v0/topstories.json') + .then(res => res.json()) + .then(data => data.map((key: string) => `${key}.json`)) + }, + getItem: (key: string) => { + const id = key.split('.')[0] + return fetch(`https://hacker-news.firebaseio.com/v0/item/${id}.json`) + .then(res => res.json()) + }, +}) + +const hackernews = defineCollection({ + type: 'data', + source: hackernewsSource, + schema: z.object({ + title: z.string(), + date: z.date(), + type: z.string(), + score: z.number(), + url: z.string(), + by: z.string(), + }), +}) + +export default defineContentConfig({ + collections: { + hackernews, + }, +}) +``` diff --git a/docus/content/docs/7.advanced/7.llms.md b/docus/content/docs/7.advanced/7.llms.md new file mode 100644 index 000000000..97656b35b --- /dev/null +++ b/docus/content/docs/7.advanced/7.llms.md @@ -0,0 +1,96 @@ +--- +title: LLMs Integration +description: Learn how to generate AI-ready content files using Nuxt Content and + the Nuxt LLMs module. +--- + +The Nuxt Content module integrates [`nuxt-llms`](https://github.com/nuxtlabs/nuxt-llms) to prepare your content for Large Language Models (LLMs). When `nuxt-llms` is detected, Content module automatically extends the LLMs module and inject collections of type [page](https://content.nuxt.com/docs/collections/types#page-type) to the LLMs module.🚀 + +## Setup + +::prose-steps +### Install the required module + +```bash [terminal] +npm install nuxt-llms +``` + +### Configure your `nuxt.config.ts` + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + modules: ['@nuxt/content', 'nuxt-llms'], + llms: { + domain: 'https://your-site.com', + title: 'Your Site Name', + description: 'A brief description of your site', + }, +}) +``` +:: + +That's it 🚀 `/llms.txt` file is automatically generated and pre-rendered. + +## Sections + +When generating content, you can create custom sections to process your content into LLM-friendly formats. + +You can create custom sections to the `llms.sections` array and define the `contentCollection` and `contentFilters` option for each section. + +::prose-warning +If there is no section defined in the `contentCollection` option, the module will only add [page](https://content.nuxt.com/docs/collections/types#page-type) collections to the LLMs module. +:: + +### `contentCollection` + +This option specifies which content collection to use as source. + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + llms: { + sections: [ + { + title: 'Documentation', + description: 'Technical documentation and guides', + contentCollection: 'docs', + }, + ], + }, +}) +``` + +### `contentFilters` + +This options defines filters to select specific content within the collection. + +You precisely control which content is included. Each filter consists of: + +- `field`: The content property to check +- `operator`: Comparison operator (`=`, `<>`, `>`, `<`, `LIKE`, `IN`, `NOT IN`, `IS NULL`, `IS NOT NULL`, etc.) +- `value`: The value to compare against + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + llms: { + sections: [ + { + title: 'Documentation', + description: 'Technical documentation and guides', + contentCollection: 'docs', + contentFilters: [ + // Only include markdown files + { field: 'extension', operator: '=', value: 'md' }, + // Only include published content + { field: 'draft', operator: '<>', value: true }, + // Filter by directory + { field: 'path', operator: 'LIKE', value: '/guide%' }, + ] + }, + ], + }, +}) +``` + +::tip{to="https://github.com/nuxtlabs/nuxt-llms"} +Checkout the nuxt-llms documentation for more information about the module. +:: diff --git a/docus/content/docs/7.advanced/8.transformers.md b/docus/content/docs/7.advanced/8.transformers.md new file mode 100644 index 000000000..51ba0deec --- /dev/null +++ b/docus/content/docs/7.advanced/8.transformers.md @@ -0,0 +1,139 @@ +--- +title: Transformers +description: Transformers in Nuxt Content allow you to programmatically parse, modify, or analyze your content files as they are processed. +--- + +Transformers in Nuxt Content allow you to programmatically parse, modify, or analyze your content files as they are processed. They are especially useful for: + +- Adding or modifying fields (e.g., appending to the title, generating slugs) +- Extracting metadata (e.g., listing used components) +- Enriching content with computed data +- Supporting new content types + +## Defining a Transformer + +You can define a transformer using the `defineTransformer` helper from `@nuxt/content`: + +```ts [~~/transformers/title-suffix.ts] +import { defineTransformer } from '@nuxt/content' + +export default defineTransformer({ + name: 'title-suffix', + extensions: ['.md'], // File extensions to apply this transformer to + transform(file) { + // Modify the file object as needed + return { + ...file, + title: file.title + ' (suffix)', + } + }, +}) +``` + +### Transformer Options + +- `name` (string): A unique name for your transformer. +- `extensions` (string[]): File extensions this transformer should apply to (e.g., `['.md']`). +- `transform` (function): The function that receives the file object and returns the modified file. + +## Registering Transformers + +Transformers are registered in your `nuxt.config.ts`: + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + content: { + build: { + transformers: [ + '~~/transformers/title-suffix', + '~~/transformers/my-custom-transformer', + ], + }, + }, +}) +``` + +## Example: Adding Metadata + +Transformers can add a `__metadata` field to the file. This field is not stored in the database but can be used for runtime logic. + +```ts [~~/transformers/component-metadata.ts] +import { defineTransformer } from '@nuxt/content' + +export default defineTransformer({ + name: 'component-metadata', + extensions: ['.md'], + transform(file) { + // Example: Detect if a custom component is used + const usesMyComponent = file.body?.includes('') + return { + ...file, + __metadata: { + components: usesMyComponent ? ['MyCustomComponent'] : [], + }, + } + }, +}) +``` + +**Note:** The `__metadata` field is only available at runtime and is not persisted in the content database. + + +## API Reference + +```ts +interface Transformer { + name: string + extensions: string[] + transform: (file: ContentFile) => ContentFile +} +``` + +- `ContentFile` is the object representing the parsed content file, including frontmatter, body, and other fields. + + +## Supporting New File Formats with Transformers + +Transformers are not limited to modifying existing content—they can also be used to add support for new file formats in Nuxt Content. By defining a transformer with a custom `parse` method, you can instruct Nuxt Content how to read and process files with new extensions, such as YAML. + +### Example: YAML File Support + +Suppose you want to support `.yml` and `.yaml` files in your content directory. You can create a transformer that parses YAML frontmatter and body, and registers it for those extensions: + +```ts [~~/transformers/yaml.ts] +import { defineTransformer } from '@nuxt/content' + +export default defineTransformer({ + name: 'Yaml', + extensions: ['.yml', '.yaml'], + parse: (file) => { + const { id, body } = file + + // parse the body with your favorite yaml parser + const parsed = parseYaml(body) + + return { + ...parsed, + id, + } + }, +}) +``` + + +Register your YAML transformer in your Nuxt config just like any other transformer: + +```ts +export default defineNuxtConfig({ + content: { + build: { + transformers: [ + '~~/transformers/yaml', + // ...other transformers + ], + }, + }, +}) +``` + +This approach allows you to extend Nuxt Content to handle any custom file format you need. diff --git a/docus/content/docs/8.studio/.navigation.yml b/docus/content/docs/8.studio/.navigation.yml new file mode 100644 index 000000000..422ecc147 --- /dev/null +++ b/docus/content/docs/8.studio/.navigation.yml @@ -0,0 +1,2 @@ +icon: i-lucide-monitor +title: Studio diff --git a/docus/content/docs/8.studio/1.setup.md b/docus/content/docs/8.studio/1.setup.md new file mode 100644 index 000000000..f850559d8 --- /dev/null +++ b/docus/content/docs/8.studio/1.setup.md @@ -0,0 +1,117 @@ +--- +title: Setup Nuxt Studio +seo: + title: Introduction and setup of Nuxt Studio - the visual CMS platform of Nuxt + Content + description: Studio is a the Git-based CMS platform for Nuxt Content websites, + providing an intuitive interface to edit your Markdown, YAML and JSON files + and publish your changes to GitHub. +navigation: + title: Setup +description: Studio is the intuitive CMS interface to edit Nuxt Content + websites. Edit your Markdown, YAML and JSON files and publish your changes to + GitHub. +--- + +Studio is an intuitive CMS interface to edit your Nuxt Content websites. + +It takes advantage of the `Preview API` included in Nuxt Content to propose the best editing experience for your content files. Editors can benefit from a user-friendly interface to edit their `Markdown`, `YAML` or `JSON` files. + +Developers can customize the editing experience and provide tools to the editor who can focus on content without requiring any technical expertise. + +:video{autoplay controls loop poster="/home/videos/HomeNotionLikePoster.webp" src="https://res.cloudinary.com/nuxt/video/upload/v1733494722/contentv3final_rc8bvu.mp4"} + +## Authentication + +The Studio admin is located on [nuxt.studio](https://nuxt.studio). From there you can either login with GitHub or with Google. Both methods give you the same editing rights but since Studio is [synchronized with GitHub](/docs/studio/github), the repository import must be handled by a GitHub user. + +::warning +Google authentication is adapted for non technical users. Google users have to join a team with existing projects to edit them. +:: + +## Connect your Nuxt Content Repository + +Once you are logged in the admin, you have the possibility to import your Nuxt Content repository from the interface. + +::tip +Two options are available, you can either import an existing repository or use one of our [templates](/templates). +:: + +The only requirement is to host your repository on GitHub. From the interface, connect to our [GitHub app](/docs/studio/github), you'll be then able to link a GitHub repository to your Studio project. + +Once the project is created, you can start editing your files with the Studio editors and publish your changes. + +## **Enable the Full Editing Experience** + +To unlock the complete range of features in Studio, make sure your project URL is properly configured in the deployment section. + +**This will enable all Studio features including:** + +- **Live preview of your website:** instantly see changes reflected on your site as you work. +- **Dynamic form generation:** Automatically generate forms to edit your files based on your Nuxt Content [collections](/docs/collections/define). +- **Enhanced markdown editors:** enjoy an improved editing experience, including the ability to list and integrate [Vue components](/docs/studio/content#vue-components) that you’ve exposed. +- **Automatic app configuration:** generate [application configurations](/docs/studio/config) based on the schema you’ve defined. + +::tip +We are proposing a GitHub Pages deployment. By using it, we will handle all the requirements for you. However, if you want to use the deployment platform of your choice, please follow the simple guideline below. +:: + +To be able to set your URL in Studio, you just need to set the `Preview API` in the content configuration of your `nuxt.config.ts` file. + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + content: { + preview: { + api: 'https://api.nuxt.studio' + } + } +}) +``` + +Once deployed, you can ensure the `Preview API` has been activated by navigating to the `/__preview.json` page on your site. This metadata page is automatically generated by the Content module and must be accessible to connect your URL in the self-hosted section of Studio. + +### Troubleshooting + +The `__preview.json` file is generated but you encounter a `Forbidden error: invalid property check` when setting your URL in the self-hosted section, follow these steps: + +::prose-steps{level="4"} + +#### Ensure the `gitInfo` field is set and accurate in the `__preview.json` file + +```json [https://your-website.com/__preview.json] +"gitInfo": { + "name": "content", + "owner": "nuxt", + "url": "https://github.com/nuxt/content" +}, +``` + + :::warning + This information should be populated automatically in most cases. We support most popular providers (NuxtHub, Vercel, Netlify, etc.), but if you're using a non supported provider or a custom one, it may not be fetched correctly. + ::: + + + +#### Override `gitInfo` in your `nuxt.config.ts` + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + content: { + preview: { + api: 'https://api.nuxt.studio', + gitInfo: { + name: 'Your repository name', + owner: 'Your repository owner/organization', + url: 'Your GitHub repository URL' + } + } + } +}) +``` + + + +#### Ensure these fields are correctly set in `__preview.json` + +The `Forbidden error` should be resolved and you should be able to set your URL on Nuxt Studio. If not, please contact us on the [Discord server](https://discord.gg/sBXDm6e8SP). +:: diff --git a/docus/content/docs/8.studio/2.github.md b/docus/content/docs/8.studio/2.github.md new file mode 100644 index 000000000..3b1e766eb --- /dev/null +++ b/docus/content/docs/8.studio/2.github.md @@ -0,0 +1,44 @@ +--- +title: Real time synchronization between Studio and GitHub +description: Nuxt Studio is natively synced with GitHub. Install the Nuxt Studio + GitHub app and enable content publication on GitHub directly from Studio. +navigation: + title: Synchronization +--- + +::tip +This section explains the syncronization behaviour between Studio and GitHub. This process is handled directly from the platform and does not require any external action. This page purpose is purely informational. +:: + +## Overview + +Nuxt Studio integrates with the [GitHub](https://github.com) API, enabling smooth synchronization between Studio and your GitHub repositories. This first-class integration is made possible through the utilization of [GitHub Apps](https://docs.github.com/en/developers/apps/getting-started-with-apps/about-apps#about-github-apps). + +## Installation + +During Studio project creation, you'll need to install the [Nuxt Studio GitHub App](https://github.com/apps/nuxt-studio) in your personal account or in the organizations you manage. Installing a GitHub App requires either organization ownership or admin permissions in a repository. If you lack the necessary permissions, the repository owner will need to approve the request. + +You can access the installation page from anywhere on the app by clicking on [Install with GitHub](https://github.com/apps/nuxt-studio/installations/new). + +### Permissions + +During the installation of our GitHub app, you will be prompted to grant certain permissions: + +- Read access to `actions`, `metadata`, `members`, and `plan` +- Read and write access to `secrets`, `administration`, `contents`, `pages`, `pull requests` and `workflows` + +We need read access to `actions` to send a notification when a workflow run fails on a pull request or on the repository default branch. `metadata` is mandatory to fetch repository data, `members` to import your organization members and repository collaborators and `plan` is used to propose features based on your GitHub account plan. + +Nuxt Studio acts on your behalf to create repositories, branches, pull requests, and perform commits and merges. We also offer one-click deployment to GitHub Pages with instant preview of workflows and environment variables secrets management. To accomplish this, we need read and write access to `administration`, `contents`, `pages`, `pull requests`, `workflows` and `secrets`. + +### Repositories Installations + +When installing our GitHub app, you will be prompted to select all repositories or a subset of them. This selection can be changed at any time by going to the [GitHub app settings](https://github.com/apps/nuxt-studio/installations/new). + +By clicking on `Install`, Nuxt Studio will install each repository you have selected, enabling you to perform all the actions listed above. + +## Uninstall + +All the data imported from GitHub is directly associated with your GitHub app installation. If you uninstall the GitHub app, all the associated data will be deleted. + +If you delete your GitHub repository, the associated Nuxt Studio project will be automatically removed. However, if this project was subscribed to a Team plan, the subscription will not be canceled automatically. You will need to manually end the subscription via [Lemon Squeezy]() or [contact us](team@nuxt.studio) for assistance. diff --git a/docus/content/docs/8.studio/3.content.md b/docus/content/docs/8.studio/3.content.md new file mode 100644 index 000000000..d8eb1d106 --- /dev/null +++ b/docus/content/docs/8.studio/3.content.md @@ -0,0 +1,251 @@ +--- +title: Edit your content +navigation: + title: Content Editors +description: Discover and select your favorite way to manage your content + between the visual or the code editor. +seo: + title: Edit your Nuxt Content website with our editors + description: Overview of our different Studio CMS editors to manage your content + and your medias. Choose between our visual editors and our code editor. +--- + +Nuxt Studio offers a versatile workspace for both developers and content writers, giving them the freedom to choose between our differents editors: + +- [Notion-like editor](#notion-like-editor-markdown-files) for `Markdown` files +- [Form editor](#form-editor-yaml-and-json-files) for `YAML` and `JSON` files +- [Code editor](#code-editor) for any kind of files (for technical users only) + +::tip +You can choose your favorite editor from the settings page of your project. +:: + +Each editor serves its own purpose. Some users are used to code edition, while others prefer a non-technical, visual approach. At the end, **code syntax is the final output** for both editors. + +## Notion-like editor (`Markdown` files) + +![edit your website with a visual interface](/docs/studio/visual-markdown-editor.webp) + +This editor is heavily inspired by Notion, well known for its intuitive design and flexibility. Much like a standard text editor, this editor is designed to be familiar and easy to use. However, it stands out with its additional features that improve the writing experience. + +::tip{to="/blog/visual-editor"} +You want to know how we've built this editor and how it works under the hood? Check this blog post. +:: + +### Frontmatter + +[Frontmatter](/docs/files/markdown#frontmatter) is a convention of Markdown-based CMS to provide meta-data to pages, like description or title or any other data you would like to store as `key: value` pair. + +Based on the user [collection and schema](/docs/collections/define) provided, a form is generated to edit this metadata from the editor. + +:video{autoplay controls loop poster="/home/videos/HomeNotionLikePoster.webp" src="https://res.cloudinary.com/nuxt/video/upload/v1739982761/frontmatterform_yjafgt.mp4"} + +::prose-note{to="#form-editor-yaml-and-json-files"} +Check this section to learn more about form generation based on schema. +:: + +### Toolbar + +Highlight your text to reveal the toolbar, giving you access to all the standard text editing features (title formatting, Bold, Italic, Strike-through, code, link, class, bullet list, numerated list...). + +### Medias + +Users can simply drag and drop images directly into the editor. An upload modal will open to let you choose the destination folder. + +By typing `/` and searching for `Image` or `Video`, they can quickly insert a media. A modal will open to let them choose the media they want to insert from the media gallery (aka the`public` folder of the Nuxt application). + +From the media modal, you can set the [alt attribute](https://www.w3schools.com/tags/att_img_alt.asp) for SEO and accessibility purpose. + +### Vue Components + +One of this editor standout features is its ability to integrate and customize any complex `Vue` component directly within the editor. + +#### Create and integrate your own component + +A developer can create any kind of visually complex components and editors will be able to use them and focus on the content. An editor can also play with the component properties, styles, and behaviour to fit its specific requirements as long as the developer made it customisable. + +::steps{level="4"} +#### Create your component + +You can create Vue components and integrate them into Markdown. They just need to be located in the `/components/content` folder to be available. + +```vue [components/content/HomeFeature.vue] + + + +``` + +#### Integrate these components easily within any Markdown file using [MDC syntax](/docs/files/markdown#mdc-syntax) + +```mdc [content/index.md] +::home-feature + --- + icon: i-mdi-vuejs + --- + #title + Embedded Vue components + #description + Edit slots and props inside the Notion-like editor. +:: +``` + +#### Edit them with our Studio editors + +The visual editor simplifies the component edition, allowing you to integrate and edit them directly in the visual editor. Non technical users can play with **slots** and **props** without any technical knowledge. + +:video{autoplay controls loop src="https://res.cloudinary.com/nuxt/video/upload/v1744126742/studio/finalpropscomps_usfabp.mp4"} + + + +All components in the `/components/content` folder are available in the editor. Studio users can type `/` anywhere while editing to access the list of available components. + +:tip[Take a look at this section to validate your `Vue` component integration in the editor in local development.]{to="/docs/studio/debug"} +:: + +#### Integrate built-in components from external libraries + +By default, you can integrate any component inside a Markdown file and it should work and be editable from Studio but external components **won't be displayed in the components list in Studio and can't be integrated manually by a Studio editor**. + +To list this component inside Studio and fetch all its metadata, you need to set it as global in your Nuxt config file. + +Here is an example to integrate Button and Icon components from the [Nuxt UI](https://ui.nuxt.com) library: + +```ts +export default defineNuxtConfig({ + hooks: { + 'components:extend': (components) => { + const globals = components.filter(c => ['UButton', 'UIcon'].includes(c.pascalName)) + + globals.forEach(c => c.global = true) + } + }, +}) +``` + +## Form editor + +![YAML and JSON edition with auto generated form](/blog/frontmatters.png) + +This editor is used whether you’re editing the [frontmatter]() of a `Markdown` file or a `JSON` / `YAML` file. + +It eliminates the need to interact directly with complex file syntax. Instead, a form is automatically generated based on the provided user [collection schema](/docs/collections/define). + +### **Defining your form with** `zod` Schema + +::prose-note{to="/docs/collections/define"} +Learn more about schema collection definition in the dedicated section. +:: + +Once the `schema` property has been defined in your collection, this will automatically generate the corresponding form on Studio interface. + +::prose-code-group +```ts [content.config.ts] +export default defineContentConfig({ + collections: { + posts: defineCollection({ + type: 'page', + source: 'blog/*.md', + schema: z.object({ + draft: z.boolean().default(false), + category: z.enum(['Alps', 'Himalaya', 'Pyrenees']).optional(), + date: z.date(), + image: z.object({ + src: z.string().editor({ input: 'media' }), + alt: z.string(), + }), + slug: z.string().editor({ hidden: true }), + icon: z.string().optional().editor({ input: 'icon' }), + authors: z.array(z.object({ + slug: z.string(), + username: z.string(), + name: z.string(), + to: z.string(), + avatar: z.object({ + src: z.string(), + alt: z.string(), + }), + })), + }), + }), + }, +}) +``` + + :::preview-card{icon="i-lucide-eye" label="Generated Form"} + ![Form preview](/docs/studio/preview-schema.png) + ::: +:: + +### **Native Inputs Mapping** + +Primitive Zod types are automatically mapped to appropriate form inputs in: + +- **String** → Text input +- **Date** → Date picker +- **Number** → Number input (counter) +- **Boolean** → Toggle switch +- **Enum** → Select dropdown +- **Arrays of strings** → List of badge inputs +- **Arrays of objects** → Accordion of items with embedded form + +:video{autoplay controls loop poster="https://res.cloudinary.com/nuxt/video/upload/v1740679550/arrayobjectandstring_r1jpvz.jpg" src="https://res.cloudinary.com/nuxt/video/upload/v1740679550/arrayobjectandstring_r1jpvz.mp4"} + +### Custom Inputs Mapping + +Studio goes beyond primitive types. You can customise form fields using the `editor` method, which extends Zod types with metadata to empower editor interface. + +This allows you to define custom inputs or hide fields. + +#### Usage + +```ts [content.config.ts] +mainScreen: z.string().editor({ input: 'media' }) +``` + +#### Options + +##### `input: 'media' | 'icon'` + +You can set the editor input type. Currently `icon` and `media` are available. + +##### `hidden: Boolean` + +This option can be set to avoid the display of a field in the Studio editor. + +::prose-tip +Studio inputs are fully extensible. We can create as many input as we want based on our users needs. +:: + +## Code editor + +![Edit directly your raw content with our code editor](/docs/studio/code-editor.webp) + +Even though the two previous editors are dedicated to a specific file extension (`md` or `yaml`/`json`). The code editor can be used with any kind of file. + +It provides full control over your content, allowing you to write raw content directly: + +- [MDC](/docs/files/markdown) syntax for `Markdown` files +- [JSON](/docs/files/json) or [YAML](/docs/files/yaml) syntax + +When your file is saved with the code editor, the content is stored exactly as you've written it, preserving all specific syntax and formatting. This editor is ideal for users comfortable with code syntax (`Markdown`, `YAML` or `JSON`) who want precise control over structure of their content. diff --git a/docus/content/docs/8.studio/4.medias.md b/docus/content/docs/8.studio/4.medias.md new file mode 100644 index 000000000..476bc3691 --- /dev/null +++ b/docus/content/docs/8.studio/4.medias.md @@ -0,0 +1,27 @@ +--- +title: Manage and integrate Medias in Nuxt Content Studio CMS +navigation: + title: Media Library +description: Explore how to browse and manage media files, and integrate them + into your projects using Nuxt Content Studio CMS features. +--- + +## Browse your medias + +All medias located in the `/public` directory are available in the **Media** tab of the Studio platform. + +![Media gallery on Studio](/docs/studio/editors-medias.webp) + +It's an intuitive interface for non technical users to manage their `/public` directory. + +Users can easily browse folders, upload new media at any level, and drag and drop media into other folders, making medias organization straightforward. + +The interface is designed to be intuitive for non-technical users. It can be viewed as a user friendly IDE. + +## Use it in the Notion-like editor + +Users can simply drag and drop images directly into the editor. An upload modal will open to let you choose the destination folder. + +By typing `/` and searching for `Image`, they can quickly insert a media. A modal will open to let them choose the media they want to insert. + +From the media modal, you can set the [alt attribute](https://www.w3schools.com/tags/att_img_alt.asp) for SEO and accessibility purpose. diff --git a/docus/content/docs/8.studio/5.config.md b/docus/content/docs/8.studio/5.config.md new file mode 100644 index 000000000..d17de322a --- /dev/null +++ b/docus/content/docs/8.studio/5.config.md @@ -0,0 +1,157 @@ +--- +title: Tailor application configuration edition +description: Discover how to customize application config edition in Studio by + providing schema. Ensure a smooth and structured content management + experience. +navigation: + title: App Config +seo: + title: Tailor data edition in Nuxt Content Studio CMS +--- + +When entering the **Config** tab of the editor, you can browse configurations to customize your website. These configurations represent the settings defined in your `app.config.ts` file. + +## `app.config.ts` + +The [`app.config.ts`](https://nuxt.com/docs/guide/directory-structure/app-config) file is a configuration file introduced in Nuxt 3. It's a TypeScript file that allows you to configure various aspects of your application settings. Developers can easily turn any website into a configurable experience using this file. + +## Customize edition + +::prose-note +Ensure you have at least an empty config file in your app. + +```ts [app.config.ts] +export default defineAppConfig({}) +``` +:: + +To create a customized editing experience for your `app.config.ts` in Studio, you need to create a `nuxt.schema.ts` file in your project. This schema serves as a representation of your `app.config.ts`. + +### Helpers + +::prose-note +Those helpers are provided by the Nuxt Content `Preview API`. +:: + +- The `group` method allows you to customize parent objects. +- The `field` method allows you to customize inputs (aka leaf). + +```ts [nuxt.schema.ts] +import { field, group } from '@nuxt/content/preview' + +export default defineNuxtSchema({ + appConfig: { + parent: group({ + title: 'Parent title', + description: 'Parent description.', + icon: 'i-icon-to-display', + fields: { + leaf: field({ + type: 'Type of component used to edit your field', + title: 'Field title', + description: 'Field Description', + icon: 'i-icon-to-display', + default: 'default value' + }) + } + }) + } +}) +``` + +::tip +It is not mandatory to include all your app config keys; only those you wish to showcase on the Studio interface need to be added. +:: + +#### Input Types + +The `type` in the `field()` method's first parameter can accept various values: + +- string +- number +- boolean +- array +- object +- icon +- media + +Based on these values, the Studio UI will adapt to display the appropriate input type. For instance, an icon picker is displayed for `icon` type or the media library is displayed for `media` type. + +Text can be displayed as a select instead on a classical input if you provide a `required` key in the `field()` method: + +```ts [nuxt.schema.ts] +import { field, group } from '@nuxt/content/preview' + +export default defineNuxtSchema({ + appConfig: { + parent: group({ + title: 'UI', + description: 'UI configuration', + icon: 'i-ph-palette-fill', + fields: { + primary: field({ + type: 'string', + title: 'Primary', + description: 'Primary color of your UI.', + icon: 'i-ph-palette', + default: 'sky', + required: ['sky', 'mint', 'rose', 'amber'] + }) + } + }) + } +}) +``` + +### Edit on Studio + +Once your schema is deployed. Any user can access the **Data** section and play with the generated form. + +Any update in the form will be directly applied to the `app.config.ts` file. You can review those changes on the review page. + +::code-group + :::preview-card{icon="i-lucide-eye" label="Preview"} + ![app config UI on Studio](/docs/studio/home-data-studio-dark.webp) + ::: + +```ts [nuxt.schema.ts] +export default defineNuxtSchema({ + ui: group({ + title: 'UI', + description: 'UI Customization.', + icon: 'i-mdi-palette-outline', + fields: { + primary: field({ + type: 'string', + title: 'Primary', + description: 'Primary color of your UI.', + icon: 'i-mdi-palette-outline', + required: ['sky', 'mint', 'rose', 'amber', 'violet', 'emerald', 'fuchsia', '...'] + }), + gray: field({ ... }), + icons: group({ + title: 'Icons', + description: 'Manage icons used in UI Pro.', + icon: 'i-mdi-application-settings-outline', + fields: { + search: field({ ...}), + dark: field({ ... }), + light: field({ ... }), + external: field({ ...}), + chevron: field({ ... }), + hash: field({ ... }) + } + }) + } + }) +}) +``` +:: + +::tip{to="/docs/studio/debug"} +Take a look at this section to validate your schema in local development. +:: + +::tip{to="https://github.com/nuxt-ui-pro/docs/blob/main/nuxt.schema.ts"} +For a practical example, take a look at the schema we've developed for the UI Pro Docs starter. +:: diff --git a/docus/content/docs/8.studio/6.debug.md b/docus/content/docs/8.studio/6.debug.md new file mode 100644 index 000000000..099c454a5 --- /dev/null +++ b/docus/content/docs/8.studio/6.debug.md @@ -0,0 +1,62 @@ +--- +title: Local Debug +description: Validate your customization in local development. +seo: + title: Test your Nuxt Content Studio customization on local. + description: Validate your Nuxt Content Studio customization in local + development to ensure a smooth edition once deployed. +--- + +## Purpose + +The goal of this section is to explain how to validate your customization in local before publishing on production. + +It can help if: + +- You want to create a `nuxt.schema.ts` file and [generate the appropriate interface](/docs/studio/config) on Studio. +- You want to integrate [custom Vue components](/docs/studio/content) and ensure edition is working as expected in the editor. + +## Tutorial + +::steps + +### Import your project on Studio + +### Clone your repository on local + +### Enable Nuxt Content preview in development + +```ts [nuxt.config.ts] +export default defineNuxtConfig({ + content: { + preview: { + // force module initialization on dev env + dev: true + } + } +}) +``` + +### Launch your app using your dev command with `--tunnel` to expose it to the internet + +```bash [Terminal] +npx nuxt dev --tunnel +``` + +![local tunnel expose](/docs/studio/dev-tunnel.png) + +### Ensure the metadata file has been generated + +`__preview.json` file should accessible from `https://your-localtunnel-url/__preview.json` + +### Copy the tunnel URL and copy it in the self-hosting section of the deployment tab on Studio platform + +:: + +::tip +That's it! You should now be able to access Studio UI and confirm that your config interface has been successfully generated and your Vue components are available with their props and slots in the editor. +:: + +::warning +Any modification of your `nuxt.config.ts` file or any changes in a Vue file require a restart of the Nuxt dev server. Once the server has restarted you can synchronize the Studio interface by calling the `Sync meta` action from command menu :shortcut{value="meta"} :shortcut{value="K"} . A refresh of the Studio app should also apply the update. +:: diff --git a/docus/content/index.md b/docus/content/index.md new file mode 100644 index 000000000..41cf1ba13 --- /dev/null +++ b/docus/content/index.md @@ -0,0 +1,445 @@ +--- +seo: + title: The git-based CMS for Nuxt projects. + description: Nuxt Content is a module for Nuxt that provides a simple way to + manage content for your application. It allows developers to write their + content in Markdown, YAML, or JSON files and then query and display it in + their application. +--- + +::u-page-hero + :::div{.hidden.sm:block} + ::::u-color-mode-image + --- + class: size-full absolute bottom-0 inset-0 z-[-1] + dark: /home/hero-dark.svg + light: /home/hero-light.svg + --- + :::: + ::: + +#title{unwrap="p"} +The git-based CMS for :br Nuxt projects. + +#description +Nuxt Content is a module for Nuxt that provides a simple way to manage content for your application. It allows developers to write their content in Markdown, YAML, CSV or JSON files and then query and display it in their application. + +#links{unwrap="p"} + :::u-button + --- + label: Get Started + size: xl + to: /docs/getting-started/installation + trailingIcon: i-lucide-arrow-right + --- + ::: + + :::u-button + --- + color: neutral + label: Open Visual Editor + size: xl + target: _blank + to: https://nuxt.studio + variant: subtle + --- + ::: +:: + +::u-page-section +#features + :::u-page-feature + --- + icon: i-lucide-files + --- + #title{unwrap="p"} + File-based CMS + + #description{unwrap="p"} + Write your content in Markdown, YAML, CSV or JSON and query it in your components. + ::: + + :::u-page-feature + --- + icon: i-lucide-filter + --- + #title{unwrap="p"} + Query Builder + + #description{unwrap="p"} + Query your content with a MongoDB-like API to fetch the right data at the right time. + ::: + + :::u-page-feature + --- + icon: i-lucide-database + --- + #title{unwrap="p"} + SQLite powered + + #description{unwrap="p"} + Add custom fields to your content, making it suitable for various types of projects. + ::: + + :::u-page-feature + --- + icon: i-simple-icons-markdown + --- + #title{unwrap="p"} + Markdown with Vue + + #description{unwrap="p"} + Use Vue components in Markdown files, with props, slots and nested components. + ::: + + :::u-page-feature + --- + icon: i-lucide-list-minus + --- + #title{unwrap="p"} + Code highlighting + + #description{unwrap="p"} + Display beautiful code blocks on your website with the Shiki integration supporting VS Code themes. + ::: + + :::u-page-feature + --- + icon: i-lucide-mouse-pointer-click + --- + #title{unwrap="p"} + Visual Editor + + #description{unwrap="p"} + Let your team edit your Nuxt Content project with Nuxt Studio, our visual editor. + ::: + + :::u-page-feature + --- + icon: i-lucide-panel-left + --- + #title{unwrap="p"} + Navigation Generation + + #description{unwrap="p"} + Generate a structured object from your content files and display a navigation menu in minutes. + ::: + + :::u-page-feature + --- + icon: i-lucide-heading-1 + --- + #title{unwrap="p"} + Prose Components + + #description{unwrap="p"} + Customize HTML typography tags with Vue components to give your content a consistent style. + ::: + + :::u-page-feature + --- + icon: i-lucide-globe + --- + #title{unwrap="p"} + Deploy everywhere + + #description{unwrap="p"} + Nuxt Content works on all hosting providers, static, server, serverless & edge. + ::: +:: + +::u-page-section +#title +Everything you need for content management + +#description +Combine file-based simplicity with Vue component power. Build content-rich websites, from documentation pages to complex applications. + + :::div{.hidden.sm:block} + ::::u-color-mode-image + --- + class: size-full absolute top-0 inset-0 + dark: /home/features-dark.svg + light: /home/features-light.svg + --- + :::: + ::: +:: + +::u-page-section +--- +reverse: true +orientation: horizontal +--- + :::tabs + ::::tabs-item{icon="i-lucide-eye" label="Preview"} + :::::browser-frame + :::::u-page-hero + ![Everest visual](/mountains/everest.jpg) + + #title + The Everest. + + #description + The Everest is the highest mountain in the world, standing at 8,848 meters above sea level. + ::::: + ::::: + :::: + + ::::tabs-item{icon="i-simple-icons-markdown" label="content/index.md"} + ```mdc [content/index.md] + --- + title: The Mountains Website + description: A website about the most iconic mountains in the world. + --- + + ::page-hero + --- + image: /mountains/everest.png + --- + #title + The Everest. + + #description + The Everest is the highest mountain in the world, standing at 8,848 meters above sea level. + :: + ``` + :::: + + ::::tabs-item{icon="i-simple-icons-vuedotjs" label="components/PageHero.vue"} + ```vue [components/PageHero.vue] + + + + ``` + :::: + ::: + +#title +Markdown meets [Vue]{.text-(--ui-primary)} components + +#description +We created the MDC syntax to let you use Vue components with props and slots inside your Markdown files. + +#features + :::u-page-feature + --- + icon: i-lucide-list + --- + #title{unwrap="p"} + Specify props with frontmatter syntax + ::: + + :::u-page-feature + --- + icon: i-lucide-hash + --- + #title{unwrap="p"} + Use components slots with `#` + ::: + + :::u-page-feature + --- + icon: i-lucide-code-xml + --- + #title{unwrap="p"} + Add any other html attributes + ::: + +#links + :::u-button + --- + color: neutral + label: Learn more about MDC + to: /docs/files/markdown#mdc-syntax + trailingIcon: i-lucide-arrow-right + variant: subtle + --- + ::: +:: + +::u-page-section +--- +orientation: horizontal +--- + :::code-group + ```vue [pages/blog.vue] + + + + ``` + + ```ts [content.config.ts] + import { defineContentConfig, defineCollection, z } from '@nuxt/content' + + export default defineContentConfig({ + collections: { + blog: defineCollection({ + source: 'blog/*.md', + type: 'page', + // Define custom schema for docs collection + schema: z.object({ + tags: z.array(z.string()), + image: z.string(), + date: z.Date() + }) + }) + } + }) + ``` + ::: + +#title +Query with [Type-Safety]{.text-(--ui-secondary)} + +#description +Define your content structure with collections and query them with schema validation and full type-safety. + +#features + :::u-page-feature + --- + icon: i-lucide-layout-grid + --- + #title{unwrap="p"} + Create collections for similar content files + ::: + + :::u-page-feature + --- + icon: i-lucide-circle-check + --- + #title{unwrap="p"} + Define schema for the collection frontmatter + ::: + + :::u-page-feature + --- + icon: i-lucide-text-cursor + --- + #title{unwrap="p"} + Get auto-completion in your Vue files + ::: + +#links + :::u-button + --- + color: neutral + label: Learn more about content collections + to: /docs/collections/define + trailingIcon: i-lucide-arrow-right + variant: subtle + --- + ::: +:: + +::u-page-section +--- +reverse: true +orientation: horizontal +--- +:video{autoplay controls loop src="https://res.cloudinary.com/nuxt/video/upload/v1733494722/contentv3final_rc8bvu.mp4"} + + + +#title{unwrap="p"} +Let [anyone edit]{.text-(--ui-primary)} your website + +#description +Edit your Nuxt Content website with **Studio**, our CMS platform with Notion-like Markdown editor and generated forms for `YAML` and `JSON` files. Live preview and online collaboration included. + +#features + :::u-page-feature + --- + icon: i-lucide-mouse-pointer-click + --- + #title{unwrap="p"} + Visual editor with drag and drop for Markdown + ::: + + :::u-page-feature + --- + icon: i-lucide-file-text + --- + #title{unwrap="p"} + Form generation for YML and JSON files + ::: + + :::u-page-feature + --- + icon: i-simple-icons-google + --- + #title{unwrap="p"} + Invite editors to login with Google and publish changes + ::: + +#links + :::u-button + --- + color: neutral + label: Discover Studio + to: /studio + trailingIcon: i-lucide-arrow-right + --- + ::: +:: + +::u-page-section + :::div{.hidden.sm:block} + ::::u-color-mode-image + --- + class: size-full absolute bottom-0 inset-0 z-[-1] + dark: /home/cta-dark.svg + light: /home/cta-light.svg + --- + :::: + ::: + +#title +Add a git-based CMS to your Nuxt project. + +#links + :::u-button + --- + label: Start reading docs + to: /docs/getting-started/installation + trailingIcon: i-lucide-arrow-right + --- + ::: + + :::u-button + --- + color: neutral + label: Open Studio + target: _blank + to: https://nuxt.studio + variant: outline + --- + ::: +:: diff --git a/docus/content/studio/index.md b/docus/content/studio/index.md new file mode 100644 index 000000000..2a1b01434 --- /dev/null +++ b/docus/content/studio/index.md @@ -0,0 +1,328 @@ +--- +seo: + title: Meet Studio, content edition for everyone. + description: Nuxt Studio brings visual editing to your Nuxt Content projects. + Anyone can contribute to the website thanks to our versatile editor that + adapt to markdown, YAML, or JSON – no technical expertise needed. Built for + developers, made for everyone. +--- + +::u-page-hero + :::div{.hidden.sm:block} + ::::u-color-mode-image + --- + class: size-full absolute bottom-0 inset-x-4 z-[-1] + dark: /home/hero-dark.svg + light: /home/hero-light.svg + --- + :::: + ::: + +#title{unwrap="p"} +Meet Studio, content edition :br for everyone. + +#description +**Nuxt Studio** brings visual editing to your Nuxt Content projects. Anyone can contribute to the website thanks to our versatile editor that adapt to markdown, YAML, or JSON. No technical expertise needed. *Built for developers, made for everyone.* + +#links{unwrap="p"} + :::u-button + --- + label: Get Started for free + size: xl + target: _blank + to: https://nuxt.studio/signin + trailingIcon: i-lucide-arrow-right + --- + ::: + + :::u-button + --- + color: neutral + label: Read the documentation + size: xl + to: /docs/studio/setup + variant: subtle + --- + ::: +:: + +::u-page-section +#features + :::u-page-feature + --- + icon: i-lucide-circle-user + --- + #title{unwrap="p"} + GitHub & Google Authentication + + #description{unwrap="p"} + Personalized workspace for each role: developers, writers, and clients. + ::: + + :::u-page-feature + --- + icon: i-lucide-file-pen + --- + #title{unwrap="p"} + Easy content updates + + #description{unwrap="p"} + From Markdown to YAML edition, our visual editor is designed for non technical users. + ::: + + :::u-page-feature + --- + icon: i-lucide-users + --- + #title{unwrap="p"} + Real-time Collaboration + + #description{unwrap="p"} + Write as a team in real-time with our collaboration features. + ::: + + :::u-page-feature + --- + icon: i-lucide-sparkles + --- + #title{unwrap="p"} + From Code to Edition + + #description{unwrap="p"} + Developers build the foundation while writers can safely edit the content. + ::: + + :::u-page-feature + --- + icon: i-lucide-panels-top-left + --- + #title{unwrap="p"} + Review before publishing + + #description{unwrap="p"} + Review your changes before making them live on your website. + ::: + + :::u-page-feature + --- + icon: i-lucide-mouse-pointer-click + --- + #title{unwrap="p"} + Ready-to-use Templates + + #description{unwrap="p"} + Get started quickly with pre-built templates for Saas sites, blogs, docs and more. + ::: +:: + +::u-page-section +--- +orientation: horizontal +--- + :::code-group + ```vue [components/content/HomeFeature.vue] + + + + ``` + + ```mdc [content/index.md] + ::home-feature + --- + icon: i-mdi-vuejs + --- + #title + Embedded Vue components + #description + Edit slots and props inside the Notion-like editor. + :: + ``` + + ::::preview-card{icon="i-lucide-eye" label="Editor"} + ![vue component edition on Studio](/docs/studio/home-content-studio-dark.webp) + :::: + ::: + +#title{unwrap="p"} +Developers create the [editing experience]{.text-(--ui-primary)} + +#description +Developers build the foundation their way: custom components, media library, and site configuration. + +#features + :::u-page-feature + --- + icon: i-lucide-settings-2 + --- + #title{unwrap="p"} + Customizable and editable Vue components + ::: + + :::u-page-feature + --- + icon: i-simple-icons-markdown + --- + #title{unwrap="p"} + Edit your Markdown with our visual editor + ::: + + :::u-page-feature + --- + icon: i-lucide-brush + --- + #title{unwrap="p"} + Edit your app.config visually + ::: + +#links + :::u-button + --- + color: neutral + label: Learn more about custom components + to: /docs/files/markdown#vue-components + trailingIcon: i-lucide-arrow-right + variant: subtle + --- + ::: +:: + +::u-page-section +--- +reverse: true +orientation: horizontal +--- +:video{autoplay controls loop src="https://res.cloudinary.com/nuxt/video/upload/v1744126742/studio/finalpropscomps_usfabp.mp4"} + +#title{unwrap="p"} +Let [anyone edit]{.text-(--ui-primary)} your Nuxt Content website + +#description +Teams and clients get a powerful visual editor for content management, from text edition to media management - all without touching code. + +#features + :::u-page-feature + --- + icon: i-lucide-mouse-pointer-click + --- + #title{unwrap="p"} + Visual editor with drag and drop for Markdown + ::: + + :::u-page-feature + --- + icon: i-lucide-file-text + --- + #title{unwrap="p"} + Form generation for `YML` and `JSON` files + ::: + + :::u-page-feature + --- + icon: i-lucide-image + --- + #title{unwrap="p"} + Manage all your medias in one place + ::: +:: + +::u-page-section +--- +orientation: horizontal +--- + :::u-color-mode-image + --- + class: size-full + dark: /home/pro-dark.svg + light: /home/pro-light.svg + --- + ::: + +#title +[Edit together]{.text-(--ui-primary)} , preview instantly + +#description +Edit content as a team and see your site come to life with live preview. From text editing to component updates, every change reflects your final website design. Experience seamless content creation without delays or manual refreshes. + +#features + :::u-page-feature + --- + icon: i-lucide-files + --- + #title{unwrap="p"} + See your changes in real-time on your website + ::: + + :::u-page-feature + --- + icon: i-lucide-link + --- + #title{unwrap="p"} + Share preview URLs to anyone with live updates + ::: + + :::u-page-feature + --- + icon: i-lucide-list + --- + #title{unwrap="p"} + Review all your changes before publishing + ::: +:: + +::u-page-section + :::div{.hidden.sm:block} + ::::u-color-mode-image + --- + class: size-full absolute bottom-0 inset-x-4 z-[-1] + dark: /home/cta-dark.svg + light: /home/cta-light.svg + --- + :::: + ::: + +#title +The [best way]{.text-(--ui-primary)} to edit your [Nuxt Content]{.text-(--ui-primary)} website + +#links + :::u-button + --- + label: Get started for free + target: _blank + to: https://nuxt.studio/signin + trailingIcon: i-lucide-arrow-right + --- + ::: + + :::u-button + --- + color: neutral + label: See pricing + to: /studio/pricing + variant: outline + --- + ::: + +#description +Import your Nuxt Content website and invite your team to collaborate today. +:: diff --git a/docus/content/studio/pricing.yml b/docus/content/studio/pricing.yml new file mode 100644 index 000000000..bb0be1c65 --- /dev/null +++ b/docus/content/studio/pricing.yml @@ -0,0 +1,221 @@ +title: Get the right plan for your project +description: Explore our options tailored to individual and team needs. Get started for free. +onboarding: + title: Team Onboarding + image: + dark: /pricing/timeline-dark.webp + light: /pricing/timeline-light.webp +plans: + solo: + title: Solo + description: For solo user signed up with their personal GitHub account. + price: Free + features: + - Unlimited projects + - 1 member + - Notion-like editor + - Live editing & preview + - Media library + - GitHub synchronization + - ... and much more + button: + label: Get started for free + color: neutral + to: 'https://nuxt.studio' + cycle: '' + team: + title: Team + description: For developers, editors and clients who want to collaborate. + price: $29 + cycle: /project/month + highlight: true + badge: Most popular + features: + - 1 project + - 2 members included + - $9 per additional member + - Team & workflow management + - Real-time collaboration + - Priority support + - All solo features + button: + label: Start 14-day free trial + to: 'https://nuxt.studio' + target: _blank + unlimited: + title: Unlimited + description: For large teams looking to collaborate on multiple projects with no limits. + cycle: /month + price: $199 + features: + - Unlimited projects + - Unlimited members + - All Solo features + - Team & workflow management + - Real-time collaboration + - Priority support + button: + label: Start 14-day free trial + to: 'https://nuxt.studio' + target: _blank +features: + title: All Features + description: Studio has a per-project pricing model that can scale to meet your needs. + includes: + projects: + title: Projects + value: + - Unlimited + - '1' + - Unlimited + members: + title: Members + value: + - '1' + - 2 included ($9 per extra) + - Unlimited + media: + title: Media Library + plans: + - solo + - team + - unlimited + support: + title: Discord support + plans: + - solo + - team + - unlimited + dedicated: + title: Dedicated Discord channel + plans: + - team + - unlimited + roles: + title: Roles and permissions + plans: + - team + - unlimited + collaboration: + title: Live Collaboration + plans: + - team + - unlimited + sync: + title: GitHub syncing + includes: + repositories: + title: Repositories + plans: + - solo + - team + - unlimited + workflow: + title: Branches and Pull Requests + plans: [] + soon: true + files: + title: Files + plans: + - solo + - team + - unlimited + actions: + title: Actions + plans: + - solo + - team + - unlimited + project: + title: Project + includes: + clone: + title: Clone a template + plans: + - solo + - team + - unlimited + import: + title: Import a repository + plans: + - solo + - team + - unlimited + editors: + title: Editors + includes: + markdown: + title: Markdown editor + plans: + - solo + - team + - unlimited + json: + title: JSON/YML form editor + plans: + - solo + - team + - unlimited + appconfig: + title: App config editor + plans: + - solo + - team + - unlimited + drag: + title: Drag'n drop + plans: + - solo + - team + - unlimited + preview: + title: Live Preview + includes: + draft: + title: Studio Draft + plans: + - solo + - team + - unlimited + branches: + title: Branches + plans: [] + soon: true + prs: + title: Pull requests + plans: [] + soon: true + deploy: + title: Deploy + includes: + gh: + title: GitHub Pages + plans: + - solo + - team + - unlimited + self: + title: Self Hosted + plans: + - solo + - team + - unlimited + publish: + title: Save changes + includes: + preview: + title: Preview differences + plans: + - solo + - team + - unlimited + branch: + title: Branch creation / merge + plans: [] + soon: true + commit: + title: Commit + plans: + - solo + - team + - unlimited diff --git a/docus/content/templates.yml b/docus/content/templates.yml new file mode 100644 index 000000000..115be7dad --- /dev/null +++ b/docus/content/templates.yml @@ -0,0 +1,2 @@ +title: Templates +description: Jumpstart your next website with our tailor-made templates. Deploy in one click. Edit them live and share real-time preview urls. diff --git a/docus/content/templates/canvas.md b/docus/content/templates/canvas.md new file mode 100644 index 000000000..5cf49ab0c --- /dev/null +++ b/docus/content/templates/canvas.md @@ -0,0 +1,56 @@ +--- +slug: canvas +subtitle: "" +title: Canvas +baseDir: . +branch: main +category: portofolio +demo: https://canvas.hrcd.fr/ +description: This is a fully customizable portfolio template with internationalization. +licenseType: free +mainScreen: /templates/canvas.jpg +name: canvas +owner: HugoRCD +image1: /templates/canvas-portfolio-1.png +image2: /templates/canvas-portfolio-2.png +draft: false +image3: /templates/canvas-portfolio-3.png +--- + +::template-core +Canvas Portfolio is a fully customizable i18n portfolio template built with Nuxt and Nuxt UI, designed to help you showcase your work, testimonials, and key information with ease. The template integrates with Nuxt Studio for a smooth editing experience, while leveraging Nuxt Content for content management. Built with performance, flexibility, and responsiveness in mind, Canvas Portfolio provides a robust foundation for developers and creatives alike. + +- **Modern Components & Layouts** – Includes built-in components. +- **Nuxt UI v3** – Utilize pre-built, customizable UI components. +- **NuxtHub ready** - Deploy on NuxtHub in seconds. +- **Tailwind CSS** – A beautiful, responsive design system. +- **Working Contact Form** – Integrated with Resend for easy email handling. +- **Multi-language Support** – Powered by Nuxt i18n. +- **SEO-Ready** – Open Graph Image (Nuxt OG Image) & Nuxt Robots for automatic robots.txt generation. +- **Good practices** – Auto-generated sitemap, optimized images (Nuxt Image), and ESLint (Nuxt config with Flat config). +- **Fully Responsive** – Adapts to all modern browsers and devices. +- **Minimal & Professional Design** – Clean, elegant, and easy to customize. + +#right + :::template-features + --- + features: + - label: Nuxt Architecture + content: Harness the full power of Nuxt 3 and its modules ecosystem. + - label: Nuxt Studio ready + content: Edit your theme content and appearance with live-preview within Nuxt + Studio. + - label: Vue Components + content: Insert built-in components (or your own) inside your content. + - label: Write Markdown + content: Enjoy the ease and simplicity of Markdown and discover MDC syntax. + - label: Deploy anywhere + content: In one click from Studio or with zero config on Vercel or Netlify. + Choose between static generation, on-demand rendering (Node) or edge-side + rendering on CloudFlare workers. + - label: Extensible + content: Customize the whole design, or add components using slots - you can + make Content-Wind your own. + --- + ::: +:: diff --git a/docus/content/templates/content-wind.md b/docus/content/templates/content-wind.md new file mode 100644 index 000000000..3fdd0f3be --- /dev/null +++ b/docus/content/templates/content-wind.md @@ -0,0 +1,52 @@ +--- +slug: content-wind +subtitle: A minimal portfolio template +title: Content Wind +baseDir: . +branch: main +category: blog +demo: https://content-wind.nuxt.space +description: A lightweight Nuxt theme to build your portfolio in minutes. +licenseType: free +mainScreen: /templates/content-wind-main.webp +name: content-wind +owner: Atinux +image1: /templates/content-wind-1.webp +image2: /templates/content-wind-2.webp +draft: true +--- + +::template-core +A lightweight Nuxt theme to build a Markdown driven website, based on Nuxt Content, TailwindCSS and Iconify + +- Use layouts in Markdown pages +- Enjoy meta tag generation +- Configurable prose components +- Generated navigation from pages +- Switch between light & dark mode +- Access 100,000 icons from 100+ icon sets +- Highlight code blocks with Shiki + +#right + :::template-features + --- + features: + - label: Nuxt Architecture + content: Harness the full power of Nuxt 3 and its modules ecosystem. + - label: Nuxt Studio ready + content: Edit your theme content and appearance with live-preview within Nuxt + Studio. + - label: Vue Components + content: Insert built-in components (or your own) inside your content. + - label: Write Markdown + content: Enjoy the ease and simplicity of Markdown and discover MDC syntax. + - label: Deploy anywhere + content: In one click from Studio or with zero config on Vercel or Netlify. + Choose between static generation, on-demand rendering (Node) or edge-side + rendering on CloudFlare workers. + - label: Extensible + content: Customize the whole design, or add components using slots - you can + make Content-Wind your own. + --- + ::: +:: diff --git a/docus/content/templates/docs.md b/docus/content/templates/docs.md new file mode 100644 index 000000000..dacbe9593 --- /dev/null +++ b/docus/content/templates/docs.md @@ -0,0 +1,50 @@ +--- +slug: docs +subtitle: "" +title: Docs +baseDir: . +branch: main +category: docs +createdAt: 2023-11-15T17:41:03.087Z +demo: https://v3.docs-template-3erl.pages.dev/ +description: Create your documentation in seconds with this template. +licenseType: nuxt-ui-pro +mainScreen: /templates/docs.jpg +name: docs +owner: nuxt-ui-pro +image1: /templates/docs-1.webp +image2: /templates/docs-2.webp +image3: /templates/docs-3.webp +--- + +::template-core +[Nuxt UI Pro](https://ui.nuxt.com/pro) is a collection of premium components, an extension of [Nuxt UI](https://ui.nuxt.com), designed to facilitate the creation of appealing and responsive Nuxt applications in a matter of minutes. + +The Nuxt UI team is dedicated to deliver the best integration and customization experience, while the Studio team is providing full compatibility with Nuxt Studio. + +- **Fully customizable**: change the style of any component from your App Config or customize them specifically through the ui prop. +- **Write Markdown with ease**: Nuxt UI Pro overrides Nuxt Content prose components to make them awesome but also adds new ones like Callout, CodeGroup, Field, etc. +- **Beautiful Typography styles**: Tailwind CSS typography plugin is pre-configured and styled to match Nuxt UI components and colors. +- **Full-Text Search out of the box**: Nuxt UI Pro ships with a ready to use command palette component. No need to setup Algolia DocSearch anymore. +- **Slots for everything**: Each component leverages the power of Vue's slots to give you the flexibility to build anything. +- **Responsive by design**: Nuxt UI Pro components aims to structure your content, they are responsive by design and will adapt to any screen size. + +#right + :::template-features + --- + features: + - label: Nuxt 3 + content: Powered by Nuxt 3 for optimal performances and SEO. + - label: Markdown + content: Write your pages with MDC thanks to Nuxt Content. + - label: Nuxt UI + content: Offers a very large set of full customizable components. + - label: TypeScript + content: A fully typed development experience. + - label: Nuxt Studio + content: Supported by Nuxt Studio for fast updates and previews. + - label: Search + content: A full-text search modal empowered by Fuse.js. + --- + ::: +:: diff --git a/docus/content/templates/docus.md b/docus/content/templates/docus.md new file mode 100644 index 000000000..ab4f933d2 --- /dev/null +++ b/docus/content/templates/docus.md @@ -0,0 +1,79 @@ +--- +slug: docus +subtitle: "" +title: Docus +baseDir: .starter +branch: main +category: docs +createdAt: 2023-11-15T17:41:03.087Z +demo: https://docus.dev +description: Write beautiful docs with Markdown +licenseType: nuxt-ui-pro +mainScreen: /templates/docus.webp +name: docus +owner: nuxtlabs +image1: /blog/docus.webp +image2: "" +image3: "" +--- + +::template-core +Docus is a theme based on the [UI Pro documentation template](https://docs-template.nuxt.dev/). While the visual style comes ready out of the box, your focus should be on writing content using the Markdown and [MDC syntax](https://content.nuxt.com/docs/files/markdown#mdc-syntax) provided by [Nuxt Content](https://content.nuxt.com). + +We use this theme across all our Nuxt module documentations, including: + + :::card-group + ::::card + --- + icon: i-simple-icons-nuxtdotjs + target: _blank + title: Nuxt Image + to: https://image.nuxt.com + --- + The documentation of `@nuxt/image` + :::: + + ::::card + --- + icon: i-simple-icons-nuxtdotjs + target: _blank + title: Nuxt Supabase + to: https://supabase.nuxtjs.org + --- + The documentation of `@nuxt/supabase` + :::: + ::: + +## Key Features + +This theme includes a range of features designed to improve documentation management: + +- **Powered by** [**Nuxt 3**](https://nuxt.com): Utilizes the latest Nuxt framework for optimal performance. +- **Built with** [**Nuxt UI**](https://ui.nuxt.com) **and** [**Nuxt UI Pro**](https://ui.nuxt.com/pro): Integrates a comprehensive suite of UI components. +- [**MDC Syntax**](https://content.nuxt.com/usage/markdown) **via** [**Nuxt Content**](https://content.nuxt.com): Supports Markdown with component integration for dynamic content. +- [**Nuxt Studio**](https://content.nuxt.com/docs/studio) **Compatible**: Write and edit your content visually. No Markdown knowledge is required! +- **Auto-generated Sidebar Navigation**: Automatically generates navigation from content structure. +- **Full-Text Search**: Includes built-in search functionality for content discovery. +- **Optimized Typography**: Features refined typography for enhanced readability. +- **Dark Mode**: Offers dark mode support for user preference. +- **Extensive Functionality**: Explore the theme to fully appreciate its capabilities. + +#right + :::template-features + --- + features: + - label: Nuxt 3 + content: Powered by Nuxt 3 for optimal performances and SEO. + - label: Markdown + content: Write your pages with MDC thanks to Nuxt Content. + - label: Nuxt UI + content: Offers a very large set of full customizable components. + - label: TypeScript + content: A fully typed development experience. + - label: Nuxt Studio + content: Supported by Nuxt Studio for fast updates and previews. + - label: Search + content: A full-text search modal empowered by Fuse.js. + --- + ::: +:: diff --git a/docus/content/templates/landing.md b/docus/content/templates/landing.md new file mode 100644 index 000000000..9e7f5133d --- /dev/null +++ b/docus/content/templates/landing.md @@ -0,0 +1,46 @@ +--- +slug: landing +subtitle: "" +title: Landing +baseDir: . +branch: main +category: minimal +demo: https://landing-template.nuxt.dev/ +description: A simple template you can use as a starting point for your next idea. +licenseType: nuxt-ui-pro +mainScreen: /templates/landing.jpg +name: Landing +owner: nuxt-ui-pro +draft: false +--- + +::div{.flex.justify-center} +:video{.rounded-lg autoplay controls loop :width='1000' src="https://res.cloudinary.com/nuxt/video/upload/v1745425967/studio/landing_q03gdo.mp4"} +:: + +::template-core +[Nuxt UI Pro](https://ui.nuxt.com/pro) is a collection of premium components, an extension of [Nuxt UI](https://ui.nuxt.com), designed to facilitate the creation of appealing and responsive Nuxt applications in a matter of minutes. + +The Nuxt UI team is dedicated to deliver the best integration and customization experience, while the Studio team is providing full compatibility with Nuxt Studio. + +- **Fully customizable**: change the style of your UI from your App Config or edit your landing page content from the `index.yml` file. +- **Beautiful Typography styles**: Tailwind CSS typography plugin is pre-configured and styled to match Nuxt UI components and colors. +- **Responsive by design**: Nuxt UI Pro components aims to structure your content, they are responsive by design and will adapt to any screen size. + +#right + :::template-features + --- + features: + - label: Nuxt 3 + content: Powered by Nuxt 3 for optimal performances and SEO. + - label: Content v3 + content: Write your content in yaml files or use Markdown with the MDC syntax. + - label: Nuxt UI v3 + content: Offers a very large set of full customizable components. + - label: TypeScript + content: A fully typed development experience. + - label: Nuxt Studio + content: Supported by Nuxt Studio for fast updates and previews. + --- + ::: +:: diff --git a/docus/content/templates/minimal-starter.md b/docus/content/templates/minimal-starter.md new file mode 100644 index 000000000..fc4368db0 --- /dev/null +++ b/docus/content/templates/minimal-starter.md @@ -0,0 +1,43 @@ +--- +slug: minimal-starter +subtitle: "" +title: Minimal Starter +baseDir: . +branch: content +category: minimal +demo: https://codesandbox.io/p/sandbox/github/nuxt/starter/tree/content +description: "Minimalist start, limitless possibilities: craft your vision!" +licenseType: free +mainScreen: /templates/starter.jpg +name: starter +owner: nuxt +image1: /templates/starter-1.webp +--- + +::template-core +Unleash your imagination with our minimalist starter: + +- Start with a clean slate and craft the application of your dreams. +- Nuxt Content all setup. +- Effortlessly compose pages using Markdown and Vue components, enhanced by the intuitive MDC syntax. + +#right + :::template-features + --- + features: + - label: Nuxt Architecture + content: Harness the full power of Nuxt 3 and its modules ecosystem. + - label: Nuxt Studio ready + content: Edit your theme content and appearance with live-preview within Nuxt + Studio. + - label: Vue Components + content: Use built-in components (or your own) inside your content. + - label: Write Markdown + content: Enjoy the ease and simplicity of Markdown and discover MDC syntax. + - label: Deploy anywhere + content: In one click from Studio or with zero config on Vercel or Netlify. + Choose between static generation, on-demand rendering (Node) or edge-side + rendering on CloudFlare workers. + --- + ::: +:: diff --git a/docus/content/templates/minted-directory.md b/docus/content/templates/minted-directory.md new file mode 100644 index 000000000..7f30cc2c6 --- /dev/null +++ b/docus/content/templates/minted-directory.md @@ -0,0 +1,47 @@ +--- +slug: minted-directory +subtitle: Starter kit to build directory website +title: Minted Directory +baseDir: . +branch: main +category: Blog +demo: https://www.minteddirectory.com/ +description: A SEO optimized directory starter kit that does not require a database. +licenseType: free +mainScreen: /templates/minted-directory-thumbnail.png +name: minted-directory +owner: masterkram +image1: /templates/minted-directory-1.png +image2: /templates/minted-directory-2.png +image3: /templates/minted-directory-3.png +draft: true +--- + +::template-core +Minted Directory is a highly customizable template designed for building successful directory websites quickly. + +- Create a directory website with a customized style/brand +- Manage listings with markdown +- SEO optimized +- Search and Categorization with tags +- Blog Pages + +#right + :::template-features + --- + features: + - label: Nuxt Architecture + content: Harness the full power of Nuxt 3 and its modules ecosystem. + - label: Nuxt Studio ready + content: Edit your theme content and appearance with live-preview within Nuxt Studio. + - label: Vue Components + content: Insert built-in components (or your own) inside your content. + - label: Write Markdown + content: Enjoy the ease and simplicity of Markdown and discover MDC syntax. + - label: Deploy anywhere + content: In one click from Studio or with zero config on Vercel or Netlify. Choose between static generation, on-demand rendering (Node) or edge-side rendering on CloudFlare workers. + - label: Extensible + content: Customize the whole design, or add components using slots - you can make Minted Directory your own. + --- + ::: +:: diff --git a/docus/content/templates/portfolio.md b/docus/content/templates/portfolio.md new file mode 100644 index 000000000..7ba0c9be9 --- /dev/null +++ b/docus/content/templates/portfolio.md @@ -0,0 +1,50 @@ +--- +slug: portfolio +subtitle: "" +title: Portfolio +baseDir: . +branch: main +category: minimal +demo: https://portfolio-template.nuxt.dev/ +description: A sleek, modern portfolio template to showcase your work, skills, + blog posts, speaking engagements, and provide contact information. +licenseType: nuxt-ui-pro +mainScreen: /templates/portfolio.jpg +name: portfolio +owner: nuxt-ui-pro +draft: false +--- + +::div{.flex.justify-center} +:video{.rounded-lg autoplay controls loop :width='1000' src="https://res.cloudinary.com/nuxt/video/upload/v1746088224/studio/d53kg6qoo802zy3dzmh0.mp4"} +:: + +::template-core +[Nuxt UI Pro](https://ui.nuxt.com/pro) is a collection of premium components, an extension of [Nuxt UI](https://ui.nuxt.com), designed to facilitate the creation of appealing and responsive Nuxt applications in a matter of minutes. + +The Nuxt UI team is dedicated to deliver the best integration and customization experience, while the Studio team is providing full compatibility with Nuxt Studio. + +- **Fully customizable**: change the style of any component from your App Config or customize them specifically through the ui prop. +- **Write Markdown with ease**: Nuxt UI Pro overrides Nuxt Content prose components to make them awesome but also adds new ones like Callout, CodeGroup, Field, etc. +- **Beautiful Typography styles**: Tailwind CSS typography plugin is pre-configured and styled to match Nuxt UI components and colors. +- **Full-Text Search out of the box**: Nuxt UI Pro ships with a ready to use command palette component. No need to setup Algolia DocSearch anymore. +- **Slots for everything**: Each component leverages the power of Vue's slots to give you the flexibility to build anything. +- **Responsive by design**: Nuxt UI Pro components aims to structure your content, they are responsive by design and will adapt to any screen size. + +#right + :::template-features + --- + features: + - label: Nuxt 3 + content: Powered by Nuxt 3 for optimal performances and SEO. + - label: Markdown + content: Write your pages with MDC thanks to Nuxt Content. + - label: Nuxt UI + content: Offers a very large set of full customizable components. + - label: TypeScript + content: A fully typed development experience. + - label: Nuxt Studio + content: Supported by Nuxt Studio for fast updates and previews. + --- + ::: +:: diff --git a/docus/content/templates/saas.md b/docus/content/templates/saas.md new file mode 100644 index 000000000..06d8de8f0 --- /dev/null +++ b/docus/content/templates/saas.md @@ -0,0 +1,51 @@ +--- +slug: saas +subtitle: "" +title: Saas +baseDir: . +branch: main +category: docs +demo: https://saas-template.nuxt.dev/ +description: A fully built SaaS application to launch your next project. +licenseType: nuxt-ui-pro +mainScreen: /templates/saas.jpg +name: SaaS +owner: nuxt-ui-pro +draft: false +--- + +::div{.flex.justify-center} +:video{.rounded-lg autoplay controls loop :width='1000' src="https://res.cloudinary.com/nuxt/video/upload/v1744979325/studio/saas_cafkml.mp4"} +:: + +::template-core +[Nuxt UI Pro](https://ui.nuxt.com/pro) is a collection of premium components, an extension of [Nuxt UI](https://ui.nuxt.com), designed to facilitate the creation of appealing and responsive Nuxt applications in a matter of minutes. + +The Nuxt UI team is dedicated to deliver the best integration and customization experience, while the Studio team is providing full compatibility with Nuxt Studio. + +- **Fully customizable**: change the style of any component from your App Config or customize them specifically through the ui prop. +- **Write Markdown with ease**: Nuxt UI Pro overrides Nuxt Content prose components to make them awesome but also adds new ones like Callout, CodeGroup, Field, etc. +- **Beautiful Typography styles**: Tailwind CSS typography plugin is pre-configured and styled to match Nuxt UI components and colors. +- **Full-Text Search out of the box**: Nuxt UI Pro ships with a ready to use command palette component. No need to setup Algolia DocSearch anymore. +- **Slots for everything**: Each component leverages the power of Vue's slots to give you the flexibility to build anything. +- **Responsive by design**: Nuxt UI Pro components aims to structure your content, they are responsive by design and will adapt to any screen size. + +#right + :::template-features + --- + features: + - label: Nuxt 3 + content: Powered by Nuxt 3 for optimal performances and SEO. + - label: Markdown + content: Write your pages with MDC thanks to Nuxt Content. + - label: Nuxt UI + content: Offers a very large set of full customizable components. + - label: TypeScript + content: A fully typed development experience. + - label: Nuxt Studio + content: Supported by Nuxt Studio for fast updates and previews. + - label: Search + content: A full-text search modal empowered by Fuse.js. + --- + ::: +:: diff --git a/docus/nuxt.config.ts b/docus/nuxt.config.ts new file mode 100644 index 000000000..070fd76d2 --- /dev/null +++ b/docus/nuxt.config.ts @@ -0,0 +1,23 @@ +export default defineNuxtConfig({ + modules: ['@nuxtjs/plausible'], + css: ['../app/assets/css/main.css'], + site: { + name: 'Nuxt Content', + }, + future: { + compatibilityVersion: 4, + }, + llms: { + domain: 'https://content.nuxt.com', + title: 'Nuxt Content', + description: 'Nuxt Content is a Git-based headless CMS for Nuxt', + notes: [ + 'The documentation only includes Nuxt Content v3 docs.', + 'The content is automatically generated from the same source as the official documentation.', + ], + full: { + title: 'Complete Documentation', + description: 'The complete documentation including all content', + }, + }, +}) diff --git a/docus/package.json b/docus/package.json new file mode 100644 index 000000000..d7f9731c9 --- /dev/null +++ b/docus/package.json @@ -0,0 +1,12 @@ +{ + "name": "docus-starter", + "scripts": { + "dev": "docus dev", + "build": "docus build" + }, + "dependencies": { + "docus": "latest", + "better-sqlite3": "^11.10.0", + "nuxt": "^3.17.3" + } +} \ No newline at end of file diff --git a/docus/public/favicon.ico b/docus/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..18993ad91cfd43e03b074dd0b5cc3f37ab38e49c GIT binary patch literal 4286 zcmeHLOKuuL5PjK%MHWVi6lD zOGiREbCw`xmFozJ^aNatJY>w+g ze6a2@u~m#^BZm@8wco9#Crlli0uLb^3E$t2-WIc^#(?t)*@`UpuofJ(Uyh@F>b3Ph z$D^m8Xq~pTkGJ4Q`Q2)te3mgkWYZ^Ijq|hkiP^9`De={bQQ%heZC$QU2UpP(-tbl8 zPWD2abEew;oat@w`uP3J^YpsgT%~jT(Dk%oU}sa$7|n6hBjDj`+I;RX(>)%lm_7N{+B7Mu%H?422lE%MBJH!!YTN2oT7xr>>N-8OF$C&qU^ z>vLsa{$0X%q1fjOe3P1mCv#lN{xQ4_*HCSAZjTb1`}mlc+9rl8$B3OP%VT@mch_~G z7Y+4b{r>9e=M+7vSI;BgB?ryZDY4m>&wcHSn81VH1N~`0gvwH{ z8dv#hG|OK`>1;j7tM#B)Z7zDN?{6=dUal}$en4GOC&)T`?_WDl691k3_alVKKo715p`xOK=z}Zd z-+4$3gn|CT#S3%{7cX37WMuf0m5-g3g@sjw=gMV1X)!rjX)!6uYpMp5YRXFg?}h3dHO+b2KfpoC<^LXsoIUaH2MCm!3UZE#nh8AhP4aKY zKI&KZw0Was|8SD!;Ol-A^_D71mMYGPT1S>D{t2Y#EmibA$dD1l2|~?8%O3^RcIFVK zgEGD0;E#5SkD_LJ3;plLl48n9=M;DV3c;i;L-fK>;)-lfOipCX3GN&Uh>{Co`v{ zUJR_v3L>(MC+D(doz7Ancn(-);m6$PVu@5>6|h)>jpaAZQ>Ln+_i zKxpX`wV62h>!DO}v5>b=s%VZlC(2@r0eCcJl_|^x3x%lQc8yIqBsPu7U&F2C;gjS8A_vKns$4`luy2Aa9%=^9n;`sS2H_Y2MI6Vmo*jcngJEz8 z34(Sr3l>1j6IoK2IqC&tiosPuoI~dxa{}{=RGQ)vc(f6VbC0W#ppak%7>GHHKH3Nr z!9eE?p(&1mRlw=t2C4kQ;zlu<;CWa6*wPzT{84hVP+B!sj?D2tG~aLt%gJg2C_tPt zNlubW4o9^mH+2ugi3xWHk3eGhH@#n7%L=-&#k2x$PeOcJ^7c*V z*4bE}8&T^+L9w7R%}fZG4UMd})>(0Bv04am!88GV${`vUHRUK5sNEbAOHU~RfE$#b z_96{aB1A6>r!4a8?2SEEm&29AJZ(7(fsO`a+uAH&0s= zK}AlouUsHbva|v&pbiQ@qo7m(`Ai%Phu0n)s@eakWT&BGy>$)jq0|9#>t!32}s$5eo(@ zOfPSq3CI+qX9&I|^_V#+qfC1d3*;(O@U5^xDor+Elb9$R^OB+ZAs~F__&se7YQScQ zGw3Y`pgb*~>>mQqlp=c##~b>)u#@m*WJtg^zgUHG-ylgcd+@?FB$*VEQPCB#^8x1> zu-XxNbui6-%NL+30v({2yTcR< zbitFAq6UC^t`)NI6z~rXn3&t1PYue=%Ec$r^1b1>BM%7yl0X$D52~aZfa&+)+dLco zN#g3j9XiGmHPE!$s_##xwlzdXT3gPCg@40ybgFYds_?pNO~cn0$IBm=`G%|DKYcEE z1mq#c_DnR+JVoprhYrU4f9e7?oFT7U@JU>cfj+B{HeI3s7!Dn<4l#ZPdVPW7xWv1O z6n)dV$1W(?3kq}wyrRsCnTu(X!YwX}fUsV=N|ObK%JqNBs<`v+!3`_sM5>o>oO=2- zdyJeZ6NB{3(Z4c&)okTHXJ3{6$=6@H`I*YLNp(oyu-rEJy^6oEUglT2?}gdPFCqfm(sqXks^KZ=c?SdKA&Pn*`tn921CmCdGj0;S>pvf$>*> zqA8?PpmYA>bsCoFNnHRS2Q_kbVdaAK^n=;NDG4D0I`ZF8c{%*#sA)-G*sFe z!x^sJwvveC{Y$B+*Xh(T^YG-?`|R&1Cjc>XJwd&*I_o)v_$a~$j!`ra6ay@mLl;g5 zjEbTX?1GeHK(Q5P405gK(DBr!O9G+cKam6lp3M?WSrq;(Drx6o-c?><7%erIFr-gd zPA)!)l?UqVORF51h#h2h-+P#h-mv2?!L zG+=ItpfV!}7pO=8Fb1T7VnvGYx`J%bJ2PDO{63-qsi3w|+Q-waP)u!QiOl?tkP5uh##bRY`K>XgX`ssWiuj-eZ*x3jeQ2B9VRh$0G$H#7vwKspofDQf_PUVCE)YTxVTEI4fj!6Q z6#&{XjSxU}e6lW@PEe4YX(0o&e0{<&r%A83f`h_0Mdj~sK}BgPp}7alht-)A(qkMA zf)pq(mJg~80UV10;rf4rG}Bv#%H^ugwL@3(sl#Q)qmZzppiO+uXY~H@>e&2n4JnB^ z-)K|8(JbBJ#@qp6|Ld^s4XgE=#y)Zg5JIlf$i{VFr32&xIHUwXa0S(hQxso6UJwim zi2Pl^KxPD@#~VYLA4LeAi^pRPL6sDK0yFCcf8!3|ZiQ^zBp5zp+>KkzNwl&Y)HD=q z4&*?bpb$@B0YaSGf6zH)Zpv!9LN?{PIilWzgCL541V|MP!igM3K%yxr7vwIWAo2VR zDZT>wcaMUQKaiyN2R|dO?{$fj$I5QxHKHSao`#*;sag5^@es5&2Ktdxj6pH@T!%7= zfym48n+GT%UyXn0#JDsu`an2=bIF3>7-Rk{4y;z(GhXnFbQC5^n!h+3S~;cWXP^yn zNpxNa3MDBf_AFJ43)r+U!0SDrs9+0{38;A|_LlDRi5?f)Gxim_#83(?bYXR3h;xz~ zfc0h3KamB>!vn?$P}$Q*K_^Enpc^w30(L|YStDmkQ15{PRMOJF<#n0bQ0yOZo-ZRlYMLQAw;MQ&hr4>j)?EC zkleE8@qe1Vq+Bxdvjc^-NneVUxQ0GP)xMU-Ib5o4C%bR%txdSZ{6NcVB`Dy&T|r8| zzh)@l*eYsO7&LpPyI5dn?}-xjw~Y(GtRIL*57p zM0eeAkg^!##&Wk2h@I9jL%ZEr$)bGAXjewo+P`qe3x8Q<5Nqph%zQ4f=5-hM9spzJ z7ZWr9!KWBhDhL#FNy2N^-|D42^8rdT--?oa?N7OG!8PM zDA0v*5Mser3N-HJ)yn>5ia5XF*6F8D1oxDFd^Q3bDM%0P8qh3i2nQ5^;02Uc79-TK zu#T*84}8&})}sd^krOpLMZKc{!R1*63A=UA*N>)5;#X@`zdL7po7G(T7ecn*v-$oY zP5pO}_q#IG$~*rXR+wym)u33De6@UkBQ#37ZiYzAiBux~CNutKKq&+)W`195Yfj2U z;#5`U*DI!sPhQd0BwoP2dX>;o#h1N3uP%q zQwYFJ(SU3~hqJ%|$N;j)GYt5R;t~Ltvxb2_0q;snqohiZhAFnpk2$*g^D9-kGK7|j|?A8}hhb`<08Kvzm zzD$P%|HrC1j_2a9HpASzW ze9^yNJ7Ss?I(p%PAb(8DQSPVqqV zOu+$=qO~}A-LEeI5db?g4r(3{z94U$sre}27zfDc6IRxCb^)7XKH%cu3<8(8T(^rr zakVzhEJYj!?+img(w|W{&)s@jk=$x%F`hg5W3l?_)w+w-CB(_W*`lhkvTOLU&4Xo! zu%@rOLxa;~wEB;Gkq*xl0?UjhMx|Ko=zNCD{P%7cUP3&WR#MH-*x5a)(|$*~AwxR& zZq**|QS{v7@7yj8RdSPTjQ z6(}1a3MJwyXw2v;BoGvSWa?7V4MInc89~_+QRG7hmY z%(#SA%s8Mo0cYeCwOn*&DY&3zf>I2U4odZcvZn=S1cen4JQr?Wf)uCx=lD=D+1BCMRr?5#v3-* zNEYGI()_}e=JWYVLu-c*!#5*l#iSPym~tQ>0^&EB3&h0F5CKTR<|+vkq(BUudPqwF zI^srw2G}$L7Ks~Em;_E734`d6N|$tY?y-P!R^y!NPGndj#0f+JswgG;MB`|H4Co6E zYN{xP?(o&570Gn`oi$g(bU*(kMRf3jN3GX$Yi)i?c;%+Q8BGiP@-9fP;M|>&m_m+P zgNny6Da2Sbb}+_gPI*23W_7#wEM^QZZ<=4A)ppbH;kHVLn#bXF|E0|D>SHRIJFWBc zW2w3i>?8bsd9}A3gwvO4D3?stJ)LM7%fT0$5MCvA%O}ccZ1^mx+o6@OKOc>n-UqUPJ(Ew$-h>%p4)!QQAVK{fe|d%} zPXdaO3Z6(3N0_n;!R4Ig&lFZ%A7o#kvlK;r5o-Zx!&U{1o!Tohl@0d5=zwhKDmnE#cUvm?c4-AtuyceRYcSrub( zE}j2!_SvbHR2r%eA;{U4yyx*a58v$(42fp_#GepyGw{Qg>F^BF{jp;&3sc;!jHTY0 z{QTsRye}*+ZMsu&+3W$Nu@bAk2tt$ncd@sZEK>plZ+4&fY#oH#5x%wMJn-6RIVk8a zU9b-Iu$!8Cx3!PB+kWyhgSW##vmoRlB0i zywpM;T%a(j*U=Cq9mWGmR zQ>GybdONmF3YA~iX4Y4y{8(z|kiidvKQ~LKipY%bKX+@%pZs>(W|yx$ygBCgZEh^0 zHXU`j-1wgAUSdfKnQ$;)I3{9MeQ@Oq`*~Zmi+ls`uWx2a@gUMsoQ>jRF~HpffS~Jw z({MU~o#Nsk{!mB@PAB|_SrGi-^f3N%inN0l2q;mw01_!Y=-Ma-7Tk zipQn4Z}(1qZ8=!zWHgNUO&;70JWj+VyQ@SB8@H}aJoUF* zlq=nT|4it$Bzf$vLk?`8n=r8?`)O-)du*uYN94bd>*9vi&p_e=zF{6slbP@R9?o2 z!>}g#k6XqMLRQg1r&(LXBkkwoZS#+{OdR}=`8zs^A@JHm<F)Egu>7_1T3JEQ zCYSj;7;L?-a%QN}w8=-|uywu#JKA9{ZTQ#dLTz`D{b*!d))wRe~)Y_rA`s$wwUj=F#%xg+} z{es}umJ5qKi>*NudOxO;{AaFsTfXJWy%tx(qyLK;CClG?^$z0+IuF92)#~f0+hQgY+HG<1;a`-J@1~rJ!Xzm zllR7&HV;2qv<%g?L8a>#E*dl9=pO|)ctj+8nZfNWo^!VpPoJ&mvunrni`;%>b%6f0 zb@Y3O_pi$(3eb7y$CB=APv%;9e(Vf#iVj-A7dFD`ED~yWj&&`z0!cYVeM=rHqbQ5- zDAPBGUEwtoO3PX9q{pOthbJh*ad4vH2EJZ0ct7Fw$nHCqMFuh#p`EX|8boFVI8YQV z5CkY~u7rbATs@u?EdlZa0ycOt61X8fcu7jjnC{-;=EM!`p%gZ7$W^ymZ!NVvVL7io zZ1K~Vi3ttEpUOXizh#AUcIJxL=@0_T93ts)I@t)=&4-bSCJcp7o(*5+97-ENXth@a zkjRd8T%^9OgP_;OYZ@{*V#96jqm$s=MN2=iYj*lySi0Iv5DlEoj_DoS{^$xktl7TJ zfJB${sx{e<)AHm~ohmMOse4(SoAZUSz`*&GcshIN9?Z4K49O*h&zK<5u*X!A(-8|F zS`xTMlO{K3s@H}|dJ?dq$~3Z=Utr(jI7W-JNvf%JSi&AL)BD8^RWo&HHNI$(_m^e$ zvAJydM2>;zgmFC2yqNlkO17Fy{%kLC>0xRp=IG%=d_cN_W7?5Aue-g2(oMgZisuS* zZzs308RJtqL}@bbLl4`O>fKNiGNX6x?f_?X_lcS0gW#sJOnalqAL|HvIZ6l-15pH| z@)&av$?Acr#--a!>rTv%Jwzcb0w-4|n88U5dP6;b-6|EfVF6f$0SjJb`0v{i{i}zE z+x-^)y~(@*$cXvyfQ_jv_x6ew>z1m4qg>T;)=wMz3P#6ao6=*|A9hT$Pwwg=@{krU zumc9vX|8&Uc?s#id^K*!FI+NnKWXbex4lnqCm#FxsznYldk5j3<-Eaz=X;D8Oi&!= z&rppR%E4iM3MT zdc$r5dek<^n#;Q@Ep1h90%j^9h$r_U-ClTp;*&yPy-HQ4E>8uHX>jYCRv123tVy%5 zM4`tk;i>rHFW+{{bVE+U?JY?Bx>#?=NJ|5;_%s!Z=ObUnwmmXl7Go{mO( zeUK-7Lv+KIMrK=A|{1 zHpqniqt@AliWaPg)$JRwncEt){#>F9QZ zJ7E`ouO}j&mW|ZZ&gV!pRBx>p`yFO^tR9I}<>O8WX2(D5t!N2WA zmgtz=#P-Q*`tQH~QoC!e{hE6dE?iUMS2^@vrOKxLWhKUq`{GXN$ZWSBzP5a&@AzvC zxoQ6Mh6Qn=ep?8;$Qm1Y$<#yd^ROD5v6YzHz@Fq6qGx4ur$WT<7TkV+fq9;nM3tUI zh<%POF_QCexG?9XBL0#A+w7PAf`CPpxkyXSS7}%b#(JvjKK_~id!k!5dRFy;g_SJ6 z&Yb98jmCQ0ecn2KeR)?sFBf%s_axZ1-oZltW9`tzy+Y?tJ5^!(Cxp6*{>LV#Na58g zZq>wXzVf(_)o zkGB3>r*{Rblx|Dqavi%Keyb93#Fcv%R;gl2%cn3qyg$56>#Vo#A7IE62DLRS1KlOf z9aAphe(Q&SjBU70N@Pi%%ved1^KM>fsuADNWov?2we#pDnU8w5;39JpyTiN6{=yHJ zss;yD4?eFMJbIA3)v)59bIJB(>3RB3RA^v_Lwnfwt&Ar*6Q95IC%8?bm0PYy2b6s1 zX>{BAG+D_%fg{R|cELR3bvw4M;TiEEa8k#z@@#612ToaA!IfmbJCUK2l~hvi`WV&> z<7`;G1-GNUDm5s2&ZB&B90otQZ=!c`feURB` zoSFu4CZI*=&%TU7+^vC$)>?lw7L z!B`=4^Rj$xVEWKnx-qIexp;r5tzFH|Po>?Bm|WlG;OmI!*ewet`?%K_44Stc%@`y{ z2yMTwNe?6FA$vADY0Q$6?h2uNho+M_-<#PJd~(_qR*MR>f{x~~p|0~d*xkMKL3itB zvu?Cmh6bVYdL4B=yOptCLV*qXuN!4aIgxtdwK1^$m4P9VvhP=JP5uB<=b@puB_&y7 zETT)QtQPjz@4^ow$>(i$hcztv1FZ>d-@0UqY<%|&A8kb}R?iVPR;}@0Zsp?r6nfg$ zm%>w5j^$7&73)qmQqXJ!CFg_>8LQuA@7yEUarDK4?I;tMPte`g}C|>-Wzw8)ZVI z+*2jE;mGEEpo11deHuGjyDO1?iRBBS-Ic9epd>x*gdm)4;-hd`bH=jO>g(Rx=4J6+>~jOJF%|cZc|PS+8C5dkU3JQsJGL(mbAPi{JjAwNS9NW*U$@$zZz!>g2z~X|5ObkyB_B5*GQ05e@BR2ViQKwk<#)r!G1=Ts?!Va<9)YZ)NdE%)f=3ymqc7C-9JBSBg-qi>6FrZ z*G+o|E=T&ync(8WfslqT$CUr&rY%|K-AkZM{ ziTqnTMeVuv_1oGl5<8V%@-X)~<&_G1`~CGUAchU=)$GFG*Y91&h6VZ5_lNN-h^aWF z86xbO+c9WSugV<_wlT|RN4XPSgAXp5DS2pLS@@uIpiqLZn62M@q`~q1DtV7Q>e_NW z!Ct;`E+Is)s^XTOjqe@I73bE)ZF%boZXqvT)Gmf&cTDkfg2I=#nY;UZ!oPj87)KOf z_P6TRUK^6g!4cOpv^uGbGP^GfFcPZq1NM^+wH)8ONA7dlwLi$?43wL;ZbPG2eqH)( zPjm;e_S_sTqvQ_t?rv$_Q_Y;Ym9^9L_q(dtKONiFDpO`~rGD0grRrbD{@Fs3he4gL z_Le`sZYCLjQ9|v`DJ7sE-hZg-xAtAWNA`MWE>CCP{i@M)CA0Zl5lo0*d(QiG1<#Uf z5m(`lHKP^w^|@{5b~afgD`%(e5h>X}=EN-a?CLD!5?mD|8cuDTwYio8aw8A<<&X?|&9D%KssB|#DnZ?v@1D>pVldE?#l z+-B4kVAs;QmqO1&PZp;w>~lOvC2%JpE9tg@OW0}B0KOo5%hsh#eBXDCS3;uM<|HSq zDP8Ep6xISS`JFt`v1mWFYBvX;Xn6JPfsBvED7$06T087$ee<|+=F{QMO#fU$?XPK; z7LCuio8F0)2V3%!Gl!)&6FGizrn4I;nqG8ul9;JT zvh{6KRiATtm@u+EGV)Kt!Qc0WTk@+4#%8X2SY>Q}Ap6x4(d%8l=+%OWgT?CY*<0PA z%G)&`PWlV8_p~}qKdXIhw=TM+XuYKIAPM(S@&2uZH-p<@k#g%EwGUt4P^;>n>)5@! zV=P-@SjU^WgZtGL-94}nB-F0x_h5Rr)*o)Z0N0Oc>a#L5D-yhWk84tAm*{Gc8fO%j z`T!|H3x8Xjdm-ns@V1vrO~!3Cd-;+5NsruyT5=9C`P4wKTDn{{TzxZWCZgSUZfw(? zFWiB=8Se`1{a4j>fJS}-$|W#<xNL#puA^?2`U*f(u;52Sak`NHPY z$=*V;BS)=m>a%2K?3{hb?W$A@qPT~@BAW1PnXzkICiU=F3u)fO!FFTwptH#Mt5DZt zU97!MHU3X+oCj|!x?_^O;YK`JR97Y3J7}JeM%iYUq}#Qts&7{;1@Bf(pxS?2w2M@? z@MoD3tq67}gqYPywJsDWFSzans70J^_iltmG?5E9(P3kqy@w0odiIVA!*^`L+Q(`S zuiq#eE-U}MuVI&~-a;}DP=S3XZ?xwomt9%>rO?lsptd@p&Q5x}^+}9$C6mOJR;MZy zzCUJWv>B>s<}ZI(nE^-0Sj?(!w=2xJv0F(v_=l&EVBJ@n1$B$#8cc-XJ#gg)9ebrq z7=bMvYY}GQj~2;otPi|=H_`|m>8OUneYlS-;$f!2R z;tU<&3r+Nwuygm22%f%YaKkDOHRI9NN#P1Yisdud+?AMMJg>Z5*l(^qAIau}k>CCy zRaxs#sv1-fs(U1Bw}_R?z3Vf?(ddnlk5k7Mull{5wKZPYceLs9PHkOrzEDChbuv|f zL`>x{{(_-?8V>I*x$T+e=l#Tg3TfL;m?!(z{<=^f6e9m-!+8cn4*knr%H$zW&=Kmi zHE70WXLvj#A1|qpsh&-=wB1^W*~+ozRQ_7g{rEY*7sDkX5(JjWYf(E-M(^C z!%kG~m#|h39&wiQ75L5JFX=CY32=>T(2{%+ttD7u*wueE7+gsmXsxw|iXSJO^ymw#YS4ZLKol?v2vccum z!_%<9OEN`IxP%%djXLaqIs9rnA!KRrgl>;!Ta&U^a_g>_X|t7!Kg(OQ4H(Whnf9~$ zN3ANq?$4rUaI?9@(J7piHVHMVe(Jy8c0YS(bu)02OyGHLiK^(^HR+enzILxR2fae@ zEfo}B^GqzB>~+{l`9q)?8P-6aD8NfE%vCnJudW}--{&wVwpz?1lsGnhN{WZ(d<)cs zSk6B&YTnQ*hRT2Pth&zcX2e6?Vo)FUXYblD}qB>>4!&o9?XIP3Vhegd2s91yu| zhHPxMkU#NAbJ*|+FDQ+7Pa|BgYc};Q>Gq3pt8yiV&yKWs5?)!?m)fogT+;LHt}0S6 zsvEWWk|f#xkU?;3>{ThZ=;Qp#i)DY_&cl;}Q}kS)Bvq`#g0gej_6j)l{!pIXGfi*h zv$FC_pW-xNXZn!cY6#b0T)yF(du-(oe~yqoP2o2f=l#?6Q|I%X<_?>p{#r--o{d{K zZ}H?ZMr?gQIeI_p5yHT8f-H&aD%YwW7Gb-k8$Q~K&Qw88=p{C6ta}a5q}kN_TWGhnchdDQRvR=>);L<)^JS}c0*fB!a*WQIm4dC7U@ZsEAHgs^3TM<%SgxLpY2o=K8&KQ?tc zk+g23=bU1cskH_0?OWLazy?UJpc51rRIYC{j}eTae6a{@Y0=Zcl~r>d!&}cMH=9_% znx=={3}J@qB?R-f`$<}Mn;~`^X)UT+D_OcJCU=Cj+Rqc%-)Bo|IdV9-PVS1A8PK)! z+w!cjeG6W+PH|LF?iV!DZwRnpE6L9NzNIZag6Wg4FuYh@YkJ7BP#+TE$a`2ZEWNO* z>{v}E$X~UMBXQo%YMBiCVnyD)ZVs}`gE8d%QWeJ%fQ?g$@eyU(>Qqy+) zEc{B)-_i|Ll1(^er0cW~_P&ztN$q55&c-NL!`+ID5(i_7mnTAA%Hx_Eq=k~Dui{*d zYFJFgP1%IS%sp7d3Ls*-?t`sECH65Gr)yyYBz!=K(j}$K#=S|s#b+(tq##z1??r+j zpC8)?7SPVpl|g1N5vJ6H`qr2Q-c)46p!u-Dg7Ng0KfM%Ee#T4++2)IEZ8j&`N{_i# ziD+#Uo(S9RTl&Rv8{QK(O2i}!kKii_JGy~sLh*i6S=(u?RTFg8&6qm~9y?1>dhgh@ zjotmPYfV(agQ7!uSOmS!A4RHteiOKRk)mX~-HZdtBd#Bvf64ei|I1vc?M@=EI@t&r zARZnt^Ppt8I_gOKR?WCjoMTw7H;Z@g4ivyf=* zlv?Z?gOH)NY&nyd58jWBpwspOnci;a-zR>*8|ONIevKE3Sr4RZjrVn;v5#RzWK8%O z6n`XTm;{$iwr9Z2#8G=JWex|o{wf>Y95`B18+R#fzZ#m~WP9z+;=9d2B=-FX!395q zTWEPVxcIgMl^uBtcNo~UWp~M{%TcLLVVE>OWOt-{nSA(lV5)5zr|g?iX1BdMbJ;5Y zwYB*8+QG?b#9l|0&7tAI@>s2vvUP=+z|hNdWMyT%FLG#@O)F&To)0 z;|E{=$tWwutpM_3cKoQ%Wj<;IBxiPwVF{pbl{ev6qBXG?lEht0AnRteh_7<_7H|cv zvR%l|{a83-H`v+`Ado81ebBYb4U>kHXiuc}$WDI^d~{ zWf3l?+4>;pJxa!k&YwJoEK;!Y;j##|j($MDte5EjGial)s*A9*G0&yqpZDjjtoeK^ z2S2;h0F~d8I5~E_G+tGD%&qYd)$u(w{oqm>zG2&K5E;~9)g3sQ(m~&UM~mj1@j0e* zX~uMHFd-^UzLd+7F0OnHZKF1ty(1#and7ozQHds*(OP1SS3S8CKnDQ=&jgW95d1Rm zd>#8U*Abz`05776t8Z=Rh}LppM;Z@GL4n7Wu^HwhfEc5H&`pKd0d~dw`;OU3WD| z64Yl=1KyCkwIyWxrHFJXL`ugzGxN=kI5B>{Y!>erQ6|wC`f?gk0f+&;Zc4nkt8+JA zi81j$*9OOzUE*}!Dr-Rj2U`z#Q+m-EvCyXG1St^m9#*SE%t-AsMpmP}(So{Q(Z#NL=&iZP!X7A~K$$ZDw6Q_ztvvmVvmuaq`aO}|(WM3o_W-i4|}tV|;Pi^b~FeG)9< zUkK^sCz4nBAkyuh`>zv%2Mqtpi{y@<&wruldW0hTDH?0jZb7;3DJW>|*-MzzxC8aK z_EUVPI(o<3dUoaLTls^|8yWE6Z{FnrUyAN}o0ccV8}v^3I~-rha=A&I1RF28D7NOM#dotrU zBI4cMjKVil`39LUxt?>?4*;)VDW}Nui`}ew#xnZis>R_`>H>Q|>{_x6Whli#!x9egv zyPe4HNZrO^)jG)rA5~s>sDVu0N%Qn^Rxqm5$XmUN9kZOThqRKZBH{%)@3S#XWnXi| zc_pOJW`7}96i4YYz2t>y)9+ytKpaO#7Q63Y{rhG<{7aGU)h ztUE8}X{A|X>@CKW@1SWv8XpBZOk4=w?eyEtyR{&1>``e)^2|H8+*cNQdKZUtjQEsuL;8sq0)v z9?WR{qhsQDqxN)F@L7{gSw+k+>kSj+gy>zkO3ks`>*o%(F@zv|LF~vjtdiuG;Yz|8 z3Rmy1B{kh=GgiCu4CY~|%R(e{gv0c7MJ=!fs@S2n`(i6sJAz^611oSIp_XM@C$bH@w2KRNTCZFseBJ7jL{0xuC&4 zBq$Rb1nQD2X!5}~p@daY_Q536mN;BMK9wT}@!0&)-5%&oewthxxr+h<`U%`|c!3oi zyO&}%kTj1A0${Vk=S)w}D!{@l!OYOp;L4lIr?bL9k7?7n1@fTm1dosOK=pRvz1OPk ze+$E#7g+le-#<6 zIDrERmIB;5`p+!+XjlYTn38%E4T^Q&F~Rx3iH|7bx9sao+zE*UR9^Ne1@}|3gnC(S64Phy?B@(>$aDG;1^7T>u@pkNPDoRucy|-nUo1!o(y#7X`m2@5Yv27#@`O%ILnarp2NTd5Xn;$ zIHFCYfYd%}q_}rHKTQmSK#q~0e%dF|oX6HeqW*yrW*2Z#)l#FSH40LG?R+)yc4RMe zWW|Ha{RqqhbAZp%}ntWy{qlZ>@^M-J&&HgWf@-%O~C!t=43>I^VDM;S%kEi_|hqe@Do=jSVq zI;UfAAeDlMgR(O9(Z{7HHcW1IxpktxTw9#D7N8)kTNEH}B489}lpv^E9V3{lWg=b7 z$Hm;pk|t>IAbT*GXV}dIL97ft@9!;vO|?nb869e2IY`1x7A^*k_Paloxgnx6`Of{3 zKr`pGIGVFbWzwsxPBlJz#7*LSrQ*TdMYcxw+3M~_QiW~HLw_6ek%wIi(stC3Yd9dc zWXwTZ7_FIS~r_ulrX>%&9uE$4twkwnGv(LiR}5kh`z=-1%N)bJp~1 zU#OdcrLFi97c83J-jCEdjjc)B0dm*@Zhy;IH;iTxlhP zHy@b%d5dH_jA}q%sE^QHOuk`s7-VYXS(-=~J@}NJBTG-4-5Sru75~y$6LJoG$o?Et z?szsWgq}MALIs7;K8h=aYM$c|rq=A)adovW<6c5P&u^&}{(N6nnAtBrk)vNg$1aQ0 zz)i+zZRp6Tw@5E5a3_z5F16Y2JE(emZORL0n`h?c1&~N8s^| z>w6Q8bF=pEFG2Q;82N?;TSu^BiLAFSX0OFo6H>4&4c`s9n&T!Lhw#?5S=l2N$A(sp z;55c5SHp6DsAf#-iH5)B!ARifa)u>l=RVim)$7J>)rA8yu2{EJKEV~Njxkk9s?9;Q z_emP^_^>VbHX|RTaW5qJIS;%`jMi9_a03in&izCsd22tpN=tCt;R zqzSnIfe306qnM~PAv9dH5GeR>3qTTzmj$haIJ1~lSJ?;REthnEj#8kp#IuFA@ODx(D{O}r zE?_6zb7bf2)!J-=&>07H!`6B7%iujKaz3NNfQNy9yLaUZka6D~)8fITQSeE7r zxUaHB7&XE=rdm(X8Cs_a(Zzg{GGuL0#TR}L+I;RlT<@Y_Y4)0TyTHQSE7|(89<(+f zrE+9XCf%G+Fq%@C`xWn7Z|8kY52mfYW$szNW-*E7a`X4i<;RxVWLS!>;H_@p)|HYt za|s`vOX&3kAmb(Av(yQibO4GhjZDz;N7>*J9YhW?u5<}5qVElIMEr!dfGiEvh>KPj zgLQM7Ic;Baqu11r~h32Tq%&EF+DB-j>gpuS&se zvxnC*;A);ouBGtI2{*N#GP`MM(dI3qnHFp$E|*gxeRD+tM?g(`*p~ySqHeO$-8riq48Flv=F05k5YPcUEu&QNl^K!x0YSVMWTleO@tf!8;P4h;w zkZPB{9uHSC5BbW|&Y7ur=$TmHu;jIh={mNOg|<23?+#(#X2t7GLGUA`v6 zYmU+*vm4T-O%?a^o=QaY<$Y2%Iwz`SilsKEgG51SKm#)?;%Hw&Gz}n80HJ&^2>1k_ zzAkK%a~LN}4&5cccizrDzHQ9469zU0~NcUz+yk@lSkK4Ek`uF#7 zui(}!gXJ+8`WN~p&5q-1Uc~n2Y%0M!*R5r2c(UV3=1RDXpq-3*lM6+GWbvIY*wqka z?K?=W(F3A~l~$A6@y9%57biJsJLCFg4Wm{|`~<1D*Q%APP6U6Oq3W-o`}uO4$yJE-~kBv-A9pWV7yP4*o!ykBFhDy)<%z5ZsTEByyE z2KG66QER%3)}A^T5cQZ0cA5T@r0U(!Kd0+$dy>lQU=#!GOdR2U{p zlLUK7t*xom!ZI9kQ0CFR6|vpk-%@72ymn;p>x=50ra2a@nb%NtVu`u!#cM;qI#rg% z1??ZIki)%df3;b+k8h9V8g!=VR<9?u{d^E38H-o*BuUM9;4^HU3UH2+gj-w}@Iu(b zHS<-IZ%uB28y45>>Gi^h_Neuw#)YVg_^47P77OLO@bpz=PrU0!eB@2nJ3eB@f?vu+VjdO^A!-s~i^0KJ zqu9nTS#%z+yxtj=2r`Pxj*G5*WQEX7(ELAC{dH6u&-Xu$Z$fa_;zf!F*CH+M4uPP> zEw~jaZLr|(P+HtII23n>;tmB`TA)ZNROv6z*XRBH<9E;5v)Mh_&7C`wdml4b28iay zals}A7LtGwfC7oZ=>JD}0I@hA58$wH19q&P-F9~L3dQEMU|Nu)+gwb=)vCSqk8fLl zUhXb;(Hw?+sS&T3k)S_L_x;=`m*!BL{y}_gDWA7cN>e32I3q{?wNZ{ki1S-hy$7D~ zqAxeWl}^w7ie`=%3CwKG>q%wH`?eS68ao)eTC)<2Jri41w(5+02-(eQx{lLU4ZS%W zO{T$y>>NCHoXG~6twRR89)+TFm2_k9$t@?x2K;yW z8jcGM!X6Ip-t1MIft+vLRm0nO(`h`QYusIq5AgWsYKxP;wbb1tTgCKzR$D*3B$o{^5d=#u#*WDuEAx5XFV0`w?g->B=8vUS%TXB=bV%nhk=4}} zu7^GT+4)QMNit)J!NowPqK+nfo_^!5e7V#C1$1evBxniIvHUQ{jtp5U*JO%(z{+~dA{|X$@V-6SEa3to0O%bcEtpvfuL5UFo zuOV%wa#}b0&W6EU$vMUeOv^ZbUyCQ*9nYWod{q|iob$ABL&dpqz-Hf17i+*CyK7F= zf6Q(vrHQfJw)*k?)2H39uS(|~s>KTf_uq(DuT?TceAdeG43l$xZ5Hr&bJS$-*< z_}ZvzDHa({S1~>uHMZa#aXaeuplf1 zKo*5CO4zRseR|y^twr2oDmAJ9A*#gWKIct}+^?nS&xcz*)(n32BGZPSgZ<4M=jK8i z3=Kzh32990_Q1YFwf2@%-H3i>_kd5IC)?vrKdMOX%o*N#ehYNAPPPtw=ha4{vo*T` z?J*))W3rc%`T2V^`)D;vpj3LnxzDW%lc1h>=wKe5oi)K2XHUr6mHpb+%Iurdu*bVM z(Jtw{E7<&PgK!Q){@`h`u^j&91l31n4q7T+R%>V84@K{to#LD`J9s(GJV+$0=0qD-?tPrCOZw#+8BVR{R_4pw z=HNSq=Gy2by5P1O`!&XZjp?N~_*QGHQe0|0*48K;&D&3T?TIU6`Ngn&-g?WdcfZF~ zY3sA^Ugs!HnPj~g(&a2+ln*ZT5FVykfC8GEBn`=urW#|fV|1>)lD_L$z$`eNu&e@M zJm9nfNy9;750s&pDH%H0EI@&q#YcW&L<~S>;2`Pcbrg!U|JQy%`~Sc37(h1w{7-58 z2Tm|pj~G)93u9XB>;OA|j3p)}k{1Grfw%{8X(-~MJz}kE1V=in#uGh{lo?{x|K}q~ z-!SCAs)F)D+t14U+q`!rKRX}Kc&%<7zVT|v$=;iS`0|2x!)4%Fz2YpF$e*d9XBP!v?@(?j#)Wc3ap$N41TAE9)QJ3+9xT%KYa_7_t zfQMhj;-W+ef~2BY$wpl;Y=W@JFr7+CHUJRl+T)Ua|>C(2KX$PMIzb=1DgnHsrSvb zg*c`)hC8%*U>vqw)(8~aYCg^*8x$ubH6ND-=}-ie)$pgX#zHU|;HVW&AYbe~5)6uk z2#Wz=dp3+{fdL&XGR$8LvqsC34&bn#fc!tOvqZMAFn|X+`5(6! zv14@Ae~bc{{Bi*J&+y~9@DwQ&;bg^PRCuC_=9HFTmSF{#Jg|_if#gVs*n#8L;<4sR zHt>s83o&342lFgQ2vSFjBH>pjuCqu*KKg>6rJhHnD{Pamiln#szzv9Dku4x?0Q`~E zTu!iyi#$d>L}Dxng(5AUouT4uI6j3&A}ts)h+!UJp?5>FVAAX`L*;*>0{|in9Q%I+ z0ZdRVm|{%-(a6$&>h?c|U0lfHVqH~{F0^<6Ww=TAaW$)uO9dwYl+vh{=Px)R0Zg^C z1^B|gf;qA=0w@+cj5b65*YTht02Br2WB9@h z3=C@k9|$vNL;^UN8{A+(9|8bWLdxJoOyvns#3L-5$2hamvG^23Zh)o`831nm4|8F@ z0{{yE7(`1Y6a{X=$N}?qC>7uWLgwN@TtE>O0Dy#G0rGfYb$lTzc^iBH$$)u;0(1Ol zb8x6ql`tm^VFu`c{6kDVK2FTL|059g|5tkt1O~7m}CBA22__EYknJwc47gGe39HrRU*E^cDG^?|dcQS1@SppBU<9UYf-Citg*uaxDk; zpR3(#2`P!yU+k6mYqq`fo2AH@M-T3OIb30^@{H%PtZ$%Be)vskxD0#WkYH15IX7Vc z&Hk{A_h42Od*in@Y(C|N?iD$D>H<;uTcI&4+7V0~W*r1CJZy%m!)_;m;l;N3X#Z47C;q3QR7_CC%N>UX|cA$xGJ=oh~cT<-R=` z!UOZxs@-sGK3oVv|HJf&0;Gs+vABSvAoOXGVUB5_geqtMt2I9IQnj$lR|+L%BeY$5 ziJNnSYnM#bCZ-b2hoOJ5$lb-w9N~#cPn1Z1ki`dn^LtgaXx8PCU+qdQSIO4DuQA+l z;=i=U%JXv0Eo-30_iE=A60dpwgw-&!q0B5Nk=$fg_=lpZHGAbH@cb8!+`|v6a)F?L zx+Xq6a0gL8>$4kTgE;fFG@32*_-N)Zyp?1KzsS|j)3=Alb=O0Prtib%rL)GHrxT(w zv?-se4Ridbrur(jM-u23ur_H5gJ(xD{Z><5`TT{QfYbH++YJ-PDrX}K(C4naSha5< z)wTG-XN~++w4+1~_An<7>IxNsO9k<1)lWw}_b%Y_XN(Eew)4(o)LqxXux7!D-t%&o z;}7536XtZ>`)Ax52NQ9zJ9dB1wmUYc{iq+oI=o7CC}E4&gBaRGJZ9Ap`Q!ZT_)Avp zT5N#0cp-nk{HtQ9`a^ol3Ip(!-?BENr+>j)R=w-xikdqA_S=!E-9L@?5{|nTwdxt; zu5}-d+RbuZ7sPsN6NvbMDDTs-S5r~SYz_{1O>%TH>kx;12zy1gwSI3~#br7Ay3wU7 zf;HT!qH})yo}s9hrqtk}mw8?2tF5q-VXQxng_+EPrb&1<=#?{Gwu}U4AG{UA+;t3u)e>)TCUU#w>qu*!0_t&J(#-hi*a^R@0nVm-oW^P_t7*AXA$2yHm zP&0&}UG8b09LaxG7fvn0HvZ%3?ShWW#syQ{Zn(yal)Sc}fEw8ldjt%S2Qj9yeE3y2 zcCXG6t$R~8*PS4Qj%zUS&CpjxwZN<0&;xB2I&SAsba@K=6T((DIp8Htf0}4BMcXL| zs>y(GF6BedN|QB>1=#cEtkJy``Rz#u%Ltlim}A<4=0lAxoSvw1UE`p{LljFnn2Vj3 z$;$1XrKD$*d)TAM20ShHb8p3aUD>1CDQ->OQolR$e|G)O>2Q$tCvd{%Ad8x~acvwH z7V+bX<}gnBoXb?T5SNI)hi4>YFw0xVyWQ$1_TF@gto9!Bg6?jkwW{D%Z$?qK5G_?i zw?3)6GoiJlliNoA^k8&SoJWGKah0$c+695G3Vl{<+}2Mku0YQh^1b&}VvEV#scnA# z2HzkF9y}`XRa(QRGETc2kbAaTL$=?lxuNhz&QZ%@ernBFCW>mS&oqPHdF7AIdW~`X z>1uZ}&HJ83>{B1U7S{8Q2hwBRA0#*r3EV#$BizMgqy?;M%6-;p9CAn##`6F($%~A* z-}=;ceL|+{Sg^(Qj~x9koz5FNk8KM>1}y>RorrwRs%lZFmbsN3JgSP2zx%UeCIf#K zqaEn6sj;WKZ!>RO!wXss8WmYuIk92hx0~_}rveS)D6CqhdQ0JGGdFG0I|aN&4?>x2 z6G|rw-UtgSLKD}583m+P^XNER4-Fg~dE9_+Sq7bJdhKloF>?&mOG&KR0K164$+z!u zWB<+~oyJ$U%=-@z{3)yZR;$GfvMv)eC~QER+qR=XD^(wNuhw{O%uU^8D)~gkgumUv z$T>Cly4Xg4ng`*I=A*52dVprRsA$ry*LORhR?e17iYNc9Krh3;-fTY7&!Lk>*ZGL{ zo5kd_W#9|-SV@~vks*`2;uF*;tPyq))DG2Gypo@p5~UEeKZ29?^_53e!M|^){akI< zFC8^pjfOP2Mxa#VXB}&*-noxT3e;wI+0v8MVB5EtXB}&*OAR;YiSMPvNFS$=@OD>% zrQi#*x0B^dH&M!i{{W`8>o}Rf#H{zo2aaLzc0oBVvm=5L5{bEY>6I`J+J6A6l{o)n z|AVU5Lo?}se5}fZN~_TI!-CouuQ+sZ-F9*-R%wgFlk8;p>L(R{zA<$GZB7$Da!u{=qaiB z#G@Qfroh?BDSFAR5h*n8>ckpifAAKr+C`a&W^4os%`JUIo?EhJR2hg-r`D2)R|jgD z!6NW3)y8-!#2?iZEgsftvA~jKp20N9-rlqoZ&2}v@NJa)u%$2?nUv5a)6n3zSX;d% zhuE~R{i+i#M$Jk>n~Bg-;~HtEtAvN_*Gk#U0^PT}I(xO~i z0+0O7Ux+Pgm+iV(xL3@?3yhAAYF1@y84h`L9KAX@{1f3I6!3mPgT~U;-Qp+NTy#j0 ze>EtMo{*WK4%l^taX7}oYz-NRt;{#<9#NapIGVYr6E88deXX!lp=~P)u3P@GRZKoA zNMyfWW9q1*=UmF+QThenkS-*4FWalQoLV8Y%rr+i+j_)uwSl5#S^2HXkUiG{B+Q;Bay!Oqt1wrRmNV@~!; zm5U1r!OiRU@Vt73uX32tu^4*p+6N)GZ_?t>AJr#9LnY`6gvg7>D``jF1ADkHrC4j+ zDCrEP9-C}g-K)}9IFy^9^C^lLqRDhD;N|++FqN{f|a-e-QK>Vd+r>Yn=Crkm4luncaBEnoj_U z;C2b};|~j}HESc}vuxnC{Fa=^e3lsTkwkoKyrK@oTukR`U9L`pLwd+ai+6yUnRvww zwO(s)bNlRMSCdFZJL8Tu*`lA=MNpe$5P_#XSv3lDTJ&*}OG>`~Y3BIZ4~p;+yr$V& zbp>}gR%uyTF=61eZ`(g0HD#LAe&BYy{fmfS%jPHzNtP<#K!h0C<>C*0dm=ljkw)H8 zx@Rm-JL+)ON6ial^1X1;4PPcvL7FL*vPMdDJ|ZJ zn8-i(Sl0alVQ62pU$f7(kw6r*bw!@yWZqa9V1V21E0NL*>n64dzt1RmW>2(UxbfRv zC-$8%=claE_uN~{fEiW}IA73<tQpv)68?s-L-) z8hA4I29%VAG_P}P%+7~(#Yp~D;;-KvdMVLE!h)8ndq(XZ+be!z%Z3*_kR{~pXwrKt zH6el55N)N75@W3EIz`9Y9s{P2Hp|lX8(H9G9R>cwV%SG4_z(jJAtfU3dfG^OvDHjw zaC&7;Y{K@2a?^YwK9EA>VE7mx;aDtQ-wbxJcfvb78WMS`RXe7IO`f!{5x;S}zfM5!Gtvuq9OAgwDKLh2oP@`FNNkIbM>hYjJj)Cihck zMK|<4rG@|G<4YccjTx`z=AMde-w^LoW07k*R6kFEkQY(JqeU%?Te$D7NyW|8(w1thSWjy=Bju6`?|q``vMWOnsEX;)+Q|lsR8*Ra>55T?_}Yu zD4M9=ow@=_Tf3MDGj7Y;W)_NZ0>^MZ6|`-V7AF(>5=d6wlAKVBfF$qR&Q~v=)7RN1b*1MjuyCf0q*@l_{3a*A{7p3&T zl>V5_IWE;GM?;q8@vX-`a*&7|c1YBrVo(Oj5_YQN-Pu1VAs<^6#OVxk(YIe&$>EO$3C*Ny^191Yag!xxsCdT(48)!_Dkq;zkv>>UWeAsG z$^E{`*Y&!-HP%L-mFa~x1s!@c(ZDu5|3QSOYu?UC#XuG3jbqZrZmap9<$2Q7qx$}n zN6VkLkQ2V8nv1-KtW6!hl90woXh`9?GCP|ol_b58ss&f7bkzeZctty+u52O4;t5Z6 z7>dWn0EWT~ronR$rE{y;aO@0pRDKy)))u(M-J8Qs-=X=9Yy9Y_UeCy6v@Rt%3f&P3 zPDihBwNd;q?+~K3LD610<%+UfPSW(emd{d+pqOct29z7bl{CdO%yuZwr}K$KeYOLU-9b_ zzGvDE`{eYSJImfwWISsI{2b>kFvFF)4t_hf?3Txw>1l62z-y;A z4tb|CM++zxzLrc))vIhI*s&d!VSi{fbBttLsp4NXAIhG_cp)1~S-O6NhD5gDQ8g-^sSPW^H=^dM~ zuW))Rl*a!ILGG+&`}8D~$&=AGwPDjeg4_Qh9>iYGkJ0&4H5KUi0HMvXp6Y< zO27ue#FSurV!<^^hcn!^t)uDV)Hj~?Qw#R&>dc?H#FXc=nTKcA1$HCa-hkbBHP)$*Gi)gpR`lyzA5+C{z zpqjr{oL$(om|#WS1{af?%jNf!Z-z2jS-Hi%?&m4I$~=ls^D8*=XUhSbIz3U+X+?J_ z^j@nCQb2RnES$f@QA(hJd-TKmHmvh`ZLp+w;SaE0sK+ zKv>o{ZiF=oN7U71jm?GQE>o=>-a<%krh3_pav;)lHY*6{nD>?>LiforoDUWlW zlN$)Xr_Sv-UeDU!d+Y0H|0j!PCgv%B)^(TJkS$}C`6wlpSc|BVjJ<`brXz#NUb;4+ zNKJF|s2}HRSa&&u+(uX{8aZUhBhnTCi1JPo8Mb9X?Y;R<$zh=hqkVc@-aA>*2_DZK z^th7Ff(>&^OJ5Jy>s*o&IzC4+M@5Xaa%QZBZSh=41lHs84<-;9NyKwE1PyI#o{pGc zcCOIaFETWyJrYi^tgwKFAn>qnvs{9V8`!jVyLNaHoLBE>oKiyD`Tdo~l&0p3a%4uv z%}0U=Q9NNC1zN?yc7IHC1H*D~n#;KjuV+e@`dPZ5+r@P+I6LoVcvQvnG!nc*=?k#_ z0fo!4wEW??e%R5?Xc0l7yh!j^BYxc8zZPAzmwrflF%rTHixqnmd%g5 zwih2>~tl4S3wYN9r!KbjGiiwWfyd1k#9Cjof0N(a58yD zQ^*yXO)kk!BA;$ED`-A-*-+&t$*&Dxe2VZi=QO;iWQ8vaGa)`Fh*ukN6vMnRIfYTe z*+*uf%I1gJ`=f|DHBr5k`ebFYF-!Jf8|Tptm1k^~NH3}NwY=mX{2u0A+)2!5ueX*C zUlT=Pe-2_xUvg}(VU0v3K==u)7AR3XP%+tZp@Vy3EhK zT{YhDv`|d5@VMqzmsj%N3G(rfKwOM^vYNEr(5I}WAt2}r=ZQcZ8|FPOe{jtimB6aG zuz5kVpJwLdWc!BkSitm4nWI%@S!R<*yN9DGOqnSb9LY7+cLfky<0|WNyidI*k42gh zvCp!3h_;yv)WbjaJl`l3w{okiYBw$hP*W?pZRHWGZkmTZo2wLjRd7BUc`Q#TFdI7~ zCDMI)I%QzO0bgaYR~{v$uBoOS9<$uD*;P>~9C{sJdg8)m6td7k*^ei+odE1?q;Eni^giJ6fs5pD5Nhc5O(T%m!7+MIZ)W|*kU1p=nOL>jOs z?wm84HFSdcT_)zJv@M!(Ar<;vRVCihYU;xgrzQ*gH}rN-Jf2dqhZuxfYX4Zwu+Pt+ zFu$!Er+Z+QMI*VeM$8k?%o^a!ER`?h(}%y!=;cIEF$2DSirstha`SXy6~RNqTo8%m zc2Y84Ek;6C46NeXrjm^CafX$j?Jcp>p94^Myc5!mk(!ZD^%F| znD_N(js^X4)2C&ur3`~8VF_M8rlEdc9@D_+Sw=YxlZ8oiris)?bS~1w29$>So+!k_ z#q6g0jV2BqzJfI(6XYL8?cViXgznynA;gCW2E-m{0_#ob5NBNs#m( zv24S9o#oO_QE7iBEn*|f6Dn+f8B9h@g3F25z;EFW?T&b;G&fAMJF>ky*dFjl;{Wl{ zds4;p;-ygjX+s$zEbh^OSv6`kkd!Z{CgOuJmJ+b5vs)anadx1(5zZA(WNt~-T%17r z&^XVt(ao;k(JS2~f|gnbRZDmaE%_DkPGn`=p~X3JNViT(U4vFD)0 z$dM*Dlvs2Z<$~>k)I(uDX|6lMG}FSqc{?GS&m~4P$HYw&V4d?R=)3nY{YH}yweb(19O>4*xeUW<-5xg7<=#0Q7LWBa`6BAA+ZB;fpRXls&FnvN z6lvBtJZgBuYtx|P>E4h(CFb;4kV05Cu47+mB9Y~{(wUTyck|pofCFO|lQ8C`dpsK3 z7KrW<9V9eEw7L4~E+~Op6b3zF^}x_EI^6i?(OSie101zhscB>@80Kem|mHcgXJc<}Z6fXvfUFsa9>`6qeHech-40@X?@ zP=%sI@Sd+;Rn%n_2W%zL-EiPl;$ur_NzGKAx%i^=8D5}GZi-ke-gbg(* z1N*bgPbl7|x+j=bHBn&2G~bfCHE3xqubNOhk4w6_FEWX7P1=#!!Oh?gZYZkX`Gp!V zJ4+K_C6>g0UZr!cIEFPHus#i8eOi9u@R!)D@b^ivK5PEkBbYV*PX>>VrGm1_6!;1c z*enuP78xi|#H85fj^OQ}fu`Ih&10WqU<|V?F;#y3)3~(^ZA>rf2mj(!lKnI=c<-}J z!QGRhFhy~^{59&s?)nb7$avbV)UZh0v5p7OkKOdlBf0HRJE_Se<|c~f(RZdq@25#$ zI6e;)Y3Yz$ZR9ga4pVquH+s+y&^^0J`b#-c@SE5TyUILZIJp}$UsHc5y%Cowz}8{c z*h!pV6aS8Oe`*|_Rw^#+Se6y_-uG12*cbYwJ9p*)uEcPudGHT#S~QUCoLUNTv4tSg zh;SDaGE)4~Z>%gXopg!UKV=YsO^G_#Yt@XaN+$Ed9t{KAFX|O~1o7$Jna5=C^++M2RHB+g zV<=`dLh_6#r~P{tau=`3(`N~LOYDjW^V51V<3=kGb-9PN;*M3M*wG%G9K6y2HHXR? zt8-{qcN#q-Vcqv`wx5w`4rae%8*RBNO%?z7*RP>BfF>BE)a?sJRefF-4}eE&b5d|P zrWB+`fLI_LtlW5U!hE56d)~wFnPdE$q4`H6zd#Hz7kL z7S5?#-PMo$Rt5ac_>Y?Cp)h~yoWhsHX3wtJlkRinl-#47W!gD}#*8Ha{HwB zAZ66U78)h3BwRm1`bBE2wwATJmfQBA;;-agQNk-6xwa}NB$o0+8)@p*wjWGk6EetK zUi7e$aCf`@LM+>=ae?Mk+Jlm6ChK?cCQ`PNDtYWn3=DrJ&6J)c)duh`v^;INyPG=b zCwaVDwEqhs55hYYUu)DYSN~2%(!I8}yhy*cfl=}!!gd06S-Pv)RS96eN4nBMG|?~s zzh0<~zbvVF&tn!rAHPU-`H?)h=h54d`l5Rv1oXLHL42*!3LAe}&;$bDT1Tt9go%M0 zVugO;h3BZPsTX61nTc&bR1mKZ^FVRA?ZjV3byp?~tyd=nz&}&jb)`7O}NZM1g0XOtTs&=E24+(&-y!HoT~41 zqxFiy_2tE2k&40OPKi(LJLv=<;q)dnkR{?Fn$&Is@o6;#6g^C|&1>q<3*p?ox4YM# zNtpoi6W`e*fz=o}#qi~lMk_ZG^8gs%n*Mlnz*?0)L^iGhM9pe9L(6Q;Lby~NwLWrM zy{|c3_?IkbPx{2^1A4CsqF(7&W&Mu!omVJy9wB2k#9Hy%zvRrcm}L6-*HrFr@`4_ z*=_Z%^2Yl4`KQYyxLin^$RuCeM2+3wA#devkneUkD#{W1@ON}Jtzw4dmAYljdbK`% z0Yf#mjL)#ef`|o^YqK*Ac`s`|8-wXRp}R92d3kwz>U^3@mUMP)CMM=v8ZqD0C+@rN zR*|O*Tq;$@DB97|A%(~^@#Po>GGdS9d^eV>=qfyt$)9 zg)vMF4vw)pJZY;xwV9^2(h4TFvKj8qq;9P&OJbm+So2w5+i0u9alP03vLU^^#tNbf`9`4?RBdHE}^$ZKL>22HlzvHigkPa zagfI5L%hlIPhyf>P}m_&XwIiU|1k5H*52`NilQEQYj{M0wuOe2yxIr%93rM4y4@>x zc_LoadId@pCg|KNez8RfRz*C5Q|o!;T1-IM8<}FT-&HL7b=Z2PxnDnDkMQz-8kX%G z8rs|O@jjkte5Ph_kUpPq#Q>(-%5vy%yN5$UB&tkx@}QJIZL6~t&K7Y_gssgw)?PN- z+%$Z`HYQ_1n6nLMlofRwE$mzaKR?r)+d6NiFsY`B)yz&FU0HoU`a8yyKdjHlre$)c zpnIKRLcl^6wI|$x?(hq4>2TlbdRPkc6ma^B2}FzV3dIe*VXj~f38%ND!z(bo9%||I zV(plkU44GG_;_(&EzvIHgb0oYEsar&5k5mYVnTVz7HRpQ@eq)6cl!V_OJ>sLb;Y4nuQV&wi4ww`Qlmm z?6#A89w?{XjTp$qw$4>CNcg@p{?t}(G;wUmhP5Z>e5=E5Cqumk zoXI5y*`Bc?V;}D^{8qXlV)S~YtTb1WVPs7AcJF1uUl#g?p-Go^uolIZ*h2$p4=(GW{T7(RUSwKEse(Kn@ zF4e-?Z%VF1H&Fie~-KwvKd=aXYtAqjkva zZ}I`5Di(ikV-h09TpQ+s-QQkkY7Oi6&0RX5gA053SC09}v}p**L|Gl8dzw_alyxtZ zmczbZG;MM6M39XZWJR6fO_vVdVD{XFCq?to3t~m!w)f@Tl6Q%QZoS2bZ7tR5mdjy6 z8&$x|;PBHJt>Tf&5JDg&W)?NPW+i=Uj*(7O>|E%q60i;Eh@Ip14Mz9i1vwf>5PGYN zw$E4J$$rHP-Z)BsWwA$auX8J#V9Hi;%|jywvv8`kg*+1Faxn`ZlS;-gj*HRjg)iJw z!5k&*uBa#+H;*_*%84VU2-8r}5K)W0qWqYojLBcfi%8o0-ae|i@udJqz5v7hK^Ytu zJl6(VzFPdMAHR;M1B^>6QRtfu{X%vB@Sec@_cV&)L}V#Cm%6LNlD#>Yb(&Ji>XyO% zZ+)blpDYgM&sav$h{y?tDeWZQoAu64p=Qq^urYEo@kJo4fm$;G$LyU0#{XbNAomh0 z+3mDM#>18+V%Yo!t8Vw>VxIPeOyK!?vuIdk;P#XJ@63@q^Jf-F)W!O*bMu~mz^X{n z@t@)aT0l0~+mfD3jy*YhXUKtVMA+cI#NAesg7=hVX)b>u6?73yTNYDQ73HqTOM_Q7wKwsB zLE8b>$rHafm(}nw-2Sm_oQ2O;E3auqFJv|G;z%O!4C=IY{u?)RukW{4hsV`&luvfo zFk8F9LejVnfprQ6f19``5g}oq#7~vo=lz`>v&YvzUljfx&>$OjPAk};ebBVA<4HDP zYyHlnUa$|N3PPXS_pd2nn|m$!{crBv;W_3Ns@1>R_4)06Ta)FP{wmq{RFB}_XP+f5 zK6@oG%LLMh{^ba}NGh)w>1y~UK6U&LcuCyfy2r44OXt-n*QYq29(s3c@yq<4lXUZ< z!*i#{>wI4356(Xz6!&cMW^Xe#?0Y@p$AqlcDdnBrL(i>(o2O%v-?uL5@74NibpHY5 zjp)5CuUCx^TlN-q{{fX3ui_royuw7>s*(X9EG#et3kQOYIYDqSQM_PsEDBa(1u`}R zTS^um2)l@4c$$!*O)-_czIU^z-5~ZO--yKnrSy_ZY9l{nYSGpIMdV^V#YE)(`lIRZ z61(<0+aMT8t;U$hu2XjNfeml!ou;4Kfu_IS?cezYJ~I;h7Jo$|T7@61BC6D)*HNo~ zc+Q0U6%Ne*kZ@I|>qp^KVcO=U=@<2yRi|qO)UmI5#Xwj6x!;>7a%p)QNtkDkw4#!UyWiC~!1GrWlMg;b$!@^)pLUF#lwP4D zu>8q*MIzpwf2891!-Cv#r%E!080g2U)jaz2JAjZk$X4y(4dj&%tNgd2ak~2t&XXYO z#H;c2SDgzfL~O>`vaJ61N71qbThlX@zcXK4w)`R*8BDHf9S+Cszhw?EIG#Vz`qHW# zr$4YJhMID2{Vn56LOcj_Q3x8`er4-htp1k-0Yl)NZhZ9?Q{yeegL?Gl{Icg-e5O#2 zE5Z1Pxn@3C+4rmTE8FHFy%WPZkEu|d8PD!YX@Na7PA)rHyqaA6685vM^~@ngGC%Z5#gv64g!?7~eC#xBD~t8M~gA@cYeg)F;|5`0W+ zg+tw#_e-;{z>2}$ICpWFSCj!NE8v{@D?o$X*`M(d=UO?v)C&%p&bA@E`w!_hY#*G- z3WUHd8@u1C2Mk!;A%8W?+;C~!-T@1YA>tEu_byO zY!4L^n9UE;{tjO;&7;2Sc+J#tJkuxbenPApZD?Qj&hpuB&iu>`Hmz1Q(G`&Xn__h( zmalIWtH`vN;^eW~k61ehqu4gTt^JO&487Bt6+N8_LVm92pS~IkW3UWG)BgSBuT9}s!v3yur1^tWa9U8z_@d;-r3QO9pcu(0 z8{++hwFK&eh$8dLVv`$=H`9nn7mP=TmY1ofqMYI_~FLA{R4f zwR&cjQ2{3@C?W42-bBAxmbhHLkyVxkV|G49StrZIxu>tr?~?=Lja-k%{nvWK=a8%0 zYCm)?i(X6>X8&kOh*hjNg@L(Vz60bYyj=HX->_WqBzug* z9&Yu;?h3lL*%G*S&NnK5$qSy)^H4&d%?r+n1IDBu;{_bdI%_;cSB#;y{sCF^^F;QN z%f;#j-mZ^Z6)T@@$_kNy(xdB}l;3uH7bq4Qw66QLmcRv<1%H>k4EGC@(yrrX#8%B| z^1Imk?Ppl`!-JFB)HtTKttU(K4SayqnDO&Jz*mgNn&nr8P|R}ejY%K9gXYKdq<5W= zI?HRHZkHJ`tUK$YY)~*QH$4pBr?a3bcj>{Frvi5n)m&Na){c5~)^EX3xoicq1G01V zXTPqfzHBdo_O|Tqa5@Z=?!OQ1ZdJ4pxwxD)4~!3a*TCX+emr3dz0srk`qV+;lSknc zBE|nB^0j(b3GOsLa(de`pp|q({AL6p&1JZ{09*2F$jHKR_*66fRJ4DbH24M{bk-`@ z`3)LLi~q?=>PM^A^9?ea)4ynOCXg7`Z06G#2VyeP1?MOx{2N`)v#7Bs><46PjP?Y@ z-Y%fXCFL&yk*_OO-C^A%1Dp@sPGtO5-VMG7`{(#u3yD8tExpqb-$A$|0vSpWT_f|# zO2=cGALpicTYo-Q9O1EHe9Bv0`~0zzGRVyM6pM}NXBr|wxAeEoCzp2;BP9z2`N9iV zjJP|-9>NcaPL>Z1=ZW`UTGwKLk#5%td_y3{A0}2IxmLnLv*w*eG{d=J&ofoud7_HfgCVQmT3xzAMz2BCfAgjslS(?Zs4&fs| z5SRZR-f%d(FmJ)OIsNUNZx0niwa&v|Y)@zo)c2PBt#47?n;zso%FDzqf@8H7&)17p zMGG$*epobA7zsyzP0Yn>Ya0Jtq?m}Y7t?_!p(!s%U80^eN&?wv3H3K3e~HD9K28n?bohUuT`GyPM#b&8n`i1znw}g65f7v z;5wZ9Q{%4wSISwqa&5r;H<`;yJ#j`{N8QR1c^Byb<7fFEk=-<7g-h*^O25UKw2!wc zPv}%!AlskIe-vHjvtWmD7M(Se%Ouv55>>{i%l5J|b|vlURw+n~;I9Q}0 z-vr|3$m|~(%ZZ4*vnpcYrIF-(G7*%Wo`86wH+oN%DAv6rT7zBl$lpjK9^xZV}HXC?Xi#>9TM6b5aGF><$GzaSRbhyYxaf;W#afF zW{c0z0GMI&kM1@3YPb@S%kER+p=Z?9~N_Jdi zC_H^b$?6_79E2{4X{B;mPHKO;8MtKYmr*0twRP5QOGL%Ie49M=$(eh!Sbol9nPvE( zXaDJ6Ueh6&>c%fg*@cprQxxI)ld9s+I&ThqmhjEbm&IU1>NK5Nm^ zF@^ocu7vrH{fEDmxX`qCQi`pPx(T~##;dIwdS2opgqnVOLPH+K1KKR)+>#$C3BodI zx)x=)mOPSPNH}N)sG5IF2%jRQU3Me$j;Ig%+LaxI%T8W{1mTehq~e?qsYkwZ6#I~{ zi_@j!AxK?34fX?|>uKBo~DTCS!GLet0r` z%48BOkNg=@^>pLVQ_}3L63H#5YGeVII*HuDQg+V#2b?9fzZ07atyMtyTPf@>qC*+7 zufI@pu5t}$f2o$5Hs7^PDe<%NPGmBL7PeWlh^{;&yTxnjWUkTAqmObl78D6%^>sFW z@8O+>c6cE4@?__rN9`bQ5#g_=ZeGP0JYLfpxAVeyEB8mHp>(C8y$#WKU1+vz$s^qf zy3mgdCE*NAMmo%THOIV)Gr_r6a|^JjY{&kM35z-?0?#!lQ@Z*_(SL(DtKvk>%7oM~ zGTVmQTvt_w<-&>H_eWT)@iVsRv_$#in$C6h1x&=vT>frOfl zP6=excmwm^O8D?*`bV@k{gr|{blIMt|xk>%%k-No5TeR}yw&so%$ zGM zrOa}4Cc9RcbkzyKNuQhxO7xNNB~ZSg}w;SahYl@CuLI4SohFT*QFi z7smcB86$Eo@vn?x%OhvhtJUybgoe}FNVs5bb6L2%k-(dvQ98GoyGsL-9f7T-^%W;c!l{Ux1YnH~JHAim0E>i*k{!JG-@) zdM9KYK`|7s-&_6=316i8{{fvqV!t3d5F!GM2wl|ST1*smM4(>>K!FoDO)tSf;O>Au z)*xu1-1$1)(EvP9a|C)7PNRdFuIs~SxJXR#E~e?8g*~J-gylWBoKEPtPMs9?QOY(% zI^9}>C7_GO#TTCtYr5nOjieM^_E{{E2MD(722P#R!7VNYyKt8v){=A!#i`4p5a!NN z05zv>Cq*nCBfgDI8NJ^`*EQZLzBTHA zWx?C%Q(9o0_6d#-Af8=kZ{~m*Dn!Z~=s$Eri5QjE3Tu8GN^743R1Vs3%yH!@*v>}E zTdy)MBsv=HyQcseRA+IreZvE?yM7ZX&Jf~fXj#q_X59BpI06;Gha9Whbmdf}*!a5@ zS?5^n5o=_HaXTlF;&NM|l{DuzoT5k5C|b)-4yyWYI`b=7WZ^Z>BsWb9R2|S@19G5P z4|HUbrm1mK=aG=7ln7ZW8oWnII0??FXp@+PKZb3mt`*RC1#^9V$T`fF10@N{ofNfN z(c8#E{nv{^-S+(}*NwoZ4T^EuJvb=BkHcdGxP?%TM%y|PussyQgd92{5Th&FY8VBq zZ>n*89OtU1OW_eI-zsrv>b=m(GENhnMz@A-Er{-`kZIfmV1RW`G%5$%Yul#yRT?PM zr&b#8iwVZ9t#JDyzzK}>{#F#WP^t?|hjmUI-YJcyWYvg zAYJxTPGw8Q##3+83Myg@tDDNdF+teT28$2EdZ)NM9D}ClG>;9q`GSIR9hWTV4*gJN ze0(t=Co?T{PGNufg)|Tt7D*~i$5Z<&>$sl3vc1IhSaPV^WrSi%jFq}CIp)B}n4w*9$PMuKxg8 z{UjklM3i`s3bgn#Ajo4jQql+mc|^cEO1cE&WLW7Wnd*ceRXVAXP-CNQ(6jx;2bV$0 zhd>$tz1yJ+1Zr0)KSVTGfx3H`7}2R-4YEQ9*(nXJ8mn|p5?D3FpiRW*WIQWlP1Fv) zsJ5dd9N}`u32L>`)fmFW%&SgjR_Im|*tjP!!^9uaCB_1|xpIcW@ZTOna|#eb6~_pJ z5S~xNgaBDaFo|>(Arc@WH6BNF;CgW9wXG+<$WDUdh&U2X6gB}wzgZEdMGPI*gQ_PC z`>PmS=_NRZ^*jL1!AmAJUg^#|a*pWKs0N9&+~b%#E1q28&8jXU{9 zo1J^7T(1i7STq?5`&n~oxW-j5<5MhPIqsSAg458Vi1J8S{4@MRS@Q0r=(4bzPyqRa zSV3gMt+S=NK~4m<%`$XXHumA8bE#E6Mi2*3qX3BA0y#>@#(M zFs`I+#A<{-ek1ly@cA=o)p)JA1xyx5GBrI=rX6d$wM_y^<{B%uMh_?iZP5{nwLsN* z?;fh=mjH0IWMrni0rLk!-LPDvqOzREi1=-5wMFLG(EZik#wEv;tSJO_ zT*=fN+jQMFtuW^Bn!cPY_K?yb5vt0E2YonU2MoalTTHf2kgki5*Ro~xDAaf?aGyk3 zS;QSeO8v>T4M^Q<>ecNP`QEB z%o$Z_&$}R=P)J4p0P0;+A<4=Zj|LwQWOQ;2IFJxNg^t%45g z+H(#cP^!JaIfE&)?=U*1k;*}ocngT>s>*pz#Q{LyX1(g98Yu0IXp^%SxCk;G+#dPf@aegiA-NgG_dnW8j=@ zP^VS~j>ufN)j_@434p~$#lwBjV+;Xr?u0k5L|s%HUOPi*P-gsbWB^s5Uepd2@_3WJBM&|)Cm}nd1lC2WZvz<75=X#*(9*76JmtMRg zr`1R(I22DSqEIr@vmH_UqqxK_yRF?2fxu{4dz%D}0&`0n_EGH;(}SEQ_<4+{H<_YX ze-}AGHbQ9%9l&k#jj zO{UwNP^$eR6-V;Zb)#~R&ovwuIB6p2*#@*BOn8J(LET?mPL0VztwQ2346a&)?jcw_ zS6~I>I^m;?uMNx?z|5%zwXr?&n|z9dIQ*g%>n1y^{u7-g2D>jM!zD{XYa1>hp$he3 zj+QfZo)CwOOItl;Drectm>Vh0fYFhw#MCP|HxE*JL zs&z(_O`r{lSKQ)Dj)*Wdq{OE>v;u7bZmp(N4iM5-5T?m0tw-GB4e2VK81Xr}0D%`V zr#)0-PecZR3#Kxpp~%p58Agw?UGr5xva*kc-A-Jv*8Nduu(gcG{{VSX=^_JigpON} zbw;fra{_m>R4rvsbWGY%bBr9c=n5ah-uFesnL)+xs`w-ZaNwq@rA0m+}9Sn zvT@u`T@x63riksrm^Ziy?x{>NTzdrZP)vdQj>?;xta_%B0CcOHoS@c#nMvf@N)=h+ z(*FR=)lzXVpHSqLU>8Cl4qvH5rt4s^v!cqvkvpsH9n?EiV+uj$$U*o93T=|kM|6}! zjl<}GH%$2Mq!OyMWy_#Fh?jh%`pQTyvsrSI@$m zOPTp{*dQne9YMnI=^xB=N0FfGlCSp0F#Ik504bvrmTa2Zel1)|5B5=$omP?L!fBgM z=uBv!4!SCVlIDp*M|>m7;j2+DyY@|C14&cDKp_+!EjZ4-6U%Z?Z9UMc93;rqL-2h& zBKl_G5Psv3x@U(X;S0hrOX!(ko>|ps@ z5Uz4BsHLp#gJ|T%(wkI+s1S5i=?8Nvl}|1aZFpm#{Mpq^N}f35D=R!Qg*F$s=%v2# zqFt(JmqdZ1LJn+N8|Wnnf?R*`BNs@Hh?s{V>X_HvQb3-VRNB(!w88khuV;Q+^7Ak z#TJH%*r;)7=$h6`ABmi)r!%4g8bB8*ZM}EunhZ8V_x}J95Zx1|PK#d1hG{4PksP|I zaeEEH+ae4W;FSe6(g@{DWkXIrrAxz`l1}LOxsHf9u(#rfD#qvII`Ges)utOd8CN}x z0|8B9?VE++HPzlZoOx4AUt4eTRrsh(4}>RJ_k5;4>U^12)h?Y;vPRig0!o%!vgXqb zI7C=ZnUu#m(o28jE9rP|8X0LeUWOoJmmfF&YG9APFGGGOeHv zrN{u*s0$;v4c13xjI4|zTmV}I4Ta0=!xSD?rEQ9otw-e+r5YFA)s z1OTn}b`lB1;W47d%$1G<#_q77#|$VyjI9(Bcx4D()Wr3bXEjF%Mxo?v`jic@H9^|t zFgncRqS87oBtmq2aMaRf>x#Lh%@I%yQ@lGH0l>kZCJ@zOWCfL#5R2pmQMCDWgz!Ln zCh(8t`|N?6UCrHiY!7GD_ea_VmWK5z02JHG>=fn@+ets=8lNXLLU3S{9hWl5;gxGO zQ%Ft;9KNZoe<|H~fy^$44bfl#neW0znN9?k3@;qX#EcY0Q0E?2^xbq=P!-RH=r>d8 z!dOpS1WpzSv)e%v+L^^b5?yZ_<0bJam_ej80d`K;k8>)28f|w{Vpi^vi zRNN$h6)LPTHw_9sLOVDo_D&lp{=G}tnRQ2c4a)YPX_%JN=+cM{ad~sxI($D=`qg|p z%&=%TUj5~+bJ`*aGXWM_%*EO(i3`JZ#NZ-z9oFg_Xc>N~pu4cz28U!6Bj zE{nfp21w%Y0brcuc2;4lQcR;u+I%v3NaZ{FDwN#OOp+X6&d59h(>yXb>Uhh_(gKF5 zFb0ZQ0%vs;i3jRXdtih0O{CHr`=JUabS{+)C1q!bC(#Et{2`lhDAPF5Cp#7tLD4&+ z4iG`~;0s7;z+E&7O2}YV;0&xFY8qo4DRIb;sbQuRt~EN)bvKBT0*O^ zRxo5EJSM1V=_*)3-EyEcR**uFK|Bd?=!j=7siNmh9fG`gfLl;}P3zqnHB7gZofEuY z3GS9RA_5pv)yNbq!gr40`GAEr86(va>uyreN4^}eQLF4Zg6Nlf^e+!I3~`R2wWUY28GK#RkYw> z!ir?8KpO;VI!=k69EOBsG!O>rTOAX&!v6r#a;sXj3g@eem5A!12TlcLY0+Nx0i#+1 zzP0B@qUX80FcoW>*I-UnHZ{$=I=D2dYB1x#lqF#Bj_MR_ zq!XzdBIW}~Y9&jqsN(D;<|M7IL8m<;Mu&w8yDVE@*Z=H$aF3?7uOd+%XFfwVn7l zkR%4KT&xqrFs4WtP%^d&q~&Hd@@B^UPed@@nc--ZTKOMUXzoyG?wIDwbXZd0&1YXl zeM^pw&@E#Js$syB{{Sh?stbM)3sjRG7I<^6LQrT0+BrsaU{Hec07W1+X(G3Tcp2&M;?nQcjUE zsw@D+qi&JHqnmSd;g~wBC{%Z%ZG5;{{{V$}Zy+0n&g=ZSae%q`wo>f}(NZLe;6ZOH zr00955u8eMWYaz)Mx-Ot&fE>tBL~lJH%}%3Do!>)0!hF@uC(jXbJ`fEfA~l;_cO6OECauAxt}ip-p97c6*u-Fl|e zg8W?2N)Hz_$WUQ2CSd?1oF=`-(=@@y49YIb0|gtfF5VX|TWLj_=%+k6$CBqkZV_o{ zZ^DVj!CK6rwyIg*k27Ixqg2vRj4yb6Jx--W^*WRq*(kp!bw5oJy5|`)g)e)KL?uNT;d=3y!vLdI5BLIgs-iKaeNgb5>t z;YiMfKwt;pR=KY1sk-;m0Lf59AnEFesc_j{PwJmWpNQGlDvz{|Nz*(o7$}C)BdH0_ zr%lXhA*$_+tfyNoJiuWV)aGkvsNFURg#@TfYo)-!&KI-VCzuq#!Wel$T+L~MHB9kb z+vZ{s(j5*@bbqNF!NqBjKQdm|dr@ywNNK+-eO zMMnsh*{4(-*3s7+B!&YsXwtpe2jmozNGX(H!<7lhN5Bf|iFwN^BTIlXeTg&0tydFr{PY^3O_6Exi( zsq-%kLmFxjm6QQ-h&&`LEbzcu@DxBU-$mlN)hhEwh|_uH2h|#lbaw&@ z7i1dAaSHrV5@YPIq6^&ON{qA@dXy@V)-uD_WB|j-DzKhjUsd6`q<+e!avJi{8Wib^ z+oG3B=8y8O5WF@onCrsUto1-;6W@caSX|*~v{_kL3L2c}25f^sSdgKp97>d6Ny?o{ z&s5d}B{I=z1!Z4a219i9;c&sigK_vkSJbp%5i6Q@3NC)`FcYfQ3Is zAaxi|@m$(R9T&K<$32sqSO);^ozbRRd@vlmM?=V!;g=iv9jIL8f-TuuQLpQRlpg_H z3uMc5LNZ~%D9)+YV=2ylyy~mz$r%C>=$ljH8I3{>bTz>B;t$LMY5Ao!F*IzlfkR{; ztyZ_yIl_EqM_`}q&`X1X=!!iNOd6Eb)CD0=!UhCz-4|9k>=A9P9T9cTcL@z0)u#b+ zrd!-DYoRtLb%ot#B|yWmbjLfzt}W;n ziBz~ucTl9P0Crr*;*0RmRh_2H^8XR0TL7K1}|d(OQAic!2LQl`d@Lhz}$?ohxP%#>M3hI~f}wLPRZOw$@lk(Fw=xukNCjYc|l zJOOTc;HVS1JTl&;W_UJWsHoq{TC{{t7ETeE8ZUHPI->15DW*|v5;{3(wOV?tjuH&; z<-&@Sum~22C@X3n=-n4bs&TQ*)L~yhUclnhmHz;7G)Ey5=#0yPq3rQ#FgAqBl)-7& zLW71K&AcdW-8D>gB61_576A6j4uUi&x8BoixN16tUfbEr**LpdQBW}N@Sdm#kn9;i zLV?VKND{6^hS-?ZPHj@)BP$+9QGPxh!fVYtaw26;+y@BN0u|e#mB^;C>{YzOT@*6Y z_d$)Fdm{Tt8YbJFS1K4ikSG{HHwpr7&5{$IG6W}q2$2R*9;kRuJLtS4&V@}7HN_$D zhVynBlw8}LhdHjOf52WP^#(8ZZ2th19PCYv7PWHvo*3gVmGXrruwEbiJJw3^MMiEO z2AUw|LDKt|hwNpuoe>T$E+=FrVR9%_qPi~GTcXO?EJT&3{3*hSSXki&b@E3EJTjr6 zaBv_O1bIN!2waZrz2C*DSf4Gp-8g6y#?$Df^6gQoL`iWOjgY4WthnzeTh7qm`Be4) z023*J;p_k%2_ZC(mS$u%3(M_2NkXNrGyeb!J-R6LUDIO;@Nk|037(2ys>VzrRv<21qDgnz7!$7#co#7%G5J?4nOw!uDVcb0G^2ns z)dD*qGOiIg;56X~H!d)Pn~+*3Q@W=ZJd-Xdk8R&5*?5)BuI>;3R4ycyged?9Ty#a} zCS?%IpM`Nkr9n-da#Lr*ZrGG5vPr;2z!wuh|y6KXlq;jBEEm z%TJ=ZjiXSxcuiI!A`pzAX&Q|9nuT9vYx*H%HBbpZ@NW;dkoa!apkBk2RRM$25 zi8?P1r98%A0>->#6;1VpOQK93=+bRFas#wXaTNosaoiy7NS@oQS~MdFvE(A$tHhy3 zsdIs}tVrK=;?uC4>eJAs)^)W272E#+){Fh8m}#<|cSX*VtU~aFoWi?1D;kM-&YGxZ zHBqk&;dJ2&N{$8E=!}R~xK8RCN`~c0aRFo!fhTo!PF)mz7Iar~$|TyLLc}$4%5FJI zxXIB7o`^U`>A{C|M{=WCg>fbofSAcwFf^We=k}D}0<-pUv4gPDSwx0>zUiILZ9RRMUZ?wXitKwVVr#1O&7}g*?Yi5p$1*U5BV zS30ts>PmIl`HU-D%ayrY)4CI*hy`f9e$7)HMeUayS_s)Nq$dE{MTKNpfx;)ATC#<|PV^f)%di z(aFvBi(Jj2!>Xnmbu+J3?;Y5#UkUnbQ=dWB#oCM~8rD6;hhXJe(#MPp5UKk&w4P(k zJ!J>ex||+}I;A_bI8JGeK}D`RAy$+;N;Kh`wiT}+m3g~E)F#x_j}|rRy6n0j_XsFc zZ&Hc)wMDKoxl>eUL`6ez!a8ln50fiSJegeZ6NVAI9WtCj=&{j$RVFmyzUoN}Bm`Z{ z39tk}2x-8{m{8JCF|3sY+Hi#BT;tIi-qY@k3^9kGOrz{<4b88BiKc5*_$Y&KbJcjh z*xR-tf8Ex1LZqWA0+OWmTdQjv$Y{^0SZ`ve(`7L0LAp*8tFfd_q`T^83AETJ4kSt( z+`|Vwl*y;AiSDRcTAmJt8)Y?w3_be|hpMMQ5XgN~tGXdF<%}Zze6q16WFT6BW;$?% zJTilIDs(H9YxGC|0J5#wa)lH@QVA^rR5Sn(SWux%Lc%S8--*kY{C8jJP;aZDHLb7? zRZZV49-$U9ow@20syH-210DBaMO}OJ zJ}F$mT!|_b^#gWIU5?OgwpQ~uFJPs9RR{OU&DJi{OA0qHomhx6nj9kA-G@a)Mhgam9VFD1c01JWh{VE zv$~#cxl{B)^Tb$g$WEI+T~Tr%bXlIdte`aXMedQF7dM+1FBXjozm!^jh*PU{pF|Eq zJ1%G|7=+4n@>5tjzAyHs;15Ljw&4U~5i=hU$VVFLfwVkfG?`^djgHC^bV1B@J>@jGwb_iVoTr(;61C8Ns@d$WA9vuQCCCPuFeg+WRnN(8f>iUntbx^3eO)oDM^98ai23*6*Y%X@W(##rsM7`=c2Ui) z%6S@PMjSKb1A$#s33iJ)&@7{5EG~;3lU`scBs63x)PBefP=F;55;uh~*+TZp1&AVe zWpj2@!d>W%YWNb-i9lD8klX+_RyY05%a#sE?uDaLn(@xiQviU`9*777h`0iaW}^dS zebly0iR!diK(vF9t(&R?3AX1QP-{=TmhIUWb0{}7;GvmL1|6QEGJ0+5rMA=)8Yawv zYn4H(Qw+`u=Pd`(e(Rb^D3_5lx_zam;BE?l*3;8@3B{q?cSWLX%xt+!Uc)EoopwH| z(Kwh=WM>KQd|W$(7z`9ku&oOcv4aJ6UL&m^3rczJQ<*XFja98_kBJw#@Fd&_SDPD< zfEYkNTHwle=j)|su zvZUmws1B+Ex{S4A7Cg|okaOjDZPs%umw>VT@SsK%8P#mP;aMe2ud*1<5jr9oCxwpZG?ri(7{Lzw(Ix0F}yBkDjVT5zDe| zlB6co5H{+A32ic7 z{!w$IWjq`hZ3)*#iZ;q{s&E|51gb|_ z3X#p>Q+|#JMylf!+FVAduK9~(=1?SBB&V9C&S}ob5Jjz2<1M?nUzCu8>6H!+83zO` z+I2&!X~=WFt9{lHw@<1hM2YH)9xj676RnmpUncLO7TDfn(FqPLcYGpTBWw$ij%*ce zK62f9rV{{U^Qd4PDFLufAs`7Mpy_MXQ&Nn?&qTGaB;5wBJ z@R~AzWN9dPCJ&0d?Ww`zsqi5|axsi(CY+0p7|$BIcWv zK02va2?Or5J&MB0JTzII_rpFt@m8W4t}T_jv&N!-Ca|}Yk+gK!h7ec{vGze zNzrm$G8ZsCRu;zx0+V!`~{4n!2wvv+^=>5 zkE(Q{454ZaB@8NYPv z)sC8PA^NZDY*RVVvx(?8SJ1oh_PH&5_3lk&QxYTG<#u)yV-j z#)wx}h}9OJ%PJJ9;Ci6O_fj*eH8HbMmEBaf3-G5p@M;R$UDZ1STY9b-ZV}Y3cn=%mdj)q1nN`nmmCy*0ujTiwBF%{ zg#Q3&tbg6FV6LiK!^x-70Z$3eXp)}FQa&P_+NU{JJvRWH9ndZ???ufWF~s=HF31@A zCb&1CIP^iy2d4lWM@5B&o*Chu9_jvyAx=~b?z0Ke@R?68(g##USI*@VUQfc&qIDi{ zIf{K(OzEn>*_zYx;eQQT%>dgXT6SI~0a2APx_^x7Zf1f(>aH9ha^VKzFi>mUB$3@V zkF>I*Mh>}zJ*L`p%rU>YR~J=wg9x?9J(C$oIr(uiAyFb-Ejkp^;mY9$RNB7I+Kg5g zI~m3hG+fPkCX9klbVX>Kq7Ny8)G9Pxg47Q)M3imOT?kdAM(N(p`I!Fzl}^17`gpk( z`?%L9vJk=0N1KW&f? znffPav7L_$C_Dw&SGa^gBkrEB2Y(50f;vt{oCiq44McaVg8+4Nfg0N5<9 zoMVfr{3*|59eSgw`XJ#1N@Qr7*G{Vt)oA0)vZ=lON{N61cZg2C(eZCP@sNW;YmcHB zGnLU2ZgS=gn3NhVr37_dR1_2~_~8Mn1-}lPs4H=*$#h`urXc8?^YIbcK*2a*5S|KMWS_0 zX^E1qmz+RQ=MvpiJmyv8`Y1S8)Xtyf6Y#eiB2W(Lo(P4Qj6kZ}GOQAu53WL*cDqrbUKsEydYav5%o@})398y6b)0_*(@i~XDuKnRCcfwQX=6# zi=_Vm>ZGAfxkb(=s=~?$?urx4RGv`-y6lUS^C1dHTq-Vfk~6YwaRcy*I3pcUJf&NPyB*QLnF6r8!A&HI%5Zvcs9tT3`B%+C{BKM~xdcwKny?Egj#hYhREx@eNhdBS#LOu8kidn^R3ciNN}aa`s&rp1nbB`epm#*mf^vw^ ztIR^s1z`aS6}itNvFNH-s#`_nw3=g-{7KCvmm7ByvbJ2IIGF)Cl<3AeGu=7W>OG{n zs5+5$+9Oe=$1sM+GO(?KPC|Rk5=t;*3I;+Bb~M>RAdC>5Pv8V>gh8AjX#W7YyQ^5s zfX-Uw|YK#4u&@$Wv7|#vOIna%Y@fcYU zMiHJ-1w3|zi7Iml->w!k$}!A1^G^(AT47sE9)&=Zeo(sVtDh8@ILSQa! zRur}sjqX#A#8*YPMTsiW_`I-=GMYl}vD_|Lm9bp*g(ObNuwfgI9C7dEwZNciw2l0- zfC##_PGbw+7+j|10tX0^p4(z`**KYWpmO7YgU3kV7}+Ses2NZ^jowdW-hdQxNiY*h z76k1PX6T-8kI_>Z0EI9@mjYJ1q4Vg0fR9M-yQwgXX${Xs1e2q(xg-@MI-$A_ZvOyf zfFf616#-+ii7n9n%XvZ~(6MU_jnFUw%CeCpBbJXu8sR?|_~IDCNlvQ#l2bs()nXN= z8mnK;s)56hwSoqos8f=DIKu0y#hY%k3f2ZU>UcWa0m0R0ObwGmIm7^+Se~60-4yV3 zKV$I&ed!l8pVEuOeebv9N9lcF~ejSF2&8)grpw`pxfPzM%u z(SHx^LZ!bDm`1fRRiLz;l=8b%qRc!&T(F_4$LC$txHxGF4tUI=3!PlpScCvlP%t-? zL}3eqPyk8*Xs(>XF|6>)z+0|v5EfJnCl_S>KOFgC(t*mC9x>;{@SPXD3mkF4v6$Tv zFV%Ac*>b_*lB6KfozZiMYGpr&l6x$~LV+btc3I^|j#fBl231z=Bc`aAO@*O5r_{ML zwhV%IUKtkDB$akH!{D>{TAx!p0srAqX{Zz+O(5HkomiD)8F zAaqfQ--dj>(@RJqP+`J~r#mSNb##&Glp9y#92(b}g*DJzJ|w{GYb(UaIYEs#I!c#J zAx(#rUDaAK^;v~z7(^6ql`i2b-)J1x&N7~BSth{TEUBp0&ZS4Qx{UJ15fojF2cjvn zP6qp$jZbJT>4Ka+o~61BqYQUcz(Hs?Zmu^%ceB)OMvNUo_Fo(T=MM0xIle6iOdXR0 zz2*%3K{ltorRpJEdmSM*x3M@7O`0IujVz`>-3orqd<^W5w0kM0OjH?6c)6}6&A|#Q zFziIH1)$CXZBctqE3ywn?+`MkLUkuPB!uQv=G_Op!YyUM{T3933mi9C;R_00q#|FD zq9zF#M7O*sZd5lKsB9oNS})mQNks+=ii&qT2nO9c{{TG;2ylcXDULA;1gDk>5D7w< z4FXQNS6Fpcw}WofD8}ixK}~2XU3-3~c-^0@~vIt2d!%J)J;f_JPUv_d*Z2nYcvoPp?xBu=Tsj2tUnb8anH84i%3 zscJGoI7E7Iyz2@YVCb7sv!Pa)R<yLi?yt6vK8z2pS#|08Jp57=+yb!fvU@(K@@JVR8a+NKtjp zxS1*+gAu4s4rw}UgaW0oxyxgKoS<dDAcO%=)n<{%7YoqGIUbIq|ESvq@o%e zUClpaEjY@tp|ub??ei5e1zh;2mW|e9qQ0W>s+NZU8YOcsQ45|5Wv~uZm2Oj?1h|tC z!U{E9X}@2U0W?{LgD@9}8Uz5)u!r!^aBz|GuN&H`b=4bu5CHth_0xtSMK*)5k`QH2 z%%Yt2R-7Y3hppW@f-OWU>vD+V%t7S?m3=vAly!syfnYyXG{{g_FLYexI`Ac{ z3*~O58}hKT!D(htWAn6jE+lV-XSZ(T{Wi^;5 zD0rOGn&t~uQ>Ot9_CT{3P|B9S;A_;kWo_W;qH8E#Tm}xmXZl=$f6YGLE+U4ctm<4&uXSeRBv&1vW0-OB8~2+*D9qy z`iq@h+iG^orf7c>aT6g}!DqsP_m&+B6l9Q1#4^x}z2!QR0MRl2 z%LGV`&>qK7H$)2@5vxo<$kZk}nZ4G!j@?sgfon|n*%R#4>bT2_iWa!w9IFlN(4hsv z($lC}4RrQ$f*nsXG+e|)9uQRUfx}ou*&bd+r=rVvX~QZ9kT&-dx|b4(em|d{8R0%h z4b!I}LJnkw0b{zt%q*d5oHbb38lchLZ^9I;sMCN7p5T&zJfjGb0V-edA>DUCASztJ zI81rNprXNvf`zc@^+MiEFLVazfOkZ@Iv`tx;!v7VjZrN#s0#ShVM9#a2dV&u$(@t@ zTV0};3!;C@nvt@IuyU-*v|XwZmTH@;B)Mw$Iv35iReL3aDqVAgXDBo%_==M!h9yZ~ zy3BF4xzTu!Cxl`aA!ZckhE%vuswI)92Ue+Tex*0rX*9Dcq?3Du_g6T*$4uvSc#5A1 zO=}^d29TWE4s|0036P8ZhyrpNyhaCo))ye{gx3H;kaywAu4e;r&J>zNf;P$!3YCt^ zo=7rOGG$9tf2MMu>YY7Mpf#5)$=zEDWTRi&8rOU+5&CnH^5JDdAR`mURms9$6au89JSEYu8(D=C z&4Rd~H2#N$K4bJvI3^GR7dq!sZElEDY@JV%M-!@^6Xq*)JI=f!oLl!BLCg zbx z$_>kmo`|@YJ9Q;aiVP5~Fc<#H)p1Rx-eV}9JC)7FumBMYD25koaHQ&;Ta}H#)UV;G zUVbTvLZB@?v6)5kb_vdLw;{VHRQ7hfh?H6g8g)R^EOIojY0-F9NIE+q6KNW5pvP2c zg3^Ht8D0ZRww$fiu9Lq+*Lxd*Icc~fY!UWAG!j#Ew;rgFT|BA6nI}%jxxh+>{7A$! za*ffo&pJolTqYEVSXhzwUKlSKdH(5;!n0RIzLiZev&k?sEyxGUF$UX;uz)hDDAmnqc<;e;CAqtS zfx_bfT@4=#REwRVz#UT>@lA&TT1GyIx*FHu>c8#408Bwbiveg(dtAmIQJ@I1?GMR2 zCRJ#4G9yAaP4Nq7Av!6Ot4fKFRD4CD8z_uuu)V{vI8jVDriitzG0h6|Y<*TBluUF) z=o8)l0J^f2YK8L)R)k$2m0LpGWpe7dmdoEX9JFMh$_{r!ginmpWwsYPhTJ5aM-Tg^ zrWUl~`Te-MspVcBUk+v(5Zz0YsnT)Pvk}!(q3sPIOHVNfnx(@>WGpP9KyyvUb?!*o z5qq_j8T?B*TqVEZ9}qpcVit!W8Ub{nY_lrtqaBeMT@h|L$nwBfEFpC8J1i&#LAd37 z-FRatKLB!}Nw^A3{{Tvqc%Lpa$l_AbDV>xMxz{G@+Bo@;x=N2LXyaFOPpN}}&@I+@i9vJ9oB@v}5Z7d0RB+0YvaP00 zWkxKZXD6wQ!JK) z@I;;C{!y~#PCg_m(FLY^uMOgQqmZ&fk&QwnhF69MW#ZJep9i9eSxvSIYeWQmNhG!q z6otf@GKGZ1qUU-dTLW!MFLad}aOP24xkc1&2jI%N^;!AZ5~62Nq{;FcGFS!2bFzI$ z689GrPX>lP-!cxaOL?#-)gFl5D6*61;Tc6NeRW*Z-}nFM?(UKtAgOeWk|`adyFt2< z?hY9xD9uKvzywK&fzpB~%8(AFm3;qRe!f5c;IYTH*WJDMob$Y=?m3%1zfvzfL_vE5 ziTATZYi9p}G)xx^%tEBDAbH2bj3uajDe zXsdG1?XV2f^Zq$APrlReCsPI~FNNw-w?sbO8}2`oYoNBRF&X@<`zPudh@Xi#v6q0k z%+W5bSUHW~flb#G;p@P=CeNeU7>@(Vw9y>MliYuI1$qA_?npCHHtDtRE&n|4qS#~N zMh@oj?s+=7qXIeIL|1SLgSg69_n>AFCMlP^kN>@H2~ghmhW*KPPpfUy4=K%(e3e1z@PpfJcR!UnnvHf*L+k44qW88kdKM1JDo8QsOGp#x!%r`&6e??9-`5jxh@r>zR zpj~XmEsJ7BFNjrJw|aX4F~>O-QHi#@t=qQzN;cbNQg`%Rsx0O~6QKlgihzM@Dowkz z&L{oN2gg`hWiX9^j>Mp6f&xJhV-x||9O+9G`e7B1la8I{1n~@x_N{Q^mYP^>`GR~) z)L+J4f(Wo>%5ISo6=8~?Wt>QdhdYml(c2oM zxR!^%b1Szc{QKls$4Ihh7h{}+uf#gG(V%C@Q{6tlVQGv|G&2>Mwf;{ymg_ld=|2$tU~Sr`$g{c7<`a*1K-#Iv?J6k+|8(8Qh-nl^ zr<3@_T8;S>qv}z&?<<|#cV4aE^iqtl3UZaFz6DR7PHNW8D6m=Qi z0r?*Hzuu2~@Z=XVLW7}e!)gtBjO^}3&Yv-E$@rq=` z{&r#=L%nCfTQ3nBbei_8?2;HN;>{am*=X@YbR-s!J++A#w%B~jAF2-Wb*x1Gy0Q(IW*S6PP>^FM@8v&7(PI~0YFt?AR2ubZ&ZT&&sJ@<# zEF0Kg^iP%_{Tq$;Ec*I{!0K0tV4+)Jg4GG?eX5%u(c%x!+(ZR!nsyX|id)L~i#XIi zfKH0}GE-V(8(V}LGMra0SJv%#a+Zj%T#AM#=99}r(o0^m9@gt(Z^r3H@O5dHf-uU& z{6Wmzu%vHJm1n#R@>s;a{6Yo`KBAmlB>vTiIa;e1wTFDD4xOiJmeWRN^D{12T_Jkv zp%`cvqIc?OSf*5$%FTd4zhYf^ea?!lR6uBCLxS6`vsSLERFu~JaNwIpp=_3D;sY20`qlO$m9e?a|i`HFEkLt-uO=kwFIMEB;V@Bb}mE|LqD#y$2JT+^X>@CXuh8Lv+9NjMqyhh8H_2g=f5e z^stSD)tOjLmRGw_P$(1~D+C><@6i@San8lQZYYRy=r#fbtP@ROyw~aV%7>2%d-yQX z9xy|hbxG&WYA1|Q`D!I47VZPuGY-YN$SV?>a*-5?VHNjI5YG-wjaTnz!AS=rXaeb z{`ti_VAM}&DdL@=>3Za6)9;WfofolMM3^kfth~C6utysT5<46!uPu}EUjLkv90}vz ziB5FX5bLc&C6{$nXmn1I|BNMPN>FM0^^%%6{$q=?RJw-~l(gWb2s15)4q2SAx`@*@ z+iZzNP$~0pXurCWLR7`5B%Y0iOl$R9unzJ`t_THptP(LoEZHeYkq4gjH#|&p{yn*fIebczte{VHSog1PlQr6p@5-j_r_x@&;WITA zzayn=&ECB_g^c=UkHKF9UAZ^2Ip15Qinp$Ck26xfiCjRvsxxetNW_fzmhRhqdgXT_ zs5QMd*IoFkvS0)yKpDLq`HcGkzFfk z9Y_b-qa16v%PM;^F}@JX$}B`uA^m;kSq3!YO=S;DE=8P!6zpx0Y^NVO+r%>hp}1(2 z@kBte(5qL4a5W;`D2k@|i;>K91ynYtwEQ}TDbf8=vB4MtA%;ElKD4sVXKux4sA;jH z{hEDdNLI zUzhC8Z8(!Qv(g~sG$>BlMEuit82{t|kLIqUC0kv~={pRAr}kX`WWuS+A?@@K8x528 z@E7J)#ILpR`Umy{`Xy#6vMW`ZNY5mQXf1(yh*egXBZ~L-v5+=6h!=`NWY86Mjqq9< zG9)QGS0&M_dP`O^a+uWr$us#C6{sfqBP-PT=}u)Khq%!lA&2c;Uc9MH(7kS`7L!@_ z;D4YVx~JaLZk)5{j(vZ4-tU_+9}5TRjKg9&Vg_`|gD z-z;nRC<`u&)1_4XZwUSNK4n*X0#P7^dO|99oP?vn^*H=qOu_NvYY^prNuWB+aUDx8 z7_u$fMe1TvTa|)=ix9+!{!;jcCW4u4Qx^%XJ&E3+M#8GeDru04pS<16BEaleb5bVm zJo)urzP``D%rvbMJ~Ar9s4$ne*VYQtr!q|)c2jn|O@Mgl3x7SGxyTSiqDqIhVXwCzBI4)%HHHai zLfcY!c-!#%0X_ybkyvivyIZGj1+r?bO73yJe_&|{VL+pC2MsDFmt1Eak_IQ5OI>SQ z;1!$yZ$c_KlZkxBzInaykz+2$kA{|_Vvo$pt9J!o&mTt-zx$*4D)D1Ccz8kw@xV8e zUMw*n!rBuK#TTMb+n%(9Y+KE@@dS|((g=OV`XS_@IUY0v1oW;&oqr7}+DG)xB8BMH z;{6Cez4#0F95OyIq*Glc*XVk#&U&1CN13?D8F*(5DKj;NJzm#Z?@h9>56xu2W?QaT zCb@dGrgEXtyczgd$~P#j%AIF zR*n|0v;tG`uD&EVvcNcB-5EPM|FN~n3@(bMNGYY0vK%{vQ71y$Ad!S)uCyfD(D4cp zSgxs$%~r8SuRzbI%kuZ{#hGnRWoi9K(tHCQikH4 zQlZrgnkv?Z7Se%>39Nb4bsK^{!a0!ySwM2Qv6NS)DSc$2QjpEVjuM|mH)Cq$n%v66 z*xCDcb5y(U(fiT#B|tvmkS-69)?LFq z*e8?8e_ZNP8C>v@*@^QU4EheVxG#gT5#KNSH5z_ z%~vth_1ZBxNbbIOh7@tlX&vy4PUly;SLkk&7=}ykbu>CCy!(-eyzq#H0cxBybMEc3 zGh+3H>*ob|XYJ%4BV+puvQ!RvrTY`9!=+jmt+D#=#lyu$H zX$HGaThbb@D-z*}1LqSmw#9k}mE`o!R+59Q6IQ5^w}uC`dj=|Ek9qu?m3XArq<2se zxY&IVHeJzgL8=?O@pFMn?TM;y(u|TO-~_R(+LVwC;p1r@`{4pt8`)Fn>&FB}%3`Ji z-K5d_w?CROLF*Kw>0yL9-AemruHgFt?%VU+|4)u>V5Sha{xc(zhtKf*Is^U?CPdCE#w3SoP z2i%+M`0XQ+M(upbt~hm*#2VcYkmd30P9Y)xiF((QUmb8(DKr@)Oa{$~Sa-Ho@gZp; zy{c{$(BRm^bu{t43nu)z;b2#eD{qpLffR`Mo{;e`5?t}~C=R|6q%^>kHj1W&5~8sw zf$~zgjlOU~;=K5=`nAxf2DR8dHgkytGgA+I-zX_pst$8JK3I`>WzfG^E>S(XrV8u8 zqc5JD@=FXTM~J;O$Xps>u0SPoVbNNpP-g8LQ}FpW%F4jpjD52!>4)YXk8UnZMOgu* z!8^IO6R_Xxj)wCAX`I%2EJ+NHOzHib4(Yxug5$Oa&2_=xSA=;rPpISJbyzBAXmp!4 zIxa4@r9ju1Ym72cFn@HBlPg#6SL*#sA!*{k`*znW-;TdDcOX4%a$-7!{Nulx`5ho?$ZXFCy92jo~?>%TzmOQ?AXe z45=ru61;!&hA-6hO7Qf8kgP|mB$`2i;52~?<$1o`&a~f58AQA!8{7b9C6;JyZ4Lx% zuH)*HL|?5+k>#WVLh1Q9G>%EYOZV2^aV+MGx-I5tgG>f}K+2NGu{K|`{k`0HQDn3= zJlYs7$ZF5@$wVk~6FI|#E4*WZGYM8s&%!N}k=@2dPz-nAJrY}7_Gt8<#&a{*iXq?? zN9FGq&GjqS_jt!Hb5#1bwpL&9r(lSwtV6Xx37pvZ3M;2t>v_HzyP56nHF=QZbJauK zPVBnlx=#j)iBV-a=92h6W8R;&CZw%+qPL%m)LQpe9h@C1`z zF(uQ#%7wQmI8_O7@#&+121??C^2UoGgPrGGZhzB6C7^E(J{J>?6{{ZQ-X@&j98)lz zY2WI1{FOB~+LrvwqzdT~)L_Dp2+%R}2nb{B{oyI*7&!d3thsuskiK{H>jyi>lEY>4 zbz3&l4)+7)nUY_mH!NIF>aysSX>pxtgH8d`HQztMu$!@~ZSmTJ+H1%g1_)WPo;@Qf z7jEQCHX(+_Bybb+Cn?O*PxvJ&IG#L+7GhknFXZ^bz{IDw3t1x~YDtnO|2?pw$9NlN zFj}ThD4-uo&Ni2?uVnT$DdHa2cbo{r+*m<~H71d&E)3KaHw7e1fy9(bV@ha?@EAQj z{!+nl(%lT>Yp@+V7-VLcLQ?>0IW&zWEa46Bb z3}l>8(?GtGm?&Yrs$FthBKFjq_GEY*@Eh?=F$)?};X!yAe=3>Hxhe!~!{0nWum%jj zh{Yot-@v9k+rp*%Vsn5We{O{AlT9La_BKM_znQJ(QG&==qXe1}JWF&-1R1o(9DE!1RJ`Z z+xv)EWg)v*0!XuWsrA+L%dbl6)HVC`a^gowgkF5P70^FSWag*gr5abBfb{qd4=YB4 z{sW1;#tTw&=AOHLM6-KKz(xa>ksz^(E_|bK0)L~xnbfqdg{@qGmWM9|-&^RN(+q1; z=>t`lt9iKO)>l`foCtGVPbz|}|58SV>Q1nUy_Rq8bB!g2twHlTc4qP@L+alc3^Qo3 z@|dvt{p~R`xYG>PebIN_Qmh?&4XvSx&q{oR82W_4F5iMOv(qEjT#a?pq|LOFBuDBq z=me5}Du~zb(+H80=s6pp0?5(iCL^gCh+Ec%3^J2i3i1ZuDz6h_`h)tMsuTXn?EM(T zOn$7-9%Mm8EaG87_kSP>&zRB#QVV;mY2At2UY%eRJ=7p{vnikBE+p?tzNsHDEfnq(r?=<>m6c>jf*Nya@-k+N;B6QFFLCZX$8Ve zcnGub=!BNIh$b)`@$A}ETf=YmNrRJ!10oRC%1=?nOyYBos?)P8bn&A&J86jTG&T2? zgtHo0>p?vGodmkOdV<%qSo6dvK7+}eP|Y+gF?}azM?3wc9w7`l6Naf&G?66$J`WR? z=fW2CX3%d(MPX@WYY^47$HkBJs)zBq)o9imhn;P|c_6ruthZN}?#O&!EF!Uh_tdK~ z3ej6AJ~ZNxF1`M7EIffDCnZiSV@KU3C=1yUpTGW`=U2i{qQOmU+DjoRreDpHOct5O zztR}T5b-?fQKb*8qzPkMMbYF611MoxkC_c}UN4~PtrVA9Yl%iPbwcHQT@dM~gStZY zXTB@~XL@9%u?ts**{8>}#dnXc`3rSZ;mK+X8E&O({&E6C3%7i^@q%^j@wvVbvC;*c zKdS4Kai#pP2;Ka#L3wq0Uwk->Njejp2p*zS$2yBy!UqavTPIWP>aRrYeU!jTIGss? zK?s~>LKB&td=rI&*v+Y_iZIz9ILk3SgwN<$DtgeAsPaIgI!1GATHl$|B8&oORCT6* z2`1sihhFzYgPvYPL|8BM;a?$0pw?ltgtoh$=fJr&geDW(y`g68qN2HOvKkXnM_Sbv zF8C=;d@20XK~OiAu~UY3quhkqPl1H(-Hm#qtGXpC9?oYiU(1arCT5)Y@!r7>?GrX)9@~dO%1O$3 zGy7%>u}0fm@(w1ZZkuP#{rYR=^{(RQg1J+1s8nO!9-%0P&O4(d5ps5f=6z1;4?_AX z!4q^pm7L=oPBZ-n&Gh~F_hv$TxXox@R#7kE;Zg4jNFju?E=VA+w3M8eZBPYB%ZJxM z+!rP@VbQ{25hpQK7>(7}H0Vu+z-ah5ega!1j$uilfG-vUX9HhSmh%AWl@ZO;lz? z^GJ1A2Es;JM!6eDgzVS<3RLC)Y7p{z8A`xqneY=8V!}b4s|0*!2QFXFf3fl-@H=b~D>}SDpL!hzKP`e&!qpRZdCGT)B>i{eGQFcD)}* z3r6oOK`3)>qBB4&l14h==kX&ti=v2cT6#x7tW1>hRPWDVW%gwU$2?Dr+I7x)B*r&*>K{mRq6HHZ-S?IXQLQ^w7ALd5Cog(4Zh*ow5Bnt6j-OaKCeH|)a>>Wz`M6)nx@EhkGC2j^Xj z&zQ>1wT(~6H=70#ltWAg9;E7aR+Z!Ou{Dufidin4cB!=uj48Q52_#h)E-v*?j#N`d zy?9jv*5Ugwi#=fXyy5dG7_j?L_yza?Si*&ev2%1mw1%zmbr)W%!Q*LJst)E|^7Kb3 zYgz(B8KrbPRF?&lZW(73Pq-DJj10I|8-#Ja-apH(!A$@s)Wa`?{tZJuH_EvA#FVEAw|nUEX!W=XFEpj5h`gu*n#B9f#E;df=lOB5DEC_RQ$J=A zfh-Pf+0<1SbkEkbW4Fa5Qto`Y76n^sv4#Sy*H1Cua%ytzalTw{;3w$xaVqWzq(Y;j zf`C}jKPz%6E_6VB+~RX2UpL(lorcmPEpU_^4R!ie{r)Yuxh5Q#(K6Cn2w-sA@|HPZ z>EH$14bO9FfNJyoSd!oDG(Ko4N~WmI?mK!7P*%ajpGYS;$Ac^eD_k-)!0(C})kH9v zq%C^v^#G4+0L=1_x94ni`%Ml22?2O^8EIP`JqT;AxE8t~6NY22JY+wPV<1mONNk;p zYkSphS1vGZsz}%tb!sg1y?}+97e^Seg!!sNyRw1F)IkHTq7Mq*7yRky>n>j|M z)x_h*%-`Bmwxx#^f10-3GX*x(GS!-fN|6rS1dKe7TDC|v{KxkIaR32`l-mRLWtZl5 zD=>O6-2rd(gzCv#2g8>RGQR|J!IA=-CRRc{td0!ssEj5JEoL@h(Fr2$I) z!|s%osV>2s^LJXpfl@_5Pwhpbe8Q645+1i*UC0Gqu} zgu(&728IK8<^LuJJph;;Ty^d_dmpRx>U!kd1o%X)Jjta1w1{2kq@%B4r8?avSk#*| zB_g_>T?XQbN86tc+q*hA34s2-T5=khjVz*Cyu&d$3*boVAc~kRi!sL49XE zZM+OT;&Xp_1*&AmZ5~+eeskA3uyr1=&j1($2p5D+8N->AO_D%N$U7S|__nB4E@7&Q zLnqv`CXyLI)^JY%2=;6t{ftvR4;{>Rl8%I^4a}JxjEKzf=Do?Ko>E_6&V-*1Pv}W1 zvzy;*rF?)M{7;&!J@;VsC?9}+wwfsAb=zjOJy`Wc0#cs~pVFR*C&tR(o2r;7G@X^Z zd|avi99CNcy;yGUHhuKkup%7=4BY4H9sAg&DyHuVRFHNY{nh@H4V5W3qB>V5usgr7_{z}tF_9MM#=rx>v2+f|g}z5V-EaMH!&2t|K+R;U>7HMXA&}1= z=xD?P2P%S@3pxYmfS-5N%R#{r;)vm2|ACbE-ERaef2@}0w~3ld+n)wsI;?B(j6TT) zgm>{D(?H?pw$_nQ;8J{0%PgB`lU@-e54j6z8#v|doyR{k@cuk>cvQzt?Fzht~zg`a@Xz4Y$>N5&9=Nx`eqjl>HJs5|=_vOkc zw0k^c|FC)SoRVu_DHDJ=fbIZ`FCcBw;`Aj@ z`J*($^_e3dk~u#cGW~FQr^Hz!>$5%31De%daX;Iz_vFX6=>sB#NHx7qV;Y*A2*nMU z1fEYvc;TEtqlwO)<|jqW8$0qB3M%ZYAL{P8Oor_odOrr3Cor2TahANjVUu)oIs`gi zaOZxZHb#_1-qB@cH=`mxg)cT<0CFDm`Ta{k30Z8Jt(#S(JNMiL8m}S&Sfx7vApat0 z7XY0g|H0$vP?z%Jx}sw|>G$VDgcXu(4UDax&?x&(!_kV5*#n`x84s*1C3?A;DY-zV zucgHnpQuFqe3L@^iU}rcw}>M+;0_bG+K>`XCwLz*mPkuaXekm{BRgGhY6f}!I<_kG z_=wS6n%Wp?Q!i)g9RBy98Q9ES;NPAi_!fLYu>dPietHc*Zve3Ve7W*{e_%5Jh*8)R zA^DV{0%zpo!UVzupmeZ9^z8p)jV(d5z42@My4P;GSH`? zO;ZWbwzSX__38CVTq_k6PMSNl^b@Gnc z&{lHdq@Cz3(TiU?g8fg2`NmTonUjb~uHSEwYFmSM7aP_0vNDhc=Vz4r*XGu?H3q;f z^J|JaTg?G;Tgbp53>r(vTHlkDRGQkt?vSE_KXdBw$VZ)%vmkdeyrs{< z(kF7G8&O}yAlj(U*iAr1$CUZiMsZy=rsGjxn_VDgmn7N)_?F8f3f%^ z54DI;hXYE|KDy-GR1P?d)X$fpcZN}KE3Q$%xzm6=_iPFTEQPqrChQhmIS~-2kH(14 z@hAn#;WaUVaZ9@o9xI-E%duLO#-F4WRKOnw1 z^v0^4WRdo?bdG&@5t?h9^^j`!7wp(zCtM;BTxG-W3+@ihpp?oceS-d{P_$H>%%hAW zr2*~a;-KpKaZ^;$b}A zj?}(jTGIKu*FTIczm96mc2fTb>I{Lo`zId}F_Bsfl-+LNc5MonosKf&V4Espb_Je$ zFy?#Q%P4KzwUEpM7VR%FhF^em}1!FdPP}iF{=29Ml2< zwBhUUJ1Rhb)$M=KQUS1%8lV&|NwH*%@CPyUG7nHE_b9*Frmff*wkhhmF}?YOFU(Ce zwCMHnY3vDWoh0PPL+X7ERoq(nKbg@muUkhkJmOA+>_Ag`uO(IerD9Lg9WsKv#!4vy zmNQ=g!`vI?OV;++3yjQtS3>#=5mA4$A=nHh>u{0db(b7-g%1s(aVk4K;yu`@Rs_`B zcA~#JglT(|G#;iW)%a9hD);}L3uE=EBk_5jq2BTo91^!X? z!#16*OU~I;lBS&2K#GULpU_Fj%Ok3XAAQu@AG*BW`5|sFEQ)g`01{g}igSu9*e8Pi z)CFf}OH8{7G%ML@cU651c&x;Eimh~v_}2fSEm*#cB#n5@kDjK%;79nw7}R8OhhRng z8OXGN5q{`ndxb~*PU$o$xj%s9@pOGist%Sm-6yk%(tkozvuA6FT5LI6n!R?lI0tkv z@Dd>6s(_9YRcb*XCULYyMVA`tbz~+Xv^dX0{7y{xdt!`hukA|g=W*65uP`Qp?|4Q2 zoQwVj*7-RFOLTS%n@CcaqK1dN1T=kV9r%~c$Y z_XY2B0ka#>`oRmp8HEr)`v1W-EGX6-rT-YzGXJHJssT9|H|3n-eaT{Z2gR-Y0TXbm zlA1PV*N+=ZdYIDOQ_=ZeZCd|nW>Qg2tc9-?81FB;L+J;O?~cl+(@e2L8~RzSqg)1? zO3wHOV_k%`xEa5`X$j%S-jGpJa&S(MF&|d;sZ=)cUKU)c3lGT`ir{{cJ#pj@xj4#gDju2bHflXVY}&3;w-d;Wl0USuAoy*#E23kA z|L!T4@tg^`J3;9fdin5lvA$n_^m~C~#elmLePMeWt8iHL=n}f4F6DD=T36_D>L1YH zRjEd=o3-C6kM%3Do8XjnHr`tmC-+tk@Na6+Ta|7e!u9s`Y^kSfLouP4qa;};{W0}F znbJ)GC&WKEU!X;)FGM+K!V(0tPB*c9rl?q9lb{@Erq8-Fj2#CcTwb;+G#V2kH#*Jd)% zCeA`;JeG)D_2>%2rgTS7s)e)WwF;Yf`tH6RU40s%?R@%TirG(Gr!Q)m*vI~|?oLJj zlHH!r8SSfBjJk{$GmKwLt*_nH{wbd3bh)rWfK5NvtHc}05xjop-zvKQEaJV8Q@f@E zx*3AKxA~KSa2K5T-ci`VfwVrMF<@0cGhJ!($f?C6Jv~^Bw4BRrh^yhtBKGI6e-a=+ z>n=+S!b*pOYFYc!it!SFR$Ygq(o;|#173ANzgccZ89%vi1FnDA!~-4-!}m?$4ZDCN zWM9s2?gCKQ4J!mh(nLQmmCT^@)g% zO$W5>TEQHp3Ys}+0S~Iumja&CWHDv}T&hW{PxI}{sjhEeDWk`f$ z)CfT$LZc$WamVEUf%-bUw6#_S+(IAWw}zrl2(i&-yH%4$Ecl0HomD@WQ#A7aSRxKV zk9FT%ZxjjFzdlQ8$eb1oHCy%d#^djS*tZQ8JA3E*UBQF-Ff=KGVQg+ii98>N=JZ@R zzPVEyYyb0q^h@tGXT$bj&8X$YsE%v&_0mPd9_o5Y2v(tP1(m@5&H8Cy-~QwwuHSS| zBlXv(I?oWlnLcP$>vVpE(@=QA^IIRIxfiJdoSZPd2CA;u@)6T)a;@&}%Av7Cp{1MA zRG;ZA{{q!Q3Eh+1<-tz1O}s;I@KUK6%G3{~TlH=vc7D8{jAWKYU8d`S5-m$Ih{cO@ zPC&7CMPC=Pshdcp_GFRzUUXo=oz;y;@33bb?mv$cP?9sE-*P@RmPmnC`u@f~BG?9b z@Np`UHP2zIL@tSt0&+jbjQ7X+i}$y{Co4WCc*arO8#gX2Y#n5{&sO@F9amqLx~~1( zfEvI~*gbD4N1wL?hUfcN2a$Ta2ebdw07$%+I@JKwDneU$zANKFjfi#k_XQeE3wsef@kD{A}ECz}2ZABz2%9t~!Q`43e8 zkoxD76Cz31Rbn2syc5?j{aHZ)En&*H=zxj7#Yg62bv(0zx4O=FOE>CxvWzlVqDc%{ zbF)yL(tnn-kOXRZsipFcq2T~=5lOcf)eDHdO$p#?s|i#B++^qcNH|#0bIGPpmB}zP znc$suxj=7|`ZRS>u@p;rngi7+UC#c^Xe{(6hvT2PZl)H!4Vaa2$&RMy+3q}z$B;&1+f*|Cd!tC>ubQJSu^`(W{i5FZb+ z8qzQ6Ft_xpbXv^x36-;G)!FUC6M?{IFJpgxH!db_uv89%d#}Fc>Iislqngr#;3xTV zBmu*U!so`+a=o-KqQP!C6HT0TIcvF%|5j+;k{ywiP{0+GEh*SgB3gqa(mbAO5Fu58 zt*MgF+{H{IpRV42;bnM+#y5pCixfs36IqkkI5>eAI}1KT-qD2UyEprN`>;ZvyJ28R zRv(;gAZ9t!rzY|==-r6~vnQBqn~&d1s3oO9RmDvlPm9<=#?b1AlJ-u~PZyvzt-F&% zl=fI92VcXezX^$J2cpaaR@onP-f*ClcCOqA7-F+6O}3TF&z_C(F$=*Gpwl>b&aw6K zIFB!rR3RrlUrpl!@WL0l4ARD@#GW2saaZdI6Nrg&yL88yRm-2z8UFeZ%rV_J&zrUK zrS%~RC*!PN%@(&rejBUNOeaXCk&*qFnM?Pa`4qOHPd@v`Ij=3WFP>*Z*(Z?kPl1*e zkV?@R2DiX;OH<}`@Zn42#8^%{GJ>gz!qjvZq`@L(H8G$MVZT|NeT=ZCZEzack4=Yj zGwL-W91tyil@*Hs0SM1)YpTqXP*UbWMNZZ_sZnoY<}456rYH>G_svDFj8K1fe@|V0r^lUfNdgna~X1 z=BJ-|Z|=UAV-r){YT$|PA!-s#&|Rr&_7B6G`Ji}~J959zK(&&1>oExYGo4Zw{0Gx` zm%`GNemzuuJK+O;>a?Om4z0m`Lwx?d_yfFXtE4{*FKJw8yF%hpwf%nwU1^q^{$xxb zSIuE7y&dF(^74x5amEx}&$CVEsH*R0T-w_dhXdh}-m-Q$3cIjW$$j}nP`#i36MvTV;&X_~Yy zE|zx3`0KL&xq+{VciF%Pu&2yCu7vcmws$;F38#1?AfO~;0iI!nGBUizcNIw`T4E{! z53^iXwL(@MbJK+D`g-GjEt_GE{E>D3C?GcBVN6kD?YU%4nXc;F&g?7fB%hlhxGO!X zLS#EFFwN07N%WOt8b_F1^nzvUF!iZ5EK~r>ZnnO@#uT;0Nc_CuCC4_Ofu1;`{Un9D ze%+>KO=3$7vM6GP=VCh>`k^GWVxWa^Ow?( z7vIqG-;aI*;Vk94a9G9l#eBb>Rs)CSqTDM$`-KA@Dr}QTv9~D}O<$)&%t}m&!Pj;m z5ZTRw0+TU3s=Y=QK3!0l z+dvBS$}Dl@&sGZy?wlBWQj>y?Gu)?Cynyoyh^Ac=C#A*U;C4`^89xj8tL}l9wrvoX zwI17JI&;A#yrjAO#eu$CmxA)@&4Wq9<83<|Q}glA^00Ul$af;frz*~kfHLK7L%Als zsXo-bnVwXdzwJ4Hwlm)M9Hw}1Kdthm0bAO^Wv#Mx!Y*_}dX3Z8Yf0;7n8{8G z|NZfLRi{e1+@om_h<=efsqxX0G|7azrNhVM;6n7C-{tfMs&8o=3ws;WX)ck$Br@jN z=l!ZxOiNF5;(K|?HdyyB1Y!D}v_if5TON_d?pLo*R<`Ub<#D z-6BUq1ybK?t)yi8@2dp%C*lqomc4$7?Vpr#tlXy9w%0V%U9-jLF>FmexIL2i=!S34 zOe(*SAfFAFWx4QwpKh4$0sHH+J1mCGzH^zK|6&6g@Pu4C3Oo*qHcyV!ZVT4(*UhK(TTz>g=mE7l)s>pK@l!-2M+ z5vmIhDb8@wS@ni7!NX!6#oJDgj8WUQT=O}^yok(%Lq}%w70fRUgF1SVcCr!&mkfrV zBUF@zuY7-;;S)c7zabBr?Dk~E1(T#Di|dYUi65?>S$)M?tnuzmvGjzjp>e@!TSqbv zBuH*l;5VU~>aUGnb_R}`9*t6B*>n6oqpadgjy@Chhm%X<2b+gQhpB?6$W-w4NoL4- z@|uQb5>%OIoE%0vg^~C(GWo*Y?yDfNX+~P?^2(*9qx|<8pZ{*_AI1QQ4*h8CPWKIN z2K8SVmD&|(;Z{{2jz3{eed}QIN?I@l-q{)Xi!bn?Z-|a?aH`?--=4hsUuFVP8)nAK ziW@}R*{+J<^X$rgibcnB+j79!4|wa~!j6XBV0wFeZc91wcm_#gi+CgalKV$Kb6CfF3|4p6hRKZBy8%Ww6@+V1N$k5O^^)d0>tzP_s z(bl$*aMbA(8DN=v!3nV|?Ho+aNIqGf2=n4)!Lt*^q%~}`ZW4$x?|Fw&JgORIth_hl zXokimEq##JieImjv-h_@iN6zPS90A*zDG@e$WfFYxqhvw5^$Lon)uhRgrq{9q?m7Zk*^? ziFp<2dXVCOpj0PSmt=kK&fz_iu22M&AUHEP3zXkB2xI}MlEDSNm{T4dKcL|WH2ut3 z_Az3ZK`_Cz8kQ}dd2#HI#)YLnhjQ`15$nWVT~Xd}av(l&6vc7KCY>u>Rt|N?V?ZD4 z{(P-4#^}~fgZqcqKt$M*qP1W;S&6?S<8%3_o?_(=L6-30)ZJ+S2XegTTDUp8Mh^B_ zPbZRp_M)a3PWG^~!D8HRF4NDyMkSGZ2>UXl=Zy82eQ4SZ#=}FLhpnY-KzC$n#4{2ihV?RI$2W+ z_kOeIs?Dh~Mc?z6eD5^#vMe}>=b~{9drTI-n@$mezHC@BJDPpfIuqX3;5!NZA^y3M zU?=7Lm=RK$=Un;rLzF0;vBd0*gFuCuSBH8>hMlac?PLHyRZ}`)9lY3!Z+Lv-KM<*A zt5csi?;^s&0IyPjWct(Jf}VE3mv0?V1yy+-Bq5skIV{1{?J7a5b&+>vj0Kz#xgkW& zY+^GXCzPB2H!UXbRsN*{dg^QMV;n-`*pjwY-m#L&D%Qd9Ha)IzmtmokT5C%8M)4^` z6Eth(IcVrR_a`IqrP^lLBEOdmMvb>!O3hW%6qcx8(%ikYaOqof)Sp~z* zgC!&X$X{jVHpRot3qra@)>wo@`1;GXx}-RzU%U~2^dPQhbX)-2=)v7BO_H&isE0)TOUGLIr`-F0%o5bS zb954ggTJ*gvOp|9w2#z_=+s53o z48pMw)vIJ70zzgMqC}v#>v}y&v)pP=xp3+69jlyUwUbFl_w8f1*(NgShx8M532{b; zPyGi=92{5kx?`5VOYGs<93^NshFookJi^2IdX-k%-msQm@8i=s8}SU+5k}5ar1C`- zl3_{Af}LR-ik*kL2}AM=LdO`>)KVsL_@pY#RGu0?}I9-<|XQe^aZ}*>#e$9G%?BD*s+3n5TCsR?WKj zh;Cq~Ij9`OdGlB=21*<$fmloi(>qT{=}WYlm(p|@Y5Ej z`buBQF|*waqP{QPoNP;^tN)$`;||N#R))|xc&3cOJpBE|TU({6l7^hW;G_RmGSPud za(;2g;DbA@xJ3v`sU$>aNj~^p^=4njo#@vXmOK$PT~QAC&I`guy0IU3;hs%SGpiMm z0Fkw~eWP)WDG?*BdF?3Gi4fP7*d=1TW93wr(x;6M;$DPl{vk07IaFUpNlQCCbTHcmULy3}b8NU1CeySY?!#m_ z?Slv0Z7MT|%oX=C)gM^`4LDp#q@;t+gw=FCJDanC{0x>_hX^OtNgx@(18_v3_G}82 zY87pv(DV0k_+0;wqqA^p@_pR)HefW2?iPf>V51S#(MmbGJET)eP-#XgN`pv9H;f)V zasx@}RIq415?{g}@B1G-$ByIK@!a=yUgrfG^Zg*5q)!wDVVWs4DYYV=Bg+aV8ArQl z^9Qje;d=cmSo~%#bKu^V7DxNO8vE>+TX6>7IjDOZiD0apvuQ}|lj)}RY_8;_bHvov z$?Qc=g|vTbwJ_89d4%CIpCPxMuZ116gI9u_iWOs11I26eq%>)kZ<0yfXYnV;OlQ;P zELjhHRZvrzu9CMi<<&a2a7&Wv#huhZ0DH6Mr1J+rqlKR%TPE76cVu1IVkR*hdK^eE ztN2{$it%R45jASRZ=4n9S$)j%>HfZ0X~5{)0&@?*0`)3M&(zP4zgX!c=Fl>7lKF-S zMiuH+3kV}3-)SQ0BoU4ne|4ME%*Tml*<^LutHuQebCqMX*B8mI7ITZo%MbPYoZtb6 zAgrEk_)aT!U;YW`xfy2~hQH>} z07HnlAv8cmNv?amIq!B|y~JvbuY6#CT=Mp%i&$WOTG;mhl9RT~nRKx|F`6RY^;orrKQzC`^fy zy+xiS%-n%QzpS5}@%7aNa1qDG>r}>{QbA2k9&#t&u?G*j&C>oyJN5EWXDl8UD2nCe zy{qtMOijX&$@iY5ku#Z3?lGA@#VOswaEw`ppIo${IRQ9)Mo6U1s$Awz5vX z7Hq~szc1Xsw5qn8>3(^APs>JSwA4;<$s} zQA^)fxcwtwpe|CZwhYi@D12Do1{gr6C3ceC>w<8^25cZ|YIChP5&lhlTe}8@0-nFN z$sGt;aRBJ=VUa8q{_5_Gq?i&TDBV>bXQk+3?}YYW_WCB_Z>>sFvW8h10k)W)Mf!pD zcaOnV`lQOs!~L9mDZ%Gx$=`1!BkefCh&MV<2Hk48_X1tZOgqQTpj3EmD*jFNY7uta zrZS4t{MDGFB8792N-`E{>^{&KCc;#9vI#57!U`5>SoLRl-48Xk)U+EY+CIX~KIYmX zVX+=Fbt>$Gyr9ZoxAzft+@E+ybtqN;hJJ!FiDL$S{p;1RjNiwWGC49LT8~V~K*^{1 zr|RQZw>x*=`?(Q^5Q`$R5BJ=eMV%Z0Nq+x5>iqrR%lkzczw66J7QFEML+KcT>e5dY z;!6G0T_>xdpGx4$%_zOOG1p+~YR*5OxCODP^$p&4PE!NGvp6S-OcbE%X0uer^zAzL z*=AHdUbfMf>nkckz`6?=mu^gRnWFmE%oHPT-TKB&W7^7UP?xZ93Ljid$W*_xyx#I7 zYlzzUDQ3*Cc(A%$)ZM=s>7*Y{6O8A-EU_|H#72l=Z)b1ZOiP`sVU0yR#_4GU_VnTxomz{bTp+l2r{ew8T1LK-1h3rqZ1r8 zW4@zhPJnl$H$d1sf0vF}C7&4?Vbq$UEfPU#_f@HZ!(ix3j?sq%hr6X}U@7zpPZByN zpZg-;3e`~CQDy+x@?6t-`?CJ`^`NQesO}XODO$Dv7~_(~-}AkL)i|zw6-gwNNb3}< zp5&e5kF2FKjoiTDS(6n7$t)h`E!)UwFlXvjhnzLs3mlN}fwKf><+=U)ma!$L0r~&ip zb4TnynNEe5#H!MX--x6Kwe2K!pWwZ;mdxlc%#r%v75@SBRU+BD%C<12!9`ZL)3UiL zmyviKx8P#buqyp$2BGDsVT{QeCo?wf#5l=*vKQNk)Z*2-s!~eMD`Hx@+VuXw5j1}X0QII~zK=PJ`(cr*y5w5?Rl3{) zsEZ1J&ws8~HHP>A)&TmJVBgx#7tFGF>0*LdmiYre%9BbBFKrw^^D;b}iyu zrlW*AKwHvqK+^jP%Zt<$vg$=jXWFrw-u4&lpt^s6P5W+misH_#fUKUcyrMqLyRFC` zS5(Y!Tk_O|6HYQ&E+Zy~szqa*YbO6Bm^z0 zKP*L?)myg6>SmzlVY@#oR$bW3=!y_(;OBT}r}u*-ge8Dzl@uCSJ{&r9sC*?a>LD zV*wz_RdruxTA1nH&6YcHBUKQOKs)(6;1=unnRE@L*pz!(%k>0a5%dIgGWa>V99TfG z5~=g~B$;mTYE$2Nnbr29^3nh-i`ZnJyygS`RBJUM;t9!~#8{T2EA6zL%j~va7)!-% zu9jv}P996-gmfG^&Xj!evBCmhT-omOD?g3-ubZdpf7$=Dot+7odWkTqj)MI0V;%eW z+%Iyxv)dNqZ1{Fb@xz-X#kHj+2)!b5Z5JZ$Rl7AjEk=UZ3 zH15{12rb?#6Q8s$rjByGYM`Ji4>?rWwU}SH@PO#wNd|{E9e~EK1&!mG(4>IwE1g)H zI{{zNE^NEj?^U4$Rn+O|E#K25%7@LY?-RkZmv0Y79v!a+#T||bLs-)1e;2jv_92p0 z|F>I;k#qBSmg5jFpFX}R*;gd}JO)-GP_Q@A1A-`E$cg*nKf$4EDeoTr2aI)h|1YR_ z{hmw&Bb>5E_?o(! zB%5(t`1o_L@vn8IHv`O6z=8IRenwaA#KNN7^8z)hQ?a6rV+c!OVu$?#21(7xTNZ@d z+2v`QgYBaU#?r;cz#f;Z6rt=tId2?lMTZNuffWZ*?B9tFhT726dn3DY`^;~zCUm|w z#c$jwmx0#95*vs~@G)!A(&sG-$ma`*f@ zEo)jj1LS!Hu_;i!0sdR`xtK6LtFf9e(TA>K!Iip@TJ9s4f$C2<*pbl?EnN}0*1@&X zAa5k97UVmP{^la|F?uT@=GUj8%~cAyWskD*@NrS+k2wvxT&oPU>yEzqQAns2sM?QYhWJ@PTYN@{trOldn9@^s9KCKH8k^pEX8_Ir%zl|*A0AmH zu^+|6y}wMh;vAgr2nuc+HIiFoB63)kkK*L&Pn##qS}gnpc|_^gQhlzXd#di+FYFL1 zf$+j}bNC*eN<67pAQ@;j<6w~|RFDb=Gah7eDzr-0<$_M%f;*cse$Mb(1ewEz1Uo>crT?RPMLSigf5Y^V zs$D*js(qZ9iuJ!pJe$b}RdYK@%H#;m1xc4{9=GRh+3U1C^nar=yz^Vm>@mRC6TR=b zk4r)8!_XaVdctyYg(Y>;Da>3iiWW2*K9H%0Uj+ayC{{zjW>hsCB~iHPbwu=7d*3zJwyS&Y z(6MB-p@KOMK!zbQ@`{U%fr+hJ=3MPUGf7(MOK$xbr&-+iC2mkwAVja1ruTSSeSn!MxsIg>Sk)7RnAZSeu z%1Z41#liBgij`y!Z#Nn|(PiT>_nh1VAe%pkG+9P0`DM&)y`Fl;iWvNXC7B1XX3|h; zg)mh(WMFzBU)#t$q7G!3G46xpB*DoF?b3?->+XpbqJ{+7;Y~R>$=8Iw{&{4{(6;^{)Xf;zHw_3wt-i zMD7b_zkT;dcOKq4LYcV_Bu-un@uNxf{#Q|vV5&&J&;~G~k8KGQi$xeOKQN!Q-c}+n zlFuLJP=N+kbL@lDAtN|SGg+Tz=Jlw>oE3;q|I2+;GzFUOmNf~IMeFLAbAEKzo~ZJB z*bJ832uetGbf)vLfI?pGozGH%|0Y{DIRag+=Gw^ZzwFxh*iQi4QR^p$ud3kHQGdZZ zon$={joFkQrcEdq(aZj2V{Nz_K~`>#tfr+iu}~CEUI`0Ae&hckbd4)+Fk{BYR{I`P zDHB1Ib}A_FS=rW)=vQC-;)djTTHgV)NfEe5t&ifr{wY?}kyH`Sm=Dj> z`QB3_K?h#PF9Zem9HwrGoYuFmI2jJp)iBrdLi>g z-ecGKjFD54_*@~5cX3zR-Khq@(y8oL3XVuI%46$W@4;nDUCJIgUR-yc$x*V%U^UW5 z25O&6w_TawpXCk+ET2U z91z?GGnxt$SIkUNmbRh36ci_6cgp+BwA*d|2BSlT@WJ45%VSs4*E`eI&xI8+dD>6uf1aSi*Np; z8+9eW!oRm7%FSr4b=c>TtYefD|86DvJey{dN1>PS0?Zk%QKl}1^8YGx3NTbflZcF7 zX2gSgX5YB*xyrJq*qb@IfW6)dJ=+xoGreJpZ~^KeMhGgbj~JU}>Q>d3p`sr6xg8Q;sd+fjfdT%ekZsvnCUlqjeQZP@^5^Fx`GA3dfn_D*<5qJ`=#;&FJwyKO8 z@pJs`XQskd+uV;S___raSp@vyXzgVt;~kTeDFWFC12CRG$2oSZD|HgsO4k=M1i${; z;rvLHR-p$*t>{&`pImlo&XXct$rKZtD;-SZx`B)+!9+4FnX~H(>=*+*_TE~N>lGUs zT%seJw=}xWoCG6QZEQu^&|}`23!5%uQZg&Ho_GV1%UACkm?DpiT7(W6DD)$rPnJG4 zzuo@5f5mg)9S`_%rt+U>|8>LX;HH|-Ro)hlc>W?3s1tYWh7FQz_CiCb-#}QbAv#}m zjUMy{iME9R9^8|?#0_i^Uy7~N1@4PMeD&Ew5jXgSN#5SnIx8yYd(+NI-723~0+G*` z!7pCy)ypsdBAHp1&FZyzo?OQ9Cxq~`ve);=I&CxP_HBckUu_j;e$bE^AA7&0<8`X; z`1&%V+q)4R$@uytL!UkA83x{(6pw)dhw4KOG9%HC7L=%fgp#kd3@jZX{IiV$>Vd3x ztJlqmb%hwEZ6mh`0qI(H{s1G`wf?Q$Z&Mxh7iqx8 zzf{tPq8KxpZ@m?GlO zS_Z&vuygi}p=)0!7>RCe?aHj~yg^5Rn~qZOQpUdEu)%C* zK$Yk-F{6#36V~e3VgC@B>jv{**tKcek?*pE9py2jpmXPUq=H18KdHqxFrJpY!Zk9- z6cVDipcVDD=K7=r`)yEqk2{QSY~9+LTtE2ZxHa@LiZ(AJbOmLu?fkpu;d=qWCLW&y zHV3?xrU#~c3`g^c59+( zLB5l<1^*$2sH9K*w$r8Xmtpkd^!5hsL@tK`vzDHwtb+F=HeVHizCw9N)mtkQpZCXt zV8VmqE)^M4#!_Fdl!HvMz>nc7f=q8hfS_WNxvad~GSkh>Qa@sCm&uupVB=NNELVt@ zkdNyUjq}q!N|yJq`5F?cJrDnMlHmepje28B%VTkAvqRFfu&Crd6Zjf|HY6%jM5Knsz)1%)L66~lL7qZ6XQ4eu|{D6cV{hol5%WcZ;%5b z+mKY8+_D^Iis4O72n}r)ls~xce4JC9=kw(~FAbz%0hnvXb_f8O6uN}UY;SYsFJKjg zjQTl4heeE44p>}sDR&5ko~GzFzA^g6#IQp63#YYk!c8JyjO)swxR5iRB|SwWC`YMp zn?xuk_~si#sM#8F+N?>Id>M7g*2&>u?f3!Rl_w?!7q;z{2Sc7KA{PKKG~JhfC9Vizy^hO1O(^O*S{eV@ugtg)_~ zQ|;ab5=9GF6{fRzW*3Nf|g`k``Pq1_7nI-QL zsAYe7Nb#}qS?Yb4(`9m4E-A~kGJ7G*di6!#b8H1TjrOi6NLMnmI$Yu!nn0;H9@)=H zI`QM24_d&HBiLtUXUX(7|7G3H})eI%@l2E3ZTvs!F{H}J!4OK?(%Cl_=V8%+IE(BlO$B>Qx<@`a^MMn#v?4I?eJee$yI zQP4uPYUf7;e?U$?S68GK_uCyBH-yZAmc%vWWT3e zIRvRw?R5z#rX zy&q5-D9RVlc(tmX+Z-qwO}mw}t~_C%U}%He<^^ver}m#TGW-7nFbIC;c`ukM;m4d4 zxA=1&TuA($;;jS=%24k#5MpY#Pcn_XR?_+4;b~qHWxCw{p;<7A82@mmKi%Lm@+si+ zAtS1aHIDY+0j~oRx1|tm&pDW*2`-~hiN{h{?B4ae0wwQFX^k^yRL{PC^quf`vd>6= zs_4W2i_yc0+u93ugEqz2?RZ;%B-RYi)dIFle<@Dr8jkjR&hC4AY_=TpnB&>JrfnTa zKb8?vy)GiUATIv*W7D0=j=wDfDt9?y&yA7vm2Ud);cS416smnOCp0 z*b!e6UYnw>=f+lC6m96mBg*#kyC^;Mx3Y_ss|Xm5#I>`xbl`NuL*dG8;w4zR8(T86 z0QlvQXEaM&?%A=}Ncsy*d2UqyHgTiIw_c$lu2}VpB%lYa@Fr!Lwboip)#_se;8~&u znq9I^5xAjQa!VK(ce}1j=Eo&GtOxNz4J5lRG!V$8wy!I;(g6)nh>l{{cw># zwrsGAqim{wmtCyMdUr{im`*bgtEC{r+CC_uTMScsnaYNz*Qm!*;1#r1lcM}xQ^L^q zeG~3C@uy?Qsr-TB1aap*isOI?6V}>3IpMCzC^3yHq8&}iC}l9{gV$W>b8x!d6kngB zGjpyZ4oPe^2w)1HhM1XoVBF!%%X2ayR{ko$2qrA%+p1!CFUEa0U0#@b&yp*C)yfvr z%bJe^3ij;7p@k(;|%V*l;5%4?OBPnMpIAq zIwW&b_;)GZY{2yZOnTQ76-9Oi%Ee|oX$)ixAzyId1SN-rwEpp(_H_-ZtQ2^ZO|0W( zro$fBmByD>IpqYsMMa4NxITq~gDB|w&KTHE;j0jf_;lZQB&`6~98XNw9I!~uy4q(v z-}Y*AV_eCy>|MktYU;JnbQsO}22PyMy)M(tJh*Y28dz#gNKs*EO!3S`7!=Ng*duH1 zA?_}a4Rf5@(8KAhq;#dc(m{;hAK9z$ZPFgNhA?XvDV##G%DhKa3Z==zC+Wmf_u7ya zdUjlSRuI>9=7nvWnz`O=>nifGHKoD+KpIqBse6^eKY;3~z76F@M-;1!arJ@=MF{(L zfe>t7a~X7yW{Xy<$~=2&@1EP0;(DIhuU&LVf(+JrNF{ts7`JaYk4!A4z%58dVGL-a zFSAJ8K7P{m9rlRSp{I@(E0$<_y;(I!zrb>dJ&p#rkH+r z@#ZlSZZB8f+_=dNxQ?u|)Q4_mN68;*Ll693_RLg#z^DdLoH@u5IV=jqno_;Rjxu#g zY>U6zUko2qj~1-t6$5eaKRPw}i#Clg$Tb|H-|-hfeXf4f^ygGmd6YFADxahpE3r=l zuJYb7eK$J4#9W!hlPIXh*k$`za-Kq7pgjU}ci##Y$TNUmsCF^?3dRFTPJYv3o_RWm zL8G=r>|~f0PbK+f;hZ!(A26f}9d^}C6n#yiAP-;TqUHd(?puCglX?he1})mq!=$@p z-cgUB$=TGWdW5cMof)3n$~0KC8x2h#pRCqTOnIMeWh8^q3rW=KCxQ;Mh;(!crXbxD zK2iii7^7<5Al!t^dhCYUyk`X~DiLI!6uoP2TfVMUbljB0uq9X}US@y`d#sucs10B3 zr0r)?v@JUlHScMr&A;YXGT_JN6Kgqqq@nK}c63@eT4EcDmsIXuS71(*{S+B^y^%%{70c!rmyIY_TQW zmDA`Nt=qI~&)W=(INcYnRU2dF&o4RG;z;&NKPK^Ny?qVZK}(Z< z(I0R5{ff*}=WAJjOYqq8Y~sj?7bL2xfX;9v$05L%HEutzT?LLQQ_X}M)1glP0a&%W zJ(9}w11a>5^o#h(1T`{5I@#C2E<&LC>;IV!aSb^h_12bsevV0`LKE}69?J~;DqG3{ZsHsUy zZ)%a)^wX!1V5x61s}HJSB8`6MStRn<^m_EyD%A1PTVxg(1Vx9|tskj)Hgp_{d4;qX z`}$WL+}od(3GWLOYXX0vB6h;cSZ^1~s#mJ)#{&CzD)k1-W#V^j=3PFZUuLrIshd4+ zq@=JLd~_5m@}d`0NKq9)f0q>d?E;?r4o?}?4ysK^WLkJMc|}*9n|Xgkr}H=8to9Ga zd)($F2)mZ{pGponTL>MB{{tKdqG&Yb#LU;R1M}wHy?-*eazP4Z7vf}@lS#pA$8v1x z>jzsI)xN&mJXq<9_Y|##c5!f?AVUG&i`ZkoHgFnTQZ)+~bIvnG^AT0It9jYDg3@MwG})!I$r?KBgdn2%;To zTcE@HtLt&bMj>EZIGxpY%A9&oseSiIto6dDgo3Ut4RU_3m8g?9lK(wS5^JKcj4JSW zP6(-xqZ4^J+SKaxjj0xV#adof` zQ4vb(NCOH8C#GK!;6i9e4U&Cf5hLe)Acb83#e%9C@e?US0Uc`k;c$mSTlHFQ2T~nn zk#0OoWV)VrLK+jm(Pm~-zYoi8ZYd^Own!~Z`@UaAdhJv=ae%{BoU%G zay?vW?J~%NB_Z|lT9>O=2uS^o+VKihB%(ppgei4sUPZeSR z42bl$qirpcqKv4HpQ??^#~bn@V$~|>hs6tpgy>ldw_9vOz)teoh9P4QdITpb5h&Efyc!Gd7WP?uE6luxibY*^=Tt0xci_Cc`MHT8Qsh?Ac`LdK- zIHyh;*rS2+Ce#WUOAMi4wl6u(5_GDR{>M5t4xsgu|EQg7F(U6c#c&WtH2C^u0Rr;vhnSVO!7D;nnIv;bwxpwA9ukja)*ktTTCk|^A zT2{IKz)YopB_cwl(LWc*zuVg59KjYw<5D<=b%*VP1vOw;aKxZxY1_f=%xC*BN|Q%~ zu3_PcuXjI+)uPu#qrtq`?=RkeQPV6=9J^L1hK}*#g-ok{hQTWxsn&82`kQ1GM|Z7u z>iEqVXqC{adx42(8}S-uIs6;<(R8h`%kCym<2CKQ(lSo# z7P6!=Uq3>T^7e8x(20@IWVj0{{p&nuLnf;@;!0RW&MB(H#Al=|_>CnF>1#CkNa7~h zrAxLS>@!mIBKX&I?u;T1R2-oR?GIBaxA|e3!d)(8^3>YHIkrN8_iZB~DuiZHNeC5q zgTBuy-F5G_G|9M==Mu&4P{z;imiWI@+mGY^ra_&-9ixZ5qY9US#8vF2D>4K&fa;zkt z^!G5Adc_rE)&Si#x1)_71moC3W%rA!h}NLsh=|Q`5KG83- zlqwEM3_oQtUnhwGR1HlOWxg6+P~hz z%{ywkW;eKY(IO1e!w1_jhRMw?wV!^2lYsaN>&8Roy+qQIWkK=B!q7?^ew$m~xX5!M ziV+@ZQ(lNZ4`$I05T|Vx z)8TCWc0(|Ary$lTo5=DUWgc{@2sK3i(AmjLYSpFPV#qe-V=bj_*)^EPQqY~LtHX~$ zOl34?k&Xh(SE>z_myuEJ=AC(`R88%_7H#LGSIqsuZW20MoVb#S*kAEvk;)Z`|MiI` zsA+2%3i#DH>R33js6D#ey}3eq7%3>^nRAfQNBfgC%{8_8VWbM0DN0{oOw$SA=m5CF=#MP48r=v=s5yE$6$3(s+^+55ZWaDIicJj+4_|Or?fh zPQ;-Id8`jg7d~Z(k&1gu8^R}HzqN;Frd-^!s!;WcjK3LT#AyE+1b{{@UC3&(Fw;w! zH4W)&-ol0CSx=Gt&)Qn<{4Y7l8&jXY_yMh(hYByY;l?v-hOl zMWT?M=c%<6Rl5ch$NHXgvX*QL79}`kJ1;{|U+19qAy#XAJ3W+<=2(GbDcVyWM*$T{ z7ICOe=I@cvheki#tg5EyS7v3x6#@FeOpcj<#Wk(8FKuH4FWe5&MyArY(k=S<=*plI zII>g*P`g0I`Xamtf03Ga{zb}%X)_hrqtk4opf^+TU3lvh{(hF>MZncyu)51lVarQ@ zqw*U=5@A5`pF{R#;LEmdbr}ne>K^Ap4}*ps^DmNHd-A+1NGCP}C|2!3Qm7n6o|t)a zI=?CE{mx7>#A^PxSPcuVX4kxRvpcJztZ3Kt*#<nFnE6`=bztTaL zn_GhF>pPZFr~ZPeOZj=s&?@_ z7!$>&`n-{E9{v&Ereeh$SsN?Au?FVF&?Bm?idzxu9MLfDws|EBB z-YxPhA`nTQS6wS>Mf)(kp@{Wh>P3jVJ6s5SWVFhSd>W*3kN20ll(x-S2w(pdK4rTL zzdZGGFLsou!fv2|^+eG{BthlXr*o0&aP`D}h4B8fR14OF6d9vD0UCNC%4g z>nRN}v^*Dl&gR}fhPXi{5Z<_2>3p>laBqIvvb`;O(5P}o;0*!5vGqBd+4tE@X&DE| ziU&U2;DKg+9XQz;(p5popkFhtr1eLKt2fu)1HX$nVMWvT?q6vxPZ`2Xw=Md+i;^UX zPXx1KO?-J)+Y&PwwNkhF1^1fIqBegBtb=A`N22}#vf19LD9R@(jT*n4TXt_tlFXm? z@?!`PSYsE)nv4pwMAou=N-2Ad`W@yWm7f9pXk>3 z_B4JyXG@kf31*~8X@Qi7Br)(L=rUR-E?~;3J_NVZ^KyEdSY#SggRFh8>Y#i#AFAIZ zvC-P{ks5BPf|-(3UyH}Zat;~%i8%d4ObbF%0o+sLA+z0UJGU5bnP8z=Bc z(}}x&`qCKVatNV^|CaATqpxcQYYVj4koYxXnBkwyD&vj5PZqpHTYLc0=3y{y-U+>j^sXkKk#G9c&} zw&G%N4D;W5_dMx5aqX+Zp_J!QB#V5`Dg9j93!hUWWd1Bc<)e}ceDw}M0{vDHP1UaK zx%sPaB@@XSMz;^(IMC8B3nnY>>xu;KJ1gwJ?=C?g~n9vqUgj~&(4bapsp0FJDiR=$MVtvicW z6M6$ya6(?b3YT`d>4qAb1XbUl@#~@So+_gyM?Y5Iu1sL-FXzn_gCZM)cIzDzX;gUn zw?>fnA85@nO|27ptB@cM`U8IhtQOxct|GFu{R;=U+a%tM;S9u11}KGTqZuS)e1niI zr6E}=7S&c8Vl<^goO9KbiVP^%M01~elp~@gSE?Ptrg1ilvh`1K0i}gs#W`JZobN@O zB3v%{veU=+HE;WT{RPS%LSyB7&qcpguC00y0vzXHApTj&+g;I+*-%t!urD!E~8~-(k@FoP^hWjm|B=Ja%=MVkcbap0ln6o z^S(XoUX(3XXcb*RSbYlbhZHEZOB!N#-C1>%ZJ%PY+rAr7sh^#CncC zrmME{=d3U9N6h@>D;<87V5n`IpbNM!;HjxDzN*}vRG>M6Y2g?Q_RF%OIhqP-#i{9w z>DjB4{(Mjo(R6OK@deMw=^`rsnd(8Z%L$Q;<*S<5Xe0k14bd0)XE)15MMbt!a7E+r z5B;3ObPnby1bV(jj+^T8uI=Jp68$ch$fK1+{Iq{FOQWx~4Q{@Y^S-u9Oz}b%G0)Kxu3h94&>uem_WL3Q5230If3EwQ28zJ!l`G z@;wbpf}pI(zR51yOo>&BmwQ;vC=GCk)#G}_-XNS!AC_H=+HItbtqe-DjO2Jugstn7 z{w$SQXGSkQJB6%Ih%W?S`+q7Om~c1@?|siaJC*7D_B|(3H6BZCL<0gaQJC!C?%Xrm z3suwJKM9O?LF|>k|3pM|I|c9yZ{xMkG$j7gnXFtUhCfgeqkBC zCr)mKntn~<$6|p9Nt!Bl?Fk~e3#}OO-xvAyez(h!4Ue41W!o^~6;&H<8UC$<@vR^= zo&@&{t~LCM@-QEp)+rIJu?t#EgYw~j;0%e?WnLZef@Bz2OOPSJRsG{oKa5EN zi~}oOodZ2G1>j=W1Ff*o?E1wF^<8$w@%wVi_hVb*(P`_NFLos#L>803pWxgzS4D4- zOTul=NoIc$|+UGyGQe|nSHXywPi?f< zqz5Y5!cBK5W!ZbH6rk|fRZf|6`G07nmy~s>#jL_r9MHwz?)Q|G_3EV959BqV3T^lo_nNsQ5*Z@ddYtddGmzG#7;!kyuZFXr9EFJ3i z0o)ww^I&1|4z9UAJcL@o*eiost!e%Nj>S%d1j;xS;#9Fb-7m_Tna{$I<1E>+mMYmQ z>U?+6jRSRDA>}HWCI6vHK#9j7$KQ7QvBO1I%8)9F&)_9gKT^Fsdht+X+ zbr#mkghYo0r}J$X%(b?D z?E{^@?}N-t49Ohu0%)aI-z>h8@?H(bej7i~g3jDc983NpLj>j%gnXk}*kh4Y6Bn^y zh>0l1K((z)j*Mjb(XsK<@4d@Jq9|Ta0^_zT>d<>C9^Hl<#Y zM>J-3J=4^F!n7_9JTav;Os$|Hwt!t)2^=8U7R>@RahU%jI`<@8JX-R9goZYh2KZpb zPeHI30uJ*$b($x``qvIJdRXnqOy3U&He07V0%N-RBf>li+%7+ z2^L}9&1@AG?(bg2hND>)^>S6^dM z>H1w@d`1R^)2uY|Zo>%MV8_UO^&KX{-h)A45ZArL!tQQT2eR;*OdF_L;3P|qZ%D+? z-j5T!u5s~mrX&TKze1znF=;ZioGb*X{bl@Mu>3$OtFEea;hS{w;6Z`0{q#rasNAqC zF)adhFE1U@m&mhGS6uKW0Fg6`^Expv~+AzQ&ljHdTn z@}un*--?ouF{nAzWY){8ESgYu(p0lyycII27AvE}=WXA(EcW2fN(wqMSSYK8&5Mcq z5fuood6pM+XSKdAqlIDeP zx9Vgpy%>z+lqo$6Ga_@(#+O3l)fNVRNl#muQINwu7a?IXhzB7|ERgkdvqk$RzKC|5NI*i2Aey=)6Ax zO4aZ@ayIac3af=(9&Qn7#Fl zi-R9`(K^sT{ble!+wJ2Kwo^7ASQ>6;_BgbT8j|`BUU&8}eRL?@dQS;j^U78z~SN#idC^n%KyLrR! z>9t*y&&>ch$+h!miRGB}tEmn3Heve19`wY!)#itiIx=}`Il2`#kK&qbD!l5=^9GSH zw}Gt1mvz@#V-FemItb~B+=ZgY5Q3q4Hj?8yv+dcvZ3^HG z>xkEK7-eH6TTFXI_Sm!IT?U1Lbz{4XY`eOgmXdLG^sz_AMsCnbjCa4l=44DFA4mSd z&0ukzyRU?%dP?s_$pk&Xn*HR$6}z9tnf#Z3Qy}C+~VU9s|veqf0fiYxJsTBoX;M(eg6Lx zn}k=YrRq4lkS@bLi2f3Lf1Z%k3)QjIK0+=}ZKH!`SQsSRbLntnaYC-#-4YiO0t@Qu zD4IP{wfHl~`EPV0xBv1nXvS;flu3?tbo*OQ)d9h0RGD`$e4vO)ja#=Nx&$XOB$agGRt3+$|S?0pb8|`iTBzkRBSyOG(*7bHxl2; zbwyskaQg~cbHrwZJ_?>}H>w#LOjhS`_3hWvxqjiUmh@oOE7qrAf9DAvPI+6$c}iuY zrl^y+++?dCcwTL1<)G3XgSWGK-rbQ_q?7pG*I1wpA~0)amCiK0Q${4NM*7}5L(z{kSNss47h00SyJzYvGoq8MiJk{Hor%nE;B(W7wg$AWl>IDh_8V~gRie@O+*qnz7 zurl_qy!2wS<||aS8)AWVudhV+u@YJgI~1wQ`vpt~lJCIDG&IkOw2j+_U%d5st1ML< z@vXnIyJmtmpyfpznZ<-z2L4^xowK017u6y(oj+ZxOjUx1u?1I&nl?vDxNJilk!P#+q$yC zM`lHR4fuAbRBd3Wr<^~;=xD23mDG2MJKHali^ZtjRrEuKsg4rIkxG4&ckeTbTdooi z0{}ak9Ts>~c$SoD>usmWH512g2N}M~DmtHyR|cGP{9eVo3)~2m{_`E_v&28|QOp zrQjB76kpG=%Re{`acl@?&74jX)3=!zX~bM*43FH>LnZvv#~rFZYO3mVT^rsG{m|pe(3YGsWl$AG7_LhOJ+a z!dj!%5qz(0Su5V${a6(AHK%71-*s!7Ze3A1(np;OPFy%#1>8Fp@U9-sCiaX!csL~N zn$DYn+x={9+xV3xH@8SaTJVVi8;m|=6p^NxAA~TLcejncO$NM6&8}1Qw|LSex)GVt zrhBhLEqt`X685_CMF!7HX*~vXz-zEg9!qpF)6np$fr!u%fc<4b6PXQjoHs`lNn|(q zC#~rAzc|}&r{T7x;WT@P&<`J0v}Fc`s55W<)(QMy{+C$%SRKIw*!y7a3_P4kPk8AF z-j!JwxgALJX&X6@U@N9L28+pf1U=<-?MtkKV*$dfg~Y*gcnP>}DoOEV^RqV5;JrcR z9XJ$yvs}nfpXhG$@2xI(KhLVC>#~?0<%&P=`Dxbl`Z*4~Hr?JUecrBhFTtn_>4Z(xyvFxRUN(Y?fK%`xvtsqk7te3b1o>R%ehz!Gd4h zrnSPe6u~@c7iM`Dw;l&Jdc8-5Me;F+>Ju<^KN^7D9S_BvKXaNshgO?aMMy2`eoGmm z_FslkYnzF_q?F)@b;_jjb19iQblFg+K#!N@vwAnkz}9uLv7Y{m&VM&Eo)w_vPEV9F zP8HpP)~S4+m-QO>1X_qHK-;J9M!X(5=Lk71ogeFNvB@ZVlbqLa9cRPp2KmqkK0(y ze+npy5tOG7)F)QoTQ@CZ6*?L`;{Xf;o@PcSfjNoOE01t=G=HI30 zR@VUy$j)T^J5gWqFfT(ZyR)*yoS{euwZ$&`YKan7U)$CHX+ZOe0Jtx`8PM3Qoyi+h zR56%+;t$|)dzaR+54M(L+n~5~r~s-iD-&m^=rwvNc%D*cl(EHihrF_$RN5KLj>)I#+xQJ%-QP^CQZhZ#Zj5C%^ht z<>+7z4<$FjRL&HwRooofXl#ZnF_9smEhYn;n0-3F-p4+Qa2h4k+)rf=@K9#>hC$fd zZm`tTfU$m^P?BXwLlCYOdxD`wA;~luP-bsuo2Hw(J6^wP2J{jrT4Cg<;`C}3hjx2u ze-(e{p%j4#t#=T-!d(KSM7TdDn94=9Hnn-vamT&jC$hcNhA+Ah#~qO?dZCqd+2v`# zR8!ApT5gG5PYRf*ZQ&m{OSCh!8CD@NQDB$4EHn36d+5?h*9Q7A%^l z{3|5v0{+tdl{{D2OLuUFcttJC{XTiJP0D&4A`nAnM_y!_vn2B-L}ss-^oeVA%N5pT z#S!fQWxs1vjGYFL;qwD_#Mr)NLB7dsP<-huX@XTlX^e3@JAK@TbT%@vs?lc7A@KxXT~WQDfkFG6m~^EqGY zT{cfzv(2YtBqAOCC<*5f)1$b7p9{FPa`|~8 zf%WMNc&j|wR)XzoDas+vJhvG2p69@j5$lvH;`M7%${^PRMwdQQu*+Zmxn}Vy>A5Gx z9zd7O&^uB^9hZnsXCLpTFFbNuc8tm`d3Af;SJcI;pad$6tq%%Kzf|O$KC)QwjKhTD zIa>Vb_Ovf*n`kSvq#{1wZ&SL;W&%-v&!FPS@6XiIwx;t;g$`77cZxb)A;bI6<)_^? z3S5H;RSycCTRkeTw)r{O&zLL#cEjzbL zyR9g!fJYt2NvZCXY&0^)O+_lZeg$GT*>TGps#61w^P42P_w}VZLVdC?^#)_>HM8M+ zr746vvR~eDtHe%p-%G<6-`P)zf(B@P_xMNvEHbI66OD^S_b{${tx`R#_qvMZA~@RC zAJmIiSkxdi=*NKdIfJqb_rrY6`z|uA4m5H8S3)`5CnrTd7g`Al!kl&oh+;GP2h#)S zsvXfJ+o*L*eV;Q%KQL%MVZ-yMnbrPk|6Rxbrc`~fWM;{fx*LnqQS1g{yvbG>ZvrF`)Q|kP}f@ zW#Q=B0%Imvx?+}GW04K&g$7t4v^w&8vY&?e)BEh-bq>+|fUj2amZL74lfDv(#WS-D zmxg*$$MRl*y%IKL9wlxkiMuga*3#oV@Qyz!F%)tP%c@{zOlAz}3Eks+$Q9v^_YbXEdziO<8KG7p?YOUf4%g3W5S zo`jUd-;7On4yfq=EiPvlg924RiYSi>66U0rA(@2?Bs-^H22&|4HFxXZ%0v{22J{JL zXZS$oiEfCAOl+mq0m;P%pV|H`zml5O23mGUzM4*r*ROrn;5I9HD%029`Yw}Ezgx@B zcjS)@t9@D)zst7V!rRXnhwNZsmyG|=$fT8IEt3%jedE(|x#p*wrfV%TZLiTQVMl@N zg1x0CJ{dkQH*(yzgC`{Nx!uP6MG6kls~bjxZeNA%XvylBU-xEiwG%}CrO+?|JAL^s za^0UBDT;hXbfQW%V`bg$i*Fq-@`mQ^a~32GUDop?e)i@$4n9Z?MowHQHW2&``h$zT z^!L1(btNT}nXky3c#1!iHFYy9-nn$Ssfz`scad(d4HA+#kmD=zKOIxNMW|YM&&wsC zr3AFKRulM6PXHFwNOGH6n%~u6aooxtP9TK7kxVJ`M`| zdym2cg`K_9Q@VJeZ|?;(?!eoGMouRDhsYJrQ^*6hLB+0wjSY!FOl@vt(&|tBITm80 zOfvjiwhY6n@vGZ*@{yuv;)4F{AB%sEm6<1g%QPV=6zpr6$&?1%#pz`;tQ*-tNd!Tf z#v{ujvmf$VxG9Y2s^Aggo*e4bH5n*S@f{e>nY7BPu&9%Ey||sO`i>2Eo9HZ{&i&Q< zcfdvGCvFwq^Cb$FSuuGEFYH!A@6j-Z1aY&7pV9RWmgLD`pF5QSi2>pf0GoV)!J^W%c&lhfsoFauaep;IOQCUgJW=G^=QgkO@LQOX_kQJ@;v zfa3vHz3ijGtZbn2sRSPC^m;Gz!<88Kbocd!1W{n?JK|u#Z{)c+*WCY{IJJ`%Of}wQ z=1MY;gMdU;D8U^kK)QNL7bYVdf|;4JJXUVG_igpqeJ}_`qzFcIfg$Zkg70MO=}>L~ zd1)Kx#?v19Q=s}(oZe7t$0cpTM(II75qiaXHKbns#OPk=ti6GuNs;A-y`(x*&qv)w zJg%#C-VdYx1gj?JikqyG6o`|3+s#Oxnwj4wFb)&UIa;in97(GxtV5%?X>Y>N+UkQ`}1?M_}1sUW2(0o>X3!c*~?8$4lz3k~$@`U1Y z)PC{3aA9KoR*Dit`-Dmow`ig)ms{u|@%E7i9N!;2aFOKW&6b$kozMu_`Bi>u(5e+j z+%B$zbb#xlZti+}QtZrLj^gyB(oLS}C5|M*6V0#&nmIR?&^b#HVl$6ah&N5kT(WUa zY~Yf#_=j$2nZX_PNo2TQ@z3i9u~dTRKe?oozx~LKF>1?3GG&RDse&_+!s*qPnWJCz zX%Or+w&G=;-upPSWN7duk*zz4Z!l7W;YJ`mFh(hGURyGXp+HYtE)4iiW&MobE5OOy zM1pL4=r|ob1~&a3HxXL@wUiXk^Nb6Yu53%C?pm9II;Qa|KIv|9mc4wbgmN1X$sd2wYzz${jM?X0$t4UM_7+7E~hKNu&=H zBr3<4T}Al*)FCT7ALGDYU(dK7+z$x~Kte;ov?#Q(p z3VMh}(xB9zD)+4jyknhv2B#SOLUhAI7&ZXO{7r5z;?0#egDM$Q#>DAvi(%QkB#rIG zhAfaMFvJc;975n1mGJ_9o5ZA&^YWZ|L6$sTIaH4Vfi|Yth;dS1YMe?feCN?F^89_{ z_79fO?H^D~A1wQv4L_&9gb@ziAQ9q68Pt8+0n;nnZW`FfLvYgfK-M-1$w~@KRK0jh z+%*4Jy?RtAH--IDF%D+1qdm;M-$cjUtOD9rKwWa@qfMHQUR7V@qJ@^FhfaiF%a`WCu)E@$2oNLsIzJ%6{rm@v@OZ%gcf9SELI`#gT%7KsU<<95#$S zJ%3DmM5LgFhakG6IO@#3%NpH64b+DX-eu1ZUK1jr@~_8FobS_{`3&FF6S()yh`K

I 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 H{+s)MXzb$* literal 0 HcmV?d00001 diff --git a/package.json b/package.json index 37c224610..da8bd72b7 100644 --- a/package.json +++ b/package.json @@ -44,8 +44,8 @@ "prepack": "nuxt-module-build build", "typecheck": "nuxi typecheck", "example": "run () { nuxi dev examples/$*; }; run", - "docs": "nuxi dev docs", - "docs:build": "nuxi build docs", + "docs": "docus dev docus", + "docs:build": "docus build docus", "dev": "nuxi dev playground", "dev:build": "nuxi build playground", "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee854f6bf..8dec0f102 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -289,6 +289,18 @@ importers: specifier: ^1.6.1 version: 1.6.1 + docus: + dependencies: + better-sqlite3: + specifier: ^11.10.0 + version: 11.10.0 + docus: + specifier: latest + version: 3.0.2(7dee646ded917034475ae42eea927616) + nuxt: + specifier: ^3.17.3 + version: 3.17.5(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.8)(@parcel/watcher@2.5.1)(@types/node@22.15.31)(better-sqlite3@11.10.0)(db0@0.3.2(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.8)(better-sqlite3@11.10.0)(sqlite3@5.1.7))(encoding@0.1.13)(eslint@9.28.0(jiti@2.4.2))(ioredis@5.6.1)(lightningcss@1.30.1)(magicast@0.3.5)(meow@13.2.0)(optionator@0.9.4)(rollup@4.41.1)(sqlite3@5.1.7)(terser@5.40.0)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(yaml@2.8.0))(vue-tsc@2.2.10(typescript@5.8.3))(yaml@2.8.0) + examples/basic: dependencies: '@nuxt/content': @@ -1744,6 +1756,9 @@ packages: '@nuxtjs/plausible@1.2.0': resolution: {integrity: sha512-pjfps32fFN77BhjqHmq2Jx4XCNso9TcYnB+S4IR2qH/c26WDfYB5mQxN5pOEiWRlMkiKq+Y45mBBFtSOVKClCA==} + '@nuxtjs/robots@5.2.10': + resolution: {integrity: sha512-WiO+VA8UwDgVLy7JCrGTrAmSNNw397OuNseKOG051ixswEDd0QhNw4cUtgNd2RkSIL7WlkfwkizdJFCd9y9iXw==} + '@octokit/auth-token@5.1.2': resolution: {integrity: sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==} engines: {node: '>= 18'} @@ -4065,6 +4080,13 @@ packages: resolution: {integrity: sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==} engines: {node: '>=0.3.1'} + docus@3.0.2: + resolution: {integrity: sha512-OmY7EgQnN7Oli/qmgQ9fz4pD14AEEMDhoTGf9CNzvwHM/qCdhtsJNUSSBiB3sDUqlsMdgg9HPJSQQtZ8oJAATQ==} + hasBin: true + peerDependencies: + better-sqlite3: 11.x + nuxt: 3.x + dom-serializer@2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} @@ -4563,6 +4585,20 @@ packages: fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + framer-motion@12.16.0: + resolution: {integrity: sha512-xryrmD4jSBQrS2IkMdcTmiS4aSKckbS7kLDCuhUn9110SQKG1w3zlq1RTqCblewg+ZYe+m3sdtzQA6cRwo5g8Q==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + fresh@2.0.0: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} @@ -4830,6 +4866,9 @@ packages: resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==} engines: {node: '>=6'} + hey-listen@1.0.8: + resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==} + hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} @@ -5792,6 +5831,17 @@ packages: engines: {node: '>=18'} hasBin: true + motion-dom@12.18.1: + resolution: {integrity: sha512-dR/4EYT23Snd+eUSLrde63Ws3oXQtJNw/krgautvTfwrN/2cHfCZMdu6CeTxVfRRWREW3Fy1f5vobRDiBb/q+w==} + + motion-utils@12.18.1: + resolution: {integrity: sha512-az26YDU4WoDP0ueAkUtABLk2BIxe28d8NH1qWT8jPGhPyf44XTdDUh8pDk9OPphaSrR9McgpcJlgwSOIw/sfkA==} + + motion-v@1.2.1: + resolution: {integrity: sha512-4EmoDPV+STphEh3/v/oLyViVwlfUrYdvom2uWK2INJ75F4CfrH8wCzHzAfoPFN7Ghns4n21QVlAYp7k+sAP1LA==} + peerDependencies: + vue: '>=3.0.0' + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -5969,6 +6019,11 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + nuxi@3.25.1: + resolution: {integrity: sha512-NeZDRVdn58QF3+clrkKRXE3PtfhE4hkmj8/Wqf6th707SDqmdBb/KZV2BE4lwL+FhgEDgtN7AMF8WZCkicudXg==} + engines: {node: ^16.10.0 || >=18.0.0} + hasBin: true + nuxt-component-meta@0.11.0: resolution: {integrity: sha512-tF+BUToseiljrQXEg/zbqDZvr/2RyEGKzj2PzVF0pR9iHTQPEkQ+8Yt91Qo3mU3crttxTP39GJEgN5npeFZ+1w==} hasBin: true @@ -10059,6 +10114,21 @@ snapshots: transitivePeerDependencies: - magicast + '@nuxtjs/robots@5.2.10(magicast@0.3.5)(vue@3.5.16(typescript@5.8.3))': + dependencies: + '@nuxt/kit': 3.17.5(magicast@0.3.5) + consola: 3.4.2 + defu: 6.1.4 + nuxt-site-config: 3.2.0(magicast@0.3.5)(vue@3.5.16(typescript@5.8.3)) + pathe: 2.0.3 + pkg-types: 2.1.0 + sirv: 3.0.1 + std-env: 3.9.0 + ufo: 1.6.1 + transitivePeerDependencies: + - magicast + - vue + '@octokit/auth-token@5.1.2': {} '@octokit/auth-token@6.0.0': {} @@ -12304,6 +12374,86 @@ snapshots: diff@8.0.2: {} + docus@3.0.2(7dee646ded917034475ae42eea927616): + dependencies: + '@iconify-json/lucide': 1.2.47 + '@iconify-json/simple-icons': 1.2.38 + '@iconify-json/vscode-icons': 1.2.22 + '@nuxt/content': 'link:' + '@nuxt/image': 1.10.0(db0@0.3.2(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.8)(better-sqlite3@11.10.0)(sqlite3@5.1.7))(ioredis@5.6.1)(magicast@0.3.5) + '@nuxt/kit': 3.17.5(magicast@0.3.5) + '@nuxt/ui-pro': 3.1.3(@babel/parser@7.27.4)(db0@0.3.2(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.8)(better-sqlite3@11.10.0)(sqlite3@5.1.7))(embla-carousel@8.6.0)(encoding@0.1.13)(ioredis@5.6.1)(jwt-decode@4.0.0)(magicast@0.3.5)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(yaml@2.8.0))(vue-router@4.5.1(vue@3.5.16(typescript@5.8.3)))(vue@3.5.16(typescript@5.8.3))(zod@3.25.58) + '@nuxtjs/mdc': 0.17.0(magicast@0.3.5) + '@nuxtjs/robots': 5.2.10(magicast@0.3.5)(vue@3.5.16(typescript@5.8.3)) + better-sqlite3: 11.10.0 + c12: 3.0.4(magicast@0.3.5) + citty: 0.1.6 + dotenv: 16.5.0 + git-url-parse: 16.1.0 + minimark: 0.2.0 + motion-v: 1.2.1(vue@3.5.16(typescript@5.8.3)) + nuxi: 3.25.1 + nuxt: 3.17.5(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.8)(@parcel/watcher@2.5.1)(@types/node@22.15.31)(better-sqlite3@11.10.0)(db0@0.3.2(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.8)(better-sqlite3@11.10.0)(sqlite3@5.1.7))(encoding@0.1.13)(eslint@9.28.0(jiti@2.4.2))(ioredis@5.6.1)(lightningcss@1.30.1)(magicast@0.3.5)(meow@13.2.0)(optionator@0.9.4)(rollup@4.41.1)(sqlite3@5.1.7)(terser@5.40.0)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(yaml@2.8.0))(vue-tsc@2.2.10(typescript@5.8.3))(yaml@2.8.0) + nuxt-llms: 0.1.3(magicast@0.3.5) + nuxt-og-image: 5.1.6(@unhead/vue@2.0.10(vue@3.5.16(typescript@5.8.3)))(magicast@0.3.5)(unstorage@1.16.0(db0@0.3.2(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.8)(better-sqlite3@11.10.0)(sqlite3@5.1.7))(ioredis@5.6.1))(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(yaml@2.8.0))(vue@3.5.16(typescript@5.8.3)) + pkg-types: 2.1.0 + scule: 1.3.0 + tailwindcss: 4.1.8 + ufo: 1.6.1 + unctx: 2.4.1 + unist-util-visit: 5.0.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@babel/parser' + - '@capacitor/preferences' + - '@deno/kv' + - '@emotion/is-prop-valid' + - '@inertiajs/vue3' + - '@netlify/blobs' + - '@planetscale/database' + - '@unhead/vue' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - '@vue/composition-api' + - async-validator + - aws4fetch + - axios + - bare-buffer + - change-case + - db0 + - drauu + - embla-carousel + - encoding + - focus-trap + - idb-keyval + - ioredis + - joi + - jwt-decode + - magicast + - nprogress + - qrcode + - react + - react-dom + - sortablejs + - superstruct + - supports-color + - typescript + - universal-cookie + - unstorage + - uploadthing + - valibot + - vite + - vue + - vue-router + - yup + - zod + dom-serializer@2.0.0: dependencies: domelementtype: 2.3.0 @@ -12906,6 +13056,12 @@ snapshots: fraction.js@4.3.7: {} + framer-motion@12.16.0: + dependencies: + motion-dom: 12.18.1 + motion-utils: 12.18.1 + tslib: 2.8.1 + fresh@2.0.0: {} fs-constants@1.0.0: {} @@ -13297,6 +13453,8 @@ snapshots: hex-rgb@4.3.0: {} + hey-listen@1.0.8: {} + hookable@5.5.3: {} hosted-git-info@7.0.2: @@ -14473,6 +14631,24 @@ snapshots: ast-module-types: 6.0.1 node-source-walk: 7.0.1 + motion-dom@12.18.1: + dependencies: + motion-utils: 12.18.1 + + motion-utils@12.18.1: {} + + motion-v@1.2.1(vue@3.5.16(typescript@5.8.3)): + dependencies: + '@vueuse/core': 10.11.1(vue@3.5.16(typescript@5.8.3)) + framer-motion: 12.16.0 + hey-listen: 1.0.8 + vue: 3.5.16(typescript@5.8.3) + transitivePeerDependencies: + - '@emotion/is-prop-valid' + - '@vue/composition-api' + - react + - react-dom + mrmime@2.0.1: {} ms@2.0.0: {} @@ -14728,6 +14904,8 @@ snapshots: dependencies: boolbase: 1.0.0 + nuxi@3.25.1: {} + nuxt-component-meta@0.11.0(magicast@0.3.5): dependencies: '@nuxt/kit': 3.17.5(magicast@0.3.5) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 89cbf062f..26a1568eb 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,6 +2,7 @@ packages: - ./ - playground - docs + - docus - examples/* - test/fixtures/* onlyBuiltDependencies: From 7eb9d1b3db1b340c5387045ec147f634c7147665 Mon Sep 17 00:00:00 2001 From: Baptiste Leproux Date: Thu, 19 Jun 2025 12:16:48 +0200 Subject: [PATCH 02/23] landing --- docus/app/app.config.ts | 24 ++++ docus/app/components/content/BrowserFrame.vue | 6 +- docus/app/components/example/ExampleAlert.vue | 26 ++++ docus/app/components/example/ExampleCard.vue | 5 + .../example/ExampleFulltextContentSearch.vue | 11 ++ .../example/ExampleFulltextFusejs.vue | 45 ++++++ .../example/ExampleFulltextMiniSearch.vue | 49 +++++++ docus/app/components/example/ExampleHero.vue | 8 ++ .../components/example/ExampleIconCard.vue | 29 ++++ .../components/example/ExampleLandingHero.vue | 22 +++ docus/app/components/example/ExampleTitle.vue | 5 + docus/content/index.md | 134 +++++++++++------- docus/public/_robots.txt | 2 + docus/public/background-hero-dark.svg | 91 ++++++++++++ docus/public/background-hero-light.svg | 91 ++++++++++++ .../public/blog/automatic-parsing-modal.webp | Bin 0 -> 34294 bytes docus/public/blog/collaborate.webp | Bin 0 -> 111658 bytes docus/public/blog/docus.webp | Bin 0 -> 59986 bytes docus/public/blog/favorite-editor.webp | Bin 0 -> 19758 bytes docus/public/blog/frontmatters.png | Bin 0 -> 1283956 bytes docus/public/blog/generated-form.png | Bin 0 -> 158974 bytes docus/public/blog/google-github.webp | Bin 0 -> 127558 bytes docus/public/blog/markdown-editor.webp | Bin 0 -> 52856 bytes docus/public/blog/migrate-docs-starter.png | Bin 0 -> 1278600 bytes docus/public/blog/nuxt-content-v3.png | Bin 0 -> 270495 bytes docus/public/blog/nuxt-studio-v2.png | Bin 0 -> 986095 bytes docus/public/blog/preview.webp | Bin 0 -> 47772 bytes docus/public/blog/studio-form-generation.png | Bin 0 -> 1201603 bytes docus/public/blog/v2-interface.webp | Bin 0 -> 48654 bytes docus/public/blog/visual-editor.webp | Bin 0 -> 58490 bytes docus/public/docs/studio/code-editor.webp | Bin 0 -> 52856 bytes docus/public/docs/studio/dev-tunnel.png | Bin 0 -> 140565 bytes docus/public/docs/studio/editors-medias.webp | Bin 0 -> 228118 bytes docus/public/docs/studio/github-sync.webp | Bin 0 -> 12668 bytes .../docs/studio/home-content-code-dark.webp | Bin 0 -> 121504 bytes .../docs/studio/home-content-studio-dark.webp | Bin 0 -> 71568 bytes .../docs/studio/home-data-studio-dark.webp | Bin 0 -> 72502 bytes docus/public/docs/studio/json-yml-forms.png | Bin 0 -> 1318954 bytes docus/public/docs/studio/local-debug.webp | Bin 0 -> 12676 bytes docus/public/docs/studio/mdc-syntax.png | Bin 0 -> 94397 bytes docus/public/docs/studio/placeholder.webp | Bin 0 -> 13130 bytes docus/public/docs/studio/preview-schema.png | Bin 0 -> 141419 bytes .../docs/studio/visual-markdown-editor.webp | Bin 0 -> 47772 bytes docus/public/gradient-dark.svg | 20 +++ docus/public/gradient-light.svg | 20 +++ docus/public/home/cta-dark.svg | 91 ++++++++++++ docus/public/home/cta-light.svg | 91 ++++++++++++ docus/public/home/features-dark.svg | 56 ++++++++ docus/public/home/features-light.svg | 56 ++++++++ docus/public/home/hero-dark.svg | 91 ++++++++++++ docus/public/home/hero-light.svg | 91 ++++++++++++ docus/public/home/pro-dark.svg | 126 ++++++++++++++++ docus/public/home/pro-light.svg | 122 ++++++++++++++++ docus/public/icon.svg | 3 + docus/public/nuxt-ui-pro-dark.svg | 14 ++ docus/public/nuxt-ui-pro-light.svg | 14 ++ docus/public/pricing/timeline-dark.webp | Bin 0 -> 21976 bytes docus/public/pricing/timeline-light.webp | Bin 0 -> 22214 bytes docus/public/social.png | Bin 0 -> 119547 bytes docus/public/studio-social.png | Bin 0 -> 230152 bytes docus/public/templates/canvas-portfolio-1.png | Bin 0 -> 274680 bytes docus/public/templates/canvas-portfolio-2.png | Bin 0 -> 432251 bytes docus/public/templates/canvas-portfolio-3.png | Bin 0 -> 440210 bytes docus/public/templates/canvas.jpg | Bin 0 -> 256183 bytes docus/public/templates/content-wind-1.webp | Bin 0 -> 12136 bytes docus/public/templates/content-wind-2.webp | Bin 0 -> 21092 bytes docus/public/templates/content-wind-main.webp | Bin 0 -> 15984 bytes docus/public/templates/docs-1.webp | Bin 0 -> 15076 bytes docus/public/templates/docs-2.webp | Bin 0 -> 18596 bytes docus/public/templates/docs-3.webp | Bin 0 -> 19300 bytes docus/public/templates/docs.jpg | Bin 0 -> 54300 bytes docus/public/templates/docus.webp | Bin 0 -> 62306 bytes docus/public/templates/landing.jpg | Bin 0 -> 56415 bytes docus/public/templates/minted-directory-1.png | Bin 0 -> 1361842 bytes docus/public/templates/minted-directory-2.png | Bin 0 -> 1105416 bytes docus/public/templates/minted-directory-3.png | Bin 0 -> 1345532 bytes .../templates/minted-directory-thumbnail.png | Bin 0 -> 516910 bytes docus/public/templates/nuxt-ui-pro-dark.svg | 14 ++ docus/public/templates/nuxt-ui-pro-light.svg | 14 ++ docus/public/templates/planet.webp | Bin 0 -> 67504 bytes docus/public/templates/portfolio.jpg | Bin 0 -> 113427 bytes docus/public/templates/saas-1.png | Bin 0 -> 563775 bytes docus/public/templates/saas-2.png | Bin 0 -> 618740 bytes docus/public/templates/saas.jpg | Bin 0 -> 84993 bytes docus/public/templates/starter-1.webp | Bin 0 -> 7788 bytes docus/public/templates/starter-main.webp | Bin 0 -> 7788 bytes docus/public/templates/starter.jpg | Bin 0 -> 154106 bytes docus/tsconfig.json | 3 + 88 files changed, 1321 insertions(+), 53 deletions(-) create mode 100644 docus/app/app.config.ts create mode 100644 docus/app/components/example/ExampleAlert.vue create mode 100644 docus/app/components/example/ExampleCard.vue create mode 100644 docus/app/components/example/ExampleFulltextContentSearch.vue create mode 100644 docus/app/components/example/ExampleFulltextFusejs.vue create mode 100644 docus/app/components/example/ExampleFulltextMiniSearch.vue create mode 100644 docus/app/components/example/ExampleHero.vue create mode 100644 docus/app/components/example/ExampleIconCard.vue create mode 100644 docus/app/components/example/ExampleLandingHero.vue create mode 100644 docus/app/components/example/ExampleTitle.vue create mode 100644 docus/public/_robots.txt create mode 100644 docus/public/background-hero-dark.svg create mode 100644 docus/public/background-hero-light.svg create mode 100644 docus/public/blog/automatic-parsing-modal.webp create mode 100644 docus/public/blog/collaborate.webp create mode 100644 docus/public/blog/docus.webp create mode 100644 docus/public/blog/favorite-editor.webp create mode 100644 docus/public/blog/frontmatters.png create mode 100644 docus/public/blog/generated-form.png create mode 100644 docus/public/blog/google-github.webp create mode 100644 docus/public/blog/markdown-editor.webp create mode 100644 docus/public/blog/migrate-docs-starter.png create mode 100644 docus/public/blog/nuxt-content-v3.png create mode 100644 docus/public/blog/nuxt-studio-v2.png create mode 100644 docus/public/blog/preview.webp create mode 100644 docus/public/blog/studio-form-generation.png create mode 100644 docus/public/blog/v2-interface.webp create mode 100644 docus/public/blog/visual-editor.webp create mode 100644 docus/public/docs/studio/code-editor.webp create mode 100644 docus/public/docs/studio/dev-tunnel.png create mode 100644 docus/public/docs/studio/editors-medias.webp create mode 100644 docus/public/docs/studio/github-sync.webp create mode 100644 docus/public/docs/studio/home-content-code-dark.webp create mode 100644 docus/public/docs/studio/home-content-studio-dark.webp create mode 100644 docus/public/docs/studio/home-data-studio-dark.webp create mode 100644 docus/public/docs/studio/json-yml-forms.png create mode 100644 docus/public/docs/studio/local-debug.webp create mode 100644 docus/public/docs/studio/mdc-syntax.png create mode 100644 docus/public/docs/studio/placeholder.webp create mode 100644 docus/public/docs/studio/preview-schema.png create mode 100644 docus/public/docs/studio/visual-markdown-editor.webp create mode 100644 docus/public/gradient-dark.svg create mode 100644 docus/public/gradient-light.svg create mode 100644 docus/public/home/cta-dark.svg create mode 100644 docus/public/home/cta-light.svg create mode 100644 docus/public/home/features-dark.svg create mode 100644 docus/public/home/features-light.svg create mode 100644 docus/public/home/hero-dark.svg create mode 100644 docus/public/home/hero-light.svg create mode 100644 docus/public/home/pro-dark.svg create mode 100644 docus/public/home/pro-light.svg create mode 100644 docus/public/icon.svg create mode 100644 docus/public/nuxt-ui-pro-dark.svg create mode 100644 docus/public/nuxt-ui-pro-light.svg create mode 100644 docus/public/pricing/timeline-dark.webp create mode 100644 docus/public/pricing/timeline-light.webp create mode 100644 docus/public/social.png create mode 100644 docus/public/studio-social.png create mode 100644 docus/public/templates/canvas-portfolio-1.png create mode 100644 docus/public/templates/canvas-portfolio-2.png create mode 100644 docus/public/templates/canvas-portfolio-3.png create mode 100644 docus/public/templates/canvas.jpg create mode 100644 docus/public/templates/content-wind-1.webp create mode 100644 docus/public/templates/content-wind-2.webp create mode 100644 docus/public/templates/content-wind-main.webp create mode 100644 docus/public/templates/docs-1.webp create mode 100644 docus/public/templates/docs-2.webp create mode 100644 docus/public/templates/docs-3.webp create mode 100644 docus/public/templates/docs.jpg create mode 100644 docus/public/templates/docus.webp create mode 100644 docus/public/templates/landing.jpg create mode 100644 docus/public/templates/minted-directory-1.png create mode 100644 docus/public/templates/minted-directory-2.png create mode 100644 docus/public/templates/minted-directory-3.png create mode 100644 docus/public/templates/minted-directory-thumbnail.png create mode 100644 docus/public/templates/nuxt-ui-pro-dark.svg create mode 100644 docus/public/templates/nuxt-ui-pro-light.svg create mode 100644 docus/public/templates/planet.webp create mode 100644 docus/public/templates/portfolio.jpg create mode 100644 docus/public/templates/saas-1.png create mode 100644 docus/public/templates/saas-2.png create mode 100644 docus/public/templates/saas.jpg create mode 100644 docus/public/templates/starter-1.webp create mode 100644 docus/public/templates/starter-main.webp create mode 100644 docus/public/templates/starter.jpg create mode 100644 docus/tsconfig.json diff --git a/docus/app/app.config.ts b/docus/app/app.config.ts new file mode 100644 index 000000000..fd525fcd0 --- /dev/null +++ b/docus/app/app.config.ts @@ -0,0 +1,24 @@ +export default defineAppConfig({ + ui: { + colors: { + primary: 'green', + secondary: 'sky', + neutral: 'slate', + }, + }, + uiPro: { + pageHero: { + slots: { + title: 'font-semibold sm:text-6xl', + description: 'sm:text-lg text-(--ui-text-toned) max-w-5xl mx-auto', + container: 'py-16 sm:py-20 lg:py-24', + }, + }, + pageSection: { + slots: { + title: 'font-semibold lg:text-4xl', + featureLeadingIcon: 'text-(--ui-text-highlighted)', + }, + }, + }, +}) diff --git a/docus/app/components/content/BrowserFrame.vue b/docus/app/components/content/BrowserFrame.vue index a8d6a4a32..d4f5e7316 100644 --- a/docus/app/components/content/BrowserFrame.vue +++ b/docus/app/components/content/BrowserFrame.vue @@ -1,5 +1,5 @@ diff --git a/docus/app/components/example/ExampleAlert.vue b/docus/app/components/example/ExampleAlert.vue new file mode 100644 index 000000000..fe7ea2232 --- /dev/null +++ b/docus/app/components/example/ExampleAlert.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/docus/app/components/example/ExampleCard.vue b/docus/app/components/example/ExampleCard.vue new file mode 100644 index 000000000..8d5246191 --- /dev/null +++ b/docus/app/components/example/ExampleCard.vue @@ -0,0 +1,5 @@ + diff --git a/docus/app/components/example/ExampleFulltextContentSearch.vue b/docus/app/components/example/ExampleFulltextContentSearch.vue new file mode 100644 index 000000000..3a5da2aec --- /dev/null +++ b/docus/app/components/example/ExampleFulltextContentSearch.vue @@ -0,0 +1,11 @@ + diff --git a/docus/app/components/example/ExampleFulltextFusejs.vue b/docus/app/components/example/ExampleFulltextFusejs.vue new file mode 100644 index 000000000..5533a1786 --- /dev/null +++ b/docus/app/components/example/ExampleFulltextFusejs.vue @@ -0,0 +1,45 @@ + + + diff --git a/docus/app/components/example/ExampleFulltextMiniSearch.vue b/docus/app/components/example/ExampleFulltextMiniSearch.vue new file mode 100644 index 000000000..650afb79f --- /dev/null +++ b/docus/app/components/example/ExampleFulltextMiniSearch.vue @@ -0,0 +1,49 @@ + + + diff --git a/docus/app/components/example/ExampleHero.vue b/docus/app/components/example/ExampleHero.vue new file mode 100644 index 000000000..e98f9e27b --- /dev/null +++ b/docus/app/components/example/ExampleHero.vue @@ -0,0 +1,8 @@ + diff --git a/docus/app/components/example/ExampleIconCard.vue b/docus/app/components/example/ExampleIconCard.vue new file mode 100644 index 000000000..13d7b9c4c --- /dev/null +++ b/docus/app/components/example/ExampleIconCard.vue @@ -0,0 +1,29 @@ + + + diff --git a/docus/app/components/example/ExampleLandingHero.vue b/docus/app/components/example/ExampleLandingHero.vue new file mode 100644 index 000000000..89a98a5c3 --- /dev/null +++ b/docus/app/components/example/ExampleLandingHero.vue @@ -0,0 +1,22 @@ + + + diff --git a/docus/app/components/example/ExampleTitle.vue b/docus/app/components/example/ExampleTitle.vue new file mode 100644 index 000000000..38d6124ac --- /dev/null +++ b/docus/app/components/example/ExampleTitle.vue @@ -0,0 +1,5 @@ + diff --git a/docus/content/index.md b/docus/content/index.md index 41cf1ba13..2eda6d47d 100644 --- a/docus/content/index.md +++ b/docus/content/index.md @@ -8,10 +8,10 @@ seo: --- ::u-page-hero - :::div{.hidden.sm:block} + :::div{class="hidden sm:block"} ::::u-color-mode-image --- - class: size-full absolute bottom-0 inset-0 z-[-1] + class: size-full absolute bottom-0 inset-0 dark: /home/hero-dark.svg light: /home/hero-light.svg --- @@ -172,11 +172,16 @@ reverse: true orientation: horizontal --- :::tabs - ::::tabs-item{icon="i-lucide-eye" label="Preview"} + ::::tabs-item + --- + icon: i-lucide-eye + label: Preview + --- :::::browser-frame - :::::u-page-hero - ![Everest visual](/mountains/everest.jpg) - + :::::example-landing-hero + --- + image: /mountains/everest.jpg + --- #title The Everest. @@ -186,16 +191,21 @@ orientation: horizontal ::::: :::: - ::::tabs-item{icon="i-simple-icons-markdown" label="content/index.md"} + ::::tabs-item + --- + class: overflow-x-auto border border-muted rounded-lg p-4 + icon: i-simple-icons-markdown + label: content/index.md + --- ```mdc [content/index.md] --- title: The Mountains Website description: A website about the most iconic mountains in the world. --- - - ::page-hero + + ::landing-hero --- - image: /mountains/everest.png + image: /mountains/everest.jpg --- #title The Everest. @@ -203,17 +213,25 @@ orientation: horizontal #description The Everest is the highest mountain in the world, standing at 8,848 meters above sea level. :: + ``` :::: - ::::tabs-item{icon="i-simple-icons-vuedotjs" label="components/PageHero.vue"} - ```vue [components/PageHero.vue] + ::::tabs-item + --- + class: overflow-x-auto border border-muted rounded-lg p-1.5 text-sm + icon: i-simple-icons-vuedotjs + label: components/LandingHero.vue + --- + ```vue [components/LandingHero.vue] ``` - ::::preview-card{icon="i-lucide-eye" label="Preview"} + ::::code-preview{icon="i-lucide-eye" label="Preview"} :::::example-title A [rich text](/) will be **rendered** by the component. ::::: @@ -273,17 +273,28 @@ The **alert** component. ```vue [Alert.vue] ``` - :::preview-card{icon="i-lucide-eye" label="Preview"} + :::code-preview{icon="i-lucide-eye" label="Preview"} ::::example-alert{type="warning"} The **alert** component. :::: @@ -373,11 +384,11 @@ defineProps({ ``` - :::preview-card{icon="i-lucide-eye" label="Preview"} + :::code-preview{icon="i-lucide-eye" label="Preview"} ::::example-icon-card --- description: Harness the full power of Nuxt and the Nuxt ecosystem. - icon: IconNuxt + icon: i-simple-icons-nuxtdotjs title: Nuxt Architecture. --- :::: @@ -395,7 +406,7 @@ Possible values are all named attributes, classes with the notation `.class-name Hello [World]{style="color: green;" .custom-class #custom-id}! ``` - :::preview-card{icon="i-lucide-eye" label="Preview"} + :::code-preview{icon="i-lucide-eye" label="Preview"} Hello [World]{#custom-id .custom-class style="color: green;"} ! ::: :: @@ -406,14 +417,14 @@ In addition to mdc components and `span`, attribute syntax will work on images, ```md [index.md] Attributes work on: -- [link](#attributes){style="background-color: pink;"}, `code`{style="color: cyan;"}, +- [link](#attributes){style="background-color: green;"}, `code`{style="color: cyan;"}, - _italic_{style="background-color: yellow; color:black;"} and **bold**{style="background-color: lightgreen;"} texts. ``` - :::preview-card{prose label="Preview"} + :::code-preview{prose label="Preview"} Attributes work on: - - [link](#attributes){style="background-color: pink;"}, `code`, + - [link](#attributes){style="background-color: green;"}, `code`, - *italic* and **bold** texts. ::: :: diff --git a/docus/content/docs/5.components/2.prose.md b/docus/content/docs/5.components/2.prose.md index 98fe8c6aa..56cc0bb75 100644 --- a/docus/content/docs/5.components/2.prose.md +++ b/docus/content/docs/5.components/2.prose.md @@ -25,7 +25,7 @@ Prose components are originally part of [`@nuxtjs/mdc`](https://github.com/nuxt- [Link](/docs/components/prose) ``` - :::preview-card{icon="i-lucide-eye" label="Preview"} + :::code-preview{icon="i-lucide-eye" label="Preview"} [Link](/docs/components/prose) ::: :: @@ -37,7 +37,7 @@ Prose components are originally part of [`@nuxtjs/mdc`](https://github.com/nuxt- > Block quote ``` - :::preview-card{icon="i-lucide-eye" label="Preview"} + :::code-preview{icon="i-lucide-eye" label="Preview"} > Block quote ::: :: @@ -53,7 +53,7 @@ Prose components are originally part of [`@nuxtjs/mdc`](https://github.com/nuxt- ``` ```` - :::preview-card{icon="i-lucide-eye" label="Preview"} + :::code-preview{icon="i-lucide-eye" label="Preview"} ```js [file.js] export default () => { console.log('Code block') @@ -89,7 +89,7 @@ If you want to use `]` in the filename, you need to escape it with 2 backslashes `const code: string = 'highlighted code inline'`{lang="ts"} ``` - :::preview-card{icon="i-lucide-eye" label="Preview"} + :::code-preview{icon="i-lucide-eye" label="Preview"} `code` `const code: string = 'highlighted code inline'` @@ -103,7 +103,7 @@ If you want to use `]` in the filename, you need to escape it with 2 backslashes # H1 Heading ``` - :::preview-card{.pt-4 label="Preview"} + :::code-preview{.pt-4 label="Preview"} # H1 Heading ::: @@ -116,7 +116,7 @@ If you want to use `]` in the filename, you need to escape it with 2 backslashes ## H2 Heading ``` - :::preview-card{icon="i-lucide-eye" label="Preview"} + :::code-preview{icon="i-lucide-eye" label="Preview"} ## H2 Heading ::: @@ -129,7 +129,7 @@ If you want to use `]` in the filename, you need to escape it with 2 backslashes ### H3 Heading ``` - :::preview-card{icon="i-lucide-eye" label="Preview"} + :::code-preview{icon="i-lucide-eye" label="Preview"} ### H3 Heading ::: @@ -142,7 +142,7 @@ If you want to use `]` in the filename, you need to escape it with 2 backslashes #### H4 Heading ``` - :::preview-card{icon="i-lucide-eye" label="Preview"} + :::code-preview{icon="i-lucide-eye" label="Preview"} #### H4 Heading ::: @@ -155,7 +155,7 @@ If you want to use `]` in the filename, you need to escape it with 2 backslashes ##### H5 Heading ``` - :::preview-card{icon="i-lucide-eye" label="Preview"} + :::code-preview{icon="i-lucide-eye" label="Preview"} ##### H5 Heading ::: @@ -168,7 +168,7 @@ If you want to use `]` in the filename, you need to escape it with 2 backslashes ###### H6 Heading ``` - :::preview-card{icon="i-lucide-eye" label="Preview"} + :::code-preview{icon="i-lucide-eye" label="Preview"} ###### H6 Heading ::: @@ -185,7 +185,7 @@ Divider under. Divider above. ``` - :::preview-card{icon="i-lucide-eye" label="Preview"} + :::code-preview{icon="i-lucide-eye" label="Preview"} Divider under. --- @@ -201,7 +201,7 @@ Divider above. ![A Cool Image](https://nuxt.com/assets/design-kit/icon-green.png) ``` - :::preview-card{icon="i-lucide-eye" label="Preview"} + :::code-preview{icon="i-lucide-eye" label="Preview"} ![A Cool Image](https://nuxt.com/assets/design-kit/icon-green.png) ::: :: @@ -216,7 +216,7 @@ Divider above. - List ``` - :::preview-card{icon="i-lucide-eye" label="Preview"} + :::code-preview{icon="i-lucide-eye" label="Preview"} - Just - An - Unordered @@ -231,7 +231,7 @@ Divider above. - List element ``` - :::preview-card{icon="i-lucide-eye" label="Preview"} + :::code-preview{icon="i-lucide-eye" label="Preview"} - List element ::: :: @@ -245,7 +245,7 @@ Divider above. 3. Baz ``` - :::preview-card{icon="i-lucide-eye" label="Preview"} + :::code-preview{icon="i-lucide-eye" label="Preview"} 1. Foo 2. Bar 3. Baz @@ -259,7 +259,7 @@ Divider above. Just a paragraph. ``` - :::preview-card{icon="i-lucide-eye" label="Preview"} + :::code-preview{icon="i-lucide-eye" label="Preview"} Just a paragraph. ::: :: @@ -271,7 +271,7 @@ Just a paragraph. **Just a strong paragraph.** ``` - :::preview-card{icon="i-lucide-eye" label="Preview"} + :::code-preview{icon="i-lucide-eye" label="Preview"} **Just a strong paragraph.** ::: :: @@ -283,7 +283,7 @@ Just a paragraph. _Just an italic paragraph._ ``` - :::preview-card{icon="i-lucide-eye" label="Preview"} + :::code-preview{icon="i-lucide-eye" label="Preview"} _Just an italic paragraph._ ::: :: @@ -299,7 +299,7 @@ _Just an italic paragraph._ | 3 | Wonderful | Website | ``` - :::preview-card{icon="i-lucide-eye" label="Preview"} + :::code-preview{icon="i-lucide-eye" label="Preview"} | Key | Type | Description | | --- | --------- | ----------- | diff --git a/docus/content/docs/7.advanced/1.fulltext-search.md b/docus/content/docs/7.advanced/1.fulltext-search.md index 3035891e4..064a6aa6f 100644 --- a/docus/content/docs/7.advanced/1.fulltext-search.md +++ b/docus/content/docs/7.advanced/1.fulltext-search.md @@ -30,7 +30,7 @@ const searchTerm = ref('') ``` - :::preview-card{label="Live Preview"} + :::code-preview{label="Preview" icon="i-lucide-eye"} ::::example-fulltext-content-search :::: ::: @@ -77,7 +77,7 @@ const result = computed(() => miniSearch.search(toValue(query))) ``` - :::preview-card{label="Live Preview"} + :::code-preview{label="Preview" icon="i-lucide-eye"} ::::example-fulltext-mini-search :::: ::: @@ -121,7 +121,7 @@ const result = computed(() => fuse.search(toValue(query)).slice(0, 10)) ``` - :::preview-card{label="Live Preview"} + :::code-preview{label="Preview" icon="i-lucide-eye"} ::::example-fulltext-fusejs :::: ::: diff --git a/docus/content/docs/8.studio/3.content.md b/docus/content/docs/8.studio/3.content.md index d8eb1d106..14079a96a 100644 --- a/docus/content/docs/8.studio/3.content.md +++ b/docus/content/docs/8.studio/3.content.md @@ -192,7 +192,7 @@ export default defineContentConfig({ }) ``` - :::preview-card{icon="i-lucide-eye" label="Generated Form"} + :::code-preview{icon="i-lucide-eye" label="Generated Form"} ![Form preview](/docs/studio/preview-schema.png) ::: :: diff --git a/docus/content/docs/8.studio/5.config.md b/docus/content/docs/8.studio/5.config.md index d17de322a..6b89a0090 100644 --- a/docus/content/docs/8.studio/5.config.md +++ b/docus/content/docs/8.studio/5.config.md @@ -110,7 +110,7 @@ Once your schema is deployed. Any user can access the **Data** section and play Any update in the form will be directly applied to the `app.config.ts` file. You can review those changes on the review page. ::code-group - :::preview-card{icon="i-lucide-eye" label="Preview"} + :::code-preview{icon="i-lucide-eye" label="Preview"} ![app config UI on Studio](/docs/studio/home-data-studio-dark.webp) ::: diff --git a/docus/content/studio/index.md b/docus/content/studio/index.md index 2a1b01434..b8a994683 100644 --- a/docus/content/studio/index.md +++ b/docus/content/studio/index.md @@ -159,7 +159,7 @@ orientation: horizontal :: ``` - ::::preview-card{icon="i-lucide-eye" label="Editor"} + ::::code-preview{icon="i-lucide-eye" label="Editor"} ![vue component edition on Studio](/docs/studio/home-content-studio-dark.webp) :::: ::: diff --git a/docus/package.json b/docus/package.json index d7f9731c9..6dd94aeb8 100644 --- a/docus/package.json +++ b/docus/package.json @@ -5,7 +5,7 @@ "build": "docus build" }, "dependencies": { - "docus": "latest", + "docus": "3.1.0-20250619-105902-65a477a", "better-sqlite3": "^11.10.0", "nuxt": "^3.17.3" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8dec0f102..79ff73fca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -295,8 +295,8 @@ importers: specifier: ^11.10.0 version: 11.10.0 docus: - specifier: latest - version: 3.0.2(7dee646ded917034475ae42eea927616) + specifier: 3.1.0-20250619-105902-65a477a + version: 3.1.0-20250619-105902-65a477a(7dee646ded917034475ae42eea927616) nuxt: specifier: ^3.17.3 version: 3.17.5(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.8)(@parcel/watcher@2.5.1)(@types/node@22.15.31)(better-sqlite3@11.10.0)(db0@0.3.2(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.8)(better-sqlite3@11.10.0)(sqlite3@5.1.7))(encoding@0.1.13)(eslint@9.28.0(jiti@2.4.2))(ioredis@5.6.1)(lightningcss@1.30.1)(magicast@0.3.5)(meow@13.2.0)(optionator@0.9.4)(rollup@4.41.1)(sqlite3@5.1.7)(terser@5.40.0)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(yaml@2.8.0))(vue-tsc@2.2.10(typescript@5.8.3))(yaml@2.8.0) @@ -4080,8 +4080,8 @@ packages: resolution: {integrity: sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==} engines: {node: '>=0.3.1'} - docus@3.0.2: - resolution: {integrity: sha512-OmY7EgQnN7Oli/qmgQ9fz4pD14AEEMDhoTGf9CNzvwHM/qCdhtsJNUSSBiB3sDUqlsMdgg9HPJSQQtZ8oJAATQ==} + docus@3.1.0-20250619-105902-65a477a: + resolution: {integrity: sha512-L2QIzjOVKw3TlybZz4mbSxt11PVNeJGVfwEjqnQApnin6f1jO0vbwV3xlDNQ1pZgJuCVTqxQPoVHVGUoREgluQ==} hasBin: true peerDependencies: better-sqlite3: 11.x @@ -12374,7 +12374,7 @@ snapshots: diff@8.0.2: {} - docus@3.0.2(7dee646ded917034475ae42eea927616): + docus@3.1.0-20250619-105902-65a477a(7dee646ded917034475ae42eea927616): dependencies: '@iconify-json/lucide': 1.2.47 '@iconify-json/simple-icons': 1.2.38 From 1aeaa49b76fc85c2c6559944e6f38d92acce236f Mon Sep 17 00:00:00 2001 From: Baptiste Leproux Date: Thu, 19 Jun 2025 16:44:34 +0200 Subject: [PATCH 04/23] header --- docus/app/components/AppHeaderCenter.vue | 45 ++++++++++++++++++++++++ docus/app/components/AppHeaderLogo.vue | 34 ++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 docus/app/components/AppHeaderCenter.vue create mode 100644 docus/app/components/AppHeaderLogo.vue diff --git a/docus/app/components/AppHeaderCenter.vue b/docus/app/components/AppHeaderCenter.vue new file mode 100644 index 000000000..877869c88 --- /dev/null +++ b/docus/app/components/AppHeaderCenter.vue @@ -0,0 +1,45 @@ + + + diff --git a/docus/app/components/AppHeaderLogo.vue b/docus/app/components/AppHeaderLogo.vue new file mode 100644 index 000000000..e905f5ab2 --- /dev/null +++ b/docus/app/components/AppHeaderLogo.vue @@ -0,0 +1,34 @@ + From 202cbc2147bcd60e7d5e97038a8b481788083a19 Mon Sep 17 00:00:00 2001 From: Baptiste Leproux Date: Thu, 19 Jun 2025 17:37:33 +0200 Subject: [PATCH 05/23] other pages --- docs/app/app.config.ts | 34 ++++ docs/app/components/AppFooter.vue | 5 + docs/app/components/post/PostPage.vue | 2 - docus/app/app.config.ts | 6 +- docus/app/components/AppHeaderCTA.vue | 9 + docus/app/components/AppHeaderCenter.vue | 6 +- docus/app/components/PostPage.vue | 171 +++++++++++++++++ docus/app/components/content/TemplateCore.vue | 10 + .../components/content/TemplateFeatures.vue | 20 ++ docus/app/pages/blog.vue | 3 + docus/app/pages/blog/[slug].vue | 3 + docus/app/pages/blog/index.vue | 51 +++++ docus/app/pages/changelog.vue | 3 + docus/app/pages/changelog/[slug].vue | 3 + docus/app/pages/changelog/index.vue | 144 +++++++++++++++ docus/app/pages/studio/index.vue | 26 +++ docus/app/pages/studio/pricing.vue | 174 ++++++++++++++++++ docus/app/pages/templates.vue | 3 + docus/app/pages/templates/[slug].vue | 136 ++++++++++++++ docus/app/pages/templates/index.vue | 57 ++++++ docus/app/utils/index.ts | 14 ++ docus/content.config.ts | 157 +++++++++++++++- docus/content/index.md | 1 + docus/nuxt.config.ts | 3 +- docus/package.json | 3 +- pnpm-lock.yaml | 9 +- 26 files changed, 1036 insertions(+), 17 deletions(-) create mode 100644 docus/app/components/AppHeaderCTA.vue create mode 100644 docus/app/components/PostPage.vue create mode 100644 docus/app/components/content/TemplateCore.vue create mode 100644 docus/app/components/content/TemplateFeatures.vue create mode 100644 docus/app/pages/blog.vue create mode 100644 docus/app/pages/blog/[slug].vue create mode 100644 docus/app/pages/blog/index.vue create mode 100644 docus/app/pages/changelog.vue create mode 100644 docus/app/pages/changelog/[slug].vue create mode 100644 docus/app/pages/changelog/index.vue create mode 100644 docus/app/pages/studio/index.vue create mode 100644 docus/app/pages/studio/pricing.vue create mode 100644 docus/app/pages/templates.vue create mode 100644 docus/app/pages/templates/[slug].vue create mode 100644 docus/app/pages/templates/index.vue create mode 100644 docus/app/utils/index.ts diff --git a/docs/app/app.config.ts b/docs/app/app.config.ts index fd525fcd0..18d2234b4 100644 --- a/docs/app/app.config.ts +++ b/docs/app/app.config.ts @@ -1,4 +1,38 @@ export default defineAppConfig({ + socials: [ + { + label: 'Nuxt Content on Discord', + icon: 'i-simple-icons-discord', + to: 'https://discord.gg/sBXDm6e8SP', + target: '_blank', + color: 'neutral', + variant: 'ghost', + }, + { + label: 'Nuxt on BlueSKy', + icon: 'i-simple-icons-bluesky', + to: 'https://go.nuxt.com/bluesky', + target: '_blank', + color: 'neutral', + variant: 'ghost', + }, + { + label: 'Nuxt on X', + icon: 'i-simple-icons-x', + to: 'https://x.com/nuxtstudio', + target: '_blank', + color: 'neutral', + variant: 'ghost', + }, + { + label: 'Nuxt Content on GitHub', + icon: 'i-simple-icons-github', + to: 'https://github.com/nuxt/content', + target: '_blank', + color: 'neutral', + variant: 'ghost', + }, + ], ui: { colors: { primary: 'green', diff --git a/docs/app/components/AppFooter.vue b/docs/app/components/AppFooter.vue index b05c4c5a2..f9cdfdc3a 100644 --- a/docs/app/components/AppFooter.vue +++ b/docs/app/components/AppFooter.vue @@ -1,6 +1,11 @@ diff --git a/docus/app/components/PostPage.vue b/docus/app/components/PostPage.vue new file mode 100644 index 000000000..376b74d8f --- /dev/null +++ b/docus/app/components/PostPage.vue @@ -0,0 +1,171 @@ + + + diff --git a/docus/app/components/content/TemplateCore.vue b/docus/app/components/content/TemplateCore.vue new file mode 100644 index 000000000..850348387 --- /dev/null +++ b/docus/app/components/content/TemplateCore.vue @@ -0,0 +1,10 @@ + diff --git a/docus/app/components/content/TemplateFeatures.vue b/docus/app/components/content/TemplateFeatures.vue new file mode 100644 index 000000000..eb5f4fcd9 --- /dev/null +++ b/docus/app/components/content/TemplateFeatures.vue @@ -0,0 +1,20 @@ + + + diff --git a/docus/app/pages/blog.vue b/docus/app/pages/blog.vue new file mode 100644 index 000000000..8f62b8bf9 --- /dev/null +++ b/docus/app/pages/blog.vue @@ -0,0 +1,3 @@ + diff --git a/docus/app/pages/blog/[slug].vue b/docus/app/pages/blog/[slug].vue new file mode 100644 index 000000000..e26affeab --- /dev/null +++ b/docus/app/pages/blog/[slug].vue @@ -0,0 +1,3 @@ + diff --git a/docus/app/pages/blog/index.vue b/docus/app/pages/blog/index.vue new file mode 100644 index 000000000..3c3fe15a5 --- /dev/null +++ b/docus/app/pages/blog/index.vue @@ -0,0 +1,51 @@ + + + diff --git a/docus/app/pages/changelog.vue b/docus/app/pages/changelog.vue new file mode 100644 index 000000000..8f62b8bf9 --- /dev/null +++ b/docus/app/pages/changelog.vue @@ -0,0 +1,3 @@ + diff --git a/docus/app/pages/changelog/[slug].vue b/docus/app/pages/changelog/[slug].vue new file mode 100644 index 000000000..30ae591e3 --- /dev/null +++ b/docus/app/pages/changelog/[slug].vue @@ -0,0 +1,3 @@ + diff --git a/docus/app/pages/changelog/index.vue b/docus/app/pages/changelog/index.vue new file mode 100644 index 000000000..97a52d561 --- /dev/null +++ b/docus/app/pages/changelog/index.vue @@ -0,0 +1,144 @@ + + + + + diff --git a/docus/app/pages/studio/index.vue b/docus/app/pages/studio/index.vue new file mode 100644 index 000000000..dd5ec2fd2 --- /dev/null +++ b/docus/app/pages/studio/index.vue @@ -0,0 +1,26 @@ + + + diff --git a/docus/app/pages/studio/pricing.vue b/docus/app/pages/studio/pricing.vue new file mode 100644 index 000000000..0584248a3 --- /dev/null +++ b/docus/app/pages/studio/pricing.vue @@ -0,0 +1,174 @@ + + + diff --git a/docus/app/pages/templates.vue b/docus/app/pages/templates.vue new file mode 100644 index 000000000..8f62b8bf9 --- /dev/null +++ b/docus/app/pages/templates.vue @@ -0,0 +1,3 @@ + diff --git a/docus/app/pages/templates/[slug].vue b/docus/app/pages/templates/[slug].vue new file mode 100644 index 000000000..a4c8fc4f1 --- /dev/null +++ b/docus/app/pages/templates/[slug].vue @@ -0,0 +1,136 @@ + + + diff --git a/docus/app/pages/templates/index.vue b/docus/app/pages/templates/index.vue new file mode 100644 index 000000000..c0ff19def --- /dev/null +++ b/docus/app/pages/templates/index.vue @@ -0,0 +1,57 @@ + + + diff --git a/docus/app/utils/index.ts b/docus/app/utils/index.ts new file mode 100644 index 000000000..625d64654 --- /dev/null +++ b/docus/app/utils/index.ts @@ -0,0 +1,14 @@ +export const formatDateByLocale = (d: string | number | Date, options: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'long', day: 'numeric' }) => { + return new Date(d).toLocaleDateString('en', options) +} + +export const TEMPLATE_BADGES = { + 'free': { + color: 'secondary' as const, + label: 'Free', + }, + 'nuxt-ui-pro': { + color: 'primary' as const, + label: 'Nuxt UI Pro', + }, +} diff --git a/docus/content.config.ts b/docus/content.config.ts index 8c2ecbf7a..99b3c0903 100644 --- a/docus/content.config.ts +++ b/docus/content.config.ts @@ -1,14 +1,43 @@ import { defineContentConfig, defineCollection, z } from '@nuxt/content' +const createPricingPlanSchema = () => z.object({ + title: z.string(), + description: z.string(), + price: z.string(), + cycle: z.string().optional(), + features: z.array(z.string()), + badge: z.string().optional(), + hightlight: z.boolean().optional(), + button: z.object({ + label: z.string(), + color: z.enum(['error', 'primary', 'neutral', 'secondary', 'success', 'info', 'warning']), + to: z.string(), + target: z.string().optional(), + }), +}) + +const createPricingFeatureSchema = () => z.object({ + title: z.string(), + plans: z.array(z.enum(['solo', 'team', 'unlimited'])).optional(), + value: z.array(z.string()).optional(), + soon: z.boolean().optional(), +}) + export default defineContentConfig({ collections: { landing: defineCollection({ type: 'page', - source: { - // @ts-expect-error __DOCS_DIR__ is not defined - cwd: globalThis.__DOCS_DIR__, + source: [{ include: 'index.md', - }, + }, { + include: 'blog.yml', + }, { + include: 'changelog.yml', + }, { + include: 'studio/index.md', + }, { + include: 'templates.yml', + }], }), docs: defineCollection({ type: 'page', @@ -24,5 +53,125 @@ export default defineContentConfig({ })).optional(), }), }), + pricing: defineCollection({ + type: 'page', + source: 'studio/pricing.yml', + schema: z.object({ + onboarding: z.object({ + title: z.string(), + image: z.object({ + dark: z.string().editor({ input: 'media' }), + light: z.string().editor({ input: 'media' }), + }), + }), + plans: z.object({ + solo: createPricingPlanSchema(), + team: createPricingPlanSchema(), + unlimited: createPricingPlanSchema(), + }), + features: z.object({ + title: z.string(), + description: z.string(), + includes: z.object({ + projects: createPricingFeatureSchema(), + members: createPricingFeatureSchema(), + media: createPricingFeatureSchema(), + support: createPricingFeatureSchema(), + dedicated: createPricingFeatureSchema(), + roles: createPricingFeatureSchema(), + collaboration: createPricingFeatureSchema(), + sync: z.object({ + title: z.string(), + includes: z.object({ + repositories: createPricingFeatureSchema(), + workflow: createPricingFeatureSchema(), + }), + }), + project: z.object({ + title: z.string(), + includes: z.object({ + clone: createPricingFeatureSchema(), + import: createPricingFeatureSchema(), + }), + }), + editors: z.object({ + title: z.string(), + includes: z.object({ + markdown: createPricingFeatureSchema(), + json: createPricingFeatureSchema(), + appconfig: createPricingFeatureSchema(), + drag: createPricingFeatureSchema(), + }), + }), + preview: z.object({ + title: z.string(), + includes: z.object({ + draft: createPricingFeatureSchema(), + branches: createPricingFeatureSchema(), + prs: createPricingFeatureSchema(), + }), + }), + deploy: z.object({ + title: z.string(), + includes: z.object({ + gh: createPricingFeatureSchema(), + self: createPricingFeatureSchema(), + }), + }), + publish: z.object({ + title: z.string(), + includes: z.object({ + preview: createPricingFeatureSchema(), + branch: createPricingFeatureSchema(), + commit: createPricingFeatureSchema(), + }), + }), + }), + }), + }), + }), + templates: defineCollection({ + type: 'page', + source: 'templates/*.md', + schema: z.object({ + draft: z.boolean().default(false), + slug: z.string().editor({ hidden: true }), + subtitle: z.string(), + baseDir: z.string(), + branch: z.string(), + category: z.enum(['docs', 'blog', 'minimal', 'saas']), + demo: z.string(), + licenseType: z.enum(['nuxt-ui-pro', 'free']), + mainScreen: z.string().editor({ input: 'media' }), + name: z.string(), + owner: z.string(), + image1: z.string().editor({ input: 'media' }), + image2: z.string().editor({ input: 'media' }), + image3: z.string().editor({ input: 'media' }), + }), + }), + posts: defineCollection({ + type: 'page', + source: [{ include: 'blog/*.md' }, { include: 'changelog/*.md' }], + schema: z.object({ + draft: z.boolean().default(false), + authors: z.array(z.object({ + slug: z.string(), + username: z.string(), + name: z.string(), + to: z.string(), + avatar: z.object({ + src: z.string(), + alt: z.string(), + }), + })), + category: z.enum(['studio', 'content']).optional(), + date: z.date(), + image: z.object({ + src: z.string().editor({ input: 'media' }), + alt: z.string(), + }), + }), + }), }, }) diff --git a/docus/content/index.md b/docus/content/index.md index 2eda6d47d..9ee7511be 100644 --- a/docus/content/index.md +++ b/docus/content/index.md @@ -5,6 +5,7 @@ seo: manage content for your application. It allows developers to write their content in Markdown, YAML, or JSON files and then query and display it in their application. + ogImage: /social.png --- ::u-page-hero diff --git a/docus/nuxt.config.ts b/docus/nuxt.config.ts index 070fd76d2..d25d1f5ac 100644 --- a/docus/nuxt.config.ts +++ b/docus/nuxt.config.ts @@ -1,5 +1,6 @@ export default defineNuxtConfig({ - modules: ['@nuxtjs/plausible'], + + modules: ['@nuxtjs/plausible', '@vueuse/nuxt'], css: ['../app/assets/css/main.css'], site: { name: 'Nuxt Content', diff --git a/docus/package.json b/docus/package.json index 6dd94aeb8..394873485 100644 --- a/docus/package.json +++ b/docus/package.json @@ -5,8 +5,9 @@ "build": "docus build" }, "dependencies": { - "docus": "3.1.0-20250619-105902-65a477a", + "@vueuse/nuxt": "^13.3.0", "better-sqlite3": "^11.10.0", + "docus": "3.1.0-20250619-105902-65a477a", "nuxt": "^3.17.3" } } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 79ff73fca..43992fd51 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -291,12 +291,15 @@ importers: docus: dependencies: + '@vueuse/nuxt': + specifier: ^13.3.0 + version: 13.3.0(magicast@0.3.5)(nuxt@3.17.5(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.8)(@parcel/watcher@2.5.1)(@types/node@22.15.31)(better-sqlite3@11.10.0)(db0@0.3.2(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.8)(better-sqlite3@11.10.0)(sqlite3@5.1.7))(encoding@0.1.13)(eslint@9.28.0(jiti@2.4.2))(ioredis@5.6.1)(lightningcss@1.30.1)(magicast@0.3.5)(meow@13.2.0)(optionator@0.9.4)(rollup@4.41.1)(sqlite3@5.1.7)(terser@5.40.0)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(yaml@2.8.0))(vue-tsc@2.2.10(typescript@5.8.3))(yaml@2.8.0))(vue@3.5.16(typescript@5.8.3)) better-sqlite3: specifier: ^11.10.0 version: 11.10.0 docus: specifier: 3.1.0-20250619-105902-65a477a - version: 3.1.0-20250619-105902-65a477a(7dee646ded917034475ae42eea927616) + version: 3.1.0-20250619-105902-65a477a(4505dd65a4aab09d81e65022b00b8739) nuxt: specifier: ^3.17.3 version: 3.17.5(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.8)(@parcel/watcher@2.5.1)(@types/node@22.15.31)(better-sqlite3@11.10.0)(db0@0.3.2(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.8)(better-sqlite3@11.10.0)(sqlite3@5.1.7))(encoding@0.1.13)(eslint@9.28.0(jiti@2.4.2))(ioredis@5.6.1)(lightningcss@1.30.1)(magicast@0.3.5)(meow@13.2.0)(optionator@0.9.4)(rollup@4.41.1)(sqlite3@5.1.7)(terser@5.40.0)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(yaml@2.8.0))(vue-tsc@2.2.10(typescript@5.8.3))(yaml@2.8.0) @@ -11381,7 +11384,7 @@ snapshots: '@vueuse/nuxt@13.3.0(magicast@0.3.5)(nuxt@3.17.5(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.8)(@parcel/watcher@2.5.1)(@types/node@22.15.31)(better-sqlite3@11.10.0)(db0@0.3.2(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.8)(better-sqlite3@11.10.0)(sqlite3@5.1.7))(encoding@0.1.13)(eslint@9.28.0(jiti@2.4.2))(ioredis@5.6.1)(lightningcss@1.30.1)(magicast@0.3.5)(meow@13.2.0)(optionator@0.9.4)(rollup@4.41.1)(sqlite3@5.1.7)(terser@5.40.0)(typescript@5.8.3)(vite@6.3.5(@types/node@22.15.31)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.40.0)(yaml@2.8.0))(vue-tsc@2.2.10(typescript@5.8.3))(yaml@2.8.0))(vue@3.5.16(typescript@5.8.3))': dependencies: - '@nuxt/kit': 3.17.4(magicast@0.3.5) + '@nuxt/kit': 3.17.5(magicast@0.3.5) '@vueuse/core': 13.3.0(vue@3.5.16(typescript@5.8.3)) '@vueuse/metadata': 13.3.0 local-pkg: 1.1.1 @@ -12374,7 +12377,7 @@ snapshots: diff@8.0.2: {} - docus@3.1.0-20250619-105902-65a477a(7dee646ded917034475ae42eea927616): + docus@3.1.0-20250619-105902-65a477a(4505dd65a4aab09d81e65022b00b8739): dependencies: '@iconify-json/lucide': 1.2.47 '@iconify-json/simple-icons': 1.2.38 From 6600c7bf3842d76bc9700d75a4646a2db7675842 Mon Sep 17 00:00:00 2001 From: Baptiste Leproux Date: Thu, 19 Jun 2025 18:09:15 +0200 Subject: [PATCH 06/23] up --- docs/app/assets/css/main.css | 5 +- docus/app/components/DocsAsideLeftTop.vue | 6 ++ docus/app/components/PostPage.vue | 2 +- docus/app/pages/studio/pricing.vue | 2 +- docus/content/index.md | 8 +- docus/content/studio/index.md | 106 +++++++++++++--------- 6 files changed, 79 insertions(+), 50 deletions(-) create mode 100644 docus/app/components/DocsAsideLeftTop.vue diff --git a/docs/app/assets/css/main.css b/docs/app/assets/css/main.css index 4bebef999..2c9e79c6f 100644 --- a/docs/app/assets/css/main.css +++ b/docs/app/assets/css/main.css @@ -1,7 +1,8 @@ -@import "tailwindcss" theme(static); +@import "tailwindcss"; @import "@nuxt/ui-pro"; -@source "../../../content/**/*.md"; +@source "../../../content/**/*"; +@source "../../../node_modules/docus/app/**/*"; @theme static { --font-sans: 'Public Sans', sans-serif; diff --git a/docus/app/components/DocsAsideLeftTop.vue b/docus/app/components/DocsAsideLeftTop.vue new file mode 100644 index 000000000..0e660939c --- /dev/null +++ b/docus/app/components/DocsAsideLeftTop.vue @@ -0,0 +1,6 @@ + diff --git a/docus/app/components/PostPage.vue b/docus/app/components/PostPage.vue index 376b74d8f..9f70709c6 100644 --- a/docus/app/components/PostPage.vue +++ b/docus/app/components/PostPage.vue @@ -157,7 +157,7 @@ function copyLink() { :links="post.body.toc.links" >