Skip to content

feat: add Cloudinary media provider and Groq & Gemini AI providers - #537

Closed
narr07 wants to merge 591 commits into
nuxt-content:mainfrom
narr07:main
Closed

feat: add Cloudinary media provider and Groq & Gemini AI providers#537
narr07 wants to merge 591 commits into
nuxt-content:mainfrom
narr07:main

Conversation

@narr07

@narr07 narr07 commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR introduces Cloudinary as an external media provider and expands AI content assistance capabilities by supporting GroqCloud and Google Gemini providers alongside Vercel AI Gateway.

Features Added

🖼️ Cloudinary External Media Provider

  • Added cloudinary provider option under studio.media configuration.
  • Server-side API integration for media listing, upload, and deletion without requiring NuxtHub.
  • Excluded Cloudinary credentials from client-side bundles for maximum security.

🤖 Multi-Provider AI Support (Groq & Gemini)

  • Support for GroqCloud (GROQ_API_KEY) offering free Llama 3.3, Llama 3.1, Gemma & Qwen models.
  • Direct support for Google Gemini (GEMINI_API_KEY / NUXT_STUDIO_AI_GEMINI_API_KEY).
  • Auto provider resolution fallback order: Groq ➔ Gemini ➔ Vercel AI Gateway.
  • Configurable provider, groqModel, groqFastModel, geminiModel, and geminiFastModel in nuxt.config.ts.

Verification & Tests

  • All 17 merge conflicts cleanly resolved and rebased with upstream/main.
  • ESLint passed (0 errors).
  • Typecheck passed (vue-tsc -p src/app/tsconfig.app.json).
  • Vitest suite passed (385/385 tests passing).

