NextJS creates a upload image api by itself and it supports Tinymce

NextJS creates a upload image api by itself and it supports Tinymce

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 public
mkdir images
cd images

create the public/images/.gitignore file

1
2
3
4
# Ignore everything in this directory
*
# Except this file
!.gitignore

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.

  1. Install express
1
yarn add express
  1. 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}`);
});
});
  1. 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

1
yarn add formidable@v3

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) {
// await saveFile(files.file);
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>
);
}

upload image

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.

NextJS creates a upload image api by itself and it supports Tinymce

https://iiiyu.com/2022/10/02/NextJS-create-a-upload-image-api-by-itself-and-it-supports-Tinymce/

Author

Ewan Xiao

Posted on

October 2nd 2022

Updated on

September 28th 2023

Licensed under

Comments