diff --git a/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift b/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift index 993e8fb315..911163c327 100644 --- a/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift +++ b/Sources/SwiftDocC/LinkTargets/LinkDestinationSummary.swift @@ -1017,7 +1017,18 @@ private extension DocumentationNode { // specialized articles, like sample code pages, that benefit from being treated as articles in // some parts of the compilation process (like curation) but not others (like link destination // summary creation and render node translation). - return metadata?.pageKind?.kind.documentationNodeKind ?? kind + let baseKind = metadata?.pageKind?.kind.documentationNodeKind ?? kind + + // For articles, check if they should be treated as API Collections (collectionGroup). + // This ensures that linkable entities have the same kind detection logic as the rendering system, + // fixing cross-framework references where API Collections were incorrectly showing as articles. + if baseKind == .article, + let article = semantic as? Article, + DocumentationContentRenderer.roleForArticle(article, nodeKind: kind) == .collectionGroup { + return .collectionGroup + } + + return baseKind } } diff --git a/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift b/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift index 5ac4655d92..43bb6f0739 100644 --- a/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift +++ b/Tests/SwiftDocCTests/LinkTargets/LinkDestinationSummaryTests.swift @@ -829,4 +829,94 @@ class LinkDestinationSummaryTests: XCTestCase { "doc://com.example.mymodule/documentation/MyModule/MyClass/myFunc()-9a7po", ]) } + + /// Tests that API Collections (articles with Topics sections) are correctly identified as `.collectionGroup` + /// kind in linkable entities, ensuring cross-framework references display the correct icon. + func testAPICollectionKindForLinkDestinationSummary() async throws { + let symbolGraph = makeSymbolGraph( + moduleName: "TestModule", + symbols: [makeSymbol(id: "test-class", kind: .class, pathComponents: ["TestClass"])] + ) + + let catalogHierarchy = Folder(name: "unit-test.docc", content: [ + TextFile(name: "APICollection.md", utf8Content: """ + # API Collection + + This is an API Collection that curates symbols. + + ## Topics + + - ``TestModule/TestClass`` + """), + JSONFile(name: "TestModule.symbols.json", content: symbolGraph) + ]) + + let (_, context) = try await loadBundle(catalog: catalogHierarchy) + let converter = DocumentationNodeConverter(context: context) + + let apiCollectionReference = ResolvedTopicReference( + bundleID: context.inputs.id, + path: "/documentation/unit-test/APICollection", + sourceLanguage: .swift + ) + let node = try context.entity(with: apiCollectionReference) + let renderNode = converter.convert(node) + + let summaries = node.externallyLinkableElementSummaries(context: context, renderNode: renderNode) + let pageSummary = try XCTUnwrap(summaries.first) + + XCTAssertEqual(pageSummary.kind, .collectionGroup) + XCTAssertEqual(pageSummary.title, "API Collection") + XCTAssertEqual(pageSummary.abstract, [.text("This is an API Collection that curates symbols.")]) + + // Verify round-trip encoding preserves the correct kind + try assertRoundTripCoding(pageSummary) + } + + /// Tests that explicit `@PageKind(article)` metadata overrides API Collection detection, + /// ensuring that explicit page kind directives take precedence over automatic detection. + func testExplicitPageKindOverridesAPICollectionDetection() async throws { + let symbolGraph = makeSymbolGraph( + moduleName: "TestModule", + symbols: [makeSymbol(id: "test-class", kind: .class, pathComponents: ["TestClass"])] + ) + + let catalogHierarchy = Folder(name: "unit-test.docc", content: [ + TextFile(name: "ExplicitArticle.md", utf8Content: """ + # Explicit Article + + This looks like an API Collection but is explicitly marked as an article. + + @Metadata { + @PageKind(article) + } + + ## Topics + + - ``TestModule/TestClass`` + """), + JSONFile(name: "TestModule.symbols.json", content: symbolGraph) + ]) + + let (_, context) = try await loadBundle(catalog: catalogHierarchy) + let converter = DocumentationNodeConverter(context: context) + + let explicitArticleReference = ResolvedTopicReference( + bundleID: context.inputs.id, + path: "/documentation/unit-test/ExplicitArticle", + sourceLanguage: .swift + ) + let node = try context.entity(with: explicitArticleReference) + let renderNode = converter.convert(node) + + let summaries = node.externallyLinkableElementSummaries(context: context, renderNode: renderNode) + let pageSummary = try XCTUnwrap(summaries.first) + + // Should be .article because of explicit @PageKind(article), not .collectionGroup + XCTAssertEqual(pageSummary.kind, .article) + XCTAssertEqual(pageSummary.title, "Explicit Article") + + // Verify round-trip encoding preserves the correct kind + try assertRoundTripCoding(pageSummary) + } }