# How to Create a Custom Uploader Plugin in PicList-Core: A Complete Developer's Guide

> Learn to create a custom uploader plugin in PicList-Core. This guide covers registering your uploader, handling image uploads, and configuring settings for a seamless developer experience.

- Repository: [Kuingsmile/piclist-core](https://github.com/kuingsmile/piclist-core)
- Tags: how-to-guide
- Published: 2026-03-05

---

**To create a custom uploader plugin in PicList-Core, you must export a `register` function that calls `ctx.helper.uploader.register()` with your uploader's name, an async `handle` function that iterates over `ctx.output` to upload images and populate `img.imgUrl`, and a `config` function that returns an array of `IPluginConfig` objects defining the settings UI.**

PicList-Core is the extensible engine behind the PicList image upload tool, designed to support both built-in and third-party upload adapters. By implementing the uploader plugin contract defined in the `kuingsmile/piclist-core` repository, you can add support for any image hosting service—whether deployed as an internal module or distributed via npm.

## Understanding the Plugin Architecture

PicList-Core loads uploader plugins through a unified plugin system that supports two distribution methods. Both follow identical implementation contracts but differ in how they are discovered and loaded by the core.

### Built-in vs External Plugins

**Built-in plugins** reside directly within the repository under `src/plugins/uploader/` and are registered in [`src/plugins/uploader/index.ts`](https://github.com/kuingsmile/piclist-core/blob/main/src/plugins/uploader/index.ts). These ship with PicList-Core and require no separate installation.

**External plugins** are published as npm packages matching the pattern `picgo-plugin-*` (or scoped `@*/picgo-plugin-*`). The `PluginLoader` class in [`src/lib/PluginLoader.ts`](https://github.com/kuingsmile/piclist-core/blob/main/src/lib/PluginLoader.ts) automatically discovers these packages at runtime using `PluginLoader.getPlugin` and `PluginLoader.registerPlugin`.

### The Uploader Contract

Every uploader must provide three core components to the registration helper:

- **`handle`** – An async function receiving the `IPicGo` context that uploads images and mutates `ctx.output` in place
- **`config`** – A function returning `IPluginConfig[]` that defines user-configurable fields in the settings UI  
- **`name`** – A display string identifying the uploader in the interface

According to the reference implementation in [`src/plugins/uploader/local.ts`](https://github.com/kuingsmile/piclist-core/blob/main/src/plugins/uploader/local.ts), the `register` function acts as the entry point called by PicList-Core during initialization.

## Implementing a Custom Uploader

Follow these steps to create a functional uploader plugin, whether built-in or external.

### Step 1: Create the Plugin File

Create a new TypeScript file in `src/plugins/uploader/` (for built-in) or as your package entry point (for external). The file must export a default `register` function that receives the `IPicGo` context.

```typescript
import { IPicGo, IPluginConfig } from '../../types'
import { createField, encodePath } from './utils'

export default function register(ctx: IPicGo): void {
  ctx.helper.uploader.register('myUploader', {
    get name() { return 'My Custom Uploader' },
    handle,
    config,
  })
}

```

### Step 2: Implement the Handle Function

The `handle` function processes the upload queue available in `ctx.output`. For each image object, you must set `img.imgUrl` to the public URL and optionally `img.hash` and `img.galleryPath`.

Based on the implementation in [`src/plugins/uploader/local.ts`](https://github.com/kuingsmile/piclist-core/blob/main/src/plugins/uploader/local.ts), the function should:

1. Retrieve configuration via `ctx.getConfig('picBed.myUploader')`
2. Iterate over `ctx.output` to access image buffers
3. Upload data via HTTP request or SDK
4. Populate required fields and clean up temporary buffers

```typescript
async function handle(ctx: IPicGo): Promise<IPicGo> {
  const cfg = ctx.getConfig<any>('picBed.myUploader')
  if (!cfg?.apiUrl) throw new Error('Missing API URL in myUploader config')

  for (const img of ctx.output) {
    if (!img.fileName) continue
    const data = img.buffer ?? (img.base64Image ? Buffer.from(img.base64Image, 'base64') : undefined)
    if (!data) continue

    // Upload to your service
    const resp = await fetch(cfg.apiUrl, {
      method: 'POST',
      headers: { Authorization: `Bearer ${cfg.token}` },
      body: data,
    })
    if (!resp.ok) throw new Error(`Upload failed: ${resp.statusText}`)
    const result = await resp.json()

    img.imgUrl = encodePath(result.url)
    img.hash = result.url
    img.galleryPath = result.url
    delete img.base64Image
    delete img.buffer
  }
  return ctx
}

```

### Step 3: Define the Configuration Interface

The `config` function returns an array defining the settings UI. Use `createField` from [`src/plugins/uploader/utils.ts`](https://github.com/kuingsmile/piclist-core/blob/main/src/plugins/uploader/utils.ts) to maintain consistency with built-in uploaders.

```typescript
function config(ctx: IPicGo): IPluginConfig[] {
  const userCfg = ctx.getConfig<any>('picBed.myUploader') ?? {}
  return [
    createField(ctx, 'myUploader', 'apiUrl', 'input', userCfg.apiUrl || '', true),
    createField(ctx, 'myUploader', 'token', 'input', userCfg.token || '', true),
  ]
}

```

### Step 4: Register the Plugin

For built-in plugins, add the registration call to [`src/plugins/uploader/index.ts`](https://github.com/kuingsmile/piclist-core/blob/main/src/plugins/uploader/index.ts):

```typescript
import myUploader from './myUploader'

const buildInUploaders = () => {
  return {
    register(ctx: IPicGo) {
      // ... existing built-ins ...
      myUploader(ctx)
    },
  }
}

```

For external plugins, ensure your [`package.json`](https://github.com/kuingsmile/piclist-core/blob/main/package.json) follows the naming convention:

```json
{
  "name": "picgo-plugin-my-uploader",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "author": "Your Name"
}

```

## Plugin Discovery and Loading Mechanics

The `PluginLoader` class orchestrates how PicList-Core finds and initializes plugins. When the application starts, `PluginLoader.load()` scans for enabled third-party packages, while [`src/plugins/uploader/index.ts`](https://github.com/kuingsmile/piclist-core/blob/main/src/plugins/uploader/index.ts) aggregates built-in registrations.

During the upload execution flow, [`src/lib/Commander.ts`](https://github.com/kuingsmile/piclist-core/blob/main/src/lib/Commander.ts) invokes the registered uploader's `handle` method. The uploader helper (`ctx.helper.uploader`) stores all registered adapters and routes requests based on the user's selected `picBed` configuration.

Key utility functions centralized in [`src/plugins/uploader/utils.ts`](https://github.com/kuingsmile/piclist-core/blob/main/src/plugins/uploader/utils.ts) include:

- **`createField`** – Generates standardized configuration UI components
- **`encodePath`** – URL-encodes file paths for safe linking  
- **`formatPathHelper`** – Handles path formatting across operating systems

## Summary

- **Plugin Contract**: Implement `register`, `handle`, and `config` functions following the pattern in [`src/plugins/uploader/local.ts`](https://github.com/kuingsmile/piclist-core/blob/main/src/plugins/uploader/local.ts)
- **Core Requirement**: The `handle` function must populate `img.imgUrl` on every item in `ctx.output` to complete the upload chain
- **Built-in Path**: Place files in `src/plugins/uploader/` and register them in [`src/plugins/uploader/index.ts`](https://github.com/kuingsmile/piclist-core/blob/main/src/plugins/uploader/index.ts) for native inclusion
- **External Path**: Publish npm packages named `picgo-plugin-*` to enable automatic discovery by [`src/lib/PluginLoader.ts`](https://github.com/kuingsmile/piclist-core/blob/main/src/lib/PluginLoader.ts)
- **Utilities**: Leverage `createField` and `encodePath` from [`src/plugins/uploader/utils.ts`](https://github.com/kuingsmile/piclist-core/blob/main/src/plugins/uploader/utils.ts) for consistent configuration handling

## Frequently Asked Questions

### What is the difference between a built-in and external uploader plugin in PicList-Core?

**Built-in plugins** are stored directly in the repository under `src/plugins/uploader/` and compiled with the core, while **external plugins** are installed as npm packages matching the `picgo-plugin-*` naming pattern. Both implement the same `register` function contract, but external plugins are discovered at runtime by `PluginLoader.getPlugin` in [`src/lib/PluginLoader.ts`](https://github.com/kuingsmile/piclist-core/blob/main/src/lib/PluginLoader.ts), whereas built-ins are explicitly imported in [`src/plugins/uploader/index.ts`](https://github.com/kuingsmile/piclist-core/blob/main/src/plugins/uploader/index.ts).

### Which fields must be set on the image object during the upload process?

According to the `handle` function implementation in [`src/plugins/uploader/local.ts`](https://github.com/kuingsmile/piclist-core/blob/main/src/plugins/uploader/local.ts), every image object in `ctx.output` must have `img.imgUrl` set to the publicly accessible URL. You should also set `img.hash` to a unique identifier (often the file path or URL) and optionally `img.galleryPath` for gallery preview support. Failure to set `img.imgUrl` will result in broken links in the final output.

### How does PicList-Core discover external uploader plugins?

The `PluginLoader` class in [`src/lib/PluginLoader.ts`](https://github.com/kuingsmile/piclist-core/blob/main/src/lib/PluginLoader.ts) scans for packages matching the regular expression `/^picgo-plugin-/` or scoped patterns like `@scope/picgo-plugin-name`. When `PluginLoader.load()` executes, it resolves these packages from `node_modules` and calls their exported `register` function via `PluginLoader.registerPlugin`, integrating them into the `ctx.helper.uploader` registry.

### Can I reuse existing helper functions when building a custom uploader?

Yes. PicList-Core provides centralized utilities in [`src/plugins/uploader/utils.ts`](https://github.com/kuingsmile/piclist-core/blob/main/src/plugins/uploader/utils.ts) that you should import into your plugin. The `createField` function generates standardized `IPluginConfig` objects for the settings UI, while `encodePath` ensures URL-safe encoding of file paths. Using these helpers maintains consistency with built-in uploaders like the local uploader defined in [`src/plugins/uploader/local.ts`](https://github.com/kuingsmile/piclist-core/blob/main/src/plugins/uploader/local.ts).