{"id":37873,"date":"2025-07-07T07:00:00","date_gmt":"2025-07-07T14:00:00","guid":{"rendered":"https:\/\/cloudinary.com\/blog\/?p=37873"},"modified":"2025-07-02T14:08:58","modified_gmt":"2025-07-02T21:08:58","slug":"contact-form-headless-wordpress-next-js-ninja-forms-part-1","status":"publish","type":"post","link":"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-1","title":{"rendered":"Build a Contact Form With Image Uploads in Headless WordPress Using Next.js, Ninja Forms, and Cloudinary (Part 1)"},"content":{"rendered":"\n<p>Contact forms are a fundamental touchpoint between site visitors and site owners, enabling customer inquiries, lead generation, and essential feedback that drives engagement. In headless WordPress, this can be tricky since the frontend and backend are separated. This means that you have to figure out a way for the frontend to send that data to your WP backend as securely as possible.&nbsp;<\/p>\n\n\n\n<p>In this post, we\u2019ll discuss implementing a simple contact form in headless WordPress using WPGraphQL, Ninja Forms, and Next.js App Router \u2014 and even how to handle optional image uploads via Cloudinary.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Prerequisites<\/h2>\n\n\n\n<p>Before we begin, you should have a basic understanding of the following:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Headless WordPress concepts<\/li>\n<\/ul>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/github.com\/wp-graphql\/wp-graphql\">The WPGraphQL plugin<\/a><\/li>\n<\/ul>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/nextjs.org\/docs\/app:\">The Next.js App Router<\/a><\/li>\n<\/ul>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/cloudinary.com\/\">A Cloudinary Account<\/a><\/li>\n<\/ul>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/nodejs.org\/en\">Node.js<\/a><\/li>\n<\/ul>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/ninjaforms.com\/\">Ninja Forms WordPress Plugin<\/a><\/li>\n<\/ul>\n\n\n\n<p>This post isn\u2019t a step-by-step walkthrough, but if you\u2019d like to explore the codebase and follow along, you can clone <a href=\"https:\/\/github.com\/Fran-A-Dev\/ninja-forms-headlesswp\/tree\/cloudinary-upload\/app\">the example repository here<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Using Ninja Forms With WPGraphQL<\/h2>\n\n\n\n<p>Ninja Forms is a flexible and user-friendly form-building plugin for WordPress. It offers a robust set of features out of the box, and its core functionality is free and open source. To expose Ninja Forms data via GraphQL, we\u2019ll use the WPGraphQL for Ninja Forms extension. This plugin adds a GraphQL schema for Ninja Forms, allowing queries and mutations for form data. For this example, we\u2019ll stick with the free tier of Ninja Forms and use its default fields: <strong>Name<\/strong>,<strong> Email<\/strong>, and <strong>Message<\/strong>.<\/p>\n\n\n\n<p>After downloading the<a href=\"https:\/\/github.com\/toriphes\/wp-graphql-ninja-forms\"> WPGraphQL for Ninja Forms plugin<\/a>, let\u2019s test that it works by requesting and submitting data to its API:<\/p>\n\n\n\n<p><strong>Requesting form default form data:<\/strong><\/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\"><span class=\"hljs-string\">``<\/span><span class=\"hljs-string\">`\n{\n\n\u00a0\u00a0form(id: \"1\", idType: DATABASE_ID) {\n\n\u00a0\u00a0\u00a0\u00a0title\n\n\u00a0\u00a0\u00a0\u00a0fields {\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0nodes {\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0fieldId\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0label\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0type\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}\n\n\u00a0\u00a0\u00a0\u00a0}\n\n\u00a0\u00a0}\n\n}\n\n`<\/span><span class=\"hljs-string\">``<\/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<p><strong>Submitting data to the form via mutation:<\/strong><\/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\">`\nmutation SubmitForm($input: SubmitFormInput!) {\n\n\u00a0\u00a0submitForm(input: $input) {\n\n\u00a0\u00a0\u00a0\u00a0success\n\n\u00a0\u00a0\u00a0\u00a0message\n\n\u00a0\u00a0\u00a0\u00a0errors {\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0fieldId\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0message\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0slug\n\n\u00a0\u00a0\u00a0\u00a0}\n\n\u00a0\u00a0}\n\n}\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<h2 class=\"wp-block-heading\">Form Submission in Next.js App Router<\/h2>\n\n\n\n<p>The next step I took was to create the API route responsible for handling the form submission safely. In App Router, route handlers allow you to create custom request handlers for a given route using the Web Request and Response APIs. We will take advantage of that convention in the <code>app\/api\/contact\/route.ts<\/code> file.<\/p>\n\n\n\n<p>This file securely bridges a Next.js frontend with a WordPress backend via GraphQL. In the <code>POST<\/code> handler, the code first reads JSON from the incoming request and expects three properties: <code>name<\/code>, <code>email<\/code>, and <code>message<\/code>. If any of these fields is missing, it immediately returns a 400 response with an error. Once validation passes, the handler constructs a WPGraphQL mutation:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php shcb-wrap-lines\"><span class=\"hljs-keyword\">const<\/span> mutation = `\n\n\u00a0\u00a0mutation SubmitForm($input: SubmitFormInput!) {\n\n\u00a0\u00a0\u00a0\u00a0submitForm(input: $input) {\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0success\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0message\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0errors {\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0fieldId\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0message\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0slug\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}\n\n\u00a0\u00a0\u00a0\u00a0}\n\n\u00a0\u00a0}\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\">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<p>Next, the code issues a <code>fetch<\/code> call to the WordPress GraphQL endpoint, which is specified via the environment variable <code>NEXT_PUBLIC_GRAPHQL_ENDPOINT<\/code>. In that call, the headers include <code>\"Content-Type\": \"application\/json\"<\/code> and an Authorization header built from another environment variable:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-string\">`Authorization: Bearer <span class=\"hljs-subst\">${process.env.WP_AUTH_TOKEN}<\/span>`<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><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>Because <code>WP_AUTH_TOKEN<\/code> is pulled from <code>process.env<\/code> , it ensures that only your Next.js app \u2014 holding this secret \u2014 can successfully authorize and submit data to the WordPress endpoint.<\/p>\n\n\n\n<p>The request body contains the <code>query<\/code> and a variables object whose <code>input<\/code> includes <code>formId: 1<\/code>, an array of field objects mapping each field\u2019s <code>id<\/code> to &#8220;value<code>: data.name<\/code>, <code>value: data.email<\/code>, and <code>value: data.message<\/code>, plus a <code>clientMutationId<\/code> set to <code>\"contact-form-submission\"<\/code>.<\/p>\n\n\n\n<p>When the response arrives, the code calls <code>await wpResponse.json()<\/code>, then checks both <code>wpResponse.ok<\/code> and <code>response.errors<\/code>. If either indicates a failure, it logs the first GraphQL error to the server console and throws an exception. In the success case, when <code>submitForm<\/code> returns something like <code>{ success: true, message: \"\u2026\", errors: [] }<\/code>, the route returns a 200 JSON response:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json shcb-wrap-lines\">{ <span class=\"hljs-attr\">\"success\"<\/span>: <span class=\"hljs-literal\">true<\/span>, <span class=\"hljs-attr\">\"message\"<\/span>: <span class=\"hljs-string\">\"Form submitted successfully\"<\/span> }<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JSON \/ JSON with Comments<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">json<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Any thrown exception or unexpected condition is caught by the catch block, which logs the error and returns a 500 response with <code>{ error: \"Form submission failed\" }<\/code>. Finally, the file exports a <code>GET<\/code> handler that always returns a 405 \u201cMethod not allowed\u201d response, ensuring only <code>POST<\/code> requests are processed.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Interfacing With the API Route via Server Actions<\/h2>\n\n\n\n<p>Now that we have discussed the API route in <code>route.ts<\/code>, let\u2019s go over the server action responsible for interfacing with that API endpoint. The <code>actions.ts<\/code> file implements a server-side action that functions as the middleware between the client-side form submission and the API endpoint.<\/p>\n\n\n\n<p>When a user triggers the form submission, the <code>submitForm<\/code> server action is invoked, which processes the form data and initiates an HTTP <code>POST<\/code> request to the <code>\/api\/contact<\/code> endpoint. This endpoint, implemented in <code>route.ts<\/code>, then executes the WordPress WPGraphQL mutation through the Ninja Forms API.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php shcb-wrap-lines\">```\n<span class=\"hljs-string\">\"use server\"<\/span>;\n\nexport async <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">submitForm<\/span><span class=\"hljs-params\">(formData: FormData)<\/span> <\/span>{\n\n\u00a0\u00a0<span class=\"hljs-keyword\">const<\/span> name = formData.get(<span class=\"hljs-string\">\"name\"<\/span>) <span class=\"hljs-keyword\">as<\/span> string;\n\n\u00a0\u00a0<span class=\"hljs-keyword\">const<\/span> email = formData.get(<span class=\"hljs-string\">\"email\"<\/span>) <span class=\"hljs-keyword\">as<\/span> string;\n\n\u00a0\u00a0<span class=\"hljs-keyword\">const<\/span> message = formData.get(<span class=\"hljs-string\">\"message\"<\/span>) <span class=\"hljs-keyword\">as<\/span> string;\n\n\u00a0\u00a0<span class=\"hljs-keyword\">const<\/span> imageUrl = formData.get(<span class=\"hljs-string\">\"imageUrl\"<\/span>) <span class=\"hljs-keyword\">as<\/span> string | <span class=\"hljs-keyword\">null<\/span>;\n\n\u00a0\u00a0<span class=\"hljs-keyword\">if<\/span> (!name || !email || !message) {\n\n\u00a0\u00a0\u00a0\u00a0<span class=\"hljs-keyword\">return<\/span> { error: <span class=\"hljs-string\">\"Please fill in all fields\"<\/span> };\n\n\u00a0\u00a0}\n\n\u00a0\u00a0<span class=\"hljs-keyword\">const<\/span> baseUrl = process.env.NEXT_PUBLIC_SITE_URL || <span class=\"hljs-string\">\"http:\/\/localhost:3000\"<\/span>;\n\n\u00a0\u00a0<span class=\"hljs-keyword\">try<\/span> {\n\n\u00a0\u00a0\u00a0\u00a0<span class=\"hljs-keyword\">const<\/span> response = await fetch(`${baseUrl}\/api\/contact`, {\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0method: <span class=\"hljs-string\">\"POST\"<\/span>,\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0headers: {\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0<span class=\"hljs-string\">\"Content-Type\"<\/span>: <span class=\"hljs-string\">\"application\/json\"<\/span>,\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0},\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0body: JSON.stringify({ name, email, message, imageUrl }),\n\n\u00a0\u00a0\u00a0\u00a0});\n\n\u00a0\u00a0\u00a0\u00a0<span class=\"hljs-keyword\">const<\/span> result = await response.json();\n\n\u00a0\u00a0\u00a0\u00a0<span class=\"hljs-keyword\">if<\/span> (!response.ok) {\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0<span class=\"hljs-keyword\">return<\/span> { error: result.error || <span class=\"hljs-string\">\"Submission failed\"<\/span> };\n\n\u00a0\u00a0\u00a0\u00a0}\n\n\u00a0\u00a0\u00a0\u00a0<span class=\"hljs-keyword\">return<\/span> {\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0success: <span class=\"hljs-string\">\"Thank you for your message! We will get back to you soon.\"<\/span>,\n\n\u00a0\u00a0\u00a0\u00a0};\n\n\u00a0\u00a0} <span class=\"hljs-keyword\">catch<\/span> (error) {\n\n\u00a0\u00a0\u00a0\u00a0<span class=\"hljs-keyword\">return<\/span> {\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0error: error <span class=\"hljs-keyword\">instanceof<\/span> Error ? error.message : <span class=\"hljs-string\">\"Failed to submit form\"<\/span>,\n\n\u00a0\u00a0\u00a0\u00a0};\n\n\u00a0\u00a0}\n\n}\n\n```<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><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<p>Notice that we also read an optional <code>imageUrl<\/code> field from <code>formData<\/code>. If the user has uploaded an image to Cloudinary, we will pass that URL along with <code>name<\/code>, <code>email<\/code>, and <code>message<\/code>. Otherwise, <code>imageUrl<\/code> remains <code>null<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Rendering the Form on the Client<\/h2>\n\n\n\n<p>The <code>contact-form.tsx<\/code> file implements a client-side form component that leverages Next.js\u2019s Form API and server actions for handling contact form submissions. The component is marked with the <code>\"use client\"<\/code> directive, indicating that it runs on the client side and utilizes React\u2019s <code>useFormStatus<\/code> hook to manage form submission states. In addition, we\u2019ve added support for optional image uploads to Cloudinary.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php shcb-wrap-lines\">```\n<span class=\"hljs-string\">\"use client\"<\/span>;\n\nimport Form from <span class=\"hljs-string\">\"next\/form\"<\/span>;\n\nimport { useFormStatus } from <span class=\"hljs-string\">\"react-dom\"<\/span>;\n\nimport { submitForm } from <span class=\"hljs-string\">\".\/actions\"<\/span>;\n\nimport { useState } from <span class=\"hljs-string\">\"react\"<\/span>;\n\n<span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">SubmitButton<\/span><span class=\"hljs-params\">()<\/span> <\/span>{\n\n\u00a0\u00a0<span class=\"hljs-keyword\">const<\/span> { pending } = useFormStatus();\n\n\u00a0\u00a0<span class=\"hljs-keyword\">return<\/span> (\n\n\u00a0\u00a0\u00a0\u00a0&lt;button\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0type=<span class=\"hljs-string\">\"submit\"<\/span>\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0disabled={pending}\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0className={`w-full py<span class=\"hljs-number\">-3<\/span> px<span class=\"hljs-number\">-6<\/span> rounded bg-yellow<span class=\"hljs-number\">-500<\/span> text-black font-semibold hover:bg-yellow<span class=\"hljs-number\">-400<\/span> transition-colors ${\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0pending ? <span class=\"hljs-string\">\"opacity-50 cursor-not-allowed\"<\/span> : <span class=\"hljs-string\">\"\"<\/span>\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}`}\n\n\u00a0\u00a0\u00a0\u00a0&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0{pending ? <span class=\"hljs-string\">\"Sending...\"<\/span> : <span class=\"hljs-string\">\"Send Message\"<\/span>}\n\n\u00a0\u00a0\u00a0\u00a0&lt;\/button&gt;\n\n\u00a0\u00a0);\n\n}\n\nexport <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">ContactForm<\/span><span class=\"hljs-params\">()<\/span> <\/span>{\n\n\u00a0\u00a0<span class=\"hljs-keyword\">const<\/span> &#91;message, setMessage] = useState&lt;{\n\n\u00a0\u00a0\u00a0\u00a0type: <span class=\"hljs-string\">\"success\"<\/span> | <span class=\"hljs-string\">\"error\"<\/span>;\n\n\u00a0\u00a0\u00a0\u00a0text: string;\n\n\u00a0\u00a0} | <span class=\"hljs-keyword\">null<\/span>&gt;(<span class=\"hljs-keyword\">null<\/span>);\n\n\u00a0\u00a0<span class=\"hljs-keyword\">const<\/span> &#91;imageUrl, setImageUrl] = useState&lt;string | <span class=\"hljs-keyword\">null<\/span>&gt;(<span class=\"hljs-keyword\">null<\/span>);\n\n\u00a0\u00a0<span class=\"hljs-keyword\">const<\/span> &#91;isUploading, setIsUploading] = useState(<span class=\"hljs-keyword\">false<\/span>);\n\n\u00a0\u00a0async <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">handleImageUpload<\/span><span class=\"hljs-params\">(event: React.ChangeEvent&lt;HTMLInputElement&gt;)<\/span> <\/span>{\n\n\u00a0\u00a0\u00a0\u00a0<span class=\"hljs-keyword\">const<\/span> file = event.target.files?.&#91;<span class=\"hljs-number\">0<\/span>];\n\n\u00a0\u00a0\u00a0\u00a0<span class=\"hljs-keyword\">if<\/span> (!file) <span class=\"hljs-keyword\">return<\/span>;\n\n\u00a0\u00a0\u00a0\u00a0setIsUploading(<span class=\"hljs-keyword\">true<\/span>);\n\n\u00a0\u00a0\u00a0\u00a0<span class=\"hljs-keyword\">const<\/span> formData = <span class=\"hljs-keyword\">new<\/span> FormData();\n\n\u00a0\u00a0\u00a0\u00a0formData.append(<span class=\"hljs-string\">\"file\"<\/span>, file);\n\n\u00a0\u00a0\u00a0\u00a0<span class=\"hljs-keyword\">try<\/span> {\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0<span class=\"hljs-keyword\">const<\/span> response = await fetch(<span class=\"hljs-string\">\"\/api\/upload\"<\/span>, {\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0method: <span class=\"hljs-string\">\"POST\"<\/span>,\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0body: formData,\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0});\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0<span class=\"hljs-keyword\">const<\/span> result = await response.json();\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0<span class=\"hljs-keyword\">if<\/span> (!response.ok) {\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0<span class=\"hljs-keyword\">throw<\/span> <span class=\"hljs-keyword\">new<\/span> Error(result.error || <span class=\"hljs-string\">\"Upload failed\"<\/span>);\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0setImageUrl(result.secure_url);\n\n\u00a0\u00a0\u00a0\u00a0} <span class=\"hljs-keyword\">catch<\/span> (error) {\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0setMessage({\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0type: <span class=\"hljs-string\">\"error\"<\/span>,\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0text: error <span class=\"hljs-keyword\">instanceof<\/span> Error ? error.message : <span class=\"hljs-string\">\"Failed to upload image\"<\/span>,\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0});\n\n\u00a0\u00a0\u00a0\u00a0} <span class=\"hljs-keyword\">finally<\/span> {\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0setIsUploading(<span class=\"hljs-keyword\">false<\/span>);\n\n\u00a0\u00a0\u00a0\u00a0}\n\n\u00a0\u00a0}\n\n\u00a0\u00a0async <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">handleSubmit<\/span><span class=\"hljs-params\">(formData: FormData)<\/span> <\/span>{\n\n\u00a0\u00a0\u00a0\u00a0<span class=\"hljs-keyword\">if<\/span> (imageUrl) {\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0formData.append(<span class=\"hljs-string\">\"imageUrl\"<\/span>, imageUrl);\n\n\u00a0\u00a0\u00a0\u00a0}\n\n\u00a0\u00a0\u00a0\u00a0<span class=\"hljs-keyword\">const<\/span> result = await submitForm(formData);\n\n\u00a0\u00a0\u00a0\u00a0<span class=\"hljs-keyword\">if<\/span> (<span class=\"hljs-string\">\"error\"<\/span> in result) {\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0setMessage({ type: <span class=\"hljs-string\">\"error\"<\/span>, text: result.error });\n\n\u00a0\u00a0\u00a0\u00a0} <span class=\"hljs-keyword\">else<\/span> {\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0setMessage({ type: <span class=\"hljs-string\">\"success\"<\/span>, text: result.success });\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0setImageUrl(<span class=\"hljs-keyword\">null<\/span>);\n\n\u00a0\u00a0\u00a0\u00a0}\n\n\u00a0\u00a0}\n\n\u00a0\u00a0<span class=\"hljs-keyword\">return<\/span> (\n\n\u00a0\u00a0\u00a0\u00a0&lt;div className=<span class=\"hljs-string\">\"space-y-6\"<\/span>&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0{message &amp;&amp; (\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;div\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0className={`p<span class=\"hljs-number\">-4<\/span> rounded ${\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0message.type === <span class=\"hljs-string\">\"success\"<\/span>\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0? <span class=\"hljs-string\">\"bg-green-500\/20 text-green-300 border border-green-500\/20\"<\/span>\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0: <span class=\"hljs-string\">\"bg-red-500\/20 text-red-300 border border-red-500\/20\"<\/span>\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0}`}\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0{message.text}\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;\/div&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0)}\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;Form action={handleSubmit} className=<span class=\"hljs-string\">\"space-y-6\"<\/span>&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;div&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;label htmlFor=<span class=\"hljs-string\">\"name\"<\/span> className=<span class=\"hljs-string\">\"block text-yellow-300 mb-2\"<\/span>&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Name\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;\/label&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;input\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0type=<span class=\"hljs-string\">\"text\"<\/span>\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0id=<span class=\"hljs-string\">\"name\"<\/span>\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0name=<span class=\"hljs-string\">\"name\"<\/span>\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0required\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0className=<span class=\"hljs-string\">\"w-full p-3 rounded bg-black\/50 border border-yellow-500\/20 text-yellow-200 focus:border-yellow-500\/60 focus:outline-none\"<\/span>\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\/&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;\/div&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;div&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;label htmlFor=<span class=\"hljs-string\">\"email\"<\/span> className=<span class=\"hljs-string\">\"block text-yellow-300 mb-2\"<\/span>&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Email\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;\/label&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;input\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0type=<span class=\"hljs-string\">\"email\"<\/span>\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0id=<span class=\"hljs-string\">\"email\"<\/span>\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0name=<span class=\"hljs-string\">\"email\"<\/span>\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0required\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0className=<span class=\"hljs-string\">\"w-full p-3 rounded bg-black\/50 border border-yellow-500\/20 text-yellow-200 focus:border-yellow-500\/60 focus:outline-none\"<\/span>\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\/&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;\/div&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;div&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;label htmlFor=<span class=\"hljs-string\">\"message\"<\/span> className=<span class=\"hljs-string\">\"block text-yellow-300 mb-2\"<\/span>&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Message\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;\/label&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;textarea\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0id=<span class=\"hljs-string\">\"message\"<\/span>\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0name=<span class=\"hljs-string\">\"message\"<\/span>\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0required\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0rows={<span class=\"hljs-number\">5<\/span>}\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0className=<span class=\"hljs-string\">\"w-full p-3 rounded bg-black\/50 border border-yellow-500\/20 text-yellow-200 focus:border-yellow-500\/60 focus:outline-none\"<\/span>\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\/&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;\/div&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;div&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;label htmlFor=<span class=\"hljs-string\">\"image\"<\/span> className=<span class=\"hljs-string\">\"block text-yellow-300 mb-2\"<\/span>&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0Image (Optional)\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;\/label&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;input\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0type=<span class=\"hljs-string\">\"file\"<\/span>\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0id=<span class=\"hljs-string\">\"image\"<\/span>\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0name=<span class=\"hljs-string\">\"image\"<\/span>\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0accept=<span class=\"hljs-string\">\"image\/*\"<\/span>\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0onChange={handleImageUpload}\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0className=<span class=\"hljs-string\">\"w-full p-3 rounded bg-black\/50 border border-yellow-500\/20 text-yellow-200 focus:border-yellow-500\/60 focus:outline-none\"<\/span>\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\/&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0{isUploading &amp;&amp; (\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;p className=<span class=\"hljs-string\">\"mt-2 text-yellow-300\"<\/span>&gt;Uploading image...&lt;\/p&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0)}\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0{imageUrl &amp;&amp; (\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;div className=<span class=\"hljs-string\">\"mt-2\"<\/span>&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;img\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0src={imageUrl}\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0alt=<span class=\"hljs-string\">\"Uploaded\"<\/span>\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0className=<span class=\"hljs-string\">\"max-w-xs rounded border border-yellow-500\/20\"<\/span>\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\/&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;\/div&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0)}\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;\/div&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;SubmitButton \/&gt;\n\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0&lt;\/Form&gt;\n\n\u00a0\u00a0\u00a0\u00a0&lt;\/div&gt;\n\n\u00a0\u00a0);\n\n}\n\n```<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><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<p>Notice that we now handle an optional image upload. When the user selects a file, <code>handleImageUpload<\/code> sends it to our new <code>\/api\/upload<\/code> endpoint (implemented below) and captures the returned <code>secure_url<\/code>. If the upload succeeds, <code>imageUrl<\/code> is stored in state; if it fails, an error message appears. When the form is submitted, we append <code>imageUrl<\/code> (if present) to <code>formData<\/code> before calling <code>submitForm<\/code>.<\/p>\n\n\n\n<p>With Part 1, we covered everything required to get a headless contact form up and running: we installed and tested Ninja Forms with WPGraphQL, built a secure Next.js API route (<code>\/api\/contact\/route.ts<\/code>) that submits name, email, and message via a GraphQL mutation, and wired up a corresponding server action (<code>actions.ts<\/code>) to bridge our client-side form to that endpoint.\u00a0<\/p>\n\n\n\n<p>We also enhanced the client form (<code>contact-form.tsx<\/code>) to allow users to pick an image, upload it to Cloudinary via a new <code>\/api\/upload<\/code> route, and then include the resulting <code>secure_url<\/code> alongside the rest of the form data. In Part 2, we\u2019ll dive into the details of the Cloudinary integration, such as how to configure and implement the server-side upload route, manage environment variables, and optimize image delivery on the frontend, so that every submission, including an optional photo, flows seamlessly into WordPress.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Contact forms are a fundamental touchpoint between site visitors and site owners, enabling customer inquiries, lead generation, and essential feedback that drives engagement. In headless WordPress, this can be tricky since the frontend and backend are separated. This means that you have to figure out a way for the frontend to send that data to [&hellip;]<\/p>\n","protected":false},"author":87,"featured_media":37875,"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-37873","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)<\/title>\n<meta name=\"description\" content=\"Create a secure contact form with optional image uploads in headless WordPress using Next.js App Router, WPGraphQL, Ninja Forms, and Cloudinary.\" \/>\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-1\" \/>\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 With Image Uploads in Headless WordPress Using Next.js, Ninja Forms, and Cloudinary (Part 1)\" \/>\n<meta property=\"og:description\" content=\"Create a secure contact form with optional image uploads in headless WordPress using Next.js App Router, WPGraphQL, Ninja Forms, and Cloudinary.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-1\" \/>\n<meta property=\"og:site_name\" content=\"Cloudinary Blog\" \/>\n<meta property=\"article:published_time\" content=\"2025-07-07T14:00:00+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1751431764\/Blog_Build_a_Contact_Form_with_Image_Uploads_in_Headless_WordPress_Using_Next.js_Ninja_Forms_and_Cloudinary_Part_1\/Blog_Build_a_Contact_Form_with_Image_Uploads_in_Headless_WordPress_Using_Next.js_Ninja_Forms_and_Cloudinary_Part_1.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-1#article\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-1\"},\"author\":{\"name\":\"melindapham\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9\"},\"headline\":\"Build a Contact Form With Image Uploads in Headless WordPress Using Next.js, Ninja Forms, and Cloudinary (Part 1)\",\"datePublished\":\"2025-07-07T14:00:00+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-1\"},\"wordCount\":950,\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-1#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1751431764\/Blog_Build_a_Contact_Form_with_Image_Uploads_in_Headless_WordPress_Using_Next.js_Ninja_Forms_and_Cloudinary_Part_1\/Blog_Build_a_Contact_Form_with_Image_Uploads_in_Headless_WordPress_Using_Next.js_Ninja_Forms_and_Cloudinary_Part_1.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-1\",\"url\":\"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-1\",\"name\":\"WordPress: Contact Forms With Image Uploads (Next.js, Ninja Forms, Cloudinary)\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-1#primaryimage\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-1#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1751431764\/Blog_Build_a_Contact_Form_with_Image_Uploads_in_Headless_WordPress_Using_Next.js_Ninja_Forms_and_Cloudinary_Part_1\/Blog_Build_a_Contact_Form_with_Image_Uploads_in_Headless_WordPress_Using_Next.js_Ninja_Forms_and_Cloudinary_Part_1.jpg?_i=AA\",\"datePublished\":\"2025-07-07T14:00:00+00:00\",\"description\":\"Create a secure contact form with optional image uploads in headless WordPress using Next.js App Router, WPGraphQL, Ninja Forms, and Cloudinary.\",\"breadcrumb\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-1#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-1\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-1#primaryimage\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1751431764\/Blog_Build_a_Contact_Form_with_Image_Uploads_in_Headless_WordPress_Using_Next.js_Ninja_Forms_and_Cloudinary_Part_1\/Blog_Build_a_Contact_Form_with_Image_Uploads_in_Headless_WordPress_Using_Next.js_Ninja_Forms_and_Cloudinary_Part_1.jpg?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1751431764\/Blog_Build_a_Contact_Form_with_Image_Uploads_in_Headless_WordPress_Using_Next.js_Ninja_Forms_and_Cloudinary_Part_1\/Blog_Build_a_Contact_Form_with_Image_Uploads_in_Headless_WordPress_Using_Next.js_Ninja_Forms_and_Cloudinary_Part_1.jpg?_i=AA\",\"width\":2000,\"height\":1100},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-1#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/cloudinary.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Build a Contact Form With Image Uploads in Headless WordPress Using Next.js, Ninja Forms, and Cloudinary (Part 1)\"}]},{\"@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)","description":"Create a secure contact form with optional image uploads in headless WordPress using Next.js App Router, WPGraphQL, Ninja Forms, and Cloudinary.","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-1","og_locale":"en_US","og_type":"article","og_title":"Build a Contact Form With Image Uploads in Headless WordPress Using Next.js, Ninja Forms, and Cloudinary (Part 1)","og_description":"Create a secure contact form with optional image uploads in headless WordPress using Next.js App Router, WPGraphQL, Ninja Forms, and Cloudinary.","og_url":"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-1","og_site_name":"Cloudinary Blog","article_published_time":"2025-07-07T14:00:00+00:00","og_image":[{"width":2000,"height":1100,"url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1751431764\/Blog_Build_a_Contact_Form_with_Image_Uploads_in_Headless_WordPress_Using_Next.js_Ninja_Forms_and_Cloudinary_Part_1\/Blog_Build_a_Contact_Form_with_Image_Uploads_in_Headless_WordPress_Using_Next.js_Ninja_Forms_and_Cloudinary_Part_1.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-1#article","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-1"},"author":{"name":"melindapham","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9"},"headline":"Build a Contact Form With Image Uploads in Headless WordPress Using Next.js, Ninja Forms, and Cloudinary (Part 1)","datePublished":"2025-07-07T14:00:00+00:00","mainEntityOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-1"},"wordCount":950,"publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-1#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1751431764\/Blog_Build_a_Contact_Form_with_Image_Uploads_in_Headless_WordPress_Using_Next.js_Ninja_Forms_and_Cloudinary_Part_1\/Blog_Build_a_Contact_Form_with_Image_Uploads_in_Headless_WordPress_Using_Next.js_Ninja_Forms_and_Cloudinary_Part_1.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-1","url":"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-1","name":"WordPress: Contact Forms With Image Uploads (Next.js, Ninja Forms, Cloudinary)","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-1#primaryimage"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-1#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1751431764\/Blog_Build_a_Contact_Form_with_Image_Uploads_in_Headless_WordPress_Using_Next.js_Ninja_Forms_and_Cloudinary_Part_1\/Blog_Build_a_Contact_Form_with_Image_Uploads_in_Headless_WordPress_Using_Next.js_Ninja_Forms_and_Cloudinary_Part_1.jpg?_i=AA","datePublished":"2025-07-07T14:00:00+00:00","description":"Create a secure contact form with optional image uploads in headless WordPress using Next.js App Router, WPGraphQL, Ninja Forms, and Cloudinary.","breadcrumb":{"@id":"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-1#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-1"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-1#primaryimage","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1751431764\/Blog_Build_a_Contact_Form_with_Image_Uploads_in_Headless_WordPress_Using_Next.js_Ninja_Forms_and_Cloudinary_Part_1\/Blog_Build_a_Contact_Form_with_Image_Uploads_in_Headless_WordPress_Using_Next.js_Ninja_Forms_and_Cloudinary_Part_1.jpg?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1751431764\/Blog_Build_a_Contact_Form_with_Image_Uploads_in_Headless_WordPress_Using_Next.js_Ninja_Forms_and_Cloudinary_Part_1\/Blog_Build_a_Contact_Form_with_Image_Uploads_in_Headless_WordPress_Using_Next.js_Ninja_Forms_and_Cloudinary_Part_1.jpg?_i=AA","width":2000,"height":1100},{"@type":"BreadcrumbList","@id":"https:\/\/cloudinary.com\/blog\/contact-form-headless-wordpress-next-js-ninja-forms-part-1#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/cloudinary.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Build a Contact Form With Image Uploads in Headless WordPress Using Next.js, Ninja Forms, and Cloudinary (Part 1)"}]},{"@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\/v1751431764\/Blog_Build_a_Contact_Form_with_Image_Uploads_in_Headless_WordPress_Using_Next.js_Ninja_Forms_and_Cloudinary_Part_1\/Blog_Build_a_Contact_Form_with_Image_Uploads_in_Headless_WordPress_Using_Next.js_Ninja_Forms_and_Cloudinary_Part_1.jpg?_i=AA","_links":{"self":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/37873","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=37873"}],"version-history":[{"count":3,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/37873\/revisions"}],"predecessor-version":[{"id":37877,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/37873\/revisions\/37877"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media\/37875"}],"wp:attachment":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media?parent=37873"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/categories?post=37873"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/tags?post=37873"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}