Automatic internal linking: the first mention of another page’s name in your rendered content becomes a link to that page. Nothing is written into your source Markdown — disable the option and every auto link is gone.
composer require pushword/link-improver
The filter joins every app’s main_content chain (right after Markdown) but stays inert until you opt in per app:
pushword:
apps:
- hosts: [example.tld]
link_improver: true
# link_improver_max_links: 0.02 # the default
# link_improver_ignored_urls: ['/'] # never a target; empty by default
There is no CSV or admin screen to maintain: each line of a page’s name field is a keyword that links to it. The first line is the name Pushword displays (breadcrumb, listings); further lines are never displayed, which makes them pure linking variants — including * wildcards, which match up to 10 characters:
name: |
Tour du Mont-Blanc
TMB en 11 étapes
tour du Mont*Blanc
Commas separate keywords too, as they do in the underlying library’s CSV format. Keyword matching is case-insensitive; the inserted anchor keeps the casing found in the content. Only published pages of the same host and locale are offered as targets, redirections excluded, and a page never links itself.
Choose names accordingly: a page named Blog or Contact will attract links from everywhere its name is written.
link_improver_ignored_urls drops pages from the map without touching their name, which keeps working everywhere else (breadcrumb, listings). Write the URLs as the report prints them — / for the homepage, /slug otherwise:
link_improver_ignored_urls: ['/', '/contact']
The homepage is the usual candidate: its name is often the brand, written on nearly every page (44% of the inserted links on one real site).
The insertion engine refuses to place a link:
p, span, b, strong, em, i, li;#fragment, ?query or the absolute base_url form);link_improver_max_links bounds the total of in-content links, existing ones included. Below 1 it is a ratio of the word count — the default 0.02 means one link per 50 words; 1 or more is an absolute count.Since the cap counts existing links, it is a target density: a page already above it gets nothing, a neglected page is filled up to it. The default 0.02 is half the median density of Wikipedia prose (one link per 20 words, measured on 30 fr/en articles); real Pushword corpora sit between one per 48 and one per 720 words, with 8–49% of pages holding no internal link.
ROLE_EDITOR only: each auto link gets an indigo background tint and a title saying it was inserted. The response is then sent private, no-store; public HTML and the shared render cache are untouched.data-auto-link attribute (grep data-auto-link on a static export).php bin/console pw:link-improver renders every published page and reports each inserted link. --host example.tld narrows to one app, --simulate renders as if link_improver: true were set, and --format follows the agent-output convention.Inbound links (auto ones included) are reported by page-scanner's pw:link:graph.
Links are decorated at render time, after the Markdown/Twig pass and before the Html* link post-processors — an auto link to a page that becomes unpublished degrades exactly like an editorial one. Rendering is byte-deterministic: the map is derived from the pages ordered by slug, longest keyword first, so two renders of the same page produce the same bytes.
A page’s output depends on the other pages' names; renaming, publishing or deleting a page bumps the host’s render epoch, so static and page-cache sites regenerate what it affects.
Links inserted by the improver are registered with the link collector, so a pages_list(…, excludeAlreadyLinked: true) rendered after the content — in the template — will not offer a page the improver just linked in the body. A pages_list written inside the content renders before the improver and cannot know about them.