Blog

Zo bouw je een online community in Next.js

· Andrij Tkachenko · 9 min leestijd

Een browservenster van een online community: een zijbalk met kanalen, een spraakruimte, een videoruimte en events, in het midden een live podium met twee mensen, eronder een hart en een opgestoken hand, en ernaast een kolom berichten, waarvan twee foto’s

Een forum bewaart wat de leden hebben geschreven. Een community die levend aanvoelt, heeft meer tegelijk te bieden. Leden delen een foto en krijgen antwoord terwijl ze er nog zijn, stappen een ruimte binnen om te praten, met hun stem of op camera, kijken live naar de events van de club en komen het podium op met hun eigen werk, en zien wie er op dit moment is. In een Next.js-app is elk daarvan een verbinding die openblijft, met veel mensen tegelijk.

Dit artikel loopt elk onderdeel langs dat een echte community-app nodig heeft, met een volledig werkend voorbeeld: Community, de fotoclub uit onze Next.js-handleiding, met zijn kanalen, zijn lounges om even binnen te lopen en zijn live events. Je kunt het live uitproberen. Log in als Mia, host van de club, want de portfolioreview begint nu: open de studio en ga live. Open de club daarna in een privévenster als Ava, kijk naar de review, steek je hand op en kom het podium op.

Wat een online community nodig heeft

  • Kanalen die bijblijven. Berichten en foto’s bereiken iedereen die meeleest meteen, met wie er typt en de reacties, en blijven bewaard voor wie later komt. De foto’s openen alleen voor leden.
  • Ruimtes om binnen te lopen. Spraakruimtes, en ruimtes met camera’s en schermen, waar leden in- en uitlopen wanneer ze willen, veel mensen tegelijk via mediaservers (hoe mediaservers werken). Hosts die iemand kunnen dempen of eruit halen.
  • Live events. Eén host voor alle leden, met een chat en reacties naast het beeld, en een lid dat het podium op wordt gehaald om eigen werk te laten zien (hoe livestreaming werkt in Next.js).
  • Wat er nu gebeurt. Wie in welke ruimte zit en welk event live is, actueel op elke pagina, zonder herladen.
  • Wie er is. Wie er online is, en een manier om diegene te schrijven of te bellen.

Dat alles loopt over verbindingen die openblijven zolang een lid op de site is, en dat kan een serverless functie niet volhouden (waarom, en wat je eraan doet). Je kunt elk onderdeel zelf bouwen, op websockets, WebRTC en eigen mediaservers. Of je laat een realtime cloud de verbindingen, de gesprekken en de streams draaien, terwijl je server houdt wat echt van jou is: de leden, de regels en de gegevens. Dat doet de club, met WordPlus Cloud.

Het voorbeeld: een fotoclub

Leden delen foto’s in kanalen om feedback te krijgen, zoals #critique en #trips. Ze lopen binnen in de lounges: Coffee lounge met hun stem, Editing hangout met camera’s en schermen. De hosts leiden live events, een portfolioreview of een les nachtfotografie, waarbij leden het podium op komen. Het is een Next.js-app met de App Router, Better Auth en Postgres, en het realtime deel bestaat uit een paar bestanden. Vijf stappen maken het.

1. Kanalen met foto’s, bewaard op je eigen server

Elk kanaal is een ruimte die naar zijn nummer heet: channel:2. De site declareert ze één keer, met dat patroon, samen met de regel wie erin mag:

lib/realtime.ts
const MESSAGES = { history: 50, emit: ['message', 'typing', 'reaction', 'read'], webhook: ['message', 'reaction'] };
realtime.room('channel:{id}', MESSAGES, async (user, _params, room) => mayWrite(user, room));

Elk lid mag in een kanaal, en verder niemand. Berichten, wie er typt, reacties en leesbevestigingen gaan van pagina naar pagina via WordPlus Cloud, verzegeld, en de webhooks message en reaction brengen elk ervan naar de server van de site, die het in de eigen database bewaart. De pagina van het kanaal is de kant-en-klare chat op die ruimte:

app/(app)/channels/[id]/channel.tsx
<Chat
    rt={useRealtime()}
    room={room}
    people={people}
    messages={`/api/messages?room=${room}`}
    upload="/api/files"
    accept="image/jpeg,image/png,image/webp,image/gif"
    maxFileSize={15 * 1024 * 1024}
/>

