Skip to content

Add Umbraco Deploy support for SeoToolkit - #557

Open
robertjf wants to merge 31 commits into
patrickdemooij9:dev/mainfrom
robertjf:feature/deploy-connector
Open

Add Umbraco Deploy support for SeoToolkit#557
robertjf wants to merge 31 commits into
patrickdemooij9:dev/mainfrom
robertjf:feature/deploy-connector

Conversation

@robertjf

Copy link
Copy Markdown
Contributor

Summary

Adds a new SeoToolkit.Umbraco.Deploy package that lets Umbraco Deploy transfer SeoToolkit settings and per-node SEO data between environments — via .uda disk artifacts, queue-for-transfer/restore, and content import/export.

Implemented from the plan at docs/superpowers/plans/2026-07-14-seotoolkit-deploy-connector.md (12 tasks, each committed separately, TDD throughout).

Resolves #232

What's included

  • 8 GuidUdi connectors (all seotoolkit- prefixed): SEO enable toggle, MetaFields doc-type settings, sitemap page-type settings, scripts, domain collections, key/values, plus per-node MetaFields values and sitemap content overrides.
  • Ride-along export: per-node SEO data is attached as dependencies to document artifacts on ArtifactExportingNotification, so it travels automatically with content transfers.
  • Disk (.uda) integration: settings artifacts are (re)written on save/delete via new Core notifications and disk-refresher handlers.
  • Transfer registration: settings entity types registered for queue-for-transfer, restore, and import/export.
  • Soft-fail: missing target document types/nodes/definitions are logged and skipped, never failing a whole deploy. Individual entity types can be disabled via SeoToolkit:Deploy:DisabledEntityTypes.

Core changes (additive, required for transfers to work)

  • ScriptManagerService.Save now inserts (preserving Key) when a script arrives with a preset Key not present in the target, instead of a silent no-op update.
  • SeoDomainsRepository.Save preserves a caller-supplied collection Id on insert instead of generating a new Guid.
  • IMetaFieldsValueRepository gains GetAllValues(Guid) and GetAllNodeKeys() to enumerate per-node values across cultures.
  • New save/delete notifications published from the ScriptManager/Domains services and the key-value controller (constructor injection of IEventAggregator).

Notable deviation from the plan

The ride-along handler uses ArtifactExportingNotification, not ArtifactExportedNotification. The export pipeline (ArtifactImportExportService) serializes the artifact between the two notifications, so dependencies added on the -ed notification would never reach the exported artifact. Recorded in the plan file.

Verification

  • dotnet build src/SeoToolkit.Umbraco.sln — succeeds, 0 errors.
  • dotnet test ... --filter "FullyQualifiedName~Deploy" — 23/23 pass.
  • dotnet pack — produces SeoToolkit.Umbraco.Deploy.1.0.0-beta1.nupkg.

Runtime end-to-end verification against a real Deploy environment (checksum/dependency-graph behaviour, pass-7 scheduling) remains a manual follow-up, as noted in the plan.

🤖 Generated with Claude Code

robertjf and others added 14 commits July 14, 2026 20:31
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eleton

Implements core foundation types for SeoToolkit Deploy:
- SeoToolkitDeployConstants with 8 GuidUdi entity types and RootKeyValuesGuid
- SeoToolkitArtifactDependency helper for Exist/Ordering defaults
- SeoToolkitDeploySettings bound from config section
- Composer/Component skeleton with UdiParser registration

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Also fixes ScriptManagerService.Save so a script arriving with a preset Key
that doesn't exist in the target is inserted (preserving the Key) rather than
silently no-op updated — required for Deploy/uSync transfers to work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Also fixes SeoDomainsRepository.Save to preserve a caller-supplied collection
Id on insert (instead of generating a new Guid), so domain collections keep
their identifier when transferred via Deploy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ectors

Adds IMetaFieldsValueRepository.GetAllValues(Guid) and GetAllNodeKeys() to
enumerate per-node values across cultures for transfer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Uses ArtifactExportingNotification (fires before serialization) rather than
ArtifactExportedNotification — the export pipeline serializes the artifact
between the two, so dependencies must be added on the -ing notification.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds ScriptSaved/Deleted, SeoDomainCollectionSaved/Deleted and SeoKeyValueSaved
notifications published from the ScriptManager/Domains services and the key-value
controller, plus disk-refresher handlers that (re)write .uda artifacts on change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@qodo-code-review

qodo-code-review Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Add Umbraco Deploy support for SeoToolkit settings and per-node SEO data

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add SeoToolkit.Umbraco.Deploy package with Deploy connectors and backoffice entrypoint.
• Persist settings to .uda and refresh signatures for per-node SEO entities.
• Fix key/id preservation and publish save/delete notifications for reliable transfers.
Diagram

