Documentation

OllowEditor Documentation

OllowEditor is a modern, lightweight, framework-friendly rich text editor for JavaScript, TypeScript, React, MERN, Next.js, Node.js, NestJS, Python, Django, Flask, FastAPI, CMS forms, admin dashboards, and publishing workflows.

JavaScript TypeScript React Next.js MERN Stack npm package Python pip package Django Flask FastAPI MIT License

Introduction

This page consolidates the standalone browser editor README, the npm package README, and the Python integration README into one reference. The core editor remains a browser-based JavaScript and CSS application. The npm package wraps that editor for JavaScript, TypeScript, React, Next.js, and MERN workflows, while the Python package distributes the compiled browser assets and adds framework helpers for Django, Django REST Framework, Flask, and FastAPI.

Important: the Python package does not reimplement OllowEditor in Python. It ships the compiled browser bundle, stylesheet, and shared initializer, then integrates them into Python application patterns.

Package Options

JavaScript Editor

Use the compiled ollow.js and ollow.css files directly in browser pages, CMS forms, and standalone integrations.

NPM Package

Use @codefortify/olloweditor for JavaScript, TypeScript, React, Next.js, MERN stack, Node.js, and NestJS workflows.

Python Package

Use olloweditor when a Python web app needs packaged OllowEditor assets plus helpers for Django, DRF, Flask, or FastAPI.

Feature Overview

Feature JavaScript/NPM Python/pip Notes
Rich text editingYesYesPython package serves the same browser editor assets.
Toolbar formattingYesYesIncludes typography, lists, links, bookmarks, and formatting controls.
ImagesYesYesFrontend capability documented in the editor and Python READMEs.
Image upload callbackYesFramework-specific backend wiringnpm exposes uploadImage; Python apps receive submitted HTML and can host upload endpoints separately.
GalleryYesYesSupported by the frontend editor capabilities list.
YouTube embedYesYesPython package relies on the same browser bundle.
TablesYesYesSupported by the editor README and npm features list.
Code blocksYesYesSupported in all distributions through the frontend editor.
Markdown import/exportYesYesFrontend workflow; the editor still stores HTML.
Export HTMLYesYesHTML export runs in the browser editor.
Export PDFYesYesDepends on browser print support.
Import DOCXYesYesRequires a browser-compatible parser such as Mammoth at runtime.
Export DOCXYesYesDepends on the configured browser-side exporter or fallback adapter.
React wrapperYesNoProvided by @codefortify/olloweditor/react.
Next.js supportYesNoClient-side loading with ssr: false.
Django integrationBackend-independent onlyYesOllowEditorWidget, OllowEditorField, staticfiles, admin support.
Flask integrationBackend-independent onlyYesExtension, asset blueprint, and Jinja helpers.
FastAPI integrationBackend-independent onlyYesStatic mount helper and template helpers.

Installation Commands

npm

npm i @codefortify/olloweditor

yarn

yarn add @codefortify/olloweditor

pnpm

pnpm add @codefortify/olloweditor

pip

pip install olloweditor

Django extra

pip install "olloweditor[django]"

Flask extra

pip install "olloweditor[flask]"

FastAPI extra

pip install "olloweditor[fastapi]"

All Python integrations

pip install "olloweditor[all]"

Basic HTML Setup

The standalone browser editor uses a synced <textarea>, the compiled stylesheet, and the compiled browser script. The textarea receives the final HTML output after editing.

browser setup
<form method="post">
  <textarea
  id="ollo-editor"
  name="content"
  data-theme="dark"
  data-persist-theme="true"
  ><h2>Article title</h2>
<p>Start writing your story...</p></textarea>

  <button type="submit">Save Article</button>
</form>

<link rel="stylesheet" href="ollow.css" />
<script src="ollow.js"></script>

Editor Initialization

Initialize the browser editor after the page loads. The global API is available as both OllowEditor and NationWireEditor. The editor README also documents global plugin registration with OllowEditor.registerPlugin(...).

init
<script>
  document.addEventListener("DOMContentLoaded", function () {
    OllowEditor.init("#ollo-editor", {
      theme: "dark",
      persistTheme: true,
      upload: {
        imageUrl: "/upload/image",
        galleryUrl: "/upload/gallery",
        attachmentUrl: "/upload/attachment",
        allowFallback: false
      }
    });
  });
</script>

Confirmed editor options in the browser README include per-editor themes with light, dark, and auto, automatic textarea synchronization, and upload adapter configuration for image, gallery, and attachment flows.

Toolbar Features

The browser editor README documents a broad toolbar surface including typography, formatting, source mode, export tools, and media blocks. The table below combines the editor README and npm package feature list.

