diff --git a/src/Umbraco.Core/Services/ContentBlueprintManager.cs b/src/Umbraco.Core/Services/ContentBlueprintManager.cs
new file mode 100644
index 0000000000..c26e46272f
--- /dev/null
+++ b/src/Umbraco.Core/Services/ContentBlueprintManager.cs
@@ -0,0 +1,373 @@
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using Microsoft.Extensions.Logging;
+using Umbraco.Cms.Core.Events;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Notifications;
+using Umbraco.Cms.Core.Persistence.Querying;
+using Umbraco.Cms.Core.Persistence.Repositories;
+using Umbraco.Cms.Core.Scoping;
+using Umbraco.Cms.Core.Services.Changes;
+using Umbraco.Extensions;
+
+namespace Umbraco.Cms.Core.Services;
+
+///
+/// Manager for content blueprint (template) operations.
+///
+///
+///
+/// This class encapsulates blueprint operations extracted from ContentService
+/// as part of the ContentService refactoring initiative (Phase 7).
+///
+///
+/// Design Decision: This class is public for DI but not intended for direct external use:
+///
+/// - Blueprint operations are tightly coupled to content entities
+/// - They don't require independent testability beyond ContentService tests
+/// - The public API remains through IContentService for backward compatibility
+///
+///
+///
+/// Notifications: Blueprint operations fire the following notifications:
+///
+/// - - after saving a blueprint
+/// - - after deleting blueprint(s)
+/// - - after save/delete for cache invalidation
+///
+///
+///
+public sealed class ContentBlueprintManager
+{
+ private readonly ICoreScopeProvider _scopeProvider;
+ private readonly IDocumentBlueprintRepository _documentBlueprintRepository;
+ private readonly ILanguageRepository _languageRepository;
+ private readonly IContentTypeRepository _contentTypeRepository;
+ private readonly IEventMessagesFactory _eventMessagesFactory;
+ private readonly IAuditService _auditService;
+ private readonly ILogger _logger;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ContentBlueprintManager(
+ ICoreScopeProvider scopeProvider,
+ IDocumentBlueprintRepository documentBlueprintRepository,
+ ILanguageRepository languageRepository,
+ IContentTypeRepository contentTypeRepository,
+ IEventMessagesFactory eventMessagesFactory,
+ IAuditService auditService,
+ ILoggerFactory loggerFactory)
+ {
+ ArgumentNullException.ThrowIfNull(scopeProvider);
+ ArgumentNullException.ThrowIfNull(documentBlueprintRepository);
+ ArgumentNullException.ThrowIfNull(languageRepository);
+ ArgumentNullException.ThrowIfNull(contentTypeRepository);
+ ArgumentNullException.ThrowIfNull(eventMessagesFactory);
+ ArgumentNullException.ThrowIfNull(auditService);
+ ArgumentNullException.ThrowIfNull(loggerFactory);
+
+ _scopeProvider = scopeProvider;
+ _documentBlueprintRepository = documentBlueprintRepository;
+ _languageRepository = languageRepository;
+ _contentTypeRepository = contentTypeRepository;
+ _eventMessagesFactory = eventMessagesFactory;
+ _auditService = auditService;
+ _logger = loggerFactory.CreateLogger();
+ }
+
+ private static readonly string?[] ArrayOfOneNullString = { null };
+
+ ///
+ /// Gets a blueprint by its integer ID.
+ ///
+ /// The blueprint ID.
+ /// The blueprint content, or null if not found.
+ public IContent? GetBlueprintById(int id)
+ {
+ using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true);
+ scope.ReadLock(Constants.Locks.ContentTree);
+
+ IContent? blueprint = _documentBlueprintRepository.Get(id);
+ if (blueprint is null)
+ {
+ return null;
+ }
+
+ blueprint.Blueprint = true;
+ return blueprint;
+ }
+
+ ///
+ /// Gets a blueprint by its GUID key.
+ ///
+ /// The blueprint GUID key.
+ /// The blueprint content, or null if not found.
+ public IContent? GetBlueprintById(Guid id)
+ {
+ using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true);
+ scope.ReadLock(Constants.Locks.ContentTree);
+
+ IContent? blueprint = _documentBlueprintRepository.Get(id);
+ if (blueprint is null)
+ {
+ return null;
+ }
+
+ blueprint.Blueprint = true;
+ return blueprint;
+ }
+
+ ///
+ /// Saves a blueprint.
+ ///
+ /// The blueprint content to save.
+ /// The user ID performing the operation.
+ [Obsolete("Use SaveBlueprint(IContent, IContent?, int) instead. Scheduled for removal in V19.")]
+ public void SaveBlueprint(IContent content, int userId = Constants.Security.SuperUserId)
+ => SaveBlueprint(content, null, userId);
+
+ ///
+ /// Saves a blueprint with optional source content reference.
+ ///
+ /// The blueprint content to save.
+ /// The source content the blueprint was created from, if any.
+ /// The user ID performing the operation.
+ public void SaveBlueprint(IContent content, IContent? createdFromContent, int userId = Constants.Security.SuperUserId)
+ {
+ ArgumentNullException.ThrowIfNull(content);
+
+ EventMessages evtMsgs = _eventMessagesFactory.Get();
+
+ content.Blueprint = true;
+
+ using ICoreScope scope = _scopeProvider.CreateCoreScope();
+ scope.WriteLock(Constants.Locks.ContentTree);
+
+ if (content.HasIdentity == false)
+ {
+ content.CreatorId = userId;
+ }
+
+ content.WriterId = userId;
+
+ _documentBlueprintRepository.Save(content);
+
+ _auditService.Add(AuditType.Save, userId, content.Id, UmbracoObjectTypes.DocumentBlueprint.GetName(), $"Saved content template: {content.Name}");
+
+ _logger.LogDebug("Saved blueprint {BlueprintId} ({BlueprintName})", content.Id, content.Name);
+
+ scope.Notifications.Publish(new ContentSavedBlueprintNotification(content, createdFromContent, evtMsgs));
+ scope.Notifications.Publish(new ContentTreeChangeNotification(content, TreeChangeTypes.RefreshNode, evtMsgs));
+
+ scope.Complete();
+ }
+
+ ///
+ /// Deletes a blueprint.
+ ///
+ /// The blueprint content to delete.
+ /// The user ID performing the operation.
+ public void DeleteBlueprint(IContent content, int userId = Constants.Security.SuperUserId)
+ {
+ ArgumentNullException.ThrowIfNull(content);
+
+ EventMessages evtMsgs = _eventMessagesFactory.Get();
+
+ using ICoreScope scope = _scopeProvider.CreateCoreScope();
+ scope.WriteLock(Constants.Locks.ContentTree);
+ _documentBlueprintRepository.Delete(content);
+
+ // Audit deletion for security traceability (v2.0: added per critical review)
+ _auditService.Add(AuditType.Delete, userId, content.Id, UmbracoObjectTypes.DocumentBlueprint.GetName(), $"Deleted content template: {content.Name}");
+
+ _logger.LogDebug("Deleted blueprint {BlueprintId} ({BlueprintName})", content.Id, content.Name);
+
+ scope.Notifications.Publish(new ContentDeletedBlueprintNotification(content, evtMsgs));
+ scope.Notifications.Publish(new ContentTreeChangeNotification(content, TreeChangeTypes.Remove, evtMsgs));
+ scope.Complete();
+ }
+
+ ///
+ /// Creates a new content item from a blueprint template.
+ ///
+ /// The blueprint to create content from.
+ /// The name for the new content.
+ /// The user ID performing the operation.
+ /// A new unsaved content item populated from the blueprint.
+ public IContent CreateContentFromBlueprint(
+ IContent blueprint,
+ string name,
+ int userId = Constants.Security.SuperUserId)
+ {
+ ArgumentNullException.ThrowIfNull(blueprint);
+ ArgumentException.ThrowIfNullOrWhiteSpace(name);
+
+ // v2.0: Use single scope for entire method (per critical review - avoids scope overhead)
+ using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true);
+
+ IContentType contentType = GetContentTypeInternal(blueprint.ContentType.Alias);
+ var content = new Content(name, -1, contentType);
+ content.Path = string.Concat(content.ParentId.ToString(), ",", content.Id);
+
+ content.CreatorId = userId;
+ content.WriterId = userId;
+
+ IEnumerable cultures = ArrayOfOneNullString;
+ if (blueprint.CultureInfos?.Count > 0)
+ {
+ cultures = blueprint.CultureInfos.Values.Select(x => x.Culture);
+ if (blueprint.CultureInfos.TryGetValue(_languageRepository.GetDefaultIsoCode(), out ContentCultureInfos defaultCulture))
+ {
+ defaultCulture.Name = name;
+ }
+ }
+
+ DateTime now = DateTime.UtcNow;
+ foreach (var culture in cultures)
+ {
+ foreach (IProperty property in blueprint.Properties)
+ {
+ var propertyCulture = property.PropertyType.VariesByCulture() ? culture : null;
+ content.SetValue(property.Alias, property.GetValue(propertyCulture), propertyCulture);
+ }
+
+ if (!string.IsNullOrEmpty(culture))
+ {
+ content.SetCultureInfo(culture, blueprint.GetCultureName(culture), now);
+ }
+ }
+
+ return content;
+ }
+
+ ///
+ /// Gets all blueprints for the specified content type IDs.
+ ///
+ /// The content type IDs to filter by (empty returns all).
+ /// Collection of blueprints.
+ public IEnumerable GetBlueprintsForContentTypes(params int[] contentTypeId)
+ {
+ using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true);
+ // v3.0: Added read lock to match GetBlueprintById pattern (per critical review)
+ scope.ReadLock(Constants.Locks.ContentTree);
+
+ IQuery query = _scopeProvider.CreateQuery();
+ if (contentTypeId.Length > 0)
+ {
+ // Need to use a List here because the expression tree cannot convert the array when used in Contains.
+ List contentTypeIdsAsList = [.. contentTypeId];
+ query.Where(x => contentTypeIdsAsList.Contains(x.ContentTypeId));
+ }
+
+ // v3.0: Materialize to array to avoid double enumeration bug (per critical review)
+ // Calling .Count() on IEnumerable then returning it would cause double database query
+ IContent[] blueprints = _documentBlueprintRepository.Get(query).Select(x =>
+ {
+ x.Blueprint = true;
+ return x;
+ }).ToArray();
+
+ // v2.0: Added debug logging for consistency with other methods (per critical review)
+ _logger.LogDebug("Retrieved {Count} blueprints for content types {ContentTypeIds}",
+ blueprints.Length, contentTypeId.Length > 0 ? string.Join(", ", contentTypeId) : "(all)");
+
+ return blueprints;
+ }
+
+ ///
+ /// Deletes all blueprints of the specified content types.
+ ///
+ /// The content type IDs whose blueprints should be deleted.
+ /// The user ID performing the operation.
+ ///
+ ///
+ /// Known Limitation: Blueprints are deleted one at a time in a loop.
+ /// If there are many blueprints (e.g., 100+), this results in N separate delete operations.
+ /// This matches the original ContentService behavior and is acceptable for Phase 7
+ /// (behavior preservation). A bulk delete optimization could be added in a future phase
+ /// if IDocumentBlueprintRepository is extended with a bulk delete method.
+ ///
+ ///
+ public void DeleteBlueprintsOfTypes(IEnumerable contentTypeIds, int userId = Constants.Security.SuperUserId)
+ {
+ ArgumentNullException.ThrowIfNull(contentTypeIds);
+
+ // v3.0: Guard against accidental deletion of all blueprints (per critical review)
+ // An empty array means "delete blueprints of no types" = do nothing (not "delete all")
+ var contentTypeIdsAsList = contentTypeIds.ToList();
+ if (contentTypeIdsAsList.Count == 0)
+ {
+ _logger.LogDebug("DeleteBlueprintsOfTypes called with empty contentTypeIds, no action taken");
+ return;
+ }
+
+ EventMessages evtMsgs = _eventMessagesFactory.Get();
+
+ using ICoreScope scope = _scopeProvider.CreateCoreScope();
+ scope.WriteLock(Constants.Locks.ContentTree);
+
+ IQuery query = _scopeProvider.CreateQuery();
+ query.Where(x => contentTypeIdsAsList.Contains(x.ContentTypeId));
+
+ IContent[]? blueprints = _documentBlueprintRepository.Get(query)?.Select(x =>
+ {
+ x.Blueprint = true;
+ return x;
+ }).ToArray();
+
+ // v2.0: Early return with scope.Complete() to ensure scope completes in all paths (per critical review)
+ if (blueprints is null || blueprints.Length == 0)
+ {
+ scope.Complete();
+ return;
+ }
+
+ foreach (IContent blueprint in blueprints)
+ {
+ _documentBlueprintRepository.Delete(blueprint);
+ }
+
+ // v2.0: Added audit logging for security traceability (per critical review)
+ _auditService.Add(AuditType.Delete, userId, -1, UmbracoObjectTypes.DocumentBlueprint.GetName(),
+ $"Deleted {blueprints.Length} content template(s) for content types: {string.Join(", ", contentTypeIdsAsList)}");
+
+ _logger.LogDebug("Deleted {Count} blueprints for content types {ContentTypeIds}",
+ blueprints.Length, string.Join(", ", contentTypeIdsAsList));
+
+ scope.Notifications.Publish(new ContentDeletedBlueprintNotification(blueprints, evtMsgs));
+ scope.Notifications.Publish(new ContentTreeChangeNotification(blueprints, TreeChangeTypes.Remove, evtMsgs));
+ scope.Complete();
+ }
+
+ ///
+ /// Deletes all blueprints of the specified content type.
+ ///
+ /// The content type ID whose blueprints should be deleted.
+ /// The user ID performing the operation.
+ public void DeleteBlueprintsOfType(int contentTypeId, int userId = Constants.Security.SuperUserId) =>
+ DeleteBlueprintsOfTypes(new[] { contentTypeId }, userId);
+
+ ///
+ /// Gets the content type by alias, throwing if not found.
+ ///
+ ///
+ /// This is an internal helper that assumes a scope is already active.
+ ///
+ private IContentType GetContentTypeInternal(string alias)
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace(alias);
+
+ IContentType? contentType = _contentTypeRepository.Get(alias);
+
+ if (contentType == null)
+ {
+ throw new InvalidOperationException($"Content type with alias '{alias}' not found.");
+ }
+
+ return contentType;
+ }
+
+ // v3.0: Removed unused GetContentType(string) method (per critical review - dead code)
+}