Skip to Content
CustomizeModel UIRecord landing page

Record landing page

The record landing page displays detailed information about a single record. This page is rendered through a generic JinjaX component provided by oarepo-ui that can be customized via model-specific partial templates.

Architecture

The record detail page follows this flow:

  1. User accesses a record URL (e.g., /mymodels/<pid_value>)
  2. UI Resource view (record_detail) fetches the record from the service layer
  3. Resource components prepare additional context data
  4. Page component renders the page using base template and model partials

The default page component (oarepo_ui.pages.RecordDetail) renders the record using your model’s base template. You can customize specific parts by overriding partial templates, or replace the entire page component.

Default Page Component

By default, oarepo-ui uses a pre-defined JinjaX page component (oarepo_ui.pages.RecordDetail) that renders your record. This component extends your model’s base template and includes your model-specific partials.

Base Template

The base template used by the default page component (source ):

oarepo_ui/templates/oarepo_ui/record_detail.html
{% extends "invenio_app_rdm/records/detail.html" %} {%- block javascript %} {{ super() }} {% if not embedded %} {{ webpack["record_sharing.js"] }} {% endif %} {%- endblock javascript %}

The embedded flag controls certain UI features when the detail page is rendered within another context (e.g., inside a modal or iframe).

Page Component Props

The default page component (oarepo_ui.pages.RecordDetail) receives the following props, which are passed as context variables to your model’s templates:

PropDescription
recordAPI record object with id, pid, links, metadata, access, files
record_uiRecord metadata serialized for UI representation
filesRecord files entries
media_filesSystem/media files entries
permissionsUser permissions to record (can_edit, can_update, can_manage)
is_previewWhether viewing in preview mode
include_deletedWhether to include deleted records
is_draftWhether record is a draft
modelRecord model configuration
model_nameName of the record model (e.g.: mymodel)
communityCommunity metadata if record belongs to one
community_uiCommunity data serialized for UI representation
user_avatarURL of the current user’s avatar image
record_owner_idID of the record owner user
ui_linksUI navigation links
extra_contextAdditional context from resource components
dShorthand for record.metadata

Template Structure

The default page component extends your model’s base template ({model_name}/record_detail.html), which is provided by nrp-model-copier. This template includes your model-specific partials and extends oarepo_ui/record_detail.html.

For a model named mymodel, the structure is:

        • record_detail.html
          • banners.html
          • main.html

The record_detail.html template (source ):

ui/mymodel/templates/semantic-ui/mymodel/record_detail.html
{% extends "oarepo_ui/record_detail.html" %} {%- block page_banners %} {% include "mymodel/record_detail/banners.html" %} {%- endblock page_banners %} {%- block page_main %} {% include "mymodel/record_detail/main.html" %} {%- endblock page_main %} {%- block record_sidebar %} {% include "mymodel/record_detail/side_bar.html" ignore missing %} {%- endblock record_sidebar %}

Available Blocks in record_detail.html

BlockPurpose
bannersBanner region for community, preview, version notices
record_bodyMain content wrapper
record_sidebarRight sidebar content
cssPage-specific CSS includes
head_metaHTML meta tags
javascriptJavaScript includes

UI Resource Configuration

The record detail route is defined in your model’s UI resource config:

ui/mymodel/__init__.py
from oarepo_ui.resources.records.config import RecordsUIResourceConfig class MymodelUIResourceConfig(RecordsUIResourceConfig): blueprint_name = "mymodel" url_prefix = "/mymodel" routes = { "record_detail": "/records/<pid_value>", # ... other routes }

See UI Resource Views for full details on UI resources.

record_detail/banners.html

Add model-specific banners at the top of the detail page. By default, no banners.html is shipped with nrp-model-copier—you can create one to customize.

Create ui/mymodel/templates/semantic-ui/mymodel/record_detail/banners.html:

ui/mymodel/templates/semantic-ui/mymodel/record_detail/banners.html
{# Custom banners for your model #} {%- if extra_context.custom_banner -%} <div class="ui message info"> <i class="info icon"></i> {{ extra_context.custom_banner }} </div> {%- endif -%} {%- if is_preview -%} <div class="ui message warning"> <i class="eye icon"></i> {{ _("Preview mode - this record is not published") }} </div> {%- endif -%}

If you don’t create banners.html, the base template uses default banners from oarepo_ui/record_detail/banners.html.

Extending Default Banners

To add to the default banners rather than replace them:

ui/mymodel/templates/semantic-ui/mymodel/record_detail/banners.html
{% extends "oarepo_ui/record_detail/banners.html" %} {%- block banner_preview_header -%} {{ super() }} {%- if extra_context.custom_warning -%} <div class="ui message warning"> <i class="exclamation triangle icon"></i> {{ extra_context.custom_warning }} </div> {%- endif -%} {%- endblock -%}

record_detail/main.html

The only partial template shipped by nrp-model-copier by default. Edit this file to customize the main content area:

ui/mymodel/templates/semantic-ui/mymodel/record_detail/main.html

This template extends oarepo_ui/record_detail/main.html and provides these overridable blocks:

BlockPurpose
record_bodyMain content wrapper
record_headerHeader section
record_header_button”Back to edit” button (preview mode)
record_header_infoPublication date, version, access status
record_titleRecord title and creators/contributors
record_contentDescription/abstract section
record_filesFiles list and preview
record_files_access_requestAccess request form for restricted files
record_media_filesSystem/media files
additional_record_detailsAdditional metadata (details.html)
record_footerFooter section
jump”Jump to top” button

Use {{ super() }} within override blocks to preserve the parent template content. Omit {{ super() }} to completely replace the section. See Templating: Jinja for more on template inheritance.

Customizing Main Content

Create/edit ui/mymodel/templates/semantic-ui/mymodel/record_detail/main.html to customize the main content area:

ui/mymodel/templates/semantic-ui/mymodel/record_detail/main.html
{% extends "oarepo_ui/record_detail/main.html" %} {# Add badge after title #} {%- block record_title -%} {{ super() }} <span class="ui label">MyModel</span> {%- endblock -%} {# Add custom metadata section #} {%- block additional_record_details -%} {{ super() }} <div class="ui segment"> <h3>MyModel Metadata</h3> <p>{{ d.custom_field }}</p> </div> {%- endblock -%}

Hide Default Elements

Override a block to hide it (omit {{ super() }}):

ui/mymodel/templates/semantic-ui/mymodel/record_detail/main.html
{%- block record_header_info -%} {# Hide publication date and version by not calling super() #} {%- endblock -%}

record_detail/record_sidebar.html

The sidebar is rendered via the record_sidebar block in record_detail.html, which by default includes the standard Invenio sidebar. You can customize it in two ways:

Add your widget to APP_RDM_DETAIL_SIDE_BAR_TEMPLATES in invenio.cfg:

invenio.cfg
APP_RDM_DETAIL_SIDE_BAR_TEMPLATES = [ "invenio_app_rdm/records/details/side_bar/versioning.html", "mymodel/record_detail/record_sidebar.html", # Your custom widget ]

Create your widget at ui/mymodel/templates/semantic-ui/mymodel/record_detail/record_sidebar.html:

ui/mymodel/templates/semantic-ui/mymodel/record_detail/custom_widget.html
<div class="ui segment"> <h3>Custom Widget</h3> <p>Your custom content here.</p> </div>

Option 2: Override via Partial Template

Create a record_sidebar.html partial to replace the default sidebar content:

ui/mymodel/templates/semantic-ui/mymodel/record_detail/record_sidebar.html
<div class="ui segments"> <div class="ui segment"> <h3>Custom Sidebar</h3> <p>Your custom content here.</p> </div> </div>

The base template includes your partial with {% include model_name ~ "/record_detail/record_sidebar.html" ignore missing %}. Create the file to override; omit it to use the default.

Custom CSS, JavaScript, and Meta Tags

Add model-specific assets without overriding the entire template:

Custom CSS

Create ui/mymodel/templates/semantic-ui/mymodel/record_detail/css.html:

ui/mymodel/templates/semantic-ui/mymodel/record_detail/css.html
<style> .my-model-detail .record-title { color: #2185d0; } </style>

Custom JavaScript

Create ui/mymodel/templates/semantic-ui/mymodel/record_detail/javascript.html:

ui/mymodel/templates/semantic-ui/mymodel/record_detail/javascript.html
<script> // Add interactivity to your detail page document.addEventListener('DOMContentLoaded', function() { console.log('MyModel detail page loaded'); }); </script>

Custom Meta Tags

Create ui/mymodel/templates/semantic-ui/mymodel/record_detail/head_meta.html:

ui/mymodel/templates/semantic-ui/mymodel/record_detail/head_meta.html
<meta name="my-model-specific" content="custom value"> <meta property="og:type" content="article">

These partials are automatically included by the base template if they exist. No registration needed—just create the files.

Using Resource Components

Add custom context data through resource components:

ui/mymodel/components.py
from oarepo_ui.resources.components import UIResourceComponent class MymodelDetailComponent(UIResourceComponent): def before_detail(self, resource, request, extra_context, **kwargs): # Add custom data to template context record = extra_context.get("record") if record: extra_context["related_records"] = self._get_related(record) def _get_related(self, record): # Fetch related records from service layer return []

Register in your UI resource config:

ui/mymodel/__init__.py
from .components import MymodelDetailComponent class MymodelUIResourceConfig(RecordsUIResourceConfig): components = [ MymodelDetailComponent, ]

Access custom context in any override block:

ui/mymodel/templates/semantic-ui/mymodel/record_detail/main.html
{%- block additional_record_details -%} {{ super() }} {% if related_records %} <div class="related-records"> <h3>Related Records</h3> {% for rec in related_records %} <div class="related-item"> <a href="{{ rec.links.self_html }}">{{ rec.metadata.title }}</a> </div> {% endfor %} </div> {% endif %} {%- endblock additional_record_details -%}

Replacing the Default Landing Page Template

There are three ways to customize the record detail page, listed in order of increasing customization:

By default, oarepo-ui uses the pre-defined page component (oarepo_ui.pages.RecordDetail). No configuration is required. The default component:

  • Extends your model’s record_detail.html template
  • Includes your model’s record_detail/banners.html and record_detail/main.html partials
  • Provides all context variables to your templates

This is the recommended approach unless you need custom page-level logic.

Option 2: Custom Page Component

If you need full control, create a JinjaX component that extends your model’s template:

Step 1: Create the JinjaX component (RecordDetail.jinja):

ui/mymodel/templates/semantic-ui/mymodel/RecordDetail.jinja
{# def record, record_ui, files, model_name, ... #} {% extends model_name ~ "/record_detail.html" %}

Step 2: Create your model’s template (record_detail.html):

ui/mymodel/templates/semantic-ui/mymodel/record_detail.html
{% extends "oarepo_ui/record_detail.html" %} {% block page_body %} <div class="my-custom-layout"> <h1>{{ d.title }}</h1> {{ super() }} </div> {% endblock %}

Step 3: Register using dot notation:

ui/mymodel/__init__.py
class MymodelUIResourceConfig(RecordsUIResourceConfig): templates = { "record_detail": "mymodel.RecordDetail", }

Two files required: The JinjaX component (RecordDetail.jinja) declares variables and extends your model template. Your model template (record_detail.html) extends the base template and overrides blocks.

For most customization needs, you don’t need to create a full page component. Instead, override specific blocks in your model’s partial templates. The default page component includes these partials, and you only customize what you need.

          • banners.html
          • main.html

This is the most maintainable approach as you only need to customize the specific parts of the UI you want to change, and automatically get updates to the base template.

How the Default Page Component Works

When you don’t configure a custom template in templates["record_detail"], the system uses the default page component (oarepo_ui.pages.RecordDetail).

Component Resolution Process

  1. Template resolution: The system looks up config.templates["record_detail"]
  2. If not configured: The default component oarepo_ui.pages.RecordDetail is used
  3. Rendering: The component extends your model’s {model_name}/record_detail.html template

Default Component Structure

The default page component (oarepo_ui/pages/RecordDetail.jinja) looks like:

{# def record, record_ui, files, media_files, permissions, is_preview, include_deleted, is_draft, model, model_name, community, community_ui, user_avatar, record_owner_id, ui_links, extra_context, d, #} {% extends model_name ~ "/record_detail.html" %}

This component extends your model’s base template (mymodel/record_detail.html), which in turn extends oarepo_ui/record_detail.html and includes your partials (banners.html, main.html, side_bar.html).

Error Pages

When users encounter errors accessing records, oarepo-ui renders appropriate error pages. These can be customized via the templates configuration.

Not Found Page (404)

The not found page is displayed when a record does not exist or the PID is invalid.

Default Template

The default not found template (source ):

oarepo_ui/templates/oarepo_ui/not_found.html
{% extends config.THEME_ERROR_TEMPLATE %} {% block page_body %} <div class="ui container centered rel-pt-2"> <h1 class="ui header inline-block"> <i class="icon search" aria-hidden="true"></i> <div class="content pl-0 rel-pr-1"> {{ _("Not found") }} </div> </h1> <p> {%- trans sitename=config.THEME_SITENAME %} The record you are trying to access does not exist in the repository. {%- endtrans %} </p> {% block pid_info %} {% if pid %} <p> {{ _("PID") }}: <code>{{ pid }}</code> </p> {% endif %} {% endblock pid_info %} </div> {% endblock page_body %}

Customizing the Not Found Page

Add a “Did you mean?” feature that suggests corrected record IDs:

ui/mymodel/templates/semantic-ui/mymodel/NotFound.jinja
{# def pid, model_name #} {% extends model_name ~ "/not_found.html" %}
ui/mymodel/templates/semantic-ui/mymodel/not_found.html
{% extends config.THEME_ERROR_TEMPLATE %} {% block page_body %} <h1>{{ _("Record Not Found") }}</h1> {% if pid %} {# Fix common typos: O→0, l→1 #} {% set fixed = pid|replace('O', '0')|replace('l', '1') %} {% if fixed != pid %} <div class="ui message"> <p>{{ _("Did you mean:") }} <a href="/records/{{ fixed }}">{{ fixed }}</a>?</p> </div> {% endif %} {% endif %} {% endblock %}

Tombstone Page (410 Gone)

The tombstone page is displayed when a user tries to access a deleted record. The metadata is kept for archival purposes, but the record is no longer accessible.

Error Handlers

By default, the following exceptions trigger the tombstone page:

ExceptionDescription
PIDDeletedErrorThe PID has been marked as deleted
RecordDeletedExceptionThe record has been deleted

These are configured in error_handlers in RecordsUIResourceConfig.

Default Template

The default tombstone template (source ):

oarepo_ui/templates/oarepo_ui/tombstone.html
{% extends config.THEME_ERROR_TEMPLATE %} {% block page_body %} <div class="ui container centered rel-pt-2"> <h1 class="ui header inline-block"> <i class="icon lightning" aria-hidden="true"></i> <div class="content pl-0 rel-pr-1"> {{ _("Gone") }} </div> </h1> <p> {%- trans sitename=config.THEME_SITENAME %} The record you are trying to access was removed. The metadata of the record is kept for archival purposes. {%- endtrans %} </p> {% block tombstone_content %} {% if tombstone %} <table class="ui table"> <tbody> {% for key, value in tombstone.items() %} {% if key and value %} <tr> <th class="left aligned pl-5">{{ _(key) }}:</th> <td> {% if key == "URL" %} <a href="{{ value }}" target="_blank">{{ value }}</a> {% else %} {{ value }} {% endif %} </td> </tr> {% endif %} {% endfor %} </tbody> </table> {% else %} <p>No tombstone (e.g. removal reason, note) available.</p> {% endif %} {% endblock tombstone_content %} </div> {% endblock page_body %}

Available Variables

The tombstone template receives the following context variables:

VariableDescription
tombstoneDictionary with tombstone information (removal reason, removal date, removed by, etc.)
pidThe PID of the deleted record
config.THEME_SITENAMESite name for translations

Customizing the Tombstone Page

Add a citation update notice for researchers who may have cited this record:

ui/mymodel/templates/semantic-ui/mymodel/Tombstone.jinja
{# def tombstone, pid, model_name #} {% extends model_name ~ "/tombstone.html" %}
ui/mymodel/templates/semantic-ui/mymodel/tombstone.html
{% extends config.THEME_ERROR_TEMPLATE %} {% block page_body %} <h1><i class="icon archive"></i> {{ _("Record Removed") }}</h1> <div class="ui message warning"> {{ _("If you cited this record, please update your references.") }} </div> {% if tombstone %} <table class="ui table"> <tr><td>ID</td><td><code>{{ pid }}</code></td></tr> {% if tombstone.removal_date %} <tr><td>{{ _("Removed") }}</td><td>{{ tombstone.removal_date }}</td></tr> {% endif %} </table> {% endif %} {% endblock %}

Register in your UI resource config using dot notation:

ui/mymodel/__init__.py
class MymodelUIResourceConfig(RecordsUIResourceConfig): # ... templates = { "tombstone": "mymodel.Tombstone", }

JinjaX Component Required: When using templates configuration for error pages, you must create a valid JinjaX page component (e.g., mymodel/Tombstone.jinja) with a {# def #} block declaring all required context variables (pid, tombstone, etc.). The component should extend your model’s template. Without the {# def #} block, the template will fail to render.

Last updated on