Feature Description
Undo / RedoHistory controls for editing sessions.
Font family and sizeApproved typography controls with saved safe classes.
Paragraph and headingsParagraph plus heading levels including H2, H3, and H4.
Bold / Italic / Underline / StrikethroughInline text formatting actions.
Links and bookmarksHyperlink management plus internal anchor insertion.
Lists and pull quotesBullet lists, numbered lists, and quote-style blocks.
HTML modeSwitch between visual editing and sanitized source mode.
Markdown and DOCX toolsMarkdown import/export plus DOCX import/export workflows.
Images, galleries, YouTube embedsMedia blocks with upload and alignment support.
Tables and code blocksStructured content blocks for technical and editorial content.
Responsive toolbarsDesktop menu bar, tablet groups, and mobile overflow drawers.
Keyboard shortcutsShortcut set for formatting, lists, sync, and modal handling.

Media Upload Integration

OllowEditor supports configurable upload flows for images, galleries, attachments, and drag-and-drop image insertion. The editor still runs in the browser, while your application decides where files are stored and what public URLs are returned.

OllowEditor is textarea-first and browser-based. It keeps the original textarea synchronized with sanitized HTML, so the same editor works with server-rendered forms, JavaScript applications, React applications, and Python framework integrations.

browser-bundle configuration
const editor = window.OllowEditor.create(
  document.getElementById("editor"),
  {
    theme: "auto",
    persistTheme: true,
    upload: {
      imageUrl: "/api/uploads/images",
      galleryUrl: "/api/uploads/galleries",
      attachmentUrl: "/api/uploads/attachments",
      allowFallback: false
    },
    plugins: {
      callout: true
    }
  }
);

Image Upload

imageUrl handles a single image sent in the image field.

Attachment Upload

attachmentUrl handles a file attachment sent in the file field.

Drag & Drop Images

Dropped images use the configured image upload flow. Image fallback rules apply to this flow too.

Upload Response Format

Single image and attachment uploads should return a url. Gallery uploads should return urls. Returned URLs must be public or otherwise accessible to the page that renders the saved HTML.

single upload
{
  "url": "/media/editor/images/file.jpg"
}
gallery upload

Request Field Names

Upload TypeRequest Field NameExpected Response
Image uploadimage{ "url": "..." }
Gallery uploadimage{ "urls": ["...", "..."] }
Attachment uploadfile{ "url": "..." }

Image and gallery uploads use the image field. Attachment uploads use the file field.

CSRF Handling

The browser runtime checks CSRF values in this order:

  1. Uses configured headers when present.
  2. Looks for csrfmiddlewaretoken.
  3. Falls back to the csrftoken cookie.
Framework note: For Django or other CSRF-protected frameworks, make sure the upload endpoint accepts the configured headers or the available CSRF token.

Fallback Behavior

  • If no upload URL is configured for images or galleries, OllowEditor can use FileReader.
  • If an upload URL is configured and allowFallback: true, the editor can fall back to local image data URLs after upload failure.
  • Attachments require a configured upload URL.
Production note: Local data URLs are useful for previews or demos, but production applications should usually upload files to backend storage and return stable public URLs.

JavaScript Upload Endpoint Example

JavaScript
const editor = window.OllowEditor.create(
  document.getElementById("editor"),
  {
    upload: {
      imageUrl: "/api/uploads/images",
      galleryUrl: "/api/uploads/galleries",
      attachmentUrl: "/api/uploads/attachments",
      allowFallback: true
    }
  }
);

Backend Examples

Express

Express + Multer
import express from "express";
import multer from "multer";

const app = express();
const upload = multer({ dest: "uploads/" });

app.post("/api/uploads/images", upload.single("image"), async (req, res) => {
  const url = `/uploads/${req.file.filename}`;
  res.json({ url });
});

app.post("/api/uploads/galleries", upload.array("image"), async (req, res) => {
  const urls = req.files.map((file) => `/uploads/${file.filename}`);
  res.json({ urls });
});

app.post("/api/uploads/attachments", upload.single("file"), async (req, res) => {
  const url = `/uploads/${req.file.filename}`;
  res.json({ url });
});

This is a simple example. Real production applications should validate file type, file size, user authorization, and storage path.

Django

Django
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from django.core.files.storage import default_storage

@require_POST
def upload_image(request):
    file = request.FILES.get("image")
    if not file:
        return JsonResponse({"error": "No image uploaded"}, status=400)
    path = default_storage.save(f"editor/images/{file.name}", file)
    return JsonResponse({"url": default_storage.url(path)})

