diff --git a/src/tooling/essc/Commands/ContentTypesCommand.cs b/src/tooling/essc/Commands/ContentTypesCommand.cs
index 684ac52c3..737a1d5a1 100644
--- a/src/tooling/essc/Commands/ContentTypesCommand.cs
+++ b/src/tooling/essc/Commands/ContentTypesCommand.cs
@@ -51,6 +51,7 @@ public async Task Types(
AnsiConsole.MarkupLine("[green]Survey already complete.[/] Use [yellow]--force[/] to re-run.");
AnsiConsole.WriteLine();
DisplayResults(state);
+ ExitIfUnregistered(state);
return;
}
@@ -110,6 +111,34 @@ await AnsiConsole.Progress()
AnsiConsole.WriteLine();
DisplayResults(state);
+ ExitIfUnregistered(state);
+ }
+
+ ///
+ /// Every discovered content type must be explicitly registered as synced (),
+ /// intentionally ignored for now (), or a non-page component/taxonomy
+ /// type that will never be synced (). Fail the run so newly-added
+ /// Contentstack content types can't silently go unclassified.
+ ///
+ private static void ExitIfUnregistered(ContentTypesState state)
+ {
+ var unregistered = state.ContentTypes.Keys
+ .Where(uid => !PageContentTypes.All.Contains(uid)
+ && !PageContentTypes.Blocked.Contains(uid)
+ && !PageContentTypes.KnownNonPages.Contains(uid))
+ .OrderBy(uid => uid, StringComparer.Ordinal)
+ .ToList();
+
+ if (unregistered.Count == 0)
+ return;
+
+ AnsiConsole.WriteLine();
+ AnsiConsole.MarkupLine(
+ $"[red bold]{unregistered.Count} unregistered content type(s) found:[/] {Markup.Escape(string.Join(", ", unregistered))}");
+ AnsiConsole.MarkupLine(
+ "[red]Add each to PageContentTypes.All (to sync it), PageContentTypes.Blocked (to ignore it for now), " +
+ "or PageContentTypes.KnownNonPages (if it's a component/taxonomy type, not a page).[/]");
+ Environment.Exit(1);
}
private static void ProcessPage(ContentTypesState state, SyncResponse response)
@@ -150,6 +179,7 @@ private static void DisplayResults(ContentTypesState state)
.AddColumn("[aqua]Content Type UID[/]")
.AddColumn(new TableColumn("[aqua]Entries[/]").RightAligned())
.AddColumn(new TableColumn("[aqua]Has URL[/]").Centered())
+ .AddColumn(new TableColumn("[aqua]Status[/]").Centered())
.AddColumn("[aqua]Sample URLs[/]");
foreach (var info in groups)
@@ -161,6 +191,14 @@ private static void DisplayResults(ContentTypesState state)
_ => $"[yellow]{info.WithUrl}[/]/{info.Total}"
};
+ var statusDisplay = info.Uid switch
+ {
+ _ when PageContentTypes.Blocked.Contains(info.Uid) => "[grey]ignored[/]",
+ _ when PageContentTypes.KnownNonPages.Contains(info.Uid) => "[grey]skipped[/]",
+ _ when PageContentTypes.All.Contains(info.Uid) => "[green]synced[/]",
+ _ => "[red bold]unregistered[/]"
+ };
+
var samples = info.SampleUrls.Count > 0
? string.Join("\n", info.SampleUrls.Select(u => $"[dim]{Markup.Escape(u)}[/]"))
: "[grey]—[/]";
@@ -169,6 +207,7 @@ private static void DisplayResults(ContentTypesState state)
new Markup(Markup.Escape(info.Uid)),
new Markup($"[white]{info.Total:N0}[/]"),
new Markup(hasUrlDisplay),
+ new Markup(statusDisplay),
new Markup(samples)
);
}
diff --git a/src/tooling/essc/ContentStack/SourcingState.cs b/src/tooling/essc/ContentStack/SourcingState.cs
index 2ee0c3ae3..f8462662f 100644
--- a/src/tooling/essc/ContentStack/SourcingState.cs
+++ b/src/tooling/essc/ContentStack/SourcingState.cs
@@ -127,4 +127,117 @@ internal static class PageContentTypes
"events_overview",
"blog_archive_overview"
];
+
+ ///
+ /// Content types that exist in Contentstack but must never be synced or indexed — e.g. content
+ /// types still being authored that aren't ready to appear in search yet.
+ ///
+ public static readonly string[] Blocked =
+ [
+ "blog_landing",
+ "example_link",
+ "examples_landing",
+ "integrations_landing",
+ "labs_integration",
+ "labs_homepage",
+ "notebook",
+ "series",
+ "tutorials_landing",
+ "tutorial",
+ "tutorial_page",
+ "tutorial_chapter",
+ "blog_v3"
+ ];
+
+ ///
+ /// Content types that are never expected to represent a standalone page — reusable components,
+ /// taxonomy/tags, navigation config, redirects, etc. Unlike , these aren't
+ /// candidates for future syncing.
+ ///
+ public static readonly string[] KnownNonPages =
+ [
+ "Sub_Navigation",
+ "accordion_faq",
+ "accordion_table",
+ "alert_bar",
+ "banner",
+ "blog_categories",
+ "blog_disclaimer",
+ "boilerplate",
+ "callout",
+ "card",
+ "card_carousel",
+ "carousel",
+ "cloud_region_locations",
+ "cloud_regions_service_provider",
+ "code_reference",
+ "column_listing",
+ "contact_languages",
+ "contact_regions",
+ "contact_worldwide_offices",
+ "content_gallery",
+ "content_promos",
+ "contributors",
+ "customer_content_type",
+ "customer_industry",
+ "customer_use_case",
+ "date_field",
+ "featured_split_listing",
+ "features",
+ "footer",
+ "footer_cta",
+ "gdpr_text",
+ "hero",
+ "hero_grid",
+ "image_alternative_text",
+ "image_reference",
+ "image_video",
+ "integration_category",
+ "integration_detail",
+ "integration_solution",
+ "listing_with_sidebar",
+ "logo_bar",
+ "marketo",
+ "marketo_form_split",
+ "meetup_events",
+ "press_contact",
+ "product_names",
+ "quotes",
+ "quotes_carousel",
+ "redirects",
+ "serve_pdfs",
+ "showcase_carousel",
+ "site_navigation_reference",
+ "sitemap_management",
+ "tab_navigation_reference",
+ "table",
+ "tags_audience",
+ "tags_campaigns",
+ "tags_content_type",
+ "tags_culture",
+ "tags_demo_features",
+ "tags_demo_solutions",
+ "tags_demo_type",
+ "tags_elastic_stack",
+ "tags_event_delivery",
+ "tags_event_type",
+ "tags_industry",
+ "tags_language",
+ "tags_meta",
+ "tags_partner",
+ "tags_region",
+ "tags_role",
+ "tags_stage",
+ "tags_teams",
+ "tags_technical_level",
+ "tags_topic",
+ "tags_use_case",
+ "text_image_video",
+ "title_text_reference",
+ "translate_content",
+ "translate_content_redesign",
+ "video_reference",
+ "video_type",
+ "vidyard_reference"
+ ];
}