messages is de eigen route van de site voor oudere berichten, die laden terwijl een lid omhoog scrolt. upload geeft het invoerveld zijn paperclip, en neemt ook foto’s aan die op de chat worden geplakt of gesleept: elke foto gaat naar de eigen route van de site zodra die gekozen is, en het bericht draagt alleen waar de foto opent. WordPlus Cloud ziet de foto nooit. De route beslist met de regel van de ruimte zelf:

app/api/files/route.ts
if (!me || !(file instanceof File) || !/^(channel|chat):\d+$/.test(room) || !(await realtime.mayJoin(room, me))) {
    return Response.json({ error: 'This photo is not for this conversation.' }, { status: 403 });
}

realtime.mayJoin() voert dezelfde regel uit als het binnengaan van de ruimte, dus wie een kanaal mag lezen, mag er een foto delen, en verder niemand. Elke foto opent op een eigen adres, dat opnieuw nagaat wie hem opent: wie niet bij de club hoort, krijgt niets, welk adres diegene ook heeft.

2. Spraakruimtes waar de hosts op letten

Een lounge is een groepsgesprek dat naar zijn nummer heet, lounge:3, waar elk lid kan binnenlopen. De regel maakt de hosts van de club tot zijn hosts:

lib/realtime.ts
realtime.groupCalls('lounge:{id}', async (user) => {
    if (!(await isMember(user))) return false;
    return (await isHost(user)) ? { admin: true } : true;
});

De lounge verschijnt op een eigen pagina, met de knop om binnen te lopen erin:

app/(app)/lounges/[id]/lounge.tsx
<CallSpot rt={rt} call={room} className="h-[calc(100dvh-14rem)] rounded-2xl">
    <GroupCallButton rt={rt} room={room} type={kind} title={name}>
        {people.length > 0 ? 'Drop in' : `Open ${name}`}
    </GroupCallButton>
</CallSpot>

type is audio voor een spraaklounge en video voor een lounge met camera’s. Voordat een lid binnen is, toont CallSpot wat erin staat: wie er nu in de lounge zit, en de knop. Eenmaal binnen neemt het gesprek die plek in: ieders beeld, of hun camera’s en schermen, en de knoppen. Een lid komt binnen met microfoon en camera uit en zet ze aan vanuit het gesprek, zodat binnenlopen nooit onverwacht een microfoon opent. Op een andere pagina van de site loopt de lounge door in een venster in de hoek, en komt terug als de pagina weer opent.

admin: true is alles wat de hosts nodig hebben. Op het scherm van een host heeft elke tegel van de anderen dempen en verwijderen, wat het gespreksscherm via dezelfde route vraagt, die het alleen van een host aanneemt.

3. Live events, met leden op het podium

Een event is een livestream die naar zijn nummer heet: event:5. De regel zegt wie wie is: de host van het event gaat live, en elk ander lid kijkt:

lib/realtime.ts
realtime.streams(
    'event:{id}',
    async (user, { id }) => {
        const event = await eventOf(id);
        if (!event || !(await isMember(user))) return false;
        return String(user) === event.host ? { role: 'host', quality: '1080p' } : 'viewer';
    },
    { stage: true, reactions: true },
);

De host gaat live vanuit een eigen studio, een pagina van de site met het beeld groot, Go live eronder, en ernaast de chat en de opgestoken handen:

app/(app)/studio/[id]/page.tsx
<LiveStream
    name={`event:${event.id}`}
    view="studio-page"
    title={event.title}
    url={`/events/${event.id}`}
    backUrl={`/events/${event.id}`}
/>

De pagina van het event geeft elk lid de speler met de chat, dezelfde <LiveStream> onder dezelfde naam. Een host kan ook live gaan vanuit OBS, met het adres en de sleutel die de studio geeft. stage: true is het podium: een lid steekt in de speler een hand op, de host ziet die onder Guests en nodigt het lid uit, en zodra het lid op Join stage drukt, gaan camera en microfoon live naast de host, voor iedereen die kijkt.

4. Wat er nu gebeurt, op elke pagina

Naast elke pagina toont een zijbalk wie er in elke lounge zit en welk event live is, met hoeveel kijkers. De site houdt daar geen tabel van bij. WordPlus Cloud weet het:

lib/realtime.ts
export async function liveNow(): Promise<Map<string, number>> {
    const live = new Map<string, number>();
    for (const stream of await realtime.listStreams()) {
        const event = idIn(stream.name, 'event');
        if (event) live.set(event, stream.viewers);
    }
    return live;
}