larbish and others added 30 commits January 5, 2026 10:59
Co-authored-by: Baptiste Leproux <leproux.baptiste@gmail.com>
Co-authored-by: Baptiste Leproux <leproux.baptiste@gmail.com>
Co-authored-by: Baptiste Leproux <leproux.baptiste@gmail.com>
…udio editor (nuxt-content#254)

Co-authored-by: Baptiste Leproux <leproux.baptiste@gmail.com>
Co-authored-by: Nuxt Studio <noreply@nuxt.studio>
Co-authored-by: Nuxt Studio <noreply@nuxt.studio>
hendrikheil and others added 19 commits June 25, 2026 11:22
…tent#507)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
… (nuxt-content#510)

Since nuxt-content#478 moved env reads into the studio-env runtime middleware, each
provider's `moderators` defaults to `''` instead of `undefined`. The OAuth
handlers parsed it as `''.split(',')` -> `['']` (length 1), so the
`moderators.length > 0` guard fired and rejected every user with a 403,
even when no allowlist was configured.

Filter out blank entries so an unset/blank allowlist yields `[]`.

Signed-off-by: Justin Chen <mail@justin0u0.com>
…uxt-content#504)

setTimeout(100) could fire before draftDocuments.load() finished, causing
selectByFsPath to fall back to stale SQLite content and drop IndexedDB draft slots.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
UIcon does not support a size prop; it was silently ignored. Remove it so the icons render at their default size, and drop the redundant size on ItemBadge.

Co-authored-by: Hendrik Heil <hendrik@heil.sh>
…d bodies (nuxt-content#515)

* test(document): reproduce component array prop round-trip conflict

Add two intentionally-failing tests showing that a component's array prop,
in the body shape @nuxt/content (@nuxtjs/mdc) produces — a colon-bound,
JSON-stringified `:prop` — is not round-tripped back to YAML frontmatter by
contentFromDocument. Instead it is emitted as a malformed inline attribute
(unescaped nested quotes), which surfaces as a permanent, un-resolvable
conflict against the source file in the Studio editor.

Covers both arrays of primitives and arrays of objects; the serializer
breaks both identically.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(document): round-trip ':key' JSON array/object props as YAML blocks

@nuxtjs/mdc encodes a component's YAML-block array/object prop as a
colon-bound, JSON-stringified attr ({ ':bar': '["baz"]' }). Two paths let
that artifact survive to comark's renderer, which then emitted it as an
inline string literal (::foo{:bar="[...]"}) instead of a YAML block —
producing a permanent phantom conflict against the repo:

- The MDC→Comark bridge unwrapped ':bar' to a real array, but the
  token-list collapse (for class/ping) then flattened primitive arrays
  back to space-joined scalars. Exempt ':'-unwrapped keys from collapse.
- A body already in comark '.nodes' shape skips the bridge entirely, so
  the binding reached render untouched. Strip array/object ':'-bindings
  in contentFromMarkdownDocument via the new unbindComarkTree.

Only array/object bindings are unwrapped; scalar bindings (:width="200",
:reverse="true") are genuine comark inline bindings that round-trip on
their own.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(document): cover ':key' prop round-trip branches

Close the coverage gaps left by the fix:

- legacy.ts bridge: a primitive array unwrapped from a ':' binding must
  survive as an array (not collapse to a space-joined string like a bare
  token-list array). This is the exact bug the collapse exemption fixed,
  previously unguarded.
- render path: object prop renders as a YAML map block; a nested
  component's array prop is unwrapped (recursion); a scalar ':' binding
  (:width="200") stays an inline binding and is not unwrapped to a block.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(document): trim verbose comments to non-obvious why

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
… last location (nuxt-content#505)

* fix(app): open file for current host route instead of forcing back to last location

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(app): source current route from host contract, not window.location

open() read window.location.pathname to find the file for the current host
page, but that includes the host app's baseURL and trailing-slash form, so it
fails to match routePath under a non-root base. Route knowledge belongs to the
host; expose it via host.app.getCurrentRoute() and let open() ask for it,
matching how routeChange already flows route data across the boundary.

Also drops the string overload (and its cast) from selectByRoute, since
app.vue was its only string caller.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…de}` wrapper (nuxt-content#517)

comark renders a fence body from the `code` child and serializes any other
`pre` attr as a `::pre{attr}` wrapper (comark >=0.4.0). A stray `code` attr on
the `pre` therefore leaked as `::pre{code="…"}` instead of a plain ``` fence.
Two paths put it there:

- Editor (state D): `createCodeBlockElement` wrote `code` (plus stale Shiki
  `class`/`tabindex`) onto the `pre`. Build the attrs explicitly as
  `{language?, filename?}`; the raw source stays in the `code` child.

- Stored original (state B): the `@nuxtjs/mdc` legacy bridge carried mdc's
  duplicated `props.code` straight onto the comark `pre`, so the conflict /
  formatting round-trip of `original.body` leaked the wrapper. `mdcToComark`
  now routes `pre` through `preMdcToComarkNode`, which moves `props.code` into
  the `code` child (the only intact copy in mdc's miscapture-repair path) and
  drops the attr. Scoped to `code` only — an authored class still round-trips
  as the canonical `::pre{.class}`, and comark drops the Shiki artifacts.

Pin comark to pkg.pr.new@10ca6d9 (bare-`shiki` class guard) workspace-wide via
overrides + `blockExoticSubdeps: false`, with the sha512 integrity computed
from the tarball so frozen-lockfile installs don't fail with
ERR_PNPM_MISSING_TARBALL_INTEGRITY.

Adds round-trip tests for both paths asserting a plain ``` fence with no
`::pre` wrapper.
…nuxt-content#516)

comark's parse now supports `linkify: false`. Pass it at both parse sites so
bare URLs in content aren't auto-converted to links at parse time — the editor
already linkifies. Bumps comark to a build that exposes the option.
…ent#519)

@nuxtjs/mdc keeps the fence's trailing newline in props.code, whereas
comark's canonical code child omits it. The MDC→Comark bridge copied
that newline into the tree, so the TipTap editor rendered an extra blank
line at the end of every code block.

Strip a single trailing newline in preMdcToComarkNode (the one point all
pre nodes converge through). renderMarkdown output is byte-identical with
or without it, so there's no round-trip or formatting-banner impact.
…xt-content#520)

Pressing Backspace at the start of the block directly below the frontmatter
node ran ProseMirror's default joinBackward, which deletes adjacent atoms in a
branch that ignores `isolating`. The frontmatter is a content-less atom holding
all its data in attrs, so the delete discarded that data; comarkToTiptap then
re-inserted an empty frontmatter node, producing the "flash then reappears
blank" behavior.

Add a Backspace keymap that reads the selection and swallows the key when it
would join into the frontmatter. The check is intentionally read-only and does
NOT probe with joinBackward: inside a TipTap command, `state.tr` is the shared
transaction TipTap auto-dispatches, so a probe would actually perform the
deletion it was meant to detect.
…nuxt-content#521)

Older Studio builds persisted legacy (MarkdownRoot/minimark) bodies in
IndexedDB; comark's renderMarkdown expects a ComarkTree and threw on
publish/render. Upgrade to ComarkTree at the read boundaries (host DB
reads + draft load) via `ensureComarkBody` in the legacy compat layer.
…nt#524)

comark renders a br node natively as a markdown hard break ('  \n').
The ':br' override (added in the comark migration to mimic the old
@nuxtjs/mdc output) no longer matches comark and corrupted output to
':br' — which the site renders as 'Unknown node: hard break' and which
collides with the :br: emoji shortcode. Delete the override and defer
to comark's native rendering.

Refs: nuxt-content#265
…ection race (nuxt-content#530)

The reselect-current-item branch in handleDraftUpdate ran unawaited, so
it could resolve after a caller's own explicit selectItemByFsPath call
(e.g. CreateDocument) and clobber currentItem/draft.current back to the
previous item, bouncing the editor back to the file list and skipping
the host navigateTo call.
…uxt-content#531)

Stored node attrs may use whatever casing the source markdown/parser
produced, while nuxt-component-meta always declares props in
camelCase, so compare both forms via kebabCase to bind a component's
declared prop to its stored value regardless of casing.
- Created a comprehensive guide for testing Vue 3 components, including examples for component tests, composables, and utilities.
- Added TypeScript-specific patterns for Vue 3 development, covering provide/inject types, strict templates, and common mistakes.
- Introduced client utilities documentation for formatting, validation, transformation, and parsing functions.
- Set up a Cloudinary playground with Nuxt Studio, including configuration files, content structure, and media handling routes.
Keep uploaded Cloudinary media visible immediately in Studio and sync delete behavior without reload.
@vercel

vercel Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

@narr07 is attempting to deploy a commit to the Nuxt Team on Vercel.

A member of the Team first needs to authorize it.

@pkg-pr-new

pkg-pr-new Bot commented Aug 1, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/nuxt-studio@537

commit: 20de921

@narr07 narr07 changed the title fix: sync Cloudinary media list with Studio feat(media,ai): support Cloudinary media provider and Groq/Gemini AI integrations Aug 1, 2026
@narr07 narr07 changed the title feat(media,ai): support Cloudinary media provider and Groq/Gemini AI integrations feat: add Cloudinary media provider and Groq & Gemini AI providers Aug 1, 2026
@hendrikheil

Copy link
Copy Markdown
Collaborator

Hi @narr07, this PR seems to combine a few different things and has a pretty messy history. Could you please split it up by feature and drop the already merged commits for an easier to read history? Thanks!

@narr07

narr07 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Hi @narr07, PR ini tampaknya menggabungkan beberapa hal berbeda dan memiliki sejarah yang cukup berantakan. Bisakah Anda membaginya berdasarkan fitur dan menghapus commit yang sudah digabungkan agar riwayat lebih mudah dibaca? Terima kasih!

Hi @hendrikheil,

I have split the changes into two clean, separate pull requests as suggested:

Both PRs now have clean histories with exactly one commit each based on the latest upstream main. I am closing this PR now. Thanks again for your guidance!

@narr07 narr07 closed this Aug 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.