-
Notifications
You must be signed in to change notification settings - Fork 340
feat(docs): add blog navigation item as an alias for changelog #17469
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 3 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
5aff049
feat(docs): add blog navigation alias
cadesark 15bf885
fix(docs): validate blog navigation aliases
cadesark adf54ac
chore: simplify blog navigation normalization
cadesark 46680d1
chore(docs): sort validator test imports
cadesark d5d6e6c
fix(docs): align blog slug defaults
cadesark 49a0ee0
chore(docs): refresh generated schemas
cadesark 144a7bb
chore(docs): refresh fern schema
cadesark File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
5 changes: 5 additions & 0 deletions
5
packages/cli/cli/changes/unreleased/add-blog-navigation-item.yml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| # yaml-language-server: $schema=../../../../../fern-changes-yml.schema.json | ||
|
|
||
| - summary: | | ||
| Add a blog navigation item as an alias for changelog navigation. | ||
| type: feat |
115 changes: 115 additions & 0 deletions
115
packages/cli/configuration-loader/src/docs-yml/__test__/blogNavigationAlias.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| import { docsYml } from "@fern-api/configuration"; | ||
| import { AbsoluteFilePath } from "@fern-api/fs-utils"; | ||
| import { createMockTaskContext } from "@fern-api/task-context"; | ||
| import { describe, expect, it } from "vitest"; | ||
|
|
||
| import { parseDocsConfiguration } from "../parseDocsConfiguration.js"; | ||
|
|
||
| const FAKE_FERN_DIR = "/fern" as AbsoluteFilePath; | ||
| const FAKE_CONFIG_PATH = "/fern/docs.yml" as AbsoluteFilePath; | ||
|
|
||
| async function parseRawDocsYml(rawDocsYml: unknown): Promise<docsYml.ParsedDocsConfiguration> { | ||
| const rawDocsConfiguration = docsYml.RawSchemas.Serializer.DocsConfiguration.parseOrThrow(rawDocsYml); | ||
| return await parseDocsConfiguration({ | ||
| rawDocsConfiguration, | ||
| absolutePathToFernFolder: FAKE_FERN_DIR, | ||
| absoluteFilepathToDocsConfig: FAKE_CONFIG_PATH, | ||
| context: createMockTaskContext() | ||
| }); | ||
| } | ||
|
|
||
| describe("blog navigation alias", () => { | ||
| it("normalizes a top-level blog item to a changelog item", async () => { | ||
| const changelog = await parseRawDocsYml({ | ||
| instances: [], | ||
| navigation: [{ changelog: "blog" }] | ||
| }); | ||
| const blog = await parseRawDocsYml({ | ||
| instances: [], | ||
| navigation: [{ blog: "blog" }] | ||
| }); | ||
| if (changelog.navigation.type !== "untabbed" || blog.navigation.type !== "untabbed") { | ||
| throw new Error("Expected untabbed navigation"); | ||
| } | ||
|
|
||
| expect(changelog.navigation).toEqual({ | ||
| type: "untabbed", | ||
| items: [ | ||
| { | ||
| type: "changelog", | ||
| changelog: [], | ||
| hidden: false, | ||
| icon: undefined, | ||
| title: "Changelog", | ||
| slug: undefined, | ||
| viewers: undefined, | ||
| orphaned: undefined, | ||
| featureFlags: undefined | ||
| } | ||
| ] | ||
| }); | ||
| expect(blog.navigation).toEqual({ | ||
| ...changelog.navigation, | ||
| items: [{ ...changelog.navigation.items[0], title: "Blog" }] | ||
| }); | ||
| }); | ||
|
|
||
| it("normalizes a tab blog item to a changelog child", async () => { | ||
| const changelog = await parseRawDocsYml({ | ||
| instances: [], | ||
| tabs: { | ||
| posts: { | ||
| "display-name": "Posts", | ||
| changelog: "blog" | ||
| } | ||
| }, | ||
| navigation: [{ tab: "posts" }] | ||
| }); | ||
| const blog = await parseRawDocsYml({ | ||
| instances: [], | ||
| tabs: { | ||
| posts: { | ||
| "display-name": "Posts", | ||
| blog: "blog" | ||
| } | ||
| }, | ||
| navigation: [{ tab: "posts" }] | ||
| }); | ||
|
|
||
| expect(changelog.navigation).toEqual({ | ||
| type: "tabbed", | ||
| items: [ | ||
| { | ||
| title: "Posts", | ||
| icon: undefined, | ||
| slug: undefined, | ||
| skipUrlSlug: undefined, | ||
| hidden: undefined, | ||
| child: { | ||
| type: "changelog", | ||
| changelog: [] | ||
| }, | ||
| viewers: undefined, | ||
| orphaned: undefined, | ||
| featureFlags: undefined | ||
| } | ||
| ] | ||
| }); | ||
| expect(blog.navigation).toEqual(changelog.navigation); | ||
| }); | ||
|
|
||
| it("preserves an explicit blog title", async () => { | ||
| const parsed = await parseRawDocsYml({ | ||
| instances: [], | ||
| navigation: [{ blog: "blog", title: "Engineering Blog" }] | ||
| }); | ||
|
|
||
| if (parsed.navigation.type !== "untabbed") { | ||
| throw new Error("Expected untabbed navigation"); | ||
| } | ||
| expect(parsed.navigation.items[0]).toMatchObject({ | ||
| type: "changelog", | ||
| title: "Engineering Blog" | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| export * as DocsYmlSchemas from "./DocsYmlSchemas.js"; | ||
| export * from "./navigation.js"; | ||
| export * from "./ParsedDocsConfiguration.js"; | ||
| export * as RawSchemas from "./schemas/index.js"; | ||
| export * from "./themeEligibleFields.js"; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import * as RawSchemas from "./schemas/index.js"; | ||
|
|
||
| export function getChangelogFolderFromNavigationItem( | ||
| item: RawSchemas.NavigationItem | ||
| ): RawSchemas.ChangelogFolderRelativePath | undefined { | ||
| if ("changelog" in item) { | ||
| return item.changelog; | ||
| } | ||
| if ("blog" in item) { | ||
| return item.blog; | ||
| } | ||
| return undefined; | ||
| } | ||
|
|
||
| export function getChangelogFolderFromTabConfig( | ||
| tab: RawSchemas.TabConfig | ||
| ): RawSchemas.ChangelogFolderRelativePath | undefined { | ||
| return tab.changelog ?? tab.blog; | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 Blog entries silently get an unservable feed address and the checker never warns
A blog navigation entry with no explicit title or slug is given the name "Blog" (
title: rawConfig.title ?? "Blog"atpackages/cli/configuration-loader/src/docs-yml/parseDocsConfiguration.ts:1762) instead of the changelog default, so it lands on a web address whose RSS/Atom/JSON feed is not served and the pre-publish check still reports no problem.Impact: Users who write the recommended
- blog: ./blogget a page whose feed URLs return 404, with no warning fromfern check.Mismatch between the parsed default title and the raw-config assumption in the feed-slug rule
At build time the normalized item becomes
{ changelog: "./blog", title: "Blog" }.DocsDefinitionResolver.toChangelogNodepassesitem.titleintoChangelogNodeConverter.toChangelogNode, which computesurlSlug: opts.slug ?? kebabCase(title)(packages/cli/docs-resolver/src/ChangelogNodeConverter.ts:74). Withtitle === "Blog"the slug isblog, which is not inCHANGELOG_FEED_ALLOWED_SLUGS(packages/cli/yaml/docs-validator/src/rules/valid-changelog-slug/valid-changelog-slug.ts:19-26), so the docs middleware will not rewriteblog.rss/blog.atom/blog.jsonto the changelog handler.Meanwhile the validator walks the raw docs.yml:
collectChangelogLocationsrecordstitle: item.title(undefined for- blog: ./blog) andgetEffectiveChangelogSlugSegmentsfalls back toDEFAULT_CHANGELOG_TITLE = "Changelog"→ segmentchangelog, which is allowlisted. SoviolationsForLocationsemits nothing even though the real URL is/blog.The rule needs to apply the same
Blogdefault thatnormalizeNavigationItemapplies (e.g. passtitle: item.title ?? (isBlogItem ? "Blog" : undefined)into the location), orblogmust be added to the allowlist. Note this also contradicts the PR's stated "same slugs, same rss/atom/json feeds" behavior.Prompt for agents
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Confirmed and fixing. The default-title mismatch is real: the parser defaults an untitled blog item to
Blog, soChangelogNodeConverterderives the segmentblog, while the rule reads the raw config (notitle) and falls back toChangelog→ validates a path the site never serves. Fixing the rule to apply the sameBlogdefault for blog aliases, plus test cases for the no-title/no-slug form in both the top-level and tab shapes — the existing test passedslug: "changelog"explicitly, which is what hid it.Not adding
blog/blogs/poststoCHANGELOG_FEED_ALLOWED_SLUGShere: #17462 already does that (in sync withpatterns.tsin fern-platform). Until it lands,fern checkcorrectly flags an untitled- blog:because the feed genuinely isn't served; after it lands, the case passes cleanly.