Skip to content

fix(deps): take fumadocs 16.15.17, which links "View as Markdown" under the base path - #413

Merged
mehnoorsiddiqui merged 6 commits into
devfrom
fix/portal-copy-markdown-errors
Oct 2, 2026
Merged

mehnoorsiddiqui merged 6 commits into
devfrom
fix/portal-copy-markdown-errors

Conversation

@mehnoorsiddiqui

@mehnoorsiddiqui mehnoorsiddiqui commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Fixes fuma-nama/fumadocs#3620 ("View as Markdown" link ignores the Vite base path since 16.15.13) by taking fumadocs 16.15.17, the release that carries its fix.

The bug

From fumadocs-ui 16.15.13, the page actions popover handed out its View as Markdown link exactly as given, while Copy Markdown on the same page prefixed Vite's base. With base: '/docs/', a reader on /docs/guides/intro was linked to /guides/intro.md, which resolves against the host's root and 404s; Copy Markdown fetched /docs/guides/intro.md and succeeded.

The change

fumadocs-core and fumadocs-ui go from 16.15.15 to 16.15.17. In layouts/shared/page-actions.js:

16.15.15 16.15.17
View as Markdown href: markdownUrl href: withBasePath(markdownUrl)
Copy Markdown fetch(url).then(res => res.text()) — a 404 page is copied as Markdown throws when !res.ok

Copy Markdown under portal serve

The res.ok check above is the fumadocs-ui 16.15.16 fix for fuma-nama/fumadocs#3612: an error response is no longer copied or cached as the page's Markdown.

Copy Markdown still caches a successful response for the life of the page, so under portal serve it keeps copying a page's Markdown from before an edit until the browser is refreshed. That was reported as fuma-nama/fumadocs#3628, and the maintainer closed it as expected in dev mode: the cache lasts until the next refresh, and a built portal is unaffected.

Tests

  • New test/portal-template/page-actions.test.ts. Renders the real ViewOptionsPopover, imported as src/routes/$.tsx imports it, through a Vite server built with a given base, and reads the link off the server-rendered markup. fumadocs-ui is kept out of SSR externals so import.meta.env.BASE_URL is defined, and the Radix popover is stubbed to render its content inline. The server doesn't watch the checkout or write a dependency cache, so the two cases take about 1 s in the default suite.

    This checks fumadocs-ui under a hand-built Vite config, not the portal's vite.config.ts, and through SSR, where the real popover is closed and the link only renders on the client. It pins the upstream fix. Once vite.config.ts sets base, the base-path work should verify the link in a built portal.

    Vite base expected href 16.15.17 16.15.15
    /docs/ /docs/guides/intro.md pass fails with /guides/intro.md
    / /guides/intro.md pass pass
  • test/portal-template/page-slugs.test.ts. 16.15.17 also changed the loader to decode both sides of a slug lookup (slugsKey), so a decoded splat now finds its page without slugsFromSplat. The test that pinned the old miss now asserts the hit.

Lockfile

The re-resolution moved the seven micromark-extension-gfm-* packages to micromark-util-types 2.0.2, while micromark stays on 2.0.3, so the lockfile carries both. 2.0.3 is still inside the 7-day gate until 2026-10-03; a pnpm dedupe after that merges them.

The minimumReleaseAgeExclude entries for fumadocs-core, fumadocs-ui and mdast-util-to-markdown are gone: the lockfile pins the versions taken, and a locked version doesn't hit the gate again.

Follow-up

Copy Markdown now throws on an error response, but fumadocs-ui's useCopyButton has no .catch, so a failed copy is an unhandled rejection with no feedback to the reader. Reported upstream as fuma-nama/fumadocs#3634; showing a failure state is left to a separate PR.

Checked

  • pnpm test (tsc, mocha, ESLint): 1503 passing, 44 pending.

🤖 Generated with Claude Code

…er the base path

