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 extensionWalkthrough: 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/