whitehall: Configurable Document Types
Whitehall offers configurable document types. Configurable documents are editionable Whitehall documents that have their content schemas defined as JSON rather than in code. The content for an edition of a configurable document is stored in the block_content JSON column on the edition_translations table. These editions have a "type", which is stored in the configurable_document_type column on the editions table.
The standard type
As of this writing, Whitehall has a single configurable type, the "standard" document type, represented by the StandardEdition model. The standard type is a simple document type with no phasing, chapters or routing logic, for example news articles, case studies and guidance. Other types such as "phased", "navigational" and "multi-part" content are planned for future implementation.
The process of migrating existing document types to the standard type is ongoing. The migration consists of the following steps: defining the new configuration for the type, migrating the data, then deleting legacy code. Note that as of 2025 we have migrated news articles (news stories, government responses etc.) and history pages to the standard configurable document type format.
Document Type Configuration
Configurable document types are defined as JSON. The JSON files are stored in the app/models/configurable_document_types directory. The types are loaded from the JSON files on the first call to the types method on the configurable document type model and cached in memory. The model provides an ergonomic way to read values from a configuration file.
The JSON for each type has these top level keys:
- 'key': The unique identifier for the document type. This is what will be stored in the edition's
configurable_document_typecolumn. - 'schema': The schema for the document type. Each type must have a root schema of the type "object". This root object contains the
propertiesfor the document type, which map to content blocks in the application. - 'associations': The associations for the document type. This is a list of strings that map to a set of association objects in the Rails app. All associations are included as concerns on the corresponding edition model (such as
StandardEdition), and then required depending on whether they are included in the document type's configuration. - 'settings': A set of configurations for the content type, including edition behaviours that we want to turn on, downstream information or admin-side rendering details.
These are the settings available for configurable document types.
| Key | Required | Description |
|---|---|---|
| edit_screens | Y | A list of rendered screens (tabs) for the content type. Controls which content blocks appear on which editing screens (such as the document and images tabs). |
| base_path_prefix | Y | The prefix for the base path at which the document will be published e.g. '/government/history for the page /government/history/10-downing-street'. |
| configurable_document_group | N | Optional grouping for document types. Used for categorisation in the admin interface, including search filters. Typically matches the publishing_api_schema_name. For example, both news_story and government_response types would have the group news_article. |
| publishing_api_schema_name | Y | Name of the schema in the Publishing API for this document type. Different types might share the same schema name. For example, news_story and government_response types both use the news_article schema. |
| publishing_api_document_type | Y | The Publishing API document type for documents of this type. |
| rendering_app | Y | The frontend application responsible for rendering this document type. |
| images_enabled | Y | Whether users can upload images for this document type. When enabled, it renders the Images tab. |
| send_change_history | Y | Whether to send change history to the Publishing API. The change history will allow users to see major change updates to the document. Most editionable types should have this enabled. |
| file_attachments_enabled | Y | Whether file attachments are allowed for this document type. When enabled, it renders the Attachments tab. |
| organisations | Y | An array of organisation content IDs. Only users from one of the listed organisations will be able to use the document type. Use "null" to allow all users to use the document type. |
| backdating_enabled | Y | Whether backdating is allowed for this document type. This is required for documents representing past printed papers or documents from other digital sources outside Whitehall. |
| history_mode_enabled | Y | Whether history mode is enabled for this document type. This controls whether the document can be marked as political. |
| translations_enabled | Y | Whether translations are supported for this document type. |
Content Blocks
Each property within a configurable document schema is represented in the application as a content block. The content block is specified via the "type" and "format" options on the property schema. Content blocks are defined in the app/models/configurable_content_blocks directory.
NB: The title and summary attributes for standard editions are not stored in the block content, but rather as first-class attributes on the edition model itself.
Content blocks are instantiated via the content block factory at the point of rendering the form or generating the Publishing API payload. The factory uses the type and format specified in the schema to determine which block class to instantiate. The block's type is used for automatic type casting in the block content model.
Each content block implements the following methods:
-
publishing_api_payload(content): Returns the value to be sent to Publishing API for the property. This can return any type. If returning a hash, ensure you use symbols for the keys. -
to_partial_path: Returns the path to the view that renders the form control for the property.
Block rendering
When we render the form for a standard edition, we initialise a "root" block" of type object. When this root block's view gets rendered at the view specified in the DefaultObject's block to_partial_path, we loop through the block's schema properties and in turn initialise and render blocks matching the specified schemas. The blocks will render in the order they are defined in the schema.
The default object block is a recursive block type, as it can contain other object blocks within its properties. This allows us to build arbitrarily deep trees of content blocks, as defined by the document type's schema. The following is an example of a simple schema with nested object properties:
{
"root_object": {
"title": "Root Object",
"type": "object",
"properties": {
"leaf_property_one": {
"title": "Leaf Property One",
"type": "string",
"format": "govspeak"
},
"object_property": {
"properties": {
"leaf_property_two": {
"title": "Leaf Property Two",
"type": "string",
"format": "image_select"
}
}
}
}
}
}
The rendering process would be as follows:
- renders the standard edition form
- renders
root_objectwhich is aDefaultObjectblock (using the_default_object.html.erbpartial) - loops through the
root_objectproperties:- renders
leaf_property_onewhich is aGovspeakblock (using the_govspeak.html.erbpartial) - renders
object_propertywhich is aDefaultObjectblock (using the_default_object.html.erbpartial)- loops through the
object_propertyproperties:- renders
leaf_property_twowhich is anImageSelectblock (using the_image_select.html.erbpartial)
- renders
- loops through the
- renders
Block views use the following partial-local variables:
- The property
schemaandcontent(default{}) are provided for the specific part of the tree being rendered by the content block. The content is for the edition's primary locale. - The location in the tree is specified by the immutable
pathobject, which provides convenience methods for doing things such as building the correct name attribute for the form control. If you are rendering child properties for an "object" block, ensure that you push a new segment onto the path (see the default object implementation for an example). - The
root(defaultfalse) attribute, which is only set totruefor the rendering of the originalDefaultObjectblock that wraps all the other blocks in the schema. - The
requiredattribute. This is set totrueif the parent object validates that the block's underlying attribute is present. - The
right_to_left(defaultfalse) attribute. This is set to true if the locale for the edition translation is set to a language which is read from right to left. - The
translated_content(defaultnil) attribute. If the edition is a translation, then this will be populated with the translated content. Blocks must populate their values with the translated content if it is provided, and may wish to show the content for the primary locale as an aid to the user. - The
errors(default[]) attribute. The validation errors for the edition. Use theerrors_forhelper function and pass it both the errors and the attribute "path" to pass the attribute errors to the form control component.
Publishing API Payload
In order to compose the details hash for Publishing API, The StandardEditionPresentercalls the DefaultObject's publishing_api_payload. This then loops through all its nested blocks, calling their publishing_api_payload methods respectively.
Relevant payload settings for Publishing API are base_path_prefix, publishing_api_schema_name, and publishing_api_document_type, defined in the document type's configuration.
Create a new content block type
- Add a new block class to the content blocks directory
- Implement the
publishing_api_payloadandto_partial_pathmethods.
- Implement the
- Add the block type to the blocks factory
- The factory's
blocksmethod returns a hash that maps each block type and format to a constructor lambda. The constructor lambda receives the configurable document edition object as its only argument. Any values from the edition object needed by the block can be passed to the block's initialize method, e.g. theImageSelectblock is passed the edition's images.
- The factory's
- Create a view partial for the block in the
app/views/admin/configurable_content_blocksdirectory- The name of the partial should correspond to the block class name.
Using a content block in the schema
To use a block, include it in the schema with the following properties:
-
title: Display label for the content block. -
description: (Optional) Description of the content block's purpose. Usually used for hint text. -
type: Data type for the content block. Should map to an active record type, with the exception of theobjectandarraytype. -
format: Format for the content block, e.g., 'govspeak' for rich text, 'image_select' for image picker. Formats represent specific use cases of some of the types. This must match one of the formats registered in the blocks factory. -
validations: (Optional) Validations to be applied to the content block. See the Content Block Validation section for more details.
Content Block Validation
Rails validations can be applied to properties by adding a validations key to the property's object schema. The value for the validations key should be an object. The keys for the object must map to a validator, as defined in the 'block content' model. The value for each key is an object which will be passed to the validator constructor. The attributes represents an array of the names of the attributes to be validated, required for all but custom validators. Other options may be passed depending on what arguments the validator's initialize method accepts.
NB: This structure means that validations technically sit at the "parent" level, so that most schemas have validations at the root level, validating the properties immediately under, such as body. Any subsequent object type blocks would have validations defined for their nested attributes.
Example:
{
"properties": {
"body": { },
"image": { }
},
"validations": {
"presence": {
"attributes": [
"body"
]
},
"max_file_size_custom_validator": {
"attributes": [
"image"
],
"maximum_file_size": 9000
}
}
}
For more complex validation logic, you must define your custom validators, such as the duration for topical events. These custom validators are still decoupled from any schema and tested independently. Though they may be specific to a document type, they can be reused across multiple types if needed.
Associations
Associations can be added to configurable document types to link them to other content in Whitehall, for example, organisations or topical events. These associations are defined in the associations key in the document type's JSON configuration file. The associations are rendered on the document form in the order they are defined in the configuration.
The available associations are:
| Association | Description |
|---|---|
ministerial_role_appointments |
An appointment links a ‘ministerial role’ to a ‘person’ for a certain duration of time. |
topical_events |
Topical events are used to communicate government activity about high-profile events or in response to a major crisis. For example, a war, pandemic, the death of a royal or the Budget. |
world_locations |
Here is the canonical list of locations we use. |
worldwide_organisations |
A worldwide organisation is a British embassy, High Commission or Consulate General in a worldwide location. |
organisations |
Includes lead and supporting organisations. |
Architecture
Configurable associations have been implemented using plain old Ruby objects that bundle together the behaviour of the association in a single object, rather than distributing it across several locations in the codebase. The exception to this is persistence behaviour, which remains within the existing Active Record edition concerns. In the future, once the current document types have been migrated to the configurable document type architecture, it may be possible to merge the concerns and the association classes. Until then, any Active Record configuration of the association should take place in the Standard Edition model or the concern.
We use a factory pattern to isolate the association from Active Record, so that the association cannot manipulate the edition model and cause unexpected side effects elsewhere in the application.
The associations are consumed by the standard edition form and the standard edition presenter. They each iterate through the list of associations configured for the document type. The standard edition form renders each association, and the standard edition presenter outputs the links for each association.
Adding a new association
Adding a new type of association involves changes to several files to handle both the admin interface for selecting the association and the data that is sent to the Publishing API.
The process is as follows:
-
Create an association class: Add a new class to the
app/models/configurable_associationsdirectory. -
Update the factory: The factory at
app/models/configurable_associations/factory.rbis responsible for instantiating the association classes. You need to add the new association to theassociationshash in this file. The key should match the one used in the document type's JSON configuration. -
Create a view partial: To allow users to select the association in the admin interface, you need to create a new ERB partial in the
app/views/admin/configurable_associationsdirectory. The name of the partial should correspond to the key of your new association (e.g.,_my_new_association.html.erb). This partial will be rendered on the document's edit page. Add theto_partial_pathmethod to the association class. Theto_partial_pathmethod should return the path of the partial template. The partial will have a variable in scope that matches the name of the association class, in snake case, which you can use to access data and methods from the association object. -
Presenter links: The
app/presenters/publishing_api/standard_edition_presenter.rbis responsible for generating the payload that is sent to the Publishing API. Thelinksmethod in this presenter iterates over the configured associations for a document type and calls thelinksmethod on each association object to build up the links hash for the payload. Ensure your new association class provides the correctly formatted links hash.