realtime.listCalls() en realtime.call(name).get() zeggen hetzelfde over de lounges en wie in elke lounge zit. Om dat actueel te houden, stuurt WordPlus Cloud de site een webhook telkens als iemand een lounge binnenloopt of verlaat, of een event live gaat of eindigt. De site zegt het in een ruimte waar elke pagina van een lid zich bij aansluit, en als een event live gaat, laat de site het elk lid weten:

lib/realtime.ts
realtime.on('stream.live', async ({ stream }) => {
    const id = idIn(stream, 'event');
    const event = id ? await eventOf(id) : null;
    if (!event) return;
    await realtime.to('club').emit('changed');
    const everyone = (await members()).map((person) => person.id).filter((person) => person !== event.host);
    await realtime.toUser(everyone).emit('live', { event: event.id, title: event.title, host: event.hostName });
});

In de layout hoort elke pagina allebei:

app/(app)/live.tsx
useRoom('club', { changed: () => router.refresh() });

useUserEvent('live', (data: { event: string; title: string; host: string }) => {
    if (path !== `/events/${data.event}`) show({ text: `${data.host} is live: ${data.title}`, href: `/events/${data.event}`, live: true });
});

Het eerste laat elke pagina zichzelf opnieuw bij de server opvragen, zonder herladen, zodat de zijbalk en de open pagina actueel blijven. Het tweede toont een melding op welke pagina een lid ook is: „Mia is live: Live portfolio review”, die het event opent. group.joined, group.left en stream.ended zeggen changed in dezelfde ruimte.

5. Wie er online is

WordPlus Cloud weet wie er online is: elke pagina waarvan de site voor het lid heeft ingestaan, maakt dat lid online, en de pagina hoort wie er komt en gaat, zonder verzoek aan de server van de site:

app/(app)/members/directory.tsx
const { online } = useOnline(people.map((person) => person.id));
const here = people.filter((person) => online[person.id]);
const away = people.filter((person) => !online[person.id]);

De ledenlijst zet wie online is bovenaan, elk lid met Message en Call, en naast elke pagina toont de layout de club op dezelfde manier, met de hosts gemarkeerd. De pagina van een lid zegt of het lid online is, of wanneer het voor het laatst gezien is.

Wat erbij zit

  • Gesprekken met z’n tweeën. De pagina van een lid bevat je chat met dat lid, foto’s inbegrepen, en jullie gesprek verschijnt naast de berichten.
  • Reacties en een chat bij elk event, naast het beeld, en live gaan vanuit OBS of elke encoder die via WHIP verstuurt.
  • Scherm delen in een lounge met camera’s, via de knoppen van het gesprek.
  • Namen en gezichten uit je app. Elke tegel, elk bericht en elk podium toont naam en foto van een lid zoals profile van de site ze geeft, zonder dat WordPlus Cloud een lijst met gebruikers bijhoudt.
  • Toegang intrekken. Als iemand de club verlaat, zeggen de regels vanaf dan nee, en realtime.call(name).remove(user) haalt die persoon meteen uit een lounge.

Probeer het, en bouw dan je eigen

In de live demo is de club een dag lang alleen van jou, dus wat je er ook deelt of wie je er ook ontmoet, niemand anders ziet het, en een event of een lounge eindigt na 10 minuten vanzelf. Onder elke pagina toont How this page works stap voor stap de code die de pagina maakt, met de waarden van de pagina die je open hebt.

De handleiding voor de communitysite bouwt de hele site, met elk bestand van de realtime kant volledig uitgeschreven. Livestreams in Next.js en gesprekken in Next.js laten elke manier zien om een stream of een gesprek in je eigen app te zetten. Voor een team in plaats van een club bouwt videovergaderen in Next.js de vergaderingen van een teamintranet.

WordPlus Cloud for Apps is geprijsd op gelijktijdige verbindingen, vanaf 1.000, maandelijks of jaarlijks gefactureerd. Met 1.000 verbindingen worden livestreams niet gemeten tot 50 kijkers tegelijk, en groepsgesprekken tot 32 mensen tegelijk op video, waarbij iemand op audio voor de helft telt. Elke 1.000 verbindingen extra voegen 20 kijkers en 12 mensen op video toe. Daarboven kost elke extra kijker of persoon op video een credit per minuut, een stream alleen geteld terwijl die live is, en gesprekken tussen twee mensen kosten geen credits. Bekijk livestreaming voor apps, videogesprekken voor apps, of de prijzen voor apps. Vragen over je integratie? Schrijf ons.