Magic-Editor-X — Examples
Next.js article page
tsx
// app/articles/[id]/page.tsx
import { EditorContent } from '@/components/EditorContent';
import { notFound } from 'next/navigation';
async function fetchArticle(id: string) {
const res = await fetch(`${process.env.STRAPI_URL}/api/articles/${id}`, {
headers: { Authorization: `Bearer ${process.env.STRAPI_API_TOKEN}` },
next: { revalidate: 60 },
});
if (!res.ok) return null;
return (await res.json()).data;
}
export default async function ArticlePage({ params }: { params: { id: string } }) {
const article = await fetchArticle(params.id);
if (!article) notFound();
return (
<article>
<h1>{article.attributes.title}</h1>
<EditorContent content={article.attributes.content} />
</article>
);
}Nuxt 3 SSR
vue
<!-- pages/articles/[id].vue -->
<script setup>
const { id } = useRoute().params;
const { data: article } = await useFetch(`/api/articles/${id}`, {
transform: (res) => res.data,
});
</script>
<template>
<article v-if="article">
<h1>{{ article.attributes.title }}</h1>
<EditorContent :content="article.attributes.content" />
</article>
</template>Markdown migration
One-time script to migrate existing articles:
typescript
// scripts/migrate-to-magic-editor.ts
import { strapi } from '../src/lib/strapi-admin';
async function migrate() {
const parser = strapi.plugin('magic-editor-x').service('parser');
const articles = await strapi.db.query('api::article.article').findMany();
for (const article of articles) {
if (article.content && typeof article.content === 'string') {
const blocks = parser.fromMarkdown(article.content);
await strapi.db.query('api::article.article').update({
where: { id: article.id },
data: { content: { blocks, time: Date.now(), version: '2.x' } },
});
console.log(`Migrated article ${article.id}`);
}
}
}
migrate().catch(console.error);Run once, in staging first:
bash
npx ts-node scripts/migrate-to-magic-editor.tsSearch indexing with Meilisearch
typescript
// src/api/article/content-types/article/lifecycles.ts
import { MeiliSearch } from 'meilisearch';
const client = new MeiliSearch({
host: process.env.MEILI_HOST,
apiKey: process.env.MEILI_API_KEY,
});
async function indexArticle(article) {
const parser = strapi.plugin('magic-editor-x').service('parser');
const text = parser.toText(article.content);
await client.index('articles').addDocuments([{
id: article.id,
title: article.title,
body: text,
publishedAt: article.publishedAt,
}]);
}
export default {
async afterCreate(event) { await indexArticle(event.result); },
async afterUpdate(event) { await indexArticle(event.result); },
async afterDelete(event) {
await client.index('articles').deleteDocument(event.result.id);
},
};Custom block: Callout
typescript
// src/plugins/editor-blocks/callout-tool.ts
export class CalloutTool {
static get toolbox() {
return { title: 'Callout', icon: '💡' };
}
constructor({ data }) {
this.data = data || { type: 'info', title: '', text: '' };
}
render() {
this.el = document.createElement('div');
this.el.className = `callout callout--${this.data.type}`;
this.el.innerHTML = `
<select class="callout__type">
<option value="info" ${this.data.type === 'info' ? 'selected' : ''}>ℹ️ Info</option>
<option value="warning" ${this.data.type === 'warning' ? 'selected' : ''}>⚠️ Warning</option>
<option value="success" ${this.data.type === 'success' ? 'selected' : ''}>✅ Success</option>
</select>
<input class="callout__title" placeholder="Title" value="${this.data.title}">
<textarea class="callout__text" placeholder="Message">${this.data.text}</textarea>
`;
return this.el;
}
save() {
return {
type: this.el.querySelector('.callout__type').value,
title: this.el.querySelector('.callout__title').value,
text: this.el.querySelector('.callout__text').value,
};
}
}Register:
typescript
// src/index.ts
import { CalloutTool } from './plugins/editor-blocks/callout-tool';
export default {
register({ strapi }) {
strapi.plugin('magic-editor-x').registerBlock({
type: 'callout',
title: 'Callout',
renderer: { class: CalloutTool },
});
},
};AI-assisted writing helper
typescript
// src/plugins/magic-editor-x-ai/server/controllers/ai.ts
export default {
async suggest(ctx) {
const { currentBlock, documentId } = ctx.request.body;
const suggestion = await callOpenAI({
prompt: `Continue this paragraph: "${currentBlock.data.text}"`,
});
ctx.body = { suggestion };
},
};Expose as a custom toolbar action in the admin UI.
Conditional content (readers vs admins)
tsx
export function EditorContent({ content, user }) {
return content.blocks
.filter((b) => !b.data?.adminOnly || user?.role === 'admin')
.map((b, i) => renderBlock(b, i));
}Export to Markdown
typescript
const parser = strapi.plugin('magic-editor-x').service('parser');
const md = parser.toMarkdown(article.content);
// Save as .md file, send to git, etc.Next: Troubleshooting →