How to dynamically manipulate canonical entity URLs I've got a project with a multi-user / multi-domain environment. There is one &quot;main&quot; portal like www.myproject.com and for every registered user with a specific custom permission has_personal_subdomain there is a subdomain and a theme with his username, e.g. user johndoe has a subdomain johndoe.myproject.com and this domain is using his custom theme johndoe.
I've already set up all the domains and the theme switching part. All editing/administrative actions are always done on the main portal domain, also users with personal subdomains work on the main portal domain.
The problem are the canonical entity links: When editing nodes, editing is always done in admin theme on the main portal domain, so of course all canonical entity links also point to the portal domain. E.g. for users with a personal subdomain, the canonical links to www.myproject.com/node/123 or www.myproject.com/my-node-url-alias, but I want johndoe.myproject.com/node/123 or johndoe.myproject.com/my-node-url-alias.
What is the best way to dynamically manipulate the canonical entity view links? I'm not asking for a complete code, I'm just looking for the best starting point/hook/event/service to inject my custom logic.
What I want to avoid:
Create a million custom hooks for hook_menu_local_tasks_alter AND hook_form_alter AND hook_views_pre_render AND whatever else...
I do not care about metatags, SEO, duplicate URLs,... I just want a better editor experience with personalized entity view links pointing to the correct subdomain using the correct theme.
I know there is the OutboundPathProcessor, but this seems a bit over the top, as I only want to change canonical URLs and none of the admin links.
Is there a service specifically for generating canonical URLs I can decorate?
Is there a service specifically for generating canonical URLs I can
decorate?

Yes, for a route based handling of outgoing canonical links you can use OutboundRouteProcessorInterface::processOutbound
For example like core implements the dynamic route &lt;current&gt;.
/core/lib/Drupal/Core/RouteProcessor/RouteProcessorCurrent.php
  public function processOutbound($route_name, Route $route, array &amp;$parameters, BubbleableMetadata $bubbleable_metadata = NULL) {
    if ($route_name === '&lt;current&gt;') {
      if ($current_route = $this-&gt;routeMatch-&gt;getRouteObject()) {
        $requirements = $current_route-&gt;getRequirements();
        // Setting _method and _schema is deprecated since 2.7. Using
        // setMethods() and setSchemes() are now the recommended ways.
        unset($requirements['_method']);
        unset($requirements['_schema']);
        $route-&gt;setRequirements($requirements);

        $route-&gt;setPath($current_route-&gt;getPath());
        $route-&gt;setSchemes($current_route-&gt;getSchemes());
        $route-&gt;setMethods($current_route-&gt;getMethods());
        $route-&gt;setOptions($current_route-&gt;getOptions());
        $route-&gt;setDefaults($current_route-&gt;getDefaults());
        $parameters = array_merge($parameters, $this-&gt;routeMatch-&gt;getRawParameters()-&gt;all());
        if ($bubbleable_metadata) {
          $bubbleable_metadata-&gt;addCacheContexts(['route']);
        }
      }
      else {
        // If we have no current route match available, point to the frontpage.
        $route-&gt;setPath('/');
      }
    }
  }