How to Deploy a Powerful Virtual Event in Record Time: With Next.js and Agility CMS

Olga Content First
10 min readMar 5, 2021

The COVID-19 global pandemic has upended the conference industry. Not only cheaper and easier to organize, but also bigger and more global in both audience and impact, virtual events are here to stay.

  • Do you want to build a high performant, mobile optimized event while maintaining a full control over the front-end experience?
  • Do you want to deliver a frictionless, engaging registration that page drives conversions?

The answer is probably yes!

But where do you start?

Here is one option….

This Virtual Event Starter Kit was used to run Next.js Conf 2020: 80K+ registrations, 40K+ attendees, 4 stages, dozens of sponsors booths.

Now you can just CLONE. DEPLOY and CUSTOMIZE with your favorite Headless CMS.

Why you will love this Starter Kit:

  • Delegation: Running a conference is difficult — you have to delegate tasks to third-parties to ensure success. Certain elements of an online conference experience are tough to get right, and we’d rather lean on established, industry leading solutions.
  • Flexibility: While delegating certain elements of the conference experience is helpful, it’s also important to own the platform. That’s why this template provides a flexible open-source codebase that can be modified for your event.
  • Reducing Risk: It’s inevitable something will go wrong during your event. This platform reduces risk by leaning on a dynamic site that outputs as static files. These static files are cached, ensuring your site is never down. Then, it uses Serverless Functions to sprinkle dynamic content on top, which are hosted by a provider with 99.99% uptime.

And all of this, in just a few clicks:

1. CLONE

This virtual event starter kit was used to run Next.js Conf 2020, which had almost 40,000 live attendees. Now it is available to you! Just clone Git Repository with 1 click!

2. DEPLOY

Another click — deploy to Vercel, a cloud platform for static sites and Serverless Functions that enables developers to host Jamstack websites: deploy instantly, scale automatically, and requires no supervision, all with no configuration.

3. CUSTOMIZE

This starter kit includes the following features but all components are fully customizable (not just “white-label”, we mean it — fully!):

  • Multiple stages with an embedded YouTube stream
  • Sponsor expo, including individual virtual booths
  • Career Fair, allowing attendees to network and find job opportunities
  • Ticket registration and generation
  • Speaker pages and bios
  • Schedule

With Agility CMS you have all the tools you need to create amazing content fast.

Built With

Live Preview

https://demo.vercel.events/

What is Next.js?

is a hybrid framework that builds on top of Next.jsReact to provide an even simpler development experience. The JavaScript framework has a gentle learning curve, which makes it simple to implement even for novice JS developers. Learn more.

What is Vercel?

Vercel is a cloud platform for hybrid applications and Serverless Functions that fits perfectly with your workflow. It enables developers to host websites and web services that deploy instantly, scale automatically, and requires no supervision, all with no configuration. Learn more.

What is Agility CMS?

Agility CMS is a headless CMS platform born in Canada in 2003: Jamstack-friendly, API-first, headless CMS with hybrid features. In the world of pure headless CMS and traditional CMSs, it is designed to offer the best of both worlds: fast and flexible for developers as well as easy and powerful for marketers. Agility CMS gives both developers and marketers the tools to build, manage, and maintain their content with ease.

Github repo

https://github.com/vercel/virtual-event-starter-kit

Running Locally

First, set local environment variables. We’ve included a read-only DatoCMS access token you can use in .env.local.example.

cp .env.local.example .env.local

Then install packages and run the development server:

yarn install
yarn dev

Open http://localhost:3000 with your browser to see the result.

Clone and Deploy

Click the button below to clone and deploy this template on Vercel.

You’ll be asked to install the DatoCMS integration. It lets you sign up or log in to DatoCMS and create a new DatoCMS project based on the data (speakers, stages, etc.) used in the demo.

Customize

CMS

Environment variables determine which CMS to use. See lib/cms-api.ts for details and .env.local.example for all environment variables. The demo (demo.vercel.events) uses DatoCMS, but we also have support for:

Constants

lib/constants.ts contains a list of variables you should customize.

Authentication and Database

Some features won’t work until you set up authentication and database. The demo (demo.vercel.events) uses GitHub OAuth for authentication and Redis for database. You can use different providers as you see fit.

Authentication

You need to have GitHub OAuth set up to be able to customize the ticket after signing up on the registration form.

First, create a GitHub OAuth application to use for authentication.

  • Set Authorization Callback URL as <your domain>/api/github-oauth
  • After creating the OAuth app, create a client secret.

Running Locally:

  • Set the Authorization Callback URL as http://localhost:3000/api/github-oauth on GitHub.
  • On .env.local, set NEXT_PUBLIC_GITHUB_OAUTH_CLIENT_ID as the Client ID of the OAuth app.
  • Set GITHUB_OAUTH_CLIENT_SECRET as the Client secret of the OAuth app.
  • Finally, make sure the NEXT_PUBLIC_SITE_ORIGIN environment variable is set as http://localhost:3000. This is required to get the OAuth popup to work locally.
  • Restart the app (yarn dev) after editing .env.local.

