Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix using images in content collections #6483

Merged
merged 5 commits into from
Mar 9, 2023
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/five-coats-tie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Fix images defined in content collections schemas not working
42 changes: 42 additions & 0 deletions packages/astro/src/assets/internal.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import fs from 'node:fs';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import { AstroSettings } from '../@types/astro.js';
import { StaticBuildOptions } from '../core/build/types.js';
import { AstroError, AstroErrorData } from '../core/errors/index.js';
import { rootRelativePath } from '../core/util.js';
import { ImageService, isLocalService, LocalImageService } from './services/service.js';
import type { ImageMetadata, ImageTransform } from './types.js';
import { imageMetadata } from './utils/metadata.js';

export function isESMImportedImage(src: ImageMetadata | string): src is ImageMetadata {
return typeof src === 'object';
Expand Down Expand Up @@ -115,3 +120,40 @@ export async function generateImage(
},
};
}

export async function emitESMImage(
id: string,
watchMode: boolean,
fileEmitter: any,
settings: AstroSettings
) {
const url = pathToFileURL(id);
const meta = await imageMetadata(url);

if (!meta) {
return;
}

// Build
if (!watchMode) {
const pathname = decodeURI(url.pathname);
const filename = path.basename(pathname, path.extname(pathname) + `.${meta.format}`);

const handle = fileEmitter({
name: filename,
source: await fs.promises.readFile(url),
type: 'asset',
});

meta.src = `__ASTRO_ASSET_IMAGE__${handle}__`;
} else {
// Pass the original file information through query params so we don't have to load the file twice
url.searchParams.append('origWidth', meta.width.toString());
url.searchParams.append('origHeight', meta.height.toString());
url.searchParams.append('origFormat', meta.format);

meta.src = rootRelativePath(settings.config, url);
}

return meta;
}
35 changes: 3 additions & 32 deletions packages/astro/src/assets/vite-plugin-assets.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import MagicString from 'magic-string';
import mime from 'mime';
import fs from 'node:fs/promises';
import path from 'node:path';
import { Readable } from 'node:stream';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { fileURLToPath } from 'node:url';
import type * as vite from 'vite';
import { normalizePath } from 'vite';
import { AstroPluginOptions, ImageTransform } from '../@types/astro';
import { error } from '../core/logger/core.js';
import { joinPaths, prependForwardSlash } from '../core/path.js';
import { rootRelativePath } from '../core/util.js';
import { VIRTUAL_MODULE_ID, VIRTUAL_SERVICE_ID } from './consts.js';
import { isESMImportedImage } from './internal.js';
import { emitESMImage, isESMImportedImage } from './internal.js';
import { isLocalService } from './services/service.js';
import { copyWasmFiles } from './services/vendor/squoosh/copy-wasm.js';
import { imageMetadata } from './utils/metadata.js';
Expand Down Expand Up @@ -202,34 +200,7 @@ export default function assets({
},
async load(id) {
if (/\.(jpeg|jpg|png|tiff|webp|gif|svg)$/.test(id)) {
const url = pathToFileURL(id);
const meta = await imageMetadata(url);

if (!meta) {
return;
}

// Build
if (!this.meta.watchMode) {
const pathname = decodeURI(url.pathname);
const filename = path.basename(pathname, path.extname(pathname) + `.${meta.format}`);

const handle = this.emitFile({
name: filename,
source: await fs.readFile(url),
type: 'asset',
});

meta.src = `__ASTRO_ASSET_IMAGE__${handle}__`;
} else {
// Pass the original file information through query params so we don't have to load the file twice
url.searchParams.append('origWidth', meta.width.toString());
url.searchParams.append('origHeight', meta.height.toString());
url.searchParams.append('origFormat', meta.format);

meta.src = rootRelativePath(settings.config, url);
}

const meta = await emitESMImage(id, this.meta.watchMode, this.emitFile, settings);
return `export default ${JSON.stringify(meta)}`;
}
},
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/content/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ async function render({
};
}

export function createImage(options: { assetsDir: string }) {
export function createImage(options: { assetsDir: string; relAssetsDir: string }) {
return () => {
if (options.assetsDir === 'undefined') {
throw new Error('Enable `experimental.assets` in your Astro config to use image()');
Expand Down
37 changes: 23 additions & 14 deletions packages/astro/src/content/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import matter from 'gray-matter';
import fsMod from 'node:fs';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import type { EmitFile } from 'rollup';
import { ErrorPayload as ViteErrorPayload, normalizePath, ViteDevServer } from 'vite';
import { z } from 'zod';
import { AstroConfig, AstroSettings } from '../@types/astro.js';
import type { ImageMetadata } from '../assets/types.js';
import { emitESMImage } from '../assets/internal.js';
import { AstroError, AstroErrorData } from '../core/errors/index.js';
import { CONTENT_TYPES_FILE } from './consts.js';

Expand Down Expand Up @@ -43,21 +44,29 @@ export const msg = {
`${collection} does not have a config. We suggest adding one for type safety!`,
};

export function extractFrontmatterAssets(data: Record<string, any>): string[] {
function findAssets(potentialAssets: Record<string, any>): ImageMetadata[] {
return Object.values(potentialAssets).reduce((acc, curr) => {
if (typeof curr === 'object') {
if (curr.__astro === true) {
acc.push(curr);
} else {
acc.push(...findAssets(curr));
}
/**
* Mutate (arf) the entryData to reroute assets to their final paths
*/
export async function patchAssets(
frontmatterEntry: Record<string, any>,
watchMode: boolean,
fileEmitter: EmitFile,
astroSettings: AstroSettings
) {
for (const key of Object.keys(frontmatterEntry)) {
if (typeof frontmatterEntry[key] === 'object' && frontmatterEntry[key] !== null) {
if (frontmatterEntry[key]['__astro_asset']) {
frontmatterEntry[key] = await emitESMImage(
frontmatterEntry[key].src,
watchMode,
fileEmitter,
astroSettings
);
} else {
await patchAssets(frontmatterEntry[key], watchMode, fileEmitter, astroSettings);
}
return acc;
}, []);
}
}

return findAssets(data).map((asset) => asset.src);
}

export function getEntrySlug({
Expand Down
13 changes: 3 additions & 10 deletions packages/astro/src/content/vite-plugin-content-imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,22 @@ import type fsMod from 'node:fs';
import { extname } from 'node:path';
import { pathToFileURL } from 'url';
import type { Plugin } from 'vite';
import { normalizePath } from 'vite';
import { AstroSettings, ContentEntryType } from '../@types/astro.js';
import { AstroErrorData } from '../core/errors/errors-data.js';
import { AstroError } from '../core/errors/errors.js';
import { escapeViteEnvReferences, getFileInfo } from '../vite-plugin-utils/index.js';
import { CONTENT_FLAG } from './consts.js';
import {
ContentConfig,
extractFrontmatterAssets,
getContentEntryExts,
getContentPaths,
getEntryData,
getEntryInfo,
getEntrySlug,
getEntryType,
globalContentConfigObserver,
patchAssets,
} from './utils.js';

function isContentFlagImport(viteId: string, contentEntryExts: string[]) {
const { searchParams, pathname } = new URL(viteId, 'file://');
return searchParams.has(CONTENT_FLAG) && contentEntryExts.some((ext) => pathname.endsWith(ext));
Expand Down Expand Up @@ -106,25 +104,20 @@ export function astroContentImportPlugin({
const slug = getEntrySlug({ ...generatedInfo, unvalidatedSlug: info.slug });

const collectionConfig = contentConfig?.collections[generatedInfo.collection];
const data = collectionConfig
let data = collectionConfig
? await getEntryData(
{ ...generatedInfo, _internal, unvalidatedData: info.data },
collectionConfig
)
: info.data;

const images = extractFrontmatterAssets(data).map(
(image) => `'${image}': await import('${normalizePath(image)}'),`
);
await patchAssets(data, this.meta.watchMode, this.emitFile, settings);

const code = escapeViteEnvReferences(`
export const id = ${JSON.stringify(generatedInfo.id)};
export const collection = ${JSON.stringify(generatedInfo.collection)};
export const slug = ${JSON.stringify(slug)};
export const body = ${JSON.stringify(info.body)};
const frontmatterImages = {
${images.join('\n')}
}
export const data = ${devalue.uneval(data) /* TODO: reuse astro props serializer */};
export const _internal = {
filePath: ${JSON.stringify(_internal.filePath)},
Expand Down
44 changes: 42 additions & 2 deletions packages/astro/test/core-image.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,10 +213,34 @@ describe('astro:image', () => {
$ = cheerio.load(html);
});

it('Adds the <img> tag', () => {
it('Adds the <img> tags', () => {
let $img = $('img');
expect($img).to.have.a.lengthOf(1);
expect($img).to.have.a.lengthOf(4);
});

it('has proper source for directly used image', () => {
let $img = $('#direct-image img');
expect($img.attr('src').startsWith('/src/')).to.equal(true);
});

it('has proper attributes for optimized image through getImage', () => {
let $img = $('#optimized-image-get-image img');
expect($img.attr('src').startsWith('/_image')).to.equal(true);
expect($img.attr('width')).to.equal('207');
expect($img.attr('height')).to.equal('243');
});

it('has proper attributes for optimized image through Image component', () => {
let $img = $('#optimized-image-component img');
expect($img.attr('src').startsWith('/_image')).to.equal(true);
expect($img.attr('width')).to.equal('207');
expect($img.attr('height')).to.equal('243');
expect($img.attr('alt')).to.equal('A penguin!');
});

it('properly handles nested images', () => {
let $img = $('#nested-image img');
expect($img.attr('src').startsWith('/src/')).to.equal(true);
});
});

Expand Down Expand Up @@ -306,6 +330,22 @@ describe('astro:image', () => {
expect(data).to.be.an.instanceOf(Buffer);
});

it('output files for content collections images', async () => {
const html = await fixture.readFile('/blog/one/index.html');

const $ = cheerio.load(html);
let $img = $('img');
expect($img).to.have.a.lengthOf(2);

const srcdirect = $('#direct-image img').attr('src');
const datadirect = await fixture.readFile(srcdirect, null);
expect(datadirect).to.be.an.instanceOf(Buffer);

const srcnested = $('#nested-image img').attr('src');
const datanested = await fixture.readFile(srcnested, null);
expect(datanested).to.be.an.instanceOf(Buffer);
});

it('quality attribute produces a different file', async () => {
const html = await fixture.readFile('/quality/index.html');
const $ = cheerio.load(html);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
title: One
image: penguin2.jpg
cover:
image: penguin1.jpg
---

# A post

text here
15 changes: 15 additions & 0 deletions packages/astro/test/fixtures/core-image-ssg/src/content/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { defineCollection, image, z } from "astro:content";

const blogCollection = defineCollection({
schema: z.object({
title: z.string(),
image: image(),
cover: z.object({
image: image()
})
}),
});

export const collections = {
blog: blogCollection
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
import { getImage } from 'astro:assets';
import { getCollection } from 'astro:content';

export async function getStaticPaths() {
const blogEntries = await getCollection('blog');
return blogEntries.map(entry => ({
params: { slug: entry.slug }, props: { entry },
}));
}

const { entry } = Astro.props;
const { Content } = await entry.render();
const myImage = await getImage(entry.data.image);
---
<html>
<head>
<title>Testing</title>
</head>
<body>
<h1>Testing</h1>

<div id="direct-image">
<img src={entry.data.image.src} width={entry.data.image.width} height={entry.data.image.height} />
</div>

<div id="nested-image">
<img src={entry.data.cover.image.src} width={entry.data.cover.image.width} height={entry.data.cover.image.height} />
</div>

<Content />
</body>
</html>
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
---
title: One
image: penguin2.jpg
cover:
image: penguin1.jpg
---

# A post
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { image, defineCollection, z } from "astro:content";
import { defineCollection, image, z } from "astro:content";

const blogCollection = defineCollection({
schema: z.object({
title: z.string(),
image: image(),
cover: z.object({
image: image()
})
}),
});

Expand Down
Loading