@require_POST
def upload_gallery(request):
    files = request.FILES.getlist("image")
    urls = []
    for file in files:
        path = default_storage.save(f"editor/gallery/{file.name}", file)
        urls.append(default_storage.url(path))
    return JsonResponse({"urls": urls})

@require_POST
def upload_attachment(request):
    file = request.FILES.get("file")
    if not file:
        return JsonResponse({"error": "No file uploaded"}, status=400)
    path = default_storage.save(f"editor/attachments/{file.name}", file)
    return JsonResponse({"url": default_storage.url(path)})

For production, validate permissions, MIME type, extension, size, and CSRF.

FastAPI

FastAPI
from fastapi import FastAPI, UploadFile, File
from fastapi.responses import JSONResponse
from pathlib import Path
import shutil

app = FastAPI()
UPLOAD_DIR = Path("uploads")
UPLOAD_DIR.mkdir(exist_ok=True)

@app.post("/api/uploads/images")
async def upload_image(image: UploadFile = File(...)):
    file_path = UPLOAD_DIR / image.filename
    with file_path.open("wb") as buffer:
        shutil.copyfileobj(image.file, buffer)
    return {"url": f"/uploads/{image.filename}"}

@app.post("/api/uploads/attachments")
async def upload_attachment(file: UploadFile = File(...)):
    file_path = UPLOAD_DIR / file.filename
    with file_path.open("wb") as buffer:
        shutil.copyfileobj(file.file, buffer)
    return {"url": f"/uploads/{file.filename}"}

This is a minimal example. In production, use safe filenames, validation, authorization, and proper static/media serving.

YouTube Embed

The browser editor accepts standard YouTube watch, short, and embed URLs and converts them into a wrapped iframe embed.

embed html
<figure class="ollow-media ollow-embed">
  <div class="ollow-video-wrapper">
    <iframe
      src="https://www.youtube.com/embed/VIDEO_ID"
      title="YouTube video player"
      frameborder="0"
      allowfullscreen
      loading="lazy">
    </iframe>
  </div>
  <figcaption>Video caption</figcaption>
</figure>

Markdown Import/Export

The browser editor supports importing Markdown into the current content and exporting editor HTML back to Markdown. Confirmed supported Markdown structures include headings, paragraphs, links, images, lists, blockquotes, horizontal rules, inline code, fenced code blocks, and basic tables.

markdown api
const editor = OllowEditor.get("#ollo-editor");

editor.importMarkdown("## Heading\n\nParagraph text", {
  mode: "replace"
});

const markdown = editor.exportMarkdown();

Export HTML

Export HTML uses sanitized editor content. The editor README confirms support for body-only export and full standalone HTML export with optional embedded styles.

export html
const html = editor.exportHTML({
  fullDocument: true,
  includeStyles: true,
  title: "Article Export"
});

Export PDF

PDF export is browser-based. The editor generates a print-ready HTML document and relies on the browser print or save-as-PDF flow rather than producing PDF bytes directly.

export pdf
editor.exportPDF({
  title: "Article",
  pageSize: "A4",
  orientation: "portrait",
  margin: "normal"
});
Limitation: PDF export depends on browser print support. To suppress browser-added headers and footers, disable them in the print dialog.

Import DOCX

DOCX import is client-side and expects an optional browser parser such as Mammoth.js to be available when that workflow is used. Supported formatting includes headings, paragraphs, inline formatting, lists, links, tables, and images when the parser supports them.

import docx
OllowEditor.init("#editor", {
  docx: {
    enabled: true
  }
});

editor.importDOCX(file, {
  mode: "replace",
  preserveFormatting: true,
  importImages: true
});

Export DOCX

True DOCX generation depends on an optional browser-compatible exporter. If that adapter is unavailable, OllowEditor can fall back to a Word-compatible HTML document flow instead of pretending to generate a native .docx.

export docx
editor.exportDOCX({
  filename: "article.docx",
  title: "Article Title",
  includeImages: true,
  fallbackToDoc: true
});

NPM Installation

The npm package ships the core editor, the React wrapper, the packaged stylesheet, and bundled TypeScript declarations.

npm
npm install @codefortify/olloweditor

CSS Import

Import the package stylesheet once in the application entry or page where the editor is used.

style.css
import "@codefortify/olloweditor/style.css";

Quick Start

The npm package mounts into a selector or DOM element and returns an OllowEditorCore instance.

quick start
import { createOllowEditor } from "@codefortify/olloweditor";
import "@codefortify/olloweditor/style.css";

const editor = createOllowEditor("#editor", {
  initialHTML: "<p>Hello OllowEditor</p>",
  placeholder: "Start writing...",
  onChange: (html) => {
    console.log(html);
  }
});

