{"id":37878,"date":"2025-07-08T07:00:00","date_gmt":"2025-07-08T14:00:00","guid":{"rendered":"https:\/\/cloudinary.com\/blog\/?p=37878"},"modified":"2025-07-08T09:47:07","modified_gmt":"2025-07-08T16:47:07","slug":"contact-form-headless-wordpress-next-js-ninja-forms-part-2","status":"publish","type":"post","link":"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-2","title":{"rendered":"Build a Contact Form in Headless WordPress Using Next.js and Ninja Forms (Part 2)"},"content":{"rendered":"\n<p>In <a href=\"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-1\">Part 1<\/a>, we set up a headless contact form by installing Ninja Forms with WPGraphQL, creating a secured Next.js API route (<code>\/api\/contact\/route.ts<\/code>) for users to submit their <strong>name<\/strong>, <strong>email<\/strong>, and <strong>message<\/strong> via GraphQL, and built a server action (<code>actions.ts<\/code>) to bridge our client-side form. We also enhanced the form component (<code>contact-form.tsx<\/code>) so that when a user selects an image, it uploads to Cloudinary and passes the resulting <code>secure_url<\/code> into our submission payload.&nbsp;<\/p>\n\n\n\n<p>In this second installment, we\u2019ll dive into the details of that Cloudinary integration: configuring environment variables, implementing the <code>lib\/cloudinary.ts<\/code> helper and <code>\/api\/upload<\/code> endpoint, and using the <code>cloudinary<\/code> and <code>next-cloudinary<\/code> npm packages to streamline server-side uploads and client-side image delivery.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Environment Variables&nbsp;<\/h2>\n\n\n\n<p>In order for our API\u2019s to consume what we need from each environment, the next step is to add our environment variables. In the <code>.env.local<\/code> file at your project\u2019s root, your environment variables should be as follows:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">NEXT_PUBLIC_GRAPHQL_ENDPOINT=<span class=\"hljs-string\">\"https:\/\/your-wpsite.com\/graphql\"<\/span>\nWP_AUTH_TOKEN=<span class=\"hljs-string\">\"your-auth-token\"<\/span>\nNEXT_PUBLIC_SITE_URL=<span class=\"hljs-string\">\"http:\/\/localhost:3000\"<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<ul class=\"wp-block-list\">\n<li><strong><code>NEXT_PUBLIC_GRAPHQL_ENDPOINT<\/code>.<\/strong> Your WPGraphQL endpoint URL.<\/li>\n\n\n\n<li><strong><code>WP_AUTH_TOKEN.<\/code><\/strong> A randomly generated token (you can use <code>openssl rand -base64 32<\/code>).<\/li>\n\n\n\n<li><strong><code>NEXT_PUBLIC_SITE_URL<\/code>.<\/strong> For local testing (e.g., <code>http:\/\/localhost:3000<\/code>).<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Uploading to Cloudinary: <code>lib\/cloudinary.ts<\/code> and <code>\/api\/upload<\/code><\/h2>\n\n\n\n<p>To make Cloudinary uploads work on the server side, we added a lightweight helper in <code>lib\/cloudinary.ts<\/code>:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-string\">``<\/span><span class=\"hljs-string\">`\nimport { v2 as cloudinary } from \"cloudinary\";\n\ncloudinary.config({\n\n\u00a0\u00a0cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,\n\n\u00a0\u00a0api_key: process.env.CLOUDINARY_API_KEY,\n\n\u00a0\u00a0api_secret: process.env.CLOUDINARY_API_SECRET,\n\n});\n\nexport { cloudinary };\n\n`<\/span><span class=\"hljs-string\">``<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This file reads your Cloudinary credentials from environment variables so that no sensitive data is checked into source control. Next, we need a Next.js API route that accepts a <code>multipart\/form-data<\/code> POST containing the user\u2019s chosen image and sends it to Cloudinary. Create a new file at <code>app\/api\/upload\/<a href=\"http:\/\/route.ts\">route.ts<\/a><\/code>:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">import<\/span> { NextRequest, NextResponse } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"next\/server\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { cloudinary } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"..\/..\/..\/lib\/cloudinary\"<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">async<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">POST<\/span>(<span class=\"hljs-params\">request: NextRequest<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> formData = <span class=\"hljs-keyword\">await<\/span> request.formData();\n  <span class=\"hljs-keyword\">const<\/span> file = formData.get(<span class=\"hljs-string\">\"file\"<\/span>) <span class=\"hljs-keyword\">as<\/span> Blob | <span class=\"hljs-literal\">null<\/span>;\n  <span class=\"hljs-keyword\">if<\/span> (!file) {\n    <span class=\"hljs-keyword\">return<\/span> NextResponse.json({ <span class=\"hljs-attr\">error<\/span>: <span class=\"hljs-string\">\"No file provided\"<\/span> }, { <span class=\"hljs-attr\">status<\/span>: <span class=\"hljs-number\">400<\/span> });\n  }\n\n  <span class=\"hljs-keyword\">try<\/span> {\n    <span class=\"hljs-comment\">\/\/ Convert Blob to a Buffer and upload to Cloudinary:<\/span>\n    <span class=\"hljs-keyword\">const<\/span> arrayBuffer = <span class=\"hljs-keyword\">await<\/span> file.arrayBuffer();\n    <span class=\"hljs-keyword\">const<\/span> buffer = Buffer.from(arrayBuffer);\n    <span class=\"hljs-keyword\">const<\/span> result = <span class=\"hljs-keyword\">await<\/span> cloudinary.uploader.upload_stream(\n      {\n        <span class=\"hljs-attr\">resource_type<\/span>: <span class=\"hljs-string\">\"image\"<\/span>,\n        <span class=\"hljs-attr\">folder<\/span>: <span class=\"hljs-string\">\"ninja-forms-uploads\"<\/span>,\n        <span class=\"hljs-attr\">use_filename<\/span>: <span class=\"hljs-literal\">true<\/span>,\n        <span class=\"hljs-attr\">unique_filename<\/span>: <span class=\"hljs-literal\">false<\/span>,\n      },\n      (error, result) =&gt; {\n        <span class=\"hljs-keyword\">if<\/span> (error || !result) {\n          <span class=\"hljs-keyword\">throw<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Error<\/span>(error?.message || <span class=\"hljs-string\">\"Cloudinary upload failed\"<\/span>);\n        }\n        <span class=\"hljs-keyword\">return<\/span> result;\n      }\n    );\n\n    <span class=\"hljs-comment\">\/\/ Since upload_stream uses a callback, we need to wrap in a Promise:<\/span>\n    <span class=\"hljs-keyword\">const<\/span> uploadPromise: <span class=\"hljs-built_in\">Promise<\/span>&lt;any&gt; = <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Promise<\/span>(<span class=\"hljs-function\">(<span class=\"hljs-params\">resolve, reject<\/span>) =&gt;<\/span> {\n      <span class=\"hljs-keyword\">const<\/span> stream = cloudinary.uploader.upload_stream(\n        {\n          <span class=\"hljs-attr\">resource_type<\/span>: <span class=\"hljs-string\">\"image\"<\/span>,\n          <span class=\"hljs-attr\">folder<\/span>: <span class=\"hljs-string\">\"ninja-forms-uploads\"<\/span>,\n          <span class=\"hljs-attr\">use_filename<\/span>: <span class=\"hljs-literal\">true<\/span>,\n          <span class=\"hljs-attr\">unique_filename<\/span>: <span class=\"hljs-literal\">false<\/span>,\n        },\n        (error, result) =&gt; {\n          <span class=\"hljs-keyword\">if<\/span> (error || !result) {\n            reject(error?.message || <span class=\"hljs-string\">\"Upload failed\"<\/span>);\n          } <span class=\"hljs-keyword\">else<\/span> {\n            resolve(result);\n          }\n        }\n      );\n      stream.end(buffer);\n    });\n\n    <span class=\"hljs-keyword\">const<\/span> uploadResult = <span class=\"hljs-keyword\">await<\/span> uploadPromise;\n    <span class=\"hljs-keyword\">return<\/span> NextResponse.json(uploadResult, { <span class=\"hljs-attr\">status<\/span>: <span class=\"hljs-number\">200<\/span> });\n  } <span class=\"hljs-keyword\">catch<\/span> (err) {\n    <span class=\"hljs-keyword\">return<\/span> NextResponse.json(\n      { <span class=\"hljs-attr\">error<\/span>: (err <span class=\"hljs-keyword\">as<\/span> <span class=\"hljs-built_in\">Error<\/span>).message || <span class=\"hljs-string\">\"Upload error\"<\/span> },\n      { <span class=\"hljs-attr\">status<\/span>: <span class=\"hljs-number\">500<\/span> }\n    );\n  }\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This <code>POST<\/code> handler:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Reads the uploaded file blob from <code>request.formData()<\/code>.<br><\/li>\n\n\n\n<li>Converts it to a buffer and passes it to Cloudinary\u2019s <code>upload_stream<\/code> API.<br><\/li>\n\n\n\n<li>Returns JSON containing <code>secure_url<\/code>, <code>public_id<\/code>, and other metadata.<br><\/li>\n\n\n\n<li>On error, it returns a 500 response with an error message.<br><\/li>\n<\/ol>\n\n\n\n<p>With that in place, the client-side <code>handleImageUpload<\/code> can call <code>fetch(\"\/api\/upload\"<\/code>, <code>{ method: \"POST\", body: formData })<\/code> and receive a JSON payload like:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php shcb-wrap-lines\">```\n{\n  <span class=\"hljs-string\">\"public_id\"<\/span>: <span class=\"hljs-string\">\"ninja-forms-uploads\/abc-def\"<\/span>,\n  <span class=\"hljs-string\">\"version\"<\/span>: <span class=\"hljs-number\">1620000000<\/span>,\n  <span class=\"hljs-string\">\"secure_url\"<\/span>: <span class=\"hljs-string\">\"https:\/\/res.cloudinary.com\/your-cloud-name\/image\/upload\/v1620000000\/abc-def.jpg\"<\/span>,\n  <span class=\"hljs-comment\">\/\/ \u2026other metadata\u2026<\/span>\n}\n\n```<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h2 class=\"wp-block-heading\">Cloudinary NPM and <a href=\"http:\/\/next.js\">Next.js<\/a> Cloudinary Package<\/h2>\n\n\n\n<p>To integrate Cloudinary into our Next.js and WPGraphQL for Ninja Forms project, we rely on two npm packages: <a href=\"https:\/\/www.npmjs.com\/package\/cloudinary\">cloudinary<\/a> and<a href=\"https:\/\/www.npmjs.com\/package\/next-cloudinary\"> next-cloudinary<\/a>. The Cloudinary package is the official Node.js SDK for Cloudinary, enabling secure configuration, uploads, and asset management. Install it with:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-string\">`npm install cloudinary`<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The Node SDK supports Node@6+ (1.x) and Node@9+ (2.x) and includes functions for transformations, optimization, and media management. In our server-side API route (<code>\/api\/upload<\/code>), we import <code>cloudinary.v2<\/code> and configure it with environment variables:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-string\">``<\/span><span class=\"hljs-string\">`\nimport { v2 as cloudinary } from \"cloudinary\";\n\ncloudinary.config({\n  cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,\n  api_key: process.env.CLOUDINARY_API_KEY,\n  api_secret: process.env.CLOUDINARY_API_SECRET,\n});\n\n`<\/span><span class=\"hljs-string\">``<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>By reading credentials from <code>process.env<\/code>, we avoid committing sensitive data. Once configured, we use <code>cloudinary.uploader.upload_stream<\/code> in <code>\/api\/upload<\/code> to convert the uploaded file (as a Buffer) and send it to Cloudinary. The SDK returns metadata such as <code>secure_url<\/code> and <code>public_id<\/code>, which we forward to our Next.js form component. The Node SDK also supports chunked uploads, upload presets, and callbacks for error handling, making it ideal for headless environments.<\/p>\n\n\n\n<p>In addition, we install <code>next-cloudinary<\/code>:<\/p>\n\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs shcb-wrap-lines\">npm install next-cloudinary<\/code><\/span><\/pre>\n\n\n<p>Next Cloudinary is a community-maintained library supported by Cloudinary\u2019s Developer Experience team. It provides React components for Next.js that automatically optimize images, generate responsive <code>srcsets<\/code>, apply transformations, and create Open Graph cards. After uploading an image, we store the returned <code>secure_url<\/code> in React state (<code>imageUrl<\/code>) and pass it into <code>CldImage<\/code>:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">import<\/span> { CldImage } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"next-cloudinary\"<\/span>;\n\n<span class=\"hljs-keyword\">return<\/span> (\n  <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">CldImage<\/span> <span class=\"hljs-attr\">width<\/span>=<span class=\"hljs-string\">\"600\"<\/span> <span class=\"hljs-attr\">height<\/span>=<span class=\"hljs-string\">\"400\"<\/span> <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">{imageUrl}<\/span> <span class=\"hljs-attr\">alt<\/span>=<span class=\"hljs-string\">\"Uploaded image preview\"<\/span> \/&gt;<\/span><\/span>\n);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p><code>CldImage<\/code> uses the <code>NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME<\/code> to request an optimized version. It automatically selects modern formats (WebP, AVIF) when supported and serves images via Cloudinary\u2019s CDN. For generating Open Graph cards, we import <code>CldOgImage<\/code>:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml shcb-wrap-lines\">```\nimport { CldOgImage } from \"next-cloudinary\";\n\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">CldOgImage<\/span> <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">{imageUrl}<\/span> <span class=\"hljs-attr\">text<\/span>=<span class=\"hljs-string\">\"Submission Preview\"<\/span> \/&gt;<\/span>\n\n```<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p><code>CldOgImage<\/code> applies a 2:1 aspect ratio and overlays text dynamically, producing social media-ready images.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Why Both Packages Are Essential<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">1. Server-Side Uploads With Cloudinary<\/h3>\n\n\n\n<p>The <code>cloudinary<\/code> Node SDK allows our <code>\/api\/upload<\/code> route to accept <code>multipart\/form-data<\/code>, convert it into a Buffer, and upload the file to Cloudinary securely. Credentials (cloud name, API key, secret) are loaded from environment variables. Using <code>upload_stream<\/code> avoids temporary disk storage. The Node SDK supports legacy Node versions and includes TypeScript definitions, so our server code enjoys type safety.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">2. Client-Side Rendering With <code>next-cloudinary<\/code><\/h3>\n\n\n\n<p>After the file is uploaded, we receive a <code>secure_url<\/code> back. Instead of rendering a plain <code>&lt;img<\/code>&gt; tag, we use <code>CldImage<\/code> from <code>next-cloudinary<\/code>, which leverages automatic transformations and optimizations at request time. For instance, <code>CldImage<\/code> requests WebP where supported, generates responsive <code>srcset<\/code> attributes, and lazy-loads. Developers can pass additional props (quality, placeholder, transformations) without writing custom code.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">3. Environment Configuration<\/h3>\n\n\n\n<p>To set up both packages, add these variables to <code>.env.local<\/code>:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=<span class=\"hljs-string\">\"your-cloud-name\"<\/span>\n\nCLOUDINARY_API_KEY=<span class=\"hljs-string\">\"your-api-key\"<\/span>\n\nCLOUDINARY_API_SECRET=<span class=\"hljs-string\">\"your-api-secret\"<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n<div class='c-callout  c-callout--inline-title c-callout--note'><strong class='c-callout__title'>Note:<\/strong> <p>If you need to know how to obtain your Cloudinary account credentials, please refer to the <a href=\"https:\/\/cloudinary.com\/documentation\">documentation here<\/a>.<\/p>\n<\/div>\n\n\n<p><code>NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME<\/code> is safe on the client. The API key and secret remain private on the server.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">4. Streamlined Developer Experience<\/h3>\n\n\n\n<p>Installation is straightforward:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-string\">`npm install cloudinary next-cloudinary`<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>In <code>lib\/cloudinary.ts<\/code>, configure <code>cloudinary<\/code>. In components, import <code>CldImage<\/code> and <code>CldOgImage<\/code>. Both packages include TypeScript types, ensuring props and methods are validated at compile time.&nbsp;<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">5. Future-Proofing and Scalability<\/h3>\n\n\n\n<p>As headless applications evolve, advanced transformations like AI cropping or background removal may be required. With both packages installed, we can adopt new Cloudinary features immediately. For example, applying background removal during upload uses an extra parameter in <strong><code>cloudinary.uploader.upload.<\/code> <\/strong>On the client, passing transformation props to <strong><code>CldImage<\/code> <\/strong>can overlay text or apply filters. Both SDKs support streaming and chunked uploads, so our project can handle large files without hitting default WordPress limits.<\/p>\n\n\n\n<p>By installing and configuring both the <code>cloudinary<\/code> Node SDK and the <code>next-cloudinary<\/code> package, our Next.js application achieves a modern image upload and delivery workflow. The <code>cloudinary<\/code> package powers server-side uploads and asset management, while <code>next-cloudinary<\/code> ensures images are delivered in optimized formats on the client. This combination enhances performance, scalability, and developer productivity, all while securely managing environment credentials and integrating directly with WPGraphQL for Ninja Forms.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Test the Form in the Browser<\/h2>\n\n\n\n<p>With everything set up, navigate to your contact form route in the browser. You should see the contact form with <strong>Name<\/strong>, <strong>Email<\/strong>, <strong>Message<\/strong>, and an <strong>Image (Optional)<\/strong> file input. When you select a file, you\u2019ll see an <strong>Uploading image\u2026<\/strong> indicator and, once complete, a preview of the uploaded image via its Cloudinary <code>secure_url<\/code>. After filling out <strong>Name<\/strong>,<strong> Email<\/strong>, and <strong>Message<\/strong> (and optionally uploading an image), click <strong>Send Message<\/strong>.<\/p>\n\n\n\n<p>The form will send all data (plus <code>imageUrl<\/code>) to your Next.js API route, which in turn calls the WPGraphQL mutation. If successful, you\u2019ll get a confirmation message; otherwise, an error will display.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Locking Down the Edit Page on WP Admin<\/h2>\n\n\n\n<p>If you\u2019re using a static form component in your frontend, any changes made to the form structure in the WordPress admin can break the submission logic. To prevent this, restrict access to Ninja Forms editing screens by creating a custom plugin that overrides its default capabilities.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Example Plugin: Ninja Forms Admin Lockdown<\/h3>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml shcb-wrap-lines\">```\n<span class=\"php\"><span class=\"hljs-meta\">&lt;?php<\/span>\n<span class=\"hljs-comment\">\/**\n * Plugin Name: Ninja Forms Admin Lockdown\n * Description: Restricts access to all Ninja Forms admin screens so that only users with the 'edit_themes' capability (typically Administrators) can view or modify forms.\n * Version:     1.0.0\n * Author:      Your Name\n * License:     GPLv2 or later\n * Text Domain: ninja-forms-admin-lockdown\n *\/<\/span>\n\n<span class=\"hljs-comment\">\/\/ Exit if accessed directly.<\/span>\n<span class=\"hljs-keyword\">if<\/span> ( ! defined( <span class=\"hljs-string\">'ABSPATH'<\/span> ) ) {\n    <span class=\"hljs-keyword\">exit<\/span>;\n}\n\n<span class=\"hljs-comment\">\/**\n * Restrict access to \u201cAll Forms\u201d and the main Ninja Forms menu.\n *\n * By default, Ninja Forms uses 'edit_posts' to gate access.\n * Returning 'edit_themes' here ensures only users with that capability can see or edit.\n *\/<\/span>\n<span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">nf_allforms_capabilities<\/span><span class=\"hljs-params\">( $cap )<\/span> <\/span>{\n    <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-string\">'edit_themes'<\/span>;\n}\nadd_filter( <span class=\"hljs-string\">'ninja_forms_admin_parent_menu_capabilities'<\/span>, <span class=\"hljs-string\">'nf_allforms_capabilities'<\/span> );\nadd_filter( <span class=\"hljs-string\">'ninja_forms_admin_all_forms_capabilities'<\/span>,   <span class=\"hljs-string\">'nf_allforms_capabilities'<\/span> );\n\n<span class=\"hljs-comment\">\/**\n * Restrict access to \u201cAdd New Form\u201d submenu.\n *\/<\/span>\n<span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">nf_newforms_capabilities<\/span><span class=\"hljs-params\">( $cap )<\/span> <\/span>{\n    <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-string\">'edit_themes'<\/span>;\n}\nadd_filter( <span class=\"hljs-string\">'ninja_forms_admin_parent_menu_capabilities'<\/span>, <span class=\"hljs-string\">'nf_newforms_capabilities'<\/span> );\nadd_filter( <span class=\"hljs-string\">'ninja_forms_admin_all_forms_capabilities'<\/span>,   <span class=\"hljs-string\">'nf_newforms_capabilities'<\/span> );\nadd_filter( <span class=\"hljs-string\">'ninja_forms_admin_add_new_capabilities'<\/span>,     <span class=\"hljs-string\">'nf_newforms_capabilities'<\/span> );\n\n```<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>In order to do this, you need to hook into Ninja Forms capability filters and return a higher-level capability. Install and activate this plugin to restrict form editing to only site Administrators (those who have the edit_themes capability).<\/p>\n\n\n\n<p>Please refer to the WordPress plugin docs for proper <a href=\"https:\/\/developer.wordpress.org\/plugins\/\">plugin creation here<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p>Integrating a contact form on a headless WordPress site doesn\u2019t have to be complex. By combining Next.js, WPGraphQL, Ninja Forms, and Cloudinary for optional image uploads, you can build a secure, modern contact form that connects your frontend and backend seamlessly. <a href=\"https:\/\/cloudinary.com\/users\/register_free\">Sign up for a free Cloudinary account<\/a> today to get started.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>In Part 1, we set up a headless contact form by installing Ninja Forms with WPGraphQL, creating a secured Next.js API route (\/api\/contact\/route.ts) for users to submit their name, email, and message via GraphQL, and built a server action (actions.ts) to bridge our client-side form. We also enhanced the form component (contact-form.tsx) so that when [&hellip;]<\/p>\n","protected":false},"author":87,"featured_media":37880,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_cloudinary_featured_overwrite":false,"footnotes":""},"categories":[1],"tags":[134,370,212,328],"class_list":["post-37878","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized","tag-guest-post","tag-image","tag-next-js","tag-wordpress"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v25.6 (Yoast SEO v26.9) - https:\/\/yoast.com\/product\/yoast-seo-premium-wordpress\/ -->\n<title>WordPress: Contact Forms With Image Uploads - Next.js, Ninja Forms, Cloudinary (Part 2)<\/title>\n<meta name=\"description\" content=\"Dive into Part 2 of building a headless WordPress contact form, focusing on secure Cloudinary image uploads, environment variables, and client\/server-side integration with Next.js and Ninja Forms.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-2\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Build a Contact Form in Headless WordPress Using Next.js and Ninja Forms (Part 2)\" \/>\n<meta property=\"og:description\" content=\"Dive into Part 2 of building a headless WordPress contact form, focusing on secure Cloudinary image uploads, environment variables, and client\/server-side integration with Next.js and Ninja Forms.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-2\" \/>\n<meta property=\"og:site_name\" content=\"Cloudinary Blog\" \/>\n<meta property=\"article:published_time\" content=\"2025-07-08T14:00:00+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2025-07-08T16:47:07+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/v1751431765\/Blog_Build_a_Contact_Form_in_Headless_WordPress_Using_Next.js_and_Ninja_Forms_Part_2\/Blog_Build_a_Contact_Form_in_Headless_WordPress_Using_Next-js_and_Ninja_Forms_Part_2-jpg?_i=AA\" \/>\n\t<meta property=\"og:image:width\" content=\"2000\" \/>\n\t<meta property=\"og:image:height\" content=\"1100\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"author\" content=\"melindapham\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"NewsArticle\",\"@id\":\"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-2#article\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-2\"},\"author\":{\"name\":\"melindapham\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9\"},\"headline\":\"Build a Contact Form in Headless WordPress Using Next.js and Ninja Forms (Part 2)\",\"datePublished\":\"2025-07-08T14:00:00+00:00\",\"dateModified\":\"2025-07-08T16:47:07+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-2\"},\"wordCount\":1112,\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-2#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1751431765\/Blog_Build_a_Contact_Form_in_Headless_WordPress_Using_Next.js_and_Ninja_Forms_Part_2\/Blog_Build_a_Contact_Form_in_Headless_WordPress_Using_Next.js_and_Ninja_Forms_Part_2.jpg?_i=AA\",\"keywords\":[\"Guest Post\",\"Image\",\"Next.js\",\"WordPress\"],\"inLanguage\":\"en-US\",\"copyrightYear\":\"2025\",\"copyrightHolder\":{\"@id\":\"https:\/\/cloudinary.com\/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-2\",\"url\":\"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-2\",\"name\":\"WordPress: Contact Forms With Image Uploads - Next.js, Ninja Forms, Cloudinary (Part 2)\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-2#primaryimage\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-2#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1751431765\/Blog_Build_a_Contact_Form_in_Headless_WordPress_Using_Next.js_and_Ninja_Forms_Part_2\/Blog_Build_a_Contact_Form_in_Headless_WordPress_Using_Next.js_and_Ninja_Forms_Part_2.jpg?_i=AA\",\"datePublished\":\"2025-07-08T14:00:00+00:00\",\"dateModified\":\"2025-07-08T16:47:07+00:00\",\"description\":\"Dive into Part 2 of building a headless WordPress contact form, focusing on secure Cloudinary image uploads, environment variables, and client\/server-side integration with Next.js and Ninja Forms.\",\"breadcrumb\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-2#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-2\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-2#primaryimage\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1751431765\/Blog_Build_a_Contact_Form_in_Headless_WordPress_Using_Next.js_and_Ninja_Forms_Part_2\/Blog_Build_a_Contact_Form_in_Headless_WordPress_Using_Next.js_and_Ninja_Forms_Part_2.jpg?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1751431765\/Blog_Build_a_Contact_Form_in_Headless_WordPress_Using_Next.js_and_Ninja_Forms_Part_2\/Blog_Build_a_Contact_Form_in_Headless_WordPress_Using_Next.js_and_Ninja_Forms_Part_2.jpg?_i=AA\",\"width\":2000,\"height\":1100},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-2#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/cloudinary.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Build a Contact Form in Headless WordPress Using Next.js and Ninja Forms (Part 2)\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\",\"url\":\"https:\/\/cloudinary.com\/blog\/\",\"name\":\"Cloudinary Blog\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/cloudinary.com\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\",\"name\":\"Cloudinary Blog\",\"url\":\"https:\/\/cloudinary.com\/blog\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA\",\"width\":312,\"height\":60,\"caption\":\"Cloudinary Blog\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9\",\"name\":\"melindapham\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g\",\"caption\":\"melindapham\"}}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"WordPress: Contact Forms With Image Uploads - Next.js, Ninja Forms, Cloudinary (Part 2)","description":"Dive into Part 2 of building a headless WordPress contact form, focusing on secure Cloudinary image uploads, environment variables, and client\/server-side integration with Next.js and Ninja Forms.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-2","og_locale":"en_US","og_type":"article","og_title":"Build a Contact Form in Headless WordPress Using Next.js and Ninja Forms (Part 2)","og_description":"Dive into Part 2 of building a headless WordPress contact form, focusing on secure Cloudinary image uploads, environment variables, and client\/server-side integration with Next.js and Ninja Forms.","og_url":"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-2","og_site_name":"Cloudinary Blog","article_published_time":"2025-07-08T14:00:00+00:00","article_modified_time":"2025-07-08T16:47:07+00:00","og_image":[{"width":2000,"height":1100,"url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/v1751431765\/Blog_Build_a_Contact_Form_in_Headless_WordPress_Using_Next.js_and_Ninja_Forms_Part_2\/Blog_Build_a_Contact_Form_in_Headless_WordPress_Using_Next-js_and_Ninja_Forms_Part_2-jpg?_i=AA","type":"image\/jpeg"}],"author":"melindapham","twitter_card":"summary_large_image","schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"NewsArticle","@id":"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-2#article","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-2"},"author":{"name":"melindapham","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9"},"headline":"Build a Contact Form in Headless WordPress Using Next.js and Ninja Forms (Part 2)","datePublished":"2025-07-08T14:00:00+00:00","dateModified":"2025-07-08T16:47:07+00:00","mainEntityOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-2"},"wordCount":1112,"publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-2#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1751431765\/Blog_Build_a_Contact_Form_in_Headless_WordPress_Using_Next.js_and_Ninja_Forms_Part_2\/Blog_Build_a_Contact_Form_in_Headless_WordPress_Using_Next.js_and_Ninja_Forms_Part_2.jpg?_i=AA","keywords":["Guest Post","Image","Next.js","WordPress"],"inLanguage":"en-US","copyrightYear":"2025","copyrightHolder":{"@id":"https:\/\/cloudinary.com\/#organization"}},{"@type":"WebPage","@id":"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-2","url":"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-2","name":"WordPress: Contact Forms With Image Uploads - Next.js, Ninja Forms, Cloudinary (Part 2)","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-2#primaryimage"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-2#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1751431765\/Blog_Build_a_Contact_Form_in_Headless_WordPress_Using_Next.js_and_Ninja_Forms_Part_2\/Blog_Build_a_Contact_Form_in_Headless_WordPress_Using_Next.js_and_Ninja_Forms_Part_2.jpg?_i=AA","datePublished":"2025-07-08T14:00:00+00:00","dateModified":"2025-07-08T16:47:07+00:00","description":"Dive into Part 2 of building a headless WordPress contact form, focusing on secure Cloudinary image uploads, environment variables, and client\/server-side integration with Next.js and Ninja Forms.","breadcrumb":{"@id":"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-2#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-2"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-2#primaryimage","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1751431765\/Blog_Build_a_Contact_Form_in_Headless_WordPress_Using_Next.js_and_Ninja_Forms_Part_2\/Blog_Build_a_Contact_Form_in_Headless_WordPress_Using_Next.js_and_Ninja_Forms_Part_2.jpg?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1751431765\/Blog_Build_a_Contact_Form_in_Headless_WordPress_Using_Next.js_and_Ninja_Forms_Part_2\/Blog_Build_a_Contact_Form_in_Headless_WordPress_Using_Next.js_and_Ninja_Forms_Part_2.jpg?_i=AA","width":2000,"height":1100},{"@type":"BreadcrumbList","@id":"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-2#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/cloudinary.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Build a Contact Form in Headless WordPress Using Next.js and Ninja Forms (Part 2)"}]},{"@type":"WebSite","@id":"https:\/\/cloudinary.com\/blog\/#website","url":"https:\/\/cloudinary.com\/blog\/","name":"Cloudinary Blog","description":"","publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/cloudinary.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/cloudinary.com\/blog\/#organization","name":"Cloudinary Blog","url":"https:\/\/cloudinary.com\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA","width":312,"height":60,"caption":"Cloudinary Blog"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9","name":"melindapham","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g","caption":"melindapham"}}]}},"jetpack_featured_media_url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1751431765\/Blog_Build_a_Contact_Form_in_Headless_WordPress_Using_Next.js_and_Ninja_Forms_Part_2\/Blog_Build_a_Contact_Form_in_Headless_WordPress_Using_Next.js_and_Ninja_Forms_Part_2.jpg?_i=AA","_links":{"self":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/37878","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/users\/87"}],"replies":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/comments?post=37878"}],"version-history":[{"count":5,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/37878\/revisions"}],"predecessor-version":[{"id":37896,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/37878\/revisions\/37896"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media\/37880"}],"wp:attachment":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media?parent=37878"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/categories?post=37878"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/tags?post=37878"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}