Skip to content

First plugin

Creating your first extension for anisun.

Manifest

Every extensions should have a metadata file, which is also called manifest. You can name it however you want, place it wherever you want, but you must use a JSON format.

Manifest type:

ts
type ManifestType = {
    logo:         string;        // required
    name:         string;        // required
    url:          string;        // required
    pages:        Array<string>; // required
    version:      string;        // required
    author:       string;        // required
    displayName?: string;
    areStyles?:   boolean;
};
  • logo - a link for the extension logo.
  • name - an extension name.
  • url - an extension bundled source code link.
  • pages - an array of custom page names, where plugins will be able to show content.
  • version - plugin's version.
  • author - plugin's author(s).
  • displayName - an extension name, which will be shown in extensions loader. Optional.
  • areStyles - when true, plugin will be loaded in the app's layout (to apply CSS styles or change UI nodes globally). Optional.

This is an example of Manifest:

json
{
    "logo": "https://anime.tatar/roxy-example-plugin-logo.png",
    "name": "css-styles-example-svelte",
    "displayName": "Example Svelte Plugin",
    "url": "https://raw.githubusercontent.com/notwindstone/anisun-svelte-css-styles-extension/refs/heads/main/dist/bundle.js",
    "pages": [],
    "version": "semver-1.0.0",
    "author": "windstone",
    "areStyles": true
}

Key notes

If you are going to make an extension with stylesheets that are gonna apply to the website UI, then you need to add a .garbageCollectorClearMe{display:none;} class to the top of your css code. This is needed because your plugin will create stylesheets every route change, which might eventually (especially if your stylesheets are big af) fuck user's website performance (until he refreshes the page, but that's still not really convenient).

About root IDs:

  • extensions-css-loader-id - loads in the layout, theoretically should always be on every page.
  • extensions-root-id - exists only on the anime page.
  • extensions-root-page-id - exists only on the custom pages.

React

Optimizing dependencies

A small problem

WARNING

Shared dependencies are not fully bundled packages. They have only those exported elements, which anisun uses itself. That means a shared React bundle will not have a useId hook, but will have useState, useEffect, useMemo, etc. If you need something that is not in the shared dependency, consider bundling the whole package to your extension output code.

How to enable optimization

INFO

If you are using a starter kit, then skip this section, because dependency sharing is enabled by default.

First, install a @paciolan/remote-component package using your package manager.

Second, create a remote-component.config.js file in the root of your project and fill it with the next content:

js
/**
 * Dependencies for Remote Components
 */

module.exports = {
    resolve: {
      react: require("react"),
      "react-dom/client": require("react-dom/client"),
    },
};

Now you just need to load this file in the webpack configuration (webpack.config.js):

js
// ...other imports
const remoteComponentConfig = require("./remote-component.config").resolve;

const externals = Object.keys(remoteComponentConfig).reduce(
    (obj, key) => ({ ...obj, [key]: key }),
    {}
);

module.exports = {
    // ...other options
    externals: {
        ...externals,
        "remote-component.config.js": "remote-component.config.js",
    },
}

How to disable optimization

Comment out or remove your externals option in the webpack configuration:

js
// ...other code

module.exports = {
    // ...other options
    externals: {
        // ...externals,
        // "remote-component.config.js": "remote-component.config.js",
    },
}

Coding

Finally, the actual coding part! Let's make an extension that will get MAL ID from pathname, fetch a trailer data with that ID from Anilist and show the data to user. Also, we will implement a custom page with a simple settings.

To be written

tsx
export default function Component() {
    // some code...

    return (
        <div className="react-extension-body" />
    );
}

Vue

To be written

vue
<template>
  <div class="vue-extension-body">
  </div>
</template>

<script setup lang="ts">
    // some code
</script>

Svelte

Let's make a style extension that will modify the UI of anisun. I'm gonna use Needy Girl Overdose assets for the styling part.

If you made it until here, this means that you already initialized your project. So I'm gonna clean up some unused code in the starting src/ folder:

js
.
└─ src
   ├─ assets 
   │  └─ svelte.svg 
   ├─ lib 
   │  └─ Counter.svelte 
   ├─ app.css
   ├─ main.ts
   ├─ App.svelte
   └─ vite-env.d.ts // auto-generated, don't touch

Now we need to change main.ts file. There we must remove export default app keywords, because we are not returning a React component. Instead, we should manually mount into the DOM. But to do so, we must ensure that the mounting element actually exists (to prevent unexpected errors from happening):

ts
import { mount } from 'svelte'
import './app.css'
import App from './App.svelte'

if (document.getElementById('extensions-css-loader-id')) { 
  mount(App, { 
    target: document.getElementById('extensions-css-loader-id')!, 
  }); 
} 

const app = mount(App, { 
  target: document.getElementById('app')!, 
}) 

export default app 

Let's open App.svelte. This file by default should have a lot of gibberish code, so remove it and write:

svelte
<div class="EXTENSION-NAME-OR-OTHER-UNIQUE-CLASS-svelte-body"></div>

Now, the styling part. I'm gonna open app.css file, remove everything and make some new styles.

First of all, I add a garbageCollectorClearMe class at the top of the file.

Now I want to show an image on the website background, considering that anisun has two theme schemes: dark and light. As I remember, TailwindCSS (my project relies on it) uses data-* attributes for this particular thing, but I have changed the dark/light theme implementation some time ago. Because of it, I just need to specify .dark and .light classes to handle both themes.

css
.garbageCollectorClearMe {
  display: none;
}

.light {
  background: linear-gradient( rgba(255, 255, 255, 0.87), rgba(255, 255, 255, 0.87) ), url("https://cdn.dynamicwallpaper.club/wallpapers/jqc5oqev0br/thumbs/1600/11.jpg");
}

.dark {
  background: linear-gradient( rgba(0, 0, 0, 0.87), rgba(0, 0, 0, 0.87) ), url("https://cdn.dynamicwallpaper.club/wallpapers/jqc5oqev0br/thumbs/1600/11.jpg");
}

I tested the website and... the hero card looked kinda out of the place. I decided to hide it for the desktop screens, but show it on the mobile phones with a border to match background and make black to pink transition not quite sharp.

css
/* previous styles */

.hero__poster-image {
  display: none;

  @media screen and (max-width: 640px) {
    display: block;
  }
}

.hero__poster-shadow {
  display: none;

  @media screen and (max-width: 640px) {
    display: block;
    border-bottom: 2px solid palevioletred;
  }
}

.scrollableCardsShadow {
  display: none;
}

Now the plugin is done. Source code is available at github.

Other frameworks

Give it a try yourself without my guide!

Building

Run bun run build or npm run build or whatever the command is for your building process.