From 399034d34ef09bfcc52162a6c934de3dc55e64fc Mon Sep 17 00:00:00 2001 From: Julian Cyliax Date: Wed, 10 Jun 2026 16:29:07 +0200 Subject: [PATCH] Add Related Items slot (#64) Renders a content item's related items as a linked list below the content via a new `RelatedItems` component registered for the `belowContent` slot in `@plone/layout`. Includes EN/DE/IT labels, a how-to guide for hiding or replacing the slot, and tests. --- docs/how-to-guides/customize-related-items.md | 50 +++++++++++++++++++ docs/how-to-guides/index.md | 1 + packages/layout/config/slots.ts | 8 +++ packages/layout/locales/de/common.json | 3 ++ packages/layout/locales/en/common.json | 3 ++ packages/layout/locales/it/common.json | 3 ++ packages/layout/news/64.feature | 1 + .../RelatedItems/RelatedItems.module.css | 10 ++++ .../slots/RelatedItems/RelatedItems.test.tsx | 40 +++++++++++++++ .../slots/RelatedItems/RelatedItems.tsx | 34 +++++++++++++ 10 files changed, 153 insertions(+) create mode 100644 docs/how-to-guides/customize-related-items.md create mode 100644 packages/layout/news/64.feature create mode 100644 packages/layout/slots/RelatedItems/RelatedItems.module.css create mode 100644 packages/layout/slots/RelatedItems/RelatedItems.test.tsx create mode 100644 packages/layout/slots/RelatedItems/RelatedItems.tsx diff --git a/docs/how-to-guides/customize-related-items.md b/docs/how-to-guides/customize-related-items.md new file mode 100644 index 000000000..696f980c7 --- /dev/null +++ b/docs/how-to-guides/customize-related-items.md @@ -0,0 +1,50 @@ +--- +myst: + html_meta: + "description": "How to hide or replace the related items list shown below content in Plone Aurora" + "property=og:description": "How to hide or replace the related items list shown below content in Plone Aurora" + "property=og:title": "Customize the related items" + "keywords": "Plone Aurora, related items, slot, belowContent" +--- + +# Customize the related items + +Plone Aurora renders a content item's related items below the content as links. +The list comes from a slot component registered for the `belowContent` slot. +This guide shows you how to hide or replace it. + +## Hide the related items + +Set `showRelatedItems` to `false` in your add-on configuration to hide the list on every page. + +```ts +import type { ConfigType } from '@plone/registry'; + +export default function install(config: ConfigType) { + config.settings.showRelatedItems = false; + return config; +} +``` + +The slot renders the list whenever an item has `relatedItems`, unless `showRelatedItems` is `false`. + +## Replace the related items component + +Register your own component for the `belowContent` slot under the same name to override the default. + +```ts +import type { ConfigType } from '@plone/registry'; +import MyRelatedItems from './MyRelatedItems'; + +export default function install(config: ConfigType) { + config.registerSlotComponent({ + name: 'RelatedItems', + slot: 'belowContent', + component: MyRelatedItems, + }); + return config; +} +``` + +Your component receives the slot props, including `content`, from which you can read `content.relatedItems`. +To learn how slots work in general, refer to {doc}`register-slots`. diff --git a/docs/how-to-guides/index.md b/docs/how-to-guides/index.md index b42b76e4c..fcb1fbcf3 100644 --- a/docs/how-to-guides/index.md +++ b/docs/how-to-guides/index.md @@ -30,4 +30,5 @@ icons configure-plate-code-block-languages bind-metadata-fields-to-plate-text-blocks custom-content-types +customize-related-items ``` diff --git a/packages/layout/config/slots.ts b/packages/layout/config/slots.ts index ab747d69c..7780bc89e 100644 --- a/packages/layout/config/slots.ts +++ b/packages/layout/config/slots.ts @@ -9,6 +9,7 @@ import HeaderTools from '../slots/Tools'; import ContentArea from '../slots/ContentArea'; import MainFooter from '../slots/MainFooter/MainFooter'; import Breadcrumbs from '../slots/Breadcrumbs'; +import RelatedItems from '../slots/RelatedItems/RelatedItems'; import { NotContentTypeCondition } from '../helpers'; export default function install(config: ConfigType) { @@ -83,5 +84,12 @@ export default function install(config: ConfigType) { component: MainFooter, }); + // Related Items + config.registerSlotComponent({ + name: 'RelatedItems', + slot: 'belowContent', + component: RelatedItems, + }); + return config; } diff --git a/packages/layout/locales/de/common.json b/packages/layout/locales/de/common.json index a9282556f..0d582f243 100644 --- a/packages/layout/locales/de/common.json +++ b/packages/layout/locales/de/common.json @@ -17,6 +17,9 @@ "toolbar": { "label": "Werkzeugleiste" }, + "relatedItems": { + "label": "Verwandte Inhalte" + }, "views": { "link": { "linkLabel": "Die Linkadresse lautet:", diff --git a/packages/layout/locales/en/common.json b/packages/layout/locales/en/common.json index 015eedda6..334c91731 100644 --- a/packages/layout/locales/en/common.json +++ b/packages/layout/locales/en/common.json @@ -17,6 +17,9 @@ "toolbar": { "label": "Toolbar" }, + "relatedItems": { + "label": "Related items" + }, "views": { "link": { "linkLabel": "The link address is:", diff --git a/packages/layout/locales/it/common.json b/packages/layout/locales/it/common.json index 6a5bf7a24..62040ccb8 100644 --- a/packages/layout/locales/it/common.json +++ b/packages/layout/locales/it/common.json @@ -17,6 +17,9 @@ "toolbar": { "label": "Barra degli strumenti" }, + "relatedItems": { + "label": "Contenuti correlati" + }, "views": { "link": { "linkLabel": "L'indirizzo del link รจ:", diff --git a/packages/layout/news/64.feature b/packages/layout/news/64.feature new file mode 100644 index 000000000..d6dc3b1c5 --- /dev/null +++ b/packages/layout/news/64.feature @@ -0,0 +1 @@ +Added a Related Items slot that renders a content item's related items as links below the content. @Cabonie diff --git a/packages/layout/slots/RelatedItems/RelatedItems.module.css b/packages/layout/slots/RelatedItems/RelatedItems.module.css new file mode 100644 index 000000000..b87d129b1 --- /dev/null +++ b/packages/layout/slots/RelatedItems/RelatedItems.module.css @@ -0,0 +1,10 @@ +@layer custom { + .relatedItems { + display: flex; + flex-direction: column; + padding: 0; + margin-top: 24px; + gap: 8px; + list-style: none; + } +} diff --git a/packages/layout/slots/RelatedItems/RelatedItems.test.tsx b/packages/layout/slots/RelatedItems/RelatedItems.test.tsx new file mode 100644 index 000000000..becb99167 --- /dev/null +++ b/packages/layout/slots/RelatedItems/RelatedItems.test.tsx @@ -0,0 +1,40 @@ +import { render, screen } from '@testing-library/react'; +import { MemoryRouter } from 'react-router'; +import { RouterProvider } from 'react-aria-components'; +import RelatedItems from './RelatedItems'; + +type Item = { '@id': string; title: string }; + +const renderRelatedItems = (relatedItems: Item[]) => + render( + + undefined}> + + + , + ); + +describe('RelatedItems slot', () => { + it('renders nothing when there are no related items', () => { + const { container } = renderRelatedItems([]); + expect(container).toBeEmptyDOMElement(); + }); + + it('renders a link for each related item', () => { + renderRelatedItems([ + { '@id': '/news/one', title: 'News One' }, + { '@id': '/events/two', title: 'Event Two' }, + ]); + expect(screen.getByRole('link', { name: 'News One' })).toHaveAttribute( + 'href', + '/news/one', + ); + expect(screen.getByRole('link', { name: 'Event Two' })).toHaveAttribute( + 'href', + '/events/two', + ); + }); +}); diff --git a/packages/layout/slots/RelatedItems/RelatedItems.tsx b/packages/layout/slots/RelatedItems/RelatedItems.tsx new file mode 100644 index 000000000..17d6d2c4f --- /dev/null +++ b/packages/layout/slots/RelatedItems/RelatedItems.tsx @@ -0,0 +1,34 @@ +import { useTranslation } from 'react-i18next'; +import { Link } from '@plone/components'; +import config from '@plone/registry'; +import type { SlotComponentProps } from '../SlotRenderer'; +import SectionWrapper from '../../components/SectionWrapper/SectionWrapper'; +import styles from './RelatedItems.module.css'; + +const RelatedItems = (props: SlotComponentProps) => { + const { t } = useTranslation(); + const items = props.content.relatedItems ?? []; + + const showRelatedItems = config.settings.showRelatedItems !== false; + if (!showRelatedItems || items.length === 0) { + return null; + } + + return ( + + + + ); +}; + +export default RelatedItems;