Extend a profile or group page#
This guide shows you how to put your own component on a profile page or a group page without shadowing either view.
The worked example is a row of badges under somebody's name saying which teams they belong to—the case that made the third slot exist.
The three slots#
Slot |
Renders |
Rendered by |
|---|---|---|
|
Above the whole view |
Volto's own |
|
Under the heading, above the description |
|
|
Below the whole view |
Volto's own |
The two outer slots need nothing from this package. Both views are registered
in config.views.contentTypesViews, and Volto's View renders aboveContent
and belowContent around whatever it resolves there, so a component registered
into either reaches a profile page the day you install it.
belowTitle is different: nothing outside a view can place anything inside one,
so each view renders that slot itself. It is where what belongs to the person
rather than to the page reads as part of their name.
1. Write the component#
A slot component is an ordinary component. It is given the content it is
rendered for—the serialized UserProfile or UserGroup—along with location,
navRoot, and data.
const TeamBadges = ({ content }) => {
const teams = content.group_ids ?? [];
if (!teams.length) {
return null;
}
return (
<p className="team-badges">
{teams.map((team) => (
<span key={team.token}>{team.title}</span>
))}
</p>
);
};
export default TeamBadges;
Return null rather than an empty element when there is nothing to draw. A slot
that renders nothing adds no markup at all, and that is the state most pages are
in.
Read the payload rather than fetching anything. A profile arrives with its
groups already on it: group_ids comes from the
pas.plugins.identity.group_membership behavior, whose field is a choice over
a vocabulary of groups, so each entry is serialized as a term—the token a
group is stored under, and the title a reader recognizes.
Profiles and groups lists every field both types carry.
2. Register it#
Register the component from your add-on's applyConfig, the same place you
register anything else.
import TeamBadges from './components/TeamBadges';
export default function applyConfig(config) {
config.registerSlotComponent({
slot: 'belowTitle',
name: 'team-badges',
component: TeamBadges,
});
return config;
}
name identifies the registration within the slot, and each name renders at
most one component: the last one registered under it whose predicates all pass.
Registering your own component under a name a package you depend on used is
therefore how you replace what it renders, and a name of your own is how you
add to it.
3. Narrow it to the pages it belongs on#
Both views render the same slot, so a component registered into belowTitle
appears on group pages as well as profiles. predicates decides where it runs:
each is a function given the same arguments as the component, and every one of
them has to return true.
config.registerSlotComponent({
slot: 'belowTitle',
name: 'team-badges',
component: TeamBadges,
predicates: [({ content }) => content['@type'] === 'UserProfile'],
});
Predicates also read location, which is how a component is restricted to one
part of a site rather than to one content type.
Verify#
Open a profile whose owner is in at least one group. The badges are between the heading and the biography, and the page has not otherwise changed.
Open a group page. With the predicate above, no badges are there. Take the predicate out, reload, and they are—which is the check that the registration is live rather than that the profile happened to render.
A profile whose owner is in no group renders exactly as it did before.
Next steps#
Frontend—everything the add-on registers, including the routes these pages live on
Profiles and groups—the fields a profile and a group carry
About profiles and groups—why membership is stored on the member
Install the frontend—installing the add-on this extends