-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'next' into feat/routes-resolved
- Loading branch information
Showing
37 changed files
with
2,213 additions
and
685 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
--- | ||
'astro': minor | ||
--- | ||
|
||
Adds experimental support for built-in SVG components. | ||
|
||
|
||
This feature allows you to import SVG files directly into your Astro project as components. By default, Astro will inline the SVG content into your HTML output. | ||
|
||
To enable this feature, set `experimental.svg` to `true` in your Astro config: | ||
|
||
```js | ||
{ | ||
experimental: { | ||
svg: true, | ||
}, | ||
} | ||
``` | ||
|
||
To use this feature, import an SVG file in your Astro project, passing any common SVG attributes to the imported component. Astro also provides a `size` attribute to set equal `height` and `width` properties: | ||
|
||
```astro | ||
--- | ||
import Logo from './path/to/svg/file.svg'; | ||
--- | ||
<Logo size={24} /> | ||
``` | ||
|
||
For a complete overview, and to give feedback on this experimental API, see the [Feature RFC](https://github.com/withastro/roadmap/pull/1035). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,90 @@ | ||
--- | ||
'astro': patch | ||
'astro': minor | ||
--- | ||
|
||
Adds experimental reponsive image support | ||
Adds experimental support for automatic responsive images | ||
|
||
This feature is experimental and may change in future versions. To enable it, set `experimental.responsiveImages` to `true` in your `astro.config.mjs` file. | ||
|
||
```js title=astro.config.mjs | ||
{ | ||
experimental: { | ||
responsiveImages: true, | ||
}, | ||
} | ||
``` | ||
|
||
When this flag is enabled, you can pass a `layout` prop to any `<Image />` or `<Picture />` component to create a responsive image. When a layout is set, images have automatically generated `srcset` and `sizes` attributes based on the image's dimensions and the layout type. Images with `responsive` and `full-width` layouts will have styles applied to ensure they resize according to their container. | ||
|
||
```astro | ||
--- | ||
import { Image, Picture } from 'astro:assets'; | ||
import myImage from '../assets/my_image.png'; | ||
--- | ||
<Image src={myImage} alt="A description of my image." layout='responsive' width={800} height={600} /> | ||
<Picture src={myImage} alt="A description of my image." layout='full-width' formats={['avif', 'webp', 'jpeg']} /> | ||
``` | ||
This `<Image />` component will generate the following HTML output: | ||
```html title=Output | ||
|
||
<img | ||
src="/_astro/my_image.hash3.webp" | ||
srcset="/_astro/my_image.hash1.webp 640w, | ||
/_astro/my_image.hash2.webp 750w, | ||
/_astro/my_image.hash3.webp 800w, | ||
/_astro/my_image.hash4.webp 828w, | ||
/_astro/my_image.hash5.webp 1080w, | ||
/_astro/my_image.hash6.webp 1280w, | ||
/_astro/my_image.hash7.webp 1600w" | ||
alt="A description of my image" | ||
sizes="(min-width: 800px) 800px, 100vw" | ||
loading="lazy" | ||
decoding="async" | ||
fetchpriority="auto" | ||
width="800" | ||
height="600" | ||
style="--w: 800; --h: 600; --fit: cover; --pos: center;" | ||
data-astro-image="responsive" | ||
> | ||
``` | ||
|
||
#### Responsive image properties | ||
|
||
These are additional properties available to the `<Image />` and `<Picture />` components when responsive images are enabled: | ||
|
||
- `layout`: The layout type for the image. Can be `responsive`, `fixed`, `full-width` or `none`. Defaults to value of `image.experimentalLayout`. | ||
- `fit`: Defines how the image should be cropped if the aspect ratio is changed. Values match those of CSS `object-fit`. Defaults to `cover`, or the value of `image.experimentalObjectFit` if set. | ||
- `position`: Defines the position of the image crop if the aspect ratio is changed. Values match those of CSS `object-position`. Defaults to `center`, or the value of `image.experimentalObjectPosition` if set. | ||
- `priority`: If set, eagerly loads the image. Otherwise images will be lazy-loaded. Use this for your largest above-the-fold image. Defaults to `false`. | ||
|
||
#### Default responsive image settings | ||
|
||
You can enable responsive images for all `<Image />` and `<Picture />` components by setting `image.experimentalLayout` with a default value. This can be overridden by the `layout` prop on each component. | ||
|
||
**Example:** | ||
```js title=astro.config.mjs | ||
{ | ||
image: { | ||
// Used for all `<Image />` and `<Picture />` components unless overridden | ||
experimentalLayout: 'responsive', | ||
}, | ||
experimental: { | ||
responsiveImages: true, | ||
}, | ||
} | ||
``` | ||
|
||
```astro | ||
--- | ||
import { Image } from 'astro:assets'; | ||
import myImage from '../assets/my_image.png'; | ||
--- | ||
<Image src={myImage} alt="This will use responsive layout" width={800} height={600} /> | ||
<Image src={myImage} alt="This will use full-width layout" layout="full-width" /> | ||
<Image src={myImage} alt="This will disable responsive images" layout="none" /> | ||
``` | ||
|
||
For a complete overview, and to give feedback on this experimental API, see the [Responsive Images RFC](https://github.com/withastro/roadmap/blob/responsive-images/proposals/0053-responsive-images.md). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import { | ||
createComponent, | ||
render, | ||
spreadAttributes, | ||
unescapeHTML, | ||
} from '../runtime/server/index.js'; | ||
import type { SSRResult } from '../types/public/index.js'; | ||
import type { ImageMetadata } from './types.js'; | ||
|
||
export interface SvgComponentProps { | ||
meta: ImageMetadata; | ||
attributes: Record<string, string>; | ||
children: string; | ||
} | ||
|
||
/** | ||
* Make sure these IDs are kept on the module-level so they're incremented on a per-page basis | ||
*/ | ||
const ids = new WeakMap<SSRResult, number>(); | ||
let counter = 0; | ||
|
||
export function createSvgComponent({ meta, attributes, children }: SvgComponentProps) { | ||
const rendered = new WeakSet<Response>(); | ||
const Component = createComponent((result, props) => { | ||
let id; | ||
if (ids.has(result)) { | ||
id = ids.get(result)!; | ||
} else { | ||
counter += 1; | ||
ids.set(result, counter); | ||
id = counter; | ||
} | ||
id = `a:${id}`; | ||
|
||
const { | ||
title: titleProp, | ||
viewBox, | ||
mode, | ||
...normalizedProps | ||
} = normalizeProps(attributes, props); | ||
const title = titleProp ? unescapeHTML(`<title>${titleProp}</title>`) : ''; | ||
|
||
if (mode === 'sprite') { | ||
// On the first render, include the symbol definition | ||
let symbol: any = ''; | ||
if (!rendered.has(result.response)) { | ||
// We only need the viewBox on the symbol definition, we can drop it everywhere else | ||
symbol = unescapeHTML(`<symbol${spreadAttributes({ viewBox, id })}>${children}</symbol>`); | ||
rendered.add(result.response); | ||
} | ||
|
||
return render`<svg${spreadAttributes(normalizedProps)}>${title}${symbol}<use href="#${id}" /></svg>`; | ||
} | ||
|
||
// Default to inline mode | ||
return render`<svg${spreadAttributes({ viewBox, ...normalizedProps })}>${title}${unescapeHTML(children)}</svg>`; | ||
}); | ||
|
||
if (import.meta.env.DEV) { | ||
// Prevent revealing that this is a component | ||
makeNonEnumerable(Component); | ||
|
||
// Maintaining the current `console.log` output for SVG imports | ||
Object.defineProperty(Component, Symbol.for('nodejs.util.inspect.custom'), { | ||
value: (_: any, opts: any, inspect: any) => inspect(meta, opts), | ||
}); | ||
} | ||
|
||
// Attaching the metadata to the component to maintain current functionality | ||
return Object.assign(Component, meta); | ||
} | ||
|
||
type SvgAttributes = Record<string, any>; | ||
|
||
/** | ||
* Some attributes required for `image/svg+xml` are irrelevant when inlined in a `text/html` document. We can save a few bytes by dropping them. | ||
*/ | ||
const ATTRS_TO_DROP = ['xmlns', 'xmlns:xlink', 'version']; | ||
const DEFAULT_ATTRS: SvgAttributes = { role: 'img' }; | ||
|
||
export function dropAttributes(attributes: SvgAttributes) { | ||
for (const attr of ATTRS_TO_DROP) { | ||
delete attributes[attr]; | ||
} | ||
|
||
return attributes; | ||
} | ||
|
||
function normalizeProps(attributes: SvgAttributes, { size, ...props }: SvgAttributes) { | ||
if (size !== undefined && props.width === undefined && props.height === undefined) { | ||
props.height = size; | ||
props.width = size; | ||
} | ||
|
||
return dropAttributes({ ...DEFAULT_ATTRS, ...attributes, ...props }); | ||
} | ||
|
||
function makeNonEnumerable(object: Record<string, any>) { | ||
for (const property in object) { | ||
Object.defineProperty(object, property, { enumerable: false }); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { parse, renderSync } from 'ultrahtml'; | ||
import type { ImageMetadata } from '../types.js'; | ||
import type { SvgComponentProps } from '../runtime.js'; | ||
import { dropAttributes } from '../runtime.js'; | ||
|
||
function parseSvg(contents: string) { | ||
const root = parse(contents); | ||
const [{ attributes, children }] = root.children; | ||
const body = renderSync({ ...root, children }); | ||
|
||
return { attributes, body }; | ||
} | ||
|
||
export type SvgRenderMode = 'inline' | 'sprite'; | ||
|
||
export function makeSvgComponent(meta: ImageMetadata, contents: Buffer | string, options?: { mode?: SvgRenderMode }) { | ||
const file = typeof contents === 'string' ? contents : contents.toString('utf-8'); | ||
const { attributes, body: children } = parseSvg(file); | ||
const props: SvgComponentProps = { | ||
meta, | ||
attributes: dropAttributes({ mode: options?.mode, ...attributes }), | ||
children, | ||
}; | ||
|
||
return `import { createSvgComponent } from 'astro/assets/runtime'; | ||
export default createSvgComponent(${JSON.stringify(props)})`; | ||
} |
Oops, something went wrong.