<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>PluginAnalytics guides</title>
<link>https://pluginanalytics.dev/blog/</link>
<atom:link href="https://pluginanalytics.dev/blog/rss.xml" rel="self" type="application/rss+xml"/>
<description>Guides for Minecraft plugin developers.</description>
<language>en</language>
<item>
<title>How to get more downloads for your Minecraft plugin on SpigotMC</title>
<link>https://pluginanalytics.dev/blog/get-more-downloads-spigotmc/</link>
<guid>https://pluginanalytics.dev/blog/get-more-downloads-spigotmc/</guid>
<pubDate>Fri, 09 Oct 2026 09:00:00 GMT</pubDate>
<category>Growth</category>
<description><![CDATA[<p>Most plugins don't fail because they're bad. They fail because a server owner scrolled past them in four seconds. Getting more downloads is mostly about those four seconds: what the listing says before anyone clicks, what the first screen of the page shows, and whether the plugin looks alive.</p>
<p>This guide goes through it in the order a server owner sees it. Everything here works for free and paid resources alike.</p>
<h2 id="start-with-the-problem-not-the-name">Start with the problem, not the name</h2>
<p>On SpigotMC's resource list, a server owner sees three things: your icon, your title and your <strong>tag line</strong>. The tag line is the sentence that sells the click, and most plugins waste it on &quot;A simple plugin that adds…&quot;.</p>
<p>Write it as the problem it solves, with the words people search for:</p>
<div class="frame" style="overflow-x:auto"><table class="table"><thead><tr><th>Weak tag line</th><th>Strong tag line</th></tr></thead><tbody><tr><td>A cool economy plugin with many features</td><td>Shops, auctions and bank accounts for Paper servers, with a GUI and no database to set up</td></tr><tr><td>Adds custom mobs</td><td>40 new mobs with animated models and no client mods: works on Spigot, Paper and Folia</td></tr><tr><td>Minigame plugin</td><td>Bed Wars with parties, cosmetics and per-arena stats, ready in five minutes</td></tr></tbody></table></div>
<p>A few rules that hold up:</p>
<ul><li>Put the <strong>platforms and versions</strong> in it if they're a selling point (&quot;1.8 to 1.21&quot;, &quot;Folia support&quot;). Server owners filter mentally by version before anything else.</li><li>Use the <strong>words a server owner would type</strong> into search: &quot;economy&quot;, &quot;shops&quot;, &quot;minigame&quot;, &quot;anticheat&quot;. SpigotMC's search and Google both read the title and tag line.</li><li>Keep the title short and memorable. If your plugin's name doesn't say what it does, the tag line has to.</li></ul>
<p>The icon matters more than it should. A clean, readable icon at small size (a single shape, strong contrast, no tiny text) gets more clicks than a screenshot squeezed into a square.</p>
<h2 id="the-first-screen-decides">The first screen decides</h2>
<p>Once someone opens your page, the part above the fold has to answer three questions: <strong>what does it do, does it work on my server, and does it look good?</strong></p>
<p>What works:</p>
<ol><li><strong>A banner or a hero image</strong> that shows the plugin in action, not just its logo.</li><li><strong>One paragraph</strong> saying what it does and for whom, in plain words.</li><li><strong>Compatibility in bold</strong>: server software, Minecraft versions, dependencies (or &quot;no dependencies&quot;). Owners running Paper 1.21 want to know in one glance that they're covered.</li><li><strong>A GIF or short video</strong> of the core feature.</li></ol>
<p>What doesn't: a wall of BBCode-coloured text, a changelog at the top, or a list of 40 commands before anyone knows why they'd want the plugin. Commands and permissions belong further down, or in the Documentation tab.</p>
<p>If you want a structure to copy, we wrote a <a href="https://pluginanalytics.dev/blog/spigotmc-resource-page-template/">free SpigotMC resource page template</a> with the BBCode ready.</p>
<h2 id="make-your-images-and-gifs-actually-load">Make your images and GIFs actually load</h2>
<p>SpigotMC doesn't host the images in your description. You link them from somewhere like Imgur, and SpigotMC serves them through its own image proxy. Two things go wrong often:</p>
<ul><li><strong>Big GIFs don't show.</strong> In our experience the proxy stops serving GIFs above roughly 3 MB: the visitor sees a broken image right where your best feature should be. Keep GIFs short (5 to 8 seconds), small (around 600 px wide) and under that size. Convert longer clips to a YouTube video and embed it instead.</li><li><strong>Huge PNGs load slowly.</strong> Export screenshots as optimised PNG or JPEG. A description made of a few tall &quot;poster&quot; images, one per section, looks polished and loads fast if each one stays well under a megabyte.</li></ul>
<p>Also: SpigotMC strips 4-byte emojis from descriptions, so 🚀 and friends simply vanish. Use plain symbols like ★ or ✔, or none.</p>
<h2 id="look-alive-updates-replies-and-reviews">Look alive: updates, replies and reviews</h2>
<p>A plugin with a last update two years ago reads as abandoned, even if it works perfectly. Server owners know that a dead plugin breaks on the next Minecraft version.</p>
<ul><li><strong>Update with every Minecraft release</strong>, even when it's only &quot;Tested on 1.21.x, no changes needed&quot;. Updated resources show up again in SpigotMC's &quot;latest updates&quot;, and the date on your page stays recent.</li><li><strong>Write useful update notes.</strong> &quot;Fixed bugs&quot; tells nobody anything. &quot;Fixed shops losing items on restart with MySQL&quot; tells a server owner you care about their data.</li><li><strong>Answer the discussion tab and reviews.</strong> A reply under a one-star review (&quot;fixed in 2.3.1, thanks for the report&quot;) often turns the review around, and everyone reading it sees a maintained plugin.</li><li><strong>Ask for reviews at the right moment.</strong> Not on every join, not in a nagging broadcast. A good moment is after an admin has used the plugin for a few days: a single, polite message to admins with a link to the review page works far better than begging in the description.</li></ul>
<h2 id="be-where-server-owners-look">Be where server owners look</h2>
<p>SpigotMC is the biggest, but it's not the only place people find plugins:</p>
<ul><li><strong>Modrinth</strong> has a growing share of Paper and Purpur users, a clean search with filters by loader and version, and a gallery. Its short summary works like SpigotMC's tag line: write it the same way.</li><li><strong>Hangar</strong> is PaperMC's own repository, and some Paper users start there.</li><li><strong>Your own Discord</strong> turns downloads into a community: support in one place, update announcements, and people who'll tell you what to build next.</li><li><strong>YouTube showcases</strong> still drive installs. Many small channels review plugins; a 3-minute video often brings more downloads than a month of forum posts.</li></ul>
<p>Cross-listing is cheap: the same jar, the same images, a description adapted to each site's format.</p>
<h2 id="measure-the-whole-path-not-just-downloads">Measure the whole path, not just downloads</h2>
<p>Downloads are the only number SpigotMC gives you, and it's the least useful one. It doesn't tell you how many people saw your page and left, or how many downloads ended up running on a server, or how many servers kept the plugin a week later.</p>
<p>The path that matters looks like this:</p>
<p><strong>listing views → downloads → servers that install it → servers still running it after a week</strong></p>
<p>Each step tells you something different:</p>
<div class="frame" style="overflow-x:auto"><table class="table"><thead><tr><th>If this step is weak</th><th>It usually means</th></tr></thead><tbody><tr><td>Views → downloads</td><td>The first screen doesn't sell: tag line, images, compatibility</td></tr><tr><td>Downloads → installs</td><td>Something stops people at setup: a dependency, a confusing config, a crash on start</td></tr><tr><td>Installs → still running after a week</td><td>The plugin doesn't deliver what the page promised, or a bug drives people away</td></tr></tbody></table></div>
<p>SpigotMC doesn't show authors how many people view their page, but you can measure it with a tracking image in your description. <a href="https://pluginanalytics.dev/docs/listing/">PluginAnalytics</a> does exactly this: an invisible pixel or a live badge on your SpigotMC and Modrinth pages counts views, downloads are read from both sites, and the SDK in your plugin counts the servers that install it and keep it. You see the whole funnel in one chart, and you see whether the new GIF you added last Tuesday changed anything.</p>
<p>Whatever you use, change one thing at a time and give it a week. That's how you find out what actually brings downloads, instead of guessing.</p>
<h2 id="a-quick-checklist">A quick checklist</h2>
<ul><li>Tag line names the problem and the platforms, in words people search for.</li><li>The first screen shows the plugin in action, says what it does and lists compatibility in bold.</li><li>GIFs under about 3 MB, images optimised, no 4-byte emojis.</li><li>An update for every Minecraft version, with update notes that say something.</li><li>Replies to reviews and questions within a few days.</li><li>Listed on Modrinth and Hangar too.</li><li>Views, downloads, installs and retention measured, one change at a time.</li></ul>]]></description>
</item>
<item>
<title>How to handle bug reports for your Minecraft plugin</title>
<link>https://pluginanalytics.dev/blog/handle-plugin-bug-reports/</link>
<guid>https://pluginanalytics.dev/blog/handle-plugin-bug-reports/</guid>
<pubDate>Fri, 09 Oct 2026 09:00:00 GMT</pubDate>
<category>Product</category>
<description><![CDATA[<p>A bug report is a gift: someone hit a problem and, instead of uninstalling, told you. The trouble is that most reports arrive as &quot;it doesn't work&quot; in a review, with no version and no log. Here's how to get better reports and handle them without drowning.</p>
<h2 id="pick-one-place">Pick one place</h2>
<p>Reports scattered across reviews, the discussion tab, DMs and three Discord channels get lost. Choose one main place and say it everywhere (description, plugin.yml, the error message itself):</p>
<div class="frame" style="overflow-x:auto"><table class="table"><thead><tr><th>Channel</th><th>Good for</th><th>Watch out</th></tr></thead><tbody><tr><td><strong>Discord</strong></td><td>Fast back-and-forth, a community</td><td>Reports scroll away; use a forum channel with tags</td></tr><tr><td><strong>GitHub issues</strong></td><td>Tracking, templates, linking to fixes</td><td>Many server owners don't have an account</td></tr><tr><td><strong>SpigotMC discussion</strong></td><td>Owners are already there</td><td>No structure, hard to track</td></tr><tr><td><strong>In-game</strong></td><td>No friction at all, right when it happens</td><td>Needs a way to collect it</td></tr></tbody></table></div>
<p>Many developers use Discord for conversation and GitHub (or a Discord forum channel) for tracking. Whatever you choose, ask people not to report bugs in reviews: point to the right place in your description and reply to the review with the link.</p>
<h2 id="what-every-report-needs">What every report needs</h2>
<p>Most bugs depend on the environment. A report without these takes three messages to start:</p>
<ul><li><strong>Plugin version</strong> and <strong>server software and version</strong> (Paper 1.21.4, Spigot 1.20.1…).</li><li><strong>Other plugins</strong> that might interact (permissions, economy, protection).</li><li><strong>What they did, what they expected, what happened.</strong></li><li><strong>The error from the console</strong>, in full, through a paste site like mclo.gs, not a screenshot.</li><li><strong>Their config</strong>, if the bug might depend on it.</li></ul>
<h2 id="a-report-template">A report template</h2>
<p>Pin this, or use it as a GitHub issue template:</p>
<pre><code>Plugin version:
Server software and version (e.g. Paper 1.21.4):
Other relevant plugins:

What I did:
What I expected:
What happened instead:

Console error (paste on https://mclo.gs and link it):</code></pre>
<h2 id="triage-without-drowning">Triage without drowning</h2>
<ul><li><strong>Reproduce first.</strong> Same plugin version, same server software, same config. If you can't reproduce it, ask for exactly what's missing.</li><li><strong>Check if it's known.</strong> Many reports are the same bug. Merge them and keep a count: three people hitting it means more than one loud one.</li><li><strong>Ask about the version.</strong> A good share of reports are already fixed in a newer version. A <a href="https://pluginanalytics.dev/blog/spigot-update-checker/">good update checker</a> reduces these a lot.</li><li><strong>Close the loop.</strong> Reply when it's fixed, with the version. People who see their report fixed become your best reviewers.</li></ul>
<h2 id="reports-that-carry-their-own-context">Reports that carry their own context</h2>
<p>The best report is one the owner can send in five seconds, from where the problem happened, that already includes everything above. That's what PluginAnalytics' <a href="https://pluginanalytics.dev/docs/reports/">in-game bug reports and ideas</a> do: the SDK adds <code>/yourplugin report &lt;message&gt;</code> and <code>/yourplugin suggest &lt;message&gt;</code>, and each report arrives in your dashboard with the plugin, Minecraft and server versions, whether it came from a player or an admin, and your plugin's own errors and log lines from the 15 minutes before, with player names, IPs and UUIDs removed. No account, no template, no paste site.</p>
<p>And for the bugs nobody reports at all, <a href="https://pluginanalytics.dev/blog/minecraft-plugin-error-tracking/">error tracking</a> catches them on every server.</p>
<h2 id="summary">Summary</h2>
<ul><li>One main place for reports, said everywhere.</li><li>A template that asks for versions, steps and the full error.</li><li>Reproduce, merge duplicates, check the version, close the loop.</li><li>Make reporting as easy as typing a command, and collect the context automatically.</li></ul>]]></description>
</item>
<item>
<title>How to see how many servers use your Minecraft plugin</title>
<link>https://pluginanalytics.dev/blog/how-many-servers-use-my-plugin/</link>
<guid>https://pluginanalytics.dev/blog/how-many-servers-use-my-plugin/</guid>
<pubDate>Fri, 09 Oct 2026 09:00:00 GMT</pubDate>
<category>Product</category>
<description><![CDATA[<p>Downloads tell you how many times your jar was fetched. They don't tell you how many servers run it: one owner downloads every update, another downloads once and never installs it, a hosting company puts one download on two hundred servers. To know how many servers actually use your plugin, the plugin has to tell you itself.</p>
<p>There are two common ways to do it. Both take a few minutes.</p>
<h2 id="option-1-bstats">Option 1: bStats</h2>
<p><a href="https://bstats.org" rel="noopener">bStats</a> is the classic: free, open source, and used by thousands of plugins. It counts servers and players and shows them on a <strong>public</strong> page, with charts for versions, platforms and countries, plus custom charts you define.</p>
<p>Setup, in short:</p>
<ol><li>Sign in at bstats.org, add your plugin and note its <strong>plugin id</strong> (a number).</li><li>Add the <code>bstats-bukkit</code> dependency from Maven Central and <strong>shade and relocate</strong> it into your jar (bStats checks this; an unrelocated copy refuses to run).</li><li>Start it in <code>onEnable</code>:</li></ol>
<pre><code class="lang-java">@Override
public void onEnable() {
    int pluginId = 12345; // from your bStats page
    Metrics metrics = new Metrics(this, pluginId);
    metrics.addCustomChart(new SimplePie(&quot;storage&quot;, () -&gt; getConfig().getString(&quot;storage.type&quot;)));
}</code></pre>
<p>The <a href="https://bstats.org/docs" rel="noopener">bStats docs</a> have the exact Maven and Gradle snippets. Your charts fill in over the next hours.</p>
<p><strong>Good for:</strong> a free, public server and player count that you and others can see.</p>
<h2 id="option-2-pluginanalytics">Option 2: PluginAnalytics</h2>
<p><a href="https://pluginanalytics.dev/">PluginAnalytics</a> counts the same servers, privately, and adds what happens inside them: which features are used, where players drop off, which errors hurt, and how your SpigotMC listing converts into installs.</p>
<ol><li>Create a project in the <a href="https://pluginanalytics.dev/p">dashboard</a> and copy its key.</li><li>Add the SDK and relocate it (<a href="https://pluginanalytics.dev/docs/install/">Gradle and Maven snippets</a>).</li><li>Start it in <code>onEnable</code>:</li></ol>
<pre><code class="lang-java">@Override
public void onEnable() {
    analytics = PluginAnalytics.start(this, &quot;pa_your_key&quot;);
}</code></pre>
<p>The first data shows up within a minute, and the dashboard updates live. Servers, versions, platforms, countries, players and your config choices are collected without any more code; <a href="https://pluginanalytics.dev/docs/events/">custom events</a> are one line each.</p>
<p><strong>Good for:</strong> private numbers, product questions and error tracking, on top of the count.</p>
<p>You can run both. They don't conflict, and many developers keep bStats for its public page.</p>
<h2 id="why-the-count-doesn-t-match-your-downloads">Why the count doesn't match your downloads</h2>
<p>Expect the number of servers to be much lower than your downloads, and don't panic:</p>
<ul><li><strong>Updates are downloads too.</strong> An owner who installs every update counts once as a server and twenty times as a download.</li><li><strong>Many downloads never get installed.</strong> People download to try, to compare, or for a server they never open.</li><li><strong>Test servers come and go.</strong> A server that ran your plugin once and was deleted still shows in your downloads.</li><li><strong>Owners can opt out.</strong> Both bStats and PluginAnalytics let server owners turn metrics off (bStats in <code>plugins/bStats/config.yml</code>, PluginAnalytics in <code>plugins/PluginAnalytics/config.yml</code>), so a share of servers never reports. That's how it should be: owners trust plugins that respect it.</li><li><strong>Offline networks and firewalls</strong> block outgoing connections, so some servers can't report at all.</li></ul>
<p>The useful number isn't the raw count but its trend, and how it compares with the step before it. If downloads double after an update but servers stay flat, something stops people between the download and a working install.</p>
<h2 id="what-a-server-count-can-t-tell-you">What a server count can't tell you</h2>
<p>A server count answers &quot;how many?&quot;. The questions that decide what you build next are different:</p>
<ul><li><strong>Do they keep it?</strong> How many servers that installed it last month still run it today? A rising count can hide a leaky bucket.</li><li><strong>What do they use?</strong> Which commands and features get used, and which ones nobody touches?</li><li><strong>Where do they get stuck?</strong> How many players start your minigame and how many finish it?</li><li><strong>What breaks?</strong> Which errors happen, on which versions, and did the last update fix them?</li><li><strong>Which config choices are common?</strong> If 90% of servers keep a default, it's a good default. If everyone changes it, it isn't.</li></ul>
<p>That's the difference between metrics and product analytics. bStats is great at the first. PluginAnalytics was built for the rest: <a href="https://pluginanalytics.dev/docs/funnels/">funnels and retention</a>, <a href="https://pluginanalytics.dev/docs/events/">events</a>, <a href="https://pluginanalytics.dev/docs/errors/">errors</a>, <a href="https://pluginanalytics.dev/docs/config/">config usage</a> and <a href="https://pluginanalytics.dev/docs/listing/">listing analytics</a>, from one line in <code>onEnable</code>.</p>
<h2 id="respect-the-server-owner">Respect the server owner</h2>
<p>Whatever you choose:</p>
<ul><li>Say in your resource description that the plugin collects anonymous usage statistics, and how to turn them off.</li><li>Never collect player names, UUIDs, IP addresses or chat. You don't need them to answer any of the questions above.</li><li>Keep it off the main thread and never let it break the plugin if the service is down.</li></ul>
<p>Both tools do this for you. If you build your own, it's the part people forget.</p>]]></description>
</item>
<item>
<title>How to make your Minecraft plugin Folia-compatible</title>
<link>https://pluginanalytics.dev/blog/make-plugin-folia-compatible/</link>
<guid>https://pluginanalytics.dev/blog/make-plugin-folia-compatible/</guid>
<pubDate>Fri, 09 Oct 2026 09:00:00 GMT</pubDate>
<category>Engineering</category>
<description><![CDATA[<p>Folia is PaperMC's fork that splits the world into regions and ticks them in parallel, on several threads. Big servers use it to hold far more players. For plugins, it removes the assumption almost every plugin makes: that there is one main thread.</p>
<p>Supporting it is mostly about scheduling. Here's what changes and how to keep a single jar that runs on Spigot, Paper and Folia.</p>
<h2 id="what-s-different-on-folia">What's different on Folia</h2>
<ul><li><strong>There's no main thread.</strong> Each region of the world ticks on its own thread. The code that touches a block or an entity must run on the thread that owns its region.</li><li><strong><code>BukkitScheduler</code> doesn't work.</strong> <code>Bukkit.getScheduler().runTask(...)</code> and friends throw <code>UnsupportedOperationException</code> on Folia.</li><li><strong>Touching the world from the wrong thread throws.</strong> Reading or changing a block, an entity or a player from another region's thread fails with an exception instead of quietly racing.</li><li><strong>Teleporting is asynchronous.</strong> Use <code>teleportAsync</code>, which returns a future.</li></ul>
<h2 id="declare-support">Declare support</h2>
<p>Folia only loads plugins that say they support it, in plugin.yml:</p>
<pre><code class="lang-yaml">folia-supported: true</code></pre>
<p>Only add it once the plugin really works on Folia: owners trust that flag.</p>
<h2 id="pick-the-right-scheduler">Pick the right scheduler</h2>
<p>Paper's API (1.20 and later) has four schedulers that work on both Paper and Folia. Use the one that matches what the task touches:</p>
<div class="frame" style="overflow-x:auto"><table class="table"><thead><tr><th>The task touches</th><th>Use</th><th>Example</th></tr></thead><tbody><tr><td>One entity or player</td><td><code>entity.getScheduler()</code></td><td>Give a player an item, update their boss bar</td></tr><tr><td>A location or chunk</td><td><code>Bukkit.getRegionScheduler()</code></td><td>Change blocks in an arena, spawn at a location</td></tr><tr><td>Nothing in the world (global state)</td><td><code>Bukkit.getGlobalRegionScheduler()</code></td><td>Day counters, broadcasting, world time</td></tr><tr><td>Nothing in the world, and it's slow</td><td><code>Bukkit.getAsyncScheduler()</code></td><td>Database, HTTP, files</td></tr></tbody></table></div>
<p>For example, a repeating task per player:</p>
<pre><code class="lang-java">player.getScheduler().runAtFixedRate(plugin, task -&gt; {
    if (!player.isOnline()) { task.cancel(); return; }
    updateScoreboard(player);
}, null, 20L, 20L);</code></pre>
<p>And going from async work back to a player:</p>
<pre><code class="lang-java">Bukkit.getAsyncScheduler().runNow(plugin, t -&gt; {
    Stats stats = database.load(player.getUniqueId());
    player.getScheduler().run(plugin, t2 -&gt; apply(player, stats), null);
});</code></pre>
<p>On Paper these run on the main thread as usual, so the same code works on both.</p>
<h2 id="one-jar-for-spigot-paper-and-folia">One jar for Spigot, Paper and Folia</h2>
<p>Spigot doesn't have these schedulers. If you want one jar for everything, detect Folia at startup and route through a small wrapper:</p>
<pre><code class="lang-java">public final class Platform {
    public static final boolean FOLIA = classExists(&quot;io.papermc.paper.threadedregions.RegionizedServer&quot;);

    private static boolean classExists(String name) {
        try { Class.forName(name); return true; } catch (ClassNotFoundException e) { return false; }
    }
}</code></pre>
<p>Then one method per kind of task: on Folia it calls the right Paper scheduler, elsewhere <code>BukkitScheduler</code>. Keep every scheduling call in your plugin going through that wrapper, so a stray <code>Bukkit.getScheduler()</code> can't slip back in. Libraries like FoliaLib do this for you if you'd rather not write it.</p>
<h2 id="common-mistakes">Common mistakes</h2>
<ul><li><strong>Iterating all players from one task</strong> and changing each of them. On Folia, schedule the change on each player's own scheduler.</li><li><strong>Global managers that tick everything</strong>: one task that updates every arena in the world. Split it per region (one region task per arena) or move the computing part to the async scheduler.</li><li><strong>Static caches shared between threads</strong> without synchronisation: what was safe with one main thread now races. Use concurrent collections.</li><li><strong>Assuming <code>Bukkit.isPrimaryThread()</code></strong> means &quot;safe to touch anything&quot;. On Folia it doesn't.</li><li><strong>Libraries.</strong> A dependency that uses <code>BukkitScheduler</code> breaks your plugin on Folia too. Check them, including the analytics or update-checker library you shade. (PluginAnalytics' SDK supports Folia with nothing to configure.)</li></ul>
<h2 id="test-it">Test it</h2>
<p>Run your plugin on a real Folia server with a few players in different areas of the world, and use every feature. Problems show as exceptions in the console, which is good: they're loud. An error tracker that tags errors by platform (like <a href="https://pluginanalytics.dev/docs/errors/">PluginAnalytics</a>) tells you whether an error only happens on Folia servers, which saves a lot of guessing once the plugin is out.</p>]]></description>
</item>
<item>
<title>Error tracking for Minecraft plugins: find the bug before the 1-star review</title>
<link>https://pluginanalytics.dev/blog/minecraft-plugin-error-tracking/</link>
<guid>https://pluginanalytics.dev/blog/minecraft-plugin-error-tracking/</guid>
<pubDate>Fri, 09 Oct 2026 09:00:00 GMT</pubDate>
<category>Engineering</category>
<description><![CDATA[<p>For every server owner who reports a bug, many more just see a red stack trace in their console, sigh, and uninstall your plugin. Some leave a one-star review on the way out. The bug that hurts you most is the one nobody tells you about.</p>
<p>This guide covers where plugin errors end up, why waiting for reports isn't enough, and how to see every error from every server.</p>
<h2 id="where-your-plugin-s-exceptions-go">Where your plugin's exceptions go</h2>
<p>When your code throws on a server, the server catches it and logs it, so the server keeps running. Each kind of entry point has its own message in the console:</p>
<div class="frame" style="overflow-x:auto"><table class="table"><thead><tr><th>Where it happened</th><th>What the console says</th></tr></thead><tbody><tr><td>An event handler</td><td><code>Could not pass event PlayerJoinEvent to YourPlugin v1.2.0</code></td></tr><tr><td>A command</td><td><code>Unhandled exception executing command 'arena' in plugin YourPlugin v1.2.0</code></td></tr><tr><td>A scheduled task</td><td><code>Plugin YourPlugin v1.2.0 generated an exception while executing task 1234</code></td></tr><tr><td>Startup</td><td><code>Error occurred while enabling YourPlugin v1.2.0 (Is it up to date?)</code></td></tr></tbody></table></div>
<p>Followed by the stack trace. The server owner sees it, maybe. You don't, unless they copy it and send it to you.</p>
<h2 id="why-send-me-your-logs-doesn-t-scale">Why &quot;send me your logs&quot; doesn't scale</h2>
<p>Asking for logs works when you have ten users. With a thousand servers it breaks down:</p>
<ul><li><strong>Most owners never report.</strong> Reporting takes effort; uninstalling takes one click.</li><li><strong>Reports arrive late and incomplete.</strong> &quot;It doesn't work&quot; with no version, no server software and a screenshot of half a stack trace.</li><li><strong>You can't tell how big a bug is.</strong> One report could be one server or three hundred.</li><li><strong>You can't tell if your fix worked.</strong> No news after an update could mean fixed, or could mean people gave up.</li></ul>
<h2 id="what-good-error-tracking-does">What good error tracking does</h2>
<p>Whatever tool you use, or if you build it yourself, these are the parts that matter:</p>
<ol><li><strong>Catch errors where they happen.</strong> Exceptions that go through your plugin's code, from event handlers, commands, tasks and async threads, plus the ones you catch yourself and want to know about.</li><li><strong>Group them.</strong> The same bug on 500 servers should be one issue with a count, not 500 alerts. Grouping by the exception type and the top frames of <em>your</em> code (without line numbers, so a small edit doesn't split an issue) works well.</li><li><strong>Keep the context.</strong> Plugin version, server software, Minecraft version, Java version. Most plugin bugs only happen on some combination of these.</li><li><strong>Strip personal data.</strong> Stack trace messages often contain player names, UUIDs and IP addresses (&quot;Could not find player Notch at 1.2.3.4&quot;). Replace them before anything leaves the server.</li><li><strong>Track regressions.</strong> When you mark an issue fixed and it happens again in a newer version, you want to know straight away.</li><li><strong>Never hurt the server.</strong> Capture off the main thread, cap how much is sent, and never throw from the error reporter itself.</li></ol>
<h2 id="report-the-errors-you-catch">Report the errors you catch</h2>
<p>The server only logs exceptions that escape your code. The ones you catch and swallow are invisible:</p>
<pre><code class="lang-java">try {
    database.save(arena);
} catch (SQLException e) {
    getLogger().warning(&quot;Couldn't save arena: &quot; + e.getMessage()); // the stack trace is gone
}</code></pre>
<p>If an error matters enough to log, it usually matters enough to report with its stack trace. Pass the exception to your logger (<code>getLogger().log(Level.WARNING, &quot;Couldn't save arena&quot;, e)</code>) or to your error tracker directly.</p>
<h2 id="options">Options</h2>
<ul><li><strong>Ask for logs</strong> (mclo.gs, pastebin, Discord). Free, works for small plugins, misses most errors.</li><li><strong>A general error tracker</strong> like Sentry, shaded into your plugin. Powerful, but it's built for web apps: you'll need to scrub player data yourself, map Bukkit's thread model, and watch the event quota of the free plan when one bug fires on thousands of servers.</li><li><strong>A tool built for plugins.</strong> <a href="https://pluginanalytics.dev/docs/errors/">PluginAnalytics</a> captures every exception that goes through your plugin on any server with no extra code, groups it into issues, replaces player names, IPs and UUIDs, shows the versions where it was first and last seen, and notifies you of new errors and regressions. It also collects your plugin's own log lines from every server, and for <a href="https://pluginanalytics.dev/docs/obfuscation/">obfuscated plugins</a> it turns stack traces back into your real class names.</li></ul>
<h2 id="fix-the-right-bug-first">Fix the right bug first</h2>
<p>Once errors arrive on their own, prioritise by impact, not by how loud the report was:</p>
<ul><li><strong>How many servers</strong> hit it, not how many times. One server in a loop can log a million errors.</li><li><strong>Which versions.</strong> A bug only on an old version is fixed by asking people to update. A bug that appeared in your latest release is urgent.</li><li><strong>Where in the flow.</strong> An error on startup costs you the install. An error in a rarely used command can wait.</li></ul>
<p>Then ship the fix, mark the issue resolved, and watch: if it comes back in the new version, you'll know before the next review does.</p>]]></description>
</item>
<item>
<title>Which part of your Minecraft plugin causes lag (and how to fix it)</title>
<link>https://pluginanalytics.dev/blog/plugin-lag-which-part/</link>
<guid>https://pluginanalytics.dev/blog/plugin-lag-which-part/</guid>
<pubDate>Fri, 09 Oct 2026 09:00:00 GMT</pubDate>
<category>Engineering</category>
<description><![CDATA[<p>A Minecraft server has 50 milliseconds per tick to do everything: move entities, grow crops, run every plugin. When a tick takes longer, the server falls behind and players feel lag. If your plugin takes 10 ms of those 50, it's using a fifth of the server, and owners notice.</p>
<p>Here's how to find which part of your plugin costs the most, the patterns that usually cause it, and how to fix them.</p>
<h2 id="measure-first-mspt-not-tps">Measure first: MSPT, not TPS</h2>
<p><strong>TPS</strong> (ticks per second) stays at 20 until the server is already overloaded, so it hides problems. <strong>MSPT</strong> (milliseconds per tick) shows how much of the 50 ms budget is used, long before TPS drops.</p>
<p>On Paper, <code>/mspt</code> shows it. A healthy server sits well under 50; if it's at 45 with your plugin and 25 without, you've found a suspect.</p>
<h2 id="profile-with-spark">Profile with spark</h2>
<p><a href="https://spark.lucko.me" rel="noopener">spark</a> is the standard profiler for Minecraft servers, and it ships with recent Paper builds. Run it while the lag happens:</p>
<pre><code>/spark profiler start
... reproduce the lag for a minute or two ...
/spark profiler stop</code></pre>
<p>You get a link to a call tree of the main thread. Open it, search for your plugin's package, and expand the heaviest branches. The method at the bottom of a thick branch is where the time goes.</p>
<p>Useful options:</p>
<ul><li><code>--only-ticks-over 50</code> records only the slow ticks, so a lag spike isn't diluted by normal ticks.</li><li><code>--thread *</code> includes other threads, if you suspect your async tasks.</li></ul>
<p>spark profiles one server at a time, the one you're on. It's the right tool to fix a lag you can reproduce.</p>
<h2 id="the-usual-suspects">The usual suspects</h2>
<p>Most plugin lag comes from a handful of patterns.</p>
<p><strong>Heavy work in <code>PlayerMoveEvent</code>.</strong> It fires many times per second per player, including when they only turn their head. Bail out early when the block didn't change:</p>
<pre><code class="lang-java">@EventHandler
public void onMove(PlayerMoveEvent e) {
    Location from = e.getFrom(), to = e.getTo();
    if (to == null || (from.getBlockX() == to.getBlockX() &amp;&amp; from.getBlockY() == to.getBlockY() &amp;&amp; from.getBlockZ() == to.getBlockZ())) return;
    // the expensive part, now ~20x less often
}</code></pre>
<p><strong>Loops over everything, every tick.</strong> Iterating all online players, all entities or all loaded chunks every tick adds up fast. Run it every 10 or 20 ticks if it doesn't need to be instant, or keep a set of only the things you care about.</p>
<p><strong>Database and file I/O on the main thread.</strong> A MySQL query that takes 30 ms on a remote database is more than half a tick. Do I/O asynchronously and come back to the main thread with the result:</p>
<pre><code class="lang-java">Bukkit.getScheduler().runTaskAsynchronously(plugin, () -&gt; {
    Stats stats = database.load(uuid);            // off the main thread
    Bukkit.getScheduler().runTask(plugin, () -&gt; apply(player, stats)); // back on it
});</code></pre>
<p><strong>Loading chunks by accident.</strong> <code>world.getBlockAt(x, y, z)</code> or <code>getChunkAt</code> on an unloaded chunk loads (or generates) it synchronously. Check <code>world.isChunkLoaded(cx, cz)</code> first, or use Paper's async chunk API.</p>
<p><strong>Updating scoreboards, holograms or boss bars every tick.</strong> Players can't read text that changes 20 times a second. Update every second, and only when the value changed.</p>
<p><strong>Recomputing what could be cached.</strong> Parsing config values, building item stacks or compiling regexes on every call. Do it once on load and keep the result.</p>
<h2 id="the-problem-with-profiling-one-server">The problem with profiling one server</h2>
<p>spark tells you what's slow on the server you're looking at. But your plugin runs on hundreds of servers with different player counts, worlds and configs, and the lag a server owner complains about may only happen with their setup. A few questions spark can't answer:</p>
<ul><li>How much of a tick does my plugin use <strong>on average, across all servers</strong>?</li><li>Which of my methods costs the most <strong>in real use</strong>, not in my test world?</li><li>Did <strong>my last release</strong> make it slower?</li></ul>
<p>That's what <a href="https://pluginanalytics.dev/docs/performance/">PluginAnalytics' performance view</a> is for. The SDK samples the main thread about ten times a second on every server and records time spent in your plugin's code, by method, and times your event handlers and commands. The dashboard shows the milliseconds per tick your plugin uses, its most expensive methods, and a comparison between versions, so a slow release stands out and notifies you. It's the same idea as spark, aggregated across all the servers running your plugin, with nothing to install for the server owner.</p>
<p>Use both: the aggregate view tells you <em>what</em> to look at, spark on a test server tells you <em>why</em>.</p>
<h2 id="a-quick-checklist">A quick checklist</h2>
<ul><li>Measure MSPT, not TPS.</li><li>Profile the lag with spark while it happens, and search for your package.</li><li>Return early in move events; throttle per-tick loops.</li><li>No database or file I/O on the main thread.</li><li>Don't load chunks by accident.</li><li>Update visual things once a second, not every tick.</li><li>Cache what doesn't change.</li><li>Compare versions: make sure each release isn't slower than the last.</li></ul>]]></description>
</item>
<item>
<title>Obfuscating a Minecraft plugin with ProGuard and Gradle (without breaking stack traces)</title>
<link>https://pluginanalytics.dev/blog/proguard-minecraft-plugin-gradle/</link>
<guid>https://pluginanalytics.dev/blog/proguard-minecraft-plugin-gradle/</guid>
<pubDate>Fri, 09 Oct 2026 09:00:00 GMT</pubDate>
<category>Engineering</category>
<description><![CDATA[<p>Premium plugins get leaked, decompiled and re-uploaded. Obfuscation doesn't make that impossible, but it makes reading and modifying your code much harder, and it shrinks the jar as a bonus. The cost: stack traces from servers become <code>at a.b.c(SourceFile:516)</code>, unless you plan for it.</p>
<p>This is the setup we use for our own plugins: Gradle with the shadow plugin, then ProGuard on the shaded jar.</p>
<h2 id="the-gradle-task">The Gradle task</h2>
<p>ProGuard runs on the jar shadow builds, with your libraries already inside and relocated. Add the ProGuard Gradle plugin to the build script's classpath:</p>
<pre><code class="lang-kotlin">// build.gradle.kts
buildscript {
    repositories { mavenCentral() }
    dependencies { classpath(&quot;com.guardsquare:proguard-gradle:7.10.0&quot;) }
}</code></pre>
<p>Then a task that reads the shadow jar and writes the obfuscated one:</p>
<pre><code class="lang-kotlin">val proguardJar by tasks.registering(proguard.gradle.ProGuardTask::class) {
    dependsOn(tasks.shadowJar)
    configuration(&quot;proguard.pro&quot;)
    injars(tasks.shadowJar.flatMap { it.archiveFile })
    outjars(layout.buildDirectory.file(&quot;libs/${project.name}-${project.version}-obfuscated.jar&quot;))
    // The JDK and the server API: ProGuard needs them to understand your code, and their names stay.
    val jdk = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(17) }.get()
        .metadata.installationPath.dir(&quot;jmods&quot;).asFile
    for (module in listOf(&quot;java.base&quot;, &quot;java.logging&quot;, &quot;java.sql&quot;, &quot;java.desktop&quot;)) {
        libraryjars(mapOf(&quot;jarfilter&quot; to &quot;!**.jar&quot;, &quot;filter&quot; to &quot;!module-info.class&quot;), File(jdk, &quot;$module.jmod&quot;))
    }
    libraryjars(configurations.compileClasspath)
    printmapping(layout.projectDirectory.file(&quot;mappings/${project.version}.txt&quot;))
}</code></pre>
<p><code>libraryjars(configurations.compileClasspath)</code> adds the Spigot or Paper API (your <code>compileOnly</code> dependencies) as a library: it's on the server, not in your jar, so ProGuard must not rename calls into it. Add any other JDK module your code uses to the list.</p>
<h2 id="the-rules-bukkit-needs">The rules Bukkit needs</h2>
<p>Bukkit finds parts of your plugin by name or by reflection. Those must survive obfuscation:</p>
<pre><code># proguard.pro
-dontoptimize
-dontnote
-dontwarn

# Annotations (event handlers), generics, inner classes; line numbers for stack traces.
-keepattributes *Annotation*,Signature,InnerClasses,EnclosingMethod,Record,Exceptions,SourceFile,LineNumberTable
-renamesourcefileattribute SourceFile

# The main class named in plugin.yml.
-keep class com.example.myplugin.MyPlugin

# Bukkit calls @EventHandler methods by reflection: keep them (their names can change).
-keepclassmembers,allowobfuscation class * {
    @org.bukkit.event.EventHandler &lt;methods&gt;;
}

# Enum.valueOf needs values() and valueOf().
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}</code></pre>
<p>Why <code>-dontoptimize</code>? Optimisation rewrites your code, and the code that runs on servers is then not quite the code you tested. Renaming and shrinking give most of the protection with none of that risk.</p>
<p>Other things to keep, if you use them:</p>
<ul><li><strong>ConfigurationSerializable classes</strong>: Bukkit calls their static <code>deserialize</code> or <code>valueOf</code> and the constructor that takes a <code>Map</code>. Keep those members.</li><li><strong>Anything you load by name</strong>: <code>Class.forName(&quot;...&quot;)</code>, reflection on your own classes, classes listed in config files.</li><li><strong>Shaded libraries that use reflection</strong>, like bStats or Gson models: keep their packages, or the classes Gson serialises.</li><li><strong>Your analytics SDK</strong>: libraries that report your plugin's own class names (like PluginAnalytics) should stay readable, so keep their relocated package: <code>-keep class com.example.myplugin.libs.analytics.** { *; }</code>.</li></ul>
<h2 id="test-the-obfuscated-jar-not-the-normal-one">Test the obfuscated jar, not the normal one</h2>
<p>Run the obfuscated jar on a real server before every release. Most ProGuard problems show up at startup (<code>NoSuchMethodError</code>, <code>ClassNotFoundException</code>) or the first time a feature uses reflection. A quick checklist: the plugin enables, commands work, a listener fires, config loads, data saves and loads.</p>
<h2 id="keep-stack-traces-readable">Keep stack traces readable</h2>
<p><code>printmapping</code> writes a file mapping every new name back to the original, a different one for every build. <strong>Keep the mapping of every version you release</strong>: commit it, or store it next to the release. Without it, a stack trace from that version can't be read.</p>
<p>To read a stack trace by hand, ProGuard ships <code>retrace</code>:</p>
<pre><code>retrace mappings/1.4.0.txt stacktrace.txt</code></pre>
<p>That works when a server owner sends you a trace. For errors you never hear about, <a href="https://pluginanalytics.dev/docs/obfuscation/">PluginAnalytics</a> applies the mapping for you: upload each version's mapping from your build, and the errors, performance data and bug reports from every server show your real class and method names, including data that arrived before you uploaded it. It's one HTTP request after ProGuard:</p>
<pre><code class="lang-kotlin">val uploadMapping by tasks.registering {
    dependsOn(proguardJar)
    doLast {
        val token = providers.gradleProperty(&quot;pluginanalytics.token&quot;).orNull ?: return@doLast
        val c = java.net.URI(&quot;https://pluginanalytics.dev/api/v1/mappings?version=${project.version}&quot;).toURL()
            .openConnection() as java.net.HttpURLConnection
        c.requestMethod = &quot;POST&quot;
        c.doOutput = true
        c.setRequestProperty(&quot;Authorization&quot;, &quot;Bearer $token&quot;)
        c.outputStream.use { it.write(file(&quot;mappings/${project.version}.txt&quot;).readBytes()) }
        check(c.responseCode == 200) { &quot;Mapping upload failed: ${c.responseCode}&quot; }
    }
}</code></pre>
<p>The version must match the <code>version</code> in your plugin.yml, which is what servers report.</p>
<h2 id="summary">Summary</h2>
<ul><li>Shadow first, ProGuard on the shaded jar, the server API as a library jar.</li><li>Keep the main class, <code>@EventHandler</code> methods, enum methods and anything reached by reflection.</li><li>Rename and shrink; skip optimisation.</li><li>Keep line numbers and the mapping of every release.</li><li>Test the obfuscated jar on a real server before you ship it.</li></ul>]]></description>
</item>
<item>
<title>Update checkers for Spigot plugins, done right</title>
<link>https://pluginanalytics.dev/blog/spigot-update-checker/</link>
<guid>https://pluginanalytics.dev/blog/spigot-update-checker/</guid>
<pubDate>Fri, 09 Oct 2026 09:00:00 GMT</pubDate>
<category>Engineering</category>
<description><![CDATA[<p>Most bugs you've already fixed keep hurting you, because servers keep running the old version. An update checker closes that gap: when you release, admins find out. Done badly, it spams consoles, blocks startup or nags every player. Here's how to do it well.</p>
<h2 id="where-to-read-the-latest-version">Where to read the latest version</h2>
<p><strong>SpigotMC</strong> has a simple endpoint that returns the latest version string of a resource as plain text:</p>
<pre><code>https://api.spigotmc.org/legacy/update.php?resource=12345</code></pre>
<p><code>12345</code> is the number at the end of your resource's URL. The value is the version you typed when you posted the update, so keep those consistent with your plugin.yml.</p>
<p><strong>Modrinth</strong> returns the versions of a project, newest first:</p>
<pre><code>https://api.modrinth.com/v2/project/your-slug/version</code></pre>
<p>Read <code>version_number</code> of the first one. Modrinth asks for a <code>User-Agent</code> that identifies your project.</p>
<p>Both APIs can lag a little behind a release, and both can be down. Your plugin must work the same either way.</p>
<h2 id="check-off-the-main-thread">Check off the main thread</h2>
<p>Never make an HTTP request on the main thread: a slow response freezes the server. Check asynchronously, on startup and then every few hours:</p>
<pre><code class="lang-java">Bukkit.getScheduler().runTaskAsynchronously(plugin, () -&gt; {
    try {
        HttpURLConnection c = (HttpURLConnection) new URL(&quot;https://api.spigotmc.org/legacy/update.php?resource=12345&quot;).openConnection();
        c.setConnectTimeout(5000);
        c.setReadTimeout(5000);
        String latest = new String(c.getInputStream().readAllBytes(), StandardCharsets.UTF_8).trim();
        if (isNewer(latest, plugin.getDescription().getVersion())) this.latest = latest;
    } catch (IOException ignored) {
        // offline or the API is down: try again next time
    }
});</code></pre>
<p>(On Folia, use the async scheduler instead: see <a href="https://pluginanalytics.dev/blog/make-plugin-folia-compatible/">making your plugin Folia-compatible</a>.)</p>
<h2 id="compare-versions-as-numbers">Compare versions as numbers</h2>
<p>String comparison says <code>1.10.0</code> is older than <code>1.9.2</code>. Compare each number in turn:</p>
<pre><code class="lang-java">static boolean isNewer(String latest, String current) {
    String[] a = latest.split(&quot;[^0-9]+&quot;), b = current.split(&quot;[^0-9]+&quot;);
    for (int i = 0; i &lt; Math.max(a.length, b.length); i++) {
        int x = i &lt; a.length &amp;&amp; !a[i].isEmpty() ? Integer.parseInt(a[i]) : 0;
        int y = i &lt; b.length &amp;&amp; !b[i].isEmpty() ? Integer.parseInt(b[i]) : 0;
        if (x != y) return x &gt; y;
    }
    return false;
}</code></pre>
<p>Never tell someone running a newer version (a dev build, say) to &quot;update&quot; to an older one.</p>
<h2 id="tell-the-right-people-once">Tell the right people, once</h2>
<ul><li><strong>Console:</strong> one line on startup when an update exists. Not every check.</li><li><strong>In game:</strong> only to players with a permission (say <code>yourplugin.update</code>, default op), when they join. Never to regular players.</li><li><strong>Include the link</strong> to your resource page, and what changed if it's short (&quot;fixes data loss with MySQL&quot;).</li><li><strong>Let owners turn it off</strong> in the config. Some networks pin versions on purpose.</li></ul>
<h2 id="critical-updates">Critical updates</h2>
<p>Some releases matter more than others: a dupe exploit, data loss, a security fix. Those deserve a stronger message (a warning in the console, a red line for admins) even if the owner usually ignores updates. Decide it per release, not per check, and say why: &quot;Versions below 2.3.1 can lose player data on restart&quot;.</p>
<h2 id="without-writing-it-yourself">Without writing it yourself</h2>
<p><a href="https://pluginanalytics.dev/docs/updates/">PluginAnalytics' update notices</a> do all of the above with no code beyond the SDK: the latest version is read from SpigotMC or Modrinth (or set by hand), admins with the permission get your message on join, you set a minimum version below which the notice becomes a warning, and you change the message in the dashboard without a new release. You also see how many servers are still on each version, which tells you whether people actually update.</p>
<h2 id="checklist">Checklist</h2>
<ul><li>Read the version from SpigotMC or Modrinth, with timeouts, off the main thread.</li><li>Compare numbers, not strings.</li><li>One console line; in-game only for admins with a permission.</li><li>Link to the resource and say what changed.</li><li>An option to turn it off.</li><li>A clear warning for critical updates.</li></ul>]]></description>
</item>
<item>
<title>A free SpigotMC resource page template (BBCode included)</title>
<link>https://pluginanalytics.dev/blog/spigotmc-resource-page-template/</link>
<guid>https://pluginanalytics.dev/blog/spigotmc-resource-page-template/</guid>
<pubDate>Fri, 09 Oct 2026 09:00:00 GMT</pubDate>
<category>Growth</category>
<description><![CDATA[<p>A good SpigotMC page answers, in this order: what the plugin does, whether it works on my server, what it looks like, and how to set it up. This template follows that order. Copy it, replace the parts in capitals and you have a page that reads well on desktop and on a phone.</p>
<h2 id="the-section-order-that-works">The section order that works</h2>
<ol><li><strong>Hero image</strong>: the plugin in action, with its name.</li><li><strong>One paragraph</strong>: what it does and for whom.</li><li><strong>Compatibility in bold</strong>: server software, Minecraft versions, dependencies.</li><li><strong>Features</strong>: 5 to 8 bullets, each starting with the benefit in bold.</li><li><strong>A GIF or video</strong> of the main feature.</li><li><strong>Getting started</strong>: install steps in 3 or 4 lines.</li><li><strong>Commands and permissions</strong>: inside spoilers, so they don't push everything down.</li><li><strong>Support and links</strong>: Discord, wiki, source, bug reports.</li></ol>
<p>Configuration details, the full command list and FAQs fit better in the <strong>Documentation</strong> tab of your resource, which SpigotMC shows next to the description.</p>
<h2 id="the-template">The template</h2>
<p>Paste this into the description editor in BB code mode (the <code>[ ]</code> button in the editor's toolbar). Replace the capitalised parts.</p>
<pre><code>[CENTER][IMG]https://i.imgur.com/YOUR_BANNER.png[/IMG][/CENTER]

[SIZE=5][B]PLUGIN NAME[/B][/SIZE] does ONE SENTENCE ABOUT WHAT IT DOES AND FOR WHOM. ONE MORE SENTENCE ON WHAT MAKES IT DIFFERENT.
[B]Works on Spigot, Paper and Folia · Minecraft 1.20 to 1.21.x · No dependencies[/B]

[SIZE=5][B]Features[/B][/SIZE]
[LIST]
[*][B]BENEFIT IN A FEW WORDS:[/B] how it works, in one sentence.
[*][B]BENEFIT IN A FEW WORDS:[/B] how it works, in one sentence.
[*][B]BENEFIT IN A FEW WORDS:[/B] how it works, in one sentence.
[*][B]BENEFIT IN A FEW WORDS:[/B] how it works, in one sentence.
[*][B]Fully configurable:[/B] every message, sound and limit in config.yml.
[/LIST]

[CENTER][IMG]https://i.imgur.com/YOUR_GIF.gif[/IMG][/CENTER]

[SIZE=5][B]Getting started[/B][/SIZE]
[LIST=1]
[*]Drop the jar into your plugins folder and restart.
[*]Edit plugins/PLUGIN NAME/config.yml if you want to change anything.
[*]Run /COMMAND to start.
[/LIST]

[SPOILER=&quot;Commands&quot;]
[LIST]
[*][B]/COMMAND[/B]: what it does
[*][B]/COMMAND reload[/B]: reloads the configuration
[/LIST]
[/SPOILER]

[SPOILER=&quot;Permissions&quot;]
[LIST]
[*][B]plugin.use[/B]: use the plugin (default: everyone)
[*][B]plugin.admin[/B]: admin commands (default: op)
[/LIST]
[/SPOILER]

[SIZE=5][B]Support[/B][/SIZE]
Questions, bugs or ideas: [URL='https://discord.gg/YOUR_INVITE']join the Discord[/URL]. Please report bugs there instead of in reviews, so they get fixed faster.</code></pre>
<p>For a video instead of a GIF, SpigotMC embeds YouTube with <code>[MEDIA=youtube]VIDEO_ID[/MEDIA]</code>, where <code>VIDEO_ID</code> is the part after <code>v=</code> in the link.</p>
<h2 id="the-bbcode-you-ll-actually-use">The BBCode you'll actually use</h2>
<p>SpigotMC's editor understands more tags than these, but these cover a clean page:</p>
<div class="frame" style="overflow-x:auto"><table class="table"><thead><tr><th>Tag</th><th>What it does</th></tr></thead><tbody><tr><td><code>[B]…[/B]</code>, <code>[I]…[/I]</code>, <code>[U]…[/U]</code></td><td>Bold, italic, underline</td></tr><tr><td><code>[SIZE=5]…[/SIZE]</code></td><td>Bigger text, for section titles (1 to 7)</td></tr><tr><td><code>[COLOR=#3f8a2a]…[/COLOR]</code></td><td>Coloured text; use it for one accent, not everywhere</td></tr><tr><td><code>[CENTER]…[/CENTER]</code></td><td>Centres images and lines</td></tr><tr><td><code>[IMG]url[/IMG]</code></td><td>An image from a link (SpigotMC doesn't host images)</td></tr><tr><td><code>[URL='link']text[/URL]</code></td><td>A link with its own text</td></tr><tr><td><code>[LIST]</code> / <code>[LIST=1]</code> with <code>[*]</code></td><td>Bulleted / numbered list</td></tr><tr><td><code>[SPOILER=&quot;Title&quot;]…[/SPOILER]</code></td><td>A collapsible block</td></tr><tr><td><code>[CODE]…[/CODE]</code></td><td>A monospaced block for config snippets</td></tr><tr><td><code>[MEDIA=youtube]id[/MEDIA]</code></td><td>An embedded YouTube video</td></tr></tbody></table></div>
<h2 id="images-and-gifs-sizes-that-work">Images and GIFs: sizes that work</h2>
<ul><li><strong>Host images on Imgur or similar</strong> and link the direct file URL (ending in <code>.png</code>, <code>.jpg</code> or <code>.gif</code>). SpigotMC loads them through its own image proxy.</li><li><strong>Keep GIFs under about 3 MB.</strong> In our experience bigger ones aren't served by the proxy and show as broken images. Short loops (5 to 8 seconds) around 600 px wide fit.</li><li><strong>Banners around 1000 px wide</strong> look sharp on desktop and scale down on phones.</li><li><strong>A few tall section images</strong> (&quot;poster&quot; panels with a heading, a screenshot and two lines of text) look polished. Keep each one well under a megabyte.</li></ul>
<h2 id="mistakes-that-break-a-description">Mistakes that break a description</h2>
<ul><li><strong>Editing the BBCode the editor shows you after switching modes.</strong> SpigotMC converts between its visual editor and BBCode, and the round trip can break list indentation. Keep your description in a text file, edit that, and paste the whole thing every time.</li><li><strong>Line breaks.</strong> SpigotMC shows every line break you type. Write each paragraph on one line, or your text breaks in odd places on wide screens.</li><li><strong>Emojis.</strong> 4-byte emojis (🚀, 🔥…) are stripped when you save. Use simple symbols (★, ✔) or none.</li><li><strong>Everything coloured and centred.</strong> It looks like a 2012 forum signature. Black text, left-aligned, with one accent colour for section titles reads best.</li><li><strong>A changelog at the top.</strong> Put it in the Updates tab, where SpigotMC already keeps it.</li></ul>
<h2 id="is-the-new-page-working">Is the new page working?</h2>
<p>Change the page, then watch whether more of the people who see it download it. SpigotMC only shows downloads, not views, so you can't tell from SpigotMC alone. A tracking image in the description fixes that: <a href="https://pluginanalytics.dev/docs/listing/">PluginAnalytics' listing tag</a> counts views on SpigotMC and Modrinth, and next to downloads and the servers that install your plugin you see whether the new page converts better than the old one.</p>
<p>For everything else that brings downloads (tag lines, update rhythm, reviews, Modrinth and Hangar), see <a href="https://pluginanalytics.dev/blog/get-more-downloads-spigotmc/">how to get more downloads on SpigotMC</a>.</p>]]></description>
</item>
</channel>
</rss>