Once it’s set up, sign up using the registration form on the home page (not on a stage page) and then click “Generate with GitHub”.

On Vercel:

  • Set the Authorization Callback URL as <your deployment’s URL>/api/github-oauth on GitHub.
  • Set NEXT_PUBLIC_GITHUB_OAUTH_CLIENT_ID and GITHUB_OAUTH_CLIENT_SECRET on Vercel Project Environment Variables Settings for the production environment.
  • Edit SITE_URL in lib/constants.ts to match your deployment’s URL (no trailing slash).
  • Push the code to redeploy the Project on Vercel.

Database

You need a database to save user data and enable the following features:

  • Generating a unique ticket number for each email when signing up on the registration form. If DB is not set up, it’ll always be 1234.
  • Generating a unique ticket image or ticket URL after signing in with GitHub. If DB is not set up, each ticket image or URL will show generic data.

The demo (demo.vercel.events) uses Redis, but you can customize it to use any database you like.

Running Redis Locally

  1. Install Redis locally and run it.
  2. Specify the following in .env.local:
REDIS_PORT=6379 # Default Redis port number
REDIS_URL=localhost
REDIS_PASSWORD=
REDIS_EMAIL_TO_ID_SECRET=foo # Come up with your own secret string

REDIS_EMAIL_TO_ID_SECRET will be used to create a hash of the email address, which will be used for the Redis key for each user data (i.e. id:<hash>). See lib/redis.ts for details.

  1. Restart the app (yarn dev) after editing .env.local.
  2. In a separate terminal window, start the Next.js dev server (yarn dev) and sign up using the registration form.
  3. In a separate terminal window, run Redis CLI, list keys (keys *) and inspect a id:<hash> key (hgetall id:<hash>). You should see the newly registered user.

Using Redis On Vercel

Provision your own Redis instance and set REDIS_PORT, REDIS_URL, REDIS_PASSWORD, and REDIS_EMAIL_TO_ID_SECRET (come up with your own secret string) on Vercel Project Environment Variables Settings for the production environment.

More Details

Stages

There are four different stages included in the seed data. Feel free to add or remove these based on your schedule. Each stage requires the user to enter their email to register with the conference before entering the event. After successfully entering their email and saving the user with your database of choice, the user is able to view the embedded YouTube stream. The login state is persisted as a httponly cookie.

One major feature of the conference platform is a near real-time sync with the CMS. Every five seconds, the stage queries /api/stages to fetch the latest information from the CMS. This allows you to make changes on the fly, without the user having the refresh the page. No need for websockets.

The primary use case for this is updating the YouTube embedded URL. Next.js Conf used this to seamlessly switch between pre-recorded videos running as a live premiere, and truly live content (e.g. panels). Plus, we had a few instances where our schedule needed to be tweaked on the fly. This implementation is fault tolerant, as well. The API route is properly cached and if the CMS was to somewhow go down, it won’t break the page.

Schedule / Speaker Pages

Schedule and speaker information is hosted in the CMS. The demo (demo.vercel.events) is seeded with images from Unsplash and a placeholder schedule. Each speaker has their own page with an image, bio, social media links, and information about their talk. The schedule is also shown as a sidebar on each corresponding stage.

Sponsor Expo

If you’d like to have your event sponsored, the Expo provides a platform to showcase sponsors with:

  • Their logo
  • Four call-to-action links
  • Embedded YouTube video
  • Link to chat room (Discord)

For Next.js Conf, we created a Discord channel for each sponsor.

Career Fair

Networking is vital for in-person conferences and replicating that environment virtually poses a challege. For the Career Fair, this starter provides the ability to list job postings, as well as an external link to talk with the company’s recruiters on Discord.

Adding Discord Chat

For Next.js Conf, we used Discord for conference attendees to chat. On each stage, we showed a highlighted message from the corresponding Discord channel. If a user in our allow list used the camera emoji (📸) it would show the message on the stage.

If you’d like to add similar functionality to your conference, you can use the API route below to fetch messages after creating a Discord bot. This API route is set up with the proper caching headers and ensures you won’t get rate-limited with high traffic.

