aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Discord Bots

Discord Welcome Bot: Personalized Canvas Image

A Discord welcome bot automatically greets every new member who joins your server, but it does more than send a plain "welcome" line: it generates a personalized image featuring the member's avatar and name. A custom graphic makes a far stronger first impression than a one-line text message. In this tutorial we'll build a working bot from scratch using discord.js and @napi-rs/canvas.

Required packages and project setup

You'll need Node.js 18 or newer. In an empty folder, initialize the project and install two main packages: discord.js to talk to Discord, and @napi-rs/canvas to draw the image. I prefer the latter because, unlike the older node-canvas, it doesn't require compiling system libraries (Cairo) — it installs cleanly on most operating systems and VPS setups.

npm init -y
npm install discord.js @napi-rs/canvas

Create your bot in the Discord Developer Portal, grab the token from the "Bot" tab, and under Privileged Gateway Intents make sure you enable the SERVER MEMBERS INTENT toggle. Without it, the bot never sees the member-join event.

Defining the client and its intents

When you create the client in discord.js v14, you declare which events you want to receive via intents. To catch new members, the GuildMembers intent is mandatory.

const { Client, GatewayIntentBits, Events, AttachmentBuilder } = require('discord.js');

const client = new Client({
  intents: [
    GatewayIntentBits.Guilds,
    GatewayIntentBits.GuildMembers,
  ],
});

client.once(Events.ClientReady, (c) => {
  console.log(`Logged in as ${c.user.tag}`);
});

client.login(process.env.DISCORD_TOKEN);

Keeping the token in an environment variable (process.env.DISCORD_TOKEN) instead of hardcoding it matters for security; if the token leaks, someone else gains control of your bot.

Catching the new member: guildMemberAdd

When someone joins, the GuildMemberAdd event fires. Here we generate the image and send it to a suitable channel. Finding the channel by ID is more reliable than by name, but I'll show the name approach for the example.

client.on(Events.GuildMemberAdd, async (member) => {
  const channel = member.guild.channels.cache.find(
    (c) => c.name === 'general'
  );
  if (!channel) return;

  const attachment = await createWelcomeImage(member);

  await channel.send({
    content: `Welcome aboard <@${member.id}>! 🎉`,
    files: [attachment],
  });
});

The <@${member.id}> expression mentions the user in the message so they get a notification. We'll write the createWelcomeImage function that produces the graphic next.

Drawing the welcome image with Canvas

This is where the magic happens. We create a canvas and draw the background, the avatar clipped into a circle, and the username on top. To make the avatar round, we use a clip mask.

const { createCanvas, loadImage } = require('@napi-rs/canvas');

async function createWelcomeImage(member) {
  const canvas = createCanvas(1024, 450);
  const ctx = canvas.getContext('2d');

  // Background
  const background = await loadImage('./assets/background.png');
  ctx.drawImage(background, 0, 0, canvas.width, canvas.height);

  // Draw the avatar as a circle
  const avatarURL = member.user.displayAvatarURL({
    extension: 'png',
    size: 256,
  });
  const avatar = await loadImage(avatarURL);

  ctx.save();
  ctx.beginPath();
  ctx.arc(512, 150, 100, 0, Math.PI * 2);
  ctx.closePath();
  ctx.clip();
  ctx.drawImage(avatar, 412, 50, 200, 200);
  ctx.restore();

  // Text
  ctx.textAlign = 'center';
  ctx.fillStyle = '#ffffff';
  ctx.font = 'bold 52px Sans';
  ctx.fillText('Welcome!', 512, 320);

  ctx.font = '36px Sans';
  ctx.fillText(member.user.username, 512, 375);

  const buffer = await canvas.encode('png');
  return new AttachmentBuilder(buffer, { name: 'welcome.png' });
}

Here we requested the avatar extension as png; the default WebP can sometimes trip up loadImage. canvas.encode('png') returns a buffer, which we pass straight into AttachmentBuilder to upload to Discord.

Using a custom font

The default "Sans" font works, but if you want one that matches your brand you can register a .ttf file. Do this once at the top of the file, before the client starts.

const { GlobalFonts } = require('@napi-rs/canvas');

GlobalFonts.registerFromPath('./assets/Poppins-Bold.ttf', 'Poppins');
// Then:  ctx.font = 'bold 52px Poppins';

Long usernames can spill off the edge of the image. A practical fix is to shrink the font size when the name exceeds a certain length, or truncate it and append .

Common mistakes and tips

  • Event never fires: Usually the SERVER MEMBERS INTENT isn't enabled in the portal, or the GuildMembers intent is missing from your code.
  • Avatar won't load: Request the extension as png, and make sure the bot has internet access.
  • Channel not found: Use the channel ID instead of the name (channels.cache.get('CHANNEL_ID')) — names can change.
  • Going live: To keep the bot online 24/7, run it on a VPS with a process manager such as pm2.

Frequently Asked Questions

Do I really need hosting for this bot?

The bot must stay connected to Discord to work, so it stops when your computer shuts down. A cheap VPS is enough to keep it permanently online; with pm2 it even restarts automatically if it crashes.

Why @napi-rs/canvas instead of node-canvas?

Their APIs are nearly identical, but @napi-rs/canvas ships precompiled binaries, so you don't have to install system dependencies like Cairo. That makes setup much easier, especially on shared hosting and across different operating systems.

Can I add which member number they are to the image?

Yes. Use member.guild.memberCount to get the current member count and draw it as text like "You're our 250th member!". With the same ctx.fillText approach you can add as much dynamic info as you like.

Want a custom welcome bot for your community? From the visual design to an auto-role system, I can build a Discord bot tailored to your needs. Get in touch and let's talk about your project.

Devamı için