Vanilla JavaScript Usage

vanilla js
<div id="editor"></div>

<script type="module">
  import { createOllowEditor } from "@codefortify/olloweditor";
  import "@codefortify/olloweditor/style.css";

  const editor = createOllowEditor("#editor", {
    initialHTML: "<p>Hello OllowEditor</p>",
    placeholder: "Start writing...",
    onChange: (html) => {
      console.log(html);
    }
  });
</script>

TypeScript Usage

The npm package bundles its type declarations. No separate @types package is required.

typescript
import {
  createOllowEditor,
  type OllowEditorOptions,
  type OllowEditorCore
} from "@codefortify/olloweditor";

import "@codefortify/olloweditor/style.css";

const options: OllowEditorOptions = {
  initialHTML: "<p>Hello TypeScript</p>",
  placeholder: "Write something...",
  onChange: (html: string) => {
    console.log(html);
  }
};

const editor: OllowEditorCore = createOllowEditor("#editor", options);

React Usage

react
import { useState } from "react";
import { OllowEditor } from "@codefortify/olloweditor/react";
import "@codefortify/olloweditor/style.css";

export default function App() {
  const [content, setContent] = useState("");

  return (
    <OllowEditor
      value={content}
      onChange={setContent}
      placeholder="Write your article..."
    />
  );
}

React TypeScript Usage

react ts
import { useState } from "react";
import {
  OllowEditor,
  type OllowEditorReactProps
} from "@codefortify/olloweditor/react";

import "@codefortify/olloweditor/style.css";

export default function App() {
  const [content, setContent] = useState<string>("");

  const uploadImage: OllowEditorReactProps["uploadImage"] = async (file) => {
    const formData = new FormData();
    formData.append("image", file);

    const response = await fetch("/api/uploads/image", {
      method: "POST",
      body: formData
    });

    const data: { url: string } = await response.json();
    return data.url;
  };

  return (
    <OllowEditor
      value={content}
      onChange={setContent}
      placeholder="Write your article..."
      uploadImage={uploadImage}
    />
  );
}

Next.js Usage

OllowEditor uses browser APIs, so load it client-side with dynamic import and ssr: false.

next.js
import dynamic from "next/dynamic";
import "@codefortify/olloweditor/style.css";

const OllowEditor = dynamic(
  () =>
    import("@codefortify/olloweditor/react").then((mod) => mod.OllowEditor),
  { ssr: false }
);

export default function Page() {
  return <OllowEditor placeholder="Write in Next.js..." />;
}

MERN Stack Usage

Confirmed npm README workflow: React frontend, OllowEditor, HTML emitted through onChange, then a backend API stores and returns content for rendering in the application.

React frontend
OllowEditor
onChange HTML/content
Express or Node API
MongoDB/database
Render in frontend/admin panel
react form
import { useState } from "react";
import { OllowEditor } from "@codefortify/olloweditor/react";
import "@codefortify/olloweditor/style.css";

export default function BlogCreateForm() {
  const [content, setContent] = useState("");

  async function handleSubmit(event) {
    event.preventDefault();

    await fetch("/api/posts", {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        title: "My first post",
        content
      })
    });
  }

  return (
    <form onSubmit={handleSubmit}>
      <OllowEditor
        value={content}
        onChange={setContent}
        placeholder="Write your blog post..."
      />

      <button type="submit">Save Post</button>
    </form>
  );
}
express api
import express from "express";

const app = express();

app.use(express.json());

app.post("/api/posts", async (req, res) => {
  const { title, content } = req.body;

  // Save title and content to MongoDB using your model.
  // Example:
  // await Post.create({ title, content });

  res.json({
    success: true,
    message: "Post saved successfully"
  });
});

Express Upload Example

The npm README documents uploadImage as a backend callback contract rather than a bundled upload provider.

express upload
import express from "express";
import multer from "multer";

const app = express();
const upload = multer({ dest: "uploads/" });

app.post("/api/uploads/image", upload.single("image"), async (req, res) => {
  // Store the uploaded file and generate a public URL.
  const url = `/uploads/${req.file.filename}`;
  res.json({ url });
});

NestJS Upload Example

nestjs
import { Controller, Post, UploadedFile, UseInterceptors } from "@nestjs/common";
import { FileInterceptor } from "@nestjs/platform-express";

@Controller("api/uploads")
export class UploadController {
  @Post("image")
  @UseInterceptors(FileInterceptor("image"))
  uploadImage(@UploadedFile() file: Express.Multer.File) {
    return {
      url: `/uploads/${file.filename}`
    };
  }
}

