What happened?
Description
In a multi-site section with propagationMethod: all, if a new entry is first created on a site that has no URL format for that section (hasUrls: false), the entry's URI on the other sites (which do have URLs) can get permanently stuck on the temporary placeholder slug (__temp_xxxxxxxx), even though the slug column itself is correctly regenerated from the title everywhere.
This looks closely related to #16347, which was fixed in 5.5.8 by changing Drafts::removeDraftData() to save with $propagate = true, then reverted back to $propagate = false in a follow-up "better fix" (to avoid a regression with programmatic post-save changes on unpublished drafts being overwritten — craftcms/ckeditor#426), replacing it with a narrower fix in SlugValidator that only guarantees the slug gets regenerated at publish time, not the URI on other sites.
Root cause as far as I can trace it:
Combine the two: if an entry's "home" site for editing has no URL format, the 'uri' dirty condition in _propagateElement() can never be satisfied again after the very first save (when the entry was brand new and isNewForSite was still true for the other sites) — and that first save is often the moment EntriesController::actionCreate() assigns a temp slug because the title hasn't been typed yet:
https://github.com/craftcms/cms/blob/5.10.9/src/controllers/EntriesController.php#L166-L174
So the URI baked in on the other (URL-having) sites at creation time is the temp one, and nothing in the normal edit → autosave → publish flow ever revisits it — because propagation is either skipped (removeDraftData) or gated on a condition (uri dirty) that can't fire from a source site whose URI is permanently null.
Manually resaving the entry (a full resave/entries or an explicit CP save with resaving/scenario forcing setElementUri() again) fixes it, because that's the third, ungated condition ($element->resaving).
Steps to reproduce
- Create a multi-site section propagated to 2 sites,
propagationMethod: all.
- Site A:
hasUrls: false for this section. Site B: hasUrls: true, e.g. uriFormat: 'foo/{slug}'.
- Make Site A the "current"/default site in the CP (or otherwise create + edit + publish the entry entirely from Site A's tab).
- Click "+ New entry" (Craft immediately creates an unpublished draft; since no title has been typed yet, the slug is
ElementHelper::tempSlug(), e.g. __temp_abc123).
- Type a title, let autosave run, then click Save/Publish — all while staying on Site A's tab.
- Check Site B's
elements_sites row for this entry.
Expected behavior
Site B's slug and uri should both reflect the real, title-derived slug (e.g. slug: foo-bar, uri: foo/foo-bar).
Actual behavior
Site B's slug is correct (foo-bar), but uri is still the original placeholder (foo/__temp_abc123) — a real, published, correctly-titled entry ends up permanently reachable only at a URL containing a random temp string, until it's manually resaved (which then breaks any link to the temp URL that had already been shared/indexed).
Confirmed as the trigger: giving Site A a real URL format for the section (hasUrls: true + a uriFormat) made the problem stop occurring for newly created entries. Also confirmed via direct DB inspection on a production instance affected by this: dozens of entries created over many months, all created/edited by the same user through the CP, all showing this exact slug correct / uri stale-temp pattern, all with dateCreated == the first save where isNewForSite would have been true.
Craft CMS version
5.10.9
PHP version
8.2
Operating system and version
No response
Database type and version
MySQL 8.0.40
Image driver and version
No response
Installed plugins and versions
No response
What happened?
Description
In a multi-site section with
propagationMethod: all, if a new entry is first created on a site that has no URL format for that section (hasUrls: false), the entry's URI on the other sites (which do have URLs) can get permanently stuck on the temporary placeholder slug (__temp_xxxxxxxx), even though theslugcolumn itself is correctly regenerated from the title everywhere.This looks closely related to #16347, which was fixed in 5.5.8 by changing
Drafts::removeDraftData()to save with$propagate = true, then reverted back to$propagate = falsein a follow-up "better fix" (to avoid a regression with programmatic post-save changes on unpublished drafts being overwritten — craftcms/ckeditor#426), replacing it with a narrower fix inSlugValidatorthat only guarantees the slug gets regenerated at publish time, not the URI on other sites.Root cause as far as I can trace it:
Elements::_propagateElement()only callssetElementUri()for a propagated-to site when$siteElement->isNewForSite, or'uri'is in$element->getDirtyAttributes()on the site actually being saved, or$element->resavingis true:https://github.com/craftcms/cms/blob/5.10.9/src/services/Elements.php#L4519-L4535
When the site being directly saved has no URL format for the section,
ElementHelper::setUniqueUri()always sets$element->uri = nulland returns immediately:https://github.com/craftcms/cms/blob/5.10.9/src/helpers/ElementHelper.php#L133-L148
So
urion that site is alwaysnull → nulland never becomes a "dirty" attribute in any meaningful sense.Drafts::removeDraftData()(the final step that turns an unpublished draft into a live entry) saves with$propagate = false:https://github.com/craftcms/cms/blob/5.10.9/src/services/Drafts.php#L407-L412
so it never re-triggers propagation to other sites at all.
Combine the two: if an entry's "home" site for editing has no URL format, the
'uri' dirtycondition in_propagateElement()can never be satisfied again after the very first save (when the entry was brand new andisNewForSitewas still true for the other sites) — and that first save is often the momentEntriesController::actionCreate()assigns a temp slug because the title hasn't been typed yet:https://github.com/craftcms/cms/blob/5.10.9/src/controllers/EntriesController.php#L166-L174
So the URI baked in on the other (URL-having) sites at creation time is the temp one, and nothing in the normal edit → autosave → publish flow ever revisits it — because propagation is either skipped (
removeDraftData) or gated on a condition (uridirty) that can't fire from a source site whose URI is permanentlynull.Manually resaving the entry (a full
resave/entriesor an explicit CP save withresaving/scenario forcingsetElementUri()again) fixes it, because that's the third, ungated condition ($element->resaving).Steps to reproduce
propagationMethod: all.hasUrls: falsefor this section. Site B:hasUrls: true, e.g.uriFormat: 'foo/{slug}'.ElementHelper::tempSlug(), e.g.__temp_abc123).elements_sitesrow for this entry.Expected behavior
Site B's
slugandurishould both reflect the real, title-derived slug (e.g.slug: foo-bar,uri: foo/foo-bar).Actual behavior
Site B's
slugis correct (foo-bar), buturiis still the original placeholder (foo/__temp_abc123) — a real, published, correctly-titled entry ends up permanently reachable only at a URL containing a random temp string, until it's manually resaved (which then breaks any link to the temp URL that had already been shared/indexed).Confirmed as the trigger: giving Site A a real URL format for the section (
hasUrls: true+ auriFormat) made the problem stop occurring for newly created entries. Also confirmed via direct DB inspection on a production instance affected by this: dozens of entries created over many months, all created/edited by the same user through the CP, all showing this exactslugcorrect /uristale-temp pattern, all with dateCreated == the first save whereisNewForSitewould have been true.Craft CMS version
5.10.9
PHP version
8.2
Operating system and version
No response
Database type and version
MySQL 8.0.40
Image driver and version
No response
Installed plugins and versions
No response