Blog

Comment créer une communauté en ligne avec Next.js

· Andrij Tkachenko · 11 min de lecture

Une fenêtre de navigateur d’une communauté en ligne : une barre latérale avec des canaux, un salon vocal, un salon vidéo et des événements, au centre une scène en direct avec deux personnes, un cœur et une main levée en dessous, et à côté une colonne de messages, dont deux photos

Un forum garde ce que ses membres ont écrit. Une communauté qui paraît vivante a davantage de choses en même temps. Les membres partagent une photo et reçoivent une réponse tant qu’ils sont encore là, entrent dans un salon pour parler, à la voix ou à la caméra, regardent les événements du club en direct et montent sur scène avec leur propre travail, et voient qui est là en ce moment. Dans une application Next.js, chacune de ces choses est une connexion qui reste ouverte, avec beaucoup de monde à la fois.

Cet article passe en revue chaque partie dont une vraie application de communauté a besoin, avec un exemple complet qui fonctionne : Community, le club photo de notre guide Next.js, avec ses canaux, ses salons où passer un moment et ses événements en direct. Vous pouvez l’essayer en ligne : soyez Mia, qui anime le club et dont la revue de portfolio commence maintenant, ouvrez le studio et passez en direct. Ouvrez ensuite le club dans une fenêtre privée en tant qu’Ava, regardez la revue, levez la main et montez sur scène.

Ce que demande une communauté en ligne

  • Des canaux qui suivent. Les messages et les photos arrivent tout de suite chez tous ceux qui lisent, avec l’indication de qui écrit et les réactions, et restent gardés pour ceux qui viennent plus tard. Les photos ne s’ouvrent que pour les membres.
  • Des salons où passer. Des salons vocaux, et des salons avec caméras et écrans, où les membres entrent et sortent comme ils veulent, beaucoup de monde à la fois grâce à des serveurs média (comment fonctionnent les serveurs média). Des hôtes qui peuvent couper le micro de quelqu’un ou le retirer.
  • Des événements en direct. Un hôte pour tous les membres, avec un chat et des réactions à côté de l’image, et un membre invité sur scène pour montrer son travail (comment fonctionne la diffusion en direct dans Next.js).
  • Ce qui se passe maintenant. Qui est dans quel salon et quel événement est en direct, à jour sur chaque page, sans recharger.
  • Qui est là. Qui est en ligne, et un moyen de lui écrire ou de l’appeler.

Tout cela repose sur des connexions qui restent ouvertes tant qu’un membre est sur le site, ce qu’une fonction serverless ne peut pas tenir (pourquoi, et que faire). Vous pouvez construire chacune de ces parties vous-même, sur des websockets, WebRTC et vos propres serveurs média. Ou vous laissez un cloud temps réel s’occuper des connexions, des appels et des diffusions, pendant que votre serveur garde ce qui vous appartient vraiment : les membres, les règles et les données. C’est ce que fait le club, avec WordPlus Cloud.

L’exemple : un club photo

Les membres partagent des photos dans des canaux pour avoir des retours, comme #critique et #trips. Ils passent dans ses salons : Coffee lounge à la voix, Editing hangout avec caméras et écrans. Les hôtes animent des événements en direct, une revue de portfolio ou un cours de photo de nuit, où des membres montent sur scène. C’est une application Next.js avec l’App Router, Better Auth et Postgres, et sa partie temps réel tient en quelques fichiers. Cinq étapes la construisent.

1. Des canaux avec photos, gardés sur votre serveur

Chaque canal est un salon nommé d’après son numéro : channel:2. Le site les déclare une seule fois, par ce motif, avec la règle de qui peut y être :

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));

Tout membre peut être dans un canal, et personne d’autre. Les messages, l’indication de qui écrit, les réactions et les accusés de lecture passent de page en page par WordPlus Cloud, scellés, et les webhooks message et reaction apportent chacun d’eux au serveur du site, qui le garde dans sa propre base de données. La page du canal est le chat tout prêt sur ce salon :

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 est la route du site pour les messages plus anciens, chargés à mesure qu’un membre remonte la conversation. upload donne au champ de saisie son trombone, et prend aussi les photos collées ou déposées sur le chat : chacune part vers la route du site dès qu’elle est choisie, et le message ne porte que l’endroit où elle s’ouvre. WordPlus Cloud ne voit jamais la photo. La route décide avec la règle du salon lui-même :

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() applique la même règle que pour entrer dans le salon : qui peut lire un canal peut y partager une photo, et personne d’autre. Chaque photo s’ouvre à une adresse à elle, qui vérifie de nouveau qui l’ouvre : quelqu’un d’extérieur au club n’obtient rien, quelle que soit l’adresse qu’il a.

2. Des salons vocaux dont les hôtes prennent soin

Un salon est un appel de groupe nommé d’après son numéro, lounge:3, où tout membre peut passer. La règle fait des hôtes du club ses hôtes :

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

Le salon s’affiche dans sa propre page, avec le bouton pour y entrer à l’intérieur :

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 vaut audio pour un salon vocal et video pour un salon avec caméras. Avant qu’un membre n’entre, CallSpot montre ce qu’il contient : qui est dans le salon en ce moment, et le bouton. Une fois dedans, l’appel prend sa place : l’image de chacun, ou leurs caméras et leurs écrans, et les commandes. Un membre entre avec le micro et la caméra coupés et les allume depuis l’appel, si bien que passer dans un salon n’ouvre jamais un micro par surprise. Sur une autre page du site, le salon continue dans une fenêtre dans le coin, et revient quand sa page s’ouvre de nouveau.