API Reference

factory
createOllowEditor(
  selector: string | HTMLElement,
  options?: OllowEditorOptions
): OllowEditorCore
options
interface OllowEditorOptions {
  initialHTML?: string;
  placeholder?: string;
  readOnly?: boolean;
  className?: string;
  onChange?: (html: string) => void;
  uploadImage?: (file: File) => Promise<string> | string;
}
  • getHTML() returns the current editor HTML.
  • setHTML(html) replaces the current editor content.
  • focus() moves focus into the editor surface.
  • destroy() removes the editor instance and listeners.

React Props

react props
interface OllowEditorReactProps {
  value?: string;
  onChange?: (html: string) => void;
  placeholder?: string;
  uploadImage?: (file: File) => Promise<string> | string;
  readOnly?: boolean;
  className?: string;
}

Package Exports

exports
import { createOllowEditor } from "@codefortify/olloweditor";
import { OllowEditor } from "@codefortify/olloweditor/react";
import "@codefortify/olloweditor/style.css";

pip Installation

The Python package does not reimplement OllowEditor in Python. It packages olloweditor.browser.js, olloweditor.css, and olloweditor-init.js.

Official integrations include Django OllowEditorField and OllowEditorWidget, Django REST Framework OllowEditorHTMLField, Flask OllowEditor(app) / init_app, and FastAPI mount_olloweditor() and template helpers.

pip
pip install olloweditor
Current README note: production PyPI publication has not been completed yet. The Python README recommends local wheel or editable installs during current development workflows.

Optional Extras

Install only the extra your Python application needs.

Django

pip install "olloweditor[django]"

Django REST Framework

pip install "olloweditor[drf]"

Flask

pip install "olloweditor[flask]"

FastAPI

pip install "olloweditor[fastapi]"

All integrations

pip install "olloweditor[all]"

Django Integration

The Python package includes OllowEditorWidget, OllowEditorField, staticfiles integration, and admin support. It keeps a textarea synchronized with HTML so normal Django form processing continues to work.

django field
from django.db import models
from olloweditor.integrations.django import OllowEditorField


class Article(models.Model):
    title = models.CharField(max_length=255)
    content = OllowEditorField()

Django Forms

django form
from django import forms
from olloweditor.integrations.django import OllowEditorWidget

from .models import Article


class ArticleForm(forms.ModelForm):
    content = forms.CharField(
        widget=OllowEditorWidget(
            options={
                "theme": "auto",
            }
        )
    )

    class Meta:
        model = Article
        fields = ["title", "content"]
template
<form method="post">
    {% csrf_token %}
    {{ form.media }}
    {{ form.as_p }}
    <button type="submit">Save article</button>
</form>

Django Admin

Once olloweditor.apps.OllowEditorConfig is installed, OllowEditorField uses OllowEditorWidget in generated ModelForms, including standard Django admin form construction.

installed apps
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "olloweditor.apps.OllowEditorConfig",
]

Django REST Framework

DRF does not render the browser editor for external API clients. It accepts the HTML string generated by an OllowEditor frontend through OllowEditorHTMLField.

serializer
from rest_framework import serializers
from olloweditor.integrations.drf import OllowEditorHTMLField


class ArticleSerializer(serializers.Serializer):
    title = serializers.CharField()
    content = OllowEditorHTMLField(
        allow_blank=True,
        required=False,
    )
sanitizer
def sanitize_article_html(value: str) -> str:
    return trusted_html_sanitizer.clean(value)


class ArticleSerializer(serializers.Serializer):
    title = serializers.CharField()
    content = OllowEditorHTMLField(
        sanitizer=sanitize_article_html,
    )

Flask Integration

The Python package exposes a Flask extension, packaged asset blueprint, and Jinja helpers.

flask app
from flask import Flask, render_template, request
from olloweditor.integrations.flask import OllowEditor


app = Flask(__name__)
olloweditor = OllowEditor(app)


@app.route("/", methods=["GET", "POST"])
def index():
    content = ""

    if request.method == "POST":
        content = request.form.get("content", "")

    return render_template("index.html", content=content)

FastAPI Integration

The Python package includes a static mount helper and template helper registration pattern for FastAPI applications.

fastapi app
from fastapi import FastAPI, Request
from fastapi.templating import Jinja2Templates

from olloweditor.integrations.fastapi import (
    mount_olloweditor,
    olloweditor_assets,
)


app = FastAPI()
mount_olloweditor(app)

templates = Jinja2Templates(directory="templates")
templates.env.globals["olloweditor_assets"] = olloweditor_assets


