diff --git a/README.md b/README.md
index 477ee1e..1c50533 100644
--- a/README.md
+++ b/README.md
@@ -1,25 +1,117 @@
[![codecov](https://codecov.io/gh/fsbraun/djangocms-rest/graph/badge.svg?token=RKQJL8L8BT)](https://codecov.io/gh/fsbraun/djangocms-rest)
[![djangocms4]( https://img.shields.io/badge/django%20CMS-4-blue.svg)](https://www.django-cms.org/en/)
-# Django CMS REST
+# django CMS Headless Mode
-This is a demo project that provides RESTful APIs for Django CMS. Currently, it offers public read-only access to pages and placeholders. This allows for the retrieval of structured and nested content from your Django CMS instance in a programmatic way.
+## What is djangocms-rest?
-Please note that this project is in its early stages and more features will be added in the future. For now, it provides basic functionality for placeholders.
+djangocms-rest enables frontend projects to consume django CMS content through a browseable
+read-only, REST/JSON API. It is based on the django rest framework (DRF).
-## Features
+For the following topics please see the ../README.md
+- installation and setup instructions
+- questions and support options
+- collaboration and contributions to this project
-- Basic functionality for reading public pages and placeholders
-- Optional HTML rendering for placeholder content (including sekizai blocks)
+## What is headless mode?
-## To dos
+A Headless CMS (Content Management System) is a backend-only content management system that provides
+content through APIs, making it decoupled from the front-end presentation layer. This allows
+developers to deliver content to any device or platform, such as websites, mobile apps, or IoT
+devices, using any technology stack. By separating content management from content presentation,
+a Headless CMS offers greater flexibility and scalability in delivering content.
-- Full language fallback support on page level
-- Menu api
-- Admin api for editing content
- - Placeholder API (adding, deleting, plugins)
- - Plugin API (changing, rendering, getting options)
-- Tests
+## What are the main benefits of running a CMS in headless mode?
+
+Running a CMS in headless mode offers several benefits, including greater flexibility in delivering
+content to multiple platforms and devices through APIs, enabling consistent and efficient
+multi-channel experiences. It enhances performance and scalability by allowing frontend and backend
+development to progress independently using the best-suited technologies. Additionally, it
+streamlines content management, making it easier to update and maintain content across various
+applications without needing to alter the underlying infrastructure.
+
+## Are there js packages for drop-in support of frontend editing in the javascript framework of my choice?
+
+The good news first: django CMS headless mode is fully backend supported and works independently
+of the javascript framework. It is fully compatible with the javascript framework of your choosing.
+
+## How can I implement a plugin for headless mode?
+
+It's pretty much the same as for a traditional django CMS project, see
+[here for instructions on how to create django CMS plugins](https://docs.django-cms.org/en/latest/how_to/09-custom_plugins.html).
+
+Let's have an example. Here is a simple plugin with two fields to render a custom header. Please
+note that the template included is just a simple visual helper to support editors to manage
+content in the django CMS backend. Also, backend developers can now toy around and test their
+django CMS code independently of a frontend project.
+
+After setting up djangocms-rest and creating such a plugin you can now run the project and see a
+REST/JSON representation of your content in your browser, ready for consumption by a decoupled
+frontend.
+
+`cms_plugins.py`:
+```
+# -*- coding: utf-8 -*-
+from cms.plugin_base import CMSPluginBase
+from cms.plugin_pool import plugin_pool
+
+from . import models
+
+
+class CustomHeadingPlugin(CMSPluginBase):
+ model = models.CustomHeadingPluginModel
+ module = 'Layout Helpers'
+ name = "My Custom Heading"
+
+ # this is just a simple, unstyled helper rendering so editors can manage content
+ render_template = 'custom_heading_plugin/plugins/custom-heading.html'
+
+ allow_children = False
+
+
+plugin_pool.register_plugin(CustomHeadingPlugin)
+```
+
+`models.py`:
+```
+from cms.models.pluginmodel import CMSPlugin
+from django.db import models
+
+
+class CustomHeadingPluginModel(CMSPlugin):
+
+ heading_text = models.CharField(
+ max_length=256,
+ )
+
+ size = models.PositiveIntegerField(default=1)
+```
+
+`templates/custom_heading_plugin/plugins/custom-heading.html`:
+```
+{{ instance.heading_text }}
+```
+
+
+## Do default plugins support headless mode out of the box?
+
+Yes, djangocms-rest provides out of the box support for any and all django CMS plugins whose content
+can be serialized.
+
+
+## Does the TextPlugin (Rich Text Editor, RTE) provide a json representation of the rich text?
+
+Yes, djangocms-text has both HTML blob and structured JSON support for rich text.
+
+URLs to other CMS objects are dynamic, in the form of `cms.object-name:`, for example
+`cms.page:2`. The frontend can then use this to resolve the object and create the appropriate URLs
+to the object's frontend representation.
+
+## I don't need pages, I just have a fixed number of content areas in my frontend application for which I need CMS support.
+
+Absolutely, you can use the djangocms-aliases package. It allows you to define custom _placeholders_
+that are not linked to any pages. djangocms-rest will then make a list of those aliases and their
+content available via the REST API.
## Requirements
diff --git a/tests/test_app/admin.py b/tests/test_app/admin.py
new file mode 100644
index 0000000..32ccadf
--- /dev/null
+++ b/tests/test_app/admin.py
@@ -0,0 +1,26 @@
+from django.contrib import admin
+
+from .models import Pizza, Topping
+
+
+class ToppingInlineAdmin(admin.TabularInline):
+ model = Topping
+ extra = 1
+
+
+class PizzaAdmin(admin.ModelAdmin):
+ fieldsets = (
+ ('', {
+ 'fields': ('description',),
+ }),
+ ('Advanced', {
+ # NOTE: Disabled because when PizzaAdmin uses a collapsed
+ # class then the order of javascript libs is incorrect.
+ # 'classes': ('collapse',),
+ 'fields': ('allergens',),
+ }),
+ )
+ inlines = [ToppingInlineAdmin]
+
+
+admin.site.register(Pizza, PizzaAdmin)
diff --git a/tests/test_app/cms_plugins.py b/tests/test_app/cms_plugins.py
new file mode 100644
index 0000000..030bcb7
--- /dev/null
+++ b/tests/test_app/cms_plugins.py
@@ -0,0 +1,67 @@
+from django.template import engines
+
+from cms.models import CMSPlugin
+from cms.plugin_base import CMSPluginBase
+from cms.plugin_pool import plugin_pool
+from cms.utils.plugins import get_plugin_model
+
+from djangocms_text.cms_plugins import TextPlugin
+from tests.test_app.models import DummyLink, DummySpacer
+
+
+@plugin_pool.register_plugin
+class PreviewDisabledPlugin(CMSPluginBase):
+ text_editor_preview = False
+
+ def get_render_template(self, context, instance, placeholder):
+ template = 'Preview is disabled for this plugin'
+ return engines['django'].from_string(template)
+
+
+@plugin_pool.register_plugin
+class SekizaiPlugin(CMSPluginBase):
+ name = 'Sekizai'
+ render_template = 'test_app/plugin_with_sekizai.html'
+
+
+@plugin_pool.register_plugin
+class ExtendedTextPlugin(TextPlugin):
+ name = 'Extended'
+
+
+@plugin_pool.register_plugin
+class DummyLinkPlugin(CMSPluginBase):
+ render_plugin = False
+ model = DummyLink
+
+
+@plugin_pool.register_plugin
+class DummySpacerPlugin(CMSPluginBase):
+ render_plugin = False
+ model = DummySpacer
+
+
+@plugin_pool.register_plugin
+class DummyParentPlugin(CMSPluginBase):
+ render_template = 'test_app/dummy_parent_plugin.html'
+ model = DummyLink
+ allow_children = True
+
+ _ckeditor_body_class = 'parent-plugin-css-class'
+ _ckeditor_body_class_label_trigger = 'parent link label'
+
+ @classmethod
+ def get_child_ckeditor_body_css_class(cls, plugin: CMSPlugin) -> str:
+ plugin_model = get_plugin_model(plugin.plugin_type)
+ plugin_instance = plugin_model.objects.get(pk=plugin.pk)
+ if plugin_instance.label == cls._ckeditor_body_class_label_trigger:
+ return cls._ckeditor_body_class
+ else:
+ return ''
+
+
+@plugin_pool.register_plugin
+class DummyChildPlugin(CMSPluginBase):
+ render_template = 'test_app/dummy_child_plugin.html'
+ child_ckeditor_body_css_class = 'child-plugin-css-class'
+ allow_children = True
diff --git a/tests/test_app/forms.py b/tests/test_app/forms.py
new file mode 100644
index 0000000..d9181c4
--- /dev/null
+++ b/tests/test_app/forms.py
@@ -0,0 +1,7 @@
+from django import forms
+
+from djangocms_text.fields import HTMLFormField
+
+
+class SimpleTextForm(forms.Form):
+ text = HTMLFormField()
diff --git a/tests/test_app/models.py b/tests/test_app/models.py
new file mode 100644
index 0000000..ae940cc
--- /dev/null
+++ b/tests/test_app/models.py
@@ -0,0 +1,38 @@
+from django.db import models
+
+from cms.models import CMSPlugin
+
+from djangocms_text.fields import HTMLField
+
+
+class SimpleText(models.Model):
+ text = HTMLField(blank=True)
+
+
+class DummyLink(CMSPlugin):
+ label = models.TextField()
+
+ class Meta:
+ abstract = False
+
+ def __str__(self):
+ return 'dummy link object'
+
+
+class DummySpacer(CMSPlugin):
+ class Meta:
+ abstract = False
+
+ def __str__(self):
+ return 'dummy spacer object'
+
+
+class Pizza(models.Model):
+ description = HTMLField()
+ allergens = HTMLField(blank=True)
+
+
+class Topping(models.Model):
+ name = models.CharField(max_length=255)
+ description = HTMLField()
+ pizza = models.ForeignKey(Pizza, on_delete=models.CASCADE)
diff --git a/tests/test_app/templates/test_app/base.html b/tests/test_app/templates/test_app/base.html
new file mode 100644
index 0000000..db9d806
--- /dev/null
+++ b/tests/test_app/templates/test_app/base.html
@@ -0,0 +1,24 @@
+{% load cms_tags static menu_tags sekizai_tags %}
+
+
+
+ {% block title %}This is my new project home page{% endblock title %}
+ {% render_block "css" %}
+
+
+
+{% cms_toolbar %}
+