import ms from 'ms';
import fetch, { Headers, RequestInit } from 'node-fetch';
import { NextApiRequest, NextApiResponse } from 'next';
interface Reaction {
emoji: { name: string };
}
interface Message {
id: string;
channel_id: string;
content: string;
timestamp: string;
author: {
username: string;
};
reactions?: Reaction[];
}
interface ReactionSelector {
id: string;
}
// After creating a bot, add the token as an environment var
const { DISCORD_BOT_TOKEN } = process.env;
// Number of seconds to cache the API response for
const EXPIRES_SECONDS = 60;
// Emoji that should be selected by a whitelisted user
// in order for this API to return the message
const EMOJI = '🎥';
// Whitelisted user IDs that are allowed to add the emoji to influence this API
const USERS = [
'752552204124291104' // username
];
// Discord base API URL
const API = 'https://discordapp.com/api/';
// Map of Stage names to Discord channel IDs
const CHANNELS = new Map<string, string>([
['a', '769350098697191515'],
['c', '769350352226877549'],
['m', '769350396623192074'],
['e', '769350429644685351']
]);
const api = (url: string, opts: RequestInit = {}) => {
const headers = new Headers(opts.headers);
headers.set('Authorization', `Bot ${DISCORD_BOT_TOKEN}`);
headers.set('User-Agent', 'Discord Bot (https://yoursite.com/conf, v0.1)');
return fetch(`${API}${url}`, {
...opts,
headers
});
};
async function getReactionSelectors(
channelId: string,
messageId: string,
emoji: string
): Promise<ReactionSelector[]> {
const res = await api(
`channels/${channelId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}`
);
if (!res.ok) {
throw new Error(`Failed to get message reactions: ${await res.text()} (${res.status})`);
}
return res.json();
}
async function getLatestMessageWithEmoji(
messages: Message[],
emoji: string,
usersWhitelist: string[]
) {
for (const message of messages) {
if (!message.content.trim()) {
// Empty message, ignore
// You could also filter messages here
continue;
}
for (const reaction of message.reactions || []) {
if (reaction.emoji.name === emoji) {
const selectors = await getReactionSelectors(message.channel_id, message.id, emoji);
const selector = selectors.find(r => usersWhitelist.includes(r.id));
if (selector) {
// The correct emoji was added from a whitelisted user
return { message, selector };
}
}
}
}
}
export default async function getDiscordMessage(req: NextApiRequest, res: NextApiResponse) {
const { stage } = req.query;
if (typeof stage !== 'string') {
return res.status(400).json({ error: 'Query parameter "stage" must be a string' });
}
const channelId = CHANNELS.get(stage);
if (!channelId) {
return res.status(400).json({ error: `Invalid "stage": ${stage}` });
}
const apiRes = await api(`channels/${channelId}/messages`);
let messages: Message[] = [];
if (apiRes.status !== 429 && apiRes.ok) {
messages = await apiRes.json();
}
if (apiRes.status === 429) {
const reset = apiRes.headers.get('X-RateLimit-Reset-After') || 5;
res.setHeader(
'Cache-Control',
`s-maxage=${reset}, public, must-revalidate, stale-while-revalidate`
);
}
const messageToShow = await getLatestMessageWithEmoji(messages, EMOJI, USERS);
if (!messageToShow) {
return res.status(404).json({ error: 'Could not find message with emoji' });
}
const body = {
username: messageToShow.message.author.username,
content: messageToShow.message.content,
timestamp: messageToShow.message.timestamp
};
// Set caching headers
const expires = new Date(Date.now() + ms(`${EXPIRES_SECONDS}s`));
res.setHeader('Expires', expires.toUTCString());
res.setHeader(
'Cache-Control',
`s-maxage=${EXPIRES_SECONDS}, immutable, must-revalidate, stale-while-revalidate`
);
return res.status(200).json(body);
}

Demo

The demo is available at https://demo.vercel.events. The data recorded or used on the demo may be removed by Vercel at any point.

Note: This project utilizes Next.js Dynamic Routes and relies on routes being defined in the codebase to build your pages. Looking for more control over your pages and what UI components are on them? Consider implementing Agility Page Management.

What Developers will love about this Event Starter:

Performance at scale: Fast Video streams and pages even with high traffic.

Mobile optimized: Responsive pages for all screen sizes are a must for virtual attendees.

Zero downloads: View in any browser on any device without any downloads.

Data management: Non-technical team members can edit speakers, sponsors, and content.

Full power to you: Complete control over the frontend experience with Next.js pages.

What Marketers will love about this Event Starter:

Custom design: User experience belongs in the hands of the event team, beyond basic white labeling.

Drive registrations: A frictionless, engaging registration page drives conversions more than any part of the event plan.

Sponsor expo: Virtual booths with Call to Action and lead generation tools.

Job fair: Online events are a natural time for job seekers and job providers to connect.

Editor and marketer friendly: Agility CMS has all the tools your team needs.

Moderated chat: Chat shouldn’t distract from content.

If you are interested to master this and add your own to your portfolio, join this WORKSHOP.

What you will learn:

  • Best practices for creating a conference platform that delights both marketing team and developers.
  • Ins and outs of Next.js legendary conference platform that brought 80k Registrations and 40K Attendees.
  • Secret tricks and tips for a viral ticketing system and engaging conference chats.
  • Practical step-by-step instructions on how to launch your own custom conference platform using Next.js and Agility CMS.

Register for the workshop here:

How to Deploy a Powerful Virtual Event in Record Time: With Next.js and Agility CMS

Originally published at https://agilitycms.com on March 11, 2021.

--

--

Olga Content First

I write about #CX and content marketing! #AgilityCMS is the all-in-one #JAMstack #CMS with tools for amazing #DX and Editor Experience. http://agilitycms.com