@app.get("/")
def index(request: Request):
    return templates.TemplateResponse(
        request=request,
        name="index.html",
        context={},
    )

Static Assets

Installing olloweditor gives the Python package three packaged frontend assets plus resource helpers.

assets
olloweditor.browser.js
olloweditor.css
olloweditor-init.js

get_static_root()
get_asset_path(filename)
asset_exists(filename)

Confirmed framework behavior: Django serves these through staticfiles, Flask through the extension blueprint, FastAPI through StaticFiles, and the base package exposes resource helpers through olloweditor.resources.

Template Helpers

Flask and FastAPI both expose helper functions that inject the packaged asset tags into server-rendered templates.

Flask

<head>
  {{ olloweditor_assets() }}
</head>

FastAPI

<head>
  {{ olloweditor_assets() }}
</head>

Python Backend Flow

The Python package keeps the browser editor tied to normal form posts or JSON requests instead of introducing a Python-native editing engine.

Textarea
OllowEditor browser UI
synchronized HTML
normal form post or JSON payload
Python backend
submitted content
# Django
content = request.POST.get("content", "")

# Flask
content = request.form.get("content", "")

# FastAPI
from typing import Annotated
from fastapi import Form

def create_article(
    content: Annotated[str, Form()],
):
    return {"content": content}

Python Framework Media Uploads

OllowEditor provides framework integrations for storing uploaded images, galleries, and attachments separately from the synchronized editor HTML. Each host application remains responsible for authentication, storage, media delivery, and production security.

Django Media Uploads

Django uploads are disabled until configured. When enabled, IMAGE, GALLERY, and ATTACHMENT requests are authenticated by default, files are stored through Django default_storage, and the returned public URLs are inserted into the editor HTML.

Django Upload Settings

settings.py
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"

OLLOWEDITOR = {
    "UPLOADS_ENABLED": True,
    "UPLOAD_REQUIRE_LOGIN": True,
    "UPLOAD_PERMISSION": None,
    "IMAGE_UPLOAD_PATH": "olloweditor/images/%Y/%m/",
    "GALLERY_UPLOAD_PATH": "olloweditor/gallery/%Y/%m/",
    "ATTACHMENT_UPLOAD_PATH": "olloweditor/attachments/%Y/%m/",
    "MAX_IMAGE_SIZE": 10 * 1024 * 1024,
    "MAX_GALLERY_FILES": 20,
    "MAX_ATTACHMENT_SIZE": 25 * 1024 * 1024,
    "MAX_IMAGE_PIXELS": 40_000_000,
    "ALLOWED_IMAGE_EXTENSIONS": [
        "jpg",
        "jpeg",
        "png",
        "gif",
        "webp",
    ],
    "ALLOWED_ATTACHMENT_EXTENSIONS": [
        "pdf",
        "doc",
        "docx",
        "xls",
        "xlsx",
        "ppt",
        "pptx",
        "txt",
        "zip",
    ],
    "ALLOW_BASE64_UPLOADS": False,
}
SettingDefault
UPLOADS_ENABLEDFalse
UPLOAD_REQUIRE_LOGINTrue
UPLOAD_PERMISSIONNone
IMAGE_UPLOAD_PATH"olloweditor/images/%Y/%m/"
GALLERY_UPLOAD_PATH"olloweditor/gallery/%Y/%m/"
ATTACHMENT_UPLOAD_PATH"olloweditor/attachments/%Y/%m/"
MAX_IMAGE_SIZE10 * 1024 * 1024
MAX_GALLERY_FILES20
MAX_ATTACHMENT_SIZE25 * 1024 * 1024
MAX_IMAGE_PIXELS40_000_000
ALLOWED_IMAGE_EXTENSIONSjpg, jpeg, png, gif, webp
ALLOWED_ATTACHMENT_EXTENSIONSpdf, doc, docx, xls, xlsx, ppt, pptx, txt, zip
ALLOW_BASE64_UPLOADSFalse

Django Upload URLs

Include the package URLs. The prefix remains controlled by the host project.

project/urls.py
from django.conf import settings
from django.conf.urls.static import static
from django.urls import include, path


urlpatterns = [
    path(
        "olloweditor/",
        include("olloweditor.integrations.django.urls"),
    ),
]

if settings.DEBUG:
    urlpatterns += static(
        settings.MEDIA_URL,
        document_root=settings.MEDIA_ROOT,
    )
  • POST /olloweditor/upload/image/
  • POST /olloweditor/upload/gallery/
  • POST /olloweditor/upload/attachment/
Production media: The static(..., document_root=...) helper serves development media only. Production applications should use object storage, a CDN, Nginx, Apache, or another appropriate media service.

