diff --git a/docs/how-to-guides/customize-content-tags.md b/docs/how-to-guides/customize-content-tags.md new file mode 100644 index 000000000..8cc2f66e6 --- /dev/null +++ b/docs/how-to-guides/customize-content-tags.md @@ -0,0 +1,50 @@ +--- +myst: + html_meta: + "description": "How to hide or replace the tags (subjects) shown below content in Plone Aurora" + "property=og:description": "How to hide or replace the tags (subjects) shown below content in Plone Aurora" + "property=og:title": "Customize the content tags" + "keywords": "Plone Aurora, tags, subjects, slot, belowContent" +--- + +# Customize the content tags + +Plone Aurora renders a content item's tags (its `subjects`) below the content as links to the search. +The tags come from a slot component registered for the `belowContent` slot. +This guide shows you how to hide or replace them. + +## Hide the tags + +Set `showTags` to `false` in your add-on configuration to hide the tags on every page. + +```ts +import type { ConfigType } from '@plone/registry'; + +export default function install(config: ConfigType) { + config.settings.showTags = false; + return config; +} +``` + +The slot renders the tags whenever an item has `subjects`, unless `showTags` is `false`. + +## Replace the tags 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 MyTags from './MyTags'; + +export default function install(config: ConfigType) { + config.registerSlotComponent({ + name: 'Tags', + slot: 'belowContent', + component: MyTags, + }); + return config; +} +``` + +Your component receives the slot props, including `content`, from which you can read `content.subjects`. +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..e37e5dd3f 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-content-tags ``` diff --git a/packages/layout/config/slots.ts b/packages/layout/config/slots.ts index ab747d69c..595e58c73 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 Tags from '../slots/Tags/Tags'; import { NotContentTypeCondition } from '../helpers'; export default function install(config: ConfigType) { @@ -70,6 +71,13 @@ export default function install(config: ConfigType) { component: ContentArea, }); + // Tags (Subjects) + config.registerSlotComponent({ + name: 'Tags', + slot: 'belowContent', + component: Tags, + }); + // Footer Slot config.registerSlotComponent({ name: 'Footer', diff --git a/packages/layout/locales/de/common.json b/packages/layout/locales/de/common.json index a9282556f..06e62b472 100644 --- a/packages/layout/locales/de/common.json +++ b/packages/layout/locales/de/common.json @@ -17,6 +17,9 @@ "toolbar": { "label": "Werkzeugleiste" }, + "tags": { + "label": "Schlagwörter" + }, "views": { "link": { "linkLabel": "Die Linkadresse lautet:", diff --git a/packages/layout/locales/en/common.json b/packages/layout/locales/en/common.json index 015eedda6..eff3820e1 100644 --- a/packages/layout/locales/en/common.json +++ b/packages/layout/locales/en/common.json @@ -17,6 +17,9 @@ "toolbar": { "label": "Toolbar" }, + "tags": { + "label": "Tags" + }, "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..a83be7deb 100644 --- a/packages/layout/locales/it/common.json +++ b/packages/layout/locales/it/common.json @@ -17,6 +17,9 @@ "toolbar": { "label": "Barra degli strumenti" }, + "tags": { + "label": "Etichette" + }, "views": { "link": { "linkLabel": "L'indirizzo del link è:", diff --git a/packages/layout/news/63.feature b/packages/layout/news/63.feature new file mode 100644 index 000000000..a728212e1 --- /dev/null +++ b/packages/layout/news/63.feature @@ -0,0 +1 @@ +Added a Tags (Subjects) slot that renders a content item's tags below the content as links to the search. @nils-pzr diff --git a/packages/layout/slots/Tags/Tags.module.css b/packages/layout/slots/Tags/Tags.module.css new file mode 100644 index 000000000..be6294cb0 --- /dev/null +++ b/packages/layout/slots/Tags/Tags.module.css @@ -0,0 +1,21 @@ +@layer custom { + .tags { + display: flex; + flex-wrap: wrap; + margin-top: 16px; + gap: 8px; + } + + .tag { + padding: 4px 12px; + border-radius: 9999px; + background: var(--quanta-azure); + color: var(--quanta-puya); + font-size: 0.875rem; + text-decoration: none; + } + + .tag:hover { + opacity: 0.8; + } +} diff --git a/packages/layout/slots/Tags/Tags.test.tsx b/packages/layout/slots/Tags/Tags.test.tsx new file mode 100644 index 000000000..9131f4b3f --- /dev/null +++ b/packages/layout/slots/Tags/Tags.test.tsx @@ -0,0 +1,32 @@ +import { render, screen } from '@testing-library/react'; +import { MemoryRouter } from 'react-router'; +import Tags from './Tags'; + +const renderTags = (subjects: string[]) => + render( + + + , + ); + +describe('Tags slot', () => { + it('renders nothing when there are no tags', () => { + const { container } = renderTags([]); + expect(container).toBeEmptyDOMElement(); + }); + + it('renders a search link for each tag', () => { + renderTags(['News', 'Plone & Co']); + expect(screen.getByRole('link', { name: 'News' })).toHaveAttribute( + 'href', + '/search?Subject=News', + ); + expect(screen.getByRole('link', { name: 'Plone & Co' })).toHaveAttribute( + 'href', + '/search?Subject=Plone%20%26%20Co', + ); + }); +}); diff --git a/packages/layout/slots/Tags/Tags.tsx b/packages/layout/slots/Tags/Tags.tsx new file mode 100644 index 000000000..a676cc958 --- /dev/null +++ b/packages/layout/slots/Tags/Tags.tsx @@ -0,0 +1,33 @@ +import { Link } from 'react-router'; +import { useTranslation } from 'react-i18next'; +import config from '@plone/registry'; +import type { SlotComponentProps } from '../SlotRenderer'; +import SectionWrapper from '../../components/SectionWrapper/SectionWrapper'; +import styles from './Tags.module.css'; + +const Tags = (props: SlotComponentProps) => { + const { t } = useTranslation(); + const tags = (props.content?.subjects ?? []) as string[]; + + if (config.settings.showTags === false || tags.length === 0) { + return null; + } + + return ( + +
+ {tags.map((tag) => ( + + {tag} + + ))} +
+
+ ); +}; + +export default Tags;