From 16.15.13 the page actions popover handed out its "View as Markdown"
link as given, while Copy Markdown on the same page prefixed Vite's
base. A portal hosted under a path linked readers to the host's root,
where the Markdown companion is not (fuma-nama/fumadocs#3620).
16.15.17 runs the link through the same withBasePath, and Copy Markdown
now fails on an error response instead of copying the error page.

A new test renders the real popover through Vite with base '/docs/' and
'/' and reads the link off the markup; it fails on 16.15.15.

The loader now decodes both sides of a slug lookup, so a decoded splat
finds its page without slugsFromSplat; page-slugs.test.ts says so.

Part of apimatic/apimatic-io#2275.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@mehnoorsiddiqui

Copy link
Copy Markdown
Contributor Author

How to test

Both checks run on a portal served from this branch. portal serve cannot host a portal under a path yet, so the first check sets Vite's base by hand.

Setup

  1. Check out this branch, then run pnpm install and pnpm build. The preview fetches the portal artifacts, so be logged in (node bin/run.js auth login).
  2. Copy test/resources/portal-inputs/default to a scratch folder, so the preview writes nothing into the repo.

1. View as Markdown under /docs (fuma-nama/fumadocs#3620)

  1. In portal-template/vite.config.ts, add base: '/docs/', to the returned config (beside publicDir). Don't commit it.
  2. Run node bin/run.js portal serve --input <scratch folder>. It prints the preview address, http://127.0.0.1:23513/docs by default.
  3. Open http://127.0.0.1:23513/docs/guides/intro.
  4. Click Open ▾ under the title and hover View as Markdown.
    • Expected: the link is /docs/guides/intro.md, and clicking it shows the page's Markdown.
    • On 16.15.15 it is /guides/intro.md, which returns 404.
  5. Click Copy Markdown and paste: you get the same Markdown.
  6. Stop the preview and revert vite.config.ts.

test/portal-template/page-actions.test.ts checks the same link without a browser.

2. Copy Markdown on an error response (fuma-nama/fumadocs#3612)

This works with or without the base from check 1.

  1. Open any content page, for example /guides/intro, and reload it so the button's cache is empty.
  2. Copy some text of your own, such as before, so you can tell whether the clipboard changes.
  3. In DevTools → Console, make every .md request answer 404 (Chrome may ask you to type allow pasting first):
    const realFetch = window.fetch;
    window.fetch = (url, init) =>
      String(url).endsWith('.md')
        ? Promise.resolve(new Response('<html>Not Found</html>', { status: 404 }))
        : realFetch(url, init);
  4. Click Copy Markdown and paste.
    • Expected: the button never shows "Copied", the console logs Failed to fetch …: 404, and the paste is still before.
    • On 16.15.15 the paste is <html>Not Found</html>.
  5. Run window.fetch = realFetch, click Copy Markdown again and paste.
    • Expected: the page's real Markdown. The failed response was not cached.
    • On 16.15.15 it is still <html>Not Found</html> until a full reload.

After an edit under portal serve, the button still copies the old Markdown until a reload. That is fuma-nama/fumadocs#3628, which upstream closed as expected in dev mode.

@mrafnadeem-apimatic mrafnadeem-apimatic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed against .ai/instructions.md; comments inline. Please squash and merge. The second commit, fix comment, doesn't pass commitlint (type-empty, subject-empty), and squashing leaves only the PR title on dev.

Comment thread package.json
"fumadocs-mdx": "15.4.0",
"fumadocs-openapi": "11.4.1",
"fumadocs-ui": "16.15.15",
"fumadocs-ui": "16.15.17",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this bump, Copy Markdown throws when the .md fetch fails (layouts/shared/page-actions.js:26), but fumadocs-ui's useCopyButton has no .catch. A failed copy is now an unhandled rejection, and the reader sees nothing: no check mark, no error, and the clipboard keeps its old contents.

It should show Failed. Try Again. Retrying is real, because 16.15.16 stopped caching failed responses, so another click fetches again. The same catch also covers a denied clipboard write.

MarkdownCopyButton has no failure state or label to hook into, so the portal needs to own the button. npx @fumadocs/cli add ai/page-actions installs LLMCopyButton as source: catch there, show the label, and leave the button enabled. An installed copy no longer picks up upstream fixes, so it has to keep fumadocs' withBasePath prefix itself.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not done in this PR, moved to a follow-up. Reported upstream as fuma-nama/fumadocs#3634, since the missing .catch is in fumadocs' shared useCopyButton.

Comment thread pnpm-workspace.yaml Outdated
Comment on lines 6 to 11
# Quarantined until 2026-10-06, and taken early: 16.15.17 links "View as Markdown" under
# the base path (apimatic/apimatic-io#2275), on top of 16.15.15's stringifier fix for
# apimatic/apimatic-io#2260, without which no portal page may contain bold text.
- 'fumadocs-core'
- 'fumadocs-ui'
- 'mdast-util-to-markdown'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.ai/instructions.md (Adding / upgrading dependencies) says only @apimatic/* is exempt from minimumReleaseAge, and that a fresh release comes in by waiting it out or with a one-off pnpm install --ignore-minimum-release-age. These entries are bare names, so they exempt every future fumadocs-core, fumadocs-ui and mdast-util-to-markdown release, not just the ones taken here, and nothing enforces "until 2026-10-06". The comment is also three lines of release history, where the instructions allow one line of why.

The lockfile already pins 16.15.17, and a locked version doesn't hit the gate again, so the entries and their comment can go:

Suggested change
# Quarantined until 2026-10-06, and taken early: 16.15.17 links "View as Markdown" under
# the base path (apimatic/apimatic-io#2275), on top of 16.15.15's stringifier fix for
# apimatic/apimatic-io#2260, without which no portal page may contain bold text.
- 'fumadocs-core'
- 'fumadocs-ui'
- 'mdast-util-to-markdown'

If they have to stay, pin them to exact versions ('fumadocs-core@16.15.17', 'fumadocs-ui@16.15.17', 'mdast-util-to-markdown@2.1.3'), give them a one-line reason (# Taken before the 7-day gate for apimatic/apimatic-io#2260 and #2275.), and update instructions.md to match.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in dc85447. Removed the three entries and the comment; pnpm install stays up to date with the lockfile as is.

Comment thread pnpm-lock.yaml
micromark-util-sanitize-uri: 2.0.1
micromark-util-symbol: 2.0.1
micromark-util-types: 2.0.3
micromark-util-types: 2.0.2

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The re-resolution moved the seven micromark-extension-gfm-* packages to micromark-util-types 2.0.2, while micromark stays on 2.0.3, so the lockfile now carries both. 2.0.3 was published 2026-09-26 and is still inside the 7-day gate, which is why pnpm picked 2.0.2 here. Run pnpm dedupe once it clears (2026-10-03), or note in the description that the split is expected, so it doesn't surface as unexplained churn in a later PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted the split in the description. pnpm dedupe can't run until 2.0.3 clears the gate on 2026-10-03, so I'll do it then.

Comment on lines +7 to +11
/**
* Renders the page actions the content route renders, for a page whose Markdown companion is
* at `markdownUrl`. The popover is stubbed to render its content inline, as an open popover
* would, so the links can be read off the server-rendered markup.
*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.ai/instructions.md allows a comment only when the design can't carry it, and then as one line of why. The names can carry this one. Delete the JSDoc, and rename popoverStub → alwaysOpenPopover and POPOVER_STUB → ALWAYS_OPEN_POPOVER (lines 5, 29, 44, 49). That keeps the one non-obvious fact: the popover renders its content inline, so the links are in the server-rendered markup.

Suggested change
/**
* Renders the page actions the content route renders, for a page whose Markdown companion is
* at `markdownUrl`. The popover is stubbed to render its content inline, as an open popover
* would, so the links can be read off the server-rendered markup.
*/

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in dc85447.

};
}

/** The `href` of the popover's "View as Markdown" link when the site is built with `base`. */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

viewAsMarkdownHref(base, markdownUrl) already says this, and .ai/instructions.md rules out comments that restate the code.

Suggested change
/** The `href` of the popover's "View as Markdown" link when the site is built with `base`. */

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in dc85447.

Comment on lines +64 to +65
// Externalized, fumadocs-ui would be loaded by Node as is, and `import.meta.env.BASE_URL`
// -- what the page actions prefix their links with -- would never be defined.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One line is enough for this why (.ai/instructions.md):

Suggested change
// Externalized, fumadocs-ui would be loaded by Node as is, and `import.meta.env.BASE_URL`
// -- what the page actions prefix their links with -- would never be defined.
// Bundled, so Vite defines import.meta.env.BASE_URL for fumadocs-ui.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in dc85447.

server: { middlewareMode: true, hmr: false, ws: false },
// Externalized, fumadocs-ui would be loaded by Node as is, and `import.meta.env.BASE_URL`
// -- what the page actions prefix their links with -- would never be defined.
ssr: { noExternal: ['fumadocs-ui'] },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the description: this checks fumadocs-ui under a hand-built Vite config, not the portal's vite.config.ts, and through SSR, where the real popover is closed and the link only renders on the client. It works as a pin on the upstream fix. Once vite.config.ts sets base, the base-path work should verify the link in a built portal.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added to the description.

try {
const { render } = (await server.ssrLoadModule(ENTRY)) as { render: (url: string) => string };
const markup = render(markdownUrl);
const link = /<a href="([^"]*)"[^>]*>(?:(?!<\/a>).)*View as Markdown/s.exec(markup);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only matches when href is the anchor's first attribute. If fumadocs moves rel/target ahead of it, the test fails with the popover renders a "View as Markdown" link, which isn't what it guards. Match href anywhere in the tag:

