If you build a website with NextJS as full stack web framework, perhaps you will encounter a need to upload a file.
But the NextJS’s official website doesn’t write how to do this.
So I investigated other developers’ code to make a working version.
Init Project I will create new project base on last blog’s demo . If you need to read it again, please click here
1 git clone https://github.com/iiiyu/nextjs-tinymce-demo.git nextjs-upload-image-demo
Create the upload image folder 1 2 3 cd publicmkdir imagescd images
create the public/images/.gitignore
file
Replace old nextjs server For every images can be visit by custom domain name. We need to define their route, so I find only one method is Custom Server on NextJS.
Install express
Create the server.js
file
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const express = require ("express" );const next = require ("next" );const port = parseInt (process.env .PORT , 10 ) || 3000 ;const dev = process.env .NODE_ENV !== "production" ;const app = next ({ dev });const handle = app.getRequestHandler ();app.prepare ().then (() => { const server = express (); server.use ("/images" , express.static (__dirname + "/public/images" )); server.all ("*" , (req, res ) => { return handle (req, res); }); server.listen (port, (err ) => { if (err) throw err; console .log (`> Ready on http://localhost:${port} ` ); }); });
To run the custom server you’ll need to update the scripts in package.json
like so:
1 2 3 4 5 6 "scripts" : { "dev" : "node server.js" , "build" : "next build" , "start" : "NODE_ENV=production node server.js" , "lint" : "next lint" }
Create the API upload Install formidable
create the pages/api/upload/file.js
file
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 import formidable from "formidable" ;export const config = { api : { bodyParser : false , }, }; const post = async (req, res ) => { const form = formidable ({ uploadDir : "./public/images" , keepExtensions : true , }); form.parse (req, async function (err, fields, files ) { if (err) { res.status (500 ).json ({ error : err }); res.end (); return ; } if (files.file && files.file [0 ] && files.file [0 ].newFilename ) { res.status (200 ).json ({ location : process.env .NEXTAUTH_URL + "/images/" + files.file [0 ].newFilename , }); res.end (); } else { res.status (500 ).json ({ error : "No file uploaded" }); res.end (); } }); }; export default async (req, res) => { req.method === "POST" ? post (req, res) : req.method === "PUT" ? console .log ("PUT" ) : req.method === "DELETE" ? console .log ("DELETE" ) : req.method === "GET" ? console .log ("GET" ) : res.status (404 ).send ("" ); };
The upload component create the components/upload/ImageUpload.jsx
file
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import { useState } from "react" ;export function ImageUpload (props ) { const [image, setImage] = useState (null ); const [createObjectURL, setCreateObjectURL] = useState (null ); const uploadToClient = (event ) => { if (event.target .files && event.target .files [0 ]) { const i = event.target .files [0 ]; setImage (i); setCreateObjectURL (URL .createObjectURL (i)); } }; const uploadToServer = async (event ) => { const body = new FormData (); body.append ("file" , image); fetch ("/api/upload/file" , { method : "POST" , body, }) .then ((res ) => res.json ()) .then ((data ) => { if (data) { if (props.onUpload ) { props.onUpload (data); } } }); }; return ( <div className ="space-y-2" > <img className ="w-1/4" src ={createObjectURL} /> <div className ="space-x-4" > <input type ="file" name ="myImage" onChange ={uploadToClient} /> <button className ="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" type ="submit" onClick ={uploadToServer} > Upload Image </button > </div > </div > ); }
Support TinyMCE Update the components/editor/CustomEditor.jsx
file
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 import { Editor } from "@tinymce/tinymce-react" ;import React , { useRef } from "react" ;export function CustomEditor (props ) { const editorRef = useRef (null ); const log = ( ) => { if (editorRef.current ) { console .log (editorRef.current .getContent ()); } }; return ( <Editor tinymceScriptSrc ={ "/assets /libs /tinymce /tinymce.min.js "} onInit ={(evt, editor ) => (editorRef.current = editor)} value={props.content} init={{ selector: "textarea", // change this value according to your HTML images_upload_url: "/api/upload/file", automatic_uploads: true, height: 500, menubar: true, plugins: [ "advlist", "autolink", "lists", "link", "image", "charmap", "preview", "anchor", "searchreplace", "visualblocks", "code", "fullscreen", "insertdatetime", "media", "table", "code", "help", "wordcount", ], toolbar: "undo redo | blocks | " + "bold italic forecolor | alignleft aligncenter " + "alignright alignjustify | bullist numlist outdent indent | " + "removeformat | help", content_style: "body { font-family:Helvetica,Arial,sans-serif; font-size:14px }", }} onEditorChange={props.handleOnEditorChange} /> ); }
Index Show Uses those components
pages/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 import Head from "next/head" ;import Image from "next/image" ;import styles from "../styles/Home.module.css" ;import { CustomEditor } from "../components/editor/CustomEditor" ;import { ImageUpload } from "../components/upload/ImageUpload" ;import { useState } from "react" ;export default function Home ( ) { const [imageUrl, setImageUrl] = useState ("" ); return ( <div className ={styles.container} > <Head > <title > Create Next App</title > <meta name ="description" content ="Generated by create next app" /> <link rel ="icon" href ="/favicon.ico" /> <script src ="https://cdn.tailwindcss.com" > </script > </Head > <main className ={styles.main} > <div className ="space-y-16" > <div > <ImageUpload onUpload ={(data) => { setImageUrl(data.location); }} /> {imageUrl ? ( <> <img className ="w-1/4" src ={imageUrl} > </img > </> ) : ( <> </> )} </div > <div > <CustomEditor /> </div > </div > </main> <footer className ={styles.footer} > <a href ="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app" target ="_blank" rel ="noopener noreferrer" > Powered by{" "} <span className ={styles.logo} > <Image src ="/vercel.svg" alt ="Vercel Logo" width ={72} height ={16} /> </span > </a > </footer > </div> ); }
One more thing I supposed this method only support deploy to self host, it can’t support deploy to vercel.
Demo Demo
P.S. This article is very subjective. If you do not feel comfortable viewing it, please close it as soon as possible. If you think my article can help you, you can subscribe to this site by using RSS .
Referrals Photo by Markus Spiske on Unsplash
This article is based on the results of several programmers. But I forgot to keep those URLs. If you need to add a referral, please send me a comment.