+
+
+
diff --git a/examples/with-content/src/components/HeaderLink.astro b/examples/with-content/src/components/HeaderLink.astro
new file mode 100644
index 000000000..e8e2e103b
--- /dev/null
+++ b/examples/with-content/src/components/HeaderLink.astro
@@ -0,0 +1,22 @@
+---
+export interface Props extends astroHTML.JSX.AnchorHTMLAttributes {}
+
+const { href, class: className, ...props } = Astro.props;
+
+const { pathname } = Astro.url;
+const isActive = href === pathname || href === pathname.replace(/\/$/, '');
+---
+
+
+
+
+
diff --git a/examples/with-content/src/consts.ts b/examples/with-content/src/consts.ts
new file mode 100644
index 000000000..1d5dbf632
--- /dev/null
+++ b/examples/with-content/src/consts.ts
@@ -0,0 +1,5 @@
+// Place any global data in this file.
+// You can import this data from anywhere in your site by using the `import` keyword.
+
+export const SITE_TITLE = 'My personal website.';
+export const SITE_DESCRIPTION = 'Welcome to my website!';
diff --git a/examples/with-content/src/content/blog/first-post.md b/examples/with-content/src/content/blog/first-post.md
new file mode 100644
index 000000000..33b844032
--- /dev/null
+++ b/examples/with-content/src/content/blog/first-post.md
@@ -0,0 +1,16 @@
+---
+title: "First post"
+description: "Lorem ipsum dolor sit amet"
+pubDate: "Jul 08 2022"
+heroImage: "/placeholder-hero.jpg"
+---
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet.
+
+Morbi tristique senectus et netus. Id semper risus in hendrerit gravida rutrum quisque non tellus. Habitasse platea dictumst quisque sagittis purus sit amet. Tellus molestie nunc non blandit massa. Cursus vitae congue mauris rhoncus. Accumsan tortor posuere ac ut. Fringilla urna porttitor rhoncus dolor. Elit ullamcorper dignissim cras tincidunt lobortis. In cursus turpis massa tincidunt dui ut ornare lectus. Integer feugiat scelerisque varius morbi enim nunc. Bibendum neque egestas congue quisque egestas diam. Cras ornare arcu dui vivamus arcu felis bibendum. Dignissim suspendisse in est ante in nibh mauris. Sed tempus urna et pharetra pharetra massa massa ultricies mi.
+
+Mollis nunc sed id semper risus in. Convallis a cras semper auctor neque. Diam sit amet nisl suscipit. Lacus viverra vitae congue eu consequat ac felis donec. Egestas integer eget aliquet nibh praesent tristique magna sit amet. Eget magna fermentum iaculis eu non diam. In vitae turpis massa sed elementum. Tristique et egestas quis ipsum suspendisse ultrices. Eget lorem dolor sed viverra ipsum. Vel turpis nunc eget lorem dolor sed viverra. Posuere ac ut consequat semper viverra nam. Laoreet suspendisse interdum consectetur libero id faucibus. Diam phasellus vestibulum lorem sed risus ultricies tristique. Rhoncus dolor purus non enim praesent elementum facilisis. Ultrices tincidunt arcu non sodales neque. Tempus egestas sed sed risus pretium quam vulputate. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare. Fringilla urna porttitor rhoncus dolor purus non. Amet dictum sit amet justo donec enim.
+
+Mattis ullamcorper velit sed ullamcorper morbi tincidunt. Tortor posuere ac ut consequat semper viverra. Tellus mauris a diam maecenas sed enim ut sem viverra. Venenatis urna cursus eget nunc scelerisque viverra mauris in. Arcu ac tortor dignissim convallis aenean et tortor at. Curabitur gravida arcu ac tortor dignissim convallis aenean et tortor. Egestas tellus rutrum tellus pellentesque eu. Fusce ut placerat orci nulla pellentesque dignissim enim sit amet. Ut enim blandit volutpat maecenas volutpat blandit aliquam etiam. Id donec ultrices tincidunt arcu. Id cursus metus aliquam eleifend mi.
+
+Tempus quam pellentesque nec nam aliquam sem. Risus at ultrices mi tempus imperdiet. Id porta nibh venenatis cras sed felis eget velit. Ipsum a arcu cursus vitae. Facilisis magna etiam tempor orci eu lobortis elementum. Tincidunt dui ut ornare lectus sit. Quisque non tellus orci ac. Blandit libero volutpat sed cras. Nec tincidunt praesent semper feugiat nibh sed pulvinar proin gravida. Egestas integer eget aliquet nibh praesent tristique magna.
diff --git a/examples/with-content/src/content/blog/markdown-style-guide.md b/examples/with-content/src/content/blog/markdown-style-guide.md
new file mode 100644
index 000000000..1f221fb91
--- /dev/null
+++ b/examples/with-content/src/content/blog/markdown-style-guide.md
@@ -0,0 +1,102 @@
+---
+title: "Markdown Style Guide"
+description: "Here is a sample of some basic Markdown syntax that can be used when writing Markdown content in Astro."
+pubDate: "Jul 01 2022"
+heroImage: "/placeholder-hero.jpg"
+---
+
+Here is a sample of some basic Markdown syntax that can be used when writing Markdown content in Astro.
+
+## Headings
+
+The following HTML `
`—`
` elements represent six levels of section headings. `
` is the highest section level while `
` is the lowest.
+
+# H1
+## H2
+### H3
+#### H4
+##### H5
+###### H6
+
+## Paragraph
+
+Xerum, quo qui aut unt expliquam qui dolut labo. Aque venitatiusda cum, voluptionse latur sitiae dolessi aut parist aut dollo enim qui voluptate ma dolestendit peritin re plis aut quas inctum laceat est volestemque commosa as cus endigna tectur, offic to cor sequas etum rerum idem sintibus eiur? Quianimin porecus evelectur, cum que nis nust voloribus ratem aut omnimi, sitatur? Quiatem. Nam, omnis sum am facea corem alique molestrunt et eos evelece arcillit ut aut eos eos nus, sin conecerem erum fuga. Ri oditatquam, ad quibus unda veliamenimin cusam et facea ipsamus es exerum sitate dolores editium rerore eost, temped molorro ratiae volorro te reribus dolorer sperchicium faceata tiustia prat.
+
+Itatur? Quiatae cullecum rem ent aut odis in re eossequodi nonsequ idebis ne sapicia is sinveli squiatum, core et que aut hariosam ex eat.
+
+## Images
+
+
+
+## Blockquotes
+
+The blockquote element represents content that is quoted from another source, optionally with a citation which must be within a `footer` or `cite` element, and optionally with in-line changes such as annotations and abbreviations.
+
+#### Blockquote without attribution
+
+> Tiam, ad mint andaepu dandae nostion secatur sequo quae.
+> **Note** that you can use *Markdown syntax* within a blockquote.
+
+#### Blockquote with attribution
+
+> Don't communicate by sharing memory, share memory by communicating.
+> — Rob Pike[^1]
+
+[^1]: The above quote is excerpted from Rob Pike's [talk](https://www.youtube.com/watch?v=PAAkCSZUG1c) during Gopherfest, November 18, 2015.
+
+## Tables
+
+| Italics | Bold | Code |
+| -------- | -------- | ------ |
+| *italics* | **bold** | `code` |
+
+## Code Blocks
+
+```html
+
+
+
+
+ Example HTML5 Document
+
+
+
Test
+
+
+```
+
+## List Types
+
+#### Ordered List
+
+1. First item
+2. Second item
+3. Third item
+
+#### Unordered List
+
+* List item
+* Another item
+* And another item
+
+#### Nested list
+
+* Fruit
+ * Apple
+ * Orange
+ * Banana
+* Dairy
+ * Milk
+ * Cheese
+
+## Other Elements — abbr, sub, sup, kbd, mark
+
+GIF is a bitmap image format.
+
+H2O
+
+Xn + Yn = Zn
+
+Press CTRL+ALT+Delete to end the session.
+
+Most salamanders are nocturnal, and hunt for insects, worms, and other small creatures.
diff --git a/examples/with-content/src/content/blog/second-post.md b/examples/with-content/src/content/blog/second-post.md
new file mode 100644
index 000000000..1bd5ee465
--- /dev/null
+++ b/examples/with-content/src/content/blog/second-post.md
@@ -0,0 +1,16 @@
+---
+title: "Second post"
+description: "Lorem ipsum dolor sit amet"
+pubDate: "Jul 22 2022"
+heroImage: "/placeholder-hero.jpg"
+---
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet.
+
+Morbi tristique senectus et netus. Id semper risus in hendrerit gravida rutrum quisque non tellus. Habitasse platea dictumst quisque sagittis purus sit amet. Tellus molestie nunc non blandit massa. Cursus vitae congue mauris rhoncus. Accumsan tortor posuere ac ut. Fringilla urna porttitor rhoncus dolor. Elit ullamcorper dignissim cras tincidunt lobortis. In cursus turpis massa tincidunt dui ut ornare lectus. Integer feugiat scelerisque varius morbi enim nunc. Bibendum neque egestas congue quisque egestas diam. Cras ornare arcu dui vivamus arcu felis bibendum. Dignissim suspendisse in est ante in nibh mauris. Sed tempus urna et pharetra pharetra massa massa ultricies mi.
+
+Mollis nunc sed id semper risus in. Convallis a cras semper auctor neque. Diam sit amet nisl suscipit. Lacus viverra vitae congue eu consequat ac felis donec. Egestas integer eget aliquet nibh praesent tristique magna sit amet. Eget magna fermentum iaculis eu non diam. In vitae turpis massa sed elementum. Tristique et egestas quis ipsum suspendisse ultrices. Eget lorem dolor sed viverra ipsum. Vel turpis nunc eget lorem dolor sed viverra. Posuere ac ut consequat semper viverra nam. Laoreet suspendisse interdum consectetur libero id faucibus. Diam phasellus vestibulum lorem sed risus ultricies tristique. Rhoncus dolor purus non enim praesent elementum facilisis. Ultrices tincidunt arcu non sodales neque. Tempus egestas sed sed risus pretium quam vulputate. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare. Fringilla urna porttitor rhoncus dolor purus non. Amet dictum sit amet justo donec enim.
+
+Mattis ullamcorper velit sed ullamcorper morbi tincidunt. Tortor posuere ac ut consequat semper viverra. Tellus mauris a diam maecenas sed enim ut sem viverra. Venenatis urna cursus eget nunc scelerisque viverra mauris in. Arcu ac tortor dignissim convallis aenean et tortor at. Curabitur gravida arcu ac tortor dignissim convallis aenean et tortor. Egestas tellus rutrum tellus pellentesque eu. Fusce ut placerat orci nulla pellentesque dignissim enim sit amet. Ut enim blandit volutpat maecenas volutpat blandit aliquam etiam. Id donec ultrices tincidunt arcu. Id cursus metus aliquam eleifend mi.
+
+Tempus quam pellentesque nec nam aliquam sem. Risus at ultrices mi tempus imperdiet. Id porta nibh venenatis cras sed felis eget velit. Ipsum a arcu cursus vitae. Facilisis magna etiam tempor orci eu lobortis elementum. Tincidunt dui ut ornare lectus sit. Quisque non tellus orci ac. Blandit libero volutpat sed cras. Nec tincidunt praesent semper feugiat nibh sed pulvinar proin gravida. Egestas integer eget aliquet nibh praesent tristique magna.
diff --git a/examples/with-content/src/content/blog/third-post.md b/examples/with-content/src/content/blog/third-post.md
new file mode 100644
index 000000000..d7f1f24b0
--- /dev/null
+++ b/examples/with-content/src/content/blog/third-post.md
@@ -0,0 +1,16 @@
+---
+title: "Third post"
+description: "Lorem ipsum dolor sit amet"
+pubDate: "Jul 15 2022"
+heroImage: "/placeholder-hero.jpg"
+---
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet.
+
+Morbi tristique senectus et netus. Id semper risus in hendrerit gravida rutrum quisque non tellus. Habitasse platea dictumst quisque sagittis purus sit amet. Tellus molestie nunc non blandit massa. Cursus vitae congue mauris rhoncus. Accumsan tortor posuere ac ut. Fringilla urna porttitor rhoncus dolor. Elit ullamcorper dignissim cras tincidunt lobortis. In cursus turpis massa tincidunt dui ut ornare lectus. Integer feugiat scelerisque varius morbi enim nunc. Bibendum neque egestas congue quisque egestas diam. Cras ornare arcu dui vivamus arcu felis bibendum. Dignissim suspendisse in est ante in nibh mauris. Sed tempus urna et pharetra pharetra massa massa ultricies mi.
+
+Mollis nunc sed id semper risus in. Convallis a cras semper auctor neque. Diam sit amet nisl suscipit. Lacus viverra vitae congue eu consequat ac felis donec. Egestas integer eget aliquet nibh praesent tristique magna sit amet. Eget magna fermentum iaculis eu non diam. In vitae turpis massa sed elementum. Tristique et egestas quis ipsum suspendisse ultrices. Eget lorem dolor sed viverra ipsum. Vel turpis nunc eget lorem dolor sed viverra. Posuere ac ut consequat semper viverra nam. Laoreet suspendisse interdum consectetur libero id faucibus. Diam phasellus vestibulum lorem sed risus ultricies tristique. Rhoncus dolor purus non enim praesent elementum facilisis. Ultrices tincidunt arcu non sodales neque. Tempus egestas sed sed risus pretium quam vulputate. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare. Fringilla urna porttitor rhoncus dolor purus non. Amet dictum sit amet justo donec enim.
+
+Mattis ullamcorper velit sed ullamcorper morbi tincidunt. Tortor posuere ac ut consequat semper viverra. Tellus mauris a diam maecenas sed enim ut sem viverra. Venenatis urna cursus eget nunc scelerisque viverra mauris in. Arcu ac tortor dignissim convallis aenean et tortor at. Curabitur gravida arcu ac tortor dignissim convallis aenean et tortor. Egestas tellus rutrum tellus pellentesque eu. Fusce ut placerat orci nulla pellentesque dignissim enim sit amet. Ut enim blandit volutpat maecenas volutpat blandit aliquam etiam. Id donec ultrices tincidunt arcu. Id cursus metus aliquam eleifend mi.
+
+Tempus quam pellentesque nec nam aliquam sem. Risus at ultrices mi tempus imperdiet. Id porta nibh venenatis cras sed felis eget velit. Ipsum a arcu cursus vitae. Facilisis magna etiam tempor orci eu lobortis elementum. Tincidunt dui ut ornare lectus sit. Quisque non tellus orci ac. Blandit libero volutpat sed cras. Nec tincidunt praesent semper feugiat nibh sed pulvinar proin gravida. Egestas integer eget aliquet nibh praesent tristique magna.
diff --git a/examples/with-content/src/content/blog/using-mdx.mdx b/examples/with-content/src/content/blog/using-mdx.mdx
new file mode 100644
index 000000000..036209d3b
--- /dev/null
+++ b/examples/with-content/src/content/blog/using-mdx.mdx
@@ -0,0 +1,31 @@
+---
+title: 'Using MDX'
+description: 'Lorem ipsum dolor sit amet'
+pubDate: 'Jul 02 2022'
+heroImage: '/placeholder-hero.jpg'
+---
+
+This theme comes with the [@astrojs/mdx](https://docs.astro.build/en/guides/integrations-guide/mdx/) integration installed and configured in your `astro.config.mjs` config file. If you prefer not to use MDX, you can disable support by removing the integration from your config file.
+
+## Why MDX?
+
+MDX is a special flavor of Markdown that supports embedded JavaScript & JSX syntax. This unlocks the ability to [mix JavaScript and UI Components into your Markdown content](https://docs.astro.build/en/guides/markdown-content/#mdx-features) for things like interactive charts or alerts.
+
+If you have existing content authored in MDX, this integration will hopefully make migrating to Astro a breeze.
+
+## Example
+
+Here is how you import and use a UI component inside of MDX.
+When you open this page in the browser, you should see the clickable button below.
+
+import HeaderLink from '../../components/HeaderLink.astro';
+
+
+ Embedded component in MDX
+
+
+## More Links
+
+- [MDX Syntax Documentation](https://mdxjs.com/docs/what-is-mdx)
+- [Astro Usage Documentation](https://docs.astro.build/en/guides/markdown-content/#markdown-and-mdx-pages)
+- **Note:** [Client Directives](https://docs.astro.build/en/reference/directives-reference/#client-directives) are still required to create interactive components. Otherwise, all components in your MDX will render as static HTML (no JavaScript) by default.
diff --git a/examples/with-content/src/content/config.ts b/examples/with-content/src/content/config.ts
new file mode 100644
index 000000000..9d436060a
--- /dev/null
+++ b/examples/with-content/src/content/config.ts
@@ -0,0 +1,18 @@
+import { defineCollection, z } from 'astro:content';
+
+const blog = defineCollection({
+ // Type-check frontmatter using a schema
+ schema: {
+ title: z.string(),
+ description: z.string(),
+ // Transform string to Date object
+ pubDate: z.string().transform((str) => new Date(str)),
+ updatedDate: z
+ .string()
+ .optional()
+ .transform((str) => (str ? new Date(str) : undefined)),
+ heroImage: z.string().optional(),
+ },
+});
+
+export const collections = { blog };
diff --git a/examples/with-content/src/content/types.generated.d.ts b/examples/with-content/src/content/types.generated.d.ts
new file mode 100644
index 000000000..af803d2cf
--- /dev/null
+++ b/examples/with-content/src/content/types.generated.d.ts
@@ -0,0 +1,86 @@
+declare module 'astro:content' {
+ export { z } from 'astro/zod';
+ export type CollectionEntry =
+ typeof entryMap[C][keyof typeof entryMap[C]] & Render;
+
+ type BaseCollectionConfig = {
+ schema?: S;
+ slug?: (entry: {
+ id: CollectionEntry['id'];
+ defaultSlug: string;
+ collection: string;
+ body: string;
+ data: import('astro/zod').infer>;
+ }) => string | Promise;
+ };
+ export function defineCollection(
+ input: BaseCollectionConfig
+ ): BaseCollectionConfig;
+
+ export function getEntry(
+ collection: C,
+ entryKey: E
+ ): Promise;
+ export function getCollection<
+ C extends keyof typeof entryMap,
+ E extends keyof typeof entryMap[C]
+ >(
+ collection: C,
+ filter?: (data: typeof entryMap[C][E]) => boolean
+ ): Promise<(typeof entryMap[C][E] & Render)[]>;
+
+ type InferEntrySchema = import('astro/zod').infer<
+ import('astro/zod').ZodObject['schema']>
+ >;
+
+ type Render = {
+ render(): Promise<{
+ Content: import('astro').MarkdownInstance<{}>['Content'];
+ headings: import('astro').MarkdownHeading[];
+ injectedFrontmatter: Record;
+ }>;
+ };
+
+ const entryMap: {
+ "blog": {
+"first-post.md": {
+ id: "first-post.md",
+ slug: "first-post",
+ body: string,
+ collection: "blog",
+ data: InferEntrySchema<"blog">
+},
+"markdown-style-guide.md": {
+ id: "markdown-style-guide.md",
+ slug: "markdown-style-guide",
+ body: string,
+ collection: "blog",
+ data: InferEntrySchema<"blog">
+},
+"second-post.md": {
+ id: "second-post.md",
+ slug: "second-post",
+ body: string,
+ collection: "blog",
+ data: InferEntrySchema<"blog">
+},
+"third-post.md": {
+ id: "third-post.md",
+ slug: "third-post",
+ body: string,
+ collection: "blog",
+ data: InferEntrySchema<"blog">
+},
+"using-mdx.mdx": {
+ id: "using-mdx.mdx",
+ slug: "using-mdx",
+ body: string,
+ collection: "blog",
+ data: InferEntrySchema<"blog">
+},
+},
+
+ };
+
+ type ContentConfig = typeof import("./config");
+}
diff --git a/examples/with-content/src/env.d.ts b/examples/with-content/src/env.d.ts
new file mode 100644
index 000000000..f964fe0cf
--- /dev/null
+++ b/examples/with-content/src/env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/examples/with-content/src/layouts/BlogPost.astro b/examples/with-content/src/layouts/BlogPost.astro
new file mode 100644
index 000000000..c2527082a
--- /dev/null
+++ b/examples/with-content/src/layouts/BlogPost.astro
@@ -0,0 +1,49 @@
+---
+import BaseHead from '../components/BaseHead.astro';
+import Header from '../components/Header.astro';
+import Footer from '../components/Footer.astro';
+import type { CollectionEntry } from 'astro:content';
+
+type Props = CollectionEntry<'blog'>['data'];
+
+const {
+ title, description, pubDate, updatedDate, heroImage,
+} = Astro.props;
+---
+
+
+
+
+
+
+
+
+
+
+
+ {heroImage && }
+
{title}
+ {pubDate && }
+ {
+ updatedDate && (
+
+ Last updated on
+
+ )
+ }
+
+
+
+
+
+
+
diff --git a/examples/with-content/src/pages/about.md b/examples/with-content/src/pages/about.md
new file mode 100644
index 000000000..2aeb6562e
--- /dev/null
+++ b/examples/with-content/src/pages/about.md
@@ -0,0 +1,17 @@
+---
+layout: "../layouts/BlogPost.astro"
+title: "About Me"
+description: "Lorem ipsum dolor sit amet"
+updatedDate: "August 08 2022"
+heroImage: "/placeholder-about.jpg"
+---
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet.
+
+Morbi tristique senectus et netus. Id semper risus in hendrerit gravida rutrum quisque non tellus. Habitasse platea dictumst quisque sagittis purus sit amet. Tellus molestie nunc non blandit massa. Cursus vitae congue mauris rhoncus. Accumsan tortor posuere ac ut. Fringilla urna porttitor rhoncus dolor. Elit ullamcorper dignissim cras tincidunt lobortis. In cursus turpis massa tincidunt dui ut ornare lectus. Integer feugiat scelerisque varius morbi enim nunc. Bibendum neque egestas congue quisque egestas diam. Cras ornare arcu dui vivamus arcu felis bibendum. Dignissim suspendisse in est ante in nibh mauris. Sed tempus urna et pharetra pharetra massa massa ultricies mi.
+
+Mollis nunc sed id semper risus in. Convallis a cras semper auctor neque. Diam sit amet nisl suscipit. Lacus viverra vitae congue eu consequat ac felis donec. Egestas integer eget aliquet nibh praesent tristique magna sit amet. Eget magna fermentum iaculis eu non diam. In vitae turpis massa sed elementum. Tristique et egestas quis ipsum suspendisse ultrices. Eget lorem dolor sed viverra ipsum. Vel turpis nunc eget lorem dolor sed viverra. Posuere ac ut consequat semper viverra nam. Laoreet suspendisse interdum consectetur libero id faucibus. Diam phasellus vestibulum lorem sed risus ultricies tristique. Rhoncus dolor purus non enim praesent elementum facilisis. Ultrices tincidunt arcu non sodales neque. Tempus egestas sed sed risus pretium quam vulputate. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare. Fringilla urna porttitor rhoncus dolor purus non. Amet dictum sit amet justo donec enim.
+
+Mattis ullamcorper velit sed ullamcorper morbi tincidunt. Tortor posuere ac ut consequat semper viverra. Tellus mauris a diam maecenas sed enim ut sem viverra. Venenatis urna cursus eget nunc scelerisque viverra mauris in. Arcu ac tortor dignissim convallis aenean et tortor at. Curabitur gravida arcu ac tortor dignissim convallis aenean et tortor. Egestas tellus rutrum tellus pellentesque eu. Fusce ut placerat orci nulla pellentesque dignissim enim sit amet. Ut enim blandit volutpat maecenas volutpat blandit aliquam etiam. Id donec ultrices tincidunt arcu. Id cursus metus aliquam eleifend mi.
+
+Tempus quam pellentesque nec nam aliquam sem. Risus at ultrices mi tempus imperdiet. Id porta nibh venenatis cras sed felis eget velit. Ipsum a arcu cursus vitae. Facilisis magna etiam tempor orci eu lobortis elementum. Tincidunt dui ut ornare lectus sit. Quisque non tellus orci ac. Blandit libero volutpat sed cras. Nec tincidunt praesent semper feugiat nibh sed pulvinar proin gravida. Egestas integer eget aliquet nibh praesent tristique magna.
\ No newline at end of file
diff --git a/examples/with-content/src/pages/blog/[...slug].astro b/examples/with-content/src/pages/blog/[...slug].astro
new file mode 100644
index 000000000..c8e86c487
--- /dev/null
+++ b/examples/with-content/src/pages/blog/[...slug].astro
@@ -0,0 +1,21 @@
+---
+import { CollectionEntry, getCollection } from "astro:content";
+import BlogPost from "../../layouts/BlogPost.astro";
+
+export async function getStaticPaths() {
+ const posts = await getCollection('blog');
+ return posts.map(post => ({
+ params: { slug: post.slug },
+ props: post,
+ }));
+}
+type Props = CollectionEntry<'blog'>;
+
+const post = Astro.props;
+const { Content } = await post.render();
+---
+
+
+
{post.data.title}
+
+
diff --git a/examples/with-content/src/pages/blog/index.astro b/examples/with-content/src/pages/blog/index.astro
new file mode 100644
index 000000000..e4f769f8a
--- /dev/null
+++ b/examples/with-content/src/pages/blog/index.astro
@@ -0,0 +1,57 @@
+---
+import BaseHead from '../../components/BaseHead.astro';
+import Header from '../../components/Header.astro';
+import Footer from '../../components/Footer.astro';
+import { SITE_TITLE, SITE_DESCRIPTION } from '../../consts';
+import { getCollection } from 'astro:content';
+
+const posts = (await getCollection('blog')).sort(
+ (a, b) => a.data.pubDate.valueOf() - b.data.pubDate.valueOf()
+)
+---
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/with-content/src/pages/index.astro b/examples/with-content/src/pages/index.astro
new file mode 100644
index 000000000..26f070da5
--- /dev/null
+++ b/examples/with-content/src/pages/index.astro
@@ -0,0 +1,50 @@
+---
+import BaseHead from '../components/BaseHead.astro';
+import Header from '../components/Header.astro';
+import Footer from '../components/Footer.astro';
+import { SITE_TITLE, SITE_DESCRIPTION } from '../consts';
+---
+
+
+
+
+
+
+
+
+
+
🧑🚀 Hello, Astronaut!
+
+ Welcome to the official Astro blog starter template. This
+ template serves as a lightweight, minimally-styled starting point for anyone looking to build
+ a personal website, blog, or portfolio with Astro.
+
+
+ This template comes with a few integrations already configured in your
+ astro.config.mjs file. You can customize your setup with
+ Astro Integrations to add tools like Tailwind,
+ React, or Vue to your project.
+
+
Here are a few ideas on how to get started with the template:
+
+
Edit this page in src/pages/index.astro
+
Edit the site header items in src/components/Header.astro
+
Add your name to the footer in src/components/Footer.astro
+
Check out the included blog posts in src/pages/blog/
+
Customize the blog post page layout in src/layouts/BlogPost.astro
+
+
+
+
diff --git a/examples/with-content/src/styles/global.css b/examples/with-content/src/styles/global.css
new file mode 100644
index 000000000..999782ff2
--- /dev/null
+++ b/examples/with-content/src/styles/global.css
@@ -0,0 +1,67 @@
+/*
+ The CSS in this style tag is based off of Bear Blog's default CSS.
+ https://github.com/HermanMartinus/bearblog/blob/297026a877bc2ab2b3bdfbd6b9f7961c350917dd/templates/styles/blog/default.css
+ License MIT: https://github.com/HermanMartinus/bearblog/blob/master/LICENSE.md
+ */
+body {
+ font-family: Verdana, sans-serif;
+ margin: auto;
+ padding: 20px;
+ max-width: 65ch;
+ text-align: left;
+ background-color: #fff;
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+ line-height: 1.5;
+ color: #444;
+}
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+strong,
+b {
+ color: #222;
+}
+a {
+ color: #3273dc;
+}
+nav a {
+ margin-right: 10px;
+}
+textarea {
+ width: 100%;
+ font-size: 16px;
+}
+input {
+ font-size: 16px;
+}
+content {
+ line-height: 1.6;
+}
+table {
+ width: 100%;
+}
+img {
+ max-width: 100%;
+ height: auto;
+}
+code {
+ padding: 2px 5px;
+ background-color: #f2f2f2;
+}
+pre {
+ padding: 1rem;
+}
+pre > code {
+ all: unset;
+}
+blockquote {
+ border: 1px solid #999;
+ color: #222;
+ padding: 2px 0px 2px 20px;
+ margin: 0px;
+ font-style: italic;
+}
diff --git a/examples/with-content/tsconfig.json b/examples/with-content/tsconfig.json
new file mode 100644
index 000000000..d78f81ec4
--- /dev/null
+++ b/examples/with-content/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "astro/tsconfigs/base"
+}
diff --git a/package.json b/package.json
index eccec29bb..fa438bd77 100644
--- a/package.json
+++ b/package.json
@@ -89,6 +89,7 @@
"eslint-plugin-no-only-tests": "^2.6.0",
"eslint-plugin-prettier": "^4.0.0",
"execa": "^6.1.0",
+ "only-allow": "^1.1.1",
"organize-imports-cli": "^0.10.0",
"prettier": "^2.7.0",
"prettier-plugin-astro": "^0.7.0",
diff --git a/packages/astro/package.json b/packages/astro/package.json
index 4e9e9dc91..8396a3e40 100644
--- a/packages/astro/package.json
+++ b/packages/astro/package.json
@@ -47,6 +47,7 @@
"./client/*": "./dist/runtime/client/*",
"./components": "./components/index.ts",
"./components/*": "./components/*",
+ "./content/internal": "./dist/content/internal.js",
"./debug": "./components/Debug.astro",
"./internal/*": "./dist/runtime/server/*",
"./package.json": "./package.json",
@@ -62,7 +63,11 @@
"./vite-plugin-markdown-legacy/*": "./dist/vite-plugin-markdown-legacy/*",
"./vite-plugin-markdown": "./dist/vite-plugin-markdown/index.js",
"./vite-plugin-markdown/*": "./dist/vite-plugin-markdown/*",
- "./dist/jsx/*": "./dist/jsx/*"
+ "./dist/jsx/*": "./dist/jsx/*",
+ "./zod": {
+ "types": "./zod.d.ts",
+ "default": "./zod.mjs"
+ }
},
"imports": {
"#astro/*": "./dist/*.js"
@@ -77,6 +82,8 @@
"astro.js",
"config.d.ts",
"config.mjs",
+ "zod.d.ts",
+ "zod.mjs",
"env.d.ts",
"client.d.ts",
"client-base.d.ts",
@@ -84,7 +91,8 @@
"astro-jsx.d.ts",
"types.d.ts",
"README.md",
- "vendor"
+ "vendor",
+ "src/content/template"
],
"scripts": {
"prebuild": "astro-scripts prebuild --to-string \"src/runtime/server/astro-island.ts\" \"src/runtime/client/{idle,load,media,only,visible}.ts\"",
@@ -122,6 +130,7 @@
"cookie": "^0.5.0",
"debug": "^4.3.4",
"deepmerge-ts": "^4.2.2",
+ "devalue": "^4.2.0",
"diff": "^5.1.0",
"es-module-lexer": "^1.1.0",
"execa": "^6.1.0",
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index 0a1eee62a..b42144e9a 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -84,6 +84,7 @@ export interface CLIFlags {
drafts?: boolean;
experimentalErrorOverlay?: boolean;
experimentalPrerender?: boolean;
+ experimentalContentCollections?: boolean;
}
export interface BuildConfig {
@@ -930,6 +931,25 @@ export interface AstroUserConfig {
* ```
*/
prerender?: boolean;
+ /**
+ * @docs
+ * @name experimental.contentCollections
+ * @type {boolean}
+ * @default `false`
+ * @version 1.7.0
+ * @description
+ * Enable experimental support for [Content Collections](/en/guides/content-collections). This makes the `src/content/` directory a reserved directory for Astro to manage, and introduces the `astro:content` module for querying this content.
+ *
+ * To enable this feature, set `experimental.contentCollections` to `true` in your Astro config:
+ *
+ * ```js
+ * {
+ * experimental: {
+ * contentCollections: true,
+ * },
+ * }
+ */
+ contentCollections?: boolean;
};
// Legacy options to be removed
diff --git a/packages/astro/src/content/consts.ts b/packages/astro/src/content/consts.ts
new file mode 100644
index 000000000..8ac53f933
--- /dev/null
+++ b/packages/astro/src/content/consts.ts
@@ -0,0 +1,10 @@
+export const contentFileExts = ['.md', '.mdx'];
+export const DELAYED_ASSET_FLAG = 'astroAssetSsr';
+export const CONTENT_FLAG = 'astroContent';
+export const VIRTUAL_MODULE_ID = 'astro:content';
+export const LINKS_PLACEHOLDER = '@@ASTRO-LINKS@@';
+export const STYLES_PLACEHOLDER = '@@ASTRO-STYLES@@';
+
+export const CONTENT_BASE = 'types.generated';
+export const CONTENT_FILE = CONTENT_BASE + '.mjs';
+export const CONTENT_TYPES_FILE = CONTENT_BASE + '.d.ts';
diff --git a/packages/astro/src/content/index.ts b/packages/astro/src/content/index.ts
new file mode 100644
index 000000000..81c8b2d70
--- /dev/null
+++ b/packages/astro/src/content/index.ts
@@ -0,0 +1,6 @@
+export {
+ astroDelayedAssetPlugin,
+ astroBundleDelayedAssetPlugin,
+} from './vite-plugin-content-assets.js';
+export { astroContentServerPlugin } from './vite-plugin-content-server.js';
+export { astroContentVirtualModPlugin } from './vite-plugin-content-virtual-mod.js';
diff --git a/packages/astro/src/content/internal.ts b/packages/astro/src/content/internal.ts
new file mode 100644
index 000000000..f35307bd6
--- /dev/null
+++ b/packages/astro/src/content/internal.ts
@@ -0,0 +1,150 @@
+import { prependForwardSlash } from '../core/path.js';
+
+import {
+ createComponent,
+ createHeadAndContent,
+ renderComponent,
+ renderTemplate,
+ renderUniqueStylesheet,
+ renderStyleElement,
+ unescapeHTML,
+} from '../runtime/server/index.js';
+
+type GlobResult = Record Promise>;
+type CollectionToEntryMap = Record;
+
+export function createCollectionToGlobResultMap({
+ globResult,
+ contentDir,
+}: {
+ globResult: GlobResult;
+ contentDir: string;
+}) {
+ const collectionToGlobResultMap: CollectionToEntryMap = {};
+ for (const key in globResult) {
+ const keyRelativeToContentDir = key.replace(new RegExp(`^${contentDir}`), '');
+ const segments = keyRelativeToContentDir.split('/');
+ if (segments.length <= 1) continue;
+ const collection = segments[0];
+ const entryId = segments.slice(1).join('/');
+ collectionToGlobResultMap[collection] ??= {};
+ collectionToGlobResultMap[collection][entryId] = globResult[key];
+ }
+ return collectionToGlobResultMap;
+}
+
+export function createGetCollection({
+ collectionToEntryMap,
+ collectionToRenderEntryMap,
+}: {
+ collectionToEntryMap: CollectionToEntryMap;
+ collectionToRenderEntryMap: CollectionToEntryMap;
+}) {
+ return async function getCollection(collection: string, filter?: () => boolean) {
+ const lazyImports = Object.values(collectionToEntryMap[collection] ?? {});
+ const entries = Promise.all(
+ lazyImports.map(async (lazyImport) => {
+ const entry = await lazyImport();
+ return {
+ id: entry.id,
+ slug: entry.slug,
+ body: entry.body,
+ collection: entry.collection,
+ data: entry.data,
+ async render() {
+ return render({
+ collection: entry.collection,
+ id: entry.id,
+ collectionToRenderEntryMap,
+ });
+ },
+ };
+ })
+ );
+ if (typeof filter === 'function') {
+ return (await entries).filter(filter);
+ } else {
+ return entries;
+ }
+ };
+}
+
+export function createGetEntry({
+ collectionToEntryMap,
+ collectionToRenderEntryMap,
+}: {
+ collectionToEntryMap: CollectionToEntryMap;
+ collectionToRenderEntryMap: CollectionToEntryMap;
+}) {
+ return async function getEntry(collection: string, entryId: string) {
+ const lazyImport = collectionToEntryMap[collection]?.[entryId];
+ if (!lazyImport) throw new Error(`Failed to import ${JSON.stringify(entryId)}.`);
+
+ const entry = await lazyImport();
+ return {
+ id: entry.id,
+ slug: entry.slug,
+ body: entry.body,
+ collection: entry.collection,
+ data: entry.data,
+ async render() {
+ return render({
+ collection: entry.collection,
+ id: entry.id,
+ collectionToRenderEntryMap,
+ });
+ },
+ };
+ };
+}
+
+async function render({
+ collection,
+ id,
+ collectionToRenderEntryMap,
+}: {
+ collection: string;
+ id: string;
+ collectionToRenderEntryMap: CollectionToEntryMap;
+}) {
+ const lazyImport = collectionToRenderEntryMap[collection]?.[id];
+ if (!lazyImport) throw new Error(`${String(collection)} → ${String(id)} does not exist.`);
+
+ const mod = await lazyImport();
+
+ const Content = createComponent({
+ factory(result, props, slots) {
+ let styles = '',
+ links = '';
+ if (Array.isArray(mod?.collectedStyles)) {
+ styles = mod.collectedStyles.map((style: any) => renderStyleElement(style)).join('');
+ }
+ if (Array.isArray(mod?.collectedLinks)) {
+ links = mod.collectedLinks
+ .map((link: any) => {
+ return renderUniqueStylesheet(result, {
+ href: prependForwardSlash(link),
+ });
+ })
+ .join('');
+ }
+
+ return createHeadAndContent(
+ unescapeHTML(styles + links) as any,
+ renderTemplate`${renderComponent(result, 'Content', mod.Content, props, slots)}`
+ );
+ },
+ propagation: 'self',
+ });
+
+ if (!mod._internal && id.endsWith('.mdx')) {
+ throw new Error(
+ `[Content] Failed to render MDX entry. Try installing @astrojs/mdx@next--content-schemas`
+ );
+ }
+ return {
+ Content,
+ headings: mod.getHeadings(),
+ injectedFrontmatter: mod._internal.injectedFrontmatter,
+ };
+}
diff --git a/packages/astro/src/content/template/types.generated.d.ts b/packages/astro/src/content/template/types.generated.d.ts
new file mode 100644
index 000000000..fc1d489f0
--- /dev/null
+++ b/packages/astro/src/content/template/types.generated.d.ts
@@ -0,0 +1,49 @@
+declare module 'astro:content' {
+ export { z } from 'astro/zod';
+ export type CollectionEntry =
+ typeof entryMap[C][keyof typeof entryMap[C]] & Render;
+
+ type BaseCollectionConfig = {
+ schema?: S;
+ slug?: (entry: {
+ id: CollectionEntry['id'];
+ defaultSlug: string;
+ collection: string;
+ body: string;
+ data: import('astro/zod').infer>;
+ }) => string | Promise;
+ };
+ export function defineCollection(
+ input: BaseCollectionConfig
+ ): BaseCollectionConfig;
+
+ export function getEntry(
+ collection: C,
+ entryKey: E
+ ): Promise;
+ export function getCollection<
+ C extends keyof typeof entryMap,
+ E extends keyof typeof entryMap[C]
+ >(
+ collection: C,
+ filter?: (data: typeof entryMap[C][E]) => boolean
+ ): Promise<(typeof entryMap[C][E] & Render)[]>;
+
+ type InferEntrySchema = import('astro/zod').infer<
+ import('astro/zod').ZodObject['schema']>
+ >;
+
+ type Render = {
+ render(): Promise<{
+ Content: import('astro').MarkdownInstance<{}>['Content'];
+ headings: import('astro').MarkdownHeading[];
+ injectedFrontmatter: Record;
+ }>;
+ };
+
+ const entryMap: {
+ // @@ENTRY_MAP@@
+ };
+
+ type ContentConfig = '@@CONTENT_CONFIG_TYPE@@';
+}
diff --git a/packages/astro/src/content/template/types.generated.mjs b/packages/astro/src/content/template/types.generated.mjs
new file mode 100644
index 000000000..b8715d811
--- /dev/null
+++ b/packages/astro/src/content/template/types.generated.mjs
@@ -0,0 +1,40 @@
+// astro-head-inject
+import {
+ createGetCollection,
+ createGetEntry,
+ createCollectionToGlobResultMap,
+} from 'astro/content/internal';
+
+export { z } from 'astro/zod';
+
+export function defineCollection(config) {
+ return config;
+}
+
+const contentDir = '@@CONTENT_DIR@@';
+
+const entryGlob = import.meta.glob('@@ENTRY_GLOB_PATH@@', {
+ query: { astroContent: true },
+});
+const collectionToEntryMap = createCollectionToGlobResultMap({
+ globResult: entryGlob,
+ contentDir,
+});
+
+const renderEntryGlob = import.meta.glob('@@RENDER_ENTRY_GLOB_PATH@@', {
+ query: { astroAssetSsr: true },
+});
+const collectionToRenderEntryMap = createCollectionToGlobResultMap({
+ globResult: renderEntryGlob,
+ contentDir,
+});
+
+export const getCollection = createGetCollection({
+ collectionToEntryMap,
+ collectionToRenderEntryMap,
+});
+
+export const getEntry = createGetEntry({
+ collectionToEntryMap,
+ collectionToRenderEntryMap,
+});
diff --git a/packages/astro/src/content/types-generator.ts b/packages/astro/src/content/types-generator.ts
new file mode 100644
index 000000000..ffe3caefd
--- /dev/null
+++ b/packages/astro/src/content/types-generator.ts
@@ -0,0 +1,356 @@
+import { normalizePath } from 'vite';
+import glob from 'fast-glob';
+import fsMod from 'node:fs';
+import * as path from 'node:path';
+import { cyan } from 'kleur/colors';
+import { info, LogOptions, warn } from '../core/logger/core.js';
+import type { AstroSettings } from '../@types/astro.js';
+import { appendForwardSlash, isRelativePath } from '../core/path.js';
+import { contentFileExts, CONTENT_TYPES_FILE } from './consts.js';
+import { fileURLToPath, pathToFileURL } from 'node:url';
+import { ContentConfig, loadContentConfig, ContentPaths, ContentObservable } from './utils.js';
+
+type ChokidarEvent = 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir';
+type RawContentEvent = { name: ChokidarEvent; entry: string };
+type ContentEvent = { name: ChokidarEvent; entry: URL };
+type EntryInfo = {
+ id: string;
+ slug: string;
+ collection: string;
+};
+
+export type GenerateContentTypes = {
+ init(): Promise;
+ queueEvent(event: RawContentEvent): void;
+};
+
+type ContentTypesEntryMetadata = { slug: string };
+type ContentTypes = Record>;
+
+type CreateContentGeneratorParams = {
+ contentPaths: ContentPaths;
+ contentConfigObserver: ContentObservable;
+ logging: LogOptions;
+ settings: AstroSettings;
+ fs: typeof fsMod;
+};
+
+type EventOpts = { logLevel: 'info' | 'warn' };
+
+class UnsupportedFileTypeError extends Error {}
+
+export async function createContentTypesGenerator({
+ contentPaths,
+ contentConfigObserver,
+ fs,
+ logging,
+ settings,
+}: CreateContentGeneratorParams): Promise {
+ const contentTypes: ContentTypes = {};
+
+ let events: Promise<{ shouldGenerateTypes: boolean; error?: Error }>[] = [];
+ let debounceTimeout: NodeJS.Timeout | undefined;
+
+ const contentTypesBase = await fsMod.promises.readFile(
+ new URL(CONTENT_TYPES_FILE, contentPaths.generatedInputDir),
+ 'utf-8'
+ );
+
+ async function init() {
+ await handleEvent({ name: 'add', entry: contentPaths.config }, { logLevel: 'warn' });
+ const globResult = await glob('./**/*.*', {
+ cwd: fileURLToPath(contentPaths.contentDir),
+ fs: {
+ readdir: fs.readdir.bind(fs),
+ readdirSync: fs.readdirSync.bind(fs),
+ },
+ });
+ const entries = globResult
+ .map((e) => new URL(e, contentPaths.contentDir))
+ .filter(
+ // Config loading handled first. Avoid running twice.
+ (e) => !e.href.startsWith(contentPaths.config.href)
+ );
+ for (const entry of entries) {
+ events.push(handleEvent({ name: 'add', entry }, { logLevel: 'warn' }));
+ }
+ await runEvents();
+ }
+
+ async function handleEvent(
+ event: ContentEvent,
+ opts?: EventOpts
+ ): Promise<{ shouldGenerateTypes: boolean; error?: Error }> {
+ const logLevel = opts?.logLevel ?? 'info';
+
+ if (event.name === 'addDir' || event.name === 'unlinkDir') {
+ const collection = normalizePath(
+ path.relative(fileURLToPath(contentPaths.contentDir), fileURLToPath(event.entry))
+ );
+ // If directory is multiple levels deep, it is not a collection. Ignore event.
+ const isCollectionEvent = collection.split('/').length === 1;
+ if (!isCollectionEvent) return { shouldGenerateTypes: false };
+ switch (event.name) {
+ case 'addDir':
+ addCollection(contentTypes, JSON.stringify(collection));
+ if (logLevel === 'info') {
+ info(logging, 'content', `${cyan(collection)} collection added`);
+ }
+ break;
+ case 'unlinkDir':
+ removeCollection(contentTypes, JSON.stringify(collection));
+ break;
+ }
+ return { shouldGenerateTypes: true };
+ }
+ const fileType = getEntryType(fileURLToPath(event.entry), contentPaths);
+ if (fileType === 'generated-types') {
+ return { shouldGenerateTypes: false };
+ }
+ if (fileType === 'config') {
+ contentConfigObserver.set({ status: 'loading' });
+ const config = await loadContentConfig({ fs, settings });
+ if (config instanceof Error) {
+ contentConfigObserver.set({ status: 'error', error: config });
+ } else {
+ contentConfigObserver.set({ status: 'loaded', config });
+ }
+
+ return { shouldGenerateTypes: true };
+ }
+ const entryInfo = getEntryInfo({
+ entry: event.entry,
+ contentDir: contentPaths.contentDir,
+ });
+ // Not a valid `src/content/` entry. Silently return.
+ if (entryInfo instanceof Error) return { shouldGenerateTypes: false };
+ if (fileType === 'unknown') {
+ if (entryInfo.id.startsWith('_') && (event.name === 'add' || event.name === 'change')) {
+ // Silently ignore `_` files.
+ return { shouldGenerateTypes: false };
+ } else {
+ return {
+ shouldGenerateTypes: false,
+ error: new UnsupportedFileTypeError(entryInfo.id),
+ };
+ }
+ }
+ if (entryInfo.collection === '.') {
+ if (['info', 'warn'].includes(logLevel)) {
+ warn(
+ logging,
+ 'content',
+ `${cyan(
+ normalizePath(
+ path.relative(fileURLToPath(contentPaths.contentDir), fileURLToPath(event.entry))
+ )
+ )} must be nested in a collection directory. Skipping.`
+ );
+ }
+ return { shouldGenerateTypes: false };
+ }
+
+ const { id, slug, collection } = entryInfo;
+ const collectionKey = JSON.stringify(collection);
+ const entryKey = JSON.stringify(id);
+
+ switch (event.name) {
+ case 'add':
+ if (!(collectionKey in contentTypes)) {
+ addCollection(contentTypes, collectionKey);
+ }
+ if (!(entryKey in contentTypes[collectionKey])) {
+ addEntry(contentTypes, collectionKey, entryKey, slug);
+ }
+ return { shouldGenerateTypes: true };
+ case 'unlink':
+ if (collectionKey in contentTypes && entryKey in contentTypes[collectionKey]) {
+ removeEntry(contentTypes, collectionKey, entryKey);
+ }
+ return { shouldGenerateTypes: true };
+ case 'change':
+ // noop. Frontmatter types are inferred from collection schema import, so they won't change!
+ return { shouldGenerateTypes: false };
+ }
+ }
+
+ function queueEvent(rawEvent: RawContentEvent, opts?: EventOpts) {
+ const event = {
+ entry: pathToFileURL(rawEvent.entry),
+ name: rawEvent.name,
+ };
+ if (!event.entry.pathname.startsWith(contentPaths.contentDir.pathname)) return;
+
+ events.push(handleEvent(event, opts));
+
+ debounceTimeout && clearTimeout(debounceTimeout);
+ debounceTimeout = setTimeout(
+ async () => runEvents(opts),
+ 50 /* debounce to batch chokidar events */
+ );
+ }
+
+ async function runEvents(opts?: EventOpts) {
+ const logLevel = opts?.logLevel ?? 'info';
+ const eventResponses = await Promise.all(events);
+ events = [];
+ let unsupportedFiles = [];
+ for (const response of eventResponses) {
+ if (response.error instanceof UnsupportedFileTypeError) {
+ unsupportedFiles.push(response.error.message);
+ }
+ }
+ if (unsupportedFiles.length > 0 && ['info', 'warn'].includes(logLevel)) {
+ warn(
+ logging,
+ 'content',
+ `Unsupported file types found. Prefix with an underscore (\`_\`) to ignore:\n- ${unsupportedFiles.join(
+ '\n'
+ )}`
+ );
+ }
+ const observable = contentConfigObserver.get();
+ if (eventResponses.some((r) => r.shouldGenerateTypes)) {
+ await writeContentFiles({
+ fs,
+ contentTypes,
+ contentPaths,
+ contentTypesBase,
+ contentConfig: observable.status === 'loaded' ? observable.config : undefined,
+ });
+ if (observable.status === 'loaded' && ['info', 'warn'].includes(logLevel)) {
+ warnNonexistentCollections({
+ logging,
+ contentConfig: observable.config,
+ contentTypes,
+ });
+ }
+ }
+ }
+ return { init, queueEvent };
+}
+
+function addCollection(contentMap: ContentTypes, collectionKey: string) {
+ contentMap[collectionKey] = {};
+}
+
+function removeCollection(contentMap: ContentTypes, collectionKey: string) {
+ delete contentMap[collectionKey];
+}
+
+function addEntry(
+ contentTypes: ContentTypes,
+ collectionKey: string,
+ entryKey: string,
+ slug: string
+) {
+ contentTypes[collectionKey][entryKey] = { slug };
+}
+
+function removeEntry(contentTypes: ContentTypes, collectionKey: string, entryKey: string) {
+ delete contentTypes[collectionKey][entryKey];
+}
+
+export function getEntryInfo({
+ entry,
+ contentDir,
+}: Pick & { entry: URL }): EntryInfo | Error {
+ const rawRelativePath = path.relative(fileURLToPath(contentDir), fileURLToPath(entry));
+ const rawCollection = path.dirname(rawRelativePath).split(path.sep).shift();
+ if (!rawCollection) return new Error();
+
+ const rawId = path.relative(rawCollection, rawRelativePath);
+ const rawSlug = rawId.replace(path.extname(rawId), '');
+ const res = {
+ id: normalizePath(rawId),
+ slug: normalizePath(rawSlug),
+ collection: normalizePath(rawCollection),
+ };
+ return res;
+}
+
+export function getEntryType(
+ entryPath: string,
+ paths: ContentPaths
+): 'content' | 'config' | 'unknown' | 'generated-types' {
+ const { dir: rawDir, ext, name, base } = path.parse(entryPath);
+ const dir = appendForwardSlash(pathToFileURL(rawDir).href);
+ if ((contentFileExts as readonly string[]).includes(ext)) {
+ return 'content';
+ } else if (new URL(name, dir).pathname === paths.config.pathname) {
+ return 'config';
+ } else if (new URL(base, dir).pathname === new URL(CONTENT_TYPES_FILE, paths.cacheDir).pathname) {
+ return 'generated-types';
+ } else {
+ return 'unknown';
+ }
+}
+
+async function writeContentFiles({
+ fs,
+ contentPaths,
+ contentTypes,
+ contentTypesBase,
+ contentConfig,
+}: {
+ fs: typeof fsMod;
+ contentPaths: ContentPaths;
+ contentTypes: ContentTypes;
+ contentTypesBase: string;
+ contentConfig?: ContentConfig;
+}) {
+ let contentTypesStr = '';
+ const collectionKeys = Object.keys(contentTypes).sort();
+ for (const collectionKey of collectionKeys) {
+ const collectionConfig = contentConfig?.collections[JSON.parse(collectionKey)];
+ contentTypesStr += `${collectionKey}: {\n`;
+ const entryKeys = Object.keys(contentTypes[collectionKey]).sort();
+ for (const entryKey of entryKeys) {
+ const entryMetadata = contentTypes[collectionKey][entryKey];
+ const dataType = collectionConfig?.schema ? `InferEntrySchema<${collectionKey}>` : 'any';
+ // If user has custom slug function, we can't predict slugs at type compilation.
+ // Would require parsing all data and evaluating ahead-of-time;
+ // We evaluate with lazy imports at dev server runtime
+ // to prevent excessive errors
+ const slugType = collectionConfig?.slug ? 'string' : JSON.stringify(entryMetadata.slug);
+ contentTypesStr += `${entryKey}: {\n id: ${entryKey},\n slug: ${slugType},\n body: string,\n collection: ${collectionKey},\n data: ${dataType}\n},\n`;
+ }
+ contentTypesStr += `},\n`;
+ }
+
+ let configPathRelativeToCacheDir = normalizePath(
+ path.relative(contentPaths.cacheDir.pathname, contentPaths.config.pathname)
+ );
+ if (!isRelativePath(configPathRelativeToCacheDir))
+ configPathRelativeToCacheDir = './' + configPathRelativeToCacheDir;
+
+ contentTypesBase = contentTypesBase.replace('// @@ENTRY_MAP@@', contentTypesStr);
+ contentTypesBase = contentTypesBase.replace(
+ "'@@CONTENT_CONFIG_TYPE@@'",
+ contentConfig ? `typeof import(${JSON.stringify(configPathRelativeToCacheDir)})` : 'never'
+ );
+
+ await fs.promises.writeFile(new URL(CONTENT_TYPES_FILE, contentPaths.cacheDir), contentTypesBase);
+}
+
+function warnNonexistentCollections({
+ contentConfig,
+ contentTypes,
+ logging,
+}: {
+ contentConfig: ContentConfig;
+ contentTypes: ContentTypes;
+ logging: LogOptions;
+}) {
+ for (const configuredCollection in contentConfig.collections) {
+ if (!contentTypes[JSON.stringify(configuredCollection)]) {
+ warn(
+ logging,
+ 'content',
+ `${JSON.stringify(
+ configuredCollection
+ )} is not a collection. Check your content config for typos.`
+ );
+ }
+ }
+}
diff --git a/packages/astro/src/content/utils.ts b/packages/astro/src/content/utils.ts
new file mode 100644
index 000000000..15458c912
--- /dev/null
+++ b/packages/astro/src/content/utils.ts
@@ -0,0 +1,224 @@
+import type fsMod from 'node:fs';
+import matter from 'gray-matter';
+import { z } from 'zod';
+import { createServer, ErrorPayload as ViteErrorPayload, ViteDevServer } from 'vite';
+import { AstroSettings } from '../@types/astro.js';
+import { astroContentVirtualModPlugin } from './vite-plugin-content-virtual-mod.js';
+import { fileURLToPath } from 'node:url';
+import { AstroError, AstroErrorData } from '../core/errors/index.js';
+
+export const collectionConfigParser = z.object({
+ schema: z.any().optional(),
+ slug: z
+ .function()
+ .args(
+ z.object({
+ id: z.string(),
+ collection: z.string(),
+ defaultSlug: z.string(),
+ body: z.string(),
+ data: z.record(z.any()),
+ })
+ )
+ .returns(z.union([z.string(), z.promise(z.string())]))
+ .optional(),
+});
+
+export const contentConfigParser = z.object({
+ collections: z.record(collectionConfigParser),
+});
+
+export type CollectionConfig = z.infer;
+export type ContentConfig = z.infer;
+
+type Entry = {
+ id: string;
+ collection: string;
+ slug: string;
+ data: any;
+ body: string;
+ _internal: { rawData: string; filePath: string };
+};
+
+export const msg = {
+ collectionConfigMissing: (collection: string) =>
+ `${collection} does not have a config. We suggest adding one for type safety!`,
+};
+
+export async function getEntrySlug(entry: Entry, collectionConfig: CollectionConfig) {
+ return (
+ collectionConfig.slug?.({
+ id: entry.id,
+ data: entry.data,
+ defaultSlug: entry.slug,
+ collection: entry.collection,
+ body: entry.body,
+ }) ?? entry.slug
+ );
+}
+
+export async function getEntryData(entry: Entry, collectionConfig: CollectionConfig) {
+ let data = entry.data;
+ if (collectionConfig.schema) {
+ // Use `safeParseAsync` to allow async transforms
+ const parsed = await z.object(collectionConfig.schema).safeParseAsync(entry.data, { errorMap });
+ if (parsed.success) {
+ data = parsed.data;
+ } else {
+ const formattedError = new AstroError({
+ ...AstroErrorData.MarkdownContentSchemaValidationError,
+ message: AstroErrorData.MarkdownContentSchemaValidationError.message(
+ entry.collection,
+ entry.id,
+ parsed.error
+ ),
+ location: {
+ file: entry._internal.filePath,
+ line: getFrontmatterErrorLine(
+ entry._internal.rawData,
+ String(parsed.error.errors[0].path[0])
+ ),
+ column: 0,
+ },
+ });
+ throw formattedError;
+ }
+ }
+ return data;
+}
+
+const flattenPath = (path: (string | number)[]) => path.join('.');
+
+const errorMap: z.ZodErrorMap = (error, ctx) => {
+ if (error.code === 'invalid_type') {
+ const badKeyPath = JSON.stringify(flattenPath(error.path));
+ if (error.received === 'undefined') {
+ return { message: `${badKeyPath} is required.` };
+ } else {
+ return { message: `${badKeyPath} should be ${error.expected}, not ${error.received}.` };
+ }
+ }
+ return { message: ctx.defaultError };
+};
+
+function getFrontmatterErrorLine(rawFrontmatter: string, frontmatterKey: string) {
+ const indexOfFrontmatterKey = rawFrontmatter.indexOf(`\n${frontmatterKey}`);
+ if (indexOfFrontmatterKey === -1) return 0;
+
+ const frontmatterBeforeKey = rawFrontmatter.substring(0, indexOfFrontmatterKey + 1);
+ const numNewlinesBeforeKey = frontmatterBeforeKey.split('\n').length;
+ return numNewlinesBeforeKey;
+}
+
+/**
+ * Match YAML exception handling from Astro core errors
+ * @see 'astro/src/core/errors.ts'
+ */
+export function parseFrontmatter(fileContents: string, filePath: string) {
+ try {
+ // `matter` is empty string on cache results
+ // clear cache to prevent this
+ (matter as any).clearCache();
+ return matter(fileContents);
+ } catch (e: any) {
+ if (e.name === 'YAMLException') {
+ const err: Error & ViteErrorPayload['err'] = e;
+ err.id = filePath;
+ err.loc = { file: e.id, line: e.mark.line + 1, column: e.mark.column };
+ err.message = e.reason;
+ throw err;
+ } else {
+ throw e;
+ }
+ }
+}
+
+export class NotFoundError extends TypeError {}
+export class ZodParseError extends TypeError {}
+
+export async function loadContentConfig({
+ fs,
+ settings,
+}: {
+ fs: typeof fsMod;
+ settings: AstroSettings;
+}): Promise {
+ const contentPaths = getContentPaths({ srcDir: settings.config.srcDir });
+ const tempConfigServer: ViteDevServer = await createServer({
+ root: fileURLToPath(settings.config.root),
+ server: { middlewareMode: true, hmr: false },
+ optimizeDeps: { entries: [] },
+ clearScreen: false,
+ appType: 'custom',
+ logLevel: 'silent',
+ plugins: [astroContentVirtualModPlugin({ settings })],
+ });
+ let unparsedConfig;
+ try {
+ unparsedConfig = await tempConfigServer.ssrLoadModule(contentPaths.config.pathname);
+ } catch {
+ return new NotFoundError('Failed to resolve content config.');
+ } finally {
+ await tempConfigServer.close();
+ }
+ const config = contentConfigParser.safeParse(unparsedConfig);
+ if (config.success) {
+ return config.data;
+ } else {
+ return new ZodParseError('Content config file is invalid.');
+ }
+}
+
+type ContentCtx =
+ | { status: 'loading' }
+ | { status: 'loaded'; config: ContentConfig }
+ | { status: 'error'; error: NotFoundError | ZodParseError };
+
+type Observable = {
+ get: () => C;
+ set: (ctx: C) => void;
+ subscribe: (fn: (ctx: C) => void) => () => void;
+};
+
+export type ContentObservable = Observable;
+
+export function contentObservable(initialCtx: ContentCtx): ContentObservable {
+ type Subscriber = (ctx: ContentCtx) => void;
+ const subscribers = new Set();
+ let ctx = initialCtx;
+ function get() {
+ return ctx;
+ }
+ function set(_ctx: ContentCtx) {
+ ctx = _ctx;
+ subscribers.forEach((fn) => fn(ctx));
+ }
+ function subscribe(fn: Subscriber) {
+ subscribers.add(fn);
+ return () => {
+ subscribers.delete(fn);
+ };
+ }
+ return {
+ get,
+ set,
+ subscribe,
+ };
+}
+
+export type ContentPaths = {
+ contentDir: URL;
+ cacheDir: URL;
+ generatedInputDir: URL;
+ config: URL;
+};
+
+export function getContentPaths({ srcDir }: { srcDir: URL }): ContentPaths {
+ return {
+ // Output generated types in content directory. May change in the future!
+ cacheDir: new URL('./content/', srcDir),
+ contentDir: new URL('./content/', srcDir),
+ generatedInputDir: new URL('../../src/content/template/', import.meta.url),
+ config: new URL('./content/config', srcDir),
+ };
+}
diff --git a/packages/astro/src/content/vite-plugin-content-assets.ts b/packages/astro/src/content/vite-plugin-content-assets.ts
new file mode 100644
index 000000000..482edae99
--- /dev/null
+++ b/packages/astro/src/content/vite-plugin-content-assets.ts
@@ -0,0 +1,96 @@
+import type { Plugin } from 'vite';
+import type { ModuleLoader } from '../core/module-loader/loader.js';
+import { moduleIsTopLevelPage, walkParentInfos } from '../core/build/graph.js';
+import { BuildInternals, getPageDataByViteID } from '../core/build/internal.js';
+import { getStylesForURL } from '../core/render/dev/css.js';
+import { pathToFileURL } from 'url';
+import { createViteLoader } from '../core/module-loader/vite.js';
+import {
+ contentFileExts,
+ DELAYED_ASSET_FLAG,
+ LINKS_PLACEHOLDER,
+ STYLES_PLACEHOLDER,
+} from './consts.js';
+
+function isDelayedAsset(url: URL): boolean {
+ return (
+ url.searchParams.has(DELAYED_ASSET_FLAG) &&
+ contentFileExts.some((ext) => url.pathname.endsWith(ext))
+ );
+}
+
+export function astroDelayedAssetPlugin({ mode }: { mode: string }): Plugin {
+ let devModuleLoader: ModuleLoader;
+ return {
+ name: 'astro-delayed-asset-plugin',
+ enforce: 'pre',
+ configureServer(server) {
+ if (mode === 'dev') {
+ devModuleLoader = createViteLoader(server);
+ }
+ },
+ load(id) {
+ const url = new URL(id, 'file://');
+ if (isDelayedAsset(url)) {
+ const code = `
+ export { Content, getHeadings, _internal } from ${JSON.stringify(url.pathname)};
+ export const collectedLinks = ${JSON.stringify(LINKS_PLACEHOLDER)};
+ export const collectedStyles = ${JSON.stringify(STYLES_PLACEHOLDER)};
+ `;
+ return { code };
+ }
+ },
+ async transform(code, id, options) {
+ if (!options?.ssr) return;
+ const url = new URL(id, 'file://');
+ if (devModuleLoader && isDelayedAsset(url)) {
+ const { pathname } = url;
+ if (!devModuleLoader.getModuleById(pathname)?.ssrModule) {
+ await devModuleLoader.import(pathname);
+ }
+ const { stylesMap, urls } = await getStylesForURL(
+ pathToFileURL(pathname),
+ devModuleLoader,
+ 'development'
+ );
+
+ return {
+ code: code
+ .replace(JSON.stringify(LINKS_PLACEHOLDER), JSON.stringify([...urls]))
+ .replace(JSON.stringify(STYLES_PLACEHOLDER), JSON.stringify([...stylesMap.values()])),
+ };
+ }
+ },
+ };
+}
+
+export function astroBundleDelayedAssetPlugin({
+ internals,
+}: {
+ internals: BuildInternals;
+}): Plugin {
+ return {
+ name: 'astro-bundle-delayed-asset-plugin',
+ async generateBundle(_options, bundle) {
+ for (const [_, chunk] of Object.entries(bundle)) {
+ if (chunk.type === 'chunk' && chunk.code.includes(LINKS_PLACEHOLDER)) {
+ for (const id of Object.keys(chunk.modules)) {
+ for (const [pageInfo, depth, order] of walkParentInfos(id, this)) {
+ if (moduleIsTopLevelPage(pageInfo)) {
+ const pageViteID = pageInfo.id;
+ const pageData = getPageDataByViteID(internals, pageViteID);
+ if (!pageData) continue;
+ const entryCss = pageData.contentCollectionCss?.get(id);
+ if (!entryCss) continue;
+ chunk.code = chunk.code.replace(
+ JSON.stringify(LINKS_PLACEHOLDER),
+ JSON.stringify([...entryCss])
+ );
+ }
+ }
+ }
+ }
+ }
+ },
+ };
+}
diff --git a/packages/astro/src/content/vite-plugin-content-server.ts b/packages/astro/src/content/vite-plugin-content-server.ts
new file mode 100644
index 000000000..e347c807a
--- /dev/null
+++ b/packages/astro/src/content/vite-plugin-content-server.ts
@@ -0,0 +1,199 @@
+import type { Plugin } from 'vite';
+import fsMod from 'node:fs';
+import { cyan } from 'kleur/colors';
+import { info, LogOptions } from '../core/logger/core.js';
+import type { AstroSettings } from '../@types/astro.js';
+import { contentFileExts, CONTENT_FLAG } from './consts.js';
+import { escapeViteEnvReferences } from '../vite-plugin-utils/index.js';
+import {
+ getEntryData,
+ getEntrySlug,
+ getContentPaths,
+ contentObservable,
+ parseFrontmatter,
+ ContentPaths,
+ ContentConfig,
+} from './utils.js';
+import * as devalue from 'devalue';
+import {
+ createContentTypesGenerator,
+ GenerateContentTypes,
+ getEntryInfo,
+ getEntryType,
+} from './types-generator.js';
+import { pathToFileURL } from 'node:url';
+import { prependForwardSlash } from '../core/path.js';
+
+interface AstroContentServerPluginParams {
+ fs: typeof fsMod;
+ logging: LogOptions;
+ settings: AstroSettings;
+ mode: string;
+}
+
+export function astroContentServerPlugin({
+ fs,
+ settings,
+ logging,
+ mode,
+}: AstroContentServerPluginParams): Plugin[] {
+ const contentPaths: ContentPaths = getContentPaths({ srcDir: settings.config.srcDir });
+ let contentDirExists = false;
+ let contentGenerator: GenerateContentTypes;
+ const contentConfigObserver = contentObservable({ status: 'loading' });
+
+ return [
+ {
+ name: 'astro-content-server-plugin',
+ async config(viteConfig) {
+ try {
+ await fs.promises.stat(contentPaths.contentDir);
+ contentDirExists = true;
+ } catch {
+ /* silently move on */
+ return;
+ }
+
+ if (contentDirExists && (mode === 'dev' || viteConfig.build?.ssr === true)) {
+ contentGenerator = await createContentTypesGenerator({
+ fs,
+ settings,
+ logging,
+ contentConfigObserver,
+ contentPaths,
+ });
+ await contentGenerator.init();
+ info(logging, 'content', 'Types generated');
+ }
+ },
+ async configureServer(viteServer) {
+ if (mode !== 'dev') return;
+
+ if (contentDirExists) {
+ info(
+ logging,
+ 'content',
+ `Watching ${cyan(
+ contentPaths.contentDir.href.replace(settings.config.root.href, '')
+ )} for changes`
+ );
+ attachListeners();
+ } else {
+ viteServer.watcher.on('addDir', (dir) => {
+ if (pathToFileURL(dir).href === contentPaths.contentDir.href) {
+ info(logging, 'content', `Content dir found. Watching for changes`);
+ contentDirExists = true;
+ attachListeners();
+ }
+ });
+ }
+
+ function attachListeners() {
+ viteServer.watcher.on('add', (entry) => {
+ contentGenerator.queueEvent({ name: 'add', entry });
+ });
+ viteServer.watcher.on('addDir', (entry) =>
+ contentGenerator.queueEvent({ name: 'addDir', entry })
+ );
+ viteServer.watcher.on('change', (entry) =>
+ contentGenerator.queueEvent({ name: 'change', entry })
+ );
+ viteServer.watcher.on('unlink', (entry) => {
+ contentGenerator.queueEvent({ name: 'unlink', entry });
+ });
+ viteServer.watcher.on('unlinkDir', (entry) =>
+ contentGenerator.queueEvent({ name: 'unlinkDir', entry })
+ );
+ }
+ },
+ },
+ {
+ name: 'astro-content-flag-plugin',
+ async load(id) {
+ const fileUrl = new URL(prependForwardSlash(id), 'file://');
+ if (isContentFlagImport(fileUrl)) {
+ const observable = contentConfigObserver.get();
+ let contentConfig: ContentConfig | undefined =
+ observable.status === 'loaded' ? observable.config : undefined;
+ if (observable.status === 'loading') {
+ // Wait for config to load
+ contentConfig = await new Promise((resolve) => {
+ const unsubscribe = contentConfigObserver.subscribe((ctx) => {
+ if (ctx.status === 'loaded') {
+ resolve(ctx.config);
+ unsubscribe();
+ } else if (ctx.status === 'error') {
+ resolve(undefined);
+ unsubscribe();
+ }
+ });
+ });
+ }
+ const rawContents = await fs.promises.readFile(fileUrl, 'utf-8');
+ const {
+ content: body,
+ data: unparsedData,
+ matter: rawData = '',
+ } = parseFrontmatter(rawContents, fileUrl.pathname);
+ const entryInfo = getEntryInfo({
+ entry: fileUrl,
+ contentDir: contentPaths.contentDir,
+ });
+ if (entryInfo instanceof Error) return;
+
+ const _internal = { filePath: fileUrl.pathname, rawData };
+ const partialEntry = { data: unparsedData, body, _internal, ...entryInfo };
+ const collectionConfig = contentConfig?.collections[entryInfo.collection];
+ const data = collectionConfig
+ ? await getEntryData(partialEntry, collectionConfig)
+ : unparsedData;
+ const slug = collectionConfig
+ ? await getEntrySlug({ ...partialEntry, data }, collectionConfig)
+ : entryInfo.slug;
+
+ const code = escapeViteEnvReferences(`
+export const id = ${JSON.stringify(entryInfo.id)};
+export const collection = ${JSON.stringify(entryInfo.collection)};
+export const slug = ${JSON.stringify(slug)};
+export const body = ${JSON.stringify(body)};
+export const data = ${devalue.uneval(data) /* TODO: reuse astro props serializer */};
+export const _internal = {
+ filePath: ${JSON.stringify(fileUrl.pathname)},
+ rawData: ${JSON.stringify(rawData)},
+};
+`);
+ return { code };
+ }
+ },
+ configureServer(viteServer) {
+ viteServer.watcher.on('all', async (event, entry) => {
+ if (
+ ['add', 'unlink', 'change'].includes(event) &&
+ getEntryType(entry, contentPaths) === 'config'
+ ) {
+ // Content modules depend on config, so we need to invalidate them.
+ for (const modUrl of viteServer.moduleGraph.urlToModuleMap.keys()) {
+ if (isContentFlagImport(new URL(modUrl, 'file://'))) {
+ const mod = await viteServer.moduleGraph.getModuleByUrl(modUrl);
+ if (mod) {
+ viteServer.moduleGraph.invalidateModule(mod);
+ }
+ }
+ }
+ }
+ });
+ },
+ async transform(code, id) {
+ if (isContentFlagImport(new URL(id, 'file://'))) {
+ // Escape before Rollup internal transform.
+ // Base on MUCH trial-and-error, inspired by MDX integration 2-step transform.
+ return { code: escapeViteEnvReferences(code) };
+ }
+ },
+ },
+ ];
+}
+
+function isContentFlagImport({ searchParams, pathname }: Pick) {
+ return searchParams.has(CONTENT_FLAG) && contentFileExts.some((ext) => pathname.endsWith(ext));
+}
diff --git a/packages/astro/src/content/vite-plugin-content-virtual-mod.ts b/packages/astro/src/content/vite-plugin-content-virtual-mod.ts
new file mode 100644
index 000000000..8c8bc5441
--- /dev/null
+++ b/packages/astro/src/content/vite-plugin-content-virtual-mod.ts
@@ -0,0 +1,48 @@
+import type { Plugin } from 'vite';
+import { normalizePath } from 'vite';
+import fsMod from 'node:fs';
+import * as path from 'node:path';
+import type { AstroSettings } from '../@types/astro.js';
+import { appendForwardSlash, prependForwardSlash } from '../core/path.js';
+import { contentFileExts, CONTENT_FILE, VIRTUAL_MODULE_ID } from './consts.js';
+import { getContentPaths } from './utils.js';
+
+interface AstroContentVirtualModPluginParams {
+ settings: AstroSettings;
+}
+
+export function astroContentVirtualModPlugin({
+ settings,
+}: AstroContentVirtualModPluginParams): Plugin {
+ const paths = getContentPaths({ srcDir: settings.config.srcDir });
+ const relContentDir = normalizePath(
+ appendForwardSlash(
+ prependForwardSlash(path.relative(settings.config.root.pathname, paths.contentDir.pathname))
+ )
+ );
+ const entryGlob = `${relContentDir}**/*{${contentFileExts.join(',')}}`;
+ const astroContentModContents = fsMod
+ .readFileSync(new URL(CONTENT_FILE, paths.generatedInputDir), 'utf-8')
+ .replace('@@CONTENT_DIR@@', relContentDir)
+ .replace('@@ENTRY_GLOB_PATH@@', entryGlob)
+ .replace('@@RENDER_ENTRY_GLOB_PATH@@', entryGlob);
+
+ const astroContentVirtualModuleId = '\0' + VIRTUAL_MODULE_ID;
+
+ return {
+ name: 'astro-content-virtual-mod-plugin',
+ enforce: 'pre',
+ resolveId(id) {
+ if (id === VIRTUAL_MODULE_ID) {
+ return astroContentVirtualModuleId;
+ }
+ },
+ load(id) {
+ if (id === astroContentVirtualModuleId) {
+ return {
+ code: astroContentModContents,
+ };
+ }
+ },
+ };
+}
diff --git a/packages/astro/src/core/build/graph.ts b/packages/astro/src/core/build/graph.ts
index 2f7deac7f..5edb07a71 100644
--- a/packages/astro/src/core/build/graph.ts
+++ b/packages/astro/src/core/build/graph.ts
@@ -6,6 +6,7 @@ import { resolvedPagesVirtualModuleId } from '../app/index.js';
export function* walkParentInfos(
id: string,
ctx: { getModuleInfo: GetModuleInfo },
+ until?: (importer: string) => boolean,
depth = 0,
order = 0,
seen = new Set(),
@@ -19,12 +20,13 @@ export function* walkParentInfos(
}
yield [info, depth, order];
}
+ if (until?.(id)) return;
const importers = (info?.importers || []).concat(info?.dynamicImporters || []);
for (const imp of importers) {
if (seen.has(imp)) {
continue;
}
- yield* walkParentInfos(imp, ctx, ++depth, order, seen, id);
+ yield* walkParentInfos(imp, ctx, until, ++depth, order, seen, id);
}
}
diff --git a/packages/astro/src/core/build/page-data.ts b/packages/astro/src/core/build/page-data.ts
index ed4bf4399..1a03b934b 100644
--- a/packages/astro/src/core/build/page-data.ts
+++ b/packages/astro/src/core/build/page-data.ts
@@ -54,6 +54,7 @@ export async function collectPagesData(
route,
moduleSpecifier: '',
css: new Map(),
+ contentCollectionCss: new Map(),
hoistedScript: undefined,
};
@@ -75,6 +76,7 @@ export async function collectPagesData(
route,
moduleSpecifier: '',
css: new Map(),
+ contentCollectionCss: new Map(),
hoistedScript: undefined,
};
}
diff --git a/packages/astro/src/core/build/static-build.ts b/packages/astro/src/core/build/static-build.ts
index c184652bf..22d1ee028 100644
--- a/packages/astro/src/core/build/static-build.ts
+++ b/packages/astro/src/core/build/static-build.ts
@@ -14,6 +14,7 @@ import { emptyDir, removeDir } from '../../core/fs/index.js';
import { prependForwardSlash } from '../../core/path.js';
import { isModeServerWithNoAdapter } from '../../core/util.js';
import { runHookBuildSetup } from '../../integrations/index.js';
+import { astroBundleDelayedAssetPlugin } from '../../content/index.js';
import { PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js';
import { AstroError, AstroErrorData } from '../errors/index.js';
import { info } from '../logger/core.js';
@@ -168,6 +169,8 @@ async function ssrBuild(opts: StaticBuildOptions, internals: BuildInternals, inp
}),
vitePluginPrerender(opts, internals),
...(viteConfig.plugins || []),
+ settings.config.experimental.contentCollections &&
+ astroBundleDelayedAssetPlugin({ internals }),
// SSR needs to be last
ssr && vitePluginSSR(internals, settings.adapter!),
],
diff --git a/packages/astro/src/core/build/types.ts b/packages/astro/src/core/build/types.ts
index 57518fb42..0c4d9a069 100644
--- a/packages/astro/src/core/build/types.ts
+++ b/packages/astro/src/core/build/types.ts
@@ -21,6 +21,7 @@ export interface PageBuildData {
route: RouteData;
moduleSpecifier: string;
css: Map;
+ contentCollectionCss: Map>;
hoistedScript: { type: 'inline' | 'external'; value: string } | undefined;
}
export type AllPagesData = Record;
diff --git a/packages/astro/src/core/build/vite-plugin-css.ts b/packages/astro/src/core/build/vite-plugin-css.ts
index 8183ff1dd..d4ddbaf16 100644
--- a/packages/astro/src/core/build/vite-plugin-css.ts
+++ b/packages/astro/src/core/build/vite-plugin-css.ts
@@ -1,12 +1,13 @@
import type { GetModuleInfo } from 'rollup';
import type { BuildInternals } from './internal';
import type { PageBuildData, StaticBuildOptions } from './types';
-
+import * as crypto from 'node:crypto';
+import * as npath from 'node:path';
import { Plugin as VitePlugin, ResolvedConfig, transformWithEsbuild } from 'vite';
import { isCSSRequest } from '../render/util.js';
import * as assetName from './css-asset-name.js';
-import { moduleIsTopLevelPage, walkParentInfos } from './graph.js';
+import { getTopLevelPages, moduleIsTopLevelPage, walkParentInfos } from './graph.js';
import {
eachPageData,
getPageDataByViteID,
@@ -14,6 +15,7 @@ import {
getPageDatasByHoistedScriptId,
isHoistedScript,
} from './internal.js';
+import { DELAYED_ASSET_FLAG } from '../../content/consts.js';
interface PluginOptions {
internals: BuildInternals;
@@ -27,6 +29,17 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[]
let resolvedConfig: ResolvedConfig;
+ function createNameHash(baseId: string, hashIds: string[]): string {
+ const baseName = baseId ? npath.parse(baseId).name : 'index';
+ const hash = crypto.createHash('sha256');
+ for (const id of hashIds) {
+ hash.update(id, 'utf-8');
+ }
+ const h = hash.digest('hex').slice(0, 8);
+ const proposedName = baseName + '.' + h;
+ return proposedName;
+ }
+
function* getParentClientOnlys(
id: string,
ctx: { getModuleInfo: GetModuleInfo }
@@ -63,6 +76,17 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[]
// For CSS, create a hash of all of the pages that use it.
// This causes CSS to be built into shared chunks when used by multiple pages.
if (isCSSRequest(id)) {
+ if (settings.config.experimental.contentCollections) {
+ for (const [pageInfo] of walkParentInfos(id, {
+ getModuleInfo: args[0].getModuleInfo,
+ })) {
+ if (new URL(pageInfo.id, 'file://').searchParams.has(DELAYED_ASSET_FLAG)) {
+ // Split delayed assets to separate modules
+ // so they can be injected where needed
+ return createNameHash(id, [id]);
+ }
+ }
+ }
return createNameForParentPages(id, args[0]);
}
};
@@ -105,6 +129,7 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[]
for (const [_, chunk] of Object.entries(bundle)) {
if (chunk.type === 'chunk') {
const c = chunk;
+
if ('viteMetadata' in chunk) {
const meta = chunk['viteMetadata'] as ViteMetadata;
@@ -145,8 +170,39 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[]
// For this CSS chunk, walk parents until you find a page. Add the CSS to that page.
for (const id of Object.keys(c.modules)) {
- for (const [pageInfo, depth, order] of walkParentInfos(id, this)) {
- if (moduleIsTopLevelPage(pageInfo)) {
+ for (const [pageInfo, depth, order] of walkParentInfos(
+ id,
+ this,
+ function until(importer) {
+ if (settings.config.experimental.contentCollections) {
+ // Short circuit when `contentCollections` is enabled.
+ return new URL(importer, 'file://').searchParams.has(DELAYED_ASSET_FLAG);
+ }
+ return false;
+ }
+ )) {
+ if (
+ settings.config.experimental.contentCollections &&
+ new URL(pageInfo.id, 'file://').searchParams.has(DELAYED_ASSET_FLAG)
+ ) {
+ for (const parent of walkParentInfos(id, this)) {
+ const parentInfo = parent[0];
+ if (moduleIsTopLevelPage(parentInfo)) {
+ const pageViteID = parentInfo.id;
+ const pageData = getPageDataByViteID(internals, pageViteID);
+ if (pageData) {
+ for (const css of meta.importedCss) {
+ const existingCss =
+ pageData.contentCollectionCss.get(pageInfo.id) ?? new Set();
+ pageData.contentCollectionCss.set(
+ pageInfo.id,
+ new Set([...existingCss, css])
+ );
+ }
+ }
+ }
+ }
+ } else if (moduleIsTopLevelPage(pageInfo)) {
const pageViteID = pageInfo.id;
const pageData = getPageDataByViteID(internals, pageViteID);
if (pageData) {
diff --git a/packages/astro/src/core/config/config.ts b/packages/astro/src/core/config/config.ts
index 6616a8213..7ac8ca110 100644
--- a/packages/astro/src/core/config/config.ts
+++ b/packages/astro/src/core/config/config.ts
@@ -106,6 +106,10 @@ export function resolveFlags(flags: Partial): CLIFlags {
: undefined,
experimentalPrerender:
typeof flags.experimentalPrerender === 'boolean' ? flags.experimentalPrerender : undefined,
+ experimentalContentCollections:
+ typeof flags.experimentalContentCollections === 'boolean'
+ ? flags.experimentalContentCollections
+ : undefined,
};
}
@@ -136,6 +140,7 @@ function mergeCLIFlags(astroConfig: AstroUserConfig, flags: CLIFlags, cmd: strin
}
if (flags.experimentalErrorOverlay) astroConfig.experimental.errorOverlay = true;
if (flags.experimentalPrerender) astroConfig.experimental.prerender = true;
+ if (flags.experimentalContentCollections) astroConfig.experimental.contentCollections = true;
return astroConfig;
}
diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts
index 4c119a859..a138f9771 100644
--- a/packages/astro/src/core/config/schema.ts
+++ b/packages/astro/src/core/config/schema.ts
@@ -50,6 +50,7 @@ const ASTRO_CONFIG_DEFAULTS: AstroUserConfig & any = {
experimental: {
errorOverlay: false,
prerender: false,
+ contentCollections: false,
},
};
@@ -195,6 +196,10 @@ export const AstroConfigSchema = z.object({
.object({
errorOverlay: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.errorOverlay),
prerender: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.experimental.prerender),
+ contentCollections: z
+ .boolean()
+ .optional()
+ .default(ASTRO_CONFIG_DEFAULTS.experimental.contentCollections),
})
.optional()
.default({}),
diff --git a/packages/astro/src/core/create-vite.ts b/packages/astro/src/core/create-vite.ts
index a54e3ea7e..57130fa4f 100644
--- a/packages/astro/src/core/create-vite.ts
+++ b/packages/astro/src/core/create-vite.ts
@@ -22,6 +22,11 @@ import astroScriptsPlugin from '../vite-plugin-scripts/index.js';
import astroScriptsPageSSRPlugin from '../vite-plugin-scripts/page-ssr.js';
import { createCustomViteLogger } from './errors/dev/index.js';
import { resolveDependency } from './util.js';
+import {
+ astroContentServerPlugin,
+ astroContentVirtualModPlugin,
+ astroDelayedAssetPlugin,
+} from '../content/index.js';
interface CreateViteOptions {
settings: AstroSettings;
@@ -116,6 +121,13 @@ export async function createVite(
astroScriptsPageSSRPlugin({ settings }),
astroHeadPropagationPlugin({ settings }),
settings.config.experimental.prerender && astroScannerPlugin({ settings, logging }),
+ ...(settings.config.experimental.contentCollections
+ ? [
+ astroContentVirtualModPlugin({ settings }),
+ astroContentServerPlugin({ fs, settings, logging, mode }),
+ astroDelayedAssetPlugin({ mode }),
+ ]
+ : []),
],
publicDir: fileURLToPath(settings.config.publicDir),
root: fileURLToPath(settings.config.root),
diff --git a/packages/astro/src/core/errors/errors-data.ts b/packages/astro/src/core/errors/errors-data.ts
index 959bdde28..c866d29ab 100644
--- a/packages/astro/src/core/errors/errors-data.ts
+++ b/packages/astro/src/core/errors/errors-data.ts
@@ -2,6 +2,8 @@
// Additionally, this code, much like `@types/astro.ts`, is used to generate documentation, so make sure to pass
// your changes by our wonderful docs team before merging!
+import type { ZodError } from 'zod';
+
interface ErrorData {
code: number;
title: string;
@@ -491,6 +493,30 @@ See https://docs.astro.build/en/guides/server-side-rendering/ for more informati
title: 'Failed to parse Markdown frontmatter.',
code: 6001,
},
+ /**
+ * @docs
+ * @message
+ * **Example error message:**
+ * Could not parse frontmatter in **blog** → **post.md**
+ * "title" is required.
+ * "date" must be a valid date.
+ * @description
+ * A Markdown document's frontmatter in `src/content/` does not match its collection schema.
+ * Make sure that all required fields are present, and that all fields are of the correct type.
+ * You can check against the collection schema in your `src/content/config.*` file.
+ * See the [Content collections documentation](https://docs.astro.build/en/guides/content-collections) for more information.
+ */
+ MarkdownContentSchemaValidationError: {
+ title: 'Content collection frontmatter invalid.',
+ code: 6002,
+ message: (collection: string, entryId: string, error: ZodError) => {
+ return [
+ `${String(collection)} → ${String(entryId)} frontmatter does not match collection schema.`,
+ ...error.errors.map((zodError) => zodError.message),
+ ].join('\n');
+ },
+ hint: 'See https://docs.astro.build/en/guides/content-collections for more information on content schemas.',
+ },
// Config Errors - 7xxx
UnknownConfigError: {
title: 'Unknown configuration error.',
diff --git a/packages/astro/src/core/render/dev/vite.ts b/packages/astro/src/core/render/dev/vite.ts
index ce864c6b4..86659f85a 100644
--- a/packages/astro/src/core/render/dev/vite.ts
+++ b/packages/astro/src/core/render/dev/vite.ts
@@ -2,6 +2,7 @@ import type { ModuleLoader, ModuleNode } from '../../module-loader/index';
import npath from 'path';
import { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from '../../constants.js';
+import { DELAYED_ASSET_FLAG } from '../../../content/consts.js';
import { unwrapId } from '../../util.js';
import { STYLE_EXTENSIONS } from '../util.js';
@@ -22,6 +23,8 @@ export async function* crawlGraph(
): AsyncGenerator {
const id = unwrapId(_id);
const importedModules = new Set();
+ if (new URL(id, 'file://').searchParams.has(DELAYED_ASSET_FLAG)) return;
+
const moduleEntriesForId = isRootFile
? // "getModulesByFile" pulls from a delayed module cache (fun implementation detail),
// So we can get up-to-date info on initial server load.
diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts
index 20fb5d5a7..2a4099074 100644
--- a/packages/astro/src/core/render/result.ts
+++ b/packages/astro/src/core/render/result.ts
@@ -99,7 +99,10 @@ class Slots {
`Expected second parameter to be an array, received a ${typeof args}. If you're trying to pass an array as a single argument and getting unexpected results, make sure you're passing your array as a item of an array. Ex: Astro.slots.render('default', [["Hello", "World"]])`
);
} else if (args.length > 0) {
- const component = await this.#slots[name]();
+ const slotValue = this.#slots[name];
+ const component = typeof slotValue === 'function' ?
+ await slotValue() :
+ await slotValue;
// Astro
const expression = getFunctionExpression(component);
diff --git a/packages/astro/src/runtime/server/astro-component.ts b/packages/astro/src/runtime/server/astro-component.ts
index f3df3a78f..bda81ba3f 100644
--- a/packages/astro/src/runtime/server/astro-component.ts
+++ b/packages/astro/src/runtime/server/astro-component.ts
@@ -20,7 +20,7 @@ function createComponentWithOptions(opts: CreateComponentOptions) {
return cb;
}
// Used in creating the component. aka the main export.
-export function createComponent(arg1: AstroComponentFactory, moduleId: string) {
+export function createComponent(arg1: AstroComponentFactory | CreateComponentOptions, moduleId?: string) {
if (typeof arg1 === 'function') {
return baseCreateComponent(arg1, moduleId);
} else {
diff --git a/packages/astro/src/runtime/server/index.ts b/packages/astro/src/runtime/server/index.ts
index 7348ea2ed..b4f402638 100644
--- a/packages/astro/src/runtime/server/index.ts
+++ b/packages/astro/src/runtime/server/index.ts
@@ -21,12 +21,15 @@ export {
renderTemplate,
renderToString,
renderUniqueStylesheet,
+ renderStyleElement,
stringifyChunk,
voidElementNames,
} from './render/index.js';
export type {
AstroComponentFactory,
AstroComponentInstance,
+ AstroComponentSlots,
+ AstroComponentSlotsWithValues,
RenderInstruction,
} from './render/index.js';
diff --git a/packages/astro/src/runtime/server/render/astro/factory.ts b/packages/astro/src/runtime/server/render/astro/factory.ts
index ec66edd55..0d8ffb787 100644
--- a/packages/astro/src/runtime/server/render/astro/factory.ts
+++ b/packages/astro/src/runtime/server/render/astro/factory.ts
@@ -12,7 +12,7 @@ export type AstroFactoryReturnValue = RenderTemplateResult | Response | HeadAndC
export interface AstroComponentFactory {
(result: any, props: any, slots: any): AstroFactoryReturnValue;
isAstroComponentFactory?: boolean;
- moduleId: string | undefined;
+ moduleId?: string | undefined;
propagation?: PropagationHint;
}
diff --git a/packages/astro/src/runtime/server/render/astro/index.ts b/packages/astro/src/runtime/server/render/astro/index.ts
index cbddf7876..03d97b148 100644
--- a/packages/astro/src/runtime/server/render/astro/index.ts
+++ b/packages/astro/src/runtime/server/render/astro/index.ts
@@ -1,7 +1,7 @@
export type { AstroComponentFactory } from './factory';
export { isAstroComponentFactory, renderToString } from './factory.js';
export { createHeadAndContent, isHeadAndContent } from './head-and-content.js';
-export type { AstroComponentInstance } from './instance';
+export type { AstroComponentInstance, ComponentSlots, ComponentSlotsWithValues } from './instance';
export { createAstroComponentInstance, isAstroComponentInstance } from './instance.js';
export {
isRenderTemplateResult,
diff --git a/packages/astro/src/runtime/server/render/astro/instance.ts b/packages/astro/src/runtime/server/render/astro/instance.ts
index 6e1cc465a..dfe855475 100644
--- a/packages/astro/src/runtime/server/render/astro/instance.ts
+++ b/packages/astro/src/runtime/server/render/astro/instance.ts
@@ -1,5 +1,6 @@
import type { SSRResult } from '../../../../@types/astro';
import type { AstroComponentFactory, AstroFactoryReturnValue } from './factory.js';
+import type { renderTemplate } from './render-template.js';
import { HydrationDirectiveProps } from '../../hydration.js';
import { isPromise } from '../../util.js';
@@ -8,6 +9,9 @@ import { isAPropagatingComponent } from './factory.js';
import { isHeadAndContent } from './head-and-content.js';
type ComponentProps = Record;
+type ComponentSlotValue = () => ReturnType;
+export type ComponentSlots = Record;
+export type ComponentSlotsWithValues = Record>;
const astroComponentInstanceSym = Symbol.for('astro.componentInstance');
@@ -16,23 +20,26 @@ export class AstroComponentInstance {
private readonly result: SSRResult;
private readonly props: ComponentProps;
- private readonly slots: any;
+ private readonly slotValues: ComponentSlotsWithValues;
private readonly factory: AstroComponentFactory;
private returnValue: ReturnType | undefined;
constructor(
result: SSRResult,
props: ComponentProps,
- slots: any,
+ slots: ComponentSlots,
factory: AstroComponentFactory
) {
this.result = result;
this.props = props;
- this.slots = slots;
this.factory = factory;
+ this.slotValues = {};
+ for(const name in slots) {
+ this.slotValues[name] = slots[name]();
+ }
}
async init() {
- this.returnValue = this.factory(this.result, this.props, this.slots);
+ this.returnValue = this.factory(this.result, this.props, this.slotValues);
return this.returnValue;
}
diff --git a/packages/astro/src/runtime/server/render/index.ts b/packages/astro/src/runtime/server/render/index.ts
index 776058d44..946d6d241 100644
--- a/packages/astro/src/runtime/server/render/index.ts
+++ b/packages/astro/src/runtime/server/render/index.ts
@@ -1,4 +1,9 @@
-export type { AstroComponentFactory, AstroComponentInstance } from './astro/index';
+export type {
+ AstroComponentFactory,
+ AstroComponentInstance,
+ ComponentSlots as AstroComponentSlots,
+ ComponentSlotsWithValues as AstroComponentSlotsWithValues
+} from './astro/index';
export {
createHeadAndContent,
renderAstroTemplateResult,
@@ -11,6 +16,7 @@ export { renderHTMLElement } from './dom.js';
export { maybeRenderHead, renderHead } from './head.js';
export { renderPage } from './page.js';
export { renderSlot } from './slot.js';
-export { renderUniqueStylesheet } from './stylesheet.js';
+export { renderUniqueStylesheet, renderStyleElement } from './tags.js';
export type { RenderInstruction } from './types';
export { addAttribute, defineScriptVars, voidElementNames } from './util.js';
+
diff --git a/packages/astro/src/runtime/server/render/stylesheet.ts b/packages/astro/src/runtime/server/render/tags.ts
similarity index 67%
rename from packages/astro/src/runtime/server/render/stylesheet.ts
rename to packages/astro/src/runtime/server/render/tags.ts
index a492e6492..61b31237d 100644
--- a/packages/astro/src/runtime/server/render/stylesheet.ts
+++ b/packages/astro/src/runtime/server/render/tags.ts
@@ -1,22 +1,26 @@
import { SSRResult } from '../../../@types/astro';
-import { markHTMLString } from '../escape.js';
import { renderElement } from './util.js';
const stylesheetRel = 'stylesheet';
+export function renderStyleElement(children: string) {
+ return renderElement('style', {
+ props: {},
+ children
+ })
+}
+
export function renderStylesheet({ href }: { href: string }) {
- return markHTMLString(
- renderElement(
- 'link',
- {
- props: {
- rel: stylesheetRel,
- href,
- },
- children: '',
+ return renderElement(
+ 'link',
+ {
+ props: {
+ rel: stylesheetRel,
+ href,
},
- false
- )
+ children: '',
+ },
+ false
);
}
diff --git a/packages/astro/src/vite-plugin-markdown/index.ts b/packages/astro/src/vite-plugin-markdown/index.ts
index 17d459570..66955dca5 100644
--- a/packages/astro/src/vite-plugin-markdown/index.ts
+++ b/packages/astro/src/vite-plugin-markdown/index.ts
@@ -10,7 +10,11 @@ import type { LogOptions } from '../core/logger/core.js';
import { warn } from '../core/logger/core.js';
import { isMarkdownFile } from '../core/util.js';
import type { PluginMetadata } from '../vite-plugin-astro/types.js';
-import { getFileInfo, safelyGetAstroData } from '../vite-plugin-utils/index.js';
+import {
+ escapeViteEnvReferences,
+ getFileInfo,
+ safelyGetAstroData,
+} from '../vite-plugin-utils/index.js';
interface AstroPluginOptions {
settings: AstroSettings;
@@ -93,6 +97,9 @@ export default function markdown({ settings, logging }: AstroPluginOptions): Plu
const html = ${JSON.stringify(html)};
+ export const _internal = {
+ injectedFrontmatter: ${JSON.stringify(injectedFrontmatter)},
+ }
export const frontmatter = ${JSON.stringify(frontmatter)};
export const file = ${JSON.stringify(fileId)};
export const url = ${JSON.stringify(fileUrl)};
@@ -169,10 +176,3 @@ export default function markdown({ settings, logging }: AstroPluginOptions): Plu
},
};
}
-
-// Converts the first dot in `import.meta.env` to its Unicode escape sequence,
-// which prevents Vite from replacing strings like `import.meta.env.SITE`
-// in our JS representation of loaded Markdown files
-function escapeViteEnvReferences(code: string) {
- return code.replace(/import\.meta\.env/g, 'import\\u002Emeta.env');
-}
diff --git a/packages/astro/src/vite-plugin-scripts/page-ssr.ts b/packages/astro/src/vite-plugin-scripts/page-ssr.ts
index 514382521..cb976cb96 100644
--- a/packages/astro/src/vite-plugin-scripts/page-ssr.ts
+++ b/packages/astro/src/vite-plugin-scripts/page-ssr.ts
@@ -1,7 +1,6 @@
import { Plugin as VitePlugin } from 'vite';
import { AstroSettings } from '../@types/astro.js';
import { PAGE_SSR_SCRIPT_ID } from './index.js';
-
import MagicString from 'magic-string';
import { isPage } from '../core/util.js';
import { normalizeFilename } from '../vite-plugin-utils/index.js';
@@ -14,7 +13,6 @@ export default function astroScriptsPostPlugin({
return {
name: 'astro:scripts:page-ssr',
enforce: 'post',
-
transform(this, code, id, options) {
if (!options?.ssr) return;
diff --git a/packages/astro/src/vite-plugin-utils/index.ts b/packages/astro/src/vite-plugin-utils/index.ts
index 02f388eb0..51479380e 100644
--- a/packages/astro/src/vite-plugin-utils/index.ts
+++ b/packages/astro/src/vite-plugin-utils/index.ts
@@ -1,5 +1,5 @@
import ancestor from 'common-ancestor-path';
-import { Data } from 'vfile';
+import type { Data } from 'vfile';
import type { AstroConfig, MarkdownAstroData } from '../@types/astro';
import {
appendExtension,
@@ -7,6 +7,15 @@ import {
removeLeadingForwardSlashWindows,
} from '../core/path.js';
+/**
+ * Converts the first dot in `import.meta.env` to its Unicode escape sequence,
+ * which prevents Vite from replacing strings like `import.meta.env.SITE`
+ * in our JS representation of modules like Markdown
+ */
+export function escapeViteEnvReferences(code: string) {
+ return code.replace(/import\.meta\.env/g, 'import\\u002Emeta.env');
+}
+
export function getFileInfo(id: string, config: AstroConfig) {
const sitePathname = appendForwardSlash(
config.site ? new URL(config.base, config.site).pathname : config.base
diff --git a/packages/astro/test/content-collections.test.js b/packages/astro/test/content-collections.test.js
new file mode 100644
index 000000000..d52521959
--- /dev/null
+++ b/packages/astro/test/content-collections.test.js
@@ -0,0 +1,189 @@
+import { expect } from 'chai';
+import { loadFixture } from './test-utils.js';
+import testAdapter from './test-adapter.js';
+import * as devalue from 'devalue';
+import * as cheerio from 'cheerio';
+
+describe('Content Collections', () => {
+ describe('Query', () => {
+ let fixture;
+ before(async () => {
+ fixture = await loadFixture({ root: './fixtures/content-collections/' });
+ await fixture.build();
+ });
+
+ describe('Collection', () => {
+ let json;
+ before(async () => {
+ const rawJson = await fixture.readFile('/collections.json');
+ json = devalue.parse(rawJson);
+ });
+
+ it('Returns `without config` collection', async () => {
+ expect(json).to.haveOwnProperty('withoutConfig');
+ expect(Array.isArray(json.withoutConfig)).to.equal(true);
+
+ const ids = json.withoutConfig.map((item) => item.id);
+ expect(ids).to.deep.equal([
+ 'columbia.md',
+ 'endeavour.md',
+ 'enterprise.md',
+ 'promo/launch-week.mdx',
+ ]);
+ });
+
+ it('Returns `with schema` collection', async () => {
+ expect(json).to.haveOwnProperty('withSchemaConfig');
+ expect(Array.isArray(json.withSchemaConfig)).to.equal(true);
+
+ const ids = json.withSchemaConfig.map((item) => item.id);
+ const publishedDates = json.withSchemaConfig.map((item) => item.data.publishedAt);
+ expect(ids).to.deep.equal(['one.md', 'three.md', 'two.md']);
+ expect(publishedDates.every((date) => date instanceof Date)).to.equal(
+ true,
+ 'Not all publishedAt dates are Date objects'
+ );
+ expect(publishedDates.map((date) => date.toISOString())).to.deep.equal([
+ '2021-01-01T00:00:00.000Z',
+ '2021-01-03T00:00:00.000Z',
+ '2021-01-02T00:00:00.000Z',
+ ]);
+ });
+
+ it('Returns `with custom slugs` collection', async () => {
+ expect(json).to.haveOwnProperty('withSlugConfig');
+ expect(Array.isArray(json.withSlugConfig)).to.equal(true);
+
+ const slugs = json.withSlugConfig.map((item) => item.slug);
+ expect(slugs).to.deep.equal(['fancy-one.md', 'excellent-three.md', 'interesting-two.md']);
+ });
+ });
+
+ describe('Entry', () => {
+ let json;
+ before(async () => {
+ const rawJson = await fixture.readFile('/entries.json');
+ json = devalue.parse(rawJson);
+ });
+
+ it('Returns `without config` collection entry', async () => {
+ expect(json).to.haveOwnProperty('columbiaWithoutConfig');
+ expect(json.columbiaWithoutConfig.id).to.equal('columbia.md');
+ });
+
+ it('Returns `with schema` collection entry', async () => {
+ expect(json).to.haveOwnProperty('oneWithSchemaConfig');
+ expect(json.oneWithSchemaConfig.id).to.equal('one.md');
+ expect(json.oneWithSchemaConfig.data.publishedAt instanceof Date).to.equal(true);
+ expect(json.oneWithSchemaConfig.data.publishedAt.toISOString()).to.equal(
+ '2021-01-01T00:00:00.000Z'
+ );
+ });
+
+ it('Returns `with custom slugs` collection entry', async () => {
+ expect(json).to.haveOwnProperty('twoWithSlugConfig');
+ expect(json.twoWithSlugConfig.slug).to.equal('interesting-two.md');
+ });
+ });
+ });
+
+ const blogSlugToContents = {
+ 'first-post': {
+ title: 'First post',
+ element: 'blockquote',
+ content: 'First post loaded: yes!',
+ },
+ 'second-post': {
+ title: 'Second post',
+ element: 'blockquote',
+ content: 'Second post loaded: yes!',
+ },
+ 'third-post': {
+ title: 'Third post',
+ element: 'blockquote',
+ content: 'Third post loaded: yes!',
+ },
+ 'using-mdx': {
+ title: 'Using MDX',
+ element: 'a[href="#"]',
+ content: 'Embedded component in MDX',
+ },
+ };
+
+ describe('Static paths integration', () => {
+ let fixture;
+
+ before(async () => {
+ fixture = await loadFixture({ root: './fixtures/content-static-paths-integration/' });
+ await fixture.build();
+ });
+
+ it('Generates expected pages', async () => {
+ for (const slug in blogSlugToContents) {
+ expect(fixture.pathExists(`/posts/${slug}`)).to.equal(true);
+ }
+ });
+
+ it('Renders titles', async () => {
+ for (const slug in blogSlugToContents) {
+ const post = await fixture.readFile(`/posts/${slug}/index.html`);
+ const $ = cheerio.load(post);
+ expect($('h1').text()).to.equal(blogSlugToContents[slug].title);
+ }
+ });
+
+ it('Renders content', async () => {
+ for (const slug in blogSlugToContents) {
+ const post = await fixture.readFile(`/posts/${slug}/index.html`);
+ const $ = cheerio.load(post);
+ expect($(blogSlugToContents[slug].element).text().trim()).to.equal(
+ blogSlugToContents[slug].content
+ );
+ }
+ });
+ });
+
+ describe('SSR integration', () => {
+ let app;
+
+ before(async () => {
+ const fixture = await loadFixture({
+ root: './fixtures/content-ssr-integration/',
+ output: 'server',
+ adapter: testAdapter(),
+ });
+ await fixture.build();
+ app = await fixture.loadTestAdapterApp();
+ });
+
+ it('Responds 200 for expected pages', async () => {
+ for (const slug in blogSlugToContents) {
+ const request = new Request('http://example.com/posts/' + slug);
+ const response = await app.render(request);
+ expect(response.status).to.equal(200);
+ }
+ });
+
+ it('Renders titles', async () => {
+ for (const slug in blogSlugToContents) {
+ const request = new Request('http://example.com/posts/' + slug);
+ const response = await app.render(request);
+ const body = await response.text();
+ const $ = cheerio.load(body);
+ expect($('h1').text()).to.equal(blogSlugToContents[slug].title);
+ }
+ });
+
+ it('Renders content', async () => {
+ for (const slug in blogSlugToContents) {
+ const request = new Request('http://example.com/posts/' + slug);
+ const response = await app.render(request);
+ const body = await response.text();
+ const $ = cheerio.load(body);
+ expect($(blogSlugToContents[slug].element).text().trim()).to.equal(
+ blogSlugToContents[slug].content
+ );
+ }
+ });
+ });
+});
diff --git a/packages/astro/test/fixtures/content-collections/astro.config.mjs b/packages/astro/test/fixtures/content-collections/astro.config.mjs
new file mode 100644
index 000000000..6f20dd805
--- /dev/null
+++ b/packages/astro/test/fixtures/content-collections/astro.config.mjs
@@ -0,0 +1,10 @@
+import { defineConfig } from 'astro/config';
+import mdx from '@astrojs/mdx';
+
+// https://astro.build/config
+export default defineConfig({
+ integrations: [mdx()],
+ experimental: {
+ contentCollections: true,
+ },
+});
diff --git a/packages/astro/test/fixtures/content-collections/package.json b/packages/astro/test/fixtures/content-collections/package.json
new file mode 100644
index 000000000..b95788cef
--- /dev/null
+++ b/packages/astro/test/fixtures/content-collections/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@test/content-collections",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "astro": "workspace:*",
+ "@astrojs/mdx": "workspace:*"
+ }
+}
diff --git a/packages/astro/test/fixtures/content-collections/src/content/config.ts b/packages/astro/test/fixtures/content-collections/src/content/config.ts
new file mode 100644
index 000000000..eadc52a74
--- /dev/null
+++ b/packages/astro/test/fixtures/content-collections/src/content/config.ts
@@ -0,0 +1,24 @@
+import { z, defineCollection } from 'astro:content';
+
+const withSlugConfig = defineCollection({
+ slug({ id, data }) {
+ return `${data.prefix}-${id}`;
+ },
+ schema: {
+ prefix: z.string(),
+ }
+});
+
+const withSchemaConfig = defineCollection({
+ schema: {
+ title: z.string(),
+ isDraft: z.boolean().default(false),
+ lang: z.enum(['en', 'fr', 'es']).default('en'),
+ publishedAt: z.date().transform((val) => new Date(val)),
+ }
+});
+
+export const collections = {
+ 'with-slug-config': withSlugConfig,
+ 'with-schema-config': withSchemaConfig,
+}
diff --git a/packages/astro/test/fixtures/content-collections/src/content/types.generated.d.ts b/packages/astro/test/fixtures/content-collections/src/content/types.generated.d.ts
new file mode 100644
index 000000000..bf03efb76
--- /dev/null
+++ b/packages/astro/test/fixtures/content-collections/src/content/types.generated.d.ts
@@ -0,0 +1,125 @@
+declare module 'astro:content' {
+ export { z } from 'astro/zod';
+ export type CollectionEntry =
+ typeof entryMap[C][keyof typeof entryMap[C]] & Render;
+
+ type BaseCollectionConfig = {
+ schema?: S;
+ slug?: (entry: {
+ id: CollectionEntry['id'];
+ defaultSlug: string;
+ collection: string;
+ body: string;
+ data: import('astro/zod').infer>;
+ }) => string | Promise;
+ };
+ export function defineCollection(
+ input: BaseCollectionConfig
+ ): BaseCollectionConfig;
+
+ export function getEntry(
+ collection: C,
+ entryKey: E
+ ): Promise;
+ export function getCollection<
+ C extends keyof typeof entryMap,
+ E extends keyof typeof entryMap[C]
+ >(
+ collection: C,
+ filter?: (data: typeof entryMap[C][E]) => boolean
+ ): Promise<(typeof entryMap[C][E] & Render)[]>;
+
+ type InferEntrySchema = import('astro/zod').infer<
+ import('astro/zod').ZodObject['schema']>
+ >;
+
+ type Render = {
+ render(): Promise<{
+ Content: import('astro').MarkdownInstance<{}>['Content'];
+ headings: import('astro').MarkdownHeading[];
+ injectedFrontmatter: Record;
+ }>;
+ };
+
+ const entryMap: {
+ "with-schema-config": {
+"one.md": {
+ id: "one.md",
+ slug: "one",
+ body: string,
+ collection: "with-schema-config",
+ data: InferEntrySchema<"with-schema-config">
+},
+"three.md": {
+ id: "three.md",
+ slug: "three",
+ body: string,
+ collection: "with-schema-config",
+ data: InferEntrySchema<"with-schema-config">
+},
+"two.md": {
+ id: "two.md",
+ slug: "two",
+ body: string,
+ collection: "with-schema-config",
+ data: InferEntrySchema<"with-schema-config">
+},
+},
+"with-slug-config": {
+"one.md": {
+ id: "one.md",
+ slug: string,
+ body: string,
+ collection: "with-slug-config",
+ data: InferEntrySchema<"with-slug-config">
+},
+"three.md": {
+ id: "three.md",
+ slug: string,
+ body: string,
+ collection: "with-slug-config",
+ data: InferEntrySchema<"with-slug-config">
+},
+"two.md": {
+ id: "two.md",
+ slug: string,
+ body: string,
+ collection: "with-slug-config",
+ data: InferEntrySchema<"with-slug-config">
+},
+},
+"without-config": {
+"columbia.md": {
+ id: "columbia.md",
+ slug: "columbia",
+ body: string,
+ collection: "without-config",
+ data: any
+},
+"endeavour.md": {
+ id: "endeavour.md",
+ slug: "endeavour",
+ body: string,
+ collection: "without-config",
+ data: any
+},
+"enterprise.md": {
+ id: "enterprise.md",
+ slug: "enterprise",
+ body: string,
+ collection: "without-config",
+ data: any
+},
+"promo/launch-week.mdx": {
+ id: "promo/launch-week.mdx",
+ slug: "promo/launch-week",
+ body: string,
+ collection: "without-config",
+ data: any
+},
+},
+
+ };
+
+ type ContentConfig = typeof import("./config");
+}
diff --git a/packages/astro/test/fixtures/content-collections/src/content/with-schema-config/one.md b/packages/astro/test/fixtures/content-collections/src/content/with-schema-config/one.md
new file mode 100644
index 000000000..a470f86cb
--- /dev/null
+++ b/packages/astro/test/fixtures/content-collections/src/content/with-schema-config/one.md
@@ -0,0 +1,8 @@
+---
+title: One
+description: The first page
+lang: en
+publishedAt: 2021-01-01
+---
+
+# It's the first page, fancy!
diff --git a/packages/astro/test/fixtures/content-collections/src/content/with-schema-config/three.md b/packages/astro/test/fixtures/content-collections/src/content/with-schema-config/three.md
new file mode 100644
index 000000000..e5c3191e5
--- /dev/null
+++ b/packages/astro/test/fixtures/content-collections/src/content/with-schema-config/three.md
@@ -0,0 +1,8 @@
+---
+title: Three
+description: The third page
+lang: es
+publishedAt: 2021-01-03
+---
+
+# It's the third page, excellent!
diff --git a/packages/astro/test/fixtures/content-collections/src/content/with-schema-config/two.md b/packages/astro/test/fixtures/content-collections/src/content/with-schema-config/two.md
new file mode 100644
index 000000000..d4de3edf7
--- /dev/null
+++ b/packages/astro/test/fixtures/content-collections/src/content/with-schema-config/two.md
@@ -0,0 +1,8 @@
+---
+title: Two
+description: The second page
+lang: en
+publishedAt: 2021-01-02
+---
+
+# It's the second page, interesting!
diff --git a/packages/astro/test/fixtures/content-collections/src/content/with-slug-config/one.md b/packages/astro/test/fixtures/content-collections/src/content/with-slug-config/one.md
new file mode 100644
index 000000000..c066d42d7
--- /dev/null
+++ b/packages/astro/test/fixtures/content-collections/src/content/with-slug-config/one.md
@@ -0,0 +1,5 @@
+---
+prefix: fancy
+---
+
+# It's the first page, fancy!
diff --git a/packages/astro/test/fixtures/content-collections/src/content/with-slug-config/three.md b/packages/astro/test/fixtures/content-collections/src/content/with-slug-config/three.md
new file mode 100644
index 000000000..6d88598e1
--- /dev/null
+++ b/packages/astro/test/fixtures/content-collections/src/content/with-slug-config/three.md
@@ -0,0 +1,5 @@
+---
+prefix: excellent
+---
+
+# It's the third page, excellent!
diff --git a/packages/astro/test/fixtures/content-collections/src/content/with-slug-config/two.md b/packages/astro/test/fixtures/content-collections/src/content/with-slug-config/two.md
new file mode 100644
index 000000000..f15270f99
--- /dev/null
+++ b/packages/astro/test/fixtures/content-collections/src/content/with-slug-config/two.md
@@ -0,0 +1,5 @@
+---
+prefix: interesting
+---
+
+# It's the second page, interesting!
diff --git a/packages/astro/test/fixtures/content-collections/src/content/without-config/columbia.md b/packages/astro/test/fixtures/content-collections/src/content/without-config/columbia.md
new file mode 100644
index 000000000..4971108e3
--- /dev/null
+++ b/packages/astro/test/fixtures/content-collections/src/content/without-config/columbia.md
@@ -0,0 +1,15 @@
+---
+title: Columbia
+description: 'Learn about the Columbia NASA space shuttle.'
+publishedDate: 'Sat May 21 2022 00:00:00 GMT-0400 (Eastern Daylight Time)'
+tags: [space, 90s]
+---
+
+**Source:** [Wikipedia](https://en.wikipedia.org/wiki/Space_Shuttle_Endeavour)
+
+Space Shuttle Endeavour (Orbiter Vehicle Designation: OV-105) is a retired orbiter from NASA's Space Shuttle program and the fifth and final operational Shuttle built. It embarked on its first mission, STS-49, in May 1992 and its 25th and final mission, STS-134, in May 2011. STS-134 was expected to be the final mission of the Space Shuttle program, but with the authorization of STS-135, Atlantis became the last shuttle to fly.
+
+The United States Congress approved the construction of Endeavour in 1987 to replace the Space Shuttle Challenger, which was destroyed in 1986.
+
+NASA chose, on cost grounds, to build much of Endeavour from spare parts rather than refitting the Space Shuttle Enterprise, and used structural spares built during the construction of Discovery and Atlantis in its assembly.
+Space Shuttle Endeavour (Orbiter Vehicle Designation: OV-105) is a retired orbiter from NASA's Space Shuttle program and the fifth and final operational Shuttle built. It embarked on its first mission, STS-49, in May 1992 and its 25th and final mission, STS-134, in May 2011. STS-134 was expected to be the final mission of the Space Shuttle program, but with the authorization of STS-135, Atlantis became the last shuttle to fly.
diff --git a/packages/astro/test/fixtures/content-collections/src/content/without-config/endeavour.md b/packages/astro/test/fixtures/content-collections/src/content/without-config/endeavour.md
new file mode 100644
index 000000000..51d6e8c42
--- /dev/null
+++ b/packages/astro/test/fixtures/content-collections/src/content/without-config/endeavour.md
@@ -0,0 +1,14 @@
+---
+title: Endeavour
+description: 'Learn about the Endeavour NASA space shuttle.'
+publishedDate: 'Sun Jul 11 2021 00:00:00 GMT-0400 (Eastern Daylight Time)'
+tags: [space, 90s]
+---
+
+**Source:** [Wikipedia](https://en.wikipedia.org/wiki/Space_Shuttle_Endeavour)
+
+Space Shuttle Endeavour (Orbiter Vehicle Designation: OV-105) is a retired orbiter from NASA's Space Shuttle program and the fifth and final operational Shuttle built. It embarked on its first mission, STS-49, in May 1992 and its 25th and final mission, STS-134, in May 2011. STS-134 was expected to be the final mission of the Space Shuttle program, but with the authorization of STS-135, Atlantis became the last shuttle to fly.
+
+The United States Congress approved the construction of Endeavour in 1987 to replace the Space Shuttle Challenger, which was destroyed in 1986.
+
+NASA chose, on cost grounds, to build much of Endeavour from spare parts rather than refitting the Space Shuttle Enterprise, and used structural spares built during the construction of Discovery and Atlantis in its assembly.
diff --git a/packages/astro/test/fixtures/content-collections/src/content/without-config/enterprise.md b/packages/astro/test/fixtures/content-collections/src/content/without-config/enterprise.md
new file mode 100644
index 000000000..3131e6d5d
--- /dev/null
+++ b/packages/astro/test/fixtures/content-collections/src/content/without-config/enterprise.md
@@ -0,0 +1,14 @@
+---
+title: 'Enterprise'
+description: 'Learn about the Enterprise NASA space shuttle.'
+publishedDate: 'Tue Jun 08 2021 00:00:00 GMT-0400 (Eastern Daylight Time)'
+tags: [space, 70s]
+---
+
+**Source:** [Wikipedia](https://en.wikipedia.org/wiki/Space_Shuttle_Enterprise)
+
+Space Shuttle Enterprise (Orbiter Vehicle Designation: OV-101) was the first orbiter of the Space Shuttle system. Rolled out on September 17, 1976, it was built for NASA as part of the Space Shuttle program to perform atmospheric test flights after being launched from a modified Boeing 747. It was constructed without engines or a functional heat shield. As a result, it was not capable of spaceflight.
+
+Originally, Enterprise had been intended to be refitted for orbital flight to become the second space-rated orbiter in service. However, during the construction of Space Shuttle Columbia, details of the final design changed, making it simpler and less costly to build Challenger around a body frame that had been built as a test article. Similarly, Enterprise was considered for refit to replace Challenger after the latter was destroyed, but Endeavour was built from structural spares instead.
+
+Enterprise was restored and placed on display in 2003 at the Smithsonian's new Steven F. Udvar-Hazy Center in Virginia. Following the retirement of the Space Shuttle fleet, Discovery replaced Enterprise at the Udvar-Hazy Center, and Enterprise was transferred to the Intrepid Sea, Air & Space Museum in New York City, where it has been on display since July 2012.
diff --git a/packages/astro/test/fixtures/content-collections/src/content/without-config/promo/launch-week-styles.css b/packages/astro/test/fixtures/content-collections/src/content/without-config/promo/launch-week-styles.css
new file mode 100644
index 000000000..cce2effe2
--- /dev/null
+++ b/packages/astro/test/fixtures/content-collections/src/content/without-config/promo/launch-week-styles.css
@@ -0,0 +1,3 @@
+body {
+ font-family: 'Comic Sans MS', sans-serif;
+}
diff --git a/packages/astro/test/fixtures/content-collections/src/content/without-config/promo/launch-week.mdx b/packages/astro/test/fixtures/content-collections/src/content/without-config/promo/launch-week.mdx
new file mode 100644
index 000000000..f7c4bac16
--- /dev/null
+++ b/packages/astro/test/fixtures/content-collections/src/content/without-config/promo/launch-week.mdx
@@ -0,0 +1,14 @@
+---
+title: 'Launch week!'
+description: 'Join us for the exciting launch of SPACE BLOG'
+publishedDate: 'Sat May 21 2022 00:00:00 GMT-0400 (Eastern Daylight Time)'
+tags: ['announcement']
+---
+
+import './launch-week-styles.css';
+
+Join us for the space blog launch!
+
+- THIS THURSDAY
+- Houston, TX
+- Dress code: **interstellar casual** ✨
diff --git a/packages/astro/test/fixtures/content-collections/src/pages/collections.json.js b/packages/astro/test/fixtures/content-collections/src/pages/collections.json.js
new file mode 100644
index 000000000..007f8a383
--- /dev/null
+++ b/packages/astro/test/fixtures/content-collections/src/pages/collections.json.js
@@ -0,0 +1,12 @@
+import { getCollection } from 'astro:content';
+import * as devalue from 'devalue';
+import { stripAllRenderFn } from '../utils.js';
+
+export async function get() {
+ const withoutConfig = stripAllRenderFn(await getCollection('without-config'));
+ const withSchemaConfig = stripAllRenderFn(await getCollection('with-schema-config'));
+ const withSlugConfig = stripAllRenderFn(await getCollection('with-slug-config'));
+ return {
+ body: devalue.stringify({withoutConfig, withSchemaConfig, withSlugConfig}),
+ }
+}
diff --git a/packages/astro/test/fixtures/content-collections/src/pages/entries.json.js b/packages/astro/test/fixtures/content-collections/src/pages/entries.json.js
new file mode 100644
index 000000000..141a7b7d1
--- /dev/null
+++ b/packages/astro/test/fixtures/content-collections/src/pages/entries.json.js
@@ -0,0 +1,12 @@
+import { getEntry } from 'astro:content';
+import * as devalue from 'devalue';
+import { stripRenderFn } from '../utils.js';
+
+export async function get() {
+ const columbiaWithoutConfig = stripRenderFn(await getEntry('without-config', 'columbia.md'));
+ const oneWithSchemaConfig = stripRenderFn(await getEntry('with-schema-config', 'one.md'));
+ const twoWithSlugConfig = stripRenderFn(await getEntry('with-slug-config', 'two.md'));
+ return {
+ body: devalue.stringify({columbiaWithoutConfig, oneWithSchemaConfig, twoWithSlugConfig}),
+ }
+}
diff --git a/packages/astro/test/fixtures/content-collections/src/pages/index.astro b/packages/astro/test/fixtures/content-collections/src/pages/index.astro
new file mode 100644
index 000000000..468b70e7b
--- /dev/null
+++ b/packages/astro/test/fixtures/content-collections/src/pages/index.astro
@@ -0,0 +1,24 @@
+---
+
+---
+
+
+
+
+ It's content time!
+
+
+
+
+
+
+
diff --git a/packages/astro/test/fixtures/content-collections/src/utils.js b/packages/astro/test/fixtures/content-collections/src/utils.js
new file mode 100644
index 000000000..3a6244327
--- /dev/null
+++ b/packages/astro/test/fixtures/content-collections/src/utils.js
@@ -0,0 +1,8 @@
+export function stripRenderFn(entryWithRender) {
+ const { render, ...entry } = entryWithRender;
+ return entry;
+}
+
+export function stripAllRenderFn(collection = []) {
+ return collection.map(stripRenderFn);
+}
diff --git a/packages/astro/test/fixtures/content-ssr-integration/astro.config.mjs b/packages/astro/test/fixtures/content-ssr-integration/astro.config.mjs
new file mode 100644
index 000000000..6f20dd805
--- /dev/null
+++ b/packages/astro/test/fixtures/content-ssr-integration/astro.config.mjs
@@ -0,0 +1,10 @@
+import { defineConfig } from 'astro/config';
+import mdx from '@astrojs/mdx';
+
+// https://astro.build/config
+export default defineConfig({
+ integrations: [mdx()],
+ experimental: {
+ contentCollections: true,
+ },
+});
diff --git a/packages/astro/test/fixtures/content-ssr-integration/package.json b/packages/astro/test/fixtures/content-ssr-integration/package.json
new file mode 100644
index 000000000..714f57e3f
--- /dev/null
+++ b/packages/astro/test/fixtures/content-ssr-integration/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@test/content-ssr-integration",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "astro": "workspace:*",
+ "@astrojs/mdx": "workspace:*"
+ }
+}
diff --git a/packages/astro/test/fixtures/content-ssr-integration/src/components/HeaderLink.astro b/packages/astro/test/fixtures/content-ssr-integration/src/components/HeaderLink.astro
new file mode 100644
index 000000000..e8e2e103b
--- /dev/null
+++ b/packages/astro/test/fixtures/content-ssr-integration/src/components/HeaderLink.astro
@@ -0,0 +1,22 @@
+---
+export interface Props extends astroHTML.JSX.AnchorHTMLAttributes {}
+
+const { href, class: className, ...props } = Astro.props;
+
+const { pathname } = Astro.url;
+const isActive = href === pathname || href === pathname.replace(/\/$/, '');
+---
+
+
+
+
+
diff --git a/packages/astro/test/fixtures/content-ssr-integration/src/content/blog/first-post.md b/packages/astro/test/fixtures/content-ssr-integration/src/content/blog/first-post.md
new file mode 100644
index 000000000..f69ff075d
--- /dev/null
+++ b/packages/astro/test/fixtures/content-ssr-integration/src/content/blog/first-post.md
@@ -0,0 +1,18 @@
+---
+title: "First post"
+description: "Lorem ipsum dolor sit amet"
+pubDate: "Jul 08 2022"
+heroImage: "/placeholder-hero.jpg"
+---
+
+> First post loaded: yes!
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet.
+
+Morbi tristique senectus et netus. Id semper risus in hendrerit gravida rutrum quisque non tellus. Habitasse platea dictumst quisque sagittis purus sit amet. Tellus molestie nunc non blandit massa. Cursus vitae congue mauris rhoncus. Accumsan tortor posuere ac ut. Fringilla urna porttitor rhoncus dolor. Elit ullamcorper dignissim cras tincidunt lobortis. In cursus turpis massa tincidunt dui ut ornare lectus. Integer feugiat scelerisque varius morbi enim nunc. Bibendum neque egestas congue quisque egestas diam. Cras ornare arcu dui vivamus arcu felis bibendum. Dignissim suspendisse in est ante in nibh mauris. Sed tempus urna et pharetra pharetra massa massa ultricies mi.
+
+Mollis nunc sed id semper risus in. Convallis a cras semper auctor neque. Diam sit amet nisl suscipit. Lacus viverra vitae congue eu consequat ac felis donec. Egestas integer eget aliquet nibh praesent tristique magna sit amet. Eget magna fermentum iaculis eu non diam. In vitae turpis massa sed elementum. Tristique et egestas quis ipsum suspendisse ultrices. Eget lorem dolor sed viverra ipsum. Vel turpis nunc eget lorem dolor sed viverra. Posuere ac ut consequat semper viverra nam. Laoreet suspendisse interdum consectetur libero id faucibus. Diam phasellus vestibulum lorem sed risus ultricies tristique. Rhoncus dolor purus non enim praesent elementum facilisis. Ultrices tincidunt arcu non sodales neque. Tempus egestas sed sed risus pretium quam vulputate. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare. Fringilla urna porttitor rhoncus dolor purus non. Amet dictum sit amet justo donec enim.
+
+Mattis ullamcorper velit sed ullamcorper morbi tincidunt. Tortor posuere ac ut consequat semper viverra. Tellus mauris a diam maecenas sed enim ut sem viverra. Venenatis urna cursus eget nunc scelerisque viverra mauris in. Arcu ac tortor dignissim convallis aenean et tortor at. Curabitur gravida arcu ac tortor dignissim convallis aenean et tortor. Egestas tellus rutrum tellus pellentesque eu. Fusce ut placerat orci nulla pellentesque dignissim enim sit amet. Ut enim blandit volutpat maecenas volutpat blandit aliquam etiam. Id donec ultrices tincidunt arcu. Id cursus metus aliquam eleifend mi.
+
+Tempus quam pellentesque nec nam aliquam sem. Risus at ultrices mi tempus imperdiet. Id porta nibh venenatis cras sed felis eget velit. Ipsum a arcu cursus vitae. Facilisis magna etiam tempor orci eu lobortis elementum. Tincidunt dui ut ornare lectus sit. Quisque non tellus orci ac. Blandit libero volutpat sed cras. Nec tincidunt praesent semper feugiat nibh sed pulvinar proin gravida. Egestas integer eget aliquet nibh praesent tristique magna.
diff --git a/packages/astro/test/fixtures/content-ssr-integration/src/content/blog/second-post.md b/packages/astro/test/fixtures/content-ssr-integration/src/content/blog/second-post.md
new file mode 100644
index 000000000..d1629b10c
--- /dev/null
+++ b/packages/astro/test/fixtures/content-ssr-integration/src/content/blog/second-post.md
@@ -0,0 +1,18 @@
+---
+title: "Second post"
+description: "Lorem ipsum dolor sit amet"
+pubDate: "Jul 22 2022"
+heroImage: "/placeholder-hero.jpg"
+---
+
+> Second post loaded: yes!
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet.
+
+Morbi tristique senectus et netus. Id semper risus in hendrerit gravida rutrum quisque non tellus. Habitasse platea dictumst quisque sagittis purus sit amet. Tellus molestie nunc non blandit massa. Cursus vitae congue mauris rhoncus. Accumsan tortor posuere ac ut. Fringilla urna porttitor rhoncus dolor. Elit ullamcorper dignissim cras tincidunt lobortis. In cursus turpis massa tincidunt dui ut ornare lectus. Integer feugiat scelerisque varius morbi enim nunc. Bibendum neque egestas congue quisque egestas diam. Cras ornare arcu dui vivamus arcu felis bibendum. Dignissim suspendisse in est ante in nibh mauris. Sed tempus urna et pharetra pharetra massa massa ultricies mi.
+
+Mollis nunc sed id semper risus in. Convallis a cras semper auctor neque. Diam sit amet nisl suscipit. Lacus viverra vitae congue eu consequat ac felis donec. Egestas integer eget aliquet nibh praesent tristique magna sit amet. Eget magna fermentum iaculis eu non diam. In vitae turpis massa sed elementum. Tristique et egestas quis ipsum suspendisse ultrices. Eget lorem dolor sed viverra ipsum. Vel turpis nunc eget lorem dolor sed viverra. Posuere ac ut consequat semper viverra nam. Laoreet suspendisse interdum consectetur libero id faucibus. Diam phasellus vestibulum lorem sed risus ultricies tristique. Rhoncus dolor purus non enim praesent elementum facilisis. Ultrices tincidunt arcu non sodales neque. Tempus egestas sed sed risus pretium quam vulputate. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare. Fringilla urna porttitor rhoncus dolor purus non. Amet dictum sit amet justo donec enim.
+
+Mattis ullamcorper velit sed ullamcorper morbi tincidunt. Tortor posuere ac ut consequat semper viverra. Tellus mauris a diam maecenas sed enim ut sem viverra. Venenatis urna cursus eget nunc scelerisque viverra mauris in. Arcu ac tortor dignissim convallis aenean et tortor at. Curabitur gravida arcu ac tortor dignissim convallis aenean et tortor. Egestas tellus rutrum tellus pellentesque eu. Fusce ut placerat orci nulla pellentesque dignissim enim sit amet. Ut enim blandit volutpat maecenas volutpat blandit aliquam etiam. Id donec ultrices tincidunt arcu. Id cursus metus aliquam eleifend mi.
+
+Tempus quam pellentesque nec nam aliquam sem. Risus at ultrices mi tempus imperdiet. Id porta nibh venenatis cras sed felis eget velit. Ipsum a arcu cursus vitae. Facilisis magna etiam tempor orci eu lobortis elementum. Tincidunt dui ut ornare lectus sit. Quisque non tellus orci ac. Blandit libero volutpat sed cras. Nec tincidunt praesent semper feugiat nibh sed pulvinar proin gravida. Egestas integer eget aliquet nibh praesent tristique magna.
diff --git a/packages/astro/test/fixtures/content-ssr-integration/src/content/blog/third-post.md b/packages/astro/test/fixtures/content-ssr-integration/src/content/blog/third-post.md
new file mode 100644
index 000000000..ac3a1b66c
--- /dev/null
+++ b/packages/astro/test/fixtures/content-ssr-integration/src/content/blog/third-post.md
@@ -0,0 +1,18 @@
+---
+title: "Third post"
+description: "Lorem ipsum dolor sit amet"
+pubDate: "Jul 15 2022"
+heroImage: "/placeholder-hero.jpg"
+---
+
+> Third post loaded: yes!
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet.
+
+Morbi tristique senectus et netus. Id semper risus in hendrerit gravida rutrum quisque non tellus. Habitasse platea dictumst quisque sagittis purus sit amet. Tellus molestie nunc non blandit massa. Cursus vitae congue mauris rhoncus. Accumsan tortor posuere ac ut. Fringilla urna porttitor rhoncus dolor. Elit ullamcorper dignissim cras tincidunt lobortis. In cursus turpis massa tincidunt dui ut ornare lectus. Integer feugiat scelerisque varius morbi enim nunc. Bibendum neque egestas congue quisque egestas diam. Cras ornare arcu dui vivamus arcu felis bibendum. Dignissim suspendisse in est ante in nibh mauris. Sed tempus urna et pharetra pharetra massa massa ultricies mi.
+
+Mollis nunc sed id semper risus in. Convallis a cras semper auctor neque. Diam sit amet nisl suscipit. Lacus viverra vitae congue eu consequat ac felis donec. Egestas integer eget aliquet nibh praesent tristique magna sit amet. Eget magna fermentum iaculis eu non diam. In vitae turpis massa sed elementum. Tristique et egestas quis ipsum suspendisse ultrices. Eget lorem dolor sed viverra ipsum. Vel turpis nunc eget lorem dolor sed viverra. Posuere ac ut consequat semper viverra nam. Laoreet suspendisse interdum consectetur libero id faucibus. Diam phasellus vestibulum lorem sed risus ultricies tristique. Rhoncus dolor purus non enim praesent elementum facilisis. Ultrices tincidunt arcu non sodales neque. Tempus egestas sed sed risus pretium quam vulputate. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare. Fringilla urna porttitor rhoncus dolor purus non. Amet dictum sit amet justo donec enim.
+
+Mattis ullamcorper velit sed ullamcorper morbi tincidunt. Tortor posuere ac ut consequat semper viverra. Tellus mauris a diam maecenas sed enim ut sem viverra. Venenatis urna cursus eget nunc scelerisque viverra mauris in. Arcu ac tortor dignissim convallis aenean et tortor at. Curabitur gravida arcu ac tortor dignissim convallis aenean et tortor. Egestas tellus rutrum tellus pellentesque eu. Fusce ut placerat orci nulla pellentesque dignissim enim sit amet. Ut enim blandit volutpat maecenas volutpat blandit aliquam etiam. Id donec ultrices tincidunt arcu. Id cursus metus aliquam eleifend mi.
+
+Tempus quam pellentesque nec nam aliquam sem. Risus at ultrices mi tempus imperdiet. Id porta nibh venenatis cras sed felis eget velit. Ipsum a arcu cursus vitae. Facilisis magna etiam tempor orci eu lobortis elementum. Tincidunt dui ut ornare lectus sit. Quisque non tellus orci ac. Blandit libero volutpat sed cras. Nec tincidunt praesent semper feugiat nibh sed pulvinar proin gravida. Egestas integer eget aliquet nibh praesent tristique magna.
diff --git a/packages/astro/test/fixtures/content-ssr-integration/src/content/blog/using-mdx.mdx b/packages/astro/test/fixtures/content-ssr-integration/src/content/blog/using-mdx.mdx
new file mode 100644
index 000000000..036209d3b
--- /dev/null
+++ b/packages/astro/test/fixtures/content-ssr-integration/src/content/blog/using-mdx.mdx
@@ -0,0 +1,31 @@
+---
+title: 'Using MDX'
+description: 'Lorem ipsum dolor sit amet'
+pubDate: 'Jul 02 2022'
+heroImage: '/placeholder-hero.jpg'
+---
+
+This theme comes with the [@astrojs/mdx](https://docs.astro.build/en/guides/integrations-guide/mdx/) integration installed and configured in your `astro.config.mjs` config file. If you prefer not to use MDX, you can disable support by removing the integration from your config file.
+
+## Why MDX?
+
+MDX is a special flavor of Markdown that supports embedded JavaScript & JSX syntax. This unlocks the ability to [mix JavaScript and UI Components into your Markdown content](https://docs.astro.build/en/guides/markdown-content/#mdx-features) for things like interactive charts or alerts.
+
+If you have existing content authored in MDX, this integration will hopefully make migrating to Astro a breeze.
+
+## Example
+
+Here is how you import and use a UI component inside of MDX.
+When you open this page in the browser, you should see the clickable button below.
+
+import HeaderLink from '../../components/HeaderLink.astro';
+
+
+ Embedded component in MDX
+
+
+## More Links
+
+- [MDX Syntax Documentation](https://mdxjs.com/docs/what-is-mdx)
+- [Astro Usage Documentation](https://docs.astro.build/en/guides/markdown-content/#markdown-and-mdx-pages)
+- **Note:** [Client Directives](https://docs.astro.build/en/reference/directives-reference/#client-directives) are still required to create interactive components. Otherwise, all components in your MDX will render as static HTML (no JavaScript) by default.
diff --git a/packages/astro/test/fixtures/content-ssr-integration/src/content/config.ts b/packages/astro/test/fixtures/content-ssr-integration/src/content/config.ts
new file mode 100644
index 000000000..f32eba6de
--- /dev/null
+++ b/packages/astro/test/fixtures/content-ssr-integration/src/content/config.ts
@@ -0,0 +1,16 @@
+import { defineCollection, z } from 'astro:content';
+
+const blog = defineCollection({
+ schema: {
+ title: z.string(),
+ description: z.string(),
+ pubDate: z.string().transform((str) => new Date(str)),
+ updatedDate: z
+ .string()
+ .optional()
+ .transform((str) => (str ? new Date(str) : undefined)),
+ heroImage: z.string().optional(),
+ },
+});
+
+export const collections = { blog };
diff --git a/packages/astro/test/fixtures/content-ssr-integration/src/content/types.generated.d.ts b/packages/astro/test/fixtures/content-ssr-integration/src/content/types.generated.d.ts
new file mode 100644
index 000000000..7183d2433
--- /dev/null
+++ b/packages/astro/test/fixtures/content-ssr-integration/src/content/types.generated.d.ts
@@ -0,0 +1,79 @@
+declare module 'astro:content' {
+ export { z } from 'astro/zod';
+ export type CollectionEntry =
+ typeof entryMap[C][keyof typeof entryMap[C]] & Render;
+
+ type BaseCollectionConfig = {
+ schema?: S;
+ slug?: (entry: {
+ id: CollectionEntry['id'];
+ defaultSlug: string;
+ collection: string;
+ body: string;
+ data: import('astro/zod').infer>;
+ }) => string | Promise;
+ };
+ export function defineCollection(
+ input: BaseCollectionConfig
+ ): BaseCollectionConfig;
+
+ export function getEntry(
+ collection: C,
+ entryKey: E
+ ): Promise;
+ export function getCollection<
+ C extends keyof typeof entryMap,
+ E extends keyof typeof entryMap[C]
+ >(
+ collection: C,
+ filter?: (data: typeof entryMap[C][E]) => boolean
+ ): Promise<(typeof entryMap[C][E] & Render)[]>;
+
+ type InferEntrySchema = import('astro/zod').infer<
+ import('astro/zod').ZodObject['schema']>
+ >;
+
+ type Render = {
+ render(): Promise<{
+ Content: import('astro').MarkdownInstance<{}>['Content'];
+ headings: import('astro').MarkdownHeading[];
+ injectedFrontmatter: Record;
+ }>;
+ };
+
+ const entryMap: {
+ "blog": {
+"first-post.md": {
+ id: "first-post.md",
+ slug: "first-post",
+ body: string,
+ collection: "blog",
+ data: InferEntrySchema<"blog">
+},
+"second-post.md": {
+ id: "second-post.md",
+ slug: "second-post",
+ body: string,
+ collection: "blog",
+ data: InferEntrySchema<"blog">
+},
+"third-post.md": {
+ id: "third-post.md",
+ slug: "third-post",
+ body: string,
+ collection: "blog",
+ data: InferEntrySchema<"blog">
+},
+"using-mdx.mdx": {
+ id: "using-mdx.mdx",
+ slug: "using-mdx",
+ body: string,
+ collection: "blog",
+ data: InferEntrySchema<"blog">
+},
+},
+
+ };
+
+ type ContentConfig = typeof import("./config");
+}
diff --git a/packages/astro/test/fixtures/content-ssr-integration/src/pages/index.astro b/packages/astro/test/fixtures/content-ssr-integration/src/pages/index.astro
new file mode 100644
index 000000000..468b70e7b
--- /dev/null
+++ b/packages/astro/test/fixtures/content-ssr-integration/src/pages/index.astro
@@ -0,0 +1,24 @@
+---
+
+---
+
+
+
+
+ It's content time!
+
+
+
+
+
+
+
diff --git a/packages/astro/test/fixtures/content-ssr-integration/src/pages/posts/[...slug].astro b/packages/astro/test/fixtures/content-ssr-integration/src/pages/posts/[...slug].astro
new file mode 100644
index 000000000..1cafafcf1
--- /dev/null
+++ b/packages/astro/test/fixtures/content-ssr-integration/src/pages/posts/[...slug].astro
@@ -0,0 +1,33 @@
+---
+import { getCollection } from 'astro:content';
+
+const { slug } = Astro.params;
+const posts = await getCollection('blog');
+const post = posts.find((post) => post.slug === slug);
+if (!post) {
+ throw new Error(`Not found: ${slug}`);
+}
+const { Content } = await post.render();
+---
+
+
+
+
+
+ {post.data.title}
+
+
+
+
{post.data.title}
+
+
+
diff --git a/packages/astro/test/fixtures/content-static-paths-integration/astro.config.mjs b/packages/astro/test/fixtures/content-static-paths-integration/astro.config.mjs
new file mode 100644
index 000000000..6f20dd805
--- /dev/null
+++ b/packages/astro/test/fixtures/content-static-paths-integration/astro.config.mjs
@@ -0,0 +1,10 @@
+import { defineConfig } from 'astro/config';
+import mdx from '@astrojs/mdx';
+
+// https://astro.build/config
+export default defineConfig({
+ integrations: [mdx()],
+ experimental: {
+ contentCollections: true,
+ },
+});
diff --git a/packages/astro/test/fixtures/content-static-paths-integration/package.json b/packages/astro/test/fixtures/content-static-paths-integration/package.json
new file mode 100644
index 000000000..92f141a6f
--- /dev/null
+++ b/packages/astro/test/fixtures/content-static-paths-integration/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@test/content-static-paths-integration",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "astro": "workspace:*",
+ "@astrojs/mdx": "workspace:*"
+ }
+}
diff --git a/packages/astro/test/fixtures/content-static-paths-integration/src/components/HeaderLink.astro b/packages/astro/test/fixtures/content-static-paths-integration/src/components/HeaderLink.astro
new file mode 100644
index 000000000..e8e2e103b
--- /dev/null
+++ b/packages/astro/test/fixtures/content-static-paths-integration/src/components/HeaderLink.astro
@@ -0,0 +1,22 @@
+---
+export interface Props extends astroHTML.JSX.AnchorHTMLAttributes {}
+
+const { href, class: className, ...props } = Astro.props;
+
+const { pathname } = Astro.url;
+const isActive = href === pathname || href === pathname.replace(/\/$/, '');
+---
+
+
+
+
+
diff --git a/packages/astro/test/fixtures/content-static-paths-integration/src/content/blog/first-post.md b/packages/astro/test/fixtures/content-static-paths-integration/src/content/blog/first-post.md
new file mode 100644
index 000000000..f69ff075d
--- /dev/null
+++ b/packages/astro/test/fixtures/content-static-paths-integration/src/content/blog/first-post.md
@@ -0,0 +1,18 @@
+---
+title: "First post"
+description: "Lorem ipsum dolor sit amet"
+pubDate: "Jul 08 2022"
+heroImage: "/placeholder-hero.jpg"
+---
+
+> First post loaded: yes!
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet.
+
+Morbi tristique senectus et netus. Id semper risus in hendrerit gravida rutrum quisque non tellus. Habitasse platea dictumst quisque sagittis purus sit amet. Tellus molestie nunc non blandit massa. Cursus vitae congue mauris rhoncus. Accumsan tortor posuere ac ut. Fringilla urna porttitor rhoncus dolor. Elit ullamcorper dignissim cras tincidunt lobortis. In cursus turpis massa tincidunt dui ut ornare lectus. Integer feugiat scelerisque varius morbi enim nunc. Bibendum neque egestas congue quisque egestas diam. Cras ornare arcu dui vivamus arcu felis bibendum. Dignissim suspendisse in est ante in nibh mauris. Sed tempus urna et pharetra pharetra massa massa ultricies mi.
+
+Mollis nunc sed id semper risus in. Convallis a cras semper auctor neque. Diam sit amet nisl suscipit. Lacus viverra vitae congue eu consequat ac felis donec. Egestas integer eget aliquet nibh praesent tristique magna sit amet. Eget magna fermentum iaculis eu non diam. In vitae turpis massa sed elementum. Tristique et egestas quis ipsum suspendisse ultrices. Eget lorem dolor sed viverra ipsum. Vel turpis nunc eget lorem dolor sed viverra. Posuere ac ut consequat semper viverra nam. Laoreet suspendisse interdum consectetur libero id faucibus. Diam phasellus vestibulum lorem sed risus ultricies tristique. Rhoncus dolor purus non enim praesent elementum facilisis. Ultrices tincidunt arcu non sodales neque. Tempus egestas sed sed risus pretium quam vulputate. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare. Fringilla urna porttitor rhoncus dolor purus non. Amet dictum sit amet justo donec enim.
+
+Mattis ullamcorper velit sed ullamcorper morbi tincidunt. Tortor posuere ac ut consequat semper viverra. Tellus mauris a diam maecenas sed enim ut sem viverra. Venenatis urna cursus eget nunc scelerisque viverra mauris in. Arcu ac tortor dignissim convallis aenean et tortor at. Curabitur gravida arcu ac tortor dignissim convallis aenean et tortor. Egestas tellus rutrum tellus pellentesque eu. Fusce ut placerat orci nulla pellentesque dignissim enim sit amet. Ut enim blandit volutpat maecenas volutpat blandit aliquam etiam. Id donec ultrices tincidunt arcu. Id cursus metus aliquam eleifend mi.
+
+Tempus quam pellentesque nec nam aliquam sem. Risus at ultrices mi tempus imperdiet. Id porta nibh venenatis cras sed felis eget velit. Ipsum a arcu cursus vitae. Facilisis magna etiam tempor orci eu lobortis elementum. Tincidunt dui ut ornare lectus sit. Quisque non tellus orci ac. Blandit libero volutpat sed cras. Nec tincidunt praesent semper feugiat nibh sed pulvinar proin gravida. Egestas integer eget aliquet nibh praesent tristique magna.
diff --git a/packages/astro/test/fixtures/content-static-paths-integration/src/content/blog/second-post.md b/packages/astro/test/fixtures/content-static-paths-integration/src/content/blog/second-post.md
new file mode 100644
index 000000000..d1629b10c
--- /dev/null
+++ b/packages/astro/test/fixtures/content-static-paths-integration/src/content/blog/second-post.md
@@ -0,0 +1,18 @@
+---
+title: "Second post"
+description: "Lorem ipsum dolor sit amet"
+pubDate: "Jul 22 2022"
+heroImage: "/placeholder-hero.jpg"
+---
+
+> Second post loaded: yes!
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet.
+
+Morbi tristique senectus et netus. Id semper risus in hendrerit gravida rutrum quisque non tellus. Habitasse platea dictumst quisque sagittis purus sit amet. Tellus molestie nunc non blandit massa. Cursus vitae congue mauris rhoncus. Accumsan tortor posuere ac ut. Fringilla urna porttitor rhoncus dolor. Elit ullamcorper dignissim cras tincidunt lobortis. In cursus turpis massa tincidunt dui ut ornare lectus. Integer feugiat scelerisque varius morbi enim nunc. Bibendum neque egestas congue quisque egestas diam. Cras ornare arcu dui vivamus arcu felis bibendum. Dignissim suspendisse in est ante in nibh mauris. Sed tempus urna et pharetra pharetra massa massa ultricies mi.
+
+Mollis nunc sed id semper risus in. Convallis a cras semper auctor neque. Diam sit amet nisl suscipit. Lacus viverra vitae congue eu consequat ac felis donec. Egestas integer eget aliquet nibh praesent tristique magna sit amet. Eget magna fermentum iaculis eu non diam. In vitae turpis massa sed elementum. Tristique et egestas quis ipsum suspendisse ultrices. Eget lorem dolor sed viverra ipsum. Vel turpis nunc eget lorem dolor sed viverra. Posuere ac ut consequat semper viverra nam. Laoreet suspendisse interdum consectetur libero id faucibus. Diam phasellus vestibulum lorem sed risus ultricies tristique. Rhoncus dolor purus non enim praesent elementum facilisis. Ultrices tincidunt arcu non sodales neque. Tempus egestas sed sed risus pretium quam vulputate. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare. Fringilla urna porttitor rhoncus dolor purus non. Amet dictum sit amet justo donec enim.
+
+Mattis ullamcorper velit sed ullamcorper morbi tincidunt. Tortor posuere ac ut consequat semper viverra. Tellus mauris a diam maecenas sed enim ut sem viverra. Venenatis urna cursus eget nunc scelerisque viverra mauris in. Arcu ac tortor dignissim convallis aenean et tortor at. Curabitur gravida arcu ac tortor dignissim convallis aenean et tortor. Egestas tellus rutrum tellus pellentesque eu. Fusce ut placerat orci nulla pellentesque dignissim enim sit amet. Ut enim blandit volutpat maecenas volutpat blandit aliquam etiam. Id donec ultrices tincidunt arcu. Id cursus metus aliquam eleifend mi.
+
+Tempus quam pellentesque nec nam aliquam sem. Risus at ultrices mi tempus imperdiet. Id porta nibh venenatis cras sed felis eget velit. Ipsum a arcu cursus vitae. Facilisis magna etiam tempor orci eu lobortis elementum. Tincidunt dui ut ornare lectus sit. Quisque non tellus orci ac. Blandit libero volutpat sed cras. Nec tincidunt praesent semper feugiat nibh sed pulvinar proin gravida. Egestas integer eget aliquet nibh praesent tristique magna.
diff --git a/packages/astro/test/fixtures/content-static-paths-integration/src/content/blog/third-post.md b/packages/astro/test/fixtures/content-static-paths-integration/src/content/blog/third-post.md
new file mode 100644
index 000000000..ac3a1b66c
--- /dev/null
+++ b/packages/astro/test/fixtures/content-static-paths-integration/src/content/blog/third-post.md
@@ -0,0 +1,18 @@
+---
+title: "Third post"
+description: "Lorem ipsum dolor sit amet"
+pubDate: "Jul 15 2022"
+heroImage: "/placeholder-hero.jpg"
+---
+
+> Third post loaded: yes!
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Vitae ultricies leo integer malesuada nunc vel risus commodo viverra. Adipiscing enim eu turpis egestas pretium. Euismod elementum nisi quis eleifend quam adipiscing. In hac habitasse platea dictumst vestibulum. Sagittis purus sit amet volutpat. Netus et malesuada fames ac turpis egestas. Eget magna fermentum iaculis eu non diam phasellus vestibulum lorem. Varius sit amet mattis vulputate enim. Habitasse platea dictumst quisque sagittis. Integer quis auctor elit sed vulputate mi. Dictumst quisque sagittis purus sit amet.
+
+Morbi tristique senectus et netus. Id semper risus in hendrerit gravida rutrum quisque non tellus. Habitasse platea dictumst quisque sagittis purus sit amet. Tellus molestie nunc non blandit massa. Cursus vitae congue mauris rhoncus. Accumsan tortor posuere ac ut. Fringilla urna porttitor rhoncus dolor. Elit ullamcorper dignissim cras tincidunt lobortis. In cursus turpis massa tincidunt dui ut ornare lectus. Integer feugiat scelerisque varius morbi enim nunc. Bibendum neque egestas congue quisque egestas diam. Cras ornare arcu dui vivamus arcu felis bibendum. Dignissim suspendisse in est ante in nibh mauris. Sed tempus urna et pharetra pharetra massa massa ultricies mi.
+
+Mollis nunc sed id semper risus in. Convallis a cras semper auctor neque. Diam sit amet nisl suscipit. Lacus viverra vitae congue eu consequat ac felis donec. Egestas integer eget aliquet nibh praesent tristique magna sit amet. Eget magna fermentum iaculis eu non diam. In vitae turpis massa sed elementum. Tristique et egestas quis ipsum suspendisse ultrices. Eget lorem dolor sed viverra ipsum. Vel turpis nunc eget lorem dolor sed viverra. Posuere ac ut consequat semper viverra nam. Laoreet suspendisse interdum consectetur libero id faucibus. Diam phasellus vestibulum lorem sed risus ultricies tristique. Rhoncus dolor purus non enim praesent elementum facilisis. Ultrices tincidunt arcu non sodales neque. Tempus egestas sed sed risus pretium quam vulputate. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus ornare. Fringilla urna porttitor rhoncus dolor purus non. Amet dictum sit amet justo donec enim.
+
+Mattis ullamcorper velit sed ullamcorper morbi tincidunt. Tortor posuere ac ut consequat semper viverra. Tellus mauris a diam maecenas sed enim ut sem viverra. Venenatis urna cursus eget nunc scelerisque viverra mauris in. Arcu ac tortor dignissim convallis aenean et tortor at. Curabitur gravida arcu ac tortor dignissim convallis aenean et tortor. Egestas tellus rutrum tellus pellentesque eu. Fusce ut placerat orci nulla pellentesque dignissim enim sit amet. Ut enim blandit volutpat maecenas volutpat blandit aliquam etiam. Id donec ultrices tincidunt arcu. Id cursus metus aliquam eleifend mi.
+
+Tempus quam pellentesque nec nam aliquam sem. Risus at ultrices mi tempus imperdiet. Id porta nibh venenatis cras sed felis eget velit. Ipsum a arcu cursus vitae. Facilisis magna etiam tempor orci eu lobortis elementum. Tincidunt dui ut ornare lectus sit. Quisque non tellus orci ac. Blandit libero volutpat sed cras. Nec tincidunt praesent semper feugiat nibh sed pulvinar proin gravida. Egestas integer eget aliquet nibh praesent tristique magna.
diff --git a/packages/astro/test/fixtures/content-static-paths-integration/src/content/blog/using-mdx.mdx b/packages/astro/test/fixtures/content-static-paths-integration/src/content/blog/using-mdx.mdx
new file mode 100644
index 000000000..036209d3b
--- /dev/null
+++ b/packages/astro/test/fixtures/content-static-paths-integration/src/content/blog/using-mdx.mdx
@@ -0,0 +1,31 @@
+---
+title: 'Using MDX'
+description: 'Lorem ipsum dolor sit amet'
+pubDate: 'Jul 02 2022'
+heroImage: '/placeholder-hero.jpg'
+---
+
+This theme comes with the [@astrojs/mdx](https://docs.astro.build/en/guides/integrations-guide/mdx/) integration installed and configured in your `astro.config.mjs` config file. If you prefer not to use MDX, you can disable support by removing the integration from your config file.
+
+## Why MDX?
+
+MDX is a special flavor of Markdown that supports embedded JavaScript & JSX syntax. This unlocks the ability to [mix JavaScript and UI Components into your Markdown content](https://docs.astro.build/en/guides/markdown-content/#mdx-features) for things like interactive charts or alerts.
+
+If you have existing content authored in MDX, this integration will hopefully make migrating to Astro a breeze.
+
+## Example
+
+Here is how you import and use a UI component inside of MDX.
+When you open this page in the browser, you should see the clickable button below.
+
+import HeaderLink from '../../components/HeaderLink.astro';
+
+
+ Embedded component in MDX
+
+
+## More Links
+
+- [MDX Syntax Documentation](https://mdxjs.com/docs/what-is-mdx)
+- [Astro Usage Documentation](https://docs.astro.build/en/guides/markdown-content/#markdown-and-mdx-pages)
+- **Note:** [Client Directives](https://docs.astro.build/en/reference/directives-reference/#client-directives) are still required to create interactive components. Otherwise, all components in your MDX will render as static HTML (no JavaScript) by default.
diff --git a/packages/astro/test/fixtures/content-static-paths-integration/src/content/config.ts b/packages/astro/test/fixtures/content-static-paths-integration/src/content/config.ts
new file mode 100644
index 000000000..f32eba6de
--- /dev/null
+++ b/packages/astro/test/fixtures/content-static-paths-integration/src/content/config.ts
@@ -0,0 +1,16 @@
+import { defineCollection, z } from 'astro:content';
+
+const blog = defineCollection({
+ schema: {
+ title: z.string(),
+ description: z.string(),
+ pubDate: z.string().transform((str) => new Date(str)),
+ updatedDate: z
+ .string()
+ .optional()
+ .transform((str) => (str ? new Date(str) : undefined)),
+ heroImage: z.string().optional(),
+ },
+});
+
+export const collections = { blog };
diff --git a/packages/astro/test/fixtures/content-static-paths-integration/src/content/types.generated.d.ts b/packages/astro/test/fixtures/content-static-paths-integration/src/content/types.generated.d.ts
new file mode 100644
index 000000000..7183d2433
--- /dev/null
+++ b/packages/astro/test/fixtures/content-static-paths-integration/src/content/types.generated.d.ts
@@ -0,0 +1,79 @@
+declare module 'astro:content' {
+ export { z } from 'astro/zod';
+ export type CollectionEntry =
+ typeof entryMap[C][keyof typeof entryMap[C]] & Render;
+
+ type BaseCollectionConfig = {
+ schema?: S;
+ slug?: (entry: {
+ id: CollectionEntry['id'];
+ defaultSlug: string;
+ collection: string;
+ body: string;
+ data: import('astro/zod').infer>;
+ }) => string | Promise;
+ };
+ export function defineCollection(
+ input: BaseCollectionConfig
+ ): BaseCollectionConfig;
+
+ export function getEntry(
+ collection: C,
+ entryKey: E
+ ): Promise;
+ export function getCollection<
+ C extends keyof typeof entryMap,
+ E extends keyof typeof entryMap[C]
+ >(
+ collection: C,
+ filter?: (data: typeof entryMap[C][E]) => boolean
+ ): Promise<(typeof entryMap[C][E] & Render)[]>;
+
+ type InferEntrySchema = import('astro/zod').infer<
+ import('astro/zod').ZodObject['schema']>
+ >;
+
+ type Render = {
+ render(): Promise<{
+ Content: import('astro').MarkdownInstance<{}>['Content'];
+ headings: import('astro').MarkdownHeading[];
+ injectedFrontmatter: Record;
+ }>;
+ };
+
+ const entryMap: {
+ "blog": {
+"first-post.md": {
+ id: "first-post.md",
+ slug: "first-post",
+ body: string,
+ collection: "blog",
+ data: InferEntrySchema<"blog">
+},
+"second-post.md": {
+ id: "second-post.md",
+ slug: "second-post",
+ body: string,
+ collection: "blog",
+ data: InferEntrySchema<"blog">
+},
+"third-post.md": {
+ id: "third-post.md",
+ slug: "third-post",
+ body: string,
+ collection: "blog",
+ data: InferEntrySchema<"blog">
+},
+"using-mdx.mdx": {
+ id: "using-mdx.mdx",
+ slug: "using-mdx",
+ body: string,
+ collection: "blog",
+ data: InferEntrySchema<"blog">
+},
+},
+
+ };
+
+ type ContentConfig = typeof import("./config");
+}
diff --git a/packages/astro/test/fixtures/content-static-paths-integration/src/pages/posts/[...slug].astro b/packages/astro/test/fixtures/content-static-paths-integration/src/pages/posts/[...slug].astro
new file mode 100644
index 000000000..7175b6110
--- /dev/null
+++ b/packages/astro/test/fixtures/content-static-paths-integration/src/pages/posts/[...slug].astro
@@ -0,0 +1,36 @@
+---
+import { getCollection } from 'astro:content';
+
+export async function getStaticPaths() {
+ const posts = await getCollection('blog');
+ return posts.map((post) => ({
+ params: { slug: post.slug },
+ props: post,
+ }));
+}
+
+const { data: { title } } = Astro.props;
+const { Content } = await Astro.props.render();
+---
+
+
+
+
+
+ {title}
+
+
+
+
{title}
+
+
+
diff --git a/packages/astro/test/fixtures/content/astro.config.mjs b/packages/astro/test/fixtures/content/astro.config.mjs
new file mode 100644
index 000000000..b331e201a
--- /dev/null
+++ b/packages/astro/test/fixtures/content/astro.config.mjs
@@ -0,0 +1,7 @@
+import { defineConfig } from 'astro/config';
+import mdx from '@astrojs/mdx';
+
+export default defineConfig({
+ integrations: [mdx()],
+ experimental: { contentCollections: true },
+});
diff --git a/packages/astro/test/fixtures/content/package.json b/packages/astro/test/fixtures/content/package.json
new file mode 100644
index 000000000..3be40eb87
--- /dev/null
+++ b/packages/astro/test/fixtures/content/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "@test/content",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "astro": "workspace:*",
+ "@astrojs/mdx": "workspace:*"
+ }
+}
diff --git a/packages/astro/test/fixtures/content/src/content/blog/one.md b/packages/astro/test/fixtures/content/src/content/blog/one.md
new file mode 100644
index 000000000..f4293f0a5
--- /dev/null
+++ b/packages/astro/test/fixtures/content/src/content/blog/one.md
@@ -0,0 +1,5 @@
+---
+title: Hello world
+description: Just a demo
+---
+This is a post
diff --git a/packages/astro/test/fixtures/content/src/content/blog/promo/launch-week-styles.css b/packages/astro/test/fixtures/content/src/content/blog/promo/launch-week-styles.css
new file mode 100644
index 000000000..cce2effe2
--- /dev/null
+++ b/packages/astro/test/fixtures/content/src/content/blog/promo/launch-week-styles.css
@@ -0,0 +1,3 @@
+body {
+ font-family: 'Comic Sans MS', sans-serif;
+}
diff --git a/packages/astro/test/fixtures/content/src/content/blog/promo/launch-week.mdx b/packages/astro/test/fixtures/content/src/content/blog/promo/launch-week.mdx
new file mode 100644
index 000000000..f7c4bac16
--- /dev/null
+++ b/packages/astro/test/fixtures/content/src/content/blog/promo/launch-week.mdx
@@ -0,0 +1,14 @@
+---
+title: 'Launch week!'
+description: 'Join us for the exciting launch of SPACE BLOG'
+publishedDate: 'Sat May 21 2022 00:00:00 GMT-0400 (Eastern Daylight Time)'
+tags: ['announcement']
+---
+
+import './launch-week-styles.css';
+
+Join us for the space blog launch!
+
+- THIS THURSDAY
+- Houston, TX
+- Dress code: **interstellar casual** ✨
diff --git a/packages/astro/test/fixtures/content/src/content/config.ts b/packages/astro/test/fixtures/content/src/content/config.ts
new file mode 100644
index 000000000..5c37f2755
--- /dev/null
+++ b/packages/astro/test/fixtures/content/src/content/config.ts
@@ -0,0 +1,10 @@
+import { z, defineCollection } from 'astro:content';
+
+const blog = defineCollection({
+ schema: {
+ title: z.string(),
+ description: z.string().max(60, 'For SEO purposes, keep descriptions short!'),
+ },
+});
+
+export const collections = { blog };
diff --git a/packages/astro/test/fixtures/content/src/content/types.generated.d.ts b/packages/astro/test/fixtures/content/src/content/types.generated.d.ts
new file mode 100644
index 000000000..99fd83ae0
--- /dev/null
+++ b/packages/astro/test/fixtures/content/src/content/types.generated.d.ts
@@ -0,0 +1,65 @@
+declare module 'astro:content' {
+ export { z } from 'astro/zod';
+ export type CollectionEntry =
+ typeof entryMap[C][keyof typeof entryMap[C]] & Render;
+
+ type BaseCollectionConfig = {
+ schema?: S;
+ slug?: (entry: {
+ id: CollectionEntry['id'];
+ defaultSlug: string;
+ collection: string;
+ body: string;
+ data: import('astro/zod').infer>;
+ }) => string | Promise;
+ };
+ export function defineCollection(
+ input: BaseCollectionConfig
+ ): BaseCollectionConfig;
+
+ export function getEntry(
+ collection: C,
+ entryKey: E
+ ): Promise;
+ export function getCollection<
+ C extends keyof typeof entryMap,
+ E extends keyof typeof entryMap[C]
+ >(
+ collection: C,
+ filter?: (data: typeof entryMap[C][E]) => boolean
+ ): Promise<(typeof entryMap[C][E] & Render)[]>;
+
+ type InferEntrySchema = import('astro/zod').infer<
+ import('astro/zod').ZodObject['schema']>
+ >;
+
+ type Render = {
+ render(): Promise<{
+ Content: import('astro').MarkdownInstance<{}>['Content'];
+ headings: import('astro').MarkdownHeading[];
+ injectedFrontmatter: Record;
+ }>;
+ };
+
+ const entryMap: {
+ "blog": {
+"one.md": {
+ id: "one.md",
+ slug: "one",
+ body: string,
+ collection: "blog",
+ data: InferEntrySchema<"blog">
+},
+"promo/launch-week.mdx": {
+ id: "promo/launch-week.mdx",
+ slug: "promo/launch-week",
+ body: string,
+ collection: "blog",
+ data: InferEntrySchema<"blog">
+},
+},
+
+ };
+
+ type ContentConfig = typeof import("./config");
+}
diff --git a/packages/astro/test/fixtures/content/src/pages/index.astro b/packages/astro/test/fixtures/content/src/pages/index.astro
new file mode 100644
index 000000000..a02472857
--- /dev/null
+++ b/packages/astro/test/fixtures/content/src/pages/index.astro
@@ -0,0 +1,19 @@
+---
+import { getCollection } from 'astro:content';
+
+const blog = await getCollection('blog');
+---
+
+
+ Index
+
+
+
Blog Posts
+
+
+ {blog.map(post => (
+
{ post.data.title }
+ ))}
+
+
+
diff --git a/packages/astro/test/fixtures/content/src/pages/launch-week.astro b/packages/astro/test/fixtures/content/src/pages/launch-week.astro
new file mode 100644
index 000000000..df343f599
--- /dev/null
+++ b/packages/astro/test/fixtures/content/src/pages/launch-week.astro
@@ -0,0 +1,15 @@
+---
+import { getCollection } from 'astro:content';
+
+const blog = await getCollection('blog');
+const launchWeekEntry = blog.find(post => post.id === 'promo/launch-week.mdx');
+const { Content } = await launchWeekEntry.render();
+---
+
+
+ Launch Week
+
+
+
+
+
diff --git a/packages/astro/test/renderEntry.test.js b/packages/astro/test/renderEntry.test.js
new file mode 100644
index 000000000..d4c5b795d
--- /dev/null
+++ b/packages/astro/test/renderEntry.test.js
@@ -0,0 +1,78 @@
+import { expect } from 'chai';
+import * as cheerio from 'cheerio';
+import { loadFixture, isWindows } from './test-utils.js';
+import testAdapter from './test-adapter.js';
+
+const describe = isWindows ? global.describe.skip : global.describe;
+
+describe('Content Collections - render()', () => {
+ describe('Build - SSG', () => {
+ /** @type {import('./test-utils').Fixture} */
+ let fixture;
+
+ before(async () => {
+ fixture = await loadFixture({
+ root: './fixtures/content/',
+ });
+ await fixture.build();
+ });
+
+ it('Page that render an entry include its CSS', async () => {
+ const html = await fixture.readFile('/launch-week/index.html');
+ const $ = cheerio.load(html);
+
+ // Renders content
+ expect($('ul li')).to.have.a.lengthOf(3);
+
+ // Includes styles
+ expect($('link[rel=stylesheet]')).to.have.a.lengthOf(1);
+ });
+
+ it('Page that does not render an entry does not include its CSS', async () => {
+ const html = await fixture.readFile('/index.html');
+ const $ = cheerio.load(html);
+
+ // Includes styles
+ expect($('link[rel=stylesheet]')).to.have.a.lengthOf(0);
+ });
+ });
+
+ describe('Build - SSR', () => {
+ /** @type {import('./test-utils').Fixture} */
+ let fixture;
+
+ before(async () => {
+ fixture = await loadFixture({
+ output: 'server',
+ root: './fixtures/content/',
+ adapter: testAdapter(),
+ });
+ await fixture.build();
+ });
+
+ it('Page that render an entry include its CSS', async () => {
+ const app = await fixture.loadTestAdapterApp();
+ const request = new Request('http://example.com/launch-week');
+ const response = await app.render(request);
+ const html = await response.text();
+ const $ = cheerio.load(html);
+
+ // Renders content
+ expect($('ul li')).to.have.a.lengthOf(3);
+
+ // Includes styles
+ expect($('link[rel=stylesheet]')).to.have.a.lengthOf(1);
+ });
+
+ it('Page that does not render an entry does not include its CSS', async () => {
+ const app = await fixture.loadTestAdapterApp();
+ const request = new Request('http://example.com/');
+ const response = await app.render(request);
+ const html = await response.text();
+ const $ = cheerio.load(html);
+
+ // Includes styles
+ expect($('link[rel=stylesheet]')).to.have.a.lengthOf(0);
+ });
+ });
+});
diff --git a/packages/astro/test/units/dev/collections-renderentry.test.js b/packages/astro/test/units/dev/collections-renderentry.test.js
new file mode 100644
index 000000000..072d004fa
--- /dev/null
+++ b/packages/astro/test/units/dev/collections-renderentry.test.js
@@ -0,0 +1,283 @@
+import { expect } from 'chai';
+import * as cheerio from 'cheerio';
+
+import { runInContainer } from '../../../dist/core/dev/index.js';
+import { createFsWithFallback, createRequestAndResponse } from '../test-utils.js';
+import { isWindows } from '../../test-utils.js';
+import mdx from '../../../../integrations/mdx/dist/index.js';
+
+const root = new URL('../../fixtures/content/', import.meta.url);
+
+const describe = isWindows ? global.describe.skip : global.describe;
+
+describe('Content Collections - render()', () => {
+ it('can be called in a page component', async () => {
+ const fs = createFsWithFallback(
+ {
+ '/src/content/config.ts': `
+ import { z, defineCollection } from 'astro:content';
+
+ const blog = defineCollection({
+ schema: {
+ title: z.string(),
+ description: z.string().max(60, 'For SEO purposes, keep descriptions short!'),
+ },
+ });
+
+ export const collections = { blog };
+ `,
+ '/src/pages/index.astro': `
+ ---
+ import { getCollection } from 'astro:content';
+ const blog = await getCollection('blog');
+ const launchWeekEntry = blog.find(post => post.id === 'promo/launch-week.mdx');
+ const { Content } = await launchWeekEntry.render();
+ ---
+