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. - 'forms': The forms configuration for the document type. This describes how the document type's fields are presented on the UI. Each form (e.g.,
documents,images) contains a hash offieldshash whose keys should match attributes defined inschema.attributes. See Block Rendering section for more details on how forms are rendered. - 'schema': The schema for the document type. It contains
attributesto define the data type for each form field andvalidationsto specify what validations to run against the attributes. More details about attributes in content blocks. - 'presenters': The presenters for the document type. This helps to define a list of presenters that will consume the document type's content. Each presenter contains a hash of keys, matching model attributes defined in
schema.attributes, whose values should map to their presenter's corresponding BlockContent payload builder method - see the Publishing API Payload section for more details. - '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 |
|---|---|---|
| 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 attribute in schema.attributes holds the data type for its corresponding content block. The block's type is used for automatic type casting in the block content model.
Content blocks are specified via the "block" property in the forms configuration. All available 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 a unique block identifier to determine which block class to instantiate.
Each content block implements the following methods:
-
to_partial_path: Returns the path to the view that renders the form control for the property.
Block rendering
Each form in the configuration is rendered as an object block. When we render the form for a standard edition, we initialise a DefaultOject block. The schema passed to the render method only selects the documents form from the configuration. This selection is not yet schema driven, but hardcoded in the standard edition view. For example, we also render images by hardcoding the form selection in the images component, which then renders the "Images" tab.
When the default object block's view gets rendered at the view specified in the DefaultObject's block to_partial_path, we loop through the corresponding form's fields and initialise and render blocks matching the specified schemas. The blocks will render in the order they are defined in the fields hash.
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 in the forms hash. The following is an example of a configuration that would display two tabs for a standard edition. The first tab also contains nested fields.
{
"forms": {
"form_corresponding_to_a_tab": {
"fields": {
"leaf_property_one": {
"title": "Leaf property one",
"description": "A block in a tabbed content view",
"block": "govspeak"
},
"nested_object": {
"title": "Nested object",
"block": "default_object",
"fields": {
"leaf_property_in_nested_object_one": {
"title": "Nested leaf one",
"block": "default_date"
},
"leaf_property_in_nested_object_two": {
"title": "Nested leaf two",
"block": "default_string"
}
}
}
}
},
"another_form_corresponding_to_a_tab": {
"fields": {
"leaf_property_two": {
"title": "Leaf property two",
"description": "A block in another tabbed content on the page",
"block": "image_select"
}
}
}
}
}
The rendering process would be as follows:
- Renders a tab view, whose rendering is typically controller by some config-driven setting, or is always rendered, such as the edition "Documents" tab.
- Renders a
DefaultObjectblock using the_default_object.html.erbpartial. We pass in theforms.form_corresponding_to_a_tabschema. - Loops through the
fieldsproperties:- Renders
leaf_property_onewhich is aGovspeakblock (using the_govspeak.html.erbpartial) - Renders
nested_object, which is anotherDefaultObjectblock.- Loops through the
nested_object'sfieldsproperties:- Renders
leaf_property_in_nested_object_one, which is aDefaultDateblock (using the_default_date.html.erbpartial) - Renders
leaf_property_in_nested_object_two, which is aDefaultStringblock (using the_default_string.html.erbpartial)
- Renders
- Loops through the
- Renders
- Renders the second tab by repeating the process for
another_form_corresponding_to_a_tab.
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 model validations specifypresencefor the current block's key. - The
required_attributesattribute. Includes all model attributes validated for presence. Passed down the tree to cater for deeply nested fields. - 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
The StandardEditionPresenter uses the document type's presenters configuration to compose the details hash for Publishing API. The presenters hash maps each attribute (defined in schema.attributes) to its corresponding block content payload builder method.
For example, a presenter configuration will look like:
{
"presenters": {
"publishing_api": {
"body": "govspeak",
"image": "image_select"
}
}
}
This instructs the presenter that the body attribute should use the govspeak payload builder and the image attribute should use the image_select payload builder.
Each block type maps to a publishing API payload builder method (see app/presenters/publishing_api/payload_builder/block_content.rb), which is called for the attributes configured in the presenter.
This separation allows the same attribute to be presented differently in the UI (via the forms configuration) and in the Publishing API payload, providing flexibility for future changes.
Create a new content block type
- Add a new block class to the content blocks directory
- Implement the
to_partial_pathmethods.
- Implement the
- Add the block type to the blocks factory
- The factory's
build_blockmethod 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.
- 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.
- If your block must use a new data type, you might need to make changes to the block content model
- Ensure you can present the content managed by your block. Check the presenter level block content abstraction. If you're adding a new data type, you might also need to add a new builder method.
Using a content block in the schema
To use a content block, you need to define it in both the schema and forms:
-
Define the UI in
forms.<form_tab>.fields.<field_name>:-
title: Display label for the field. -
description: (Optional) Help text shown to the user. -
block: Block component to use (e.g.,govspeak,image_select). Must match a format registered in the blocks factory.
-
-
Define the data type in
schema.attributes.<field_name>:-
type: Data type for the attribute (e.g.,string,integer,date). Used for type casting in the block content model.
-
-
Define the Publishing API mapping in
presenters.publishing_api:- Maps the attribute name to its payload builder method (e.g.,
"body": "govspeak").
- Maps the attribute name to its payload builder method (e.g.,
-
Add other presenters as needed, following the same mapping structure above.
Content Block Validation
Rails validations can be applied to properties by adding a validations key to schema, alongside the attributes specification. 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:
{
"schema": {
"attributes": {
"body": {
"type": "string"
},
"image": {
"type": "integer"
}
},
"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. 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.