withastro/astro · error · Error

Invalid route — parameter name must match /^[a-zA-Z0-9_$]+$/

Error message

Invalid route ${file} — parameter name must match /^[a-zA-Z0-9_$]+$/

What it means

The Vercel adapter's redirects/route parser (`getParts` in lib/redirects.ts) splits each path segment on dynamic brackets and requires every dynamic parameter's name to match /^(?:[\.\.\.]?[\w$]+$)/ — i.e. letters, digits, underscore, dollar only, optionally prefixed with `...` for spread params. When a bracketed segment contains anything else (dashes, spaces, unicode, stray regex fragments), the file path is rejected with this error.

Solutions

  1. Rename the file/folder so the bracketed param uses only [a-zA-Z0-9_$]: `[slug]` instead of `[post-slug]`
  2. For spread params use exactly `[...name]` (three dots then a valid identifier)
  3. For custom regex routing, keep the name before the `=` valid: `[lang=([a-z]{2})]`
  4. Remember the dash belongs in the URL value, not the param name — the param can still match kebab-case slugs at runtime

Example fix

// before: src/pages/blog/[post-slug].astro

// after: src/pages/blog/[slug].astro
// the route still matches /blog/my-post-slug
Defensive patterns

Strategy: validation

Validate before calling

const PARAM_RE = /^(?:\.\.\.)?[a-zA-Z0-9_$]+$/;
function hasValidParams(segments: string[]): boolean {
  return segments.every((s) => {
    const m = /^\[(.+)\]$/.exec(s);
    return !m || PARAM_RE.test(m[1].split('=')[0]);
  });
}

Prevention

When it happens

Trigger: Creating a page like `src/pages/[foo-bar].astro`, `src/pages/products/[itemId!].astro`, or a spread like `src/pages/[...path slug].astro`. Also triggered by custom-regex segments whose leftover name is empty or invalid after the `([^(]+)$` extraction.

Common situations: Porting URLs that used dashes (kebab-case slugs) into dynamic params, generating pages with a script that embeds punctuation in filenames, or copying Next.js-style `[...slug].tsx` patterns with slight modifications.

Related errors


AI-assisted analysis of withastro/astro@52e6c34790 (2026-08-18). Data as JSON: /api/errors/c15bf8c363cf7b90. Report an issue: GitHub.

Appendix: source

Thrown at packages/integrations/vercel/src/lib/redirects.ts:22

import type { AstroConfig, IntegrationResolvedRoute, RoutePart } from 'astro';

const pathJoin = nodePath.posix.join;

// Copied from astro/packages/astro/src/core/routing/manifest/create.ts
// Disable eslint as we're not sure how to improve this regex yet
// eslint-disable-next-line regexp/no-super-linear-backtracking
const ROUTE_DYNAMIC_SPLIT = /\[(.+?\(.+?\)|.+?)\]/;
const ROUTE_SPREAD = /^\.{3}.+$/;
function getParts(part: string, file: string) {
	const result: RoutePart[] = [];
	part.split(ROUTE_DYNAMIC_SPLIT).map((str, i) => {
		if (!str) return;
		const dynamic = i % 2 === 1;

		const [, content] = dynamic ? /([^(]+)$/.exec(str) || [null, null] : [null, str];

		if (!content || (dynamic && !/^(?:\.\.\.)?[\w$]+$/.test(content))) {
			throw new Error(`Invalid route ${file} — parameter name must match /^[a-zA-Z0-9_$]+$/`);
		}

		result.push({
			content,
			dynamic,
			spread: dynamic && ROUTE_SPREAD.test(content),
		});
	});

	return result;
}
/**
 * Convert Astro routes into Vercel path-to-regexp syntax, which are the input for getTransformedRoutes
 */
function getMatchPattern(segments: RoutePart[][]) {
	return segments
		.map((segment) => {
			return segment

View on GitHub (pinned to 52e6c34790)