graph TD
  UD{{"Umbraco Deploy"}} --> SDK["SeoToolkit Deploy pkg"] --> DISK[(".uda artifacts")]
  SDK --> CORE["SeoToolkit Core"] --> DB[("SeoToolkit data")]
  SDK --> UI["Backoffice UI"] --> API["Deploy helper API"]
  API --> CORE
  subgraph Legend
    direction LR
    _ext{{"External"}} ~~~ _svc["Service/Module"] ~~~ _db[("Storage")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use Deploy value connectors (property-level) instead of custom entity types
  • ➕ Leverages existing content/property serialization pipeline
  • ➕ May reduce custom artifact modeling for per-node data
  • ➖ SeoToolkit data is largely stored outside standard Umbraco properties
  • ➖ Doesn’t naturally cover settings-like entities (domains/scripts/key-values) as first-class Deploy items
2. Rely on uSync-only deployments for SeoToolkit configuration
  • ➕ Operationally simpler if uSync already used for config sync
  • ➕ Avoids Deploy-specific pass scheduling and signatures
  • ➖ Doesn’t integrate with Deploy queue/restore/import-export workflows
  • ➖ Per-node SEO data transfer remains manual or requires separate solution
3. Pre-validate dependencies and expose a “skipped items” report
  • ➕ Better operator visibility when doc types/nodes/definitions are missing
  • ➕ Reduces silent drift between environments
  • ➖ More UI/API work; still needs soft-fail behavior to avoid whole-deploy failure
  • ➖ Adds ongoing maintenance surface

Recommendation: Keep the PR’s approach: first-class Deploy entity types for settings, plus per-node entities with signature refresh and explicit dependencies. It matches Deploy’s model without invasive changes to SeoToolkit persistence. Consider a follow-up to surface a clear report of skipped artifacts when dependencies are missing.

Files changed (93) +8114 / -34

Enhancement (54) +2410 / -5
SeoKeyValueSettingsController.csPublish key/value saved notification from controller +7/-1

Publish key/value saved notification from controller

• Injects IEventAggregator and publishes SeoKeyValueSavedNotification after persisting key/values to enable disk refreshers.

src/SeoToolkit.Umbraco.Common.Core/Controllers/SeoKeyValueSettingsController.cs

SeoDomainCollectionDeletedNotification.csIntroduce domain collection deleted notification +15/-0

Introduce domain collection deleted notification

• Adds a notification type to signal domain collection deletion for Deploy disk/signature refresh logic.

src/SeoToolkit.Umbraco.Common.Core/Notifications/SeoDomainCollectionDeletedNotification.cs

SeoDomainCollectionSavedNotification.csIntroduce domain collection saved notification +15/-0

Introduce domain collection saved notification

• Adds a notification type to signal domain collection save events for Deploy refreshers.

src/SeoToolkit.Umbraco.Common.Core/Notifications/SeoDomainCollectionSavedNotification.cs

SeoKeyValueSavedNotification.csIntroduce key/value saved notification +15/-0

Introduce key/value saved notification

• Adds a notification type emitted after key/value writes so Deploy artifacts can be rewritten on disk.

src/SeoToolkit.Umbraco.Common.Core/Notifications/SeoKeyValueSavedNotification.cs

SeoDomainsService.csEmit domain collection save/delete notifications +8/-1

Emit domain collection save/delete notifications

• Injects IEventAggregator and publishes saved/deleted notifications after persisting domain collections so disk artifacts can be refreshed.

src/SeoToolkit.Umbraco.Common.Core/Services/Domains/SeoDomainsService.cs

DomainCollectionArtifact.csAdd domain collection artifact model +15/-0

Add domain collection artifact model

• Adds a Deploy artifact representing SeoToolkit domain collections in a portable format for transfer/restore.

src/SeoToolkit.Umbraco.Deploy/Artifacts/DomainCollectionArtifact.cs

KeyValuesArtifact.csAdd key/values artifact model +14/-0

Add key/values artifact model

• Adds a Deploy artifact for global/per-domain key/values, including optional dependency linkage to a domain collection.

src/SeoToolkit.Umbraco.Deploy/Artifacts/KeyValuesArtifact.cs

MetaFieldsSettingArtifact.csAdd MetaFields settings artifact model +24/-0

Add MetaFields settings artifact model

• Adds a Deploy artifact for doc-type MetaFields settings, including inheritance and serialized field payloads.

src/SeoToolkit.Umbraco.Deploy/Artifacts/MetaFieldsSettingArtifact.cs

MetaFieldsValueArtifact.csAdd per-node MetaFields values artifact model +13/-0

Add per-node MetaFields values artifact model

• Adds a Deploy artifact for per-node/culture MetaFields values and associated dependency collection.

src/SeoToolkit.Umbraco.Deploy/Artifacts/MetaFieldsValueArtifact.cs

ScriptArtifact.csAdd script artifact model +18/-0

Add script artifact model

• Adds a Deploy artifact for Script Manager scripts (definition alias, config, domain scope, sort order).

src/SeoToolkit.Umbraco.Deploy/Artifacts/ScriptArtifact.cs

SeoSettingArtifact.csAdd SEO enable toggle artifact model +12/-0

Add SEO enable toggle artifact model

• Adds a Deploy artifact capturing per-document-type SEO enabled state.

src/SeoToolkit.Umbraco.Deploy/Artifacts/SeoSettingArtifact.cs

SitemapContentArtifact.csAdd per-node sitemap content artifact model +16/-0

Add per-node sitemap content artifact model

• Adds a Deploy artifact for per-node sitemap overrides, keyed by node GUID.

src/SeoToolkit.Umbraco.Deploy/Artifacts/SitemapContentArtifact.cs

SitemapPageTypeArtifact.csAdd sitemap page-type artifact model +16/-0

Add sitemap page-type artifact model

• Adds a Deploy artifact for per-document-type sitemap settings.

src/SeoToolkit.Umbraco.Deploy/Artifacts/SitemapPageTypeArtifact.cs

SeoToolkitDeployComponent.csRegister UDI types, disk entities, and transfer entities +96/-0

Register UDI types, disk entities, and transfer entities

• Registers the eight seotoolkit-* GuidUdi types, configures which are disk-backed, and registers all types with Deploy transfer/restore/import-export capabilities.

src/SeoToolkit.Umbraco.Deploy/Composing/SeoToolkitDeployComponent.cs

SeoToolkitDeployComposer.csBind Deploy settings and register notification handlers +38/-0

Bind Deploy settings and register notification handlers

• Binds SeoToolkit:Deploy settings and wires disk refresher/signature refresher handlers and the Deploy component into Umbraco composition.

src/SeoToolkit.Umbraco.Deploy/Composing/SeoToolkitDeployComposer.cs

SeoToolkitDomainCollectionServiceConnector.csImplement domain collection service connector +99/-0

Implement domain collection service connector

• Implements export/import of domain collections with portable domain-name serialization and soft-fail resolution on restore.

src/SeoToolkit.Umbraco.Deploy/Connectors/ServiceConnectors/SeoToolkitDomainCollectionServiceConnector.cs

SeoToolkitEntityServiceConnectorBase.csAdd shared connector base with disable and soft-missing support +170/-0

Add shared connector base with disable and soft-missing support

• Provides common Deploy connector behavior: selector validation, operation-scoped caching, disabled-entity filtering, and optional missing-entity no-op behavior.

src/SeoToolkit.Umbraco.Deploy/Connectors/ServiceConnectors/SeoToolkitEntityServiceConnectorBase.cs

SeoToolkitKeyValuesServiceConnector.csImplement key/values service connector +142/-0

Implement key/values service connector

• Implements export/import for key/values with stable ordering, domain dependencies, and convergent restore semantics; publishes a saved notification after restore writes.

src/SeoToolkit.Umbraco.Deploy/Connectors/ServiceConnectors/SeoToolkitKeyValuesServiceConnector.cs

SeoToolkitMetaFieldsSettingServiceConnector.csImplement MetaFields settings service connector +180/-0

Implement MetaFields settings service connector

• Implements export/import for doc-type MetaFields settings using portable editor-wire JSON and dependency capture (UDIs/media); restores converge by replacing settings from a fresh DTO.

src/SeoToolkit.Umbraco.Deploy/Connectors/ServiceConnectors/SeoToolkitMetaFieldsSettingServiceConnector.cs

SeoToolkitMetaFieldsValueServiceConnector.csImplement per-node MetaFields values service connector +190/-0

Implement per-node MetaFields values service connector

• Implements export/import for per-node MetaFields values across cultures with stable ordering and dependency discovery; restores skip missing nodes and notify caches post-write.

src/SeoToolkit.Umbraco.Deploy/Connectors/ServiceConnectors/SeoToolkitMetaFieldsValueServiceConnector.cs

SeoToolkitScriptServiceConnector.csImplement script service connector +130/-0

Implement script service connector

• Exports/imports scripts across global and domain scopes, emits domain collection dependencies, and skips restores when the target lacks the script definition type.

src/SeoToolkit.Umbraco.Deploy/Connectors/ServiceConnectors/SeoToolkitScriptServiceConnector.cs

SeoToolkitSeoSettingServiceConnector.csImplement SEO enable toggle service connector +103/-0

Implement SEO enable toggle service connector

• Exports/imports per-document-type SEO enable state with doc-type dependencies and skip-on-missing-type behavior.

src/SeoToolkit.Umbraco.Deploy/Connectors/ServiceConnectors/SeoToolkitSeoSettingServiceConnector.cs

SeoToolkitSitemapContentServiceConnector.csImplement per-node sitemap content service connector +98/-0

Implement per-node sitemap content service connector

• Exports/imports per-node sitemap overrides with document dependencies; restores skip if the node doesn’t exist in the target.

src/SeoToolkit.Umbraco.Deploy/Connectors/ServiceConnectors/SeoToolkitSitemapContentServiceConnector.cs

SeoToolkitSitemapPageTypeServiceConnector.csImplement sitemap page-type service connector +95/-0

Implement sitemap page-type service connector

• Exports/imports per-document-type sitemap settings with doc-type dependencies and skip-on-missing-type behavior.

src/SeoToolkit.Umbraco.Deploy/Connectors/ServiceConnectors/SeoToolkitSitemapPageTypeServiceConnector.cs

SeoToolkitDeployController.csAdd backoffice endpoint to enumerate per-node SEO entities +42/-0

Add backoffice endpoint to enumerate per-node SEO entities

• Adds a backoffice API that reports whether a node has MetaFields values and/or sitemap overrides, enabling the client to only queue existing entities.

src/SeoToolkit.Umbraco.Deploy/Controllers/SeoToolkitDeployController.cs

ManifestLoader.csInject backoffice entrypoint manifest +47/-0

Inject backoffice entrypoint manifest

• Registers an IPackageManifestReader that provides the SeoToolkit Deploy backoffice entrypoint extension and version metadata.

src/SeoToolkit.Umbraco.Deploy/ManifestLoader.cs

KeyValuesModel.csAdd key/values connector model +5/-0

Add key/values connector model

• Introduces a connector-friendly model representing a key/value set and its artifact identity/scope.

src/SeoToolkit.Umbraco.Deploy/Models/KeyValuesModel.cs

MetaFieldsNodeValuesModel.csAdd per-node MetaFields values model +5/-0

Add per-node MetaFields values model

• Introduces a model combining node key/name with culture-keyed MetaFields values for artifact serialization.

src/SeoToolkit.Umbraco.Deploy/Models/MetaFieldsNodeValuesModel.cs

SeoSettingModel.csAdd SEO enable toggle connector model +4/-0

Add SEO enable toggle connector model

• Introduces a model for per-content-type SEO enable state used by the connector.

src/SeoToolkit.Umbraco.Deploy/Models/SeoSettingModel.cs

SeoTransferItemsViewModel.csAdd transfer items API view model +18/-0

Add transfer items API view model

• Defines response DTOs returned by the per-node transfer-items endpoint.

src/SeoToolkit.Umbraco.Deploy/Models/SeoTransferItemsViewModel.cs

SeoToolkitDiskRefresherHandlers.csImplement disk .uda refreshers for settings entity types +127/-0

Implement disk .uda refreshers for settings entity types

• Adds notification handlers that build artifacts via the relevant connector and write/delete them on disk, updating/clearing signatures alongside.

src/SeoToolkit.Umbraco.Deploy/NotificationHandlers/SeoToolkitDiskRefresherHandlers.cs

SeoToolkitSignatureRefresherHandlers.csImplement signature refreshers for per-node entity types +54/-0

Implement signature refreshers for per-node entity types

• Adds handlers that recompute signatures for per-node SEO entities on change, clearing signatures when a node has no remaining data.

src/SeoToolkit.Umbraco.Deploy/NotificationHandlers/SeoToolkitSignatureRefresherHandlers.cs

SeoToolkitArtifactDependency.csAdd dependency helper wrapper +13/-0

Add dependency helper wrapper

• Adds a convenience wrapper over ArtifactDependency to standardize dependency creation for SeoToolkit artifacts.

src/SeoToolkit.Umbraco.Deploy/SeoToolkitArtifactDependency.cs

SeoToolkitDeployConstants.csAdd Deploy entity type constants +22/-0

Add Deploy entity type constants

• Defines the eight seotoolkit-* UDI entity types and a well-known GUID for the global key/values artifact identity.

src/SeoToolkit.Umbraco.Deploy/SeoToolkitDeployConstants.cs

UdiJsonHelper.csAdd JSON UDI scanner helper +32/-0

Add JSON UDI scanner helper

• Adds a regex-based helper to discover embedded umb:// UDIs in JSON to record artifact dependencies deterministically.

src/SeoToolkit.Umbraco.Deploy/UdiJsonHelper.cs

queueSeoEntityAction.tsImplement queue-for-transfer action +56/-0

Implement queue-for-transfer action

• Adds a backoffice action that queues SeoToolkit Deploy entities into the Deploy transfer queue.

src/SeoToolkit.Umbraco.Deploy/assets/src/actions/queueSeoEntityAction.ts

restoreSeoEntityAction.tsImplement restore action +56/-0

Implement restore action

• Adds a backoffice action that triggers restore of selected SeoToolkit Deploy entities.

src/SeoToolkit.Umbraco.Deploy/assets/src/actions/restoreSeoEntityAction.ts

transferSeoAction.tsImplement transfer orchestration action +41/-0

Implement transfer orchestration action

• Adds logic to orchestrate SEO transfer/queue flows from the UI, coordinating per-node selections and API calls.

src/SeoToolkit.Umbraco.Deploy/assets/src/actions/transferSeoAction.ts

deployRestoreModal.tsAdd Deploy restore modal API integration +23/-0

Add Deploy restore modal API integration

• Adds a helper API module used by UI actions to integrate with Deploy restore modal behavior.

src/SeoToolkit.Umbraco.Deploy/assets/src/api/deployRestoreModal.ts

deployTransferQueue.tsAdd Deploy transfer queue API integration +21/-0

Add Deploy transfer queue API integration

• Adds helpers to invoke Deploy transfer queue operations from the backoffice UI.

src/SeoToolkit.Umbraco.Deploy/assets/src/api/deployTransferQueue.ts

seoDeployClient.tsAdd typed backoffice Deploy client +70/-0

Add typed backoffice Deploy client

• Implements a typed client for calling the SeoToolkitDeploy backoffice endpoint and related Deploy operations.

src/SeoToolkit.Umbraco.Deploy/assets/src/api/seoDeployClient.ts

seoDeployItems.tsAdd per-node transfer-items API helper +28/-0

Add per-node transfer-items API helper

• Implements a client call to retrieve per-node SeoToolkit entity types to transfer for a specific content key.

src/SeoToolkit.Umbraco.Deploy/assets/src/api/seoDeployItems.ts

index.tsAdd backoffice entrypoint bootstrap +6/-0

Add backoffice entrypoint bootstrap

• Adds the main entrypoint wiring that registers/manifests the Deploy UI integration bundle.

src/SeoToolkit.Umbraco.Deploy/assets/src/index.ts

deployManifests.tsAdd extension manifests for Deploy UI integration +57/-0

Add extension manifests for Deploy UI integration

• Defines backoffice extension manifests needed to surface SeoToolkit Deploy actions/entrypoint within Umbraco.

src/SeoToolkit.Umbraco.Deploy/assets/src/manifests/deployManifests.ts

UmbracoMediaConverter.csEnhance media converter for dependency discovery +22/-2

Enhance media converter for dependency discovery

• Improves media conversion to support enumeration of referenced media keys for Deploy dependency tracking.

src/SeoToolkit.Umbraco.MetaFields.Core/Common/Converters/EditorConverters/UmbracoMediaConverter.cs

IMediaReferenceConverter.csAdd media reference converter contract +20/-0

Add media reference converter contract

• Introduces an interface for converters to report referenced media keys to build Deploy artifact dependencies.

src/SeoToolkit.Umbraco.MetaFields.Core/Interfaces/Converters/IMediaReferenceConverter.cs

IMetaFieldsValueService.csExpose per-node change notification API +9/-0

Expose per-node change notification API

• Extends the service contract to support notifying listeners/caches that a node’s values changed (used by Deploy restores).

src/SeoToolkit.Umbraco.MetaFields.Core/Interfaces/Services/IMetaFieldsValueService.cs

MetaFieldsValueChangedNotification.csAdd MetaFields value changed notification +20/-0

Add MetaFields value changed notification

• Adds a notification raised on per-node value changes to keep Deploy signatures current.

src/SeoToolkit.Umbraco.MetaFields.Core/Notifications/MetaFieldsValueChangedNotification.cs

IMetaFieldsValueRepository.csAdd cross-culture and enumeration APIs +16/-0

Add cross-culture and enumeration APIs

• Adds GetAllValues(Guid), GetAllNodeKeys(), and HasAnyValues(Guid) to support per-node exports and transfer-item discovery.

src/SeoToolkit.Umbraco.MetaFields.Core/Repositories/SeoValueRepository/IMetaFieldsValueRepository.cs

MetaFieldsValueService.csPublish change notifications for meta field values +15/-1

Publish change notifications for meta field values

• Emits MetaFieldsValueChangedNotification when per-node values change so Deploy signature refreshers can detect updates.

src/SeoToolkit.Umbraco.MetaFields.Core/Services/MetaFieldsValueService/MetaFieldsValueService.cs

ScriptDeletedNotification.csAdd script deleted notification +15/-0

Add script deleted notification

• Adds a notification type emitted on script deletion to support disk artifact cleanup and signature clearing.

src/SeoToolkit.Umbraco.ScriptManager.Core/Notifications/ScriptDeletedNotification.cs

ScriptSavedNotification.csAdd script saved notification +15/-0

Add script saved notification

• Adds a notification type emitted on script save to support disk artifact refresh and signature updates.

src/SeoToolkit.Umbraco.ScriptManager.Core/Notifications/ScriptSavedNotification.cs

SitemapContentChangedNotification.csAdd sitemap content changed notification +20/-0

Add sitemap content changed notification

• Adds a per-node notification emitted when sitemap content settings change or are reset, enabling Deploy signature refresh.

src/SeoToolkit.Umbraco.Sitemap.Core/Notifications/SitemapContentChangedNotification.cs

SitemapService.csPublish sitemap content change notifications +2/-0

Publish sitemap content change notifications

• Publishes SitemapContentChangedNotification after setting or deleting per-node sitemap settings so Deploy can detect changes to transfer.

src/SeoToolkit.Umbraco.Sitemap.Core/Services/SitemapService/SitemapService.cs

Bug fix (6) +114 / -28
SeoDomainsRepository.csPreserve domain collection IDs on insert +5/-3

Preserve domain collection IDs on insert

• Ensures a caller-supplied collection Id is preserved when inserting a missing collection, enabling Deploy round-trips; also uses autoComplete scopes for reads.

src/SeoToolkit.Umbraco.Common.Core/Repositories/Domains/SeoDomainsRepository.cs

ListValueConverter.csAdjust list editor value conversion +9/-1

Adjust list editor value conversion

• Refines conversion behavior to better support portable serialization/deserialization paths used in deploy export/import.

src/SeoToolkit.Umbraco.MetaFields.Core/Common/Converters/EditorConverters/ListValueConverter.cs

MetaFieldsSettingsDatabaseRepository.csAdjust MetaFields settings repository for deploy scenarios +5/-5

Adjust MetaFields settings repository for deploy scenarios

• Refines persistence/query behavior to support deterministic export/import and restore convergence.

src/SeoToolkit.Umbraco.MetaFields.Core/Repositories/MetaFieldsSettingsRepository/MetaFieldsSettingsDatabaseRepository.cs

MetaFieldsDatabaseRepository.csImplement new meta value enumeration and fix culture/null handling +68/-14

Implement new meta value enumeration and fix culture/null handling

• Implements new enumeration APIs, fixes delete/exists/get semantics for NULL vs empty cultures, uses generic NPoco delete calls, and resolves node IDs via IIdKeyMap to support unpublished content during deploy.

src/SeoToolkit.Umbraco.MetaFields.Core/Repositories/SeoValueRepository/MetaFieldsDatabaseRepository.cs

ScriptRepository.csFix null mapping for missing scripts +6/-4

Fix null mapping for missing scripts

• Avoids converting null database rows into models by returning null when the script entity is not found.

src/SeoToolkit.Umbraco.ScriptManager.Core/Repositories/ScriptRepository.cs

ScriptManagerService.csInsert scripts with preset keys and publish notifications +21/-1

Insert scripts with preset keys and publish notifications

• Changes Save() to insert a script when a provided Key is not present locally (preserving Key) and publishes ScriptSaved/ScriptDeleted notifications via IEventAggregator.

src/SeoToolkit.Umbraco.ScriptManager.Core/Services/ScriptManagerService.cs

Tests (18) +1672 / -0
DiskRefresherHandlerTests.csAdd disk refresher handler tests +66/-0

Add disk refresher handler tests

• Adds tests asserting settings artifacts are written/deleted on relevant notifications and signatures stay consistent.

src/SeoToolkit.Tests/SeoToolkit.Tests/Deploy/DiskRefresherHandlerTests.cs

DomainCollectionConnectorTests.csAdd domain collection connector tests +81/-0

Add domain collection connector tests

• Adds tests for export/import of domain collections, portable domain name handling, and stable serialization behavior.

src/SeoToolkit.Tests/SeoToolkit.Tests/Deploy/DomainCollectionConnectorTests.cs

KeyValuesConnectorTests.csAdd key/values connector tests +84/-0

Add key/values connector tests

• Adds tests for global and per-domain key/values artifacts and convergent restore semantics.

src/SeoToolkit.Tests/SeoToolkit.Tests/Deploy/KeyValuesConnectorTests.cs

MetaFieldsSettingConnectorTests.csAdd MetaFields settings connector tests +203/-0

Add MetaFields settings connector tests

• Adds tests covering artifact generation (including dependency capture) and restore behavior for doc-type MetaFields settings.

src/SeoToolkit.Tests/SeoToolkit.Tests/Deploy/MetaFieldsSettingConnectorTests.cs

MetaFieldsSettingConvergenceTests.csAdd MetaFields settings convergence tests +112/-0

Add MetaFields settings convergence tests

• Ensures restores can converge to the artifact’s authoritative settings by removing target-only data.

src/SeoToolkit.Tests/SeoToolkit.Tests/Deploy/MetaFieldsSettingConvergenceTests.cs

MetaFieldsValueConnectorTests.csAdd per-node MetaFields values connector tests +233/-0

Add per-node MetaFields values connector tests

• Tests per-node/culture artifact export/import behavior, ordering stability, and dependency detection.

src/SeoToolkit.Tests/SeoToolkit.Tests/Deploy/MetaFieldsValueConnectorTests.cs

MetaFieldsValueServiceTests.csAdd MetaFields value repository/service tests +74/-0

Add MetaFields value repository/service tests

• Validates repository enumeration helpers and change notification paths used by Deploy integration.

src/SeoToolkit.Tests/SeoToolkit.Tests/Deploy/MetaFieldsValueServiceTests.cs

ScriptConnectorTests.csAdd script connector tests +129/-0

Add script connector tests

• Tests script artifact round-trip behavior, domain scoping, and skip-on-missing-definition handling.

src/SeoToolkit.Tests/SeoToolkit.Tests/Deploy/ScriptConnectorTests.cs

ScriptManagerServiceSaveTests.csAdd ScriptManagerService.Save regression tests +59/-0

Add ScriptManagerService.Save regression tests

• Ensures scripts transferred with a preset Key are inserted when missing in the target environment.

src/SeoToolkit.Tests/SeoToolkit.Tests/Deploy/ScriptManagerServiceSaveTests.cs

SeoSettingConnectorTests.csAdd SEO enable-toggle connector tests +93/-0

Add SEO enable-toggle connector tests

• Covers export/import for per-document-type SEO enable toggles and dependency behavior.

src/SeoToolkit.Tests/SeoToolkit.Tests/Deploy/SeoSettingConnectorTests.cs

SeoToolkitDeployConstantsTests.csAdd Deploy constants tests +41/-0

Add Deploy constants tests

• Verifies the seotoolkit-* UDI entity types and well-known GUID are stable/correct.

src/SeoToolkit.Tests/SeoToolkit.Tests/Deploy/SeoToolkitDeployConstantsTests.cs

SitemapContentConnectorTests.csAdd sitemap content connector tests +72/-0

Add sitemap content connector tests

• Tests per-node sitemap content overrides artifact behavior and restore processing.

src/SeoToolkit.Tests/SeoToolkit.Tests/Deploy/SitemapContentConnectorTests.cs

SitemapPageTypeConnectorTests.csAdd sitemap page-type connector tests +77/-0

Add sitemap page-type connector tests

• Tests export/import of per-content-type sitemap settings and missing-type skip semantics.

src/SeoToolkit.Tests/SeoToolkit.Tests/Deploy/SitemapPageTypeConnectorTests.cs

SitemapServiceContentSettingsTests.csAdd sitemap content notification tests +55/-0

Add sitemap content notification tests

• Ensures sitemap content changes publish the notifications required for Deploy signature refresh.

src/SeoToolkit.Tests/SeoToolkit.Tests/Deploy/SitemapServiceContentSettingsTests.cs

UdiJsonHelperTests.csAdd UDI JSON helper tests +32/-0

Add UDI JSON helper tests

• Tests extraction of distinct umb:// UDIs from JSON blobs to support dependency capture.

src/SeoToolkit.Tests/SeoToolkit.Tests/Deploy/UdiJsonHelperTests.cs

UmbracoMediaConverterTests.csAdd media converter tests for dependency discovery +66/-0

Add media converter tests for dependency discovery

• Validates media conversion behavior used to enumerate referenced media keys for Deploy dependencies.

src/SeoToolkit.Tests/SeoToolkit.Tests/Deploy/UmbracoMediaConverterTests.cs

seoDeployClient.test.tsAdd JS tests for Deploy client wrapper +143/-0

Add JS tests for Deploy client wrapper

• Adds unit tests covering the typed HTTP client used by the Deploy UI integration.

src/SeoToolkit.Umbraco.Deploy/assets/src/api/seoDeployClient.test.ts

seoDeployItems.test.tsAdd JS tests for per-node item discovery API +52/-0

Add JS tests for per-node item discovery API

• Adds unit tests validating the helper that fetches which per-node SEO entities exist for a document node.

src/SeoToolkit.Umbraco.Deploy/assets/src/api/seoDeployItems.test.ts

Documentation (3) +3566 / -0
2026-07-14-seotoolkit-deploy-connector.mdAdd implementation plan for Deploy connector +3413/-0

Add implementation plan for Deploy connector

• Adds a detailed, step-by-step execution plan for implementing the SeoToolkit Deploy connector, including verification notes and recorded deviations.

docs/superpowers/plans/2026-07-14-seotoolkit-deploy-connector.md

2026-07-14-seotoolkit-deploy-connector-design.mdAdd Deploy connector design spec +85/-0

Add Deploy connector design spec

• Documents the intended architecture, entity types, and integration strategy for Umbraco Deploy support.

docs/superpowers/specs/2026-07-14-seotoolkit-deploy-connector-design.md

README.mdAdd Deploy package README +68/-0

Add Deploy package README

• Documents entity types, configuration options, soft-fail behavior, dependency semantics, and known follow-ups for the Deploy integration.

src/SeoToolkit.Umbraco.Deploy/README.md

Other (12) +352 / -1
.gitignoreIgnore new Deploy asset build outputs +4/-1

Ignore new Deploy asset build outputs

• Extends ignore rules to cover additional generated artifacts introduced by the Deploy package/assets pipeline.

.gitignore

Directory.Build.propsAdjust shared build/pack configuration +2/-0

Adjust shared build/pack configuration

• Updates shared MSBuild properties to accommodate the new Deploy project and packaging/build expectations.

src/Directory.Build.props

SeoToolkit.Tests.csprojWire Deploy test suite into test project +1/-0

Wire Deploy test suite into test project

• Updates the test project to include new Deploy-focused tests and any required references.

src/SeoToolkit.Tests/SeoToolkit.Tests/SeoToolkit.Tests.csproj

SeoToolkitDeploySettings.csAdd Deploy configuration model +14/-0

Add Deploy configuration model

• Adds configuration binding for DisabledEntityTypes to allow selectively disabling connectors.

src/SeoToolkit.Umbraco.Deploy/Configuration/SeoToolkitDeploySettings.cs

SeoToolkit.Umbraco.Deploy.csprojAdd Deploy package project and dependencies +30/-0

Add Deploy package project and dependencies

• Creates the SeoToolkit.Umbraco.Deploy package project, references Umbraco.Deploy.Infrastructure, and configures static web asset packaging and metadata.

src/SeoToolkit.Umbraco.Deploy/SeoToolkit.Umbraco.Deploy.csproj

.gitignoreIgnore node/vite generated outputs +24/-0

Ignore node/vite generated outputs

• Adds ignores for node_modules, dist, and related generated front-end files within the Deploy assets folder.

src/SeoToolkit.Umbraco.Deploy/assets/.gitignore

package.jsonAdd Deploy backoffice UI build config +19/-0

Add Deploy backoffice UI build config

• Adds Node/Vite/TypeScript tooling configuration for building and testing the Deploy backoffice entrypoint.

src/SeoToolkit.Umbraco.Deploy/assets/package.json

vite-env.d.tsAdd Vite TS environment typings +1/-0

Add Vite TS environment typings

• Adds TypeScript environment typing required by the Vite build setup.

src/SeoToolkit.Umbraco.Deploy/assets/src/vite-env.d.ts

tsconfig.jsonAdd TypeScript compiler configuration +28/-0

Add TypeScript compiler configuration

• Adds TS configuration for the Deploy asset codebase.

src/SeoToolkit.Umbraco.Deploy/assets/tsconfig.json

vite.config.tsAdd Vite build configuration +21/-0

Add Vite build configuration

• Defines the Vite build output used by the manifest entrypoint (deploy.js).

src/SeoToolkit.Umbraco.Deploy/assets/vite.config.ts

SeoToolkit.Umbraco.slnAdd Deploy project to solution +202/-0

Add Deploy project to solution

• Updates the solution to include the new SeoToolkit.Umbraco.Deploy project and associated build entries.

src/SeoToolkit.Umbraco.sln

umbraco-marketplace-seotoolkit.umbraco.deploy.jsonAdd marketplace metadata for Deploy package +6/-0

Add marketplace metadata for Deploy package

• Adds marketplace/packaging metadata related to distributing the new Deploy connector package.

umbraco-marketplace-seotoolkit.umbraco.deploy.json

@qodo-code-review

qodo-code-review Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (2) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 8 rules

Grey Divider


Action required

1. SeoToolkit.Umbraco.Deploy missing ManifestLoader 📘 Rule violation ⌂ Architecture
Description
The new SeoToolkit.Umbraco.Deploy Umbraco package project registers an IComposer via
SeoToolkitDeployComposer, but the project does not include the required ManifestLoader.cs file
with a public class ManifestLoader. This breaks the standard integration point required by the
compliance checklist and may prevent consistent package initialization expectations.
Code

src/SeoToolkit.Umbraco.Deploy/Composing/SeoToolkitDeployComposer.cs[R12-17]

+namespace SeoToolkit.Umbraco.Deploy.Composing
+{
+    public class SeoToolkitDeployComposer : IComposer
+    {
+        public void Compose(IUmbracoBuilder builder)
+        {
Relevance

⭐⭐⭐ High

Repo repeatedly maintains ManifestLoader.cs per package; missing one likely fixed to match
established pattern.

PR-#496
PR-#476

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1560501 requires a ManifestLoader.cs file containing `public class
ManifestLoader` in each Umbraco package project. The added integration file is
SeoToolkitDeployComposer : IComposer, not ManifestLoader, and the project added is the Umbraco
Deploy package project.

Rule 1560501: Require ManifestLoader.cs in each Umbraco package project
src/SeoToolkit.Umbraco.Deploy/Composing/SeoToolkitDeployComposer.cs[12-34]
src/SeoToolkit.Umbraco.Deploy/SeoToolkit.Umbraco.Deploy.csproj[1-22]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Compliance requires every Umbraco package project to include a `ManifestLoader.cs` file containing a `public class ManifestLoader`. The new `SeoToolkit.Umbraco.Deploy` project currently uses `SeoToolkitDeployComposer` instead.

## Issue Context
To satisfy the rule without changing existing registration behavior, add a root-level `ManifestLoader.cs` that implements `IComposer` and either:
- moves the existing compose logic into `ManifestLoader`, or
- delegates to/instantiates the existing `SeoToolkitDeployComposer` logic (keeping behavior identical).

## Fix Focus Areas
- src/SeoToolkit.Umbraco.Deploy/Composing/SeoToolkitDeployComposer.cs[12-34]
- src/SeoToolkit.Umbraco.Deploy/ManifestLoader.cs[1-999]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Meta values never cleared ✓ Resolved 🐞 Bug ≡ Correctness
Description
SeoToolkitMetaFieldsValueServiceConnector.ProcessAsync skips entries where the artifact value is
null and never deletes values that are absent from the artifact, so clearing/removing meta field
values in the source environment will not clear them in the target. Target nodes can retain stale
SEO values across deploys.
Code

src/SeoToolkit.Umbraco.Deploy/Connectors/ServiceConnectors/SeoToolkitMetaFieldsValueServiceConnector.cs[R114-137]

+            foreach (var (culture, fields) in state.Artifact.Values)
+            {
+                foreach (var (alias, json) in fields)
+                {
+                    if (json is null)
+                    {
+                        continue;
+                    }
+
+                    var value = JsonConvert.DeserializeObject(json);
+                    if (value is null)
+                    {
+                        continue;
+                    }
+
+                    if (valueRepository.Exists(nodeKey, alias, culture))
+                    {
+                        valueRepository.Update(nodeKey, alias, culture, value);
+                    }
+                    else
+                    {
+                        valueRepository.Add(nodeKey, alias, culture, value);
+                    }
+                }
Relevance

⭐⭐ Medium

No historical evidence that restores must delete/clear values absent/null in artifact.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The connector explicitly continues when json is null and only Add/Update for present values, while
the repository API includes Delete(...), so removed/cleared values will persist on the target.

src/SeoToolkit.Umbraco.Deploy/Connectors/ServiceConnectors/SeoToolkitMetaFieldsValueServiceConnector.cs[114-137]
src/SeoToolkit.Umbraco.MetaFields.Core/Repositories/SeoValueRepository/IMetaFieldsValueRepository.cs[20-36]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`SeoToolkitMetaFieldsValueServiceConnector.ProcessAsync()` only adds/updates values present in the artifact and skips `json is null`, but it never deletes existing target values that are no longer present (or explicitly cleared) in the source.

### Issue Context
The repository supports deleting values, so the connector can make the target match the artifact.

### Fix Focus Areas
- src/SeoToolkit.Umbraco.Deploy/Connectors/ServiceConnectors/SeoToolkitMetaFieldsValueServiceConnector.cs[114-137]
- src/SeoToolkit.Umbraco.MetaFields.Core/Repositories/SeoValueRepository/IMetaFieldsValueRepository.cs[20-36]

### Suggested fix
- Treat `json == null` as a delete instruction for that `(nodeKey, alias, culture)` (call `valueRepository.Delete(...)`).
- Before applying, read the current target values for the node (e.g. `valueRepository.GetAllValues(nodeKey)`), and delete any `(culture, alias)` pairs that exist in the target but not in `state.Artifact.Values`.
- Then add/update the remaining artifact values as you already do.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Domain scripts not exported ✓ Resolved 🐞 Bug ≡ Correctness
Description
SeoToolkitScriptServiceConnector.GetEntitiesAsync only enumerates scriptManagerService.GetAll(null),
so scripts associated with a domain collection are never included when exporting the open/root
range. As a result, Deploy queue-for-transfer/restore of “all scripts” silently misses domain-scoped
scripts.
Code

src/SeoToolkit.Umbraco.Deploy/Connectors/ServiceConnectors/SeoToolkitScriptServiceConnector.cs[R33-42]

+        public override async IAsyncEnumerable<Script> GetEntitiesAsync(
+            [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
+        {
+            await Task.CompletedTask;
+            // GetAll is per-domain; enumerate the no-domain scripts plus every known script by key.
+            foreach (var script in scriptManagerService.GetAll(null))
+            {
+                yield return script;
+            }
+        }
Relevance

⭐⭐ Medium

Domain-scoped ScriptManager exists, but no history about export enumeration across domains.

PR-#417

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The connector’s root expansion yields only GetAll(null), while the ScriptManager
service/repository explicitly filter by DomainId, so domain-specific scripts require a non-null
domainId and will be skipped by this enumeration.

src/SeoToolkit.Umbraco.Deploy/Connectors/ServiceConnectors/SeoToolkitScriptServiceConnector.cs[33-42]
src/SeoToolkit.Umbraco.ScriptManager.Core/Services/ScriptManagerService.cs[101-107]
src/SeoToolkit.Umbraco.ScriptManager.Core/Repositories/ScriptRepository.cs[70-84]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`SeoToolkitScriptServiceConnector.GetEntitiesAsync()` only yields `scriptManagerService.GetAll(null)`, which returns scripts with `DomainId == null` only. Domain-scoped scripts therefore never get exported when expanding the root/open UDI range.

### Issue Context
`IScriptManagerService.GetAll(Guid? domainId)` (and the underlying repository) filters results by `DomainId`, so exporting all scripts requires enumerating per-domain collections as well.

### Fix Focus Areas
- src/SeoToolkit.Umbraco.Deploy/Connectors/ServiceConnectors/SeoToolkitScriptServiceConnector.cs[33-42]

### Suggested fix
- Inject a domain collection source (e.g., `ISeoDomainsService`) and enumerate:
 - global scripts: `GetAll(null)`
 - each domain collection’s scripts: `GetAll(collection.Id)`
- Deduplicate by `Script.Key` when yielding.
- If you don’t want the connector to depend on domains service, add a repository/service method to return all scripts unfiltered and use that here.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
4. Meta settings not reset ✓ Resolved 🐞 Bug ≡ Correctness
Description
SeoToolkitMetaFieldsSettingServiceConnector.ProcessAsync reuses state.Entity and only adds/updates
dto.Fields entries from the artifact, but never clears dto.Fields first. If the artifact removes
fields or removes inheritance, the old field settings/inheritance can persist in the target after
restore.
Code

src/SeoToolkit.Umbraco.Deploy/Connectors/ServiceConnectors/SeoToolkitMetaFieldsSettingServiceConnector.cs[R111-138]

+            var dto = state.Entity ?? new DocumentTypeSettingsDto { Content = contentType };
+            dto.Content = contentType;
+
+            if (state.Artifact.InheritanceUdi is not null)
+            {
+                dto.Inheritance = contentTypeService.Get(state.Artifact.InheritanceUdi.Guid);
+            }
+
+            foreach (var field in state.Artifact.Fields)
+            {
+                var seoField = seoFieldCollection.Get(field.Alias);
+                if (seoField is null)
+                {
+                    continue; // field type not installed in target; skip
+                }
+
+                var valueDto = new DocumentTypeValueDto { UseInheritedValue = field.UseInheritedValue };
+                if (!string.IsNullOrWhiteSpace(field.Value))
+                {
+                    valueDto.Value = seoField.Editor.ValueConverter.ConvertDatabaseToObject(
+                        JsonConvert.DeserializeObject(field.Value));
+                }
+
+                if (!dto.Fields.TryAdd(seoField, valueDto))
+                {
+                    dto.Fields[seoField] = valueDto;
+                }
+            }
Relevance

⭐⭐ Medium

No prior review evidence about clearing DTO.Fields/resetting inheritance during restore connectors.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The connector reuses an existing DTO and mutates its Fields dictionary without clearing it, and it
doesn’t clear Inheritance when the artifact doesn’t specify one; because Fields is a persistent
mutable dictionary, removed entries can remain.

src/SeoToolkit.Umbraco.Deploy/Connectors/ServiceConnectors/SeoToolkitMetaFieldsSettingServiceConnector.cs[111-140]
src/SeoToolkit.Umbraco.MetaFields.Core/Models/MetaFieldsSettings/Business/DocumentTypeSettingsDto.cs[8-18]
src/SeoToolkit.Umbraco.MetaFields.Core/Repositories/MetaFieldsSettingsRepository/MetaFieldsSettingsDatabaseRepository.cs[64-73]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`SeoToolkitMetaFieldsSettingServiceConnector.ProcessAsync()` merges artifact fields into an existing `DocumentTypeSettingsDto` without clearing prior state, and it only sets `dto.Inheritance` when `InheritanceUdi` is present (never clears it when absent). This causes removed settings to remain on the target.

### Issue Context
`DocumentTypeSettingsDto.Fields` is a mutable dictionary; the repository update stores whatever is in that dictionary.

### Fix Focus Areas
- src/SeoToolkit.Umbraco.Deploy/Connectors/ServiceConnectors/SeoToolkitMetaFieldsSettingServiceConnector.cs[111-138]
- src/SeoToolkit.Umbraco.MetaFields.Core/Models/MetaFieldsSettings/Business/DocumentTypeSettingsDto.cs[8-18]

### Suggested fix
- Prefer building a fresh DTO from the artifact each time:
 - `var dto = new DocumentTypeSettingsDto { Content = contentType, Fields = new Dictionary<ISeoField, DocumentTypeValueDto>() };`
 - If `InheritanceUdi` is present, resolve it; otherwise explicitly clear inheritance (set to `null` if permitted).
 - Populate `dto.Fields` exclusively from `state.Artifact.Fields`.
- Then call `metaFieldsSettingsService.Set(dto)`.
- If you intentionally want to preserve target-only fields for missing field types, do so explicitly and document the rule.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

5. Key/values never delete ✓ Resolved 🐞 Bug ≡ Correctness
Description
SeoToolkitKeyValuesServiceConnector.ProcessAsync only sets keys present in the artifact and never
deletes keys missing from the artifact. This makes restores non-convergent because keys removed in
the source can remain in the target indefinitely.
Code

src/SeoToolkit.Umbraco.Deploy/Connectors/ServiceConnectors/SeoToolkitKeyValuesServiceConnector.cs[R112-116]

+            Guid? domainId = state.Artifact.DomainCollectionUdi?.Guid;
+            foreach (var (key, value) in state.Artifact.Values)
+            {
+                keyValueRepository.Set(key, value, domainId);
+            }
Relevance

⭐⭐ Medium

No historical evidence on Deploy connectors requiring delete-on-missing semantics for convergence.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The connector processes only artifact entries via Set, while the repository provides Delete and the
README confirms target-only keys are never deleted; this proves non-convergent behavior by design.

src/SeoToolkit.Umbraco.Deploy/Connectors/ServiceConnectors/SeoToolkitKeyValuesServiceConnector.cs[99-118]
src/SeoToolkit.Umbraco.Common.Core/Repositories/SeoKeyValueRepository/ISeoKeyValueRepository.cs[7-13]
src/SeoToolkit.Umbraco.Deploy/README.md[43-48]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`SeoToolkitKeyValuesServiceConnector.ProcessAsync()` applies artifact keys with `Set(...)` but does not remove target keys that are not in the artifact.

### Issue Context
The repository supports deleting keys, and the package README documents the current overwrite-only behavior.

### Fix Focus Areas
- src/SeoToolkit.Umbraco.Deploy/Connectors/ServiceConnectors/SeoToolkitKeyValuesServiceConnector.cs[112-116]
- src/SeoToolkit.Umbraco.Common.Core/Repositories/SeoKeyValueRepository/ISeoKeyValueRepository.cs[7-13]
- src/SeoToolkit.Umbraco.Deploy/README.md[43-48]

### Suggested fix
- If convergence is desired (or behind a config flag):
 - Load current target keys for the domain via `keyValueRepository.Get(domainId)`.
 - Delete keys not present in `state.Artifact.Values` via `keyValueRepository.Delete(key, domainId)`.
 - Then `Set(...)` artifact keys as today.
- If overwrite-only is intended, consider making this explicit via connector settings and/or naming to avoid surprise.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Export handler deserializes values ✓ Resolved 🐞 Bug ➹ Performance
Description
SeoToolkitContentExportedHandler calls valueRepository.GetAllValues(documentUdi.Guid).Count for each
exported document, which fetches and JSON-deserializes all meta field values just to check
existence. This adds avoidable overhead to large transfers.
Code

src/SeoToolkit.Umbraco.Deploy/NotificationHandlers/SeoToolkitContentExportedHandler.cs[R34-40]

+            var extraDependencies = new List<ArtifactDependency>();
+
+            if (valueRepository.GetAllValues(documentUdi.Guid).Count > 0)
+            {
+                extraDependencies.Add(new SeoToolkitArtifactDependency(
+                    new GuidUdi(SeoToolkitDeployConstants.UdiEntityType.MetaFieldsValue, documentUdi.Guid)));
+            }
Relevance

⭐ Low

Repo often rejects perf-only refactors (pagination/bulk operations); likely won’t change
Count/GetAllValues just for overhead.

PR-#460

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The handler checks .Count > 0 by calling GetAllValues(Guid), and that repository method fetches
entities and deserializes each row’s JSON; this demonstrates unnecessary work on every exported
document.

src/SeoToolkit.Umbraco.Deploy/NotificationHandlers/SeoToolkitContentExportedHandler.cs[34-46]
src/SeoToolkit.Umbraco.MetaFields.Core/Repositories/SeoValueRepository/MetaFieldsDatabaseRepository.cs[125-134]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The content exporting notification handler loads and deserializes all meta-field values for a node merely to test whether any exist.

### Issue Context
`GetAllValues(Guid)` performs a DB fetch and `JsonConvert.DeserializeObject(...)` per row.

### Fix Focus Areas
- src/SeoToolkit.Umbraco.Deploy/NotificationHandlers/SeoToolkitContentExportedHandler.cs[34-40]
- src/SeoToolkit.Umbraco.MetaFields.Core/Repositories/SeoValueRepository/MetaFieldsDatabaseRepository.cs[125-134]

### Suggested fix
- Add a repository method like `bool HasAnyValues(Guid nodeKey)` implemented as an `EXISTS`/`COUNT(1)` query without selecting `UserValue`.
- Use that method in `SeoToolkitContentExportedHandler` instead of `GetAllValues(...).Count > 0`.
- Keep `GetAllValues(Guid)` for when you actually need to build the per-node artifact.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Tests added under src/ 📘 Rule violation ▣ Testability
Description
New deploy-related test files are added under src/SeoToolkit.Tests/..., but the compliance
checklist requires test code to live under a repository-root SeoToolkit.Tests/ directory. This
indicates the test placement does not meet the required directory convention.
Code

src/SeoToolkit.Tests/SeoToolkit.Tests/Deploy/SeoToolkitDeployConstantsTests.cs[R1-8]

+using NUnit.Framework;
+using SeoToolkit.Umbraco.Deploy;
+using Umbraco.Cms.Core.Deploy;
+
+namespace SeoToolkit.Tests.Deploy
+{
+    [TestFixture]
+    public class SeoToolkitDeployConstantsTests
Relevance

⭐ Low

Repo uses src/-scoped solution/test layout; no evidence enforcing repo-root test folder
convention.

PR-#498

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1560502 mandates test sources be located under a repository-root
SeoToolkit.Tests/ directory. This PR adds new test sources under src/SeoToolkit.Tests/...,
demonstrating nonconformance to the specified path convention.

Rule 1560502: Place test code in SeoToolkit.Tests directory
src/SeoToolkit.Tests/SeoToolkit.Tests/Deploy/SeoToolkitDeployConstantsTests.cs[1-8]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Compliance requires all automated test code to reside under a repository-root `SeoToolkit.Tests/` directory. This PR adds new test files under `src/SeoToolkit.Tests/...` instead.

## Issue Context
If the repo convention has shifted to `src/SeoToolkit.Tests`, either:
- move the test project and all tests to `./SeoToolkit.Tests/...`, or
- confirm/update the compliance rule to reflect the actual repo convention.

## Fix Focus Areas
- src/SeoToolkit.Tests/SeoToolkit.Tests/Deploy/SeoToolkitDeployConstantsTests.cs[1-41]
- src/SeoToolkit.Tests/SeoToolkit.Tests/SeoToolkit.Tests.csproj[21-25]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
8. Node key query loads all ✓ Resolved 🐞 Bug ➹ Performance
Description
MetaFieldsDatabaseRepository.GetAllNodeKeys fetches every MetaFieldsValueEntity row (including the
NVARCHAR(MAX) UserValue column) and then does Distinct in memory. This is avoidably slow and
memory-heavy for large datasets, and it is used to enumerate per-node values for Deploy exports.
Code

src/SeoToolkit.Umbraco.MetaFields.Core/Repositories/SeoValueRepository/MetaFieldsDatabaseRepository.cs[R136-143]

+        public IEnumerable<Guid> GetAllNodeKeys()
+        {
+            using var scope = _scopeProvider.CreateScope();
+            return scope.Database
+                .Fetch<MetaFieldsValueEntity>(scope.SqlContext.Sql().SelectAll().From<MetaFieldsValueEntity>())
+                .Select(it => it.NodeKey)
+                .Distinct()
+                .ToArray();
Relevance

⭐ Low

Similar DB perf optimizations (bulk delete/pagination) were rejected; SQL DISTINCT refactor likely
deprioritized.

PR-#421
PR-#460

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The method explicitly selects all columns for all rows and only afterwards projects
NodeKey/distincts, while the entity includes a large UserValue column; this proves unnecessary I/O
and allocation.

src/SeoToolkit.Umbraco.MetaFields.Core/Repositories/SeoValueRepository/MetaFieldsDatabaseRepository.cs[136-144]
src/SeoToolkit.Umbraco.MetaFields.Core/Models/MetaFieldsValue/Database/MetaFieldsValueEntity.cs[10-28]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`GetAllNodeKeys()` currently executes `SelectAll().From<MetaFieldsValueEntity>()`, materializes all rows (including `UserValue`), and then performs `Distinct()` in memory. This scales poorly.

### Issue Context
Only the distinct `NodeKey` values are needed.

### Fix Focus Areas
- src/SeoToolkit.Umbraco.MetaFields.Core/Repositories/SeoValueRepository/MetaFieldsDatabaseRepository.cs[136-143]

### Suggested fix
- Replace the `Fetch<MetaFieldsValueEntity>(SelectAll...)` with a query that selects only `NodeKey` and performs `DISTINCT` in SQL.
 - e.g. use NPoco to `SELECT DISTINCT NodeKey FROM SeoToolkitMetaFieldsValue` and map to `Guid`.
- Return the results as an array/list to avoid multiple enumerations.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread src/SeoToolkit.Umbraco.Deploy/Composing/SeoToolkitDeployComposer.cs
@patrickdemooij9

Copy link
Copy Markdown
Owner

Hi @robertjf
Thank you for this massive PR! It might take a bit for me to go through all of this, but I'll certainly give it a try. I did try adding Umbraco Deploy support, but I got into trouble because of this: https://forum.umbraco.com/t/custom-content-dependencies-for-umbraco-deploy/6990. It was not possible for me to attach the SeoToolkit dependency to the content, resulting in mismatches. I wonder if you found a way around it, as I couldn't find anything when looking at it back then

@robertjf

Copy link
Copy Markdown
Contributor Author

I'll be thoroughly testing the new code today, but it is largely based on the Commerce Deploy project and uses notifications to write the seo record as a dependency rather than try to inject it into the content - it also picked up a few bugs in the existing code.

Anyway, I'll do a full review and end-to-end test today on an actual project

Resolves qodo review findings on PR patrickdemooij9#557:

- Add ManifestLoader to the Deploy package (convention parity with the
  other packages); registers a package manifest, no backoffice entry point.
- Export domain-scoped scripts, not just global ones, by enumerating each
  domain collection in the script connector (deduped by key).
- Optimise per-node value enumeration: GetAllNodeKeys uses SELECT DISTINCT
  NodeKey, and a new HasAnyValues(Guid) COUNT query backs the content
  export dependency check (no more loading/deserialising all values).
- Add opt-in convergent restores via SeoToolkit:Deploy:PruneMissing
  (default off, preserving documented overwrite-only behaviour). When on,
  key/values and meta field values delete target-only data and meta field
  settings rebuild from a fresh DTO (dropping removed fields/inheritance).

Adds test coverage for domain-script export and prune-on/off behaviour.
README updated for the new setting. 89/89 tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@robertjf

Copy link
Copy Markdown
Contributor Author

Follow-up on the informational findings (#6#8, 7687c29)

#7 GetAllNodeKeys loads all rows — Fixed. Now SELECT DISTINCT NodeKey instead of fetching every entity (incl. the NVARCHAR(MAX) UserValue column) and de-duping in memory.

#8 Export handler deserialises to count — Fixed. Added IMetaFieldsValueRepository.HasAnyValues(Guid) (a COUNT query that doesn't touch UserValue) and switched SeoToolkitContentExportedHandler to it, instead of GetAllValues(...).Count > 0.

#6 Tests under src/ — Not changing this. The whole repo places its test project at src/SeoToolkit.Tests; there is no repository-root SeoToolkit.Tests/ and nothing enforces one, so the new tests follow the existing convention. The compliance rule looks stale relative to the actual layout. Happy to move everything if that convention is meant to change, but that would be a repo-wide move rather than something specific to this PR.

All 89 tests pass.

Follow-up addressing the "custom dependencies on existing artifacts"
limitation discussed in the Umbraco forum/Deploy issues:

- Ride-along per-node dependencies now use ArtifactDependencyMode.Match
  instead of Exist, so Deploy compares the artifact checksum and
  re-transfers per-node SEO data whenever it changes (Exist only ensured
  presence and left stale values on the target after the first transfer).
- Register MetaFieldsValue and SitemapContent as disk entity types so
  their .uda is written/read per node when the node is exported, rather
  than only resolving during server-to-server transfers.
- Removing some of a node's MetaFields values now converges on the target
  under PruneMissing (an empty artifact clears all remaining values).
  Documented the full-removal boundary case in the README.

90/90 tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@robertjf

Copy link
Copy Markdown
Contributor Author

Per-node dependency reliability (fb56518)

Reviewed two long-standing reports about attaching custom data to existing Deploy artifacts:

Both conclude it wasn't possible to attach third-party per-node data to a content artifact. That capability now exists (ArtifactExportingNotification) and is exactly what this connector's SeoToolkitContentExportedHandler uses — so the core limitation is resolved. Two reliability gaps that the underlying concern implies were tightened up:

  1. Updates now propagate. Ride-along dependencies were ArtifactDependencyMode.Exist, which only ensures a value row is present on the target — changed SEO data would not re-transfer. Switched to Match so Deploy compares the per-node artifact checksum and re-transfers on change.
  2. Per-node .uda export. MetaFieldsValue and SitemapContent are now registered as disk entity types, so their .uda is written/read per node when the node is exported (previously they only resolved during server-to-server transfers). They remain non-queue-for-transfer, dependency-driven entities.
  3. Clearing. Removing some of a node's MetaFields values converges on the target under PruneMissing. Removing all of a node's values is a documented boundary (nothing left to attach to the export); sitemap overrides have no such boundary since they're a single wholesale-replaced record.

Runtime verification note: the Match checksum comparison and per-node .uda disk write/read behaviour depend on Deploy's dependency-graph traversal and are covered by unit tests at the connector/handler level; end-to-end confirmation against a live Deploy environment remains the manual follow-up already noted in the plan. 90/90 tests pass.

Closes the full-clear convergence gap for per-node MetaFields values:

- Add MetaFieldsValueChangedNotification, published by MetaFieldsValueService
  whenever a node's values are added/updated (AddValues) or removed (Delete).
- Add MetaFieldsValueDiskRefresherHandler: rewrites the node's .uda while it
  still has values and deletes it once the node is emptied, so removing all of
  a node's SEO values now propagates as a delete on restore instead of leaving
  stale data on the target.

Tests cover both disk-refresher branches and the service publishing the
notification on add and delete. README updated. 94/94 tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@robertjf

Copy link
Copy Markdown
Contributor Author

Per-node MetaFields value convergence — full fix (82e6566)

Closed the full-clear boundary noted in the previous comment (no longer just documented):

  • New MetaFieldsValueChangedNotification, published from MetaFieldsValueService on both AddValues and Delete.
  • New MetaFieldsValueDiskRefresherHandler mirrors the settings disk-refresher pattern: it rewrites the node's .uda while values remain and deletes it once the node is emptied. Removing all of a node's SEO values now propagates as a delete on restore, rather than leaving stale target data.

This makes per-node values behave like the settings entities on the disk model, so convergence no longer depends on the ride-along dependency being present at export time.

Notes:

  • Only the Guid service overloads publish; the int overloads are [Obsolete] and unused by the shipped controller.
  • Content-delete cleanup of orphaned value rows is out of scope (separate concern from deploy).

Tests cover both disk-refresher branches (write when values remain, delete when emptied) and the service publishing on add/delete. 94/94 pass. The disk write/delete + Match re-transfer still want one end-to-end pass against a live Deploy environment, as previously flagged.

@robertjf

robertjf commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

New changes have been pushed pending an audit by Claude Fable - here are the findings:

SeoToolkit.Umbraco.Deploy — Audit Findings

Source-code audit of the Deploy package (branch feature/deploy-connector, PR #557), 2026-07-15.
Findings are ordered by severity. Tick items off as they're fixed.


1. MetaFields settings with an image field break on export (HIGH)

  • Fixed

Where:

  • src/SeoToolkit.Umbraco.Deploy/Connectors/ServiceConnectors/SeoToolkitMetaFieldsSettingServiceConnector.csGetArtifactAsync (~line 70) and ProcessAsync (~line 138)
  • src/SeoToolkit.Umbraco.MetaFields.Core/Mappers/DocumentTypeSettingsMapper.cs (~line 71)
  • src/SeoToolkit.Umbraco.MetaFields.Core/Common/Converters/EditorConverters/UmbracoMediaConverter.cs

Problem:
The connector exports JsonConvert.SerializeObject(valueDto.Value), but the DTO returned by
metaFieldsSettingsService.Get()/GetAll() holds object-form values: the entity→DTO mapper
runs ConvertDatabaseToObject on each field. For SeoImageEditEditor (the default social/OG
image field) the object-form is an IPublishedContent. Newtonsoft-serializing that either
throws (self-referencing loop) or emits a huge, environment-specific blob — either way the
artifact is broken.

On import the connector runs ConvertDatabaseToObject(JsonConvert.DeserializeObject(json));
UmbracoMediaConverter does Guid.TryParse on the JObject, fails, and returns null — the value
is silently lost on the target.

Text and FieldValueConverter fields only round-trip because their object-form and
database-form happen to coincide. Any GUID-based converter breaks.

Fix:
Round-trip through the portable editor wire format instead:

  • Export: seoField.Editor.ValueConverter.ConvertObjectToEditorValue(valueDto.Value) → serialize that.
  • Import: deserialize → ConvertEditorToDatabaseValue(...) → store.

For media this yields a MediaEditorModel[{ mediaKey: <guid> }] which is stable across
environments. Needs a live-environment repro to confirm current throw-vs-blob behaviour, but the
type mismatch is unambiguous.


2. Target-side writes bypass cache invalidation and notifications (MEDIUM-HIGH)

  • Fixed (MetaFieldsValue connector)
  • Fixed (KeyValues connector)

Where:

  • src/SeoToolkit.Umbraco.Deploy/Connectors/ServiceConnectors/SeoToolkitMetaFieldsValueServiceConnector.csProcessAsync
  • src/SeoToolkit.Umbraco.Deploy/Connectors/ServiceConnectors/SeoToolkitKeyValuesServiceConnector.csProcessAsync

Problem:
Six connectors process through services (which clear caches and publish notifications); these two
write straight to repositories:

  • MetaFieldsValue calls valueRepository.Add/Update/Delete directly.
    MetaFieldsValueService.GetUserValues caches per node/culture for 30 minutes and is only
    invalidated via DistributedCache.Refresh(SeoValueCacheRefresher.CacheGuid, …). After a
    transfer the target keeps rendering stale meta values for up to 30 min — worse on
    load-balanced setups since the distributed cache is never touched. It also never publishes
    MetaFieldsValueChangedNotification, so the target's own .uda is not refreshed by
    MetaFieldsValueDiskRefresherHandler.
  • KeyValues never publishes SeoKeyValueSavedNotification (only the controller does), so
    the target's key/values .uda refresh is skipped. No runtime-cache concern — the repository
    is uncached.

Fix:
After processing, refresh the distributed cache and publish the same notifications the services
do — or add service-level methods the connector can call so the behaviour can't drift.


3. Media referenced by SEO values gets no artifact dependency (MEDIUM)

  • Fixed

Where:

  • src/SeoToolkit.Umbraco.Deploy/UdiJsonHelper.cs (regex, line 8)
  • Both connectors that call UdiJsonHelper.FindUdis (MetaFieldsValue, MetaFieldsSetting)

Problem:
The regex only matches literal umb://document|media|member/<32hex> strings inside value JSON.
The image field stores a bare media GUID (database-form), so a node's OG image — or a doc
type's default image — never produces a media dependency. The transfer can land on a target
where the referenced media doesn't exist, giving broken images with no deploy warning.

Fix:
Interacts with #1: once values round-trip via the editor wire format, emit a
umb://media/<guid> dependency for GUID-typed converter values (e.g. by asking the converter,
or special-casing known GUID-based editors). Keep the regex scan for values that genuinely embed
UDI strings (e.g. RTE-like content).


4. Artifact checksums built from unordered dictionaries (MEDIUM)

  • Fixed

Where:

  • SeoToolkitMetaFieldsValueServiceConnector.GetArtifactAsyncValues (culture → alias → json)
  • SeoToolkitKeyValuesServiceConnector / KeyValuesModelValues
  • SeoToolkitScriptServiceConnectorConfig
  • SeoToolkitDomainCollectionServiceConnectorSettings, DomainNames
  • SeoToolkitMetaFieldsSettingServiceConnectorFields list order

Problem:
Umbraco Deploy hashes DeployArtifactBase.ChecksumJson — the serialized artifact. All the
dictionary/list properties above are populated from SQL fetches with no ORDER BY, so identical
data can serialize in a different order and produce a different checksum. Because the per-node
artifacts are attached to documents as Match dependencies, an order flip makes documents
report "out of date" and re-transfer for no reason.

Fix:
OrderBy culture/alias/key (ordinal) when building every artifact collection, and order
Fields by alias. Cheap, and standard practice for Deploy connectors.


5. Sitemap-content .uda never refreshed on save/delete (MEDIUM-LOW)

  • Fixed

Where:

  • src/SeoToolkit.Umbraco.Sitemap.Core/Services/SitemapService/SitemapService.csSetContentSettings (~line 53)
  • src/SeoToolkit.Umbraco.Deploy/NotificationHandlers/SeoToolkitDiskRefresherHandlers.cs

Problem:
We fixed this exact gap for MetaFields values (MetaFieldsValueChangedNotification +
MetaFieldsValueDiskRefresherHandler), but the sitemap half is missing. SetContentSettings
publishes nothing — and it silently deletes the row when settings return to all-default
values — yet seotoolkit-sitemap-content is registered as a disk entity type. Per-node sitemap
.uda files therefore go stale (or orphaned, after a reset-to-default) on the source.

Fix:
Mirror the MetaFields pattern: publish a SitemapContentChangedNotification(nodeKey) from
SetContentSettings (both the Set and the delete-on-default branches), and add a disk refresher
handler that writes the .uda while settings exist and deletes it when they don't.


6. MetaFieldsValueService.Delete never clears the cache (LOW)

  • Fixed

Where:

  • src/SeoToolkit.Umbraco.MetaFields.Core/Services/MetaFieldsValueService/MetaFieldsValueService.cs — both Delete overloads

Problem:
AddValues calls ClearCache(nodeId) (distributed cache refresh); neither Delete overload
does. A deleted value keeps serving from the runtime cache for up to 30 minutes. Pre-existing
for the int overload, but this PR touched the Guid overload (added the notification publish), so
it's a natural fix to fold in.

Fix: add ClearCache(nodeId) to both overloads — one line each.


7. Legacy NodeId column can be written as 0 (LOW)

  • Fixed / accepted

Where:

  • src/SeoToolkit.Umbraco.MetaFields.Core/Repositories/SeoValueRepository/MetaFieldsDatabaseRepository.cs — Guid Add/Update (GetNodeId)

Problem:
The Guid overloads resolve the legacy NodeId int via the published content cache. During a
deploy (content just created, possibly unpublished) this resolves to 0, so the row is written
with NodeId = 0 and is invisible to the obsolete int-based read paths. Low impact since the
int paths are [Obsolete] and being phased out, but worth either accepting explicitly or
resolving via IIdKeyMap/IContentService instead of the published cache.


8. Prune can't delete NULL-culture rows (LOW)

  • Fixed

Where:

  • MetaFieldsDatabaseRepository.GetAllValues(Guid) (groups Culture ?? "") vs Delete(Guid, alias, culture) (Culture == culture)

Problem:
GetAllValues maps a NULL culture to "", but the prune path in
SeoToolkitMetaFieldsValueServiceConnector.ProcessAsync then calls Delete(nodeKey, alias, ""),
which never matches SQL NULL. Rows with a NULL culture can never be pruned (silently). The
entity defaults Culture to "" so NULLs should be rare — likely only legacy data.

Fix: in Delete, treat "" as "empty or NULL" (it.Culture == culture || it.Culture == null
when culture is empty), or normalise NULL cultures in a migration.


9. Minor notes / polish

  • Disabled domain-collection connector can fail dependent deploys. Scripts and
    key/values emit Exist dependencies on seotoolkit-domain-collection; if that entity type is
    in DisabledEntityTypes the dependency can't be satisfied and the deploy may error. At
    minimum, document the interaction in the README.
  • Version range convention: Umbraco.Deploy.Infrastructure is pinned to
    [18.0.0,18.999) — conventional form is [18.0.0,19.0.0).
  • Naming: SeoToolkitContentExportedHandler handles the Exporting notification —
    rename to SeoToolkitContentExportingHandler for clarity.
  • N+1 in SeoToolkitSeoSettingServiceConnector.GetEntitiesAsync: calls
    seoSettingsService.GetAll() once per key via GetEntityAsync. Cached, so low impact; could
    enumerate the dictionary once.

Suggested fix order

  1. 1 + 3 together — value round-trip and media dependencies (can genuinely break exports or silently lose data).
  2. 2, 4 — target-side cache/notification bypass and checksum stability (degrade correctness after otherwise-successful deploys).
  3. 5, 6 — sitemap-content disk refresh and Delete cache clear (consistency with the pattern already shipped).
  4. 7, 8, 9 — legacy/polish items, batch as convenient.

@robertjf
robertjf marked this pull request as draft July 15, 2026 01:01
@robertjf

robertjf commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up code review — 8 findings, 7 fixed

Ran a second-pass high-effort review over the deploy-connector diff. It found that two items from the earlier audit were only partially resolved, plus six issues the first audit missed. Fixes are in 8f1ae49; a version pin + marketplace descriptor are in 54bb94c.

Correctness fixes

# Issue Fix
1 ScriptRepository.Get(Guid/int) returned ToModel(FirstOrDefault(...))ToModel(null) NRE for an unknown key, so ScriptManagerService.Save's Get(key) is null insert-detection could never fire → every first-time script transfer crashed Get returns null for a missing row
2 Audit 1 fixed the export serialization but the import overwrite-merge still re-serialized target-only object-form values (e.g. media IPublishedContent) → broken blob Round-trip preserved fields object→editor→database form before Set
3 MetaFieldsSetting ProcessAsync mutated state.Entity in place — the service's 30-min cached DTO Build a fresh DTO; never mutate the cached instance
4 Element doc types passed the null-check and hit Set, which throws ArgumentExceptionaborted the whole deploy Skip when contentType.IsElement
5 Audit 8 fixed Delete only; Exists(Guid)/GetAllValues(Guid,culture) still exact-matched culture → legacy NULL-culture rows caused duplicate inserts and a later ToDictionary ArgumentException Both now treat empty culture as empty-or-NULL
6 GetEntityName/GetEntityUdi dereferenced entity.Content with no guard → an orphaned settings row (deleted content type) NRE'd the entire export Skip rows with null Content on export
7 ContentExportingHandler appended Match dependencies without checking DisabledEntityTypes → a disabled connector yields no artifact, leaving an unsatisfiable dependency that can fail the document transfer Skip deps for disabled entity types
8 Deleting a domain collection removed only its own .uda; the collection's KeyValues .uda (with an Exist dep on the collection) was left orphaned Delete the KeyValues .uda too

Findings 1, 4 and the culture-match issue (5) are code-traced; the rest are reproducible under specific data/config states (orphaned rows, legacy NULL cultures, disabled connectors).

Notes

  • Tests: ContentExportingHandlerTests updated for the new ctor param + a new disabled-entity-type test. Full suite green (105/105), build clean.
  • Highest-risk change is 2/3 (the overwrite-merge persistence path). Logic mirrors the already-trusted export round-trip, but it's only exercised by unit tests — a live deploy of a doc type carrying a media/OG-image setting in overwrite mode would be the real confirmation. Worth a manual check before merge.

Follow-up fixes found while diagnosing why SeoToolkit schema failed to
import (and kept re-flagging as "changed") on a self-hosted restore.

* Session scope poisoning (the import blocker): repository read/delete
  methods created scopes with CreateScope() but never completed them.
  Nested inside Deploy's session scope this poisoned the ambient scope
  ("Failed to complete the session because its scope would not complete").
  Read/delete scopes now use CreateScope(autoComplete: true), matching the
  sibling repositories. Affects MetaFieldsSettingsDatabaseRepository,
  MetaFieldsDatabaseRepository and SeoDomainsRepository (the metafields
  Delete scopes also never completed, so deletes silently rolled back).

* Per-node types are content-coupled, not schema: MetaFieldsValue and
  SitemapContent are no longer registered as disk entity types (that wrote
  a schema .uda per content node). They travel with their document as Match
  dependencies via SeoToolkitContentExportingHandler. Removed their disk
  refresher handlers.

* Schema converges like core artifacts: removed the PruneMissing setting.
  Collection-valued connectors (key/values, metafields settings/values) now
  reconcile unconditionally, replacing the target to match the artifact --
  otherwise target-only rows meant the checksum never matched the source.

* ListValueConverter (keywords): ConvertDatabaseToObject now always returns
  a non-null array, so null and empty-string forms both export as "[]" and
  converge instead of flipping between "[]" and an omitted field.

* Disk refreshers refresh signatures: SeoToolkitDiskRefresherHandlerBase now
  calls ISignatureService.SetSignature/ClearSignature alongside the disk
  write, matching EntitySavedDeployRefresherNotificationAsyncHandlerBase.

* Cache entity resolution via IContextCache in the base connector, per the
  Deploy extending docs.

Adds MetaFieldsSettingConvergenceTests; updates existing tests for the
removed PruneMissing parameter, signature refresh, and PassThroughCache.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@robertjf

Copy link
Copy Markdown
Contributor Author

Pushed a set of follow-up fixes to the deploy connector (dd51a9e), found while testing a self-hosted restore where SeoToolkit schema wouldn't import.

Root cause of the import failure was mundane but fatal: a few repository read/delete methods opened scopes with CreateScope() and never completed them. Running nested inside Deploy's session scope, that poisons the ambient scope and the session fails with "Failed to complete the session because its scope would not complete." — so metafields-setting never imported. Switching those reads/deletes to CreateScope(autoComplete: true) (matching the repos that already worked) fixes it.

While tracking that down I also cleaned up a few related things that caused entities to re-flag as "changed" on every deploy:

  • Per-node types (MetaFieldsValue, SitemapContent) are now content-coupled rather than registered as disk entities — they no longer dump a schema .uda per content node; they ride along with their document as Match dependencies.
  • Removed PruneMissing so schema converges like core artifacts (unconditional reconcile-to-match) instead of leaving target-only rows that never matched the source checksum.
  • ListValueConverter (keywords) now returns a non-null array for null/empty, so it can't flip between "[]" and an omitted field across environments.
  • Disk refreshers now refresh signatures (ISignatureService), matching the framework's EntitySavedDeployRefresher… base — a stale signature was another source of phantom "changed" state.
  • Base connector caches entity resolution via IContextCache as recommended in the extending docs.

New MetaFieldsSettingConvergenceTests proves every metafields-setting converter round-trips import→DB→export to the same value (incl. the keywords empty-form regression); existing tests updated for the dropped PruneMissing param, signature assertions, and PassThroughCache. All 118 tests green.

@robertjf

Copy link
Copy Markdown
Contributor Author

@patrickdemooij9 the latest patch has been tested on a real project and is working well for deploying schema changes (settings, etc.) using Deploy, but it currently doesn't transfer the actual metadata values etc. which I'm now working on.

Because there is no way to hook into a content item transfer currently (I'll make a feature request), we can't just ride along with the content item transfer unless we add something like a property to the content item so that it gets pulled in as a dependency. So the current alternative is to add actions to the content action menu and the publish button (sub-action) to transfer/queue the seo meta values independently.

Move the MetaFieldsValue / SitemapContent per-node types from the
content-export coupling approach to first-class transfer/restore
entities, and add the backoffice UI to drive transfer/queue/restore.

- Register per-node types as transfer entities (SupportsImportExport
  false so they never write a schema .uda); refresh their Deploy
  signature on change via new signature-refresher handlers.
- Remove the now-dead SeoToolkitContentExportingHandler and its tests.
- Add SeoToolkitEntityServiceConnectorBase.AllowMissingEntity so a node
  with no SEO data of a given kind is a queue/transfer no-op, not a throw.
- Add SeoToolkitDeployController (seoTransferItems) so the client can
  discover which per-node SEO entities exist for a node.
- Add the Vite/TS backoffice frontend: Transfer SEO Now, Add SEO to
  Transfer Queue, and Restore SEO actions; register the entry point in
  ManifestLoader and switch the project to the Razor SDK.
- Fix InvalidProgramException on transfer: MetaFieldsDatabaseRepository
  now uses the generic Delete<T>(Sql) overload instead of the
  non-generic Delete(object) that NPoco mis-binds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@robertjf

Copy link
Copy Markdown
Contributor Author

Update: per-node SEO as transfer entities + backoffice actions

Reworked how the per-node types (MetaFieldsValue / SitemapContent) flow through Deploy, and added the backoffice UI to drive it.

Architecture change

  • Per-node types are now first-class transfer/restore entities instead of being coupled to content export. They register with SupportsImportExport = false so they never write a schema .uda; their Deploy signature is refreshed on change via new signature-refresher handlers, so an edit is detected as a change to transfer.
  • Removed the now-dead SeoToolkitContentExportingHandler (and its tests) along with the ArtifactExportingNotification wiring.
  • SeoToolkitEntityServiceConnectorBase gains AllowMissingEntity: for per-node types, a node with no SEO data of a given kind is a queue/transfer no-op rather than a throw.

Backoffice UI (new Vite/TS frontend)

  • Transfer SEO Now (workspace action), Add SEO to Transfer Queue, and Restore SEO (entity actions) — the SEO entity actions sit directly below Deploy's Partial restore in the content menu.
  • New SeoToolkitDeployController (seoTransferItems) lets the client discover which per-node SEO entities actually exist for a node, so actions only move what has data.
  • Entry point registered in ManifestLoader; project switched to Microsoft.NET.Sdk.Razor with StaticWebAssetBasePath, matching the other SeoToolkit projects.

Bug fix

  • Fixed an InvalidProgramException thrown on the target during transfer: MetaFieldsDatabaseRepository now uses the generic Delete<T>(Sql) overload. The non-generic Delete(object) was binding the Sql argument as the POCO to delete, which made NPoco emit an invalid setter.

Restore SEO: open Deploy's own partial-restore dialog for source-environment
selection and drop the forced ignoreDependencies flag. Forcing it returned a
500 ("Ignoring dependencies is not allowed") when the environment disallows
it; unnecessary because the SEO artifacts depend on the document in Exist mode,
so a normal restore never touches document content.

Transfer SEO Now: deploy to the target's deployUrl instead of umbracoUrl.
umbracoUrl is the backoffice URL, so opening the remote Deploy session against
it 404'd -> RemoteApiException ("The remote API was not found") in
SourceDeployWorkItem.

Add SEO To Transfer Queue: route through Deploy's DeployTransferQueueManager
context (add()) instead of a raw POST to /queue/add, so the transfer queue
widget refreshes (reload + broadcast + entity signs), matching the native
"Add to Transfer Queue" action. Removed the now-dead queueAdd client method.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@robertjf

Copy link
Copy Markdown
Contributor Author

Follow-up: fix SEO transfer/restore backoffice actions (50e40b9)

Three runtime fixes to the per-node SEO deploy actions, each verified against Umbraco Deploy's own frontend/backend source.

1. Restore SEO — 500 Ignoring dependencies is not allowed
The action forced ignoreDependencies: true, which Deploy rejects when the environment's allowRestoreIgnoreDependencies is off. It was never needed: the SEO artifacts depend on the document in Exist mode, so a normal partial restore only checks the document exists on the target and never touches its content. The action now opens Deploy's own Deploy.Modal.PartialRestore dialog so the user picks the source environment, and takes ignoreDependencies (default false) from that dialog — which only offers the toggle when the environment permits it.

2. Transfer SEO Now — RemoteApiException: The remote API was not found
Instant deploy was sending clientConfiguration.target.umbracoUrl (the target's backoffice URL) as the deploy target, so opening the remote Deploy session 404'd inside SourceDeployWorkItem.BeginSessionAsync. Deploy's native transfer-now.action uses target.deployUrl (the Deploy API endpoint). Fixed to read deployUrl.

3. Add SEO To Transfer Queue — queue widget didn't refresh
The action POSTed straight to /queue/add, bypassing the client-side queue state. Deploy's transfer-queue widget is driven by the DeployTransferQueueManager context, whose add() does the POST and reloads + broadcasts + updates entity signs. The action now goes through that context (re-declared by its registered alias, no dependency on @umbraco-deploy/*), matching the native "Add to Transfer Queue" behaviour. The now-dead raw queueAdd client method was removed.

Verification: tsc clean, 13/13 unit tests pass (added coverage asserting instant-deploy targets deployUrl), Vite lib build succeeds. Runtime behaviour in the backoffice still to be confirmed on a Cloud environment.

@robertjf
robertjf marked this pull request as ready for review July 23, 2026 23:26
@robertjf

Copy link
Copy Markdown
Contributor Author

@patrickdemooij9 this is now ready for review - I've fully tested it on a real project and it's working well.
A few UI changes:

Transferring / Restoring SEO metadata/robots etc. can't happen when triggered by a node transfer, so I've had to add to the Publish Button and the Content Action menu:

image image

The transfer mechanism currently transfers the SEO metadata / robots and the content node itself - an acceptable workaround, but we could change this to only queue the SEO metadata/robots.

Ideally, we wouldn't need these additional actions, but there's currently no way to hook into that transfer that I've found so far!

@qodo-code-review

qodo-code-review Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 8 rules

Grey Divider


Remediation recommended

1. Guid.Empty domain collection Id ✓ Resolved 🐞 Bug ≡ Correctness
Description
Both SeoDomainsRepository.Save and ScriptManagerService.Save preserve caller-supplied nullable GUID
identifiers on insert, but they only guard against null and therefore treat Guid.Empty as a valid
value that can be persisted as the primary key. If any caller passes Guid.Empty (e.g.,
default-initialized Guid?), this can insert all-zero identifiers, break identity semantics, and
cause primary-key collisions/duplicate-key failures.
Code

src/SeoToolkit.Umbraco.Common.Core/Repositories/Domains/SeoDomainsRepository.cs[R84-89]

            collectionEntity ??= new SeoDomainCollectionEntity
            {
-                Id = Guid.NewGuid()
+                // Preserve a caller-supplied Id when it doesn't exist yet in this environment
+                // (e.g. a collection transferred via Deploy) so the identifier round-trips.
+                Id = collection.Id ?? Guid.NewGuid()
            };
Relevance

⭐⭐⭐ High

Correctness guard against invalid all-zero GUID keys; team commonly accepts GUID-related data
integrity fixes.

PR-#440

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
In SeoDomainsRepository.Save, the insert path assigns the entity Id via null-coalescing (e.g.,
collection.Id ?? Guid.NewGuid()), which means a non-null Guid? containing Guid.Empty will be
persisted unchanged into a non-nullable Guid primary key column. Similarly,
ScriptManagerService.Save uses script.Key.Value when checking for existence and then proceeds to
an insert (Add) path that preserves the provided key; because Script.Key is a nullable Guid?,
it can still contain Guid.Empty, and the repository maps Key = script.Key.Value into the
required ScriptEntity.Key column, allowing an all-zero GUID to reach the database as the primary
key.

src/SeoToolkit.Umbraco.Common.Core/Repositories/Domains/SeoDomainsRepository.cs[71-90]
src/SeoToolkit.Umbraco.Common.Core/Models/Business/SeoDomainCollection.cs[6-12]
src/SeoToolkit.Umbraco.Common.Core/Models/Database/SeoDomainCollectionEntity.cs[7-17]
src/SeoToolkit.Umbraco.ScriptManager.Core/Services/ScriptManagerService.cs[39-56]
src/SeoToolkit.Umbraco.ScriptManager.Core/Models/Business/Script.cs[7-17]
src/SeoToolkit.Umbraco.ScriptManager.Core/Repositories/ScriptRepository.cs[112-123]
src/SeoToolkit.Umbraco.ScriptManager.Core/Models/Database/ScriptEntity.cs[10-18]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`SeoDomainsRepository.Save` and `ScriptManagerService.Save` preserve caller-supplied nullable GUID identifiers on insert, but they only check for null and therefore treat `Guid.Empty` as a valid identifier. This allows `Guid.Empty` to be persisted into non-nullable primary key columns (e.g., `SeoDomainCollection.Id`/`ScriptEntity.Key`), risking duplicate-key collisions and invalid all-zero identifiers in the database.

## Issue Context
- `SeoDomainCollection.Id` and `Script.Key` are `Guid?`, so callers can supply `Guid.Empty` (non-null) inadvertently (e.g., default initialization).
- The current insert logic uses null-coalescing / null checks, so `Guid.Empty` bypasses regeneration and is written as-is.
- The persistence layer maps these values into required, non-nullable Guid primary key columns, so multiple inserts using `Guid.Empty` can collide and/or corrupt identity semantics.

## Fix Focus Areas
- src/SeoToolkit.Umbraco.Common.Core/Repositories/Domains/SeoDomainsRepository.cs[71-90]
- src/SeoToolkit.Umbraco.ScriptManager.Core/Services/ScriptManagerService.cs[39-56]
- src/SeoToolkit.Umbraco.ScriptManager.Core/Repositories/ScriptRepository.cs[112-123]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@robertjf
robertjf marked this pull request as draft July 23, 2026 23:38
@robertjf
robertjf marked this pull request as ready for review July 23, 2026 23:54
@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit f646876

@robertjf
robertjf marked this pull request as draft July 24, 2026 00:02
@robertjf
robertjf marked this pull request as ready for review July 24, 2026 00:02
@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit 0ae23a6

@patrickdemooij9

Copy link
Copy Markdown
Owner

@patrickdemooij9 this is now ready for review - I've fully tested it on a real project and it's working well. A few UI changes:

Transferring / Restoring SEO metadata/robots etc. can't happen when triggered by a node transfer, so I've had to add to the Publish Button and the Content Action menu:

image image
The transfer mechanism currently transfers the SEO metadata / robots and the content node itself - an acceptable workaround, but we could change this to only queue the SEO metadata/robots.

Ideally, we wouldn't need these additional actions, but there's currently no way to hook into that transfer that I've found so far!

Ah yeah, that was the same issue that I ran up against. But I never thought of working around it like that! I think that's perfect for now until they make a change where we can transfer it together with the content.
I'll see if I can make some time soon to look through the entire PR as it's quite a lot. Thank you so much for taking the time on creating this!

…descendants

Transfer and queue SEO entities without moving the content node itself (the
SEO artifacts depend on the document in Exist mode only, so Deploy just
requires the node to exist upstream).

The tree "Add SEO to Transfer Queue" action now reuses Deploy's native queue
dialog (Deploy.Modal.Queue) to capture the "include descendants" choice, and
queuing runs server-side in a single request: a new POST seoQueueAdd endpoint
enumerates the node (and every descendant when requested) and adds each SEO
entity to the transfer queue via ITransferQueue — so the client makes one call
and refreshes the queue widget once, instead of one queue/add per item.

Adds SeoToolkitDeployControllerTests covering single-node queuing, descendants,
and the unauthorized case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@robertjf

Copy link
Copy Markdown
Contributor Author

Update: SEO transfer independent of the content node + optional descendants (server-side)

Pushed aad5684 with two behaviour changes to the transfer/queue actions:

1. SEO transfers independently of the content node. Both "Transfer SEO Now" (instant) and "Add SEO to Transfer Queue" now move only the SEO entities, never the document node. This is safe because the SEO artifacts declare only an Exist-mode dependency on the document — Deploy requires the node to already exist upstream but never re-transfers its content.

2. Optional descendants when queuing — done server-side in one call. The tree "Add SEO to Transfer Queue" action reuses Deploy's own queue dialog (Deploy.Modal.Queue, re-declared by alias) to capture the Include descendants choice. A new endpoint then does the work server-side:

  • POST seoQueueAdd(contentKey, includeDescendants, releaseDate) on SeoToolkitDeployController enumerates the node (and every descendant via IContentService.GetPagedDescendants when requested), resolves each SEO entity's NamedUdiRange through IServiceConnectorFactory, and adds it to the transfer queue via ITransferQueue.Add(...) — mirroring Deploy's own AddToQueueController.
  • The client makes a single request and refreshes the queue widget once, instead of one queue/add per SEO entity (which previously made items trickle in one at a time).

Tests: new SeoToolkitDeployControllerTests cover single-node queuing, descendants, and the unauthorized case; the TS client/util tests cover queueSeo and buildTransferSet.

Notes:

  • The reused dialog also shows culture/publish options; for SEO only includeDescendants and releaseDate are used.
  • The endpoint relies on backoffice-access auth and does not (yet) replicate Deploy's granular per-UDI QueueForTransfer permission check — easy to add if you'd prefer full parity.

@robertjf

Copy link
Copy Markdown
Contributor Author

@patrickdemooij9 any chance you might be able to merge this in at some stage? :)

@patrickdemooij9

patrickdemooij9 commented Sep 10, 2026

Copy link
Copy Markdown
Owner

Hi @robertjf
Sorry that it took so long. I initially forgot a bit about it and I was on vacation when you wrote the headsup. But I am back now and looking at the PR! I did run into an issue straight away trying to run it for myself. Something about the IDiskEntityService not being able to be resolved.

'Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: Umbraco.Cms.Core.Events.INotificationAsyncHandler`1[SeoToolkit.Umbraco.Common.Core.Notifications.SeoSettingSavedNotification] Lifetime: Transient ImplementationType: SeoToolkit.Umbraco.Deploy.NotificationHandlers.SeoSettingDiskRefresherHandler': Unable to resolve service for type 'Umbraco.Deploy.Infrastructure.Disk.IDiskEntityService' while attempting to activate 'SeoToolkit.Umbraco.Deploy.NotificationHandlers.SeoSettingDiskRefresherHandler'.) (Error while validating the service descriptor 'ServiceType: Umbraco.Cms.Core.Events.INotificationAsyncHandler`1[SeoToolkit.Umbraco.MetaFields.Core.Notifications.MetaFieldSettingsSavedNotification] Lifetime: Transient ImplementationType: SeoToolkit.Umbraco.Deploy.NotificationHandlers.MetaFieldsSettingDiskRefresherHandler': Unable to resolve service for type 'Umbraco.Deploy.Infrastructure.Disk.IDiskEntityService' while attempting to activate 'SeoToolkit.Umbraco.Deploy.NotificationHandlers.MetaFieldsSettingDiskRefresherHandler'.) (Error while validating the service descriptor 'ServiceType: Umbraco.Cms.Core.Events.INotificationAsyncHandler`1[SeoToolkit.Umbraco.Sitemap.Core.Notifications.SitemapPageSettingsSavedNotification] Lifetime: Transient ImplementationType: SeoToolkit.Umbraco.Deploy.NotificationHandlers.SitemapPageTypeDiskRefresherHandler': Unable to resolve service for type 'Umbraco.Deploy.Infrastructure.Disk.IDiskEntityService' while attempting to activate 'SeoToolkit.Umbraco.Deploy.NotificationHandlers.SitemapPageTypeDiskRefresherHandler'.) (Error while validating the service descriptor 'ServiceType: Umbraco.Cms.Core.Events.INotificationAsyncHandler`1[SeoToolkit.Umbraco.ScriptManager.Core.Notifications.ScriptSavedNotification] Lifetime: Transient ImplementationType: SeoToolkit.Umbraco.Deploy.NotificationHandlers.ScriptDiskRefresherHandler': Unable to resolve service for type 'Umbraco.Deploy.Infrastructure.Disk.IDiskEntityService' while attempting to activate 'SeoToolkit.Umbraco.Deploy.NotificationHandlers.ScriptDiskRefresherHandler'.) (Error while validating the service descriptor 'ServiceType: Umbraco.Cms.Core.Events.INotificationAsyncHandler`1[SeoToolkit.Umbraco.ScriptManager.Core.Notifications.ScriptDeletedNotification] Lifetime: Transient ImplementationType: SeoToolkit.Umbraco.Deploy.NotificationHandlers.ScriptDiskRefresherHandler': Unable to resolve service for type 'Umbraco.Deploy.Infrastructure.Disk.IDiskEntityService' while attempting to activate 'SeoToolkit.Umbraco.Deploy.NotificationHandlers.ScriptDiskRefresherHandler'.) (Error while validating the service descriptor 'ServiceType: Umbraco.Cms.Core.Events.INotificationAsyncHandler`1[SeoToolkit.Umbraco.Common.Core.Notifications.SeoDomainCollectionSavedNotification] Lifetime: Transient ImplementationType: SeoToolkit.Umbraco.Deploy.NotificationHandlers.DomainCollectionDiskRefresherHandler': Unable to resolve service for type 'Umbraco.Deploy.Infrastructure.Disk.IDiskEntityService' while attempting to activate 'SeoToolkit.Umbraco.Deploy.NotificationHandlers.DomainCollectionDiskRefresherHandler'.) (Error while validating the service descriptor 'ServiceType: Umbraco.Cms.Core.Events.INotificationAsyncHandler`1[SeoToolkit.Umbraco.Common.Core.Notifications.SeoDomainCollectionDeletedNotification] Lifetime: Transient ImplementationType: SeoToolkit.Umbraco.Deploy.NotificationHandlers.DomainCollectionDiskRefresherHandler': Unable to resolve service for type 'Umbraco.Deploy.Infrastructure.Disk.IDiskEntityService' while attempting to activate 'SeoToolkit.Umbraco.Deploy.NotificationHandlers.DomainCollectionDiskRefresherHandler'.) (Error while validating the service descriptor 'ServiceType: Umbraco.Cms.Core.Events.INotificationAsyncHandler`1[SeoToolkit.Umbraco.Common.Core.Notifications.SeoKeyValueSavedNotification] Lifetime: Transient ImplementationType: SeoToolkit.Umbraco.Deploy.NotificationHandlers.KeyValuesDiskRefresherHandler': Unable to resolve service for type 'Umbraco.Deploy.Infrastructure.Disk.IDiskEntityService' while attempting to activate 'SeoToolkit.Umbraco.Deploy.NotificationHandlers.KeyValuesDiskRefresherHandler'.) (Error while validating the service descriptor 'ServiceType: Umbraco.Cms.Core.Events.INotificationAsyncHandler`1[SeoToolkit.Umbraco.MetaFields.Core.Notifications.MetaFieldsValueChangedNotification] Lifetime: Transient ImplementationType: SeoToolkit.Umbraco.Deploy.NotificationHandlers.MetaFieldsValueSignatureRefresherHandler': Unable to resolve service for type 'Umbraco.Deploy.Core.Connectors.ServiceConnectors.IServiceConnectorFactory' while attempting to activate 'SeoToolkit.Umbraco.Deploy.NotificationHandlers.MetaFieldsValueSignatureRefresherHandler'.) (Error while validating the service descriptor 'ServiceType: Umbraco.Cms.Core.Events.INotificationAsyncHandler`1[SeoToolkit.Umbraco.Sitemap.Core.Notifications.SitemapContentChangedNotification] Lifetime: Transient ImplementationType: SeoToolkit.Umbraco.Deploy.NotificationHandlers.SitemapContentSignatureRefresherHandler': Unable to resolve service for type 'Umbraco.Deploy.Core.Connectors.ServiceConnectors.IServiceConnectorFactory' while attempting to activate 'SeoToolkit.Umbraco.Deploy.NotificationHandlers.SitemapContentSignatureRefresherHandler'.) (Error while validating the service descriptor 'ServiceType: SeoToolkit.Umbraco.Deploy.Composing.SeoToolkitDeployComponent Lifetime: Singleton ImplementationType: SeoToolkit.Umbraco.Deploy.Composing.SeoToolkitDeployComponent': Unable to resolve service for type 'Umbraco.Deploy.Infrastructure.Disk.IDiskEntityService' while attempting to activate 'SeoToolkit.Umbraco.Deploy.Composing.SeoToolkitDeployComponent'.)'

I'll see if I can fix it myself, but just giving you a headsup in case you know the answer

============

Managed to find the issue. I didn't have Umbraco deploy installed on the site. I would have thought the package would add that as a reference, but good to know that it doesn't

namespace SeoToolkit.Umbraco.Deploy.Connectors.ServiceConnectors
{
[UdiDefinition(SeoToolkitDeployConstants.UdiEntityType.Script, UdiType.GuidUdi)]
public class SeoToolkitScriptServiceConnector(

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the uSync implementation, I actually didn't add scriptmanager to the syncs as I think each environment will probably have different settings for their scripts anyway. Do you have a different view on that? We could still keep it here though as people are able to turn it off if they don't want it

restoreNodes: udis.map((udi) => ({ udi, includeDescendants: false, selector: "this" })),
}),
});
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to use this seoDeployClient? Can't we use the hey-api like in the other projects so we have typesafety on the endpoints?

// Order names/settings (ordinal) for a stable serialization/checksum.
var domainNames = entity.DomainIds
.Select(id => allDomains.FirstOrDefault(d => d.Id == id)?.DomainName)
.Where(name => name is not null)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you could use .WhereNotNull() here so you don't have to do .Where().Select()

keyValueRepository.Set(key, value, domainId);
}

// The repository write skips the controller's notification, so publish it here — that

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we perhaps move the notification handler to the repository then? I think that makes more sense than having it only trigger in the controller

var json = value is null ? null : JsonConvert.SerializeObject(value);

// UDIs embedded in the value JSON (e.g. RTE-like content).
foreach (var referencedUdi in UdiJsonHelper.FindUdis(json))

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am trying to think in which case we have Udis inside of the content. I can only think of the media one and you already have that covered below

}
}

// We wrote straight to the repository (rather than through the service's culture-aware

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know why we can't go through the service class and have to go straight to the repository?

@patrickdemooij9

Copy link
Copy Markdown
Owner

Hi @robertjf

Thanks again for the great work you did! I am sorry it took a bit for me to go through it but I have finally done it (it did take some hours :P)
I also added a second site to the solution, so it's a lot easier to test and see what happens. If you run both sites, they should be connected through Umbraco deploy

I'll also let Claude do a review through the code to see if it spots anything that I might have missed, but I think the above points will be the main things to still look at. Do let me know if you need any help with and I am more than happy to help out!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for Umbraco Deploy

2 participants