admin: true est tout ce qu’il faut aux hôtes. Sur l’écran d’un hôte, chaque vignette des autres a couper le micro et retirer, que l’écran d’appel demande par la même route, qui ne les accepte que d’un hôte.

3. Des événements en direct, avec des membres sur scène

Un événement est une diffusion en direct nommée d’après son numéro : event:5. Sa règle dit qui est qui : l’hôte de l’événement passe en direct, et tout autre membre regarde :

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 },
);

L’hôte passe en direct depuis un studio à lui, une page du site avec l’image en grand, Go live en dessous, et à côté le chat et les mains levées :

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}`}
/>

La page de l’événement donne à chaque membre le lecteur avec son chat, le même <LiveStream> sous le même nom. Un hôte peut aussi passer en direct depuis OBS, avec l’adresse et la clé que donne le studio. stage: true, c’est la scène : un membre lève la main depuis le lecteur, l’hôte la voit dans Guests et l’invite, et dès qu’il appuie sur Join stage, sa caméra et son micro passent en direct à côté de l’hôte, pour tous ceux qui regardent.

4. Ce qui se passe maintenant, sur chaque page

À côté de chaque page, une barre latérale montre qui est dans chaque salon et quel événement est en direct, avec combien le regardent. Le site ne garde aucune table de tout cela. WordPlus Cloud le sait :

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() et realtime.call(name).get() disent la même chose des salons et de qui est dans chacun. Pour que tout reste à jour, WordPlus Cloud envoie au site un webhook chaque fois que quelqu’un entre dans un salon ou en sort, ou qu’un événement commence ou se termine. Le site le dit dans un salon que rejoint chaque page d’un membre, et quand un événement passe en direct, il le dit à chaque membre :

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 });
});

Dans le layout, chaque page entend les deux :

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 });
});

Le premier fait que chaque page se redemande au serveur, sans recharger, si bien que la barre latérale et la page ouverte restent à jour. Le second affiche un avis sur la page où se trouve un membre : « Mia is live: Live portfolio review », qui ouvre l’événement. group.joined, group.left et stream.ended disent changed dans le même salon.

5. Qui est en ligne

WordPlus Cloud sait qui est en ligne : chaque page dont le site s’est porté garant du membre met ce membre en ligne, et la page apprend qui arrive et qui part, sans aucune requête au serveur du 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]);

La liste des membres met d’abord ceux qui sont en ligne, chacun avec Message et Call, et à côté de chaque page le layout liste le club de la même façon, avec les hôtes signalés. La page d’un membre dit s’il est en ligne, ou quand il a été vu pour la dernière fois.

Ce qui est inclus

  • Les conversations à deux. La page d’un membre contient votre conversation avec cette personne, photos comprises, et son appel s’affiche à côté des messages.
  • Des réactions et un chat à chaque événement, à côté de l’image, et le passage en direct depuis OBS ou tout encodeur qui envoie en WHIP.
  • Le partage d’écran dans un salon avec caméras, depuis les commandes de l’appel.
  • Les noms et les visages de votre application. Chaque vignette, message et scène montre le nom et la photo d’un membre tels que les donne le profile du site, sans aucune liste d’utilisateurs gardée dans WordPlus Cloud.
  • Retirer l’accès. Quand quelqu’un quitte le club, les règles disent non à partir de là, et realtime.call(name).remove(user) le retire tout de suite d’un salon.

Essayez, puis construisez le vôtre

Sur la démo en ligne, le club est rien qu’à vous pendant une journée, si bien que personne d’autre ne voit ce que vous y partagez ni qui vous y retrouvez, et un événement ou un salon se termine de lui-même au bout de 10 minutes. Sous chaque page, How this page works montre le code qui la fait fonctionner, étape par étape, avec les valeurs de la page que vous avez ouverte.

Le guide du site de communauté construit tout le site, avec chaque fichier de sa partie temps réel en entier. Les diffusions en direct dans Next.js et les appels dans Next.js présentent toutes les façons de mettre une diffusion ou un appel dans votre propre application. Pour une équipe plutôt qu’un club, la visioconférence dans Next.js construit les réunions d’un intranet d’équipe.

WordPlus Cloud for Apps a un prix selon les connexions simultanées, à partir de 1 000, facturé au mois ou à l’année. Avec 1 000 connexions, les diffusions en direct ne sont pas décomptées jusqu’à 50 spectateurs à la fois, ni les appels de groupe jusqu’à 32 personnes en vidéo à la fois, une personne en audio comptant pour moitié. Chaque tranche de 1 000 connexions en plus ajoute 20 spectateurs et 12 personnes en vidéo. Au-delà, chaque spectateur ou personne en vidéo de plus prend un crédit par minute, une diffusion comptée seulement pendant qu’elle est en direct, et les appels entre deux personnes n’en prennent aucun. Découvrez le live streaming pour les applications, les appels vidéo pour les applications, ou consultez les tarifs pour les applications. Des questions sur votre intégration ? Écrivez-nous.