User interface of MEF (Multilingual Entity File), the linked entity hub of RERO+. MEF aggregates and aligns authority records from VIAF, IdRef, GND and RERO and gives a unified identifier to persons, organisations, concepts and places.
The application, available at mef.rero.ch, provides:
- the home page, with a search field and the entity types (agents, concepts, places) with their number of records;
- the search of all MEF entities, with facets and sorting;
- the detail view of an entity: main record, one tab per source record (GND, IdRef, RERO), raw JSON of the
records, related records of the redirects (
relation_pid); - the "How MEF linking works" page;
- a light, dark or automatic (system setting) color theme.
The data comes from the REST API of the rero-mef backend.
- Angular 21 (standalone components, signals, zoneless change detection, server-side rendering)
- @rero/ng-core: search, detail view, translations and PrimeNG theme
- PrimeNG 21 with the ng-core preset
- Tailwind CSS 4
- Vitest for the unit tests
- pnpm as package manager
- Node.js 24 or later
- pnpm 12 (the exact version is set in the
packageManagerfield ofpackage.json)
pnpm install
pnpm startThen open http://localhost:4200/.
In development, the API calls (/api/...) go through the Angular dev server proxy (proxy.conf.json) to
the test server https://mef.test.rero.ch, so no local backend is needed (production:
https://mef.rero.ch). The proxy is required because the MEF servers don't send CORS headers: the browser
can't call them directly from localhost. To use a local rero-mef instance, change the target of
proxy.conf.json.
| Command | Description |
|---|---|
pnpm start |
Development server with live reload and the API proxy |
pnpm build |
Production build in dist/mef-ui |
pnpm serve:ssr |
Production server (after pnpm build), port 4000 |
pnpm serve:ssr:local |
Same, on localhost, to check the server rendering |
pnpm watch |
Development build in watch mode |
pnpm test |
Unit tests (Vitest) |
pnpm format |
Format the code with Prettier (imports sorted) |
pnpm format:check |
Check the formatting, without changing files (CI) |
The configuration is in src/environments/ and is read by AppConfigService (src/app/app-config.service.ts),
which extends the ng-core CoreConfigService.
The same production build runs on the test and production servers: the API_URL environment variable of the
server (pnpm serve:ssr) sets apiBaseUrl and $refPrefix when it starts, and the server passes them on to the
browser with the page (RuntimeConfig, src/app/runtime-config.ts). Without it, the values below apply.
API_URL=https://mef.test.rero.ch node dist/mef-ui/server/server.mjs # test server
API_URL=https://mef.rero.ch node dist/mef-ui/server/server.mjs # production server| Key | Development | Production | Usage |
|---|---|---|---|
apiBaseUrl |
'' (relative URLs) |
https://mef.rero.ch |
Base URL of the API calls made by the application |
$refPrefix |
https://mef.test.rero.ch |
https://mef.rero.ch |
Absolute URL of the records (links to the raw JSON) |
apiBaseUrl must stay empty in development, otherwise the API calls bypass the proxy and are blocked by CORS: don't
set API_URL with pnpm start.
src/
├── app/
│ ├── app.config.ts # Application providers (ng-core, PrimeNG, translations, HTTP, router)
│ ├── app.config.server.ts # Additional providers on the server
│ ├── app.routes.server.ts # Rendering mode of the routes on the server
│ ├── app.routes.ts # Routes and search configuration of the record types
│ ├── theme.service.ts # Light / dark / automatic color theme
│ ├── components/
│ │ ├── entity/ # Entities: search result (entity-brief), detail view (entity-detail),
│ │ │ # shared components (icon, type, sources, field), model and utils
│ │ ├── error/ # Error page (404)
│ │ ├── footer/ # Footer: RERO+ brand, links and versions (application and backend)
│ │ ├── frontpage/ # Home page
│ │ ├── header/ # Header: large on the home page, compact on the other pages
│ │ ├── home-search/ # Search field of the home page
│ │ ├── linking/ # "How MEF linking works" page
│ │ ├── menu/ # Header navigation (PrimeNG Menubar)
│ │ └── theme-switch/ # Color theme selector of the header
│ └── interceptors/ # HTTP interceptors (date range facets, no browser cache, page status)
├── environments/ # Configuration per environment
├── main.server.ts # Bootstrap of the application on the server
├── server.ts # Node.js (Express) server: static files and server-side rendering
├── testing/ # Test helpers
├── styles.css # Global styles: Tailwind, theme colors, base styles
└── test-setup.ts # Browser APIs missing in jsdom, for the tests
docker/ holds the production image (Dockerfile), see Docker.
The search and detail routes of ng-core are configured in app.routes.ts through record types:
| URL | Content | API endpoint |
|---|---|---|
/ |
Home page | |
/mef |
Search of all the entities | /api/all/mef/ |
/agents/detail/:pid |
Detail of an agent | /api/agents/mef/:pid |
/concepts/detail/:pid |
Detail of a concept | /api/concepts/mef/:pid |
/places/detail/:pid |
Detail of a place | /api/places/mef/:pid |
/linking |
How MEF linking works |
Notes:
- Only the search (
:type) and detail (:type/detail/:pid) routes of ng-core are used: MEF is read only, and the editor routes would add the editor dependencies to the bundle. agents,conceptsandplacesare hidden record types (hideInTabs), used for the detail views, so that a record is loaded from its own API index (the/api/all/mef/:pidredirect drops the query string).- The
/linkingand error pages are lazy loaded. - The
creation_dateandupdate_datefacets use the ng-core date range widget. ThedateRangeAggregationInterceptoradapts the request and response formats between the widget and the backend. - The
noCacheInterceptorbypasses the browser HTTP cache for the API GET requests: theETagof the API doesn't change when only an embedded source record changes, so the browser would display old data.
The pages are rendered on the server (Angular SSR), so that the search engines index the home page, the search results and the detail views, then hydrated in the browser.
- Rendering modes (
app.routes.server.ts): all the pages are rendered at each request, even the static ones (/linking), as they must contain the runtime configuration of the server (API_URL): no prerendering at build time. The home page is sent withCache-Control: public, max-age=300: its record counts change slowly, so the browsers and the proxies can keep it 5 minutes./linkingis sent withCache-Control: public, max-age=3600. - API calls: the
apiTransferCacheInterceptortransfers the API responses of the server to the browser with the page, so the browser doesn't request them again during the hydration. It replaces the HTTP transfer cache of Angular, which never stores the API responses (Cache-Control: no-cache,Set-Cookie). In development, the server calls go through the proxy too. - HTTP status: the error page answers
404, and theresponseStatusInterceptorgives the page the404or410status of the API (unknown PID), so that the error pages are not indexed. - Browser APIs:
window,document,localStorage... don't exist on the server. InjectDOCUMENTand testdefaultView(seeThemeService), or run the code inafterNextRender(). - Allowed hosts: the server only renders the requests whose
Hostheader is listed insecurity.allowedHostsofangular.json(mef.rero.ch,mef.test.rero.ch), to prevent SSRF attacks. Add a host with theNG_ALLOWED_HOSTSenvironment variable (comma separated);pnpm serve:ssr:localallowslocalhost, to check the server rendering of the production build locally. The browser calls of this build go tohttps://mef.rero.chand are blocked by CORS (new search, facets...): usepnpm startto test the application. - Proxy headers (
server.ts): the server trusts theX-Forwarded-For,X-Forwarded-HostandX-Forwarded-Protoheaders of the reverse proxies (trustProxyHeaders). With any otherX-Forwarded-*header, Angular skips the server rendering and sends the empty page shell, without the runtime configuration: the browser then callshttps://mef.rero.ch, blocked by CORS on the other hosts. The reverse proxies must replace these headers, not append to the values sent by the client. - Deployment:
pnpm buildgenerates the browser files (dist/mef-ui/browser) and the server (dist/mef-ui/server/server.mjs), run with Node.js 24 (PORTenvironment variable, 4000 by default;API_URL, see Configuration). - ng-core patch (
patches/@rero__ng-core.patch, applied by pnpm): two ng-core 21.3.0 lines break the server rendering:RecordSearchResultComponentreadswindow.location.href, andeasymde(markdown editor) is imported statically, while CodeMirror needs the DOM as soon as it is loaded. To remove once fixed in ng-core.
docker/Dockerfile builds the production image: the application is built in a Node.js image, then only the
dist folder is copied in a small Node.js Alpine image, as the server bundle contains its dependencies. The server
runs as the node user, behind tini, which forwards the stop signals.
docker build -f docker/Dockerfile -t mef-ui .
docker run -d -p 4000:4000 -e API_URL=https://mef.test.rero.ch mef-uiThe configuration is passed with environment variables, the same image runs on the test and production servers:
| Variable | Default | Usage |
|---|---|---|
API_URL |
https://mef.rero.ch |
Base URL of the MEF API (see Configuration) |
NG_ALLOWED_HOSTS |
Additional allowed host names, comma separated (localhost to test) |
|
PORT |
4000 |
Listening port of the server |
The Host header of the requests must be an allowed host name, otherwise the server answers 400: the reverse
proxy must pass on the original Host. It may send X-Forwarded-For, X-Forwarded-Host (an allowed host name)
and X-Forwarded-Proto, but no other X-Forwarded-* header (see Server-side rendering). The health check requests a static file (/favicon.ico), without
rendering nor API call. The build context is filtered by docker/Dockerfile.dockerignore.
To test the image locally, allow localhost:
docker run --rm -p 4000:4000 -e API_URL=https://mef.test.rero.ch -e NG_ALLOWED_HOSTS=localhost mef-uiThe pages are rendered by the server with the data of API_URL, but the later calls of the browser to the API (new
search, facets, pagination...) are blocked by CORS, as http://localhost:4000 is not the origin of the API.
- Tailwind first: style the templates with Tailwind utility classes.
- Semantic colors: the content colors are declared once in the
@themeblock ofsrc/styles.cssand used as utilities:bg-surface,bg-surface-muted,border-line,text-ink,text-ink-muted,text-heading,text-link,text-danger... The MEF blue (mef-blue) is used by the header, in both themes. - Dark mode: the
app-darkclass on<html>enables the dark theme, for PrimeNG (darkModeSelectorinapp.config.ts) and for Tailwind (dark:variant). The semantic colors take the PrimeNG dark palette values. The mode is chosen in the header (ThemeService) and applied before Angular starts by a script ofindex.html, to avoid a flash of the light theme. - ng-core and PrimeNG: the ng-core utility classes (
core:...) are loaded throughangular.json, afterstyles.css. PrimeNG uses the ng-core preset (primeNGConfig), whose CSS layers let the Tailwind utilities override the PrimeNG component styles. Customize PrimeNG components with their design tokens ([dt]) or pass-through classes ([pt]). - Accessibility: the pages must pass the AXE checks and follow the WCAG AA rules (contrast, focus, ARIA).
-
Standalone components with
ChangeDetectionStrategy.OnPush,input()/output()functions and signals. -
Native control flow (
@if,@for) in the templates. -
Prettier formats the code on save (VS Code settings in
.vscode/), with sorted imports: third party imports first, then a blank line, then the project imports. -
Every source file starts with the license header:
// SPDX-FileCopyrightText: Fondation RERO+ // SPDX-License-Identifier: AGPL-3.0-or-later
<!-- SPDX-FileCopyrightText: Fondation RERO+ SPDX-License-Identifier: AGPL-3.0-or-later -->
In CSS files, use a
/* ... */comment with the same lines.
The GitHub Actions workflow (.github/workflows/ci.yml) checks the formatting, runs the unit tests and
builds the application, on each push to staging and each pull request.
The Release workflow (.github/workflows/release.yml) is started by hand from the Actions tab, on staging,
once its CI is green. semantic-release computes the next version from
the conventional commits since the last tag, updates package.json and CHANGELOG.md, tags the commit,
publishes a GitHub Release, then fast-forwards master to it.
The major version follows the Angular one, as in rero-ils-ui: a breaking change only bumps the minor version.
The versions start from the v21.0.0 tag, to push once on staging before the first release.
.claude/CLAUDE.md: project instructions (Angular best practices, accessibility)..claude/skills/angular-developer: official Angular agent skill, see.claude/skills/README.mdto update it..mcp.json(Claude Code) and.vscode/mcp.json(VS Code): Angular CLI MCP server (pnpm exec ng mcp).
GNU Affero General Public License v3.0 or later
(AGPL-3.0-or-later), © Fondation RERO+.