fix(content): guard afterParse hook to prevent silent HMR failures#577
fix(content): guard afterParse hook to prevent silent HMR failures#577
Conversation
Nuxt Content's dev file watcher has no error handling around the content:file:afterParse hook. If the hook throws, the entire HMR update silently fails (no broadcast, no WebSocket message to client). Wrap the hook body in try/catch so errors are logged instead of breaking hot reload. Also add a null guard on ctx.collection.fields to prevent TypeError when the fields object is unavailable.
There was a problem hiding this comment.
Pull request overview
Defensively hardens the Nuxt Content v3 integration so the content:file:afterParse hook cannot break the dev hot-reload pipeline when sitemap metadata processing encounters unexpected input.
Changes:
- Wraps the
content:file:afterParsehook body intry/catchto prevent thrown errors from aborting downstream Nuxt Content HMR handling. - Adds a null/undefined guard for
ctx.collection.fieldsbefore using theinoperator.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/module.ts
Outdated
| ctx.content.sitemap = defu(typeof content.sitemap === 'object' ? content.sitemap : {}, defaults) as Partial<SitemapUrl> | ||
| } | ||
| catch (e) { | ||
| logger.warn('Failed to process sitemap data for content file, skipping.', e) |
There was a problem hiding this comment.
The catch block logs a generic warning without identifying which content file/collection caused the failure. Including ctx.collection.name and ctx.content.path (or similar identifiers) in the log message/metadata would make these failures actionable when multiple files are being parsed in dev mode.
| logger.warn('Failed to process sitemap data for content file, skipping.', e) | |
| logger.warn(`Failed to process sitemap data for content file (collection: ${ctx.collection?.name}, path: ${ctx.content?.path}), skipping.`, e) |
| nuxt.hooks.hook('content:file:afterParse' as any, (ctx: FileAfterParseHook) => { | ||
| const content = ctx.content as any as { | ||
| body: { value: [string, Record<string, any>][] } | ||
| sitemap?: Partial<SitemapUrl> | false | ||
| path: string | ||
| updatedAt?: string | ||
| } & Record<string, any> | ||
| nuxtV3Collections.add(ctx.collection.name) | ||
| // ignore .dot files and paths | ||
| if (String(ctx.content.path).includes('/.')) { | ||
| ctx.content.sitemap = null | ||
| return | ||
| } | ||
| if (!('sitemap' in ctx.collection.fields)) { | ||
| ctx.content.sitemap = null | ||
| return | ||
| } | ||
| // support sitemap: false | ||
| if (typeof content.sitemap !== 'undefined' && !content.sitemap) { | ||
| ctx.content.sitemap = null | ||
| return | ||
| } | ||
| if (ctx.content.robots === false) { | ||
| ctx.content.sitemap = null | ||
| return | ||
| } | ||
| // add any top level images | ||
| const images: SitemapUrl['images'] = [] | ||
| if (config.discoverImages) { | ||
| images.push(...(content.body?.value | ||
| ?.filter(c => | ||
| ['image', 'img', 'nuxtimg', 'nuxt-img'].includes(c[0]), | ||
| try { | ||
| const content = ctx.content as any as { | ||
| body: { value: [string, Record<string, any>][] } | ||
| sitemap?: Partial<SitemapUrl> | false | ||
| path: string | ||
| updatedAt?: string | ||
| } & Record<string, any> | ||
| nuxtV3Collections.add(ctx.collection.name) | ||
| // ignore .dot files and paths | ||
| if (String(ctx.content.path).includes('/.')) { | ||
| ctx.content.sitemap = null | ||
| return | ||
| } | ||
| if (!ctx.collection.fields || !('sitemap' in ctx.collection.fields)) { | ||
| ctx.content.sitemap = null | ||
| return |
There was a problem hiding this comment.
This change is a regression guard for Nuxt Content v3 parsing/HMR failures, but there’s no automated coverage ensuring the hook stays non-throwing when ctx.collection.fields is missing (or when the hook body throws). Since the repo already has e2e coverage for content-v3, it would be good to add a small fixture/test that exercises a collection with fields undefined and asserts sitemap endpoints still respond (and no unhandled error is thrown).
🔗 Linked issue
Related to harlan-zw/nuxt-seo#498
❓ Type of change
📚 Description
Nuxt Content's dev file watcher has no error handling around the
content:file:afterParsehook. If the hook throws, thebroadcast()call never executes and the client never receives the HMR update, so the user sees "file modified" in logs but the page never refreshes.This wraps the hook body in try/catch (logging instead of throwing) and adds a null guard on
ctx.collection.fieldsbefore theinoperator check.The primary fix for #498 is #576 (
defineSitemapSchema()), which avoids the overlapping collection source issue entirely. This PR is defensive hardening so the hook can never break HMR regardless of the cause.