Error Summary
- Source: grafana-logs
- Item ID: grafana-fn-content-directory-var-task-public-content-not-found-returning-empty-slug-list
- Level: WARN
- Message:
Content directory /var/task/public/content/ not found, returning empty slug list
- Hit count: 292 (recent window)
- Request example:
01KPTG6Z7AXST53NM89RZFZAYE
Emitted by src/lib/utils/md.ts:67-69 inside getPostSlugs.
Analysis
The WARN itself is benign — getPostSlugs catches ENOENT and returns [] by design. What the 292 hits reveal is that the graceful fallback is running on every request in the Netlify serverless function. Two facts combine to make this a silent SEO regression:
public/content/ is intentionally excluded from the Netlify function bundle via outputFileTracingExcludes in next.config.js:240 (to keep the bundle size down). So /var/task/public/content/ does not exist at runtime.
app/sitemap.ts is runtime-dynamic, not statically generated. It transitively depends on getAppsData() (via getAllPagesWithTranslations → getDynamicIntlPagePaths), which hits the data layer. Next.js therefore serves sitemap.xml from the lambda rather than prerendering it.
As a result, every sitemap request:
- Calls
getPostSlugs("/") at runtime → ENOENT → returns [] → WARN logged
getAllPagesWithTranslations ends up with zero MD pages (only intl paths + dynamic app paths)
- The emitted sitemap.xml is missing every content-driven page (
/about/, /defi/, /guides/*, etc.) across all 25 locales
This also explains the companion queue item grafana-fn-error-enoent-no-such-file-or-directory-scandir-x-at-async-o-next-se (100 hits) — getVideoSlugs() in src/lib/utils/videos.ts does the same readdir against public/content/videos but does not have ENOENT handling, so it throws from the same sitemap.xml runtime invocation.
Affected Files
app/sitemap.ts (runtime sitemap generator)
src/lib/i18n/translationRegistry.ts (calls getPostSlugs("/") at line 131)
src/lib/utils/md.ts (emits the WARN)
src/lib/utils/videos.ts (related ENOENT, see companion item)
next.config.js (the outputFileTracingExcludes configuration that removes public/content)
Suggested Approach
Any of the following would fix the root cause — they trade off differently:
-
Make sitemap.xml static. Split the sitemap so it does not depend on getAppsData() runtime fetches (e.g., read a build-time-serialized apps list). Add export const dynamic = 'force-static' and revalidate on a schedule. This is the highest-impact fix for SEO and removes the runtime filesystem dependency entirely.
-
Pre-compute a slug manifest at build time. Serialize getPostSlugs("/") output to e.g. src/data/generated/md-slugs.json during the build step, then read the JSON at runtime instead of scanning public/content/. Cheapest to implement; also fixes the video ENOENT with a parallel video-slugs.json.
-
Narrow the lambda exclusion. Keep public/content/**/*.md but exclude the heavier binary assets. Simplest diff but increases function bundle size and can regress cold start.
Confidence Assessment
Low. The WARN is not a bug — the handler is intentional. The remediation is an architectural decision about whether the sitemap should be static or runtime, and how to get the slug list into the lambda. Needs human review before a fix direction is chosen.
Opened automatically by the Recovery Agent.
Error Summary
Content directory /var/task/public/content/ not found, returning empty slug list01KPTG6Z7AXST53NM89RZFZAYEEmitted by
src/lib/utils/md.ts:67-69insidegetPostSlugs.Analysis
The WARN itself is benign —
getPostSlugscatchesENOENTand returns[]by design. What the 292 hits reveal is that the graceful fallback is running on every request in the Netlify serverless function. Two facts combine to make this a silent SEO regression:public/content/is intentionally excluded from the Netlify function bundle viaoutputFileTracingExcludesinnext.config.js:240(to keep the bundle size down). So/var/task/public/content/does not exist at runtime.app/sitemap.tsis runtime-dynamic, not statically generated. It transitively depends ongetAppsData()(viagetAllPagesWithTranslations→getDynamicIntlPagePaths), which hits the data layer. Next.js therefore serves sitemap.xml from the lambda rather than prerendering it.As a result, every sitemap request:
getPostSlugs("/")at runtime → ENOENT → returns[]→ WARN loggedgetAllPagesWithTranslationsends up with zero MD pages (only intl paths + dynamic app paths)/about/,/defi/,/guides/*, etc.) across all 25 localesThis also explains the companion queue item
grafana-fn-error-enoent-no-such-file-or-directory-scandir-x-at-async-o-next-se(100 hits) —getVideoSlugs()insrc/lib/utils/videos.tsdoes the samereaddiragainstpublic/content/videosbut does not have ENOENT handling, so it throws from the same sitemap.xml runtime invocation.Affected Files
app/sitemap.ts(runtime sitemap generator)src/lib/i18n/translationRegistry.ts(callsgetPostSlugs("/")at line 131)src/lib/utils/md.ts(emits the WARN)src/lib/utils/videos.ts(related ENOENT, see companion item)next.config.js(theoutputFileTracingExcludesconfiguration that removespublic/content)Suggested Approach
Any of the following would fix the root cause — they trade off differently:
Make sitemap.xml static. Split the sitemap so it does not depend on
getAppsData()runtime fetches (e.g., read a build-time-serialized apps list). Addexport const dynamic = 'force-static'andrevalidateon a schedule. This is the highest-impact fix for SEO and removes the runtime filesystem dependency entirely.Pre-compute a slug manifest at build time. Serialize
getPostSlugs("/")output to e.g.src/data/generated/md-slugs.jsonduring the build step, then read the JSON at runtime instead of scanningpublic/content/. Cheapest to implement; also fixes the video ENOENT with a parallelvideo-slugs.json.Narrow the lambda exclusion. Keep
public/content/**/*.mdbut exclude the heavier binary assets. Simplest diff but increases function bundle size and can regress cold start.Confidence Assessment
Low. The WARN is not a bug — the handler is intentional. The remediation is an architectural decision about whether the sitemap should be static or runtime, and how to get the slug list into the lambda. Needs human review before a fix direction is chosen.
Opened automatically by the Recovery Agent.