From 92e81ea247434379e6e098633d8ae375dc513d62 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Tue, 7 Jul 2026 18:18:34 +0200 Subject: [PATCH] Classify new Contentstack content types in essc discovery Register newly-added Contentstack content types (blog_landing, tutorial, notebook, etc.) as intentionally blocked, and non-page component/taxonomy types (accordion_faq, tags_*, hero, etc.) as known non-pages. The `contentstack types` command now shows a per-type status and fails when it finds a type that isn't classified in any of the three registries. Co-Authored-By: Claude Sonnet 5 --- .../essc/Commands/ContentTypesCommand.cs | 39 ++++++ .../essc/ContentStack/SourcingState.cs | 113 ++++++++++++++++++ 2 files changed, 152 insertions(+) diff --git a/src/tooling/essc/Commands/ContentTypesCommand.cs b/src/tooling/essc/Commands/ContentTypesCommand.cs index 684ac52c3b..737a1d5a16 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 2ee0c3ae38..f8462662fc 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" + ]; }