guideNext.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 CKEditor Ecosystem dashboard. 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.