Per-Site Apple Touch Icons on a Shared-Codebase Drupal Multisite

When someone adds a website to their iPhone's home screen, iOS needs an icon. If the page declares one with <link rel="apple-touch-icon">, Safari uses it. If not, it goes looking at fixed paths in the root of the domain: /apple-touch-icon.png, /apple-touch-icon-precomposed.png, and size-specific variants. Plenty of other clients and crawlers request the same paths blindly. If they don't exist, you get a blurry screenshot on the home screen and a steady stream of 404s in the logs.

On a normal site the fix is to put the files in the docroot. On the platform I work on, that isn't possible.

Why the docroot doesn't work on a multisite

The platform is a Drupal multisite on Acquia Site Factory: dozens of brand sites running from one shared codebase and one docroot. A file in the docroot is the same file for every site. Put a brand's icon there and every other brand shows it too.

Each site does have its own files directory, but that lives under a site-specific path like sites/<site id>/files/, not at the root of the domain. So the files have to be stored per site and served at root paths.

The approach: upload per site, map in the theme

The themes have a "Touch icons" field at the bottom of each site's theme settings. Icons added there are served at the root paths for that site only. The flow for a site is:

  1. Upload the icons through the media library first. They end up in the site's own public files, for example public://2025-02/apple-touch-icon-180x180.png.
  2. Then reference those files in the theme's touch icon settings. Pointing the setting at a file that hasn't been uploaded through Drupal is the most likely way to get an error, so the order matters.
  3. Replace the old icon. Sites that already had one icon got it removed and replaced with the new set, so every site ends up with exactly the same structure.

Eight images per site, each used twice: once as the normal icon and once as the -precomposed variant, which older iOS versions request to get an icon without the glossy effect. That's sixteen URLs per site:

/apple-touch-icon.png
/apple-touch-icon-precomposed.png
/apple-touch-icon-72x72.png
/apple-touch-icon-72x72-precomposed.png
/apple-touch-icon-76x76.png
/apple-touch-icon-76x76-precomposed.png
/apple-touch-icon-114x114.png
/apple-touch-icon-114x114-precomposed.png
/apple-touch-icon-120x120.png
/apple-touch-icon-120x120-precomposed.png
/apple-touch-icon-144x144.png
/apple-touch-icon-144x144-precomposed.png
/apple-touch-icon-152x152.png
/apple-touch-icon-152x152-precomposed.png
/apple-touch-icon-180x180.png
/apple-touch-icon-180x180-precomposed.png

Rolling it out

Two things kept the rollout short. First, delete unused sites before starting. On a platform with many sites, every unused one you remove is sixteen icons nobody has to upload. Second, prepare one site completely as the reference, so everyone doing the rest can compare with a working example instead of a written description.

After a site is done, check every URL, on the live domains too:

for path in $(cat icons.txt); do
  printf "%s %s\n" "$(curl -s -o /dev/null -w '%{http_code}' "https://www.example.com$path")" "$path"
done

Sixteen 200s means done. Anything else means a size was skipped or entered wrong.

Takeaway

On a multisite, anything that is expected at a fixed root path, like touch icons, robots.txt, ads.txt or verification files, can't simply be a file in the docroot. Decide early how per-site root files are stored and served. Doing it once in the theme or a small module beats special cases for every new file someone asks for.

← All posts