Configured server-upload mode stores a public media URL in the HTML:

stored HTML
<img src="/media/olloweditor/images/2026/07/generated.png">

Configured server-upload mode does not save a base64 data URL:

not stored
<img src="data:image/png;base64,...">
Cloud storage: Cloud storage works through the configured Django storage backend. The upload views call default_storage.save(...) and default_storage.url(...); they do not join paths onto MEDIA_ROOT or return physical filesystem paths.

Django REST Framework Uploads

OllowEditorHTMLField accepts and validates the HTML string produced by a separate OllowEditor frontend. DRF does not initialize the JavaScript editor.

serializer field
from rest_framework import serializers
from olloweditor.integrations.drf import OllowEditorHTMLField


class ArticleSerializer(serializers.Serializer):
    title = serializers.CharField()
    content = OllowEditorHTMLField(
        allow_blank=True,
        required=False,
    )
The field accepts an optional sanitizer callable and an optional DRF validator callable. A sanitizer must return a string.

Register the reusable multipart upload views

DRF upload URLs
from django.urls import path

from olloweditor.integrations.drf import (
    OllowEditorAttachmentUploadView,
    OllowEditorGalleryUploadView,
    OllowEditorImageUploadView,
)


urlpatterns = [
    path(
        "api/olloweditor/upload/image/",
        OllowEditorImageUploadView.as_view(),
    ),
    path(
        "api/olloweditor/upload/gallery/",
        OllowEditorGalleryUploadView.as_view(),
    ),
    path(
        "api/olloweditor/upload/attachment/",
        OllowEditorAttachmentUploadView.as_view(),
    ),
]

The views use DRF MultiPartParser and FormParser, reuse the Django upload settings and default_storage, and require authentication when UPLOAD_REQUIRE_LOGIN is true. Set DRF authentication globally, or subclass a view to set authentication_classes and permission_classes. Session authentication retains Django CSRF checks; token and JWT authentication remain host-application decisions.

Plain text previews

Use plain text in API list responses. Do not return stored HTML as a trusted preview.

preview serializer
from rest_framework import serializers
from olloweditor.previews import extract_olloweditor_text


class ArticleSerializer(serializers.ModelSerializer):
    preview = serializers.SerializerMethodField()

    def get_preview(self, obj) -> str:
        return extract_olloweditor_text(
            obj.content,
            max_length=140,
        )

Flask Uploads

The Flask extension registers packaged asset routes, upload endpoints, Jinja helpers, and an optional development media route for local storage.

Flask application
from flask import Flask, g
from olloweditor.integrations.flask import OllowEditor


def upload_user_is_authenticated() -> bool:
    return getattr(g, "user", None) is not None


def upload_user_has_permission() -> bool:
    user = getattr(g, "user", None)
    return bool(user and user.can_upload)


app = Flask(__name__)
app.config.update(
    OLLOWEDITOR_UPLOADS_ENABLED=True,
    OLLOWEDITOR_UPLOAD_AUTH_REQUIRED=True,
    OLLOWEDITOR_AUTH_CHECK=upload_user_is_authenticated,
    OLLOWEDITOR_PERMISSION_CHECK=upload_user_has_permission,
    OLLOWEDITOR_UPLOAD_PERMISSION_REQUIRED=True,
    OLLOWEDITOR_UPLOAD_ROOT="./media",
    OLLOWEDITOR_MEDIA_URL="/media/",
)

olloweditor = OllowEditor(app)

The default URL prefix is /olloweditor. It serves the three upload endpoints under /olloweditor/upload/ and injects these Jinja globals:

  • olloweditor_assets()
  • olloweditor_textarea(...)
  • extract_olloweditor_text(...)
Jinja template
<!doctype html>
<html lang="en">
  <head>
    {{ olloweditor_assets() }}
  </head>
  <body>
    <form method="post">
      {{ olloweditor_textarea("content", article.content) }}
      <button type="submit">Save</button>
    </form>
  </body>
</html>
Configuration supports custom upload paths, extension lists, size and pixel limits, OLLOWEDITOR_CSRF_TOKEN_CALLBACK, and OLLOWEDITOR_CSRF_HEADER_NAME. Flask-WTF and Flask-Login are not mandatory; connect their CSRF and authentication mechanisms through application callbacks.

Set OLLOWEDITOR_STORAGE to an object implementing UploadStorageProtocol.save(...) and delete(...) for S3, Google Cloud Storage, Azure Blob Storage, a CDN-backed service, or private application storage. The default LocalFilesystemUploadStorage uses OLLOWEDITOR_UPLOAD_ROOT and OLLOWEDITOR_MEDIA_URL.