Suggested change
const link = /<a href="([^"]*)"[^>]*>(?:(?!<\/a>).)*View as Markdown/s.exec(markup);
const link = /<a [^>]*href="([^"]*)"[^>]*>(?:(?!<\/a>).)*View as Markdown/s.exec(markup);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in dc85447.

Comment on lines +80 to +81
// fuma-nama/fumadocs#3620: from 16.15.13 the link was handed out as given, so a portal hosted
// under a path sent the reader to the host's root, where the Markdown companion is not.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.ai/instructions.md rules out comments that narrate a change. The description already cites fuma-nama/fumadocs#3620, and the describe/it names say what's checked.

Suggested change
// fuma-nama/fumadocs#3620: from 16.15.13 the link was handed out as given, so a portal hosted
// under a path sent the reader to the host's root, where the Markdown companion is not.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in dc85447.

Comment thread test/portal-template/page-slugs.test.ts Outdated
Comment on lines +69 to +71
// Since fumadocs-core 16.15.17 the loader decodes both sides of the lookup, so the splat
// finds the page without slugsFromSplat too.
it('is found from the splat as it arrives too', () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This narrates a version change, which .ai/instructions.md rules out. The test name can carry it:

Suggested change
// Since fumadocs-core 16.15.17 the loader decodes both sides of the lookup, so the splat
// finds the page without slugsFromSplat too.
it('is found from the splat as it arrives too', () => {
it('is found from the splat as it arrives, without slugsFromSplat', () => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in dc85447.

mehnoorsiddiqui and others added 4 commits October 2, 2026 16:33
Drop the fumadocs release-age excludes, since the lockfile already pins
the versions taken. Stop the page-actions test's Vite server from
watching the checkout and writing a dependency cache, match the link's
href in any attribute position, and trim comments the names now carry.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… dev merge

The merge renamed the 12.1.0 entry to 12.1.1 by hand, so it kept 12.1.0's
dependencies and missed @fumadocs/json-schema, and its peers still named
fumadocs-core and fumadocs-ui 16.15.15, which the lockfile no longer has.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@sonarqubecloud

sonarqubecloud Bot commented Oct 2, 2026

Copy link
Copy Markdown

@mehnoorsiddiqui
mehnoorsiddiqui merged commit 137fc7d into dev Oct 2, 2026
10 checks passed
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 2.0.0-beta.3 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug][UI] "View as Markdown" link ignores the Vite base path since 16.15.13

2 participants