withastro / roadmap Goto Github PK
View Code? Open in Web Editor NEWIdeas, suggestions, and formal RFC proposals for the Astro project.
Ideas, suggestions, and formal RFC proposals for the Astro project.
A brief, one or two sentence explanation of the proposal.
Add an API to integrations which allows them to define middleware so that users do not need to go through the boilerplate of creating a middleware file and importing the functionality.
When we built the middleware API we anticipated a need for integrations to hook into it, but we decided not to make that a goal for v1 due to the complexity of ordering. Yet the need keeps coming up for the same reasons as before, integrations want to transparently add middleware.
Astro v4.5.9
Node v18.17.1
System Linux (x64)
Package Manager npm
Output static
Adapter none
Integrations none
No response
Astro seems to support images co-location in content collections only when Markdown syntax is used. When using HTML syntax (ie. an img
tag), the image is not processed and result in a 404:
![a working image](../assets/image.png)
, the image is processed and visible in the browser.<img alt="a failing image" src="../assets/image.png" />
, the image seems to be ignored. I only see the alt text and the console shows a 404
.I think the issue comes from those lines in packages/markdown/remark/src/remark-collect-images.ts
:
visit(tree, ['image', 'imageReference'], (node: Image | ImageReference) => {
if (node.type === 'image') {
if (shouldOptimizeImage(node.url)) imagePaths.add(node.url);
}
if (node.type === 'imageReference') {
const imageDefinition = definition(node.identifier);
if (imageDefinition) {
if (shouldOptimizeImage(imageDefinition.url)) imagePaths.add(imageDefinition.url);
}
}
});
When using HTML syntax, the node type is html
so the image is not collected by Astro. I guess we need to parse the HTML with a regex to collect the src
attribute for all images encountered in the node.value
.
In addition to the Stackblitz link below, here are the few steps to reproduce the bug:
npm create astro@latest -- --template minimal
(answer Yes
to all and Strict
to Typescript)src/content
: mkdir -p src/content/{assets,blog}
src/content/assets
touch src/content/blog/first-post.md
![a working image](../assets/image.png)
<img alt="a failing image" src="../assets/image.png" />
src/pages/index.astro
to load our post:---
import { getEntry } from "astro:content";
const entry = await getEntry("blog", "first-post");
const { Content } = await entry.render();
---
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>Astro</title>
</head>
<body>
<h1>{entry.data.title}</h1>
<Content />
</body>
</html>
npm run dev
404
in the terminal).I use a Git submodule to separate the content
directory from the website code so I need to colocate the images with Markdown files. In addition, sometimes I want to wrap the images in a figure
to add a figcaption
so I need to write HTML inside the Markdown file.
I should be able to use both ![alt text](./path/to/an/img.jpg)
and <img alt="alt text" src="./path/to/an/img.jpg" />
to render an image in a Markdown file.
https://stackblitz.com/edit/astro-markdown-images?file=src%2Fcontent%2Fblog%2Ffirst-post.md
Introduce a middleware to Astro, where you can define code that runs on every request. This API should work regardless of the rendering mode (SSG or SSR) or adapter used. Also introduces a simple way to share request-specific data between proposed middleware, API routes, and .astro
routes.
Middleware has been one of the most heavily requested feature in Astro. It's useful for handling common tasks like auth guards and setting cache headers. For me, it would make handling authentication much easier.
This proposal is heavily inspired by SvelteKit's handle
hooks;
Middlewares can be defined in src/middleware.ts
by exporting middleware
(array):
export cost middleware = []
A simple middleware looks like so:
export const middleware: Middleware = async (context: APIContext, resolve: Resolve) => {
const session = await getSession(context.request);
const isProtectedRoute = matchRoute(context.url);
if (!session && isProtectedRoute) {
// early response
return new Response(null, {
status: 400,
});
}
context.cookies.set("random-cookie", "some-value");
const response = await resolve(context); // resolve api route or render page
return response;
};
context
is the same one provided to API route handlers. Most of Astro's request handling process will be behind resolve()
, and the response object returned from middleware
will be sent to the user's browser.
sequence
can be imported to run multiple middlewares in sequence.
export const middleware: Middleware = sequence(
async (context, resolve) => {
console.log("1a");
const response = await resolve(event);
console.log("1b");
return response;
},
async (context, resolve) => {
console.log("2a");
const response = await resolve(event);
console.log("2b");
return response;
}
);
The log result for this example is:
1a
2a
2b
1b
locals
This proposal also adds locals
property to APIContext
and AsroGlobal
. This locals
object will be forwarded across the request handling process, allowing for data to be shared between middlewares, API routes, and .astro
pages. This is useful for storing request specific data, such as user data, across the rendering step.
export const middleware = async (context: APIContext, resolve: Resolve) => {
context.session = await getSession(context.request);
const response = await resolve(context);
return response;
};
---
// pages/index.astro
const session = Astro.locals.session;
---
The value type of locals
can be anything as it won't be JSON-stringified:
---
// pages/index.astro
const session = await Astro.locals.getSession();
---
locals
can be typed inside src/env.d.ts
(I think it's possible?):
// src/env.d.ts
// at least one of these should be possible
type Locals {}
declare namespace App {
type Locals = {}
}
The middleware function will run on each route's pre-rendering step, as if it were handling a normal SSR request.
export const middleware = async (context: APIContext, resolve: Resolve) => {
const response = await resolve(context); // pre-render step here
return response;
};
It would be useful if Astro could support redirects for static builds. Possibly for server builds, too?
Sometimes users want to rename a route or refactor how pages are organized. It's not possible to just rename a page as there may be links pointing to it, so redirects have to be generated. While this is possible to do manually, it's not very convenient.
Redirects for static
and server
mode are fairly straight-forward to implement. Other tools like Jekyll expose this ability, and hosting providers do as well.
It would be helpful if Astro could generate these automatically as a convenience. This feature could also normalize the implementation across hosting providers and output mode.
static
modeserver
mode to avoid host-specific redirect configuration?None at this time.
API TBD, but this could either be a built-in redirect
configuration
import { defineConfig } from 'astro/config';
export default defineConfig({
redirects: {
'/foo/bar': '/unicorn/rainbow'
}
})
or exposed as an official integration
import { defineConfig } from 'astro/config';
import redirects from '@astrojs/redirects';
export default defineConfig({
integrations: [
redirects({
'/foo/bar': '/unicorn/rainbow'
})
]
})
Let's support importing and using .svg files as Astro components.
Users have reported that .svg files are currently difficult to use—this is a problem we should provide an optimized solution for. Working with .svgs doesn’t need to be difficult or bloat your client-side JavaScript.
The Astro team has discussed and experimented with an "icon" primitive since before Astro v1.0 shipped. Now that we have shipped 3.0 with full astro:assets support, I firmly believe all of the pieces are in place to finally make this happen.
.svg
files as if they were an Astro component<svg>
element while overriding the existing props<svg>
on a page can hurt browser parsing performance. We can automatically reduce the number of nodes by using <symbol>
and <use>
.<svg>
often have xmlns
and xmlns:xlink
and version
attributes. These are not needed in HTML5 and we can automatically drop them to save a few bytes..svg
import remains unchanged.svg
components in frameworks without inlining them into the client side JavaScript
.svg
icons is an anti-pattern. It’s a terrible idea that bloats your JavaScript and creates all sorts of headaches for HMR and clientside performance in production.<Icon>
component for each supported framework, we’ll make it easy for users to avoid this anti-pattern. It’s so common now because there aren’t many other good solutions.@iconify/json
supportunplugin-icons
support? Making this as seamless as possible would be awesome but supporting local .svg
files is a good place to start.Allow pages to be marked as being partials, preventing the doctype tag and any head injection from taking place. A partial is defined as a template which belongs in the context of another page and is dependent on that page in order to be fully functional.
Partials are a technique that has been used by web applications for decades, popularized in frameworks such as Ruby on Rails. Frontend oriented JavaScript frameworks have typically not used partials, but instead use JSON APIs and front-end templating in order to dynamically change parts of the page. Nevertheless, partials have remained a niche feature, and with Astro's backend focus we have had interest in support for a long time.
Recently the popularity of projects like htmx and Unpoly have revived interest in this technique. Since Astro treats each page request as a request for a full page it automatically attaches the doctype and head elements, making it difficult to simply inject into the page.
.html
files in a static build.innerHTML
.
pages
folder).Provide the infrastructure to protect Astro websites from CSRF attacks
Most background is available here: https://owasp.org/www-community/attacks/csrf
Astro should provide some level of security to users.
✅ I am using the latest version of Astro and all plugins.
✅ I am using a version of Node that Astro supports (v18.17.1 or v20.3.0 or higher.)
Astro v4.11.1
Node v20.12.0
System macOS (arm64)
Package Manager bun
Output hybrid
Adapter @astrojs/vercel/serverless
Integrations @astrojs/starlight
@astrojs/react
@astrojs/tailwind
@astrojs/mdx
N/A
Once processed by Astro, Images in Content Collection have the following form.
type ImageMetaData = {
src: string;
width: number;
height: number;
format: "png" | "jpg" | "jpeg" | "tiff" | "webp" | "gif" | "svg" | "avif";
}
Which is perfect for when images are rendered in the app itself.
In my application, I'm using the @vercel/og
package to generate open graph social media images for pages of the site using Astro Endpoints. The issue is that if I try to render a normal <img/>
tag and use the image.src
I get the following error from @vercel/og
Image source must be an absolute URL: /@fs/Users/.../src/content/.../index.png?origWidth=1260&origHeight=1000&origFormat=png
I know this is a very specific issue with Vercel OG but since Astro doesn't provide a baked in way of generating such images, I was wondering if it would be possible to include the raw and absolute URL of the source image in the ImageMetaData so that it could be used in such scenarios.
I included a Stackblitz below where the /og.png
Endpoint works if you comment out the <img/>
tag but crashes if you don't.
https://stackblitz.com/edit/github-vyt8ws?file=src%2Fpages%2F_renderOgImage.tsx
Originally posted by disrae June 4, 2024
The Astro config does not have access to the slug, so the sitemap generator can only access one domain, there is no data to make the domain dynamic with.
For repos with two domains it would be nice pass a function to the generator which is applied to the url strings as they are generated.
For repos that implement two or more domains based on locale (my case), I need to be able to generate a sitemap that has the correct domain for each slug. For every slug that has '/en-us' in the string I want to switch out the domain.
A lightweight, non-breaking change to give more custom control in the sitemap generation process.
I would simply want to add it in the options object for the sitemap generator.
Programmatic control over routing.
As astro grows in popularity, it is being used for more and more use cases. One of these is multitenant websites where a tenant may be given a subdomain. This proposal introduces a powerful primitive that allows astro users to build websites with complex routing requirements. Rather than a limitation, file-based routing is now a sensible default!
functionPerRoute
Provide an option to specify a CDN URL for static assets to serve from in production.
Large traffic sites often have CDN servers that are optimized to serve assets only. For example, a site served from https://astro.build
would reference all it's assets from https://cdn.astro.build
.
A CDN URL would also allow assets from multiple sites to be deployed to the same CDN server to share the performance, where they can be separated by URL subpaths.
There are also prior art from other frameworks that users want in Astro too:
import.meta.env.*
) to prepend links manually.astro:assets
images, and @astrojs/image
.<link href>
or <img src>
etc.public
directory.
Astro config:
// astro.config.js
export default defineConfig({
build: {
cdnUrl: 'https://cdn.example.com'
}
})
Generated HTML example:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://cdn.example.com/index.123456.css" />
</head>
<body>
<!-- if astro images are used -->
<img src="https://cdn.example.com/penguin.png" />
</body>
</html>
Details and behaviours:
build.assets
option of _astro
directory, the user will copy all the files from the dist/_astro
directory to the https://cdn.example.com
server.base
option, because base
is the serving base of the user-facing site URL, e.g. https://example.com/foo/
. (similar to prior art)https://cdn.example.com/project-a
, and Astro would include it in the built URL.Framework | Option | What users have to do | Public files supported? |
---|---|---|---|
Nextjs | assetPrefix |
Upload .next/static/ as https://cdn.com/_next/static/ |
❌ |
Nuxt | app.cdnURL |
Upload .output/public/_nuxt/ as https://cdn.com/_nuxt/ |
❌ |
SvelteKit | path.assets |
Upload .svelte-kit/output/client/_app/ as https://cdn.com/_app/ |
❌ |
*Upload path may depend on deployment target / adapter
Incremental Build support in Astro aims to significantly speed up repeated runs of astro build
. Initial builds will populate some kind of cache, which will allow subsequent builds to bypass Rollup for unchanged trees of the module graph.
The original incremental build support proposal is one of our oldest and most highly upvoted issues. Unfortunately, users have not shared many concrete examples in that thread. However, from the extensive performance profiling we've done, we know that Rollup is the main bottleneck in Astro's build process.
Now that our rendering, Markdown, and MDX performance has been optimized about as far as we can take it, now is the time to explore new options. The best option we have is to move as much of our build process out of Rollup as possible, likely with some sort of build cache. That is the goal of introducing incremental builds.
Why now? The reason is straightforward—caching is notoriously difficult to get right. We did not want to take on the additional complexity until our API surface was stable and our internal build process was easier to reason about. Thanks to significant effort from @bluwy and @ematipico in the past few months, we're now in a good place to tackle this.
astro build
performanceastro build
as possiblestatic
, hybrid
, and server
outputsContent Collections are a key primitive that brings people to Astro. Content Collections make it easy to work with local content (MD, MDX, Markdoc, etc) inside of your Astro project. They give you structure (src/content/[collection-name]/*
), schema validation for frontmatter, and querying APIs.
// A folder full of Markdown (MDX) files
defineCollection({
name: 'blog',
data: glob('./content/blog/*.mdx'),
});
// A single file containing an array of objects
defineCollection({
name: 'authors',
data: file('./content/authors.json'),
});
// Remote data, loaded with a custom npm package
defineCollection({
name: 'articles',
data: storyblokLoader({startsWith: 'articles/posts'}),
});
// Custom data, loaded from anywhere you'd like
defineCollection({
name: 'my-custom-collection',
data: () => { /* ... */ },
});
Astro is a server-first framework. As such, it has always relied on the Multi-Page Application (MPA) architecture, meaning that navigations result in a full page refresh and server round-trip.
This proposal offers an opt-in Client-side Routing (CSR) experience, typically associated with Single-Page Application (SPA) architectures.
Note:
For a great overview of relevant terms (MPA/SPA/PEMPA/PESPA), see The Web’s Next Transition by Kent C. Dodds.
As users build increasingly complex experiences with Astro, UI Persistence has surfaced as a clear missing piece in the full Astro story. Client-side routing is currently the only way to achieve UI persistence across navigations.
MPAs are the backbone of the web and remain a natural default for Astro projects. SPAs come with their own set of incredibly complex considerations and drawbacks—importantly, we do not believe that client-side routing is a "solved problem" despite becoming the default approach in the industry.
However, the team has discussed a mythical "SPA mode" since the earliest days of the project. Now that we are extremely confident in Astro's mental model and internal priorities, there is no better time to begin layering in new primitives to round out Astro's use cases.
🔵 points are not finalized, should be discussed and resolved during prototyping
output
modes<head>
elements (title
, link
, script
, etc) should be swapped automaticallynavigation
APIAdd a --watch
flag to astro check
.
In development mode there isn't a way to know if your .astro
files contain no errors, aside from editor feedback. If you use astro check
you will know about errors, but might not see them until CI unless you remember to run the command before pushing.
.astro
files during development workflow.astro check --watch
astro
are you using?2.5.5
no
pnpm
Windows
Chrome
I have tried it like this:
astro.config.js
:
import {defineConfig} from 'astro/config'
export default defineConfig({
integrations: [
{
name: ''
, hooks: {
'astro:server:setup': ({server: {middlewares}}) => {
middlewares.use((request, response, next) => {
response.locals = 'test'//or response.locals.test = 'test'
next()
})
}
}
}
]
})
But in the static file endpoint I am always getting an empty locals
object ({}
):
((src/pages/
)index
).js
:
export const get = async ({locals}) => {
console.log(locals)
return {body: ''}
}
https://stackblitz.com/edit/github-z6jvzs?file=astro.config.js,src%2Fpages%2Findex.js
Provide an API for integrations to implement custom client: directives to provide greater control for when client-side JS is loaded and executed.
The last client directive added to core was the client:only
directive in August 2021. Since that time the core team has been hesitant to add new client directives despite the community asking about them.
Allowing custom client directives would both:
Some examples of custom directives that people have wanted in the past:
client:visible
currently does.client:
loading to get rid of the precompile step (this is a core repo refactor / improvement).client:
.client:only
load vs idle vs visible problem.To use a custom directive in your app, add an integration like any other.
import { defineConfig } from 'astro/config';
import onClickDirective from '@matthewp/astro-click-directive';
export default defineConfig({
integrations: [onClickDirective()]
});
And then use it in your Astro components:
---
import Counter from '../components/Counter.jsx';
---
<Counter client:click />
An implementer of a custom client directive would do so through the hooks API:
export default function() {
return {
hooks: {
'astro:config:setup': ({ addClientDirective }) => {
addClientDirective({
name: '@matthewp/astro-click-directive',
key: 'click',
entrypoint: '@matthewp/astro-click-directive/client.js'
});
},
}
}
}
The entrypoint being:
export default function(load, opts, element) {
element.addEventListener('click', async () => {
const hydrate = await load();
await hydrate();
}, { once: true });
}
Make an enhancement to Astro's Hybrid Rendering support to make pre-rendering the default for all pages, and opt-out instead of opt-in.
Currently, pre-rendering pages in SSR is possible thanks to Hybrid Rendering. However, this uses output: 'server'
which defaults all pages to server-rendered. This can be annoying for mostly-static sites that then need to add export const prerender = true;
to most pages.
One common use-case (mentioned in the original pre-rendering RFC) is a fully static site with a few server-side api routes. The current implementation requires a migrating user to add output: 'server'
to their configuration and then manually add export const prerender = true
to each .astro
page in their src/pages
directory before continuing.
This was originally discussed in the pre-rendering RFC. However it was ultimately removed because it was assumed that this would require a new third internal implementation (static
and server
(SSR) already exist today) which was considered too much of a maintenance burden to take on.
export const prerender = true
output
mode in the user-facing configuration API is okay, as long as it leverages the existing static
or server
internal build implementation.feat/container
branch for an exploration of implementation.Astro components are tightly coupled to astro
(the metaframework). This proposal introduces a possible server-side API for rendering .astro
files in isolation, outside of the full astro
build pipeline.
Some of our own proposals, as well as many third-party tool integrations, are blocked until we expose a proper server-side rendering API for .astro
components. Other frameworks have tools like preact-render-to-string
or react-dom/server
that make rendering components on the server straight-forward.
We've avoided doing this because... it's very hard! Part of the appeal of .astro
components is that they have immediate access to a number of powerful APIs, context about your site, and a rich set of framework renderers. In the full astro
build process, we are able to wire up all this context invisibly. Wiring up the context manually as a third-party is prohibitively complex.
.astro
components in isolationastro
parity, but abstract away internal APIs.astro
files on the server.astro
component output.mdx
compiledContent/html output?@astrojs/*
) if possibleimport { AstroContainer } from 'astro/container';
import Component from './components/Component.astro';
const astro = new AstroContainer();
console.log(await astro.renderToString(Component, { props, slots }))
The container can be optionally constructed with some settings that are typically reserved for Astro configuration.
const astro = new AstroContainer({
mode?: 'development' | 'production';
site?: string;
renderers?: Renderer[]
});
The second argument to renderToString
optionally provides render-specific data that may be exposed through the Astro global, like props
, slots
, request
, and/or params
.
await astro.renderToString(Component, { props, slots, request, params })
Allow styles in Astro files to override global styles.
A common pattern in an application is to have some global styles in a global style file like so:
global.css
input {
border-width: 1px;
}
And then to use them in an Astro component.
---
import '../global.css';
---
<style>
input {
border-width: 2px;
}
</style>
<input type="text">
The intent is to override the styles on this page (or component). But due to chunking of shared dependencies, we cannot guarantee that CSS ordering will be correct.
And due to the use of :where()
for scoping, scoped styles can't win in specificity to apply the overriden styles. (as :where
preserves specifity, unlike Vue or Svelte that adds attributes/classes to increase specificity) (See original RFC for details)
We need a solution for this.
Below are possible solutions:
@layer
: https://developer.mozilla.org/en-US/docs/Web/CSS/@layer. However, this doesn't have better browser support than :where
.astro.config
?), and order them first to have lower priority.@scope
: https://www.w3.org/TR/css-scoping-1/Provide a configuration option to specify inlining behavior of styles authored or imported in astro modules.
There has been a constant interest in inlining styles while still taking advantage of scoping and other processing steps since before 1.0 (see: withastro/astro#918), with many users incorrectly assuming that is:inline
directive is the solution (see: withastro/astro#6388).
Simple one-page websites do not benefit from an external stylesheet, since there is no other page that could use the cached stylesheet. On the other hand, large websites are overoptimized for cacheability, since our chunking splits the styles too granularly. Case in point, Astro docs homepage has 20 linked stylesheets, 14 of them are less than 1kb (see: withastro/astro#6528).
So far we have not provided a way to allow inlining stylesheets, prompting workarounds. However, coming from other frameworks, users might expect to be able to configure this behavior.
server
and static
outputs.astro
are you using?2.6.4
Node
pnpm, npm
Mac
Chrome
When I try to test parallel rendering of React components with promises it seems to block the entire document, until the promises resolve.
await new Promise((res) => setTimeout(res, 1800));
const ReactComponent = () => {
return (
<h1>{1800}ms Delayed</h1>
);
};
export default ReactComponent;
Parallel rendring works as intended when I use Astro components. If I'm doing something wrong, please point me in the right direction.
https://stackblitz.com/edit/github-wsphny?file=src%2Fpages%2Findex.astro
When building for production, we should safely collapse whitespace when generating the HTML
Astro users are often confused why we don't minify HTML by default.
The long answer is:
server
modeThe shorter answer is:
Probably just a boolean config option. Name TBD.
Astro v4.2.4
Node v20.11.0
System macOS (arm64)
Package Manager pnpm
Output hybrid
Adapter @astrojs/vercel/serverless
Integrations auto-import
@astrojs/mdx
astro-icon
@astrojs/partytown
@astrojs/sitemap
astro-robots-txt
No response
Right now, <style define:vars={{}}>
adds the variable definitions to every single element in the component. This drastically increases the footprint of components, it's difficult to read, and it will cause issues if a user intentionally wants to override a local variable within the component itself.
The CSS var definitions should only be added to the top-level components.
This issue was closed before it was discussed in withastro/astro#7328.
https://stackblitz.com/edit/github-sdeict-dc8tm6?file=src%2Fcomponents%2FDefineVars.astro
astro:assets
shipped successfully in 3.0. Now that it's available for everyone to enjoy, we'd like to start extending it with new features. One of the most requested feature coming from 3.0 was a Picture component, to allow supporting multiple format and sizes.
Images on the web are tricky 😅
When it comes to performance, it's best for pages to load the correct size and image format based on the browser's support and window size. Multiple sizes can be handled by <img srcset="">
, but only the <picture>
element has support for defining multiple image formats like .jpg
, .webp
, and .avif
for the browser to select from. Ideally, supporting both is the most optimal thing to do, but a Picture component would be a start.
<Picture>
component in astro:assets
that supports building multiple and formats and sizes_image
API route used to optimize images in SSR pages
None of the implementation below are finals, they're merely inspirations to get the discussion going.
@astrojs/image
's <Picture>
@astrojs/image
included a <Picture>
component that may be a decent starting point. It had a few shortcomings (didn't apply width
and height
as expected, couldn't set attributes on the underlying img
etc.) but nothing we wouldn't be able to solve in a more complete version.
---
import { Picture } from "@astrojs/image/components"
---
<Picture widths={[450, 900, 1800]} formats={["avif", "webp", "jpg"]} />
Essentially an easier to use normal picture
element. Every source
element could inherit props from the Picture
component (somehow), so you'd be able to set, ex: a src
for Picture that would work for every Source
that does not have its own src
---
import {Picture, Source} from "astro:assets";
import myImage from "../something.png";
---
<Picture src={myImage}>
<Source format="avif" width={500} />
<Source />
<Picture>
"Would love to add a stylesheet feature to the sitemap integration, similar to the styling features available for the RSS feed. Being able to pass in a custom XSL stylesheet with maybe a sensible default would be tremendous!"
Originally posted by @johneatmon in #146 (comment)
Sara: What he said!
I'm currently manually copying and pasting to make this happen, but that means the sitemap I will link to won't update on every build.
This RFC aims to improve DX (and eventually security) around env variables.
Env variables are an important part of any web application. You need to store sensitive data (think API secrets, tokens etc) without being leaked inside your git repo. But that's only the 1st part of the story. It's easy to leak this data by importing in the wrong place, eg. the frontend like Resend a few weeks ago.
Other JS frameworks (eg. SvelteKit) are handling env pretty well. From my understanding, the env story is currently a bit tricky in Astro. According to the docs, here is how env variables are currently handled:
import.meta.env
import.meta.env
includes some default variables like SSR
, BASE_URL
...PUBLIC_
import.meta.env
on the client side will be undefined
(value will be accessible server side).env
(or .env.production
, .env.development
) and CLIprocess.env
, or following the used runtime (eg. Deno.env.get()
for the deno adapter)process.env
has any protection against client-side usage (like import.meta.env
), but I guess it doesn'tAstro v4.12.2
Node v20.15.1
System macOS (arm64)
Package Manager unknown
Output static
Adapter none
Integrations astro-icon
@astrojs/preact
@astrojs/mdx
@astrojs/sitemap
No response
<Code>
doesn't support passing a meta string to Shiki's highlighter, breaking the functionality of multiple shiki transformers.
<Code>
should support passing a meta string to Shiki's highlighter and not break the functionality of multiple shiki transformers.
It's not super clear which CSS-in-JS libraries work, so I'm creating this issue as sort of a place to start the conversation and document the current status.
Here's a few popular libraries that I know about (will keep this list updated):
Library | Status | Notes |
---|---|---|
styled-components | 🟡 Partially works | Prod build errors with: styled.div is not a function . Can be worked around with client:only or by using buildSsrCjsExternalHeuristics and ssr.noExternal (will cause FOUC). |
emotion | 🟡 Partially works | Prod build errors with: styled.div is not a function . Can be worked around with client:only or by using conditional default import (will cause FOUC). Can also patch @astrojs/react . |
linaria | 🟡 Partially works | Prod build errors with: Named export 'styled' not found . Can be worked around using buildSsrCjsExternalHeuristics and ssr.noExternal or by downgrading to v3. |
stitches | 🟡 Partially works | <style> tag for SSR needs to be in React component |
typestyle | ✅ Works | - |
vanilla-extract | ✅ Works | - |
solid-styled | 🟡 Partially works | Causes FOUC |
styled-jsx | ❌ Doesn't work | No vite or rollup plugin, requires babel |
compiled | ❌ Doesn't work | No vite or rollup plugin, requires babel |
From what I understand, if a library doesn't work in astro, it's because of one or more of these reasons*:
*I could be wrong so would be nice if someone can verify the above points.
Additionally, here's what @FredKSchott would like to see (quote message from discord):
- A table of which CSS-in-JS libraries are/aren't supported in Astro. If not supported, it would also list the reason. If supported, these could also have tests in the repo so that we don't break them in the future.
- Some time to go through the ones that aren't supported and try to fix any issues that are fixable by Astro.
- For any the css-in-JS libraries that still aren't supported after that, a clear error message inside of Astro core when it sees you try to import the package.
https://github.com/mayank99/astro-css-in-js-tests
This proposal aims to outline a plan for a core story for images in Astro, doing so by:
src/content
)src/assets
folderUsing images in Astro is currently a bit confusing. Should my images go in public
or src
? How do I refer to them?
For this, we'd like to introduce a src/assets
folder. This folder would be used for your source assets, as in, your not optimized, raw assets. Usage of this folder would be recommended, but not forced.
To make it easier to use this folder from anywhere in the project, an alias will be provided so it is possible to write ~/assets
. Due to necessitating tsconfig.json
changes, this will only affect new projects, and is completely optional.
It is fairly common for one of the property of a Markdown piece of content to need to be a reference to an asset (think, cover image for an article, picture for an author etc).
In tandem with the src/assets
folder, we'd like to introduce a way for users to specify that a specific property needs to refer to a valid asset from the src/assets
folder. This would allow us to provide validation of the asset, think cover needs to be a png of size 240x240
, including validating that the file actually exists.
src/assets
is supported and they would be optimized, like before.src/assets
is also supported (ex: ![...](./image.png)
) in addition to the src/assets
folder (ex: ![...](~/assets/image.png)
)public
folder can still be referred to, however, they won't be optimized / transformed. We would like for this folder to be kept for its original purpose, for assets that will be copied as-is.src/assets
folder. Remote images are currently out of scope and still need to be referred to using a z.string
.src
and they'll be optimized.![]
syntax and refer to images in src/assets
or relative to the file and they'll be optimized.src/assets
.(see #447 for an earlier version of this proposal)
The current Image
component can be confusing to use at time. Why do I need to set an aspectRatio
? (what even is the aspect ratio of my image?) Why is my image getting cropped? What format should I use? What does quality
means?
We'd like to make it so less properties are needed in general. In most cases, the width
and height
would be automatically inferred from the source file and not passing them would just result in the same dimensions, but a smaller weight.
For cases where they're needed, we'd like to remove the aspectRatio
property, instead preferring that users manually set both width
and height
. Combined with [[#Better behaviour for resizing images]], we believe that this will lead to a more consistant and easier to resonate about experience.
format
would be set to webp
by default, removing the need for it to be passed completely. We believe WebP to be a sensible default for most images, providing with a smaller file size while not sacrificing on compatibility and support for transparency.
For quality
, we'd like to make it easier for people to use without needing to know the in-and-out of the loader's algorithm for quality. We propose to fix this by introducing different presets that users can choose from, for example: quality: 'low'
would automatically set an appropriate, approximative quality that is considered 'low' (specific implementation is left to the loaders to decide.)
Despite the tremendous power it offered, users were often confused by how @astrojs/image
would crop their images to fit the desired aspect ratio. This was especially confusing for users coming from other popular frameworks that don't offer this feature.
As such, this feature would be removed of the base toolkit. Resizing would now only operate in ways that follow the original image aspect ratio (based on width
). To control how an image is fitted inside its container, the object-fit
and object-position
CSS properties can be used.
For users used to other frameworks, this is a similar behaviour to the one offered by NextJS's next/image
and Eleventy's eleventy-img
.
src
. Remote images (ex: http://example.com/image.png
, /image.png
or ${import.meta.env.BASE_URL}/image.png
) would however require width
and height
to be set manually.src
can be a dynamic import, but be aware that it must respect Vite's limitations on dynamic importsformat
would be set to webp
by default, with the possibility to override as needed. If you desire to have the same format as the original, for remote images it needs to be manually provided whereas for local images, it is possible to do format={myImage.format}
.quality
now accepts two types of values, either a preset (low | mid | high | max
) or a specific number (0 - 100
).alt
is a required property in all cases. It can be explicitly set to ""
, for cases where an alt-text is not required.loading="lazy"
and decoding="async"
, with possibility to override as needed.src
, width
, height
, quality
, and all the properties natively available on the img
tag (alt
, loading
, decoding
, style
etc)Currently, importing images in Astro returns a simple string
with the path of the image. The @astrojs/image
integration enhance this by instead returning the following shape: {src: string, width: number, height: number, format: string}
, allowing users to easily construct img
tags with no CLS:
---
import image from "../my_image.png"
---
<img src={image.src} width={image.width} height={image.height} />
This shape should also give most of the information an user could need to build their own image integrations based on ESM imports.
Since this would be directly in core, there would be no more configuration changes needed to get the proper types and as such, editor integration should be painless (completions, type checking etc) and address user confusion around this.
In order to avoid any breakage, this shape would be under an experimental flag until the next major release of Astro. Similarly to content collections, we would automatically update your env.d.ts
with the type changes needed for this.
alt
, loading
, decoding
)srcset
.svg
supportWe realize that many of those features not being goals are considered to be downgrades compared to @astrojs/image
. This is on purpose as we'd like to approach this project with a "small MVP, add features over time" mentality.
Creating a perfect Image
component for core is a way more complex project than it might seems, the balance between "This has too much options, it's too confusing to use" and "This doesn't have enough options, it's unusable for anything even a little bit complex" is incredibly hard to hit.
We believe that, much like we did with other features in Astro, by building a good core and providing ways build around it, we'll ultimately achieve the best result possible.
All the examples below show the result path in the built website
src/assets
---
import { Image } from "astro:image";
import myImage from "~/assets/my_image.png"; // Image is 1600x900
// Relative paths are also supported:
// import myImage from "../../assets/my_image.png"
---
<Image src={myImage} alt="Image showing Elsa from the movie Frozen, she has blonde hair and is wearing a long blue dress" />
<img src="/_astro/my_image.hash.webp" width="1600" height="900" decoding="async" loading="lazy" alt="Image showing Elsa from the movie Frozen, she has blonde hair and is wearing a long blue dress" />
src/assets
---
import { Image } from "astro:image";
import myImage from "~/assets/my_image.png"; // Image is 1600x900
---
<Image src={myImage} width={800} alt="..." />
<!-- Result image is a webp of 800x450 -->
<img src="..." width="800" height="450" decoding="async" loading="lazy" alt="..." />
---
import { Image } from "astro:image";
import myImage from "~/assets/my_image.png"; // Image is 1600x900
---
<Image src={myImage} width={800} height={900} alt="..." />
<!-- Result image is a webp of 800x450 -->
<img src="..." width="800" height="900" decoding="async" loading="lazy" alt="..." />
---
import { Image } from "astro:image";
// Image is 1920x1080
---
<Image src="https://example.com/image.png" alt="..." />
<!-- ERROR! `width` and `height` are required -->
<Image src="https://example.com/image.png" width={1280} alt="..." />
<!-- ERROR! `height` is required -->
<Image src="https://example.com/image.png" width={1280} height={720} alt="..." />
<!-- Remote images are not optimized or resized.
<img src="https://example.com/image.png" decoding="async" loading="lazy" width="1280" height="720" alt="...">
Source image is a .png of 640x960
---
cover: ~/assets/cover.png
---
Result is a .webp of 640x960
{
cover: {
src: "/_astro/cover.hash.webp",
width: 640,
height: 960
}
}
My super image of 640x480:
![...](~/assets/my_image.png)
OR
![...](./my_image.png)
OR
![...](../../assets/my_image.png)
<img src="/_astro/my_image.hash.webp" width="640" height="480" loading="lazy" decoding="async" alt="...">
First-class support for localized routes, aka i18n routing.
Many websites need to ship support for translated/localised websites for many reasons:
Nowadays there are workarounds in Astro to make it work, although these workarounds have limitations, and because of that many users can't ship their website properly, or they have to work more to compensate the lack of first-class support.
Accept-Language
header, so support SSR;.astro
components, endpoints, middleware);astro
are you using?1.6.0
None
npm
macOS Big Sur v11.4
When using the @astrojs/alpinejs
integration (v0.1.2) in an Astro project with strict TypeScript config, using shorthand attributes (i.e. @click
instead of x-on:click
), I get the following VS Code TypeScript error:
Property
_click
does not exist on typeButtonHTMLAttributes
It's not really reproducible outside of VSCode as StackBlitz doesn't really do the same level of error checking, but I left a link anyways.
In my mind the fix for this is to have the Alpine integration modify the astro-jsx.d.ts
file to account for all of the various Alpine attributes. In addition to fixing this error, this should (hopefully?) also add VS Code autocomplete support for those attributes—which I think would be really neat!
I'm more than happy to go in and write typings for the different attributes (that seems straightforward enough)—I'm just not sure how to patch into the Astro Integrations API to modify the typings that VS Code / Astro is looking at. If someone could point me in the right direction though, I'm more than willing to figure it out!
https://stackblitz.com/edit/github-pf1rr5-egafck
Introduce a standard to store data separately from your content (ex. JSON files), with a way to "reference" this data from existing content collections.
Content collections are restricted to supporting .md
, .mdx
, and .mdoc
files. This is limiting for other forms of data you may need to store, namely raw data formats like JSON.
Taking a blog post as the example, there will likely be author information thats reused across multiple blog posts. To standardize updates when, say, updating an author's profile picture, it's best to store authors in a separate data entry, with an API to reference this data from any blog post by ID.
The content collections API was built generically to support this future, choosing format-agnostic naming like data
instead of frontmatter
and body
instead of rawContent
. Because of this, expanding support to new data formats without API changes is a natural progression.
We have a few use cases in mind considering data collections and data references. We expect this list to grow through the RFC discussion and learning from our community!
i18n/
collection containing en.json
, fr.json
, etc.alt
text or image widths and heights for standard assets. For example, an images/banner.json
file containing the src
as a string, alt text, and a preferred width
src/data/
directory distinct from src/content/
, or simply allow data collections within src/content/
.Astro v4.2.4
Node v18.18.0
System Linux (x64)
Package Manager npm
Output static
Adapter none
Integrations none
No response
Astro seems to have the concept of process.env
on the server, but it doesn't populate it with stuff from .env.local
for example. This is reaally annoying for library authors trying to automatically infer values for their users. If there is a process.env
, we usually chooses to got with that, and only falling back to import.meta.env
if that fails (for example if typeof process === "undefined"
)
either process not to exist or for the variables to be put there. this middleground seems very confusing...
https://stackblitz.com/edit/github-hdhenl?file=src%2Fpages%2Fapi%2Fhello.ts
This is a proposal to add Markdoc support to content collections.
We've received multiple user requests for Markdoc support since its public launch. In fact, we've seen early community projects bringing Markdoc to robust project themes.
Markdoc is also designed to solve existing limitations of MDX in Astro.
Markdoc is built to solve (2) by separating content from the components, styles, and assets you choose to render. You can use an Astro component renderer when using on your site, Markdoc's own html
renderer for RSS, and even write your own renderer to traverse Markdoc pages yourself. (1) Is something we're excited to test, requiring a thorough performance benchmark.
The content collections API was built generically to support this future, choosing format-agnostic naming like data
instead of frontmatter
and body
instead of rawContent
. Because of this, introducing new authoring formats is possible without breaking changes.
@astrojs/markdoc
integration that adds .mdoc
support to content collections.src/pages/
support for Markdoc files. See discussion for context..md
extension. This would mean overriding Astro's .md
renderer, which is tightly coupled to remark and your markdown
configuration options today. We agree using .md
for Markdoc is a common use case, and deserves a separate proposal to make Astro's Markdown rendering flexible.client:
directive.headings
property (which can be tackled in future releases) and frontmatter manipulation via remark (since remark is incompatible with Markdoc).Markdoc will be introduced as an integration. To standardize our process for adding new collection teams, we may experiment with a (private) integration helper internally. This example shows an addContentEntryType
hook to setup the .mdoc
extension, and attach logic for parsing the data
and body
properties:
// @astrojs/markdoc/index.ts
export const markdoc: AstroIntegration = () => ({
'astro:config:setup'({ addContentEntryType }) {
addContentEntryType({
extensions: ['.mdoc'],
parser: '@astrojs/markdoc/contentEntryParser',
});
}
}
});
// @astrojs/markdoc/contentEntryParser.ts
import parseFrontmatter from 'gray-matter';
export default {
getEntryInfo({ contents }) {
const parsed = parseFrontmatter(contents);
return {
// The unparsed data object that can be passed to a Zod schema.
data: parsed.data,
// The body of the data file. This should be the raw file contents with metadata (i.e. frontmatter block) stripped
body: parsed.content,
// (Optional) The untouched frontmatter block with newlines and formatting preserved. Used for computing error line hints.
rawData: parsed.matter,
}
}
}
// astro.config.mjs
import markdoc from '@astrojs/markdoc';
export default {
integrations: [markdoc()],
}
Say you've authored a collection of blog posts using Markdoc. You can store these entries as a blog
collection, identically to Markdown or MDX:
src/content/
blog/
# Could also use `.md`
post-1.mdoc
post-2.mdoc
post-3.mdoc
...
Then, you can query entry frontmatter with the same getCollection()
and getEntryBySlug()
APIs:
---
import { getCollection, getEntryBySlug } from 'astro:content';
const blog = await getCollection('blog');
const firstEntry = await getEntryBySlug('blog', 'post-1');
---
Users should also be free to render Markdoc contents using a Content
component. This will be exposed from the render()
result, and feature two props:
components?: Record<string, ComponentRenderer>
: A mapping from Markdoc tags or elements to Astro components.config?: import('@markdoc/markdoc').Config
: An (optional) Markdoc config to be used during the transformation step.---
import Title from '../components/Title.astro';
import Marquee from '../components/Marquee.astro';
import { getEntryBySlug } from 'astro:content';
const mdocEntry = await getEntryBySlug('blog', 'test');
const { Content } = await mdocEntry.render();
---
<html lang="en">
<body>
<Content
config={{
variables: { underlineTitle: true },
}}
components={{
h1: Title,
marquee: Marquee,
}}
/>
</body>
</html>
This solution is flexible, but we expect users to reuse config
and components
across their project. For this, we will recommend creating a utility component to encapsulate that config. Here is one example that can render any blog
collection entry with an {% aside /%}
shortcode:
---
// src/components/BlogContent.astro
import Aside from './Aside.astro';
import type { CollectionEntry } from 'astro:content';
type Props = {
entry: CollectionEntry<'blog'>;
};
const { entry } = Astro.props;
const { Content } = await entry.render();
---
<Content
config={{
tags: {
aside: {
render: 'Aside',
attributes: {
type: { type: String },
title: { type: String },
},
},
},
}}
components={{ Aside }}
/>
Now, you can pass any blog collection entry to render the result with this config:
---
import { getEntryBySlug } from 'astro:content';
import BlogContent from '../components/BlogContent.astro';
const mdocEntry = await getEntryBySlug('blog', 'test');
---
<h1>{intro.data.title}</h1>
<BlogContent entry={mdocEntry} />
See this example video for more.
Component renderers can also include a props()
function to map Markdoc attributes and AST entries to component props. This is useful when:
This example maps Markdoc's generated data-language
attribute for code blocks to the lang
prop used by Astro's Code
component, and stringifies the contents to HTML for use with Shiki:
---
import { Code } from 'astro/components';
import { Title } from '../components/Title.astro';
import Markdoc from '@markdoc/markdoc';
...
---
...
<Content
components={{
h1: Title,
pre: {
component: Code,
props({ attributes, getTreeNode }) {
return {
lang: attributes['data-language'],
code: Markdoc.renderers.html(getTreeNode().children),
};
},
},
}}
/>
Deprecate @astrojs/prefetch
and provide first-class prefetch support in Astro.
With the introduction of View Transitions, it includes partial prefetching code for snappy navigation between pages. We can take this opportunity to support prefetching in core, and share the prefetch behaviour with View Transitions.
I've started an implementation before an RFC as the initial plan was to simply move @astrojs/prefetch
to core. However, it would also be a good time to polish up and extend the API.
NOTE: Stage 2 does not mention solution details, but I've written one in the discussion: withastro/astro#8951
Allow islands within a prerendered page to be server-rendered at runtime.
Often a page may be mostly static, but with only parts that need to be rendered on-demand. For example, a product page might need to show stock levels or personalised recommendations. Currently the only option for these is to either render the whole page on demand, or to render the dynamic parts on the client. This proposal introduces the concept of deferred islands, which are not prerendered, but rather server rendered on demand at runtime.
Next.js is working on a solution called partial pre-rendering, which allows most of the page to be prerendered, with individual postponed parts rendered using SSR on-demand. The implementation is quite different from what I propose for Astro, but the concept is similar.
A component would be deferred by setting the server:defer
directive.
<Like server:defer />
The "fallback"
slot can be used to specify a placeholder that is pre-rendered and displayed while the component is loading.
<Avatar server:defer>
<div slot="fallback">Guest</div>
</Avatar>
The page can pass props to the component like normal, and these are available when rendering the component:
---
import Like from "../components/Like";
export const prerender = true;
const post = await getPost(Astro.params.slug)
---
<Like server:defer post={post.id} />
The component itself does not need to do anything to support deferred rendering, so it should work with any existing component. However deferred components can optionally use special powers, and can detect if they were deferred by checking the Astro.deferred
prop. This means that it was deferred at build time but is now being rendered on-demand.
The special powers are because during deferred rendering, a component is rendered like a mini page. This means it can use features such as Astro.cookies
, and setting headers on Astro.response
. The Astro.url
and Astro.request.url
are from the original page, and are passed in the request along with the props.
---
// Like.astro
const { post } = Astro.props
let user = { name: "Guest" }
// If this is a deferred render then we may have a user's cookie
if (Astro.deferred) {
user = await getUser(Astro.cookies.get('session'))
}
---
<div>
<span class="name">{user?.name}</span>
</div>
When rendering the static page, postponed elements would not be rendered, and instead would render an <astro-island>
containing any placeholder. The <astro-island>
would embed the serialized props, as well as the URL for the deferred endpoint.
When the page has loaded, a request would be made to each deferred endpoint (see "GET vs POST" below for considerations). This request would pass all of the props and other serialized context.
On the server, the component would be rendered effectively in a thin wrapper page that decoded and forwarded the props, and rewrites the Astro global values.
When the browser has loaded the response from the endpoint, it would use it to replace the content of the island.
The runtime for replacing the deferred islands would be in an inline script tag. A simplified version without error handling could look like this:
document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll("astro-island[defer]").forEach(async (element) => {
const props = JSON.parse(element.getAttribute('props'));
const endpoint = element.getAttribute('component-endpoint');
const response = await fetch(endpoint, {
method: "POST",
body: JSON.stringify({ props, url: document.location.href })
}).then(res => res.text());
const range = document.createRange()
range.selectNodeContents(element)
const fragment = range.createContextualFragment(response)
range.deleteContents()
range.insertNode(fragment)
})
})
One of the unanswered questions is whether to use GET or POST requests for the deferred component endpoints. The benefit of the POST is that it can send arbitrarily large request bodies. The benefits of GET are that they are cacheable and can be preloaded in the page head. Some options are:
A alternative approach would be to send the postponed components in the same response as the initial shell. This is how Next.js PPR is currently implemented. This has some benefits, but I think they are outweighed by drawbacks in most cases. Astro has always been static-first, and I think that approach is best here too.
Primarily, a prerendered, static page is easily cacheable, both in the browser but also in a CDN. This is not the case when the deferred data is in the same response. The benefits of this are that the static part can be cached at the edge, near to users, with a very fast response time. The deferred content can be rendered and served near to the site's data without blocking rendering of the rest of the page. If you want to stream the deferred content in the same response, you either have to render everything at the origin and take the hit on distance from users, or render it all at the edge and take the hit on distance from your data. In some cases rendering everything at the edge is fine (e.g. if there's no central data source or API access), and Astro already supports that.
You can work around this with logic at the edge to combine a local cached shell and a stream of the updates from the origin, and edge middleware to do this could be a helpful option. It still prevents the use of the browser cache though, because it can't make conditional requests for the prerendered page - the whole things needs to be sent on every request in case the deferred data has changed.
astro
are you using?1.9.1
no
npm
Linux
When printing out the routeData for a dynamic route (e.g. test-[id].astro), a url for a specific page is printed.
Perhaps you should remove the distURL for dynamic routes. Or even better, add all URLs for the route to the routeData
astro
are you using?1.9.1
no
npm
Linux
When printing out the routeData for a dynamic route (e.g. test-[id].astro), a url for a specific page is printed.
Perhaps you should remove the distURL for dynamic routes. Or even better, add all URLs for the route to the routeData
astro
are you using?2.4.5
Vercel
pnpm
Max
N/A
I recently shipped an example for plaiceholder
using the experimental (and wonderful) "assets" feature
https://github.com/joe-bell/plaiceholder/blob/main/examples/astro
One minor thing that I've had to patch around for now, is that an image's attributes
are a bit looser than I would expect:
https://github.com/joe-bell/plaiceholder/blob/main/examples/astro/src/pages/base64/multiple.astro#L18
My expectation here would be that getImage()
is at least retrieving the height
/width
for me (that's what's outputted, but the types don't reflect that currently)
Thanks again for all your work on this feature, having a blast :rock
https://github.com/joe-bell/plaiceholder/blob/main/examples/astro
astro
are you using?2.9.3
Node
npm
Mac
Chrome
After installing only production packages (npm i --omit dev
) for the project, I noticed that possibly many installed packages are not required for the runtime. They probably should be declared as devDependencies
. e.g. typescript
, @babel/*
, esbuild
, vite
. Having them declared as dependencies
greatly increases the size of node_modules
, making, e.g. building a small docker image of the app very difficult. The node_modules
for a "hello world" app has about 160 MB.
All dependencies that are not required for the runtime should be declared as devDependencies
.
https://github.com/withastro/astro/blob/main/packages/astro/package.json
Let's make handling form submissions easy and type-safe both server-side and client-side.
Content-Type
.!
or as string
casts as in the example formData.get('expected')! as string
.form
element with the action
property.useActionState()
and useFormStatus()
hooks in React 19.hybrid
output.required
and type
properties on an input..astro
frontmatter. Frontmatter forms a function closure, which can lead to misuse of variables within an action handler. This challenge is shared by getStaticPaths()
and it would be best to avoid repeating this pattern in future APIs.Form submissions are a core building block of the web that Astro has yet to form an opinion on (pun intended).
So far, Astro has been rewarded for waiting on the platform and the Astro community to mature before designing a primitive. By waiting on view transitions, we found a SPA-like routing solution grounded in native APIs. By waiting on libSQL, we found a data storage solution for content sites and apps alike. Now, we've waited on other major frameworks to forge new paths with form actions. This includes Remix actions, SvelteKit actions, React server actions, and more.
At the same time, Astro just launched its database primitive: Astro DB. This is propelling the Astro community from static content to more dynamic use cases:
To meet our community where it's heading, Astro needs a form submission story.
Astro presents two solutions to handling form submissions with today's primitives. Though capable, these tools are either too primitive or present unacceptable tradeoffs for common use cases.
JSON API routes allow developers to handle POST requests and return a JSON response to the client. Astro suggests this approach with a documentation recipe, demonstrating how to create an API route and handle the result from a Preact component.
However, REST endpoints are overly primitive for basic use cases. The developer is left to handle parse errors and API contracts themselves, without type safety on either side. To properly handle all error cases, this grows complex for even the simplest of forms:
// src/api/board/[board]/card/[card].ts
import { db, Card, eq, and, desc } from "astro:db";
import type { APIContext } from "astro";
const POST = async ({params, request}: APIContext) => {
if (!params.card || !params.board) {
return new Response(
JSON.stringify({ success: false, error: "Invalid board or card ID" }),
{ status: 400 }
);
}
if (!request.headers.get("content-type")?.includes("application/x-www-form-urlencoded")) {
return new Response(
JSON.stringify({ success: false, error: "Must be a form request" }),
{ status: 400 }
);
}
const body = await request.formData();
const name = body.get("name");
const description = body.get("description");
if (typeof name !== "string" || typeof description !== "string") {
return new Response(
JSON.stringify({ success: false, error: "Invalid form data" }),
{ status: 400 }
);
}
// Actual app logic starts here!
const res = await db
.update(Card)
.set({ name, description })
.where(
and(eq(Card.boardId, params.board), eq(Card.id, params.card))
);
return new Response(
JSON.stringify({ success: res.rowsAffected > 0 }),
{ status: res.rowsAffected > 0 ? 200 : 404 }
);
}
The client should also guard against malformed response values. This is accomplished through runtime validation with Zod, or a type cast to the response the client expects. Managing this contract in both places leaves room for types to fall out-of-sync. The manual work of defining and casting types is also added complexity that the Astro docs avoid for beginner use cases.
What's more, there is no guidance to progressively enhance this form. By default, a browser will send the form data to the action
field specified on the <form>
element, and rerender the page with the action
response. This default behavior is important to consider when a user submits a form before client JS has finished parsing, a common concern for poor internet connections.
However, we cannot apply our API route as the action
. Since our API route returns JSON, the user would be greeted by a stringified JSON blob rather than the refreshed contents of the page. The developer would need to duplicate this API handler into the page frontmatter to return HTML with the refreshed content. This is added complexity that our docs understandably don't discuss.
View transitions for forms allow developers to handle a submission from Astro frontmatter and re-render the page with a SPA-like refresh.
---
const formData = await Astro.request.formData();
const likes = handleSubmission(formData);
---
<form method="POST">
<button>Likes {likes}</button>
</form>
This avoids common pitfalls with MPA form submissions, including the "Confirm resubmission?" dialog a user may receive attempting to reload the page. This solution also progressively enhances based on the default form action
handler.
However, handling submissions from the page's frontmatter is prohibitive for static sites that cannot afford to server-render every route. It also triggers unnecessary work when client-side update is contained. For example, clicking "Likes" in this example will re-render the blog post and remount all client components without the transition:persist
decorator.
Last, the user is left to figure out common needs like optimistic UI updates and loading states. The user can attach event listeners for the view transition lifecycle, though we lack documentation on how to do so from popular client frameworks like React.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.