# Post Providers

URL: https://zerodev.ca/docs/medusa/developer-documentation/posts

> Let the core /post command and dashboard Post page send your module's panels into a channel.



Modules never ship their own `/<module> post` or `/<module> panel` subcommands. Instead they declare post providers, and Core exposes every provider through one `/post` slash command and one dashboard **Post** page.

Post providers reside in `resources/posts/` and load from every `*.js` file located directly in that directory.

## Base Class Definition

```js title="modules/example/resources/posts/panel.js"
import { Posts } from "#medusa/modules";

export class PanelPost extends Posts {
    constructor(medusa) {
        super(medusa, { name: "panel" });
    }

    panels() {
        return this.medusa.modules.handlers.get(this.module, "panels");
    }

    items(guildId) {
        return this.panels().keys(guildId).map(key => ({ value: key, label: this.panels().panel(guildId, key).title }));
    }

    async message(guild, item) {
        const panel = this.panels().panel(guild.id, item);
        return panel ? this.panels().panelMessage(panel) : null;
    }

    async posted(message, { guild, channel, item, actor }) {
        await this.medusa.logs.post(this.module, "panelPosted", { guildId: guild.id, actor, tokens: { panel: item, channel: `<#${channel.id}>` } });
    }
}
```

## Constructor Options

| Property | Type     | Default  | Description                                                                                                                                                                                        |
| -------- | -------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`   | `string` | Required | Provider identifier, unique within the module. The public id is `<module>:<name>`.                                                                                                                 |
| `label`  | `string` | `""`     | Optional suffix shown after the module name. Only needed when a module has more than one provider. The dashboard translates it through `schema.<module>.config.<name>.label` when that key exists. |
| `order`  | `number` | `0`      | Sort priority between providers of the same module.                                                                                                                                                |

The type shown to users is the module display name (the `module.<id>.name` translation on the dashboard), plus the label when set.

## Methods

| Method                     | Returns                   | Description                                                                                                                                                           |
| -------------------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `items(guildId)`           | `Array<{ value, label }>` | Entries the user picks from, such as configured panels. Return `[]` when the module has a single message to post; the item option and selector are then hidden.       |
| `target(guild)`            | `Channel \| null`         | Optional default channel, for example a channel in a connected appeal server. Used when no channel is chosen and always listed on the dashboard.                      |
| `message(guild, item)`     | `Promise<object \| null>` | Builds the message payload. Return `null` when there is nothing to post.                                                                                              |
| `posted(message, context)` | `Promise<void>`           | Runs after a successful send. Store the message, add reactions, or write a log. `context` holds `guild`, `channel`, `item` and `actor` (the Discord user, or `null`). |

<Callout type="info">
  `message` and `posted` run inside the guild scope, so config reads resolve to the server the post was made in.
</Callout>

## Post Manager Methods

The root `this.medusa.posts` service validates and sends posts for both the command and the dashboard:

```js
const types = this.medusa.posts.types(guildId);

const provider = this.medusa.posts.entry("ticket:panel", guildId);

const { message, channel } = await this.medusa.posts.send({
    id: "ticket:panel",
    item: "support",
    guildId,
    channelId,
    actor: interaction.user
});
```

`send` throws an `Error` with a localized message when the server is unavailable, the type or item does not exist, the channel is missing or cannot hold messages, `message` returns `null`, or Discord rejects the send.

## Dashboard Route

The Post page talks to the core module route `/api/modules/core/posts`:

| Method | Body                      | Response                                                                                                                                |
| ------ | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `GET`  | -                         | `{ types: [{ id, module, name, label, items, target }] }` for the server in `x-guild-id`, limited to modules run by the bot in `x-bot`. |
| `POST` | `{ type, item, channel }` | `{ success: true, channel }`, or `400` with `{ error }`.                                                                                |
