Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/glob-hash-filenames.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Fixes the `glob()` loader skipping content files whose paths contain `#` or `?`
15 changes: 12 additions & 3 deletions packages/astro/src/content/loaders/glob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,20 @@ interface GlobOptions {
deferRender?: boolean;
}

/**
* Resolves a glob entry path, relative to `base`, to a file URL.
*/
function resolveEntryUrl(entry: string, base: URL): URL {
// The `./` prefix keeps a leading `name:` from being parsed as a URL scheme (#17762).
// `encodeURI` leaves `#` and `?` as-is, which would start a hash or query (#18199).
return new URL('./' + encodeURI(entry).replace(/[#?]/g, encodeURIComponent), base);
}

function generateIdDefault({ entry, base, data }: GenerateIdOptions, isLegacy?: boolean): string {
if (data.slug) {
return String(data.slug);
}
const entryURL = new URL('./' + encodeURI(entry), base);
const entryURL = resolveEntryUrl(entry, base);
if (isLegacy) {
// Legacy behavior: use ID based on path, not slug
const { id } = getContentEntryIdAndSlug({
Expand Down Expand Up @@ -132,7 +141,7 @@ export function glob(globOptions: GlobOptions & { [secretLegacyFlag]?: boolean }
logger.warn(`No entry type found for ${entry}`);
return;
}
const fileUrl = new URL('./' + encodeURI(entry), base);
const fileUrl = resolveEntryUrl(entry, base);
const contents = await fs.readFile(fileUrl, 'utf-8').catch((err) => {
logger.error(`Error reading ${entry}: ${err.message}`);
return;
Expand Down Expand Up @@ -313,7 +322,7 @@ export function glob(globOptions: GlobOptions & { [secretLegacyFlag]?: boolean }
);

function isConfigFile(file: string) {
const fileUrl = new URL('./' + encodeURI(file), baseDir);
const fileUrl = resolveEntryUrl(file, baseDir);
return configFiles.has(fileUrl.href);
}

Expand Down
63 changes: 63 additions & 0 deletions packages/astro/test/units/content-layer/glob-loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -690,4 +690,67 @@ describe('Glob Loader', () => {
assert.ok(colonEntry, 'Entry with colon in filename should be loaded');
assert.ok(colonEntry.body?.includes('colon in its filename'));
});

it('loads files whose paths contain # or ?', async () => {
const tempDir = createTempDir();
const contentDir = join(fileURLToPath(tempDir), 'src', 'content', 'blog');
mkdirSync(join(contentDir, 'f#'), { recursive: true });
writeFileSync(
join(contentDir, 'c#-basics.md'),
'---\ntitle: C# basics\n---\n\nPattern matching in C#.',
);
writeFileSync(join(contentDir, 'f#', 'intro.md'), '---\ntitle: F# intro\n---\n\nIntro to F#.');
// `?` is reserved in Windows filenames.
if (process.platform !== 'win32') {
writeFileSync(join(contentDir, 'why?.md'), '---\ntitle: Why\n---\n\nA question.');
}

const store = new MutableDataStore();
const errors: string[] = [];
const settings = createMinimalSettings(tempDir, {
contentEntryTypes: [createMarkdownEntryType()],
});
const logger = new AstroLogger({
destination: {
write: (msg: any) => {
if (msg.level === 'error') {
errors.push(msg.message);
}
return true;
},
},
level: 'info',
});

const collections = {
blog: defineCollection({
loader: glob({ pattern: '**/*.md', base: 'src/content/blog' }),
}),
};

const contentLayer = new ContentLayer({
settings,
logger,
store,
contentConfigObserver: createTestConfigObserver(collections),
});

await contentLayer.sync();

assert.deepEqual(errors, []);
const entries = store.values('blog');
const csharp = entries.find((e) => e.id === 'c-basics');
assert.ok(csharp, 'Entry with # in filename should be loaded');
assert.ok(csharp.body?.includes('Pattern matching in C#.'));
assert.ok(
entries.find((e) => e.id === 'f/intro'),
'Entry inside a directory with # should be loaded',
);
if (process.platform !== 'win32') {
assert.ok(
entries.find((e) => e.id === 'why'),
'Entry with ? in filename should be loaded',
);
}
});
});
Loading