Next.js
This guide describes the integration of CKBox in a Next.js application. If you prefer to jump straight to the code, you can find the complete code of the application described in this guide on GitHub.
# Prerequisites
Before we start, please ensure that you have a recent LTS version of Node.js installed in your system. If the tool is not available from the command line, please follow Node.js download instructions first.
The guide assumes that we will be using Next.js v13 and its Pages Router.
# Creating the application
As a base in our example, we will create a new Next.js project using the create-next-app
command. Select the following answers in the step-by-step wizard:
npx create-next-app@13 ckbox-nextjs-example
- TypeScript Yes
- ESLint Yes
- Tailwind CSS Yes
src/
directory No- App Router No
- import alias No
After the project is created, you can enter the directory and start the development server:
cd ckbox-nextjs-example && npm run dev
# Environment variables
First, let’s create an .env.local
file and add the first entry: NEXT_PUBLIC_URL=http://localhost:3000
. It will be later referenced in the tokenUrl
configuration option of CKBox and CKEditor 5.
# .env.local
NEXT_PUBLIC_URL=http://localhost:3000
# CKEditor component with CKBox plugin
Let’s start by installing the required dependencies:
npm add ckbox @ckeditor/ckeditor5-build-classic @ckeditor/ckeditor5-react
As of the time of writing this guide, the CKEditor React component cannot be used with SSR in the Next.js project. Therefore, we will start by creating a simple component that will help us keep all editor’s dependencies in one place.
For the CKBox plugin with CKEditor 5, we will be using the lark
theme, a built-in preset that brings CKBox and CKEditor together in terms of styling.
Below you can find the complete code. Please note that CKBox is a peer dependency of CKEditor and the latter relies on the window.CKBox
object. We can ensure that CKBox is registered in the global scope by simply importing CKBox UMD build via import 'ckbox/dist/ckbox'
.
// components/CKEditor.tsx
// This component can be used on client-side only
// Do not use it with SSR
import React from "react";
import { CKEditor as CKEditorComponent } from "@ckeditor/ckeditor5-react";
import ClassicEditor from "@ckeditor/ckeditor5-build-classic";
import "ckbox/dist/styles/themes/lark.css";
// CKBox is a peer dependency of CKEditor. It must be present in the global scope.
// Importing UMD build of CKBox will make sure that `window.CKBox` will be available.
import "ckbox/dist/ckbox";
export default function CKEditor() {
const config = {
ckbox: {
tokenUrl: `${process.env.NEXT_PUBLIC_URL}/api/ckbox`,
theme: "lark",
},
toolbar: [
"ckbox",
"imageUpload",
"|",
"heading",
"|",
"undo",
"redo",
"|",
"bold",
"italic",
"|",
"blockQuote",
"indent",
"link",
"|",
"bulletedList",
"numberedList",
],
};
return (
<>
<style>{`.ck-editor__editable_inline { min-height: 400px; }`}</style>
<CKEditorComponent editor={ClassicEditor} config={config} />
</>
);
}
The above example includes a predefined CKEditor 5 build, which contains the required CKBox plugin. Note that CKEditor is configured to use CKBox by setting the required parameters of the ckbox
property. Please note that in the ckbox.tokenUrl
configuration option we pass the URL of the token endpoint that will be created in the next steps of this guide.
Finally, let’s disable React’s Strict Mode since it is interfering with the CKEditor 5 React component in development mode.
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: false,
};
module.exports = nextConfig;
# UI components
Let’s continue by creating a few UI components that we will be using across pages. In addition to re-usability, these components will bring a nicer look and feel to our app.
First, let’s update the styles/globals.css
file. We will get rid of styling introduced by the create-next-app
command and we will only leave Tailwind’s directives:
/* styles/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
Then, let’s add the Button
, Link
, and Page
components.
// components/Button.tsx
import React from "react";
type Props = {
children: React.ReactNode;
onClick: () => void;
};
export default function Button({ children, onClick }: Props) {
return (
<button
className="text-white bg-blue-600 hover:bg-blue-700 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-1.5"
onClick={onClick}
>
{children}
</button>
);
}
// components/Link.tsx
import React from "react";
import NextLink, { type LinkProps } from "next/link";
export default function Link(
props: Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, keyof LinkProps> &
LinkProps & {
children?: React.ReactNode;
} & React.RefAttributes<HTMLAnchorElement>
) {
return <NextLink className="text-blue-600 dark:text-blue-500 hover:underline" {...props} />;
}
// components/Page.tsx
import React from "react";
type Props = {
children: React.ReactNode;
};
export default function Page({ children }: Props) {
return <main className="w-full max-w-4xl mx-auto py-16 h-full flex flex-col">{children}</main>;
}
# Authentication
In a typical scenario access to CKBox will be restricted to authenticated users only. Therefore, let’s introduce a simple authentication mechanism to our app. Let’s start by installing NextAuth.js, a popular authentication library for Next.js:
npm add next-auth
Then, let’s initialize API routes. We will be using CredentialsProvider
which will allow us to introduce a simple password-based authentication. We will be using 3 test users and each one of them will be assigned a different CKBox role: user
, admin
, or superadmin
.
Below you can find the complete code for authentication handlers.
// pages/api/auth/[...nextauth].ts
import NextAuth from "next-auth/next";
import CredentialsProvider from "next-auth/providers/credentials";
import type { AuthOptions, User } from "next-auth";
const dbUsers: (User & { password: string })[] = [
{
id: "1",
role: "user",
email: "user@acme.com",
password: "testpwd123",
name: "John",
},
{
id: "2",
role: "admin",
email: "admin@acme.com",
password: "testpwd123",
name: "Joe",
},
{
id: "3",
role: "superadmin",
email: "superadmin@acme.com",
password: "testpwd123",
name: "Alice",
},
];
export const authOptions: AuthOptions = {
providers: [
CredentialsProvider({
credentials: {
email: {
label: "Email",
},
password: {
label: "Password",
},
},
authorize: (credentials) => {
const email = credentials?.email;
const password = credentials?.password;
const dbUser = dbUsers.find(
(dbUser) => dbUser.email === email && dbUser.password === password
);
if (!dbUser) {
throw new Error("Auth: Provide correct email and password");
}
return {
id: dbUser.email,
email: dbUser.email,
name: dbUser.name,
role: dbUser.role,
};
},
}),
],
callbacks: {
jwt: ({ token, user }) => {
if (user) {
token.user = user;
}
return token;
},
session: ({ token, session }) => {
session.user = token.user;
return session;
},
},
};
export default NextAuth(authOptions);
The authentication library expects us to set a couple of environment variables, namely NEXTAUTH_URL
and NEXTAUTH_SECRET
. Let’s add them to the .env.local
file:
# .env.local
# Already set
NEXT_PUBLIC_URL=http://localhost:3000
# Added now
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=G4JLoUae5Nke7/CZBkzFsR5NzLDKCcijsyUhTq3fQrA=
In production, you must generate your unique value for NEXTAUTH_SECRET
, see NextAuth’s documentation.
At this point, you are likely seeing type errors in the [...nextauth].ts
file. In order to get rid of them, we must adjust NextAuth’s types as per the library’s official guide. This step will allow us to use custom typings for Session
, User
, and JWT
objects. Let’s create auth.d.ts
file in the project’s root:
// auth.d.ts
import NextAuth, { DefaultSession } from "next-auth";
declare module "next-auth" {
type CKBoxRole = "user" | "admin" | "superadmin";
interface User {
email: string;
name: string;
role: CKBoxRole;
}
interface Session {
user: User & DefaultSession["user"];
}
}
declare module "next-auth/jwt" {
interface JWT {
user: User;
}
}
Next, let’s augment the main App
component with NextAuth’s session provider.
// pages/_app.tsx
import { SessionProvider } from "next-auth/react";
import type { AppProps } from "next/app";
import "@/styles/globals.css";
function App({ Component, pageProps: { session, ...pageProps } }: AppProps) {
return (
<SessionProvider session={session}>
<Component {...pageProps} />
</SessionProvider>
);
}
export default App;
Finally, let’s add the Nav
component that will allow users to sign in.
// components/Nav.tsx
import React from "react";
import { signIn, signOut, useSession } from "next-auth/react";
import Link from "./Link";
import Button from "./Button";
export default function Nav() {
const { data, status } = useSession();
return (
<nav className="border-b border-gray-200 py-5 relative z-20 bg-background shadow-[0_0_15px_0_rgb(0,0,0,0.1)]">
<div className="flex items-center lg:px-6 px-8 mx-auto max-w-7xl">
<div className="flex-1 hidden md:flex">
<Link href="/">Home</Link>
</div>
<div className="flex-1 justify-end flex items-center md:flex gap-3 h-8">
{status === "authenticated" ? (
<>
<span>Welcome, {data?.user?.name}!</span>
<Button onClick={() => signOut()}>Sign out</Button>
</>
) : status === "loading" ? null : (
<Button onClick={() => signIn()}>Sign in</Button>
)}
<Link
href="https://github.com/ckbox-io/ckbox-nextjs-example"
target="_blank"
rel="noreferrer"
>
GitHub
</Link>
</div>
</div>
</nav>
);
}
The Nav
component will be displayed as part of the Layout
component. Let’s add it, too.
// components/Layout.tsx
import React from "react";
import Nav from "./Nav";
type Props = {
children: React.ReactNode;
};
export default function Layout({ children }: Props) {
return (
<div className="mx-auto h-screen flex flex-col">
<Nav />
<div className="px-8 flex-1 bg-accents-0">{children}</div>
</div>
);
}
# Token URL
CKBox, like other CKEditor Cloud Services, uses JWT tokens for authentication and authorization. All these tokens are generated on your application side and signed with a shared secret that you can obtain in the Customer Portal. For information on how to create access credentials, please refer to the Creating access credentials article in the Authentication guide
Now that we have the required access credentials, namely: the environment ID and the access key, let’s create the token endpoint. As a base, we will use the code of the generic token endpoint in Node.js.
First, let’s install the jsonwebtoken library for creating JWT tokens:
npm add jsonwebtoken
npm add -D @types/jsonwebtoken
As a reference, we will use the code of the generic token endpoint in Node.js. Let’s wrap the logic of the Node.js token endpoint with the Next.js API handler and generate a token with the payload required by CKBox. Please note that access to this endpoint is limited to authenticated users only.
// pages/api/ckbox.ts
import jwt from "jsonwebtoken";
import { getServerSession } from "next-auth";
import type { NextApiRequest, NextApiResponse } from "next";
import { authOptions } from "./auth/[...nextauth]";
const CKBOX_ENVIRONMENT_ID = process.env.CKBOX_ENVIRONMENT_ID;
const CKBOX_ACCESS_KEY = process.env.CKBOX_ACCESS_KEY;
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getServerSession(req, res, authOptions);
if (session && CKBOX_ACCESS_KEY && CKBOX_ENVIRONMENT_ID) {
const user = session.user;
const payload = {
aud: CKBOX_ENVIRONMENT_ID,
sub: user.id,
auth: {
ckbox: {
role: user.role,
},
},
};
res.send(
jwt.sign(payload, CKBOX_ACCESS_KEY, {
algorithm: "HS256",
expiresIn: "1h",
})
);
} else {
res.status(401);
}
res.end();
}
As you can see on the code listed above, the access credentials required to sign JWT tokens are obtained from the environment variables. Thanks to this, you can conveniently add them to the .env.local
file:
# .env.local
# Already set
NEXT_PUBLIC_URL=http://localhost:3000
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=G4JLoUae5Nke7/CZBkzFsR5NzLDKCcijsyUhTq3fQrA=
# Added now
CKBOX_ENVIRONMENT_ID=REPLACE-WITH-ENVIRONMENT-ID
CKBOX_ACCESS_KEY=REPLACE-WITH-ACCESS-KEY
# Adding CKBox pages
CKBox can be embedded in the application in multiple ways. Examples in this guide will cover three popular scenarios:
- CKBox integrated with CKEditor 5
- CKBox used as a file picker in dialog mode
- CKBox used as a file manager in inline mode
# CKBox with CKEditor
Let’s use components created in previous steps to compose the /ckeditor
page:
// pages/ckeditor.tsx
import { GetServerSideProps } from "next";
import { getServerSession } from "next-auth";
import Layout from "@/components/Layout";
import Page from "@/components/Page";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
import dynamic from "next/dynamic";
// Use client-side rendering for CKEditor component
const CKEditor = dynamic(() => import("@/components/CKEditor").then((e) => e.default), {
ssr: false,
});
export default function CKEditorPage() {
return (
<Layout>
<Page>
<section className="flex flex-col gap-6">
<h2 className="text-4xl font-semibold tracking-tight">CKEditor</h2>
</section>
<hr className="border-t border-accents-2 my-6" />
<section className="flex flex-col gap-3 h-4/5">
In this example CKBox is integrated with CKEditor. With CKBox plugin, CKEditor
will upload files directly to your CKBox environment. Use icon in the top-left
corner of the editor to open CKBox as a file picker.
<CKEditor />
</section>
</Page>
</Layout>
);
}
// Access to this page allowed to signed-in users only
export const getServerSideProps: GetServerSideProps = async ({ req, res, resolvedUrl }) => {
const session = await getServerSession(req, res, authOptions);
if (!session) {
return {
redirect: {
destination: `/api/auth/signin?callbackUrl=${encodeURIComponent(resolvedUrl)}`,
permanent: false,
},
};
}
return {
props: {
session,
},
};
};
# CKBox as file picker
One of the common scenarios is to use CKBox as a file picker, where the user can choose one of the files stored in the file manager. After choosing the file, we want to obtain information about the chosen files, especially their URLs. This can be achieved using the assets.onChoose
callback passed as the CKBox’s configuration option.
In Next.js we can use CKBox directly as a React component. Currently, however, the CKBox
official component cannot be rendered on the server. Therefore, we must ensure that it’s used on the client side only.
Let’s start by installing the required packages:
npm add @ckbox/core @ckbox/components
The complete page code can be found below.
// pages/file-picker.tsx
import React from 'react';
import { GetServerSideProps } from 'next';
import dynamic from 'next/dynamic';
import { getServerSession } from 'next-auth';
import type { Asset, Props } from '@ckbox/core';
import Layout from '@/components/Layout';
import Page from '@/components/Page';
import Button from '@/components/Button';
import Link from '@/components/Link';
import { authOptions } from '@/pages/api/auth/[...nextauth]';
// CKBox cannot be currently rendered on the server
const CKBox = dynamic(() => import('@ckbox/core').then((e) => e.CKBox), {
ssr: false
});
// Let's import stylesheet separately, since it's not bundled with the official React component
import '@ckbox/components/dist/styles/ckbox.css';
export default function FilePicker() {
const [assets, setAssets] = React.useState<Asset[]>([]);
const [open, setOpen] = React.useState(false);
const handleOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
const handleChoose = (assets: Asset[]) => {
setOpen(false);
setAssets(assets);
};
const ckboxProps: Props = {
assets: { onChoose: handleChoose },
dialog: { open, onClose: handleClose },
tokenUrl: `${process.env.NEXT_PUBLIC_URL}/api/ckbox`
};
return (
<Layout>
<Page>
<section className="flex flex-col gap-6">
<h2 className="text-4xl font-semibold tracking-tight">
File Picker
</h2>
</section>
<hr className="border-t border-accents-2 my-6" />
<section className="flex flex-col gap-3">
One of the common scenarios is to use CKBox as a file
picker, where the user can choose one of the files stored in
the file manager. After choosing the file, we want to obtain
information about the chosen files, especially their URLs.
<div>
<Button onClick={handleOpen}>Choose assets</Button>
</div>
<CKBox {...ckboxProps} />
</section>
<section className="flex flex-col gap-3">
<ul>
{assets.map(({ data }) => {
const name = `${data.name}.${data.extension}`;
const content = data.url ? (
<Link
target="_blank"
rel="noreferrer"
href={data.url}
>
{name}
</Link>
) : (
name
);
return <li key={data.id}>{content}</li>;
})}
</ul>
</section>
</Page>
</Layout>
);
}
// Access to this page allowed to signed-in users only
export const getServerSideProps: GetServerSideProps = async ({
req,
res,
resolvedUrl
}) => {
const session = await getServerSession(req, res, authOptions);
if (!session) {
return {
redirect: {
destination: `/api/auth/signin?callbackUrl=${encodeURIComponent(
resolvedUrl
)}`,
permanent: false
}
};
}
return {
props: {
session
}
};
};
At this point, it’s also important to adjust the Webpack config of Next.js. We must ensure that @ckbox/*
dependencies are not code-split. Below is the complete next.config.js
file.
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: false,
webpack: (config, options) => {
if (!options.isServer) {
config.optimization.splitChunks.cacheGroups = {
...config.optimization.splitChunks.cacheGroups,
ckbox: {
test: /@ckbox\//,
minChunks: 1,
priority: 50,
},
};
}
return config;
},
};
module.exports = nextConfig;
# CKBox in inline mode
To start CKBox in inline mode, you can simply mount it in the desired place of your app. This is the default embedding mode for CKBox, so no additional configuration props besides tokenUrl
are required. CKBox will occupy as much space as allowed by its parent element.
// pages/inline.tsx
import { GetServerSideProps } from "next";
import { getServerSession } from "next-auth";
import dynamic from "next/dynamic";
import Layout from "@/components/Layout";
import Page from "@/components/Page";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
// CKBox cannot be currently rendered on the server
const CKBox = dynamic(() => import("@ckbox/core").then((e) => e.CKBox), {
ssr: false,
});
// Let's import stylesheet separately, since it's not bundled with the React component
import "@ckbox/components/dist/styles/ckbox.css";
export default function Inline() {
return (
<Layout>
<Page>
<section className="flex flex-col gap-6">
<h2 className="text-4xl font-semibold tracking-tight">Inline</h2>
</section>
<hr className="border-t border-accents-2 my-6" />
<section className="flex flex-col gap-3 flex-1">
<p>
To start CKBox in inline mode, you can instantiate it in an arbitrary
container. CKBox will respect height and width of the container.
</p>
<CKBox tokenUrl={`${process.env.NEXT_PUBLIC_URL}/api/ckbox`} />
</section>
</Page>
</Layout>
);
}
// Access to this page allowed to signed-in users only
export const getServerSideProps: GetServerSideProps = async ({ req, res, resolvedUrl }) => {
const session = await getServerSession(req, res, authOptions);
if (!session) {
return {
redirect: {
destination: `/api/auth/signin?callbackUrl=${encodeURIComponent(resolvedUrl)}`,
permanent: false,
},
};
}
return {
props: {
session,
},
};
};
# Home page
Finally, let’s tweak the home page so that it displays links to all pages created above.
// pages/index.tsx
import Layout from "@/components/Layout";
import Link from "@/components/Link";
import Page from "@/components/Page";
export default function Home() {
return (
<Layout>
<Page>
<section className="flex flex-col gap-6">
<h2 className="text-3xl font-semibold tracking-tight">
CKBox integration with Next.js
</h2>
</section>
<hr className="border-t border-accents-2 my-6" />
<section className="flex flex-col gap-3">
<p>Below you can find example integrations of CKBox.</p>
<p>
In a typical scenario access to CKBox will be restricted to authenticated
users only. Therefore, each sample is restricted to signed in users only.
Use different credentials to unlock various CKBox roles. See available users{" "}
<Link
href="https://github.com/ckbox-io/ckbox-nextjs-example/blob/main/pages/api/auth/%5B...nextauth%5D.ts"
target="_blank"
rel="noreferrer"
>
here
</Link>
.
</p>
<div className="flex-1 hidden md:flex gap-2">
<ol>
<li>
<Link href="/inline">Inline mode</Link>
</li>
<li>
<Link href="/file-picker">File picker</Link>
</li>
<li>
<Link href="/ckeditor">CKEditor</Link>
</li>
</ol>
</div>
</section>
</Page>
</Layout>
);
}
# Congratulations
Congratulations on completing the guide! You can now access the app in development mode:
npm run dev
Or use the production build:
npm run build && npm start
# Complete code
On GitHub, you can find the complete code of the application described in this guide.