coil-cms 0.1.1

CMS capabilities for the Coil framework.
Documentation
use super::super::core::CmsModule;
use super::super::support::{cms_live_pages_repository, default_retry_policy};
use coil_auth::Capability;
use coil_core::{
    BulkOperationDefinition, BulkOperationKind, BulkOperationScope, CapabilityContract,
    CoreServiceDependency, EventSubscription, ExtensionSlotDescriptor, ExtensionSlotKind,
    HttpSurfaceArea, HttpSurfaceContribution, IntegrationKind, IntegrationPoint, JobContract,
    JobTriggerKind, MigrationContract, ModuleBehavior, ModuleDependency, ModuleManifest,
    RouteSurface, RouteSurfaceKind, SearchDocumentKind, SearchFieldContribution, SearchFieldRole,
    SearchIndexContribution, SearchInvalidationRule, SearchInvalidationTrigger,
    SearchRebuildStrategy, SearchVisibility,
};

pub(super) fn build_manifest(module: &CmsModule) -> ModuleManifest {
    ModuleManifest::new(module.name.clone())
            .with_required_capabilities(vec![
                Capability::CmsPageRead,
                Capability::CmsPageEdit,
                Capability::CmsPagePublish,
                Capability::CmsNavigationEdit,
            ])
            .with_optional_capabilities(vec![
                Capability::AdminShellAccess,
                Capability::SeoMetadataEdit,
                Capability::I18nTranslationEdit,
                Capability::AssetRead,
                Capability::AssetReadPublic,
                Capability::AssetPublish,
                Capability::AssetReplace,
            ])
            .with_config_namespace(module.config_namespace.clone())
            .with_capability_contracts(vec![
                CapabilityContract::required(Capability::CmsPageRead, ["page"]),
                CapabilityContract::required(Capability::CmsPageEdit, ["page"]),
                CapabilityContract::required(Capability::CmsPagePublish, ["page"]),
                CapabilityContract::required(Capability::CmsNavigationEdit, ["navigation"]),
                CapabilityContract::optional(Capability::AdminShellAccess, ["admin_module"]),
                CapabilityContract::optional(Capability::SeoMetadataEdit, ["page", "navigation"]),
                CapabilityContract::optional(
                    Capability::I18nTranslationEdit,
                    ["page", "navigation"],
                ),
                CapabilityContract::optional(Capability::AssetRead, ["asset", "media"]),
                CapabilityContract::optional(Capability::AssetReadPublic, ["asset", "media"]),
                CapabilityContract::optional(Capability::AssetPublish, ["asset", "media"]),
                CapabilityContract::optional(Capability::AssetReplace, ["asset", "media"]),
            ])
            .with_module_dependencies(vec![
                ModuleDependency::optional(
                    "admin",
                    "CMS contributes editor and navigation resources into the shared admin shell when installed",
                ),
                ModuleDependency::optional(
                    "media",
                    "CMS pages can reference managed assets through the shared media library",
                ),
            ])
            .with_core_service_dependencies(vec![
                CoreServiceDependency::Auth,
                CoreServiceDependency::Data,
                CoreServiceDependency::Cache,
                CoreServiceDependency::Jobs,
                CoreServiceDependency::Storage,
                CoreServiceDependency::I18n,
                CoreServiceDependency::Seo,
                CoreServiceDependency::Template,
                CoreServiceDependency::A11y,
                CoreServiceDependency::Observability,
            ])
            .with_migrations(vec![
                MigrationContract::new(
                    "cms.pages",
                    10,
                    "Creates localized page, revision, and publication workflow tables",
                ),
                MigrationContract::new(
                    "cms.navigation",
                    20,
                    "Creates navigation trees and editorial route bindings",
                ),
                MigrationContract::new(
                    "cms.redirects",
                    30,
                    "Creates redirect rules and route handoff metadata",
                ),
            ])
            .with_route_surfaces(vec![
                RouteSurface::new("cms.page", RouteSurfaceKind::FrontendPage, "/pages/{slug}")
                    .localized(),
                RouteSurface::new(
                    "cms.preview",
                    RouteSurfaceKind::Fragment,
                    "/admin/pages/preview",
                )
                .gated_by(Capability::CmsPageRead),
                RouteSurface::new("cms.pages.index", RouteSurfaceKind::AdminPage, "/admin/pages")
                    .gated_by(Capability::CmsPageRead),
                RouteSurface::new(
                    "cms.navigation.index",
                    RouteSurfaceKind::AdminPage,
                    "/admin/navigation",
                )
                .gated_by(Capability::CmsNavigationEdit),
                RouteSurface::new(
                    "cms.redirects.index",
                    RouteSurfaceKind::AdminPage,
                    "/admin/redirects",
                )
                .gated_by(Capability::CmsPageEdit),
                RouteSurface::new(
                    "cms.pages.save-draft",
                    RouteSurfaceKind::AdminAction,
                    "/admin/pages/draft",
                )
                .gated_by(Capability::CmsPageEdit),
                RouteSurface::new(
                    "cms.pages.publish",
                    RouteSurfaceKind::AdminAction,
                    "/admin/pages/publish",
                )
                .gated_by(Capability::CmsPagePublish),
                RouteSurface::new(
                    "cms.pages.unpublish",
                    RouteSurfaceKind::AdminAction,
                    "/admin/pages/unpublish",
                )
                .gated_by(Capability::CmsPagePublish),
                RouteSurface::new(
                    "cms.navigation.save",
                    RouteSurfaceKind::AdminAction,
                    "/admin/navigation/save",
                )
                .gated_by(Capability::CmsNavigationEdit),
                RouteSurface::new(
                    "cms.redirects.save",
                    RouteSurfaceKind::AdminAction,
                    "/admin/redirects/save",
                )
                .gated_by(Capability::CmsPageEdit),
            ])
            .with_jobs(vec![
                JobContract::new(
                    "cms.publish-scheduled",
                    JobTriggerKind::Scheduled,
                    true,
                    "Promotes scheduled revisions into the live site at their publish window",
                ),
                JobContract::new(
                    "cms.cache.invalidate",
                    JobTriggerKind::DomainEvent,
                    true,
                    "Invalidates navigation, sitemap, and page caches after editorial changes",
                ),
            ])
            .with_event_subscriptions(vec![
                EventSubscription::new(
                    "cms.page.publish-requested",
                    Some("cms.publish-scheduled"),
                    "Schedules future publication work for editorial workflows",
                ),
                EventSubscription::new(
                    "media.asset.published",
                    Some("cms.cache.invalidate"),
                    "Refreshes page fragments that depend on newly published managed assets",
                ),
            ])
            .with_integration_points(vec![
                IntegrationPoint::new(
                    IntegrationKind::AdminNavigation,
                    "admin.content",
                    "Adds page, navigation, and redirect resources to the shared content section",
                ),
                IntegrationPoint::new(
                    IntegrationKind::FrontendRendering,
                    "page.render",
                    "Owns page composition on top of the shared HTML-first template engine",
                ),
                IntegrationPoint::new(
                    IntegrationKind::SeoMetadata,
                    "page.head",
                    "Emits localized canonical metadata, robots directives, and sitemap entries",
                ),
                IntegrationPoint::new(
                    IntegrationKind::JsonLd,
                    "page.schema",
                    "Supplies structured data fragments for page and navigation surfaces",
                ),
                IntegrationPoint::new(
                    IntegrationKind::CacheInvalidation,
                    "page.publish",
                    "Invalidates route, navigation, sitemap, and metadata fragments when publication changes",
                ),
            ])
            .with_behaviors(vec![
                ModuleBehavior::CacheInvalidation,
                ModuleBehavior::LocalizedContent,
                ModuleBehavior::SeoMetadata,
                ModuleBehavior::JsonLd,
                ModuleBehavior::AccessibleAdminUi,
                ModuleBehavior::AsyncJobs,
                ModuleBehavior::AuthGovernedPublication,
            ])
            .with_extension_slots(vec![
                ExtensionSlotDescriptor::new(
                    ExtensionSlotKind::AdminWidget,
                    "cms.page.editor.sidebar",
                    "Allows customer app widgets to augment the page editor without owning the editor runtime",
                ),
                ExtensionSlotDescriptor::new(
                    ExtensionSlotKind::RenderHook,
                    "cms.page.render",
                    "Allows bounded content embellishments during page rendering",
                ),
            ])
            .with_admin_resources(module.admin_resources.clone())
            .with_search_contributions(vec![SearchIndexContribution::new(
                "search.cms.pages",
                SearchDocumentKind::Page,
                SearchVisibility::Public,
                true,
                vec![
                    SearchFieldContribution::new(
                        "title",
                        "title",
                        SearchFieldRole::Title,
                        true,
                        true,
                    ),
                    SearchFieldContribution::new(
                        "body",
                        "body_html",
                        SearchFieldRole::Body,
                        false,
                        true,
                    ),
                    SearchFieldContribution::new(
                        "seo",
                        "seo",
                        SearchFieldRole::Metadata,
                        true,
                        false,
                    ),
                ],
                vec![
                    SearchInvalidationRule::new(
                        SearchInvalidationTrigger::Published,
                        "page published",
                    ),
                    SearchInvalidationRule::new(
                        SearchInvalidationTrigger::Updated,
                        "page updated",
                    ),
                    SearchInvalidationRule::new(
                        SearchInvalidationTrigger::Unpublished,
                        "page unpublished",
                    ),
                ],
                SearchRebuildStrategy::OnInvalidate,
            )])
            .with_bulk_operations(vec![
                BulkOperationDefinition::new(
                    "bulk.cms.publish",
                    "Bulk publish pages",
                    Some("Publishes editorially approved pages through idempotent background work".to_string()),
                    Capability::CmsPagePublish,
                    BulkOperationKind::Publish,
                    BulkOperationScope::Cms,
                    default_retry_policy(),
                    Some(500),
                    true,
                ),
                BulkOperationDefinition::new(
                    "bulk.cms.unpublish",
                    "Bulk unpublish pages",
                    Some("Withdraws published pages without requiring per-row request-time mutations".to_string()),
                    Capability::CmsPagePublish,
                    BulkOperationKind::Unpublish,
                    BulkOperationScope::Cms,
                    default_retry_policy(),
                    Some(500),
                    true,
                ),
            ])
            .with_data_repositories(vec![cms_live_pages_repository()])
            .with_http_surfaces(vec![
                HttpSurfaceContribution::page(
                    "cms.page",
                    HttpSurfaceArea::Public,
                    "/pages/{slug}",
                    "cms/page",
                )
                .localized(),
                HttpSurfaceContribution::fragment(
                    "cms.preview",
                    "/admin/pages/preview",
                    "cms/preview",
                    "preview-pane",
                )
                .gated_by(Capability::CmsPageRead),
                HttpSurfaceContribution::page(
                    "cms.pages.index",
                    HttpSurfaceArea::Admin,
                    "/admin/pages",
                    "cms/pages",
                )
                .gated_by(Capability::CmsPageRead),
                HttpSurfaceContribution::page(
                    "cms.navigation.index",
                    HttpSurfaceArea::Admin,
                    "/admin/navigation",
                    "cms/navigation",
                )
                .gated_by(Capability::CmsNavigationEdit),
                HttpSurfaceContribution::page(
                    "cms.redirects.index",
                    HttpSurfaceArea::Admin,
                    "/admin/redirects",
                    "cms/redirects",
                )
                .gated_by(Capability::CmsPageEdit),
                HttpSurfaceContribution::redirect(
                    "cms.pages.save-draft",
                    coil_core::HttpSurfaceMethod::Post,
                    HttpSurfaceArea::Admin,
                    "/admin/pages/draft",
                    "/admin/pages",
                    303,
                )
                .gated_by(Capability::CmsPageEdit),
                HttpSurfaceContribution::redirect(
                    "cms.pages.publish",
                    coil_core::HttpSurfaceMethod::Post,
                    HttpSurfaceArea::Admin,
                    "/admin/pages/publish",
                    "/admin/pages",
                    303,
                )
                .gated_by(Capability::CmsPagePublish),
                HttpSurfaceContribution::redirect(
                    "cms.pages.unpublish",
                    coil_core::HttpSurfaceMethod::Post,
                    HttpSurfaceArea::Admin,
                    "/admin/pages/unpublish",
                    "/admin/pages",
                    303,
                )
                .gated_by(Capability::CmsPagePublish),
                HttpSurfaceContribution::redirect(
                    "cms.navigation.save",
                    coil_core::HttpSurfaceMethod::Post,
                    HttpSurfaceArea::Admin,
                    "/admin/navigation/save",
                    "/admin/navigation",
                    303,
                )
                .gated_by(Capability::CmsNavigationEdit),
                HttpSurfaceContribution::redirect(
                    "cms.redirects.save",
                    coil_core::HttpSurfaceMethod::Post,
                    HttpSurfaceArea::Admin,
                    "/admin/redirects/save",
                    "/admin/redirects",
                    303,
                )
                .gated_by(Capability::CmsPageEdit),
            ])
}