Plugins

Full-Stack Plugins

End-to-end plugin development: combine a .NET backend module with a Vue 3 frontend component for complete feature extensions.

Overview

Full-stack plugins combine backend and frontend parts into a single distributable package. The backend provides API endpoints and business logic, while the frontend adds UI components and editor extensions.

my-fullstack-plugin/
├── manifest.json           # Combined manifest
├── backend/
│   ├── MyPlugin.cs         # IRasepiPlugin implementation
│   ├── MyController.cs     # API endpoints
│   ├── MyService.cs        # Business logic
│   └── MyPlugin.csproj
└── frontend/
    ├── index.ts            # Frontend entry
    ├── components/
    │   └── MyView.vue      # UI component
    └── extensions/
        └── myBlock.ts      # TipTap extension

Walkthrough: Diagram Plugin

Let's build a full-stack plugin that adds Mermaid diagram support to entries — with backend rendering, a custom TipTap block, and a sidebar preview widget.

Step 1: Backend — Service

public class DiagramService
{
    public async Task<string> RenderAsync(
        string mermaidCode)
    {
        // Call mermaid CLI or JS renderer
        // Return SVG string
        return await _renderer.RenderToSvg(mermaidCode);
    }
}

Step 2: Backend — Controller

[ApiController]
[Route("api/plugins/diagrams")]
public class DiagramController : ControllerBase
{
    private readonly DiagramService _service;

    [HttpPost("render")]
    public async Task<IActionResult> Render(
        [FromBody] RenderRequest request)
    {
        var svg = await _service.RenderAsync(
            request.MermaidCode);
        return Ok(new { svg });
    }
}

Step 3: Backend — Plugin Registration

public class DiagramPlugin : IRasepiPlugin
{
    public PluginManifest Manifest => new()
    {
        Id = "rasepi-diagrams",
        Name = "Mermaid Diagrams",
        Version = "1.0.0",
    };

    public void ConfigureServices(
        IServiceCollection services)
    {
        services.AddScoped<DiagramService>();
    }
}

Step 4: Frontend — TipTap Node

import { Node, mergeAttributes } from '@tiptap/core'
import { VueNodeViewRenderer } from '@tiptap/vue-3'
import DiagramBlock from './DiagramBlock.vue'

export const DiagramNode = Node.create({
  name: 'diagram',
  group: 'block',
  atom: true,

  addAttributes() {
    return {
      blockId: { default: null },
      deleted: { default: false },
      mermaidCode: { default: '' },
      renderedSvg: { default: '' },
    }
  },

  addNodeView() {
    return VueNodeViewRenderer(DiagramBlock)
  },
})

Step 5: Frontend — Block Component

<script setup lang="ts">
import { ref, watch } from 'vue'
import { NodeViewWrapper } from '@tiptap/vue-3'

const props = defineProps(['node', 'updateAttributes'])
const code = ref(props.node.attrs.mermaidCode)
const svg = ref(props.node.attrs.renderedSvg)
const loading = ref(false)

async function render() {
  loading.value = true
  const res = await fetch('/plugins/diagrams/render', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ mermaidCode: code.value }),
  })
  const data = await res.json()
  svg.value = data.svg
  props.updateAttributes({
    mermaidCode: code.value,
    renderedSvg: data.svg,
  })
  loading.value = false
}
</script>

<template>
  <NodeViewWrapper class="diagram-block">
    <textarea v-model="code" />
    <button @click="render">Render</button>
    <div v-if="svg" v-html="svg" />
  </NodeViewWrapper>
</template>

Combined Manifest

{
  "id": "rasepi-diagrams",
  "name": "Mermaid Diagrams",
  "version": "1.0.0",
  "author": "Your Name",
  "description": "Mermaid diagram blocks with live preview",
  "backend": {
    "assembly": "RasepiDiagrams.dll",
    "pluginClass": "DiagramPlugin"
  },
  "frontend": {
    "entry": "frontend/index.ts",
    "type": "editor",
    "slots": ["editor-toolbar"]
  }
}

Backend ↔ Frontend Communication

REST API

Plugin controllers expose endpoints under /plugins/{'{'}pluginId{'}'}/. Frontend components call them via standard fetch.

SignalR

For real-time features, plugins can register SignalR hub methods. Frontend subscribes via the existing connection.

Plugin Context

Both sides receive tenant and user context automatically. Backend via IPluginContext, frontend via injected pluginApi.

Testing

Backend Unit Tests

[Fact]
public async Task DiagramService_RenderAsync_ReturnsSvg()
{
    var service = new DiagramService();
    var svg = await service.RenderAsync(
        "graph TD; A-->B;");
    Assert.Contains("<svg", svg);
}

Frontend Component Tests

import { mount } from '@vue/test-utils'
import DiagramBlock from './DiagramBlock.vue'

test('renders textarea', () => {
  const wrapper = mount(DiagramBlock, {
    props: {
      node: { attrs: { mermaidCode: '', renderedSvg: '' } },
      updateAttributes: vi.fn(),
    },
  })
  expect(wrapper.find('textarea').exists()).toBe(true)
})

Packaging & Distribution

Full-stack plugins can be distributed as a zip archive or npm package containing both parts.

# Build backend
cd backend && dotnet build -c Release

# Build frontend
cd frontend && npm run build

# Package
zip -r rasepi-diagrams-1.0.0.zip \
  manifest.json \
  backend/bin/Release/net8.0/RasepiDiagrams.dll \
  frontend/dist/

Install by extracting to the plugins directory:

unzip rasepi-diagrams-1.0.0.zip -d plugins/rasepi-diagrams/