Use extract_olloweditor_text(article.content, max_length=140) for list pages. Do not render arbitrary stored content with Jinja |safe.

FastAPI Uploads

OllowEditorFastAPI mounts the packaged static assets, installs upload routes, adds template helpers, and can mount a development media directory for its local storage adapter.

FastAPI application
from fastapi import FastAPI
from fastapi.templating import Jinja2Templates

from olloweditor.integrations.fastapi import OllowEditorFastAPI


async def require_user() -> bool:
    return True


async def require_upload_permission() -> bool:
    return True


app = FastAPI()
templates = Jinja2Templates(directory="templates")

olloweditor = OllowEditorFastAPI(
    uploads_enabled=True,
    upload_root="./media",
    media_url="/media/",
    auth_required=True,
    auth_dependency=require_user,
    permission_dependency=require_upload_permission,
)
olloweditor.init_app(app, templates=templates)
  • static assets: /olloweditor/static/
  • IMAGE: POST /olloweditor/upload/image/
  • GALLERY: POST /olloweditor/upload/gallery/
  • ATTACHMENT: POST /olloweditor/upload/attachment/

The routes use FastAPI UploadFile and require python-multipart, which is included in the fastapi extra. Authentication and permission callbacks may be synchronous or asynchronous; the host application supplies its own identity system.

Templates receive olloweditor_assets(), olloweditor_textarea(...), and extract_olloweditor_text(...). A custom storage implementing UploadStorageProtocol can replace local filesystem storage. The local media mount is intended for development, not production delivery.

Upload Response Contract

Successful responses return structured JSON. Uploaded binaries are stored separately from rich text. Responses contain public URLs from the configured storage backend, never physical filesystem paths. Cloud and CDN adapters may return absolute public URLs.

IMAGE success
{
  "success": true,
  "type": "image",
  "url": "/media/olloweditor/images/generated.png",
  "name": "original.png",
  "size": 12345
}
ATTACHMENT success
{
  "success": true,
  "type": "attachment",
  "url": "/media/olloweditor/attachments/generated.pdf",
  "name": "report.pdf",
  "size": 12345
}
GALLERY success

Gallery success preserves selection order.

Structured error payload
{
  "success": false,
  "error": {
    "code": "invalid_file_type",
    "message": "This file type is not allowed."
  }
}
FastAPI returns the same error object under its standard HTTP exception detail key. Successful responses use the structures shown above.

Security Notes

Production security: Production upload endpoints should validate authentication, permissions, file extensions, MIME types, file sizes, image pixel limits, storage paths, and CSRF behavior. Do not treat stored editor HTML as trusted preview text. Use extract_olloweditor_text(...) for previews and sanitize untrusted HTML before rendering.

Security Notes

OllowEditor sanitizes and normalizes content on the client, including pasted HTML, source-mode edits, Markdown import, DOCX import, URLs, embeds, and plugin-inserted markup. This is not a complete security boundary.

If content or uploads are untrusted, the host application should:

  • Validate and sanitize HTML on the server.
  • Validate upload authorization.
  • Validate MIME type, extension, and size.
  • Apply normal CSRF protection on upload endpoints.
  • Use an appropriate Content Security Policy.

Browser Support

The npm README lists the following modern browser targets:

  • Chrome
  • Edge
  • Firefox
  • Safari
  • Other modern Chromium-based browsers

Package Structure

npm package
olloweditor/
├── src/
│   ├── core/
│   ├── react/
│   ├── styles/
│   └── types/
├── dist/
├── examples/
├── website/
├── package.json
├── vite.config.js
├── tsconfig.json
└── README.md

Local Development

The npm README documents the standard frontend development flow, and the Python README adds the Python-specific environment setup.

frontend dev
git clone https://github.com/CodeFortifyCloud/olloweditor.git
cd olloweditor
npm install
npm run dev
python dev
npm ci
npm run build
npm run typecheck
npm test
npm run build:python-assets
npm run verify:python-assets

cd python
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -e ".[all,dev,test]"

Build

npm build
npm run build
python distributions
cd python
rm -rf build dist src/*.egg-info
python -m build
python -m twine check dist/*

Publishing

npm publish
npm pack --dry-run
npm pack
npm login
npm publish --access public
python wheel verification
pip install dist/olloweditor-<version>-py3-none-any.whl
python scripts/check_wheel_contents.py dist/*.whl
python scripts/verify_wheel_installs.py dist/*.whl

The Python README documents build and verification commands, but it does not publish a final PyPI release command in the same way the npm README does.

License

MIT License.