Book and Library¶
The Book feature provides a comprehensive system for creating structured, hierarchical documentation with automatic table of contents numbering, PDF export capabilities, and user access management. The Library content type serves as a container for organizing multiple books.
Overview¶
The Book system consists of:
Library - Container for organizing multiple books
Book - Main container with table of contents, custom styling, and PDF export
Chapter - Hierarchical content units with automatic numbering
Paragraph - Content blocks within chapters
Key features include:
Automatic table of contents generation with hierarchical numbering (1, 1.1, 1.1.1, etc.)
PDF export via WeasyPrint integration
Custom CSS styling per book
User registration and access request workflows
Ask the book owner a question (via browser form or REST API)
REST API with enhanced serialization
Content Types¶
Library¶
A container specifically designed for organizing books.
Portal Type: Library
Allowed Content Types: Book only
Book¶
The main documentation container with chapters and table of contents support.
Portal Type: Book
Allowed Content Types: Chapter only
Schema Fields (via behaviors):
custom_css(Text) - Custom CSS with variable substitutioninclude_default_css(Bool) - Include default book CSS (default: True)header_image(NamedBlobImage) - Header image for PDF export
Chapter¶
Hierarchical content container that can nest other chapters.
Portal Type: Chapter
Allowed Content Types:
Chapter- Nested chapters (subchapters)Paragraph- Content blocksTableBlock- Table contentFileListingBlock- File listingsMediaFolder- Media containersCommentBlock- Comments
Schema Fields:
hide(Bool) - Hide from table of contents (default: False)
Paragraph¶
A content block within chapters with optional TOC visibility.
Table of Contents¶
The Toc class generates hierarchical chapter numbers automatically.
Number Generation¶
Chapters receive automatic numbers based on their position in the hierarchy:
First chapter:
1Second chapter:
2First subchapter of chapter 1:
1.1Second subchapter of chapter 1:
1.2First subchapter of chapter 2:
2.1Deep nesting:
1.1.1,1.1.2,2.1.1, etc.
Usage:
from wcs.backend.book.toc import Toc
# Get chapter number
chapter = context # A Chapter object
number = Toc(chapter).number() # Returns "1.1.2" or None if hidden
TOC Visibility¶
Chapters can be hidden from the table of contents:
# Hide chapter from TOC
chapter.hide = True
chapter.reindexObject()
Hidden chapters:
Do not appear in table of contents
Return
NonefromToc(chapter).number()Their children still calculate numbers correctly (skipping the hidden parent in numbering)
Catalog Index¶
The show_in_toc index enables efficient TOC queries:
# Query chapters visible in TOC
from plone import api
catalog = api.portal.get_tool('portal_catalog')
results = catalog.searchResults(
portal_type='Chapter',
show_in_toc=True,
path='/Plone/my-book'
)
REST API¶
Enhanced Serialization¶
Books and chapters include automatic TOC number calculation in REST API responses.
Book Response:
{
"@id": "http://localhost:8080/Plone/my-book",
"@type": "Book",
"title": "User Manual",
"items": [
{
"@id": "http://localhost:8080/Plone/my-book/chapter-1",
"@type": "Chapter",
"title": "Introduction",
"number": "1"
},
{
"@id": "http://localhost:8080/Plone/my-book/chapter-2",
"@type": "Chapter",
"title": "Getting Started",
"number": "2"
}
]
}
Chapter Response:
{
"@id": "http://localhost:8080/Plone/my-book/chapter-1/subchapter-1",
"@type": "Chapter",
"title": "Installation",
"number": "1.1",
"items": [
{
"@type": "Paragraph",
"title": "Prerequisites",
"number": "1.1.1"
}
]
}
GET @book-toc¶
Returns the full table of contents tree for a book. This endpoint uses catalog queries only (no content object wake-ups), making it significantly faster than fetching the book serialization or context navigation for large books.
const response = await fetch('https://example.com/my-book/@book-toc', {
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer <token>',
},
});
import requests
response = requests.get(
'https://example.com/my-book/@book-toc',
headers={
'Accept': 'application/json',
'Authorization': 'Bearer <token>',
},
)
Response:
{
"@id": "http://localhost:8080/Plone/my-book",
"title": "User Manual",
"number": "",
"items": [
{
"@id": "http://localhost:8080/Plone/my-book/chapter-1",
"title": "Introduction",
"description": "An introductory chapter",
"review_state": "published",
"number": "1",
"items": [
{
"@id": "http://localhost:8080/Plone/my-book/chapter-1/overview",
"title": "Overview",
"description": "",
"review_state": "published",
"number": "1.1",
"items": []
}
]
},
{
"@id": "http://localhost:8080/Plone/my-book/chapter-2",
"title": "Getting Started",
"description": "",
"review_state": "published",
"number": "2",
"items": []
}
]
}
Chapters with hide=True are excluded from the response. The tree is nested
to the full depth of the book hierarchy.
GET @book-keywords¶
Returns all unique keywords in a book with occurrence counts.
GET /Plone/my-book/@book-keywords HTTP/1.1
Host: localhost:8080
Accept: application/json
Response:
{
"keywords": [
{"keyword": "Baurecht", "count": 5},
{"keyword": "Eigentum", "count": 3},
{"keyword": "Vertrag", "count": 2}
],
"total": 3
}
GET @book-keyword-search¶
Searches for paragraphs containing a specific keyword within a book.
GET /Plone/my-book/@book-keyword-search?keyword=Baurecht HTTP/1.1
Host: localhost:8080
Accept: application/json
Response:
{
"@id": "http://localhost:8080/Plone/my-book/@book-keyword-search?keyword=Baurecht",
"keyword": "Baurecht",
"items": [
{
"@id": "http://localhost:8080/Plone/my-book/chapter-1/paragraph-1",
"title": "Introduction Paragraph",
"UID": "abc123",
"parent_chapter": {
"@id": "http://localhost:8080/Plone/my-book/chapter-1",
"title": "Introduction",
"number": "1"
}
}
],
"items_total": 1
}
Pagination:
Supports b_start and b_size query parameters for pagination.
GET /Plone/my-book/@book-keyword-search?keyword=Baurecht&b_size=10&b_start=0 HTTP/1.1
Host: localhost:8080
Accept: application/json
POST @ask-book-owner¶
Allows an authenticated user to ask a question to the book owner. The endpoint
creates a task in the current user’s task container with the book owner
(determined by the IBookOwner behavior’s moderator_email or fallback_email)
as the responsible party.
Requires: Authentication, IBookOwner behavior enabled on the book
const response = await fetch('https://example.com/my-book/@ask-book-owner', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer <token>',
},
body: JSON.stringify({
question: 'What does chapter 3 cover?',
}),
});
import requests
response = requests.post(
'https://example.com/my-book/@ask-book-owner',
headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer <token>',
},
json={
'question': 'What does chapter 3 cover?',
},
)
Successful Response (201)¶
Returns the created task object (standard plone.restapi task response).
Request Body¶
Field |
Required |
Description |
|---|---|---|
|
Yes |
The question text. Must not be empty or whitespace-only. |
Error Responses¶
Status |
Error message |
Cause |
|---|---|---|
401 |
|
Anonymous user. |
400 |
|
Missing or empty |
400 |
|
|
400 |
|
Neither |
400 |
|
No Plone user matches the moderator email. |
Task Details¶
The created task has the following properties:
Initiator: The authenticated user who asked the question
Responsible: The Plone user matching the book’s moderator email
Related: The book
Action:
frageText: HTML containing the asker’s name, email, and the question
If the current user does not yet have a task container, one is created automatically.
PDF Export¶
Books support PDF generation via WeasyPrint integration with an external PDF server.
Configuration¶
Environment variables:
PDFSERVER_URL- PDF server URL (default:http://localhost:8040)PLONE_BACKEND_HOST- Backend URL for PDF server to fetch content
Registry records (see also Email URL Configuration below):
wcs.backend.admin_url- Canonical backend URL for moderator email linkswcs.backend.preview.endpoint- Frontend URL for user-facing email links
PDF Server Endpoints¶
POST @pdfserver-convert
Initiates PDF conversion for the current content.
POST /Plone/my-book/@pdfserver-convert HTTP/1.1
Host: localhost:8080
Accept: application/json
Content-Type: application/json
Response:
{
"uid": "conversion-job-uid",
"status": "pending"
}
GET @pdfserver-status
Check conversion status.
GET /Plone/my-book/@pdfserver-status?uid=conversion-job-uid HTTP/1.1
Host: localhost:8080
Accept: application/json
Response:
{
"uid": "conversion-job-uid",
"status": "completed"
}
GET @@pdfserver-download
Download the generated PDF.
GET /Plone/my-book/@@pdfserver-download?uid=conversion-job-uid HTTP/1.1
Host: localhost:8080
Response: PDF file download
WeasyPrint Views¶
The following views render content for PDF generation:
@@view_weasyprint- Main WeasyPrint template for books@@toc_weasyprint- Table of contents for PDF@@chapter_weasyprint- Chapter rendering
Custom CSS¶
Books support custom CSS with variable substitution:
/* Available variables: $portal_url, $book_url */
.book-header {
background-image: url($book_url/@@images/header_image);
}
.book-logo {
background-image: url($portal_url/++resource++images/logo.png);
}
CSS is served via:
@@custom-book.css- Custom CSS with variable substitution@@book-variables.css- CSS custom properties for dates and images
User Access Management¶
The Book feature includes a complete user registration and access request workflow.
Book Owner Behavior¶
Books can have the IBookOwner behavior for managing access:
Fields:
moderator_email- Email for access request notificationsfallback_email- Fallback email if moderator is unavailablegroup- Plone group for book access
Registration Form¶
A custom registration form (@@register) allows users to:
Create a new account
Select books to request access to
Trigger email notifications to book moderators
Browser View: @@register (available when IBackendBookLayer is active)
Request Book Access¶
Users can request access to books via the @request-book-access endpoint on the
site root. The endpoint supports two flows: authenticated users requesting access
to additional books, and anonymous users registering a new account while
requesting access in a single step.
Browser Form: @@request-book-access (authenticated users only)
Authenticated User¶
const response = await fetch('https://example.com/@request-book-access', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer <token>',
},
body: JSON.stringify({
books: ['<book-uid>'],
}),
});
import requests
response = requests.post(
'https://example.com/@request-book-access',
headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer <token>',
},
json={
'books': ['<book-uid>'],
},
)
Anonymous User (Registration)¶
An anonymous user submits a list of book UIDs together with their email address and full name. The system creates a new user account, sends a password reset email to the user, and sends an access request email to each book’s moderator.
When Keycloak is active, user creation is delegated to Keycloak, which handles the password reset / verification email. Without Keycloak, the system sends a standard Plone password reset email.
const response = await fetch('https://example.com/@request-book-access', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
books: ['<book-uid>'],
username: '[email protected]',
fullname: 'John Doe',
url: 'https://www.example.com',
}),
});
import requests
response = requests.post(
'https://example.com/@request-book-access',
headers={
'Accept': 'application/json',
'Content-Type': 'application/json',
},
json={
'books': ['<book-uid>'],
'username': '[email protected]',
'fullname': 'John Doe',
'url': 'https://www.example.com',
},
)
Successful Response (200)¶
{
"success": true
}
Request Body¶
Field |
Required |
Description |
|---|---|---|
|
Yes |
List of book UIDs to request access to. Must contain at least one entry. Each UID must resolve to an existing book that has a moderator email and a group configured. To build a book selection form, use the |
|
Anonymous only |
Email address of the new user. Must be a valid email. Must not belong to an existing account. |
|
Anonymous only |
Full name of the new user. |
|
No |
Redirect URL for the Keycloak verification email. When provided, this URL is used as the redirect target in the Keycloak email link, overriding the |
Error Responses¶
All error responses return HTTP status 400 with a JSON body containing an
error key.
Error message |
Cause |
|---|---|
|
The |
|
An anonymous request is missing |
|
The |
|
An account with the given |
|
A book UID does not resolve to an existing book. |
|
The book exists but has no moderator or fallback email address set. |
|
The book exists but has no group assigned for user registration. |
Prerequisites¶
For the anonymous registration flow, the Plone site must allow anonymous users
to add portal members (the Add portal member permission granted to the
Anonymous role). Additionally, plone.use_email_as_login should be
enabled in the registry.
Approve Access¶
Book moderators can approve access requests via @@book-share:
/Plone/my-book/@@book-share?userid=john.doe
This:
Adds the user to the book’s configured group
Sends a confirmation email to the user
Email URL Configuration¶
When the book access flow is triggered from the frontend (via the REST API),
absolute_url() resolves to the API domain, which is not suitable for email
links. Two registry records control how email URLs are generated:
wcs.backend.admin_url— The canonical backend/admin URL (e.g.https://backend.example.ch). Used for thebook-sharelink in moderator emails. Falls back toabsolute_url()if not set (suitable for local development).wcs.backend.preview.endpoint— The frontend URL (e.g.https://www.example.ch). Used for user-facing book links when the request originates from the REST API.
How it works:
When the REST API triggers the access request, the preview.endpoint value is
encoded as a frontend_url query parameter on the book-share link in the
moderator email. When the moderator clicks the link, ApproveBookParticipant
reads this parameter and passes it through to the access-granted email. This
way the frontend URL is preserved across the moderator approval step without
relying on the request context at approval time.
When the flow starts from the backend (e.g. @@register or
@@request-book-access browser form), no frontend_url parameter is added to
the share link, so the access-granted email falls back to absolute_url().
Keycloak verification email redirect:
When Keycloak is active and a new user is created via the REST API, Keycloak sends a verification/password-reset email with a redirect link. The redirect URL is determined in this order:
The
urlfield from the request body (explicit override)The
wcs.backend.preview.endpointregistry record (frontend URL)The Keycloak plugin’s static
redirect_uriproperty (fallback)
When the flow is triggered via the backend form (@@register), the Keycloak
plugin’s static redirect_uri is used directly (no override).
Email URL behavior:
Recipient |
REST API flow |
Backend form flow |
|
|---|---|---|---|
Access request ( |
Moderator |
|
|
Access granted |
User |
|
|
Keycloak verification |
User |
|
Plugin |
Email Templates¶
The following email templates are used:
new-user-access-book-request-mail- Sent to the book moderator when a user requests access. Contains abook-sharelink pointing to the backend, optionally carrying afrontend_urlparameter.user-access-granted- Sent to the user when access is approved. Contains a link to the book, using thefrontend_urlfrom the share link if present, otherwise falling back toabsolute_url().new-user-created-and-access-to-book-mail- Sent to newly created users with password reset instructions and a book link.
PDF Views¶
@@pdf_view- PDF conversion interface@@pdfserver-download- PDF download
Ask Book Owner¶
The “Ask Owner” feature allows authenticated users to send a question to the book’s moderator directly from the book view. The question is submitted as a task in the user’s task area, with the book owner set as the responsible party.
Browser View¶
The @@ask-owner view displays a simple form with a text area. It appears as an
additional tab (“Ask Owner”) in the book navigation bar alongside “Table of
Contents”, “Keyword Search”, and “AI Ask”.
The tab is only visible when the IBookOwner behavior is enabled on the book.
On submission, the view:
Validates the question is not empty
Resolves the book’s moderator email to a Plone user
Creates a task in the current user’s task container
Redirects back to the book with a success message
REST API¶
See POST @ask-book-owner in the REST API section above.
Prerequisites¶
The
IBookOwnerbehavior must be enabled on the bookA
moderator_emailorfallback_emailmust be configuredThe email must correspond to an existing Plone user
The user asking the question must be authenticated
AI Ask (RAG) Configuration¶
Books can individually enable or disable the AI Ask feature. This allows administrators to control which books expose the AI Ask tab to users.
Prerequisites¶
Global RAG must be enabled via the
RAG_ENABLED=trueenvironment variableThe book must have
rag_enabledset toTrue
When both conditions are met, the “AI Ask” tab appears in the book’s navigation alongside “Table of Contents” and “Keyword Search”.
Permissions¶
The rag_enabled field requires the Manage RAG properties permission to edit. By default, only Manager and Site Administrator roles have this permission.
REST API¶
The rag_enabled field is included in the book’s REST API response:
// JavaScript example using fetch
const response = await fetch('/plone/my-book', {
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer <token>'
}
});
const book = await response.json();
console.log(book.rag_enabled); // true or false
Asking Questions¶
When enabled, the AI Ask feature allows users to ask natural language questions about the book’s content. The system searches through the book’s chapters and paragraphs using hybrid search (combining keyword and semantic matching) and generates answers using an LLM.
The @rag-ask endpoint supports a path parameter to scope questions to a specific book:
// Ask a question scoped to a specific book
const response = await fetch('/plone/@rag-ask', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
question: 'What is covered in chapter 3?',
path: '/plone/my-book'
})
});
See the RAG documentation for complete details on the RAG system configuration and API.
Integration Guide¶
Querying TOC Structure¶
from plone import api
from wcs.backend.book.toc import Toc
book = api.content.get(path='/library/my-book')
catalog = api.portal.get_tool('portal_catalog')
# Get all visible chapters
chapters = catalog.searchResults(
portal_type='Chapter',
path='/'.join(book.getPhysicalPath()),
show_in_toc=True,
sort_on='getObjPositionInParent'
)
for brain in chapters:
chapter = brain.getObject()
number = Toc(chapter).number()
print(f"{number} {chapter.Title()}")
File Locations¶
Core Module:
wcs/backend/book/content.py- Content type classes (Library, Book, Chapter, Paragraph)wcs/backend/book/behaviors.py- Behaviors (IShowInToc, IBookCustomCSS, IBookConfiguration, IBookOwner)wcs/backend/book/toc.py- Table of contents generatorwcs/backend/book/utils.py- Utility functionswcs/backend/book/indexer.py- Catalog indexers (show_in_toc, book_keywords)wcs/backend/book/keyword_utils.py- Keyword extraction utilitieswcs/backend/book/configure.zcml- ZCML configuration
REST API:
wcs/backend/book/restapi.py- Serializers and services
Views:
wcs/backend/book/views.py- Browser viewswcs/backend/book/templates/- View templates
PDF Generation:
wcs/backend/book/pdfserver_client.py- PDF server clientwcs/backend/book/weasyprint/views.py- WeasyPrint viewswcs/backend/book/weasyprint/templates/- PDF templateswcs/backend/book/weasyprint/resources/- PDF CSS resources
User Management:
wcs/backend/book/register_form.py- Registration formwcs/backend/book/emails.py- Email adapterswcs/backend/book/templates/emails/- Email templates
Type Definitions:
wcs/backend/profiles/default/types/Library.xmlwcs/backend/profiles/default/types/Book.xmlwcs/backend/profiles/default/types/Chapter.xmlwcs/backend/profiles/default/types/Paragraph.xml