Allen Helton
Posted on August 29, 2023
Have you ever given an in-person presentation? As you talk, you look around the room and see heads nodding or people taking notes. Maybe someone raises their hand and asks a question about something you said. It’s easier to get engagement from someone when you can see them directly.
But online presentations are hard. You (generally) can’t see the people you’re presenting to. It feels the exact same as if you were rehearsing by yourself or presenting to a thousand people in your audience. There’s a disconnect between you and your audience that prevents you from knowing if you’re crushing it or flopping.
A few months ago, I saw a post from Michael Liendo describing how he uses Apple Keynote and WebSockets to maximize engagement in his presentations. He built a website that allows members of his audience to send emojis across the screen in real-time as he presents. How cool is that?!
I wanted that. I wanted to add the ability to send comments, too. So I read through his blog post and was immediately inspired, but there was a problem. I don’t have a Mac (I know, I know) so I can’t use Keynote. Plus his design involved using the AWS console to make a WebSocket API in AppSync. I felt like while this was incredibly cool for a single presentation it didn’t seem like it would scale well.
I already put WAY more effort into building presentations than I probably should, I can’t afford rebuilding a web app to help drive audience engagement every. single. time. I wish I had time for that, but the reality is…I don’t. With this in mind, I had a few requirements to run with for enhancing Michael’s phenomenal idea:
- Compatible with Google Slides (who doesn’t like free?!)
- Dynamically support presentations as I build them
- Minimize the architecture and deployment requirements
- Show some fun stats at the end of the presentation
Let’s take a look at how I built my live reaction app to satisfy these requirements.
Google Slides support
If you’ve ever built a presentation with PowerPoint or Keynote, you’ll know that Google Slides is… not that. It has a limited set of features and animations, but for the features it does have, it does them really well. Plus it’s free and has great online collaboration capabilities. If you already have Keynote or PowerPoint presentations, you can import them to Google Slides no problem.
When setting out on the build for this app I had two things in mind - avoid messing with presentation html and allow presentations from multiple authors. So I began poking around the Slides user interface until I found how to make presentations publicly accessible using the Publish to web feature.
When you publish a presentation to the web, you’re provided with an option to embed it. This will give you an iframe with a link to your slides. You can drop that into any web page and view it hosted in your app just like that! Upon closer inspection of the embedded code, I noticed something particularly useful:
<iframe
src="https://docs.google.com/presentation/d/e/2PACX-1vRLPL95FvyFg9DxT0iMfmOFLVwxTgEDFfl8Z/embed?start=false&loop=false&delayms=60000"
frameborder="0"
width="1440"
height="839"
allowfullscreen="true"
mozallowfullscreen="true"
webkitallowfullscreen="true"/>
It looked like this could be parameterized! The presentation id is after https://docs.google.com/presentation/d/e/
part of the url. So all I had to do was drop the iframe in my Next.js app page and parameterize the src element to generically render presentations! It would look something like this: https://docs.google.com/presentation/d/e/${slidesId}/embed?start=false&loop=false&delayms=60000
.
So to recap here, for Google Slides I had to publish it to the web then grab the id from the embedded iframe that was returned to me. Now I had to figure out what to do with that.
Dynamic presentation support
As I said earlier, I don’t want to rebuild this app every time I make a new presentation. I just want to take my presentation id, give it to the app, and be done. If I can do that, I’d consider it a win.
I structured my web app to look up presentations dynamically. My page structure looks like this:
├─pages
│ ├─[name]
│ │ ├─index.js
│ │ ├─react.js
│ │ ├─results.js
There are a few pages here, each with their own flavor of “dynamic-icity”.
Presentation page
That presentation id is not user friendly at all. I wanted to alias it with a friendly name so people don’t have to type that monstrosity. So I created an API endpoint in my app to do the mapping for me. For now I did a hardcoded list, but as I give more and more presentations I will move this over to a presentation management page where I store and update the details in a database.
import slides from '../../../lib/slides';
export default async function handler(req, res) {
try {
const { name } = req.query;
const presentation = slides.find(m => m.name == name.toLowerCase());
if (!presentation) {
return res.status(404).json({ message: `A presentation with the name "${name}" could not be found.` });
}
return res.status(200).json({ id: presentation.id, title: presentation.title });
} catch (err) {
console.error(err);
res.status(500).json({ message: 'Something went wrong' });
}
};
The slides import is just a json array that has the id from Google Slides, title, and friendly name of my presentations:
const slides = [
{
name: 'caching-use-cases',
id: '2PACX-1vQxQnmKrdy1FX3KzTWs7mC89UHDNH5kVeiUJpeZBnQiWNYXX6QjupaUln',
title: 'You DO Have A Use Case For Caching'
},
{
name: 'building-a-serverless-cache',
id: '2PACX-1vSmwWzT1uMNfXpfwujfHFyOCrFjKbL8X43sd5xOpAmlK01lEICEm2kg',
title: 'Behind the Scenes: Building a Serverless Caching Service'
}
];
So when someone hits the /caching-use-cases endpoint in my app, the page will fetch the Google Slides id and title from the server side component and will use that to render the content in the iframe.
Reaction page
I wanted to be like Michael. The whole point of this was to drive audience engagement, so I had to provide a user interface for people to react to my presentation as I’m giving it. That’s where the /[name]/react path comes into play.
First, I had to get people to that page. But I didn’t want to hardcode anything, that was requirement #1. Luckily, I stumbled upon the react-qr-code library that will dynamically create and render QR codes in React apps. So I added a card underneath the presentation display that will always be visible so users can scan it with their phones and jump directly to the reactions.
<Card variation="elevated" borderRadius="medium" width="99%" padding="relative.small">
<Flex direction="row" justifyContent="space-between">
<Flex direction="row" alignItems="center">
<Link href={`${router.asPath}/react`}>
<QRCode
value={`https://${process.env.DOMAIN_NAME}${router.asPath}/react`}
size={256}
style={{ height: "auto", maxWidth: "4em" }} />
</Link>
<Heading level={4}>Scan the QR code to react live!</Heading>
</Flex>
</Flex>
</Card>
In case you’re wondering, I’m using the Amplify UI components for this project. I’m not much of a UI developer, so having these styled components has been a life saver! Anyway, adding the card beneath the presentation will result in something like this:
This will be visible during the entire presentation, so it doesn’t matter if audience members come in early or late, they’ll always be able to get to the reaction page to send some emojis or ask questions. The reaction page is optimized for mobile viewing, giving audience members the choice of three emojis or to add their own question/comment.
As the audience presses an emoji or types in a question, a message will be sent over our managed WebSocket (more on this in a sec) to the presentation page and it will be displayed on screen. Don’t worry, I’ve built in comment throttling and profanity filters for the inevitable hecklers.
Small deployment
Another one of my objectives with this project was to make a small, self-contained web app that doesn’t rely on a large backing architecture. This is meant to be simple. I didn’t want to mess with WebSockets or bounce around in the AWS console creating a bunch of cloud resources. Instead, I opted to take advantage of Momento to do all that hard work for me. This leaves my architecture small and simple 👇
Everything is self-contained in the Next.js app. The mappings of friendly names to presentation ids is done on the server-side component of the app and the WebSockets are handled via Momento Topics. I don’t have to manage cloud resources like WebSocket channels/topics or subscriptions. I plug in the Momento Web SDK and it just works. Literally.
Really the only thing you have to do to get this in the cloud is set up your web hosting. Since there aren’t dependencies on any specific cloud vendors, you could host this in Vercel, Fastly, or something like AWS Amplify (my personal preference). But before you go and set it up, there are two things you need to do first:
- Update the /lib/slides.js file with your presentations
- Configure three environment variables
- MOMENTO_AUTH - API key issued via the Momento Console. This token will be used to configure short-lived API tokens the server-side component sends down to browser sessions
- NEXT_PUBLIC_CACHE_NAME - Name of the cache to use for Momento. This must exist in the same region as your API key. The app does all the work for you, you just need to create a cache with any name you want.
- NEXT_PUBLIC_DOMAIN_NAME - Base url of the custom domain for your app. It doesn’t even need to be a custom domain, you could update this to the generated domain after you deploy.
Then get it deployed! Once deployed, it will just start working.
Fun stats
I mentioned one of my requirements was to show some fun stats at the end of the presentation. What’s more fun than seeing who reacted the most and what the most used reactions are?!
Every time someone pushes one of the reaction buttons, a score is incremented for both the person reacting and the reaction used.
await cacheClientRef.current.sortedSetIncrementScore(process.env.NEXT_PUBLIC_CACHE_NAME, `${name}-reacters`, data.username);
await cacheClientRef.current.sortedSetIncrementScore(process.env.NEXT_PUBLIC_CACHE_NAME, name, data.reaction);
By using a sorted set, I’m building a leaderboard without having to do any of the hard work. I’m incrementing the score for a specific username and for a specific reaction in a cache. When the presentation is over and it’s time to look at the results, I can fetch the scores in descending order, which gives me the leaderboard effect automatically.
const getLeaderboard = async (cacheClient, leaderboardName) => {
let board;
const leaderboardResponse = await cacheClient.sortedSetFetchByRank(process.env.NEXT_PUBLIC_CACHE_NAME, leaderboardName, { startRank: 0, order: 'DESC' });
if (leaderboardResponse instanceof CacheSortedSetFetch.Hit) {
const results = leaderboardResponse.valueArrayStringElements();
if (results.length) {
board = results.map((result, index) => {
return {
rank: index + 1,
username: result.value,
score: result.score
};
});
}
}
return board;
};
You can see once again, the leaderboard results are dynamic, using the friendly name of the presentation as the key that stores the data. This ends up with a page looking like this:
This brings a little competition and fun to the presentation, hopefully keeping the audience fully engaged.
Try it yourself
I would highly encourage you to try this out yourself. Get your audience involved and make your presentations stand out.
If you’re wondering how much this would cost you, you’ll probably like the answer - nothing!
Hosting platforms like Vercel cost nothing to host hobby projects. Momento is priced at $0.50 per GB of data transfer with 5GB free every month. Each reaction sends an 80 byte message, so we can calculate your free amount of reactions as:
5 GB / (80 bytes x 2 (data in from publisher and out to subscriber)) = 33.5 million messages
So if you keep your reaction count to fewer than 33.5 million a month, then you’re well within the free tier 🙂. But if you do surpass it, you can get ~13 million reactions for $1.
At the end of the day, the goal is to help people understand and remember your message. Feel free to change the reactions, add more, take away the commenting, anything that helps keep the attention on your content.
Thank you to Michael Liendo, who gave me inspiration to build this. It’s a lot of fun and has lots of potential for enhancements in the future. I’m excited to deliver engaging presentations and get real-time audience feedback.
If you’re ready to try, I’m always available to help, be it with your deployment, getting a Momento token, or figuring out how to publish your presentations. You can contact me or someone from the Momento team on Discord or via the Momento website.
Happy coding!
Posted on August 29, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.