aboutsummaryrefslogtreecommitdiff
path: root/examples
diff options
context:
space:
mode:
authorGravatar github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> 2025-06-05 14:25:23 +0000
committerGravatar github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> 2025-06-05 14:25:23 +0000
commite586d7d704d475afe3373a1de6ae20d504f79d6d (patch)
tree7e3fa24807cebd48a86bd40f866d792181191ee9 /examples
downloadastro-latest.tar.gz
astro-latest.tar.zst
astro-latest.zip
Sync from a8e1c0a7402940e0fc5beef669522b315052df1blatest
Diffstat (limited to 'examples')
-rw-r--r--examples/README.md21
-rw-r--r--examples/basics/.codesandbox/Dockerfile1
-rw-r--r--examples/basics/.gitignore24
-rw-r--r--examples/basics/.vscode/extensions.json4
-rw-r--r--examples/basics/.vscode/launch.json11
-rw-r--r--examples/basics/README.md48
-rw-r--r--examples/basics/astro.config.mjs5
-rw-r--r--examples/basics/package.json15
-rw-r--r--examples/basics/public/favicon.svg9
-rw-r--r--examples/basics/src/assets/astro.svg1
-rw-r--r--examples/basics/src/assets/background.svg1
-rw-r--r--examples/basics/src/components/Welcome.astro210
-rw-r--r--examples/basics/src/layouts/Layout.astro22
-rw-r--r--examples/basics/src/pages/index.astro11
-rw-r--r--examples/basics/tsconfig.json5
-rw-r--r--examples/blog/.codesandbox/Dockerfile1
-rw-r--r--examples/blog/.gitignore24
-rw-r--r--examples/blog/.vscode/extensions.json4
-rw-r--r--examples/blog/.vscode/launch.json11
-rw-r--r--examples/blog/README.md68
-rw-r--r--examples/blog/astro.config.mjs10
-rw-r--r--examples/blog/package.json19
-rw-r--r--examples/blog/public/favicon.svg9
-rw-r--r--examples/blog/public/fonts/atkinson-bold.woffbin0 -> 23780 bytes
-rw-r--r--examples/blog/public/fonts/atkinson-regular.woffbin0 -> 22792 bytes
-rw-r--r--examples/blog/src/assets/blog-placeholder-1.jpgbin0 -> 32040 bytes
-rw-r--r--examples/blog/src/assets/blog-placeholder-2.jpgbin0 -> 33136 bytes
-rw-r--r--examples/blog/src/assets/blog-placeholder-3.jpgbin0 -> 28687 bytes
-rw-r--r--examples/blog/src/assets/blog-placeholder-4.jpgbin0 -> 38690 bytes
-rw-r--r--examples/blog/src/assets/blog-placeholder-5.jpgbin0 -> 34890 bytes
-rw-r--r--examples/blog/src/assets/blog-placeholder-about.jpgbin0 -> 21606 bytes
-rw-r--r--examples/blog/src/components/BaseHead.astro57
-rw-r--r--examples/blog/src/components/Footer.astro62
-rw-r--r--examples/blog/src/components/FormattedDate.astro17
-rw-r--r--examples/blog/src/components/Header.astro85
-rw-r--r--examples/blog/src/components/HeaderLink.astro24
-rw-r--r--examples/blog/src/consts.ts5
-rw-r--r--examples/blog/src/content.config.ts18
-rw-r--r--examples/blog/src/content/blog/first-post.md16
-rw-r--r--examples/blog/src/content/blog/markdown-style-guide.md214
-rw-r--r--examples/blog/src/content/blog/second-post.md16
-rw-r--r--examples/blog/src/content/blog/third-post.md16
-rw-r--r--examples/blog/src/content/blog/using-mdx.mdx31
-rw-r--r--examples/blog/src/layouts/BlogPost.astro86
-rw-r--r--examples/blog/src/pages/about.astro63
-rw-r--r--examples/blog/src/pages/blog/[...slug].astro21
-rw-r--r--examples/blog/src/pages/blog/index.astro114
-rw-r--r--examples/blog/src/pages/index.astro49
-rw-r--r--examples/blog/src/pages/rss.xml.js16
-rw-r--r--examples/blog/src/styles/global.css155
-rw-r--r--examples/blog/tsconfig.json8
-rw-r--r--examples/component/.gitignore24
-rw-r--r--examples/component/README.md35
-rw-r--r--examples/component/index.ts6
-rw-r--r--examples/component/package.json23
-rw-r--r--examples/component/src/MyComponent.astro8
-rw-r--r--examples/component/tsconfig.json8
-rw-r--r--examples/container-with-vitest/.codesandbox/Dockerfile1
-rw-r--r--examples/container-with-vitest/.gitignore24
-rw-r--r--examples/container-with-vitest/README.md11
-rw-r--r--examples/container-with-vitest/astro.config.ts7
-rw-r--r--examples/container-with-vitest/package.json24
-rw-r--r--examples/container-with-vitest/public/favicon.svg9
-rw-r--r--examples/container-with-vitest/src/components/Card.astro8
-rw-r--r--examples/container-with-vitest/src/components/Counter.jsx14
-rw-r--r--examples/container-with-vitest/src/components/CounterLight.astro9
-rw-r--r--examples/container-with-vitest/src/components/ReactWrapper.astro5
-rw-r--r--examples/container-with-vitest/src/pages/[locale].astro20
-rw-r--r--examples/container-with-vitest/src/pages/api.ts11
-rw-r--r--examples/container-with-vitest/src/pages/index.astro16
-rw-r--r--examples/container-with-vitest/test/Card.test.ts29
-rw-r--r--examples/container-with-vitest/test/ReactWrapper.test.ts17
-rw-r--r--examples/container-with-vitest/test/[locale].test.ts16
-rw-r--r--examples/container-with-vitest/tsconfig.json5
-rw-r--r--examples/container-with-vitest/vitest.config.ts9
-rw-r--r--examples/framework-alpine/.codesandbox/Dockerfile1
-rw-r--r--examples/framework-alpine/.gitignore24
-rw-r--r--examples/framework-alpine/.vscode/extensions.json4
-rw-r--r--examples/framework-alpine/.vscode/launch.json11
-rw-r--r--examples/framework-alpine/README.md11
-rw-r--r--examples/framework-alpine/astro.config.mjs8
-rw-r--r--examples/framework-alpine/package.json18
-rw-r--r--examples/framework-alpine/public/favicon.svg9
-rw-r--r--examples/framework-alpine/src/components/Counter.astro34
-rw-r--r--examples/framework-alpine/src/pages/index.astro39
-rw-r--r--examples/framework-alpine/tsconfig.json5
-rw-r--r--examples/framework-multiple/.codesandbox/Dockerfile1
-rw-r--r--examples/framework-multiple/.gitignore24
-rw-r--r--examples/framework-multiple/.vscode/extensions.json4
-rw-r--r--examples/framework-multiple/.vscode/launch.json11
-rw-r--r--examples/framework-multiple/README.md13
-rw-r--r--examples/framework-multiple/astro.config.mjs19
-rw-r--r--examples/framework-multiple/package.json28
-rw-r--r--examples/framework-multiple/public/favicon.svg9
-rw-r--r--examples/framework-multiple/src/components/preact/PreactCounter.tsx22
-rw-r--r--examples/framework-multiple/src/components/react/ReactCounter.tsx21
-rw-r--r--examples/framework-multiple/src/components/solid/SolidCounter.tsx21
-rw-r--r--examples/framework-multiple/src/components/svelte/SvelteCounter.svelte30
-rw-r--r--examples/framework-multiple/src/components/vue/VueCounter.vue33
-rw-r--r--examples/framework-multiple/src/pages/index.astro48
-rw-r--r--examples/framework-multiple/src/styles/global.css21
-rw-r--r--examples/framework-multiple/tsconfig.json9
-rw-r--r--examples/framework-preact/.codesandbox/Dockerfile1
-rw-r--r--examples/framework-preact/.gitignore24
-rw-r--r--examples/framework-preact/.vscode/extensions.json4
-rw-r--r--examples/framework-preact/.vscode/launch.json11
-rw-r--r--examples/framework-preact/README.md13
-rw-r--r--examples/framework-preact/astro.config.mjs9
-rw-r--r--examples/framework-preact/package.json18
-rw-r--r--examples/framework-preact/public/favicon.svg9
-rw-r--r--examples/framework-preact/src/components/Counter.css7
-rw-r--r--examples/framework-preact/src/components/Counter.tsx30
-rw-r--r--examples/framework-preact/src/components/Message.css3
-rw-r--r--examples/framework-preact/src/components/Message.tsx6
-rw-r--r--examples/framework-preact/src/pages/index.astro41
-rw-r--r--examples/framework-preact/tsconfig.json10
-rw-r--r--examples/framework-react/.codesandbox/Dockerfile1
-rw-r--r--examples/framework-react/.gitignore24
-rw-r--r--examples/framework-react/.vscode/extensions.json4
-rw-r--r--examples/framework-react/.vscode/launch.json11
-rw-r--r--examples/framework-react/README.md13
-rw-r--r--examples/framework-react/astro.config.mjs9
-rw-r--r--examples/framework-react/package.json20
-rw-r--r--examples/framework-react/public/favicon.svg9
-rw-r--r--examples/framework-react/src/components/Counter.css11
-rw-r--r--examples/framework-react/src/components/Counter.tsx25
-rw-r--r--examples/framework-react/src/pages/index.astro36
-rw-r--r--examples/framework-react/tsconfig.json9
-rw-r--r--examples/framework-solid/.codesandbox/Dockerfile1
-rw-r--r--examples/framework-solid/.gitignore24
-rw-r--r--examples/framework-solid/.vscode/extensions.json4
-rw-r--r--examples/framework-solid/.vscode/launch.json11
-rw-r--r--examples/framework-solid/README.md13
-rw-r--r--examples/framework-solid/astro.config.mjs9
-rw-r--r--examples/framework-solid/package.json17
-rw-r--r--examples/framework-solid/public/favicon.svg9
-rw-r--r--examples/framework-solid/src/components/Counter.css11
-rw-r--r--examples/framework-solid/src/components/Counter.tsx19
-rw-r--r--examples/framework-solid/src/pages/index.astro33
-rw-r--r--examples/framework-solid/tsconfig.json10
-rw-r--r--examples/framework-svelte/.codesandbox/Dockerfile1
-rw-r--r--examples/framework-svelte/.gitignore24
-rw-r--r--examples/framework-svelte/.vscode/extensions.json4
-rw-r--r--examples/framework-svelte/.vscode/launch.json11
-rw-r--r--examples/framework-svelte/README.md11
-rw-r--r--examples/framework-svelte/astro.config.mjs9
-rw-r--r--examples/framework-svelte/package.json17
-rw-r--r--examples/framework-svelte/public/favicon.svg9
-rw-r--r--examples/framework-svelte/src/components/Counter.svelte41
-rw-r--r--examples/framework-svelte/src/pages/index.astro33
-rw-r--r--examples/framework-svelte/svelte.config.js5
-rw-r--r--examples/framework-svelte/tsconfig.json5
-rw-r--r--examples/framework-vue/.codesandbox/Dockerfile1
-rw-r--r--examples/framework-vue/.gitignore24
-rw-r--r--examples/framework-vue/.vscode/extensions.json4
-rw-r--r--examples/framework-vue/.vscode/launch.json11
-rw-r--r--examples/framework-vue/README.md11
-rw-r--r--examples/framework-vue/astro.config.mjs9
-rw-r--r--examples/framework-vue/package.json17
-rw-r--r--examples/framework-vue/public/favicon.svg9
-rw-r--r--examples/framework-vue/src/components/Counter.vue32
-rw-r--r--examples/framework-vue/src/pages/index.astro33
-rw-r--r--examples/framework-vue/tsconfig.json9
-rw-r--r--examples/hackernews/.codesandbox/Dockerfile1
-rw-r--r--examples/hackernews/.gitignore24
-rw-r--r--examples/hackernews/.vscode/extensions.json4
-rw-r--r--examples/hackernews/.vscode/launch.json11
-rw-r--r--examples/hackernews/README.md59
-rw-r--r--examples/hackernews/astro.config.mjs11
-rw-r--r--examples/hackernews/package.json16
-rw-r--r--examples/hackernews/public/favicon.svg9
-rw-r--r--examples/hackernews/src/components/Comment.astro59
-rw-r--r--examples/hackernews/src/components/For.astro23
-rw-r--r--examples/hackernews/src/components/Nav.astro99
-rw-r--r--examples/hackernews/src/components/Show.astro9
-rw-r--r--examples/hackernews/src/components/Story.astro77
-rw-r--r--examples/hackernews/src/components/Toggle.astro78
-rw-r--r--examples/hackernews/src/layouts/Layout.astro36
-rw-r--r--examples/hackernews/src/lib/api.ts24
-rw-r--r--examples/hackernews/src/pages/[...stories].astro105
-rw-r--r--examples/hackernews/src/pages/stories/[id].astro96
-rw-r--r--examples/hackernews/src/pages/users/[id].astro69
-rw-r--r--examples/hackernews/src/types.ts27
-rw-r--r--examples/hackernews/tsconfig.json5
-rw-r--r--examples/integration/.gitignore24
-rw-r--r--examples/integration/README.md33
-rw-r--r--examples/integration/index.ts23
-rw-r--r--examples/integration/package.json23
-rw-r--r--examples/integration/tsconfig.json3
-rw-r--r--examples/minimal/.codesandbox/Dockerfile1
-rw-r--r--examples/minimal/.gitignore24
-rw-r--r--examples/minimal/.vscode/extensions.json4
-rw-r--r--examples/minimal/.vscode/launch.json11
-rw-r--r--examples/minimal/README.md47
-rw-r--r--examples/minimal/astro.config.mjs5
-rw-r--r--examples/minimal/package.json15
-rw-r--r--examples/minimal/public/favicon.svg9
-rw-r--r--examples/minimal/src/pages/index.astro16
-rw-r--r--examples/minimal/tsconfig.json5
-rw-r--r--examples/portfolio/.codesandbox/Dockerfile1
-rw-r--r--examples/portfolio/.gitignore24
-rw-r--r--examples/portfolio/.vscode/extensions.json4
-rw-r--r--examples/portfolio/.vscode/launch.json11
-rw-r--r--examples/portfolio/README.md30
-rw-r--r--examples/portfolio/astro.config.mjs5
-rw-r--r--examples/portfolio/package.json15
-rw-r--r--examples/portfolio/public/assets/at-work.jpgbin0 -> 100769 bytes
-rw-r--r--examples/portfolio/public/assets/backgrounds/bg-footer-dark-1440w.jpgbin0 -> 15589 bytes
-rw-r--r--examples/portfolio/public/assets/backgrounds/bg-footer-dark-800w.jpgbin0 -> 6360 bytes
-rw-r--r--examples/portfolio/public/assets/backgrounds/bg-footer-light-1440w.jpgbin0 -> 12614 bytes
-rw-r--r--examples/portfolio/public/assets/backgrounds/bg-footer-light-800w.jpgbin0 -> 4735 bytes
-rw-r--r--examples/portfolio/public/assets/backgrounds/bg-main-dark-1440w.jpgbin0 -> 28973 bytes
-rw-r--r--examples/portfolio/public/assets/backgrounds/bg-main-dark-800w.jpgbin0 -> 12727 bytes
-rw-r--r--examples/portfolio/public/assets/backgrounds/bg-main-dark.svg1
-rw-r--r--examples/portfolio/public/assets/backgrounds/bg-main-light-1440w.jpgbin0 -> 21502 bytes
-rw-r--r--examples/portfolio/public/assets/backgrounds/bg-main-light-800w.jpgbin0 -> 14098 bytes
-rw-r--r--examples/portfolio/public/assets/backgrounds/bg-main-light.svg1
-rw-r--r--examples/portfolio/public/assets/backgrounds/bg-subtle-1-dark-1440w.jpgbin0 -> 21122 bytes
-rw-r--r--examples/portfolio/public/assets/backgrounds/bg-subtle-1-dark-800w.jpgbin0 -> 6710 bytes
-rw-r--r--examples/portfolio/public/assets/backgrounds/bg-subtle-1-light-1440w.jpgbin0 -> 15807 bytes
-rw-r--r--examples/portfolio/public/assets/backgrounds/bg-subtle-1-light-800w.jpgbin0 -> 6340 bytes
-rw-r--r--examples/portfolio/public/assets/backgrounds/bg-subtle-2-dark-1440w.jpgbin0 -> 16804 bytes
-rw-r--r--examples/portfolio/public/assets/backgrounds/bg-subtle-2-dark-800w.jpgbin0 -> 7519 bytes
-rw-r--r--examples/portfolio/public/assets/backgrounds/bg-subtle-2-light-1440w.jpgbin0 -> 14445 bytes
-rw-r--r--examples/portfolio/public/assets/backgrounds/bg-subtle-2-light-800w.jpgbin0 -> 5420 bytes
-rw-r--r--examples/portfolio/public/assets/backgrounds/noise.pngbin0 -> 27223 bytes
-rw-r--r--examples/portfolio/public/assets/portrait.jpgbin0 -> 131394 bytes
-rw-r--r--examples/portfolio/public/assets/stock-1.jpgbin0 -> 39459 bytes
-rw-r--r--examples/portfolio/public/assets/stock-2.jpgbin0 -> 20120 bytes
-rw-r--r--examples/portfolio/public/assets/stock-3.jpgbin0 -> 21810 bytes
-rw-r--r--examples/portfolio/public/assets/stock-4.jpgbin0 -> 27864 bytes
-rw-r--r--examples/portfolio/public/favicon.svg9
-rw-r--r--examples/portfolio/src/components/CallToAction.astro56
-rw-r--r--examples/portfolio/src/components/ContactCTA.astro46
-rw-r--r--examples/portfolio/src/components/Footer.astro74
-rw-r--r--examples/portfolio/src/components/Grid.astro65
-rw-r--r--examples/portfolio/src/components/Hero.astro54
-rw-r--r--examples/portfolio/src/components/Icon.astro56
-rw-r--r--examples/portfolio/src/components/IconPaths.ts38
-rw-r--r--examples/portfolio/src/components/MainHead.astro47
-rw-r--r--examples/portfolio/src/components/Nav.astro354
-rw-r--r--examples/portfolio/src/components/Pill.astro16
-rw-r--r--examples/portfolio/src/components/PortfolioPreview.astro64
-rw-r--r--examples/portfolio/src/components/Skills.astro62
-rw-r--r--examples/portfolio/src/components/ThemeToggle.astro95
-rw-r--r--examples/portfolio/src/content.config.ts17
-rw-r--r--examples/portfolio/src/content/work/bloom-box.md23
-rw-r--r--examples/portfolio/src/content/work/h20.md22
-rw-r--r--examples/portfolio/src/content/work/markdown-mystery-tour.md35
-rw-r--r--examples/portfolio/src/content/work/nested/duvet-genius.md22
-rw-r--r--examples/portfolio/src/layouts/BaseLayout.astro118
-rw-r--r--examples/portfolio/src/pages/404.astro8
-rw-r--r--examples/portfolio/src/pages/about.astro120
-rw-r--r--examples/portfolio/src/pages/index.astro252
-rw-r--r--examples/portfolio/src/pages/work.astro39
-rw-r--r--examples/portfolio/src/pages/work/[...slug].astro152
-rw-r--r--examples/portfolio/src/styles/global.css262
-rw-r--r--examples/portfolio/tsconfig.json5
-rw-r--r--examples/ssr/.codesandbox/Dockerfile1
-rw-r--r--examples/ssr/.gitignore24
-rw-r--r--examples/ssr/.vscode/extensions.json4
-rw-r--r--examples/ssr/.vscode/launch.json11
-rw-r--r--examples/ssr/astro.config.mjs13
-rw-r--r--examples/ssr/package.json19
-rw-r--r--examples/ssr/public/favicon.svg9
-rw-r--r--examples/ssr/public/images/products/cereal.jpgbin0 -> 497260 bytes
-rw-r--r--examples/ssr/public/images/products/muffins.jpgbin0 -> 141531 bytes
-rw-r--r--examples/ssr/public/images/products/oats.jpgbin0 -> 102566 bytes
-rw-r--r--examples/ssr/public/images/products/yogurt.jpgbin0 -> 79431 bytes
-rw-r--r--examples/ssr/src/api.ts72
-rw-r--r--examples/ssr/src/components/AddToCart.svelte53
-rw-r--r--examples/ssr/src/components/Cart.svelte34
-rw-r--r--examples/ssr/src/components/Container.astro13
-rw-r--r--examples/ssr/src/components/Header.astro49
-rw-r--r--examples/ssr/src/components/ProductListing.astro70
-rw-r--r--examples/ssr/src/components/TextDecorationSkip.astro23
-rw-r--r--examples/ssr/src/models/db.json28
-rw-r--r--examples/ssr/src/models/db.ts6
-rw-r--r--examples/ssr/src/models/session.ts2
-rw-r--r--examples/ssr/src/pages/api/cart.ts38
-rw-r--r--examples/ssr/src/pages/api/products.ts5
-rw-r--r--examples/ssr/src/pages/api/products/[id].ts16
-rw-r--r--examples/ssr/src/pages/cart.astro51
-rw-r--r--examples/ssr/src/pages/index.astro33
-rw-r--r--examples/ssr/src/pages/login.astro58
-rw-r--r--examples/ssr/src/pages/login.form.async.ts14
-rw-r--r--examples/ssr/src/pages/login.form.ts16
-rw-r--r--examples/ssr/src/pages/products/[id].astro45
-rw-r--r--examples/ssr/src/styles/common.css3
-rw-r--r--examples/ssr/tsconfig.json5
-rw-r--r--examples/starlog/.gitignore24
-rw-r--r--examples/starlog/README.md7
-rw-r--r--examples/starlog/astro.config.mjs7
-rw-r--r--examples/starlog/package.json16
-rw-r--r--examples/starlog/public/favicon.svg9
-rw-r--r--examples/starlog/src/assets/starlog-placeholder-1.jpgbin0 -> 408855 bytes
-rw-r--r--examples/starlog/src/assets/starlog-placeholder-14.jpgbin0 -> 363691 bytes
-rw-r--r--examples/starlog/src/assets/starlog-placeholder-18.jpgbin0 -> 385009 bytes
-rw-r--r--examples/starlog/src/assets/starlog-placeholder-2.jpgbin0 -> 453830 bytes
-rw-r--r--examples/starlog/src/components/BaseHead.astro21
-rw-r--r--examples/starlog/src/components/Footer.astro12
-rw-r--r--examples/starlog/src/components/FormattedDate.astro25
-rw-r--r--examples/starlog/src/components/Header.astro54
-rw-r--r--examples/starlog/src/components/SEO.astro87
-rw-r--r--examples/starlog/src/consts.ts5
-rw-r--r--examples/starlog/src/content.config.ts22
-rw-r--r--examples/starlog/src/content/releases/1_0.md29
-rw-r--r--examples/starlog/src/content/releases/1_4.md29
-rw-r--r--examples/starlog/src/content/releases/1_8.md29
-rw-r--r--examples/starlog/src/content/releases/2_0.md38
-rw-r--r--examples/starlog/src/layouts/IndexLayout.astro22
-rw-r--r--examples/starlog/src/layouts/PostLayout.astro39
-rw-r--r--examples/starlog/src/pages/index.astro36
-rw-r--r--examples/starlog/src/pages/releases/[slug].astro21
-rw-r--r--examples/starlog/src/styles/colors.scss63
-rw-r--r--examples/starlog/src/styles/global.scss3
-rw-r--r--examples/starlog/src/styles/layout.scss295
-rw-r--r--examples/starlog/src/styles/type.scss70
-rw-r--r--examples/starlog/tsconfig.json5
-rw-r--r--examples/toolbar-app/.codesandbox/Dockerfile1
-rw-r--r--examples/toolbar-app/.gitignore21
-rw-r--r--examples/toolbar-app/README.md40
-rw-r--r--examples/toolbar-app/package.json21
-rw-r--r--examples/toolbar-app/src/app.ts16
-rw-r--r--examples/toolbar-app/src/integration.ts17
-rw-r--r--examples/toolbar-app/tsconfig.json7
-rw-r--r--examples/with-markdoc/.codesandbox/Dockerfile1
-rw-r--r--examples/with-markdoc/.gitignore24
-rw-r--r--examples/with-markdoc/.vscode/extensions.json4
-rw-r--r--examples/with-markdoc/.vscode/launch.json11
-rw-r--r--examples/with-markdoc/README.md56
-rw-r--r--examples/with-markdoc/astro.config.mjs8
-rw-r--r--examples/with-markdoc/markdoc.config.mjs13
-rw-r--r--examples/with-markdoc/package.json16
-rw-r--r--examples/with-markdoc/public/favicon.svg9
-rw-r--r--examples/with-markdoc/src/components/Aside.astro116
-rw-r--r--examples/with-markdoc/src/content.config.ts5
-rw-r--r--examples/with-markdoc/src/content/docs/intro.mdoc39
-rw-r--r--examples/with-markdoc/src/layouts/Layout.astro42
-rw-r--r--examples/with-markdoc/src/pages/index.astro28
-rw-r--r--examples/with-markdoc/tsconfig.json8
-rw-r--r--examples/with-mdx/.codesandbox/Dockerfile1
-rw-r--r--examples/with-mdx/.gitignore24
-rw-r--r--examples/with-mdx/.vscode/extensions.json4
-rw-r--r--examples/with-mdx/.vscode/launch.json11
-rw-r--r--examples/with-mdx/README.md11
-rw-r--r--examples/with-mdx/astro.config.mjs9
-rw-r--r--examples/with-mdx/package.json18
-rw-r--r--examples/with-mdx/public/favicon.svg9
-rw-r--r--examples/with-mdx/src/components/Counter.jsx18
-rw-r--r--examples/with-mdx/src/components/Title.astro7
-rw-r--r--examples/with-mdx/src/pages/index.mdx29
-rw-r--r--examples/with-mdx/tsconfig.json5
-rw-r--r--examples/with-nanostores/.codesandbox/Dockerfile1
-rw-r--r--examples/with-nanostores/.gitignore24
-rw-r--r--examples/with-nanostores/README.md11
-rw-r--r--examples/with-nanostores/astro.config.mjs9
-rw-r--r--examples/with-nanostores/package.json19
-rw-r--r--examples/with-nanostores/public/favicon.svg9
-rw-r--r--examples/with-nanostores/public/images/astronaut-figurine.pngbin0 -> 498339 bytes
-rw-r--r--examples/with-nanostores/src/cartStore.ts31
-rw-r--r--examples/with-nanostores/src/components/AddToCartForm.tsx18
-rw-r--r--examples/with-nanostores/src/components/CartFlyout.module.css29
-rw-r--r--examples/with-nanostores/src/components/CartFlyout.tsx28
-rw-r--r--examples/with-nanostores/src/components/CartFlyoutToggle.tsx7
-rw-r--r--examples/with-nanostores/src/components/FigurineDescription.astro44
-rw-r--r--examples/with-nanostores/src/layouts/Layout.astro113
-rw-r--r--examples/with-nanostores/src/pages/index.astro50
-rw-r--r--examples/with-nanostores/src/utils.ts4
-rw-r--r--examples/with-nanostores/tsconfig.json10
-rw-r--r--examples/with-tailwindcss/.codesandbox/Dockerfile1
-rw-r--r--examples/with-tailwindcss/.gitignore24
-rw-r--r--examples/with-tailwindcss/README.md13
-rw-r--r--examples/with-tailwindcss/astro.config.mjs10
-rw-r--r--examples/with-tailwindcss/package.json20
-rw-r--r--examples/with-tailwindcss/public/favicon.svg9
-rw-r--r--examples/with-tailwindcss/src/components/Button.astro19
-rw-r--r--examples/with-tailwindcss/src/layouts/main.astro16
-rw-r--r--examples/with-tailwindcss/src/pages/index.astro25
-rw-r--r--examples/with-tailwindcss/src/pages/markdown-page.md16
-rw-r--r--examples/with-tailwindcss/src/styles/global.css1
-rw-r--r--examples/with-tailwindcss/tsconfig.json5
-rw-r--r--examples/with-vitest/.codesandbox/Dockerfile1
-rw-r--r--examples/with-vitest/.gitignore24
-rw-r--r--examples/with-vitest/README.md11
-rw-r--r--examples/with-vitest/astro.config.ts5
-rw-r--r--examples/with-vitest/package.json17
-rw-r--r--examples/with-vitest/public/favicon.svg9
-rw-r--r--examples/with-vitest/src/pages/index.astro16
-rw-r--r--examples/with-vitest/test/basic.test.ts21
-rw-r--r--examples/with-vitest/tsconfig.json5
-rw-r--r--examples/with-vitest/vitest.config.ts9
392 files changed, 9577 insertions, 0 deletions
diff --git a/examples/README.md b/examples/README.md
new file mode 100644
index 000000000..1482084d4
--- /dev/null
+++ b/examples/README.md
@@ -0,0 +1,21 @@
+# Astro Examples Library
+
+The easiest way to check out one of these examples on your machine is by running this command in an empty directory:
+
+```sh
+npm create astro@latest -- --template [EXAMPLE_NAME]
+```
+
+## Community Examples
+
+Visit [awesome-astro](https://github.com/one-aalam/awesome-astro) for a full list of community examples. You can use `npm create astro@latest` to check out any community examples:
+
+```sh
+npm create astro@latest -- --template [GITHUB_USER]/[REPO_NAME]
+```
+
+Paths to examples nested inside of a repo are also supported:
+
+```sh
+npm create astro@latest -- --template [GITHUB_USER]/[REPO_NAME]/path/to/example
+```
diff --git a/examples/basics/.codesandbox/Dockerfile b/examples/basics/.codesandbox/Dockerfile
new file mode 100644
index 000000000..c3b5c81a1
--- /dev/null
+++ b/examples/basics/.codesandbox/Dockerfile
@@ -0,0 +1 @@
+FROM node:18-bullseye
diff --git a/examples/basics/.gitignore b/examples/basics/.gitignore
new file mode 100644
index 000000000..016b59ea1
--- /dev/null
+++ b/examples/basics/.gitignore
@@ -0,0 +1,24 @@
+# build output
+dist/
+
+# generated types
+.astro/
+
+# dependencies
+node_modules/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
+
+# jetbrains setting folder
+.idea/
diff --git a/examples/basics/.vscode/extensions.json b/examples/basics/.vscode/extensions.json
new file mode 100644
index 000000000..22a15055d
--- /dev/null
+++ b/examples/basics/.vscode/extensions.json
@@ -0,0 +1,4 @@
+{
+ "recommendations": ["astro-build.astro-vscode"],
+ "unwantedRecommendations": []
+}
diff --git a/examples/basics/.vscode/launch.json b/examples/basics/.vscode/launch.json
new file mode 100644
index 000000000..d64220976
--- /dev/null
+++ b/examples/basics/.vscode/launch.json
@@ -0,0 +1,11 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "command": "./node_modules/.bin/astro dev",
+ "name": "Development server",
+ "request": "launch",
+ "type": "node-terminal"
+ }
+ ]
+}
diff --git a/examples/basics/README.md b/examples/basics/README.md
new file mode 100644
index 000000000..ff19a3e7e
--- /dev/null
+++ b/examples/basics/README.md
@@ -0,0 +1,48 @@
+# Astro Starter Kit: Basics
+
+```sh
+npm create astro@latest -- --template basics
+```
+
+[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics)
+[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/basics)
+[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/basics/devcontainer.json)
+
+> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
+
+![just-the-basics](https://github.com/withastro/astro/assets/2244813/a0a5533c-a856-4198-8470-2d67b1d7c554)
+
+## 🚀 Project Structure
+
+Inside of your Astro project, you'll see the following folders and files:
+
+```text
+/
+├── public/
+│ └── favicon.svg
+├── src/
+│ ├── layouts/
+│ │ └── Layout.astro
+│ └── pages/
+│ └── index.astro
+└── package.json
+```
+
+To learn more about the folder structure of an Astro project, refer to [our guide on project structure](https://docs.astro.build/en/basics/project-structure/).
+
+## 🧞 Commands
+
+All commands are run from the root of the project, from a terminal:
+
+| Command | Action |
+| :------------------------ | :----------------------------------------------- |
+| `npm install` | Installs dependencies |
+| `npm run dev` | Starts local dev server at `localhost:4321` |
+| `npm run build` | Build your production site to `./dist/` |
+| `npm run preview` | Preview your build locally, before deploying |
+| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
+| `npm run astro -- --help` | Get help using the Astro CLI |
+
+## 👀 Want to learn more?
+
+Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
diff --git a/examples/basics/astro.config.mjs b/examples/basics/astro.config.mjs
new file mode 100644
index 000000000..e762ba5cf
--- /dev/null
+++ b/examples/basics/astro.config.mjs
@@ -0,0 +1,5 @@
+// @ts-check
+import { defineConfig } from 'astro/config';
+
+// https://astro.build/config
+export default defineConfig({});
diff --git a/examples/basics/package.json b/examples/basics/package.json
new file mode 100644
index 000000000..072a4e0fe
--- /dev/null
+++ b/examples/basics/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "@example/basics",
+ "type": "module",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "dev": "astro dev",
+ "build": "astro build",
+ "preview": "astro preview",
+ "astro": "astro"
+ },
+ "dependencies": {
+ "astro": "^5.9.0"
+ }
+}
diff --git a/examples/basics/public/favicon.svg b/examples/basics/public/favicon.svg
new file mode 100644
index 000000000..f157bd1c5
--- /dev/null
+++ b/examples/basics/public/favicon.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
+ <path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
+ <style>
+ path { fill: #000; }
+ @media (prefers-color-scheme: dark) {
+ path { fill: #FFF; }
+ }
+ </style>
+</svg>
diff --git a/examples/basics/src/assets/astro.svg b/examples/basics/src/assets/astro.svg
new file mode 100644
index 000000000..8cf8fb0c7
--- /dev/null
+++ b/examples/basics/src/assets/astro.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="115" height="48"><path fill="#17191E" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="url(#a)" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="#17191E" d="M.02 30.31s4.02-1.95 8.05-1.95l3.04-9.4c.11-.45.44-.76.82-.76.37 0 .7.31.82.76l3.04 9.4c4.77 0 8.05 1.95 8.05 1.95L17 11.71c-.2-.56-.53-.91-.98-.91H7.83c-.44 0-.76.35-.97.9L.02 30.31Zm42.37-5.97c0 1.64-2.05 2.62-4.88 2.62-1.85 0-2.5-.45-2.5-1.41 0-1 .8-1.49 2.65-1.49 1.67 0 3.09.03 4.73.23v.05Zm.03-2.04a21.37 21.37 0 0 0-4.37-.36c-5.32 0-7.82 1.25-7.82 4.18 0 3.04 1.71 4.2 5.68 4.2 3.35 0 5.63-.84 6.46-2.92h.14c-.03.5-.05 1-.05 1.4 0 1.07.18 1.16 1.06 1.16h4.15a16.9 16.9 0 0 1-.36-4c0-1.67.06-2.93.06-4.62 0-3.45-2.07-5.64-8.56-5.64-2.8 0-5.9.48-8.26 1.19.22.93.54 2.83.7 4.06 2.04-.96 4.95-1.37 7.2-1.37 3.11 0 3.97.71 3.97 2.15v.57Zm11.37 3c-.56.07-1.33.07-2.12.07-.83 0-1.6-.03-2.12-.1l-.02.58c0 2.85 1.87 4.52 8.45 4.52 6.2 0 8.2-1.64 8.2-4.55 0-2.74-1.33-4.09-7.2-4.39-4.58-.2-4.99-.7-4.99-1.28 0-.66.59-1 3.65-1 3.18 0 4.03.43 4.03 1.35v.2a46.13 46.13 0 0 1 4.24.03l.02-.55c0-3.36-2.8-4.46-8.2-4.46-6.08 0-8.13 1.49-8.13 4.39 0 2.6 1.64 4.23 7.48 4.48 4.3.14 4.77.62 4.77 1.28 0 .7-.7 1.03-3.71 1.03-3.47 0-4.35-.48-4.35-1.47v-.13Zm19.82-12.05a17.5 17.5 0 0 1-6.24 3.48c.03.84.03 2.4.03 3.24l1.5.02c-.02 1.63-.04 3.6-.04 4.9 0 3.04 1.6 5.32 6.58 5.32 2.1 0 3.5-.23 5.23-.6a43.77 43.77 0 0 1-.46-4.13c-1.03.34-2.34.53-3.78.53-2 0-2.82-.55-2.82-2.13 0-1.37 0-2.65.03-3.84 2.57.02 5.13.07 6.64.11-.02-1.18.03-2.9.1-4.04-2.2.04-4.65.07-6.68.07l.07-2.93h-.16Zm13.46 6.04a767.33 767.33 0 0 1 .07-3.18H82.6c.07 1.96.07 3.98.07 6.92 0 2.95-.03 4.99-.07 6.93h5.18c-.09-1.37-.11-3.68-.11-5.65 0-3.1 1.26-4 4.12-4 1.33 0 2.28.16 3.1.46.03-1.16.26-3.43.4-4.43-.86-.25-1.81-.41-2.96-.41-2.46-.03-4.26.98-5.1 3.38l-.17-.02Zm22.55 3.65c0 2.5-1.8 3.66-4.64 3.66-2.81 0-4.61-1.1-4.61-3.66s1.82-3.52 4.61-3.52c2.82 0 4.64 1.03 4.64 3.52Zm4.71-.11c0-4.96-3.87-7.18-9.35-7.18-5.5 0-9.23 2.22-9.23 7.18 0 4.94 3.49 7.59 9.21 7.59 5.77 0 9.37-2.65 9.37-7.6Z"/><defs><linearGradient id="a" x1="6.33" x2="19.43" y1="40.8" y2="34.6" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient></defs></svg>
diff --git a/examples/basics/src/assets/background.svg b/examples/basics/src/assets/background.svg
new file mode 100644
index 000000000..4b2be0ac0
--- /dev/null
+++ b/examples/basics/src/assets/background.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="1024" fill="none"><path fill="url(#a)" fill-rule="evenodd" d="M-217.58 475.75c91.82-72.02 225.52-29.38 341.2-44.74C240 415.56 372.33 315.14 466.77 384.9c102.9 76.02 44.74 246.76 90.31 366.31 29.83 78.24 90.48 136.14 129.48 210.23 57.92 109.99 169.67 208.23 155.9 331.77-13.52 121.26-103.42 264.33-224.23 281.37-141.96 20.03-232.72-220.96-374.06-196.99-151.7 25.73-172.68 330.24-325.85 315.72-128.6-12.2-110.9-230.73-128.15-358.76-12.16-90.14 65.87-176.25 44.1-264.57-26.42-107.2-167.12-163.46-176.72-273.45-10.15-116.29 33.01-248.75 124.87-320.79Z" clip-rule="evenodd" style="opacity:.154"/><path fill="url(#b)" fill-rule="evenodd" d="M1103.43 115.43c146.42-19.45 275.33-155.84 413.5-103.59 188.09 71.13 409 212.64 407.06 413.88-1.94 201.25-259.28 278.6-414.96 405.96-130 106.35-240.24 294.39-405.6 265.3-163.7-28.8-161.93-274.12-284.34-386.66-134.95-124.06-436-101.46-445.82-284.6-9.68-180.38 247.41-246.3 413.54-316.9 101.01-42.93 207.83 21.06 316.62 6.61Z" clip-rule="evenodd" style="opacity:.154"/><defs><linearGradient id="b" x1="373" x2="1995.44" y1="1100" y2="118.03" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient><linearGradient id="a" x1="107.37" x2="1130.66" y1="1993.35" y2="1026.31" gradientUnits="userSpaceOnUse"><stop stop-color="#3245FF"/><stop offset="1" stop-color="#BC52EE"/></linearGradient></defs></svg>
diff --git a/examples/basics/src/components/Welcome.astro b/examples/basics/src/components/Welcome.astro
new file mode 100644
index 000000000..52e033341
--- /dev/null
+++ b/examples/basics/src/components/Welcome.astro
@@ -0,0 +1,210 @@
+---
+import astroLogo from '../assets/astro.svg';
+import background from '../assets/background.svg';
+---
+
+<div id="container">
+ <img id="background" src={background.src} alt="" fetchpriority="high" />
+ <main>
+ <section id="hero">
+ <a href="https://astro.build"
+ ><img src={astroLogo.src} width="115" height="48" alt="Astro Homepage" /></a
+ >
+ <h1>
+ To get started, open the <code><pre>src/pages</pre></code> directory in your project.
+ </h1>
+ <section id="links">
+ <a class="button" href="https://docs.astro.build">Read our docs</a>
+ <a href="https://astro.build/chat"
+ >Join our Discord <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"
+ ><path
+ fill="currentColor"
+ d="M107.7 8.07A105.15 105.15 0 0 0 81.47 0a72.06 72.06 0 0 0-3.36 6.83 97.68 97.68 0 0 0-29.11 0A72.37 72.37 0 0 0 45.64 0a105.89 105.89 0 0 0-26.25 8.09C2.79 32.65-1.71 56.6.54 80.21a105.73 105.73 0 0 0 32.17 16.15 77.7 77.7 0 0 0 6.89-11.11 68.42 68.42 0 0 1-10.85-5.18c.91-.66 1.8-1.34 2.66-2a75.57 75.57 0 0 0 64.32 0c.87.71 1.76 1.39 2.66 2a68.68 68.68 0 0 1-10.87 5.19 77 77 0 0 0 6.89 11.1 105.25 105.25 0 0 0 32.19-16.14c2.64-27.38-4.51-51.11-18.9-72.15ZM42.45 65.69C36.18 65.69 31 60 31 53s5-12.74 11.43-12.74S54 46 53.89 53s-5.05 12.69-11.44 12.69Zm42.24 0C78.41 65.69 73.25 60 73.25 53s5-12.74 11.44-12.74S96.23 46 96.12 53s-5.04 12.69-11.43 12.69Z"
+ ></path></svg
+ >
+ </a>
+ </section>
+ </section>
+ </main>
+
+ <a href="https://astro.build/blog/astro-5/" id="news" class="box">
+ <svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"
+ ><path
+ d="M24.667 12c1.333 1.414 2 3.192 2 5.334 0 4.62-4.934 5.7-7.334 12C18.444 28.567 18 27.456 18 26c0-4.642 6.667-7.053 6.667-14Zm-5.334-5.333c1.6 1.65 2.4 3.43 2.4 5.333 0 6.602-8.06 7.59-6.4 17.334C13.111 27.787 12 25.564 12 22.666c0-4.434 7.333-8 7.333-16Zm-6-5.333C15.111 3.555 16 5.556 16 7.333c0 8.333-11.333 10.962-5.333 22-3.488-.774-6-4-6-8 0-8.667 8.666-10 8.666-20Z"
+ fill="#111827"></path></svg
+ >
+ <h2>What's New in Astro 5.0?</h2>
+ <p>
+ From content layers to server islands, click to learn more about the new features and
+ improvements in Astro 5.0
+ </p>
+ </a>
+</div>
+
+<style>
+ #background {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: -1;
+ filter: blur(100px);
+ }
+
+ #container {
+ font-family: Inter, Roboto, 'Helvetica Neue', 'Arial Nova', 'Nimbus Sans', Arial, sans-serif;
+ height: 100%;
+ }
+
+ main {
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ }
+
+ #hero {
+ display: flex;
+ align-items: start;
+ flex-direction: column;
+ justify-content: center;
+ padding: 16px;
+ }
+
+ h1 {
+ font-size: 22px;
+ margin-top: 0.25em;
+ }
+
+ #links {
+ display: flex;
+ gap: 16px;
+ }
+
+ #links a {
+ display: flex;
+ align-items: center;
+ padding: 10px 12px;
+ color: #111827;
+ text-decoration: none;
+ transition: color 0.2s;
+ }
+
+ #links a:hover {
+ color: rgb(78, 80, 86);
+ }
+
+ #links a svg {
+ height: 1em;
+ margin-left: 8px;
+ }
+
+ #links a.button {
+ color: white;
+ background: linear-gradient(83.21deg, #3245ff 0%, #bc52ee 100%);
+ box-shadow:
+ inset 0 0 0 1px rgba(255, 255, 255, 0.12),
+ inset 0 -2px 0 rgba(0, 0, 0, 0.24);
+ border-radius: 10px;
+ }
+
+ #links a.button:hover {
+ color: rgb(230, 230, 230);
+ box-shadow: none;
+ }
+
+ pre {
+ font-family:
+ ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono',
+ monospace;
+ font-weight: normal;
+ background: linear-gradient(14deg, #d83333 0%, #f041ff 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+ margin: 0;
+ }
+
+ h2 {
+ margin: 0 0 1em;
+ font-weight: normal;
+ color: #111827;
+ font-size: 20px;
+ }
+
+ p {
+ color: #4b5563;
+ font-size: 16px;
+ line-height: 24px;
+ letter-spacing: -0.006em;
+ margin: 0;
+ }
+
+ code {
+ display: inline-block;
+ background:
+ linear-gradient(66.77deg, #f3cddd 0%, #f5cee7 100%) padding-box,
+ linear-gradient(155deg, #d83333 0%, #f041ff 18%, #f5cee7 45%) border-box;
+ border-radius: 8px;
+ border: 1px solid transparent;
+ padding: 6px 8px;
+ }
+
+ .box {
+ padding: 16px;
+ background: rgba(255, 255, 255, 1);
+ border-radius: 16px;
+ border: 1px solid white;
+ }
+
+ #news {
+ position: absolute;
+ bottom: 16px;
+ right: 16px;
+ max-width: 300px;
+ text-decoration: none;
+ transition: background 0.2s;
+ backdrop-filter: blur(50px);
+ }
+
+ #news:hover {
+ background: rgba(255, 255, 255, 0.55);
+ }
+
+ @media screen and (max-height: 368px) {
+ #news {
+ display: none;
+ }
+ }
+
+ @media screen and (max-width: 768px) {
+ #container {
+ display: flex;
+ flex-direction: column;
+ }
+
+ #hero {
+ display: block;
+ padding-top: 10%;
+ }
+
+ #links {
+ flex-wrap: wrap;
+ }
+
+ #links a.button {
+ padding: 14px 18px;
+ }
+
+ #news {
+ right: 16px;
+ left: 16px;
+ bottom: 2.5rem;
+ max-width: 100%;
+ }
+
+ h1 {
+ line-height: 1.5;
+ }
+ }
+</style>
diff --git a/examples/basics/src/layouts/Layout.astro b/examples/basics/src/layouts/Layout.astro
new file mode 100644
index 000000000..e455c6106
--- /dev/null
+++ b/examples/basics/src/layouts/Layout.astro
@@ -0,0 +1,22 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width" />
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+ <meta name="generator" content={Astro.generator} />
+ <title>Astro Basics</title>
+ </head>
+ <body>
+ <slot />
+ </body>
+</html>
+
+<style>
+ html,
+ body {
+ margin: 0;
+ width: 100%;
+ height: 100%;
+ }
+</style>
diff --git a/examples/basics/src/pages/index.astro b/examples/basics/src/pages/index.astro
new file mode 100644
index 000000000..c04f3602b
--- /dev/null
+++ b/examples/basics/src/pages/index.astro
@@ -0,0 +1,11 @@
+---
+import Welcome from '../components/Welcome.astro';
+import Layout from '../layouts/Layout.astro';
+
+// Welcome to Astro! Wondering what to do next? Check out the Astro documentation at https://docs.astro.build
+// Don't want to use any of this? Delete everything in this file, the `assets`, `components`, and `layouts` directories, and start fresh.
+---
+
+<Layout>
+ <Welcome />
+</Layout>
diff --git a/examples/basics/tsconfig.json b/examples/basics/tsconfig.json
new file mode 100644
index 000000000..8bf91d3bb
--- /dev/null
+++ b/examples/basics/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "extends": "astro/tsconfigs/strict",
+ "include": [".astro/types.d.ts", "**/*"],
+ "exclude": ["dist"]
+}
diff --git a/examples/blog/.codesandbox/Dockerfile b/examples/blog/.codesandbox/Dockerfile
new file mode 100644
index 000000000..c3b5c81a1
--- /dev/null
+++ b/examples/blog/.codesandbox/Dockerfile
@@ -0,0 +1 @@
+FROM node:18-bullseye
diff --git a/examples/blog/.gitignore b/examples/blog/.gitignore
new file mode 100644
index 000000000..16d54bb13
--- /dev/null
+++ b/examples/blog/.gitignore
@@ -0,0 +1,24 @@
+# build output
+dist/
+# generated types
+.astro/
+
+# dependencies
+node_modules/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
+
+# jetbrains setting folder
+.idea/
diff --git a/examples/blog/.vscode/extensions.json b/examples/blog/.vscode/extensions.json
new file mode 100644
index 000000000..56f043d30
--- /dev/null
+++ b/examples/blog/.vscode/extensions.json
@@ -0,0 +1,4 @@
+{
+ "recommendations": ["astro-build.astro-vscode", "unifiedjs.vscode-mdx"],
+ "unwantedRecommendations": []
+}
diff --git a/examples/blog/.vscode/launch.json b/examples/blog/.vscode/launch.json
new file mode 100644
index 000000000..d64220976
--- /dev/null
+++ b/examples/blog/.vscode/launch.json
@@ -0,0 +1,11 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "command": "./node_modules/.bin/astro dev",
+ "name": "Development server",
+ "request": "launch",
+ "type": "node-terminal"
+ }
+ ]
+}
diff --git a/examples/blog/README.md b/examples/blog/README.md
new file mode 100644
index 000000000..758716e07
--- /dev/null
+++ b/examples/blog/README.md
@@ -0,0 +1,68 @@
+# Astro Starter Kit: Blog
+
+```sh
+npm create astro@latest -- --template blog
+```
+
+[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/blog)
+[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/blog)
+[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/blog/devcontainer.json)
+
+> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
+
+![blog](https://github.com/withastro/astro/assets/2244813/ff10799f-a816-4703-b967-c78997e8323d)
+
+Features:
+
+- ✅ Minimal styling (make it your own!)
+- ✅ 100/100 Lighthouse performance
+- ✅ SEO-friendly with canonical URLs and OpenGraph data
+- ✅ Sitemap support
+- ✅ RSS Feed support
+- ✅ Markdown & MDX support
+
+## 🚀 Project Structure
+
+Inside of your Astro project, you'll see the following folders and files:
+
+```text
+├── public/
+├── src/
+│   ├── components/
+│   ├── content/
+│   ├── layouts/
+│   └── pages/
+├── astro.config.mjs
+├── README.md
+├── package.json
+└── tsconfig.json
+```
+
+Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
+
+There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
+
+The `src/content/` directory contains "collections" of related Markdown and MDX documents. Use `getCollection()` to retrieve posts from `src/content/blog/`, and type-check your frontmatter using an optional schema. See [Astro's Content Collections docs](https://docs.astro.build/en/guides/content-collections/) to learn more.
+
+Any static assets, like images, can be placed in the `public/` directory.
+
+## 🧞 Commands
+
+All commands are run from the root of the project, from a terminal:
+
+| Command | Action |
+| :------------------------ | :----------------------------------------------- |
+| `npm install` | Installs dependencies |
+| `npm run dev` | Starts local dev server at `localhost:4321` |
+| `npm run build` | Build your production site to `./dist/` |
+| `npm run preview` | Preview your build locally, before deploying |
+| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
+| `npm run astro -- --help` | Get help using the Astro CLI |
+
+## 👀 Want to learn more?
+
+Check out [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
+
+## Credit
+
+This theme is based off of the lovely [Bear Blog](https://github.com/HermanMartinus/bearblog/).
diff --git a/examples/blog/astro.config.mjs b/examples/blog/astro.config.mjs
new file mode 100644
index 000000000..d45f3951e
--- /dev/null
+++ b/examples/blog/astro.config.mjs
@@ -0,0 +1,10 @@
+// @ts-check
+import { defineConfig } from 'astro/config';
+import mdx from '@astrojs/mdx';
+import sitemap from '@astrojs/sitemap';
+
+// https://astro.build/config
+export default defineConfig({
+ site: 'https://example.com',
+ integrations: [mdx(), sitemap()],
+});
diff --git a/examples/blog/package.json b/examples/blog/package.json
new file mode 100644
index 000000000..0d974a4c2
--- /dev/null
+++ b/examples/blog/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "@example/blog",
+ "type": "module",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "dev": "astro dev",
+ "build": "astro build",
+ "preview": "astro preview",
+ "astro": "astro"
+ },
+ "dependencies": {
+ "@astrojs/mdx": "^4.3.0",
+ "@astrojs/rss": "^4.0.12",
+ "@astrojs/sitemap": "^3.4.1",
+ "astro": "^5.9.0",
+ "sharp": "^0.34.2"
+ }
+}
diff --git a/examples/blog/public/favicon.svg b/examples/blog/public/favicon.svg
new file mode 100644
index 000000000..f157bd1c5
--- /dev/null
+++ b/examples/blog/public/favicon.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
+ <path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
+ <style>
+ path { fill: #000; }
+ @media (prefers-color-scheme: dark) {
+ path { fill: #FFF; }
+ }
+ </style>
+</svg>
diff --git a/examples/blog/public/fonts/atkinson-bold.woff b/examples/blog/public/fonts/atkinson-bold.woff
new file mode 100644
index 000000000..e7f8977ec
--- /dev/null
+++ b/examples/blog/public/fonts/atkinson-bold.woff
Binary files differ
diff --git a/examples/blog/public/fonts/atkinson-regular.woff b/examples/blog/public/fonts/atkinson-regular.woff
new file mode 100644
index 000000000..bbe09c584
--- /dev/null
+++ b/examples/blog/public/fonts/atkinson-regular.woff
Binary files differ
diff --git a/examples/blog/src/assets/blog-placeholder-1.jpg b/examples/blog/src/assets/blog-placeholder-1.jpg
new file mode 100644
index 000000000..74d4009b5
--- /dev/null
+++ b/examples/blog/src/assets/blog-placeholder-1.jpg
Binary files differ
diff --git a/examples/blog/src/assets/blog-placeholder-2.jpg b/examples/blog/src/assets/blog-placeholder-2.jpg
new file mode 100644
index 000000000..c4214b0e6
--- /dev/null
+++ b/examples/blog/src/assets/blog-placeholder-2.jpg
Binary files differ
diff --git a/examples/blog/src/assets/blog-placeholder-3.jpg b/examples/blog/src/assets/blog-placeholder-3.jpg
new file mode 100644
index 000000000..fbe2ac0cb
--- /dev/null
+++ b/examples/blog/src/assets/blog-placeholder-3.jpg
Binary files differ
diff --git a/examples/blog/src/assets/blog-placeholder-4.jpg b/examples/blog/src/assets/blog-placeholder-4.jpg
new file mode 100644
index 000000000..f4fc88e29
--- /dev/null
+++ b/examples/blog/src/assets/blog-placeholder-4.jpg
Binary files differ
diff --git a/examples/blog/src/assets/blog-placeholder-5.jpg b/examples/blog/src/assets/blog-placeholder-5.jpg
new file mode 100644
index 000000000..c5646746c
--- /dev/null
+++ b/examples/blog/src/assets/blog-placeholder-5.jpg
Binary files differ
diff --git a/examples/blog/src/assets/blog-placeholder-about.jpg b/examples/blog/src/assets/blog-placeholder-about.jpg
new file mode 100644
index 000000000..cf5f68532
--- /dev/null
+++ b/examples/blog/src/assets/blog-placeholder-about.jpg
Binary files differ
diff --git a/examples/blog/src/components/BaseHead.astro b/examples/blog/src/components/BaseHead.astro
new file mode 100644
index 000000000..de6ea95da
--- /dev/null
+++ b/examples/blog/src/components/BaseHead.astro
@@ -0,0 +1,57 @@
+---
+// Import the global.css file here so that it is included on
+// all pages through the use of the <BaseHead /> component.
+import '../styles/global.css';
+import { SITE_TITLE } from '../consts';
+import FallbackImage from '../assets/blog-placeholder-1.jpg';
+import type { ImageMetadata } from 'astro';
+
+interface Props {
+ title: string;
+ description: string;
+ image?: ImageMetadata;
+}
+
+const canonicalURL = new URL(Astro.url.pathname, Astro.site);
+
+const { title, description, image = FallbackImage } = Astro.props;
+---
+
+<!-- Global Metadata -->
+<meta charset="utf-8" />
+<meta name="viewport" content="width=device-width,initial-scale=1" />
+<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+<link rel="sitemap" href="/sitemap-index.xml" />
+<link
+ rel="alternate"
+ type="application/rss+xml"
+ title={SITE_TITLE}
+ href={new URL('rss.xml', Astro.site)}
+/>
+<meta name="generator" content={Astro.generator} />
+
+<!-- Font preloads -->
+<link rel="preload" href="/fonts/atkinson-regular.woff" as="font" type="font/woff" crossorigin />
+<link rel="preload" href="/fonts/atkinson-bold.woff" as="font" type="font/woff" crossorigin />
+
+<!-- Canonical URL -->
+<link rel="canonical" href={canonicalURL} />
+
+<!-- Primary Meta Tags -->
+<title>{title}</title>
+<meta name="title" content={title} />
+<meta name="description" content={description} />
+
+<!-- Open Graph / Facebook -->
+<meta property="og:type" content="website" />
+<meta property="og:url" content={Astro.url} />
+<meta property="og:title" content={title} />
+<meta property="og:description" content={description} />
+<meta property="og:image" content={new URL(image.src, Astro.url)} />
+
+<!-- Twitter -->
+<meta property="twitter:card" content="summary_large_image" />
+<meta property="twitter:url" content={Astro.url} />
+<meta property="twitter:title" content={title} />
+<meta property="twitter:description" content={description} />
+<meta property="twitter:image" content={new URL(image.src, Astro.url)} />
diff --git a/examples/blog/src/components/Footer.astro b/examples/blog/src/components/Footer.astro
new file mode 100644
index 000000000..96c2fce91
--- /dev/null
+++ b/examples/blog/src/components/Footer.astro
@@ -0,0 +1,62 @@
+---
+const today = new Date();
+---
+
+<footer>
+ &copy; {today.getFullYear()} Your name here. All rights reserved.
+ <div class="social-links">
+ <a href="https://m.webtoo.ls/@astro" target="_blank">
+ <span class="sr-only">Follow Astro on Mastodon</span>
+ <svg
+ viewBox="0 0 16 16"
+ aria-hidden="true"
+ width="32"
+ height="32"
+ astro-icon="social/mastodon"
+ ><path
+ fill="currentColor"
+ d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a3.614 3.614 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522c0-.859.22-1.541.66-2.046.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764.442.505.661 1.187.661 2.046v4.203z"
+ ></path></svg
+ >
+ </a>
+ <a href="https://twitter.com/astrodotbuild" target="_blank">
+ <span class="sr-only">Follow Astro on Twitter</span>
+ <svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32" astro-icon="social/twitter"
+ ><path
+ fill="currentColor"
+ d="M5.026 15c6.038 0 9.341-5.003 9.341-9.334 0-.14 0-.282-.006-.422A6.685 6.685 0 0 0 16 3.542a6.658 6.658 0 0 1-1.889.518 3.301 3.301 0 0 0 1.447-1.817 6.533 6.533 0 0 1-2.087.793A3.286 3.286 0 0 0 7.875 6.03a9.325 9.325 0 0 1-6.767-3.429 3.289 3.289 0 0 0 1.018 4.382A3.323 3.323 0 0 1 .64 6.575v.045a3.288 3.288 0 0 0 2.632 3.218 3.203 3.203 0 0 1-.865.115 3.23 3.23 0 0 1-.614-.057 3.283 3.283 0 0 0 3.067 2.277A6.588 6.588 0 0 1 .78 13.58a6.32 6.32 0 0 1-.78-.045A9.344 9.344 0 0 0 5.026 15z"
+ ></path></svg
+ >
+ </a>
+ <a href="https://github.com/withastro/astro" target="_blank">
+ <span class="sr-only">Go to Astro's GitHub repo</span>
+ <svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32" astro-icon="social/github"
+ ><path
+ fill="currentColor"
+ d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"
+ ></path></svg
+ >
+ </a>
+ </div>
+</footer>
+<style>
+ footer {
+ padding: 2em 1em 6em 1em;
+ background: linear-gradient(var(--gray-gradient)) no-repeat;
+ color: rgb(var(--gray));
+ text-align: center;
+ }
+ .social-links {
+ display: flex;
+ justify-content: center;
+ gap: 1em;
+ margin-top: 1em;
+ }
+ .social-links a {
+ text-decoration: none;
+ color: rgb(var(--gray));
+ }
+ .social-links a:hover {
+ color: rgb(var(--gray-dark));
+ }
+</style>
diff --git a/examples/blog/src/components/FormattedDate.astro b/examples/blog/src/components/FormattedDate.astro
new file mode 100644
index 000000000..1bcce73a2
--- /dev/null
+++ b/examples/blog/src/components/FormattedDate.astro
@@ -0,0 +1,17 @@
+---
+interface Props {
+ date: Date;
+}
+
+const { date } = Astro.props;
+---
+
+<time datetime={date.toISOString()}>
+ {
+ date.toLocaleDateString('en-us', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ })
+ }
+</time>
diff --git a/examples/blog/src/components/Header.astro b/examples/blog/src/components/Header.astro
new file mode 100644
index 000000000..c9ab99f1d
--- /dev/null
+++ b/examples/blog/src/components/Header.astro
@@ -0,0 +1,85 @@
+---
+import HeaderLink from './HeaderLink.astro';
+import { SITE_TITLE } from '../consts';
+---
+
+<header>
+ <nav>
+ <h2><a href="/">{SITE_TITLE}</a></h2>
+ <div class="internal-links">
+ <HeaderLink href="/">Home</HeaderLink>
+ <HeaderLink href="/blog">Blog</HeaderLink>
+ <HeaderLink href="/about">About</HeaderLink>
+ </div>
+ <div class="social-links">
+ <a href="https://m.webtoo.ls/@astro" target="_blank">
+ <span class="sr-only">Follow Astro on Mastodon</span>
+ <svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32"
+ ><path
+ fill="currentColor"
+ d="M11.19 12.195c2.016-.24 3.77-1.475 3.99-2.603.348-1.778.32-4.339.32-4.339 0-3.47-2.286-4.488-2.286-4.488C12.062.238 10.083.017 8.027 0h-.05C5.92.017 3.942.238 2.79.765c0 0-2.285 1.017-2.285 4.488l-.002.662c-.004.64-.007 1.35.011 2.091.083 3.394.626 6.74 3.78 7.57 1.454.383 2.703.463 3.709.408 1.823-.1 2.847-.647 2.847-.647l-.06-1.317s-1.303.41-2.767.36c-1.45-.05-2.98-.156-3.215-1.928a3.614 3.614 0 0 1-.033-.496s1.424.346 3.228.428c1.103.05 2.137-.064 3.188-.189zm1.613-2.47H11.13v-4.08c0-.859-.364-1.295-1.091-1.295-.804 0-1.207.517-1.207 1.541v2.233H7.168V5.89c0-1.024-.403-1.541-1.207-1.541-.727 0-1.091.436-1.091 1.296v4.079H3.197V5.522c0-.859.22-1.541.66-2.046.456-.505 1.052-.764 1.793-.764.856 0 1.504.328 1.933.983L8 4.39l.417-.695c.429-.655 1.077-.983 1.934-.983.74 0 1.336.259 1.791.764.442.505.661 1.187.661 2.046v4.203z"
+ ></path></svg
+ >
+ </a>
+ <a href="https://twitter.com/astrodotbuild" target="_blank">
+ <span class="sr-only">Follow Astro on Twitter</span>
+ <svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32"
+ ><path
+ fill="currentColor"
+ d="M5.026 15c6.038 0 9.341-5.003 9.341-9.334 0-.14 0-.282-.006-.422A6.685 6.685 0 0 0 16 3.542a6.658 6.658 0 0 1-1.889.518 3.301 3.301 0 0 0 1.447-1.817 6.533 6.533 0 0 1-2.087.793A3.286 3.286 0 0 0 7.875 6.03a9.325 9.325 0 0 1-6.767-3.429 3.289 3.289 0 0 0 1.018 4.382A3.323 3.323 0 0 1 .64 6.575v.045a3.288 3.288 0 0 0 2.632 3.218 3.203 3.203 0 0 1-.865.115 3.23 3.23 0 0 1-.614-.057 3.283 3.283 0 0 0 3.067 2.277A6.588 6.588 0 0 1 .78 13.58a6.32 6.32 0 0 1-.78-.045A9.344 9.344 0 0 0 5.026 15z"
+ ></path></svg
+ >
+ </a>
+ <a href="https://github.com/withastro/astro" target="_blank">
+ <span class="sr-only">Go to Astro's GitHub repo</span>
+ <svg viewBox="0 0 16 16" aria-hidden="true" width="32" height="32"
+ ><path
+ fill="currentColor"
+ d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"
+ ></path></svg
+ >
+ </a>
+ </div>
+ </nav>
+</header>
+<style>
+ header {
+ margin: 0;
+ padding: 0 1em;
+ background: white;
+ box-shadow: 0 2px 8px rgba(var(--black), 5%);
+ }
+ h2 {
+ margin: 0;
+ font-size: 1em;
+ }
+
+ h2 a,
+ h2 a.active {
+ text-decoration: none;
+ }
+ nav {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+ nav a {
+ padding: 1em 0.5em;
+ color: var(--black);
+ border-bottom: 4px solid transparent;
+ text-decoration: none;
+ }
+ nav a.active {
+ text-decoration: none;
+ border-bottom-color: var(--accent);
+ }
+ .social-links,
+ .social-links a {
+ display: flex;
+ }
+ @media (max-width: 720px) {
+ .social-links {
+ display: none;
+ }
+ }
+</style>
diff --git a/examples/blog/src/components/HeaderLink.astro b/examples/blog/src/components/HeaderLink.astro
new file mode 100644
index 000000000..41da8462e
--- /dev/null
+++ b/examples/blog/src/components/HeaderLink.astro
@@ -0,0 +1,24 @@
+---
+import type { HTMLAttributes } from 'astro/types';
+
+type Props = HTMLAttributes<'a'>;
+
+const { href, class: className, ...props } = Astro.props;
+const pathname = Astro.url.pathname.replace(import.meta.env.BASE_URL, '');
+const subpath = pathname.match(/[^\/]+/g);
+const isActive = href === pathname || href === '/' + (subpath?.[0] || '');
+---
+
+<a href={href} class:list={[className, { active: isActive }]} {...props}>
+ <slot />
+</a>
+<style>
+ a {
+ display: inline-block;
+ text-decoration: none;
+ }
+ a.active {
+ font-weight: bolder;
+ text-decoration: underline;
+ }
+</style>
diff --git a/examples/blog/src/consts.ts b/examples/blog/src/consts.ts
new file mode 100644
index 000000000..0df8a61f4
--- /dev/null
+++ b/examples/blog/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 = 'Astro Blog';
+export const SITE_DESCRIPTION = 'Welcome to my website!';
diff --git a/examples/blog/src/content.config.ts b/examples/blog/src/content.config.ts
new file mode 100644
index 000000000..27bd39403
--- /dev/null
+++ b/examples/blog/src/content.config.ts
@@ -0,0 +1,18 @@
+import { glob } from 'astro/loaders';
+import { defineCollection, z } from 'astro:content';
+
+const blog = defineCollection({
+ // Load Markdown and MDX files in the `src/content/blog/` directory.
+ loader: glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }),
+ // Type-check frontmatter using a schema
+ schema: ({ image }) => z.object({
+ title: z.string(),
+ description: z.string(),
+ // Transform string to Date object
+ pubDate: z.coerce.date(),
+ updatedDate: z.coerce.date().optional(),
+ heroImage: image().optional(),
+ }),
+});
+
+export const collections = { blog };
diff --git a/examples/blog/src/content/blog/first-post.md b/examples/blog/src/content/blog/first-post.md
new file mode 100644
index 000000000..cb457dcb3
--- /dev/null
+++ b/examples/blog/src/content/blog/first-post.md
@@ -0,0 +1,16 @@
+---
+title: 'First post'
+description: 'Lorem ipsum dolor sit amet'
+pubDate: 'Jul 08 2022'
+heroImage: '../../assets/blog-placeholder-3.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/blog/src/content/blog/markdown-style-guide.md b/examples/blog/src/content/blog/markdown-style-guide.md
new file mode 100644
index 000000000..1c010bea8
--- /dev/null
+++ b/examples/blog/src/content/blog/markdown-style-guide.md
@@ -0,0 +1,214 @@
+---
+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: 'Jun 19 2024'
+heroImage: '../../assets/blog-placeholder-1.jpg'
+---
+
+Here is a sample of some basic Markdown syntax that can be used when writing Markdown content in Astro.
+
+## Headings
+
+The following HTML `<h1>`—`<h6>` elements represent six levels of section headings. `<h1>` is the highest section level while `<h6>` 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
+
+### Syntax
+
+```markdown
+![Alt text](./full/or/relative/path/of/image)
+```
+
+### Output
+
+![blog placeholder](/blog-placeholder-about.jpg)
+
+## 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
+
+#### Syntax
+
+```markdown
+> Tiam, ad mint andaepu dandae nostion secatur sequo quae.
+> **Note** that you can use _Markdown syntax_ within a blockquote.
+```
+
+#### Output
+
+> Tiam, ad mint andaepu dandae nostion secatur sequo quae.
+> **Note** that you can use _Markdown syntax_ within a blockquote.
+
+### Blockquote with attribution
+
+#### Syntax
+
+```markdown
+> Don't communicate by sharing memory, share memory by communicating.<br>
+> — <cite>Rob Pike[^1]</cite>
+```
+
+#### Output
+
+> Don't communicate by sharing memory, share memory by communicating.<br>
+> — <cite>Rob Pike[^1]</cite>
+
+[^1]: The above quote is excerpted from Rob Pike's [talk](https://www.youtube.com/watch?v=PAAkCSZUG1c) during Gopherfest, November 18, 2015.
+
+## Tables
+
+### Syntax
+
+```markdown
+| Italics | Bold | Code |
+| --------- | -------- | ------ |
+| _italics_ | **bold** | `code` |
+```
+
+### Output
+
+| Italics | Bold | Code |
+| --------- | -------- | ------ |
+| _italics_ | **bold** | `code` |
+
+## Code Blocks
+
+### Syntax
+
+we can use 3 backticks ``` in new line and write snippet and close with 3 backticks on new line and to highlight language specific syntax, write one word of language name after first 3 backticks, for eg. html, javascript, css, markdown, typescript, txt, bash
+
+````markdown
+```html
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <title>Example HTML5 Document</title>
+ </head>
+ <body>
+ <p>Test</p>
+ </body>
+</html>
+```
+````
+
+### Output
+
+```html
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <title>Example HTML5 Document</title>
+ </head>
+ <body>
+ <p>Test</p>
+ </body>
+</html>
+```
+
+## List Types
+
+### Ordered List
+
+#### Syntax
+
+```markdown
+1. First item
+2. Second item
+3. Third item
+```
+
+#### Output
+
+1. First item
+2. Second item
+3. Third item
+
+### Unordered List
+
+#### Syntax
+
+```markdown
+- List item
+- Another item
+- And another item
+```
+
+#### Output
+
+- List item
+- Another item
+- And another item
+
+### Nested list
+
+#### Syntax
+
+```markdown
+- Fruit
+ - Apple
+ - Orange
+ - Banana
+- Dairy
+ - Milk
+ - Cheese
+```
+
+#### Output
+
+- Fruit
+ - Apple
+ - Orange
+ - Banana
+- Dairy
+ - Milk
+ - Cheese
+
+## Other Elements — abbr, sub, sup, kbd, mark
+
+### Syntax
+
+```markdown
+<abbr title="Graphics Interchange Format">GIF</abbr> is a bitmap image format.
+
+H<sub>2</sub>O
+
+X<sup>n</sup> + Y<sup>n</sup> = Z<sup>n</sup>
+
+Press <kbd>CTRL</kbd> + <kbd>ALT</kbd> + <kbd>Delete</kbd> to end the session.
+
+Most <mark>salamanders</mark> are nocturnal, and hunt for insects, worms, and other small creatures.
+```
+
+### Output
+
+<abbr title="Graphics Interchange Format">GIF</abbr> is a bitmap image format.
+
+H<sub>2</sub>O
+
+X<sup>n</sup> + Y<sup>n</sup> = Z<sup>n</sup>
+
+Press <kbd>CTRL</kbd> + <kbd>ALT</kbd> + <kbd>Delete</kbd> to end the session.
+
+Most <mark>salamanders</mark> are nocturnal, and hunt for insects, worms, and other small creatures.
diff --git a/examples/blog/src/content/blog/second-post.md b/examples/blog/src/content/blog/second-post.md
new file mode 100644
index 000000000..79337d413
--- /dev/null
+++ b/examples/blog/src/content/blog/second-post.md
@@ -0,0 +1,16 @@
+---
+title: 'Second post'
+description: 'Lorem ipsum dolor sit amet'
+pubDate: 'Jul 15 2022'
+heroImage: '../../assets/blog-placeholder-4.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/blog/src/content/blog/third-post.md b/examples/blog/src/content/blog/third-post.md
new file mode 100644
index 000000000..b871fa15b
--- /dev/null
+++ b/examples/blog/src/content/blog/third-post.md
@@ -0,0 +1,16 @@
+---
+title: 'Third post'
+description: 'Lorem ipsum dolor sit amet'
+pubDate: 'Jul 22 2022'
+heroImage: '../../assets/blog-placeholder-2.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/blog/src/content/blog/using-mdx.mdx b/examples/blog/src/content/blog/using-mdx.mdx
new file mode 100644
index 000000000..bd8856e2e
--- /dev/null
+++ b/examples/blog/src/content/blog/using-mdx.mdx
@@ -0,0 +1,31 @@
+---
+title: 'Using MDX'
+description: 'Lorem ipsum dolor sit amet'
+pubDate: 'Jun 01 2024'
+heroImage: '../../assets/blog-placeholder-5.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';
+
+<HeaderLink href="#" onclick="alert('clicked!')">
+ Embedded component in MDX
+</HeaderLink>
+
+## 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/blog/src/layouts/BlogPost.astro b/examples/blog/src/layouts/BlogPost.astro
new file mode 100644
index 000000000..2b2d5eb2c
--- /dev/null
+++ b/examples/blog/src/layouts/BlogPost.astro
@@ -0,0 +1,86 @@
+---
+import type { CollectionEntry } from 'astro:content';
+import BaseHead from '../components/BaseHead.astro';
+import Header from '../components/Header.astro';
+import Footer from '../components/Footer.astro';
+import FormattedDate from '../components/FormattedDate.astro';
+import { Image } from 'astro:assets';
+
+type Props = CollectionEntry<'blog'>['data'];
+
+const { title, description, pubDate, updatedDate, heroImage } = Astro.props;
+---
+
+<html lang="en">
+ <head>
+ <BaseHead title={title} description={description} />
+ <style>
+ main {
+ width: calc(100% - 2em);
+ max-width: 100%;
+ margin: 0;
+ }
+ .hero-image {
+ width: 100%;
+ }
+ .hero-image img {
+ display: block;
+ margin: 0 auto;
+ border-radius: 12px;
+ box-shadow: var(--box-shadow);
+ }
+ .prose {
+ width: 720px;
+ max-width: calc(100% - 2em);
+ margin: auto;
+ padding: 1em;
+ color: rgb(var(--gray-dark));
+ }
+ .title {
+ margin-bottom: 1em;
+ padding: 1em 0;
+ text-align: center;
+ line-height: 1;
+ }
+ .title h1 {
+ margin: 0 0 0.5em 0;
+ }
+ .date {
+ margin-bottom: 0.5em;
+ color: rgb(var(--gray));
+ }
+ .last-updated-on {
+ font-style: italic;
+ }
+ </style>
+ </head>
+
+ <body>
+ <Header />
+ <main>
+ <article>
+ <div class="hero-image">
+ {heroImage && <Image width={1020} height={510} src={heroImage} alt="" />}
+ </div>
+ <div class="prose">
+ <div class="title">
+ <div class="date">
+ <FormattedDate date={pubDate} />
+ {
+ updatedDate && (
+ <div class="last-updated-on">
+ Last updated on <FormattedDate date={updatedDate} />
+ </div>
+ )
+ }
+ </div>
+ <h1>{title}</h1>
+ <hr />
+ </div>
+ <slot />
+ </div>
+ </article>
+ </main>
+ <Footer />
+ </body>
+</html>
diff --git a/examples/blog/src/pages/about.astro b/examples/blog/src/pages/about.astro
new file mode 100644
index 000000000..478a62447
--- /dev/null
+++ b/examples/blog/src/pages/about.astro
@@ -0,0 +1,63 @@
+---
+import Layout from '../layouts/BlogPost.astro';
+import AboutHeroImage from '../assets/blog-placeholder-about.jpg';
+---
+
+<Layout
+ title="About Me"
+ description="Lorem ipsum dolor sit amet"
+ pubDate={new Date('August 08 2021')}
+ heroImage={AboutHeroImage}
+>
+ <p>
+ 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.
+ </p>
+
+ <p>
+ 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.
+ </p>
+
+ <p>
+ 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.
+ </p>
+
+ <p>
+ 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.
+ </p>
+
+ <p>
+ 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.
+ </p>
+</Layout>
diff --git a/examples/blog/src/pages/blog/[...slug].astro b/examples/blog/src/pages/blog/[...slug].astro
new file mode 100644
index 000000000..096bd1e82
--- /dev/null
+++ b/examples/blog/src/pages/blog/[...slug].astro
@@ -0,0 +1,21 @@
+---
+import { type CollectionEntry, getCollection } from 'astro:content';
+import BlogPost from '../../layouts/BlogPost.astro';
+import { render } from 'astro:content';
+
+export async function getStaticPaths() {
+ const posts = await getCollection('blog');
+ return posts.map((post) => ({
+ params: { slug: post.id },
+ props: post,
+ }));
+}
+type Props = CollectionEntry<'blog'>;
+
+const post = Astro.props;
+const { Content } = await render(post);
+---
+
+<BlogPost {...post.data}>
+ <Content />
+</BlogPost>
diff --git a/examples/blog/src/pages/blog/index.astro b/examples/blog/src/pages/blog/index.astro
new file mode 100644
index 000000000..1e4db9008
--- /dev/null
+++ b/examples/blog/src/pages/blog/index.astro
@@ -0,0 +1,114 @@
+---
+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';
+import FormattedDate from '../../components/FormattedDate.astro';
+import { Image } from 'astro:assets';
+
+const posts = (await getCollection('blog')).sort(
+ (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
+);
+---
+
+<!doctype html>
+<html lang="en">
+ <head>
+ <BaseHead title={SITE_TITLE} description={SITE_DESCRIPTION} />
+ <style>
+ main {
+ width: 960px;
+ }
+ ul {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 2rem;
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+ }
+ ul li {
+ width: calc(50% - 1rem);
+ }
+ ul li * {
+ text-decoration: none;
+ transition: 0.2s ease;
+ }
+ ul li:first-child {
+ width: 100%;
+ margin-bottom: 1rem;
+ text-align: center;
+ }
+ ul li:first-child img {
+ width: 100%;
+ }
+ ul li:first-child .title {
+ font-size: 2.369rem;
+ }
+ ul li img {
+ margin-bottom: 0.5rem;
+ border-radius: 12px;
+ }
+ ul li a {
+ display: block;
+ }
+ .title {
+ margin: 0;
+ color: rgb(var(--black));
+ line-height: 1;
+ }
+ .date {
+ margin: 0;
+ color: rgb(var(--gray));
+ }
+ ul li a:hover h4,
+ ul li a:hover .date {
+ color: rgb(var(--accent));
+ }
+ ul a:hover img {
+ box-shadow: var(--box-shadow);
+ }
+ @media (max-width: 720px) {
+ ul {
+ gap: 0.5em;
+ }
+ ul li {
+ width: 100%;
+ text-align: center;
+ }
+ ul li:first-child {
+ margin-bottom: 0;
+ }
+ ul li:first-child .title {
+ font-size: 1.563em;
+ }
+ }
+ </style>
+ </head>
+ <body>
+ <Header />
+ <main>
+ <section>
+ <ul>
+ {
+ posts.map((post) => (
+ <li>
+ <a href={`/blog/${post.id}/`}>
+ {post.data.heroImage && (
+ <Image width={720} height={360} src={post.data.heroImage} alt="" />
+ )}
+ <h4 class="title">{post.data.title}</h4>
+ <p class="date">
+ <FormattedDate date={post.data.pubDate} />
+ </p>
+ </a>
+ </li>
+ ))
+ }
+ </ul>
+ </section>
+ </main>
+ <Footer />
+ </body>
+</html>
diff --git a/examples/blog/src/pages/index.astro b/examples/blog/src/pages/index.astro
new file mode 100644
index 000000000..6d7c4caed
--- /dev/null
+++ b/examples/blog/src/pages/index.astro
@@ -0,0 +1,49 @@
+---
+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';
+---
+
+<!doctype html>
+<html lang="en">
+ <head>
+ <BaseHead title={SITE_TITLE} description={SITE_DESCRIPTION} />
+ </head>
+ <body>
+ <Header />
+ <main>
+ <h1>🧑‍🚀 Hello, Astronaut!</h1>
+ <p>
+ Welcome to the official <a href="https://astro.build/">Astro</a> 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.
+ </p>
+ <p>
+ This template comes with a few integrations already configured in your
+ <code>astro.config.mjs</code> file. You can customize your setup with
+ <a href="https://astro.build/integrations">Astro Integrations</a> to add tools like Tailwind,
+ React, or Vue to your project.
+ </p>
+ <p>Here are a few ideas on how to get started with the template:</p>
+ <ul>
+ <li>Edit this page in <code>src/pages/index.astro</code></li>
+ <li>Edit the site header items in <code>src/components/Header.astro</code></li>
+ <li>Add your name to the footer in <code>src/components/Footer.astro</code></li>
+ <li>Check out the included blog posts in <code>src/content/blog/</code></li>
+ <li>Customize the blog post page layout in <code>src/layouts/BlogPost.astro</code></li>
+ </ul>
+ <p>
+ Have fun! If you get stuck, remember to
+ <a href="https://docs.astro.build/">read the docs</a>
+ or <a href="https://astro.build/chat">join us on Discord</a> to ask questions.
+ </p>
+ <p>
+ Looking for a blog template with a bit more personality? Check out
+ <a href="https://github.com/Charca/astro-blog-template">astro-blog-template</a>
+ by <a href="https://twitter.com/Charca">Maxi Ferreira</a>.
+ </p>
+ </main>
+ <Footer />
+ </body>
+</html>
diff --git a/examples/blog/src/pages/rss.xml.js b/examples/blog/src/pages/rss.xml.js
new file mode 100644
index 000000000..ae5e4c4ec
--- /dev/null
+++ b/examples/blog/src/pages/rss.xml.js
@@ -0,0 +1,16 @@
+import rss from '@astrojs/rss';
+import { getCollection } from 'astro:content';
+import { SITE_TITLE, SITE_DESCRIPTION } from '../consts';
+
+export async function GET(context) {
+ const posts = await getCollection('blog');
+ return rss({
+ title: SITE_TITLE,
+ description: SITE_DESCRIPTION,
+ site: context.site,
+ items: posts.map((post) => ({
+ ...post.data,
+ link: `/blog/${post.id}/`,
+ })),
+ });
+}
diff --git a/examples/blog/src/styles/global.css b/examples/blog/src/styles/global.css
new file mode 100644
index 000000000..d52c23e03
--- /dev/null
+++ b/examples/blog/src/styles/global.css
@@ -0,0 +1,155 @@
+/*
+ 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
+ */
+
+:root {
+ --accent: #2337ff;
+ --accent-dark: #000d8a;
+ --black: 15, 18, 25;
+ --gray: 96, 115, 159;
+ --gray-light: 229, 233, 240;
+ --gray-dark: 34, 41, 57;
+ --gray-gradient: rgba(var(--gray-light), 50%), #fff;
+ --box-shadow:
+ 0 2px 6px rgba(var(--gray), 25%), 0 8px 24px rgba(var(--gray), 33%),
+ 0 16px 32px rgba(var(--gray), 33%);
+}
+@font-face {
+ font-family: 'Atkinson';
+ src: url('/fonts/atkinson-regular.woff') format('woff');
+ font-weight: 400;
+ font-style: normal;
+ font-display: swap;
+}
+@font-face {
+ font-family: 'Atkinson';
+ src: url('/fonts/atkinson-bold.woff') format('woff');
+ font-weight: 700;
+ font-style: normal;
+ font-display: swap;
+}
+body {
+ font-family: 'Atkinson', sans-serif;
+ margin: 0;
+ padding: 0;
+ text-align: left;
+ background: linear-gradient(var(--gray-gradient)) no-repeat;
+ background-size: 100% 600px;
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+ color: rgb(var(--gray-dark));
+ font-size: 20px;
+ line-height: 1.7;
+}
+main {
+ width: 720px;
+ max-width: calc(100% - 2em);
+ margin: auto;
+ padding: 3em 1em;
+}
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ margin: 0 0 0.5rem 0;
+ color: rgb(var(--black));
+ line-height: 1.2;
+}
+h1 {
+ font-size: 3.052em;
+}
+h2 {
+ font-size: 2.441em;
+}
+h3 {
+ font-size: 1.953em;
+}
+h4 {
+ font-size: 1.563em;
+}
+h5 {
+ font-size: 1.25em;
+}
+strong,
+b {
+ font-weight: 700;
+}
+a {
+ color: var(--accent);
+}
+a:hover {
+ color: var(--accent);
+}
+p {
+ margin-bottom: 1em;
+}
+.prose p {
+ margin-bottom: 2em;
+}
+textarea {
+ width: 100%;
+ font-size: 16px;
+}
+input {
+ font-size: 16px;
+}
+table {
+ width: 100%;
+}
+img {
+ max-width: 100%;
+ height: auto;
+ border-radius: 8px;
+}
+code {
+ padding: 2px 5px;
+ background-color: rgb(var(--gray-light));
+ border-radius: 2px;
+}
+pre {
+ padding: 1.5em;
+ border-radius: 8px;
+}
+pre > code {
+ all: unset;
+}
+blockquote {
+ border-left: 4px solid var(--accent);
+ padding: 0 0 0 20px;
+ margin: 0px;
+ font-size: 1.333em;
+}
+hr {
+ border: none;
+ border-top: 1px solid rgb(var(--gray-light));
+}
+@media (max-width: 720px) {
+ body {
+ font-size: 18px;
+ }
+ main {
+ padding: 1em;
+ }
+}
+
+.sr-only {
+ border: 0;
+ padding: 0;
+ margin: 0;
+ position: absolute !important;
+ height: 1px;
+ width: 1px;
+ overflow: hidden;
+ /* IE6, IE7 - a 0 height clip, off to the bottom right of the visible 1px box */
+ clip: rect(1px 1px 1px 1px);
+ /* maybe deprecated but we need to support legacy browsers */
+ clip: rect(1px, 1px, 1px, 1px);
+ /* modern browsers, clip-path works inwards from each corner */
+ clip-path: inset(50%);
+ /* added line to stop words getting smushed together (as they go onto separate lines and some screen readers do not understand line feeds as a space */
+ white-space: nowrap;
+}
diff --git a/examples/blog/tsconfig.json b/examples/blog/tsconfig.json
new file mode 100644
index 000000000..0dc098dd7
--- /dev/null
+++ b/examples/blog/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "astro/tsconfigs/strict",
+ "include": [".astro/types.d.ts", "**/*"],
+ "exclude": ["dist"],
+ "compilerOptions": {
+ "strictNullChecks": true
+ }
+}
diff --git a/examples/component/.gitignore b/examples/component/.gitignore
new file mode 100644
index 000000000..16d54bb13
--- /dev/null
+++ b/examples/component/.gitignore
@@ -0,0 +1,24 @@
+# build output
+dist/
+# generated types
+.astro/
+
+# dependencies
+node_modules/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
+
+# jetbrains setting folder
+.idea/
diff --git a/examples/component/README.md b/examples/component/README.md
new file mode 100644
index 000000000..f51958529
--- /dev/null
+++ b/examples/component/README.md
@@ -0,0 +1,35 @@
+# Astro Starter Kit: Component Package
+
+This is a template for an Astro component library. Use this template for writing components to use in multiple projects or publish to NPM.
+
+```sh
+npm create astro@latest -- --template component
+```
+
+[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/non-html-pages)
+[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/non-html-pages)
+[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/component/devcontainer.json)
+
+## 🚀 Project Structure
+
+Inside of your Astro project, you'll see the following folders and files:
+
+```text
+/
+├── index.ts
+├── src
+│ └── MyComponent.astro
+├── tsconfig.json
+├── package.json
+```
+
+The `index.ts` file is the "entry point" for your package. Export your components in `index.ts` to make them importable from your package.
+
+## 🧞 Commands
+
+All commands are run from the root of the project, from a terminal:
+
+| Command | Action |
+| :------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `npm link` | Registers this package locally. Run `npm link my-component-library` in an Astro project to install your components |
+| `npm publish` | [Publishes](https://docs.npmjs.com/creating-and-publishing-unscoped-public-packages#publishing-unscoped-public-packages) this package to NPM. Requires you to be [logged in](https://docs.npmjs.com/cli/v8/commands/npm-adduser) |
diff --git a/examples/component/index.ts b/examples/component/index.ts
new file mode 100644
index 000000000..0e2e94fb2
--- /dev/null
+++ b/examples/component/index.ts
@@ -0,0 +1,6 @@
+// Do not write code directly here, instead use the `src` folder!
+// Then, use this file to export everything you want your user to access.
+
+import MyComponent from './src/MyComponent.astro';
+
+export default MyComponent;
diff --git a/examples/component/package.json b/examples/component/package.json
new file mode 100644
index 000000000..73c41e159
--- /dev/null
+++ b/examples/component/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "@example/component",
+ "private": true,
+ "version": "0.0.1",
+ "type": "module",
+ "exports": {
+ ".": "./index.ts"
+ },
+ "files": [
+ "src",
+ "index.ts"
+ ],
+ "keywords": [
+ "astro-component"
+ ],
+ "scripts": {},
+ "devDependencies": {
+ "astro": "^5.9.0"
+ },
+ "peerDependencies": {
+ "astro": "^4.0.0 || ^5.0.0"
+ }
+}
diff --git a/examples/component/src/MyComponent.astro b/examples/component/src/MyComponent.astro
new file mode 100644
index 000000000..2a3ebcea5
--- /dev/null
+++ b/examples/component/src/MyComponent.astro
@@ -0,0 +1,8 @@
+---
+// Write your component code in this file!
+interface Props {
+ prefix?: string;
+}
+---
+
+<div>{Astro.props.prefix} My special component</div>
diff --git a/examples/component/tsconfig.json b/examples/component/tsconfig.json
new file mode 100644
index 000000000..f85ec795b
--- /dev/null
+++ b/examples/component/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "astro/tsconfigs/strict",
+ "include": [".astro/types.d.ts", "**/*"],
+ "exclude": ["dist"],
+ "compilerOptions": {
+ "jsx": "preserve"
+ }
+}
diff --git a/examples/container-with-vitest/.codesandbox/Dockerfile b/examples/container-with-vitest/.codesandbox/Dockerfile
new file mode 100644
index 000000000..c3b5c81a1
--- /dev/null
+++ b/examples/container-with-vitest/.codesandbox/Dockerfile
@@ -0,0 +1 @@
+FROM node:18-bullseye
diff --git a/examples/container-with-vitest/.gitignore b/examples/container-with-vitest/.gitignore
new file mode 100644
index 000000000..16d54bb13
--- /dev/null
+++ b/examples/container-with-vitest/.gitignore
@@ -0,0 +1,24 @@
+# build output
+dist/
+# generated types
+.astro/
+
+# dependencies
+node_modules/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
+
+# jetbrains setting folder
+.idea/
diff --git a/examples/container-with-vitest/README.md b/examples/container-with-vitest/README.md
new file mode 100644
index 000000000..116268944
--- /dev/null
+++ b/examples/container-with-vitest/README.md
@@ -0,0 +1,11 @@
+# Astro + [Vitest](https://vitest.dev/) + Container API Example
+
+```sh
+npm create astro@latest -- --template container-with-vitest
+```
+
+[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/with-vitest)
+[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/with-vitest)
+[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/with-vitest/devcontainer.json)
+
+This example showcases Astro working with [Vitest](https://vitest.dev/) and how to test components using the Container API.
diff --git a/examples/container-with-vitest/astro.config.ts b/examples/container-with-vitest/astro.config.ts
new file mode 100644
index 000000000..17257d4f1
--- /dev/null
+++ b/examples/container-with-vitest/astro.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'astro/config';
+import react from "@astrojs/react"
+
+// https://astro.build/config
+export default defineConfig({
+ integrations: [react()]
+});
diff --git a/examples/container-with-vitest/package.json b/examples/container-with-vitest/package.json
new file mode 100644
index 000000000..473e3f11b
--- /dev/null
+++ b/examples/container-with-vitest/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "@example/container-with-vitest",
+ "type": "module",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "dev": "astro dev",
+ "build": "astro build",
+ "preview": "astro preview",
+ "astro": "astro",
+ "test": "vitest run"
+ },
+ "dependencies": {
+ "@astrojs/react": "^4.3.0",
+ "astro": "^5.9.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "vitest": "^3.1.1"
+ },
+ "devDependencies": {
+ "@types/react": "^18.3.20",
+ "@types/react-dom": "^18.3.6"
+ }
+}
diff --git a/examples/container-with-vitest/public/favicon.svg b/examples/container-with-vitest/public/favicon.svg
new file mode 100644
index 000000000..f157bd1c5
--- /dev/null
+++ b/examples/container-with-vitest/public/favicon.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
+ <path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
+ <style>
+ path { fill: #000; }
+ @media (prefers-color-scheme: dark) {
+ path { fill: #FFF; }
+ }
+ </style>
+</svg>
diff --git a/examples/container-with-vitest/src/components/Card.astro b/examples/container-with-vitest/src/components/Card.astro
new file mode 100644
index 000000000..a7e49f41c
--- /dev/null
+++ b/examples/container-with-vitest/src/components/Card.astro
@@ -0,0 +1,8 @@
+---
+
+---
+
+<div>
+ This is a card
+ <slot />
+</div>
diff --git a/examples/container-with-vitest/src/components/Counter.jsx b/examples/container-with-vitest/src/components/Counter.jsx
new file mode 100644
index 000000000..2148bf3d8
--- /dev/null
+++ b/examples/container-with-vitest/src/components/Counter.jsx
@@ -0,0 +1,14 @@
+import { useState } from 'react';
+
+export default function({ initialCount }) {
+ const [count, setCount] = useState(initialCount || 0);
+ return (
+ <div className="rounded-t-lg overflow-hidden border-t border-l border-r border-gray-400 text-center p-4">
+ <h2 className="font-semibold text-lg">Counter</h2>
+ <h3 className="font-medium text-lg">Count: {count}</h3>
+ <button
+ className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
+ onClick={() => setCount(count + 1)}>Increment</button>
+ </div>
+ )
+}
diff --git a/examples/container-with-vitest/src/components/CounterLight.astro b/examples/container-with-vitest/src/components/CounterLight.astro
new file mode 100644
index 000000000..7cee23bbe
--- /dev/null
+++ b/examples/container-with-vitest/src/components/CounterLight.astro
@@ -0,0 +1,9 @@
+---
+interface Props {
+ count?: number;
+}
+
+let { count = 0 } = Astro.props;
+---
+
+<p id="counter">{count}</p>
diff --git a/examples/container-with-vitest/src/components/ReactWrapper.astro b/examples/container-with-vitest/src/components/ReactWrapper.astro
new file mode 100644
index 000000000..73ac6baeb
--- /dev/null
+++ b/examples/container-with-vitest/src/components/ReactWrapper.astro
@@ -0,0 +1,5 @@
+---
+import Counter from './Counter.jsx';
+---
+
+<Counter initialCount={5} />
diff --git a/examples/container-with-vitest/src/pages/[locale].astro b/examples/container-with-vitest/src/pages/[locale].astro
new file mode 100644
index 000000000..b76d36d39
--- /dev/null
+++ b/examples/container-with-vitest/src/pages/[locale].astro
@@ -0,0 +1,20 @@
+---
+export function getStaticPaths() {
+ return [{ params: { locale: 'en' } }];
+}
+const { locale } = Astro.params;
+---
+
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+ <meta name="viewport" content="width=device-width" />
+ <meta name="generator" content={Astro.generator} />
+ <title>Astro</title>
+ </head>
+ <body>
+ <h1>Astro</h1>
+ <p>Locale: {locale}</p>
+ </body>
+</html>
diff --git a/examples/container-with-vitest/src/pages/api.ts b/examples/container-with-vitest/src/pages/api.ts
new file mode 100644
index 000000000..c30def5bb
--- /dev/null
+++ b/examples/container-with-vitest/src/pages/api.ts
@@ -0,0 +1,11 @@
+export function GET() {
+ const json = {
+ foo: 'bar',
+ number: 1,
+ };
+ return new Response(JSON.stringify(json), {
+ headers: {
+ 'content-type': 'application/json',
+ },
+ });
+}
diff --git a/examples/container-with-vitest/src/pages/index.astro b/examples/container-with-vitest/src/pages/index.astro
new file mode 100644
index 000000000..2d1410736
--- /dev/null
+++ b/examples/container-with-vitest/src/pages/index.astro
@@ -0,0 +1,16 @@
+---
+
+---
+
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+ <meta name="viewport" content="width=device-width" />
+ <meta name="generator" content={Astro.generator} />
+ <title>Astro</title>
+ </head>
+ <body>
+ <h1>Astro</h1>
+ </body>
+</html>
diff --git a/examples/container-with-vitest/test/Card.test.ts b/examples/container-with-vitest/test/Card.test.ts
new file mode 100644
index 000000000..119087a36
--- /dev/null
+++ b/examples/container-with-vitest/test/Card.test.ts
@@ -0,0 +1,29 @@
+import { experimental_AstroContainer as AstroContainer } from 'astro/container';
+import { expect, test } from 'vitest';
+import Card from '../src/components/Card.astro';
+import CounterLight from '../src/components/CounterLight.astro';
+
+test('Card with slots', async () => {
+ const container = await AstroContainer.create();
+ const result = await container.renderToString(Card, {
+ slots: {
+ default: 'Card content',
+ },
+ });
+
+ expect(result).toContain('This is a card');
+ expect(result).toContain('Card content');
+});
+
+test('Card with nested CounterLight', async () => {
+ const container = await AstroContainer.create();
+ const counterLight = await container.renderToString(CounterLight, { props: { count: 1 } });
+ const result = await container.renderToString(Card, {
+ slots: {
+ default: counterLight,
+ },
+ });
+
+ expect(result).toContain('This is a card');
+ expect(result).toContain(counterLight);
+});
diff --git a/examples/container-with-vitest/test/ReactWrapper.test.ts b/examples/container-with-vitest/test/ReactWrapper.test.ts
new file mode 100644
index 000000000..6adbff6cf
--- /dev/null
+++ b/examples/container-with-vitest/test/ReactWrapper.test.ts
@@ -0,0 +1,17 @@
+import { loadRenderers } from 'astro:container';
+import { getContainerRenderer } from '@astrojs/react';
+import { experimental_AstroContainer as AstroContainer } from 'astro/container';
+import { expect, test } from 'vitest';
+import ReactWrapper from '../src/components/ReactWrapper.astro';
+
+const renderers = await loadRenderers([getContainerRenderer()]);
+const container = await AstroContainer.create({
+ renderers,
+});
+
+test('ReactWrapper with react renderer', async () => {
+ const result = await container.renderToString(ReactWrapper);
+
+ expect(result).toContain('Counter');
+ expect(result).toContain('Count: <!-- -->5');
+});
diff --git a/examples/container-with-vitest/test/[locale].test.ts b/examples/container-with-vitest/test/[locale].test.ts
new file mode 100644
index 000000000..db450df53
--- /dev/null
+++ b/examples/container-with-vitest/test/[locale].test.ts
@@ -0,0 +1,16 @@
+import { experimental_AstroContainer as AstroContainer } from 'astro/container';
+import { expect, test } from 'vitest';
+import Locale from '../src/pages/[locale].astro';
+
+test('Dynamic route', async () => {
+ const container = await AstroContainer.create();
+ // @ts-ignore
+ const result = await container.renderToString(Locale, {
+ params: {
+ locale: 'en',
+ },
+ request: new Request('http://example.com/en'),
+ });
+
+ expect(result).toContain('Locale: en');
+});
diff --git a/examples/container-with-vitest/tsconfig.json b/examples/container-with-vitest/tsconfig.json
new file mode 100644
index 000000000..8bf91d3bb
--- /dev/null
+++ b/examples/container-with-vitest/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "extends": "astro/tsconfigs/strict",
+ "include": [".astro/types.d.ts", "**/*"],
+ "exclude": ["dist"]
+}
diff --git a/examples/container-with-vitest/vitest.config.ts b/examples/container-with-vitest/vitest.config.ts
new file mode 100644
index 000000000..a34f19bb1
--- /dev/null
+++ b/examples/container-with-vitest/vitest.config.ts
@@ -0,0 +1,9 @@
+/// <reference types="vitest" />
+import { getViteConfig } from 'astro/config';
+
+export default getViteConfig({
+ test: {
+ /* for example, use global to avoid globals imports (describe, test, expect): */
+ // globals: true,
+ },
+});
diff --git a/examples/framework-alpine/.codesandbox/Dockerfile b/examples/framework-alpine/.codesandbox/Dockerfile
new file mode 100644
index 000000000..c3b5c81a1
--- /dev/null
+++ b/examples/framework-alpine/.codesandbox/Dockerfile
@@ -0,0 +1 @@
+FROM node:18-bullseye
diff --git a/examples/framework-alpine/.gitignore b/examples/framework-alpine/.gitignore
new file mode 100644
index 000000000..16d54bb13
--- /dev/null
+++ b/examples/framework-alpine/.gitignore
@@ -0,0 +1,24 @@
+# build output
+dist/
+# generated types
+.astro/
+
+# dependencies
+node_modules/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
+
+# jetbrains setting folder
+.idea/
diff --git a/examples/framework-alpine/.vscode/extensions.json b/examples/framework-alpine/.vscode/extensions.json
new file mode 100644
index 000000000..22a15055d
--- /dev/null
+++ b/examples/framework-alpine/.vscode/extensions.json
@@ -0,0 +1,4 @@
+{
+ "recommendations": ["astro-build.astro-vscode"],
+ "unwantedRecommendations": []
+}
diff --git a/examples/framework-alpine/.vscode/launch.json b/examples/framework-alpine/.vscode/launch.json
new file mode 100644
index 000000000..d64220976
--- /dev/null
+++ b/examples/framework-alpine/.vscode/launch.json
@@ -0,0 +1,11 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "command": "./node_modules/.bin/astro dev",
+ "name": "Development server",
+ "request": "launch",
+ "type": "node-terminal"
+ }
+ ]
+}
diff --git a/examples/framework-alpine/README.md b/examples/framework-alpine/README.md
new file mode 100644
index 000000000..22cc75374
--- /dev/null
+++ b/examples/framework-alpine/README.md
@@ -0,0 +1,11 @@
+# Astro + AlpineJS Example
+
+```sh
+npm create astro@latest -- --template framework-alpine
+```
+
+[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/framework-alpine)
+[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/framework-alpine)
+[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/framework-alpine/devcontainer.json)
+
+This example showcases Astro working with [AlpineJS](https://alpinejs.dev/).
diff --git a/examples/framework-alpine/astro.config.mjs b/examples/framework-alpine/astro.config.mjs
new file mode 100644
index 000000000..5fddf7717
--- /dev/null
+++ b/examples/framework-alpine/astro.config.mjs
@@ -0,0 +1,8 @@
+// @ts-check
+import { defineConfig } from 'astro/config';
+import alpine from '@astrojs/alpinejs';
+
+// https://astro.build/config
+export default defineConfig({
+ integrations: [alpine()],
+});
diff --git a/examples/framework-alpine/package.json b/examples/framework-alpine/package.json
new file mode 100644
index 000000000..6b57ab0ab
--- /dev/null
+++ b/examples/framework-alpine/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "@example/framework-alpine",
+ "type": "module",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "dev": "astro dev",
+ "build": "astro build",
+ "preview": "astro preview",
+ "astro": "astro"
+ },
+ "dependencies": {
+ "@astrojs/alpinejs": "^0.4.8",
+ "@types/alpinejs": "^3.13.11",
+ "alpinejs": "^3.14.9",
+ "astro": "^5.9.0"
+ }
+}
diff --git a/examples/framework-alpine/public/favicon.svg b/examples/framework-alpine/public/favicon.svg
new file mode 100644
index 000000000..f157bd1c5
--- /dev/null
+++ b/examples/framework-alpine/public/favicon.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
+ <path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
+ <style>
+ path { fill: #000; }
+ @media (prefers-color-scheme: dark) {
+ path { fill: #FFF; }
+ }
+ </style>
+</svg>
diff --git a/examples/framework-alpine/src/components/Counter.astro b/examples/framework-alpine/src/components/Counter.astro
new file mode 100644
index 000000000..00182f006
--- /dev/null
+++ b/examples/framework-alpine/src/components/Counter.astro
@@ -0,0 +1,34 @@
+---
+// Full Astro Component Syntax:
+// https://docs.astro.build/basics/astro-components/
+
+interface Props {
+ initialCount?: number;
+}
+
+const { initialCount = 0 } = Astro.props;
+---
+
+<div class="counter" x-data={`{ count: ${initialCount} }`}>
+ <button x-on:click="count--">-</button>
+ <pre x-text="count">{ initialCount }</pre>
+ <button x-on:click="count++">+</button>
+</div>
+
+<div class="counter-message">
+ <slot />
+</div>
+
+<style>
+ .counter {
+ display: grid;
+ font-size: 2em;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ margin-top: 2em;
+ place-items: center;
+ }
+
+ .counter-message {
+ text-align: center;
+ }
+</style>
diff --git a/examples/framework-alpine/src/pages/index.astro b/examples/framework-alpine/src/pages/index.astro
new file mode 100644
index 000000000..e77744cb2
--- /dev/null
+++ b/examples/framework-alpine/src/pages/index.astro
@@ -0,0 +1,39 @@
+---
+// Component Imports
+import Counter from '../components/Counter.astro';
+
+// Full Astro Component Syntax:
+// https://docs.astro.build/basics/astro-components/
+---
+
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width" />
+ <meta name="generator" content={Astro.generator} />
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+ <style>
+ html,
+ body {
+ font-family: system-ui;
+ margin: 0;
+ }
+ body {
+ padding: 2rem;
+ }
+ </style>
+ </head>
+ <body>
+ <main>
+ <!-- Note: no `client:load` necessary since AlpineJS is always included -->
+ <Counter>
+ <h1>Hello, AlpineJS!</h1>
+ </Counter>
+
+ <!-- Note: pass props to Astro components to initialize Alpine with a certain state -->
+ <Counter initialCount={5}>
+ <h2>Use Astro to pass in server-side props</h2>
+ </Counter>
+ </main>
+ </body>
+</html>
diff --git a/examples/framework-alpine/tsconfig.json b/examples/framework-alpine/tsconfig.json
new file mode 100644
index 000000000..8bf91d3bb
--- /dev/null
+++ b/examples/framework-alpine/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "extends": "astro/tsconfigs/strict",
+ "include": [".astro/types.d.ts", "**/*"],
+ "exclude": ["dist"]
+}
diff --git a/examples/framework-multiple/.codesandbox/Dockerfile b/examples/framework-multiple/.codesandbox/Dockerfile
new file mode 100644
index 000000000..c3b5c81a1
--- /dev/null
+++ b/examples/framework-multiple/.codesandbox/Dockerfile
@@ -0,0 +1 @@
+FROM node:18-bullseye
diff --git a/examples/framework-multiple/.gitignore b/examples/framework-multiple/.gitignore
new file mode 100644
index 000000000..16d54bb13
--- /dev/null
+++ b/examples/framework-multiple/.gitignore
@@ -0,0 +1,24 @@
+# build output
+dist/
+# generated types
+.astro/
+
+# dependencies
+node_modules/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
+
+# jetbrains setting folder
+.idea/
diff --git a/examples/framework-multiple/.vscode/extensions.json b/examples/framework-multiple/.vscode/extensions.json
new file mode 100644
index 000000000..22a15055d
--- /dev/null
+++ b/examples/framework-multiple/.vscode/extensions.json
@@ -0,0 +1,4 @@
+{
+ "recommendations": ["astro-build.astro-vscode"],
+ "unwantedRecommendations": []
+}
diff --git a/examples/framework-multiple/.vscode/launch.json b/examples/framework-multiple/.vscode/launch.json
new file mode 100644
index 000000000..d64220976
--- /dev/null
+++ b/examples/framework-multiple/.vscode/launch.json
@@ -0,0 +1,11 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "command": "./node_modules/.bin/astro dev",
+ "name": "Development server",
+ "request": "launch",
+ "type": "node-terminal"
+ }
+ ]
+}
diff --git a/examples/framework-multiple/README.md b/examples/framework-multiple/README.md
new file mode 100644
index 000000000..30b020eff
--- /dev/null
+++ b/examples/framework-multiple/README.md
@@ -0,0 +1,13 @@
+# Kitchen Sink: Microfrontends with Astro
+
+```sh
+npm create astro@latest -- --template framework-multiple
+```
+
+[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/framework-multiple)
+[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/framework-multiple)
+[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/framework-multiple/devcontainer.json)
+
+This example showcases Astro's built-in support for multiple frameworks ([React](https://react.dev), [Preact](https://preactjs.com), [Svelte](https://svelte.dev), and [Vue (`v3.x`)](https://v3.vuejs.org/)).
+
+No configuration is needed to enable these frameworks—just start writing components in `src/components`.
diff --git a/examples/framework-multiple/astro.config.mjs b/examples/framework-multiple/astro.config.mjs
new file mode 100644
index 000000000..7609d3fc6
--- /dev/null
+++ b/examples/framework-multiple/astro.config.mjs
@@ -0,0 +1,19 @@
+// @ts-check
+import { defineConfig } from 'astro/config';
+import preact from '@astrojs/preact';
+import react from '@astrojs/react';
+import svelte from '@astrojs/svelte';
+import vue from '@astrojs/vue';
+import solid from '@astrojs/solid-js';
+
+// https://astro.build/config
+export default defineConfig({
+ // Enable many frameworks to support all different kinds of components.
+ integrations: [
+ preact({ include: ['**/preact/*'] }),
+ solid({ include: ['**/solid/*'] }),
+ react({ include: ['**/react/*'] }),
+ svelte(),
+ vue(),
+ ],
+});
diff --git a/examples/framework-multiple/package.json b/examples/framework-multiple/package.json
new file mode 100644
index 000000000..38c9b084c
--- /dev/null
+++ b/examples/framework-multiple/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "@example/framework-multiple",
+ "type": "module",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "dev": "astro dev",
+ "build": "astro build",
+ "preview": "astro preview",
+ "astro": "astro"
+ },
+ "dependencies": {
+ "@astrojs/preact": "^4.1.0",
+ "@astrojs/react": "^4.3.0",
+ "@astrojs/solid-js": "^5.1.0",
+ "@astrojs/svelte": "^7.1.0",
+ "@astrojs/vue": "^5.1.0",
+ "@types/react": "^18.3.20",
+ "@types/react-dom": "^18.3.6",
+ "astro": "^5.9.0",
+ "preact": "^10.26.5",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "solid-js": "^1.9.5",
+ "svelte": "^5.25.7",
+ "vue": "^3.5.13"
+ }
+}
diff --git a/examples/framework-multiple/public/favicon.svg b/examples/framework-multiple/public/favicon.svg
new file mode 100644
index 000000000..f157bd1c5
--- /dev/null
+++ b/examples/framework-multiple/public/favicon.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
+ <path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
+ <style>
+ path { fill: #000; }
+ @media (prefers-color-scheme: dark) {
+ path { fill: #FFF; }
+ }
+ </style>
+</svg>
diff --git a/examples/framework-multiple/src/components/preact/PreactCounter.tsx b/examples/framework-multiple/src/components/preact/PreactCounter.tsx
new file mode 100644
index 000000000..5ad164cc2
--- /dev/null
+++ b/examples/framework-multiple/src/components/preact/PreactCounter.tsx
@@ -0,0 +1,22 @@
+/** @jsxImportSource preact */
+
+import { useState } from 'preact/hooks';
+import type { ComponentChildren } from 'preact';
+
+/** A counter written with Preact */
+export function PreactCounter({ children }: { children?: ComponentChildren }) {
+ const [count, setCount] = useState(0);
+ const add = () => setCount((i) => i + 1);
+ const subtract = () => setCount((i) => i - 1);
+
+ return (
+ <>
+ <div class="counter">
+ <button onClick={subtract}>-</button>
+ <pre>{count}</pre>
+ <button onClick={add}>+</button>
+ </div>
+ <div class="counter-message">{children}</div>
+ </>
+ );
+}
diff --git a/examples/framework-multiple/src/components/react/ReactCounter.tsx b/examples/framework-multiple/src/components/react/ReactCounter.tsx
new file mode 100644
index 000000000..84681035d
--- /dev/null
+++ b/examples/framework-multiple/src/components/react/ReactCounter.tsx
@@ -0,0 +1,21 @@
+/** @jsxImportSource react */
+
+import { useState, type ReactNode } from 'react';
+
+/** A counter written with React */
+export function Counter({ children }: { children?: ReactNode }) {
+ const [count, setCount] = useState(0);
+ const add = () => setCount((i) => i + 1);
+ const subtract = () => setCount((i) => i - 1);
+
+ return (
+ <>
+ <div className="counter">
+ <button onClick={subtract}>-</button>
+ <pre>{count}</pre>
+ <button onClick={add}>+</button>
+ </div>
+ <div className="counter-message">{children}</div>
+ </>
+ );
+}
diff --git a/examples/framework-multiple/src/components/solid/SolidCounter.tsx b/examples/framework-multiple/src/components/solid/SolidCounter.tsx
new file mode 100644
index 000000000..cb9219608
--- /dev/null
+++ b/examples/framework-multiple/src/components/solid/SolidCounter.tsx
@@ -0,0 +1,21 @@
+/** @jsxImportSource solid-js */
+
+import { createSignal, type JSX } from 'solid-js';
+
+/** A counter written with Solid */
+export default function SolidCounter(props: { children?: JSX.Element }) {
+ const [count, setCount] = createSignal(0);
+ const add = () => setCount(count() + 1);
+ const subtract = () => setCount(count() - 1);
+
+ return (
+ <>
+ <div id="solid" class="counter">
+ <button onClick={subtract}>-</button>
+ <pre>{count()}</pre>
+ <button onClick={add}>+</button>
+ </div>
+ <div class="counter-message">{props.children}</div>
+ </>
+ );
+}
diff --git a/examples/framework-multiple/src/components/svelte/SvelteCounter.svelte b/examples/framework-multiple/src/components/svelte/SvelteCounter.svelte
new file mode 100644
index 000000000..641312ae1
--- /dev/null
+++ b/examples/framework-multiple/src/components/svelte/SvelteCounter.svelte
@@ -0,0 +1,30 @@
+<!-- @component
+A counter written with Svelte
+-->
+<script lang="ts">
+ import type { Snippet } from 'svelte';
+
+ interface Props {
+ children?: Snippet
+ }
+
+ let { children }: Props = $props();
+ let count = $state(0);
+
+ function add() {
+ count += 1;
+ }
+
+ function subtract() {
+ count -= 1;
+ }
+</script>
+
+<div class="counter">
+ <button onclick={subtract}>-</button>
+ <pre>{count}</pre>
+ <button onclick={add}>+</button>
+</div>
+<div class="counter-message">
+ {@render children?.()}
+</div>
diff --git a/examples/framework-multiple/src/components/vue/VueCounter.vue b/examples/framework-multiple/src/components/vue/VueCounter.vue
new file mode 100644
index 000000000..74820f7f0
--- /dev/null
+++ b/examples/framework-multiple/src/components/vue/VueCounter.vue
@@ -0,0 +1,33 @@
+<template>
+ <!--
+ Seeing type errors on the word `class`?
+ This unfortunately happens because @types/react's JSX definitions leak into every file due to being declared globally.
+ There's currently no way to prevent this when using both Vue and React with TypeScript in the same project.
+ You can read more about this issue here: https://github.com/johnsoncodehk/volar/discussions/592
+ -->
+ <div class="counter">
+ <button @click="subtract()">-</button>
+ <pre>{{ count }}</pre>
+ <button @click="add()">+</button>
+ </div>
+ <div class="counter-message">
+ <slot />
+ </div>
+</template>
+
+<script lang="ts">
+import { ref } from 'vue';
+export default {
+ setup() {
+ const count = ref(0);
+ const add = () => (count.value = count.value + 1);
+ const subtract = () => (count.value = count.value - 1);
+
+ return {
+ count,
+ add,
+ subtract,
+ };
+ },
+};
+</script>
diff --git a/examples/framework-multiple/src/pages/index.astro b/examples/framework-multiple/src/pages/index.astro
new file mode 100644
index 000000000..d420111a2
--- /dev/null
+++ b/examples/framework-multiple/src/pages/index.astro
@@ -0,0 +1,48 @@
+---
+// Style Imports
+import '../styles/global.css';
+
+// Component Imports
+// For JSX components, all the common ways of exporting (under a namespace, specific export, default export etc) are supported!
+import * as react from '../components/react/ReactCounter';
+import { PreactCounter } from '../components/preact/PreactCounter';
+import SolidCounter from '../components/solid/SolidCounter';
+
+import VueCounter from '../components/vue/VueCounter.vue';
+import SvelteCounter from '../components/svelte/SvelteCounter.svelte';
+
+// Full Astro Component Syntax:
+// https://docs.astro.build/basics/astro-components/
+---
+
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width" />
+ <meta name="generator" content={Astro.generator} />
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+ </head>
+ <body>
+ <main>
+ <react.Counter client:visible>
+ <h1>Hello from React!</h1>
+ </react.Counter>
+
+ <PreactCounter client:visible>
+ <h1>Hello from Preact!</h1>
+ </PreactCounter>
+
+ <SolidCounter client:visible>
+ <h1>Hello from Solid!</h1>
+ </SolidCounter>
+
+ <VueCounter client:visible>
+ <h1>Hello from Vue!</h1>
+ </VueCounter>
+
+ <SvelteCounter client:visible>
+ <h1>Hello from Svelte!</h1>
+ </SvelteCounter>
+ </main>
+ </body>
+</html>
diff --git a/examples/framework-multiple/src/styles/global.css b/examples/framework-multiple/src/styles/global.css
new file mode 100644
index 000000000..4912b4c39
--- /dev/null
+++ b/examples/framework-multiple/src/styles/global.css
@@ -0,0 +1,21 @@
+html,
+body {
+ font-family: system-ui;
+ margin: 0;
+}
+
+body {
+ padding: 2rem;
+}
+
+.counter {
+ display: grid;
+ font-size: 2em;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ margin-top: 2em;
+ place-items: center;
+}
+
+.counter-message {
+ text-align: center;
+}
diff --git a/examples/framework-multiple/tsconfig.json b/examples/framework-multiple/tsconfig.json
new file mode 100644
index 000000000..a19c58a62
--- /dev/null
+++ b/examples/framework-multiple/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "astro/tsconfigs/strict",
+ "include": [".astro/types.d.ts", "**/*"],
+ "exclude": ["dist"],
+ "compilerOptions": {
+ // Needed for TypeScript intellisense in the template inside Vue files
+ "jsx": "preserve"
+ }
+}
diff --git a/examples/framework-preact/.codesandbox/Dockerfile b/examples/framework-preact/.codesandbox/Dockerfile
new file mode 100644
index 000000000..c3b5c81a1
--- /dev/null
+++ b/examples/framework-preact/.codesandbox/Dockerfile
@@ -0,0 +1 @@
+FROM node:18-bullseye
diff --git a/examples/framework-preact/.gitignore b/examples/framework-preact/.gitignore
new file mode 100644
index 000000000..16d54bb13
--- /dev/null
+++ b/examples/framework-preact/.gitignore
@@ -0,0 +1,24 @@
+# build output
+dist/
+# generated types
+.astro/
+
+# dependencies
+node_modules/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
+
+# jetbrains setting folder
+.idea/
diff --git a/examples/framework-preact/.vscode/extensions.json b/examples/framework-preact/.vscode/extensions.json
new file mode 100644
index 000000000..22a15055d
--- /dev/null
+++ b/examples/framework-preact/.vscode/extensions.json
@@ -0,0 +1,4 @@
+{
+ "recommendations": ["astro-build.astro-vscode"],
+ "unwantedRecommendations": []
+}
diff --git a/examples/framework-preact/.vscode/launch.json b/examples/framework-preact/.vscode/launch.json
new file mode 100644
index 000000000..d64220976
--- /dev/null
+++ b/examples/framework-preact/.vscode/launch.json
@@ -0,0 +1,11 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "command": "./node_modules/.bin/astro dev",
+ "name": "Development server",
+ "request": "launch",
+ "type": "node-terminal"
+ }
+ ]
+}
diff --git a/examples/framework-preact/README.md b/examples/framework-preact/README.md
new file mode 100644
index 000000000..04efb2d6e
--- /dev/null
+++ b/examples/framework-preact/README.md
@@ -0,0 +1,13 @@
+# Astro + Preact Example
+
+```sh
+npm create astro@latest -- --template framework-preact
+```
+
+[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/framework-preact)
+[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/framework-preact)
+[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/framework-preact/devcontainer.json)
+
+This example showcases Astro working with [Preact](https://preactjs.com).
+
+Write your Preact components as `.jsx` or `.tsx` files in your project.
diff --git a/examples/framework-preact/astro.config.mjs b/examples/framework-preact/astro.config.mjs
new file mode 100644
index 000000000..1d726f9fe
--- /dev/null
+++ b/examples/framework-preact/astro.config.mjs
@@ -0,0 +1,9 @@
+// @ts-check
+import { defineConfig } from 'astro/config';
+import preact from '@astrojs/preact';
+
+// https://astro.build/config
+export default defineConfig({
+ // Enable Preact to support Preact JSX components.
+ integrations: [preact()],
+});
diff --git a/examples/framework-preact/package.json b/examples/framework-preact/package.json
new file mode 100644
index 000000000..4b081ed66
--- /dev/null
+++ b/examples/framework-preact/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "@example/framework-preact",
+ "type": "module",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "dev": "astro dev",
+ "build": "astro build",
+ "preview": "astro preview",
+ "astro": "astro"
+ },
+ "dependencies": {
+ "@astrojs/preact": "^4.1.0",
+ "@preact/signals": "^2.0.3",
+ "astro": "^5.9.0",
+ "preact": "^10.26.5"
+ }
+}
diff --git a/examples/framework-preact/public/favicon.svg b/examples/framework-preact/public/favicon.svg
new file mode 100644
index 000000000..f157bd1c5
--- /dev/null
+++ b/examples/framework-preact/public/favicon.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
+ <path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
+ <style>
+ path { fill: #000; }
+ @media (prefers-color-scheme: dark) {
+ path { fill: #FFF; }
+ }
+ </style>
+</svg>
diff --git a/examples/framework-preact/src/components/Counter.css b/examples/framework-preact/src/components/Counter.css
new file mode 100644
index 000000000..656b19d42
--- /dev/null
+++ b/examples/framework-preact/src/components/Counter.css
@@ -0,0 +1,7 @@
+.counter {
+ display: grid;
+ font-size: 2em;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ margin-top: 2em;
+ place-items: center;
+}
diff --git a/examples/framework-preact/src/components/Counter.tsx b/examples/framework-preact/src/components/Counter.tsx
new file mode 100644
index 000000000..a63bf0cd7
--- /dev/null
+++ b/examples/framework-preact/src/components/Counter.tsx
@@ -0,0 +1,30 @@
+import type { ComponentChildren } from 'preact';
+import type { Signal } from '@preact/signals';
+import { lazy, Suspense } from 'preact/compat';
+import './Counter.css';
+
+const Message = lazy(async () => import('./Message'));
+const Fallback = () => <p>Loading...</p>;
+
+type Props = {
+ children: ComponentChildren;
+ count: Signal<number>;
+};
+
+export default function Counter({ children, count }: Props) {
+ const add = () => count.value++;
+ const subtract = () => count.value--;
+
+ return (
+ <>
+ <div class="counter">
+ <button onClick={subtract}>-</button>
+ <pre>{count}</pre>
+ <button onClick={add}>+</button>
+ </div>
+ <Suspense fallback={Fallback}>
+ <Message>{children}</Message>
+ </Suspense>
+ </>
+ );
+}
diff --git a/examples/framework-preact/src/components/Message.css b/examples/framework-preact/src/components/Message.css
new file mode 100644
index 000000000..71ffc2c31
--- /dev/null
+++ b/examples/framework-preact/src/components/Message.css
@@ -0,0 +1,3 @@
+.message {
+ text-align: center;
+}
diff --git a/examples/framework-preact/src/components/Message.tsx b/examples/framework-preact/src/components/Message.tsx
new file mode 100644
index 000000000..58b798c14
--- /dev/null
+++ b/examples/framework-preact/src/components/Message.tsx
@@ -0,0 +1,6 @@
+import type { ComponentChildren } from 'preact';
+import './Message.css';
+
+export default function Message({ children }: { children: ComponentChildren }) {
+ return <div class="message">{children}</div>;
+}
diff --git a/examples/framework-preact/src/pages/index.astro b/examples/framework-preact/src/pages/index.astro
new file mode 100644
index 000000000..639fed70c
--- /dev/null
+++ b/examples/framework-preact/src/pages/index.astro
@@ -0,0 +1,41 @@
+---
+// Component Imports
+import Counter from '../components/Counter';
+
+import { signal } from '@preact/signals';
+
+// Full Astro Component Syntax:
+// https://docs.astro.build/basics/astro-components/
+
+const count = signal(0);
+---
+
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width" />
+ <meta name="generator" content={Astro.generator} />
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+ <style>
+ html,
+ body {
+ font-family: system-ui;
+ margin: 0;
+ }
+ body {
+ padding: 2rem;
+ }
+ </style>
+ </head>
+ <body>
+ <main>
+ <Counter count={count} client:visible>
+ <h1>Hello, Preact 1!</h1>
+ </Counter>
+
+ <Counter count={count} client:visible>
+ <h1>Hello, Preact 2!</h1>
+ </Counter>
+ </main>
+ </body>
+</html>
diff --git a/examples/framework-preact/tsconfig.json b/examples/framework-preact/tsconfig.json
new file mode 100644
index 000000000..c8983c2ef
--- /dev/null
+++ b/examples/framework-preact/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "astro/tsconfigs/strict",
+ "include": [".astro/types.d.ts", "**/*"],
+ "exclude": ["dist"],
+ "compilerOptions": {
+ // Preact specific settings
+ "jsx": "react-jsx",
+ "jsxImportSource": "preact"
+ }
+}
diff --git a/examples/framework-react/.codesandbox/Dockerfile b/examples/framework-react/.codesandbox/Dockerfile
new file mode 100644
index 000000000..c3b5c81a1
--- /dev/null
+++ b/examples/framework-react/.codesandbox/Dockerfile
@@ -0,0 +1 @@
+FROM node:18-bullseye
diff --git a/examples/framework-react/.gitignore b/examples/framework-react/.gitignore
new file mode 100644
index 000000000..16d54bb13
--- /dev/null
+++ b/examples/framework-react/.gitignore
@@ -0,0 +1,24 @@
+# build output
+dist/
+# generated types
+.astro/
+
+# dependencies
+node_modules/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
+
+# jetbrains setting folder
+.idea/
diff --git a/examples/framework-react/.vscode/extensions.json b/examples/framework-react/.vscode/extensions.json
new file mode 100644
index 000000000..22a15055d
--- /dev/null
+++ b/examples/framework-react/.vscode/extensions.json
@@ -0,0 +1,4 @@
+{
+ "recommendations": ["astro-build.astro-vscode"],
+ "unwantedRecommendations": []
+}
diff --git a/examples/framework-react/.vscode/launch.json b/examples/framework-react/.vscode/launch.json
new file mode 100644
index 000000000..d64220976
--- /dev/null
+++ b/examples/framework-react/.vscode/launch.json
@@ -0,0 +1,11 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "command": "./node_modules/.bin/astro dev",
+ "name": "Development server",
+ "request": "launch",
+ "type": "node-terminal"
+ }
+ ]
+}
diff --git a/examples/framework-react/README.md b/examples/framework-react/README.md
new file mode 100644
index 000000000..f036afa8c
--- /dev/null
+++ b/examples/framework-react/README.md
@@ -0,0 +1,13 @@
+# Astro + React Example
+
+```sh
+npm create astro@latest -- --template framework-react
+```
+
+[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/framework-react)
+[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/framework-react)
+[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/framework-react/devcontainer.json)
+
+This example showcases Astro working with [React](https://react.dev).
+
+Write your React components as `.jsx` or `.tsx` files in your project.
diff --git a/examples/framework-react/astro.config.mjs b/examples/framework-react/astro.config.mjs
new file mode 100644
index 000000000..36ed34a5e
--- /dev/null
+++ b/examples/framework-react/astro.config.mjs
@@ -0,0 +1,9 @@
+// @ts-check
+import { defineConfig } from 'astro/config';
+import react from '@astrojs/react';
+
+// https://astro.build/config
+export default defineConfig({
+ // Enable React to support React JSX components.
+ integrations: [react()],
+});
diff --git a/examples/framework-react/package.json b/examples/framework-react/package.json
new file mode 100644
index 000000000..e4df1335f
--- /dev/null
+++ b/examples/framework-react/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "@example/framework-react",
+ "type": "module",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "dev": "astro dev",
+ "build": "astro build",
+ "preview": "astro preview",
+ "astro": "astro"
+ },
+ "dependencies": {
+ "@astrojs/react": "^4.3.0",
+ "@types/react": "^18.3.20",
+ "@types/react-dom": "^18.3.6",
+ "astro": "^5.9.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ }
+}
diff --git a/examples/framework-react/public/favicon.svg b/examples/framework-react/public/favicon.svg
new file mode 100644
index 000000000..f157bd1c5
--- /dev/null
+++ b/examples/framework-react/public/favicon.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
+ <path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
+ <style>
+ path { fill: #000; }
+ @media (prefers-color-scheme: dark) {
+ path { fill: #FFF; }
+ }
+ </style>
+</svg>
diff --git a/examples/framework-react/src/components/Counter.css b/examples/framework-react/src/components/Counter.css
new file mode 100644
index 000000000..fb21044d7
--- /dev/null
+++ b/examples/framework-react/src/components/Counter.css
@@ -0,0 +1,11 @@
+.counter {
+ display: grid;
+ font-size: 2em;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ margin-top: 2em;
+ place-items: center;
+}
+
+.counter-message {
+ text-align: center;
+}
diff --git a/examples/framework-react/src/components/Counter.tsx b/examples/framework-react/src/components/Counter.tsx
new file mode 100644
index 000000000..cc416d3f1
--- /dev/null
+++ b/examples/framework-react/src/components/Counter.tsx
@@ -0,0 +1,25 @@
+import { useState } from 'react';
+import './Counter.css';
+
+export default function Counter({
+ children,
+ count: initialCount,
+}: {
+ children: JSX.Element;
+ count: number;
+}) {
+ const [count, setCount] = useState(initialCount);
+ const add = () => setCount((i) => i + 1);
+ const subtract = () => setCount((i) => i - 1);
+
+ return (
+ <>
+ <div className="counter">
+ <button onClick={subtract}>-</button>
+ <pre>{count}</pre>
+ <button onClick={add}>+</button>
+ </div>
+ <div className="counter-message">{children}</div>
+ </>
+ );
+}
diff --git a/examples/framework-react/src/pages/index.astro b/examples/framework-react/src/pages/index.astro
new file mode 100644
index 000000000..5bc1f8d36
--- /dev/null
+++ b/examples/framework-react/src/pages/index.astro
@@ -0,0 +1,36 @@
+---
+// Component Imports
+import Counter from '../components/Counter';
+const someProps = {
+ count: 0,
+};
+
+// Full Astro Component Syntax:
+// https://docs.astro.build/basics/astro-components/
+---
+
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width" />
+ <meta name="generator" content={Astro.generator} />
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+ <style>
+ html,
+ body {
+ font-family: system-ui;
+ margin: 0;
+ }
+ body {
+ padding: 2rem;
+ }
+ </style>
+ </head>
+ <body>
+ <main>
+ <Counter {...someProps} client:visible>
+ <h1>Hello, React!</h1>
+ </Counter>
+ </main>
+ </body>
+</html>
diff --git a/examples/framework-react/tsconfig.json b/examples/framework-react/tsconfig.json
new file mode 100644
index 000000000..92a18df90
--- /dev/null
+++ b/examples/framework-react/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "astro/tsconfigs/strict",
+ "include": [".astro/types.d.ts", "**/*"],
+ "exclude": ["dist"],
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "jsxImportSource": "react"
+ }
+}
diff --git a/examples/framework-solid/.codesandbox/Dockerfile b/examples/framework-solid/.codesandbox/Dockerfile
new file mode 100644
index 000000000..c3b5c81a1
--- /dev/null
+++ b/examples/framework-solid/.codesandbox/Dockerfile
@@ -0,0 +1 @@
+FROM node:18-bullseye
diff --git a/examples/framework-solid/.gitignore b/examples/framework-solid/.gitignore
new file mode 100644
index 000000000..16d54bb13
--- /dev/null
+++ b/examples/framework-solid/.gitignore
@@ -0,0 +1,24 @@
+# build output
+dist/
+# generated types
+.astro/
+
+# dependencies
+node_modules/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
+
+# jetbrains setting folder
+.idea/
diff --git a/examples/framework-solid/.vscode/extensions.json b/examples/framework-solid/.vscode/extensions.json
new file mode 100644
index 000000000..22a15055d
--- /dev/null
+++ b/examples/framework-solid/.vscode/extensions.json
@@ -0,0 +1,4 @@
+{
+ "recommendations": ["astro-build.astro-vscode"],
+ "unwantedRecommendations": []
+}
diff --git a/examples/framework-solid/.vscode/launch.json b/examples/framework-solid/.vscode/launch.json
new file mode 100644
index 000000000..d64220976
--- /dev/null
+++ b/examples/framework-solid/.vscode/launch.json
@@ -0,0 +1,11 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "command": "./node_modules/.bin/astro dev",
+ "name": "Development server",
+ "request": "launch",
+ "type": "node-terminal"
+ }
+ ]
+}
diff --git a/examples/framework-solid/README.md b/examples/framework-solid/README.md
new file mode 100644
index 000000000..2943a7692
--- /dev/null
+++ b/examples/framework-solid/README.md
@@ -0,0 +1,13 @@
+# Astro + Solid.js Example
+
+```sh
+npm create astro@latest -- --template framework-solid
+```
+
+[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/framework-solid)
+[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/framework-solid)
+[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/framework-solid/devcontainer.json)
+
+This example showcases Astro working with [Solid](https://www.solidjs.com/).
+
+Write your Solid components as `.jsx` or `.tsx` files in your project.
diff --git a/examples/framework-solid/astro.config.mjs b/examples/framework-solid/astro.config.mjs
new file mode 100644
index 000000000..8bdaa2679
--- /dev/null
+++ b/examples/framework-solid/astro.config.mjs
@@ -0,0 +1,9 @@
+// @ts-check
+import { defineConfig } from 'astro/config';
+import solid from '@astrojs/solid-js';
+
+// https://astro.build/config
+export default defineConfig({
+ // Enable Solid to support Solid JSX components.
+ integrations: [solid()],
+});
diff --git a/examples/framework-solid/package.json b/examples/framework-solid/package.json
new file mode 100644
index 000000000..da35aa2b0
--- /dev/null
+++ b/examples/framework-solid/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "@example/framework-solid",
+ "type": "module",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "dev": "astro dev",
+ "build": "astro build",
+ "preview": "astro preview",
+ "astro": "astro"
+ },
+ "dependencies": {
+ "@astrojs/solid-js": "^5.1.0",
+ "astro": "^5.9.0",
+ "solid-js": "^1.9.5"
+ }
+}
diff --git a/examples/framework-solid/public/favicon.svg b/examples/framework-solid/public/favicon.svg
new file mode 100644
index 000000000..f157bd1c5
--- /dev/null
+++ b/examples/framework-solid/public/favicon.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
+ <path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
+ <style>
+ path { fill: #000; }
+ @media (prefers-color-scheme: dark) {
+ path { fill: #FFF; }
+ }
+ </style>
+</svg>
diff --git a/examples/framework-solid/src/components/Counter.css b/examples/framework-solid/src/components/Counter.css
new file mode 100644
index 000000000..cffdbda7b
--- /dev/null
+++ b/examples/framework-solid/src/components/Counter.css
@@ -0,0 +1,11 @@
+.counter {
+ display: grid;
+ font-size: 2em;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ margin-top: 3em;
+ place-items: center;
+}
+
+.counter-message {
+ text-align: center;
+}
diff --git a/examples/framework-solid/src/components/Counter.tsx b/examples/framework-solid/src/components/Counter.tsx
new file mode 100644
index 000000000..d6f941999
--- /dev/null
+++ b/examples/framework-solid/src/components/Counter.tsx
@@ -0,0 +1,19 @@
+import { createSignal, type JSX } from 'solid-js';
+import './Counter.css';
+
+export default function Counter(props: { children?: JSX.Element }) {
+ const [count, setCount] = createSignal(0);
+ const add = () => setCount(count() + 1);
+ const subtract = () => setCount(count() - 1);
+
+ return (
+ <>
+ <div class="counter">
+ <button onClick={subtract}>-</button>
+ <pre>{count()}</pre>
+ <button onClick={add}>+</button>
+ </div>
+ <div class="counter-message">{props.children}</div>
+ </>
+ );
+}
diff --git a/examples/framework-solid/src/pages/index.astro b/examples/framework-solid/src/pages/index.astro
new file mode 100644
index 000000000..b191ef47f
--- /dev/null
+++ b/examples/framework-solid/src/pages/index.astro
@@ -0,0 +1,33 @@
+---
+// Component Imports
+import Counter from '../components/Counter';
+
+// Full Astro Component Syntax:
+// https://docs.astro.build/basics/astro-components/
+---
+
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width" />
+ <meta name="generator" content={Astro.generator} />
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+ <style>
+ html,
+ body {
+ font-family: system-ui;
+ margin: 0;
+ }
+ body {
+ padding: 2rem;
+ }
+ </style>
+ </head>
+ <body>
+ <main>
+ <Counter client:visible>
+ <h1>Hello, Solid!</h1>
+ </Counter>
+ </main>
+ </body>
+</html>
diff --git a/examples/framework-solid/tsconfig.json b/examples/framework-solid/tsconfig.json
new file mode 100644
index 000000000..76e1efdba
--- /dev/null
+++ b/examples/framework-solid/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "astro/tsconfigs/strict",
+ "include": [".astro/types.d.ts", "**/*"],
+ "exclude": ["dist"],
+ "compilerOptions": {
+ // Solid specific settings
+ "jsx": "preserve",
+ "jsxImportSource": "solid-js"
+ }
+}
diff --git a/examples/framework-svelte/.codesandbox/Dockerfile b/examples/framework-svelte/.codesandbox/Dockerfile
new file mode 100644
index 000000000..c3b5c81a1
--- /dev/null
+++ b/examples/framework-svelte/.codesandbox/Dockerfile
@@ -0,0 +1 @@
+FROM node:18-bullseye
diff --git a/examples/framework-svelte/.gitignore b/examples/framework-svelte/.gitignore
new file mode 100644
index 000000000..16d54bb13
--- /dev/null
+++ b/examples/framework-svelte/.gitignore
@@ -0,0 +1,24 @@
+# build output
+dist/
+# generated types
+.astro/
+
+# dependencies
+node_modules/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
+
+# jetbrains setting folder
+.idea/
diff --git a/examples/framework-svelte/.vscode/extensions.json b/examples/framework-svelte/.vscode/extensions.json
new file mode 100644
index 000000000..22a15055d
--- /dev/null
+++ b/examples/framework-svelte/.vscode/extensions.json
@@ -0,0 +1,4 @@
+{
+ "recommendations": ["astro-build.astro-vscode"],
+ "unwantedRecommendations": []
+}
diff --git a/examples/framework-svelte/.vscode/launch.json b/examples/framework-svelte/.vscode/launch.json
new file mode 100644
index 000000000..d64220976
--- /dev/null
+++ b/examples/framework-svelte/.vscode/launch.json
@@ -0,0 +1,11 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "command": "./node_modules/.bin/astro dev",
+ "name": "Development server",
+ "request": "launch",
+ "type": "node-terminal"
+ }
+ ]
+}
diff --git a/examples/framework-svelte/README.md b/examples/framework-svelte/README.md
new file mode 100644
index 000000000..5c5a24cc0
--- /dev/null
+++ b/examples/framework-svelte/README.md
@@ -0,0 +1,11 @@
+# Astro + Svelte Example
+
+```sh
+npm create astro@latest -- --template framework-svelte
+```
+
+[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/framework-svelte)
+[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/framework-svelte)
+[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/framework-svelte/devcontainer.json)
+
+This example showcases Astro working with [Svelte](https://svelte.dev/).
diff --git a/examples/framework-svelte/astro.config.mjs b/examples/framework-svelte/astro.config.mjs
new file mode 100644
index 000000000..5eeedb56e
--- /dev/null
+++ b/examples/framework-svelte/astro.config.mjs
@@ -0,0 +1,9 @@
+// @ts-check
+import { defineConfig } from 'astro/config';
+import svelte from '@astrojs/svelte';
+
+// https://astro.build/config
+export default defineConfig({
+ // Enable Svelte to support Svelte components.
+ integrations: [svelte()],
+});
diff --git a/examples/framework-svelte/package.json b/examples/framework-svelte/package.json
new file mode 100644
index 000000000..841a6825f
--- /dev/null
+++ b/examples/framework-svelte/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "@example/framework-svelte",
+ "type": "module",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "dev": "astro dev",
+ "build": "astro build",
+ "preview": "astro preview",
+ "astro": "astro"
+ },
+ "dependencies": {
+ "@astrojs/svelte": "^7.1.0",
+ "astro": "^5.9.0",
+ "svelte": "^5.25.7"
+ }
+}
diff --git a/examples/framework-svelte/public/favicon.svg b/examples/framework-svelte/public/favicon.svg
new file mode 100644
index 000000000..f157bd1c5
--- /dev/null
+++ b/examples/framework-svelte/public/favicon.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
+ <path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
+ <style>
+ path { fill: #000; }
+ @media (prefers-color-scheme: dark) {
+ path { fill: #FFF; }
+ }
+ </style>
+</svg>
diff --git a/examples/framework-svelte/src/components/Counter.svelte b/examples/framework-svelte/src/components/Counter.svelte
new file mode 100644
index 000000000..a11538645
--- /dev/null
+++ b/examples/framework-svelte/src/components/Counter.svelte
@@ -0,0 +1,41 @@
+<script lang="ts">
+ import type { Snippet } from 'svelte';
+
+ interface Props {
+ children?: Snippet
+ }
+
+ let { children }: Props = $props();
+ let count = $state(0);
+
+ function add() {
+ count += 1;
+ }
+
+ function subtract() {
+ count -= 1;
+ }
+</script>
+
+<div class="counter">
+ <button onclick={subtract}>-</button>
+ <pre>{count}</pre>
+ <button onclick={add}>+</button>
+</div>
+<div class="message">
+ {@render children?.()}
+</div>
+
+<style>
+ .counter {
+ display: grid;
+ font-size: 2em;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ margin-top: 2em;
+ place-items: center;
+ }
+
+ .message {
+ text-align: center;
+ }
+</style>
diff --git a/examples/framework-svelte/src/pages/index.astro b/examples/framework-svelte/src/pages/index.astro
new file mode 100644
index 000000000..c4f0c1efb
--- /dev/null
+++ b/examples/framework-svelte/src/pages/index.astro
@@ -0,0 +1,33 @@
+---
+// Component Imports
+import Counter from '../components/Counter.svelte';
+
+// Full Astro Component Syntax:
+// https://docs.astro.build/basics/astro-components/
+---
+
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width" />
+ <meta name="generator" content={Astro.generator} />
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+ <style>
+ html,
+ body {
+ font-family: system-ui;
+ margin: 0;
+ }
+ body {
+ padding: 2rem;
+ }
+ </style>
+ </head>
+ <body>
+ <main>
+ <Counter client:visible>
+ <h1>Hello, Svelte!</h1>
+ </Counter>
+ </main>
+ </body>
+</html>
diff --git a/examples/framework-svelte/svelte.config.js b/examples/framework-svelte/svelte.config.js
new file mode 100644
index 000000000..cbaee33df
--- /dev/null
+++ b/examples/framework-svelte/svelte.config.js
@@ -0,0 +1,5 @@
+import { vitePreprocess } from '@astrojs/svelte';
+
+export default {
+ preprocess: vitePreprocess(),
+};
diff --git a/examples/framework-svelte/tsconfig.json b/examples/framework-svelte/tsconfig.json
new file mode 100644
index 000000000..8bf91d3bb
--- /dev/null
+++ b/examples/framework-svelte/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "extends": "astro/tsconfigs/strict",
+ "include": [".astro/types.d.ts", "**/*"],
+ "exclude": ["dist"]
+}
diff --git a/examples/framework-vue/.codesandbox/Dockerfile b/examples/framework-vue/.codesandbox/Dockerfile
new file mode 100644
index 000000000..c3b5c81a1
--- /dev/null
+++ b/examples/framework-vue/.codesandbox/Dockerfile
@@ -0,0 +1 @@
+FROM node:18-bullseye
diff --git a/examples/framework-vue/.gitignore b/examples/framework-vue/.gitignore
new file mode 100644
index 000000000..16d54bb13
--- /dev/null
+++ b/examples/framework-vue/.gitignore
@@ -0,0 +1,24 @@
+# build output
+dist/
+# generated types
+.astro/
+
+# dependencies
+node_modules/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
+
+# jetbrains setting folder
+.idea/
diff --git a/examples/framework-vue/.vscode/extensions.json b/examples/framework-vue/.vscode/extensions.json
new file mode 100644
index 000000000..22a15055d
--- /dev/null
+++ b/examples/framework-vue/.vscode/extensions.json
@@ -0,0 +1,4 @@
+{
+ "recommendations": ["astro-build.astro-vscode"],
+ "unwantedRecommendations": []
+}
diff --git a/examples/framework-vue/.vscode/launch.json b/examples/framework-vue/.vscode/launch.json
new file mode 100644
index 000000000..d64220976
--- /dev/null
+++ b/examples/framework-vue/.vscode/launch.json
@@ -0,0 +1,11 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "command": "./node_modules/.bin/astro dev",
+ "name": "Development server",
+ "request": "launch",
+ "type": "node-terminal"
+ }
+ ]
+}
diff --git a/examples/framework-vue/README.md b/examples/framework-vue/README.md
new file mode 100644
index 000000000..14e778765
--- /dev/null
+++ b/examples/framework-vue/README.md
@@ -0,0 +1,11 @@
+# Astro + Vue Example
+
+```sh
+npm create astro@latest -- --template framework-vue
+```
+
+[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/framework-vue)
+[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/framework-vue)
+[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/framework-vue/devcontainer.json)
+
+This example showcases Astro working with [Vue](https://v3.vuejs.org/).
diff --git a/examples/framework-vue/astro.config.mjs b/examples/framework-vue/astro.config.mjs
new file mode 100644
index 000000000..5afe92269
--- /dev/null
+++ b/examples/framework-vue/astro.config.mjs
@@ -0,0 +1,9 @@
+// @ts-check
+import { defineConfig } from 'astro/config';
+import vue from '@astrojs/vue';
+
+// https://astro.build/config
+export default defineConfig({
+ // Enable Vue to support Vue components.
+ integrations: [vue()],
+});
diff --git a/examples/framework-vue/package.json b/examples/framework-vue/package.json
new file mode 100644
index 000000000..714ab5405
--- /dev/null
+++ b/examples/framework-vue/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "@example/framework-vue",
+ "type": "module",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "dev": "astro dev",
+ "build": "astro build",
+ "preview": "astro preview",
+ "astro": "astro"
+ },
+ "dependencies": {
+ "@astrojs/vue": "^5.1.0",
+ "astro": "^5.9.0",
+ "vue": "^3.5.13"
+ }
+}
diff --git a/examples/framework-vue/public/favicon.svg b/examples/framework-vue/public/favicon.svg
new file mode 100644
index 000000000..f157bd1c5
--- /dev/null
+++ b/examples/framework-vue/public/favicon.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
+ <path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
+ <style>
+ path { fill: #000; }
+ @media (prefers-color-scheme: dark) {
+ path { fill: #FFF; }
+ }
+ </style>
+</svg>
diff --git a/examples/framework-vue/src/components/Counter.vue b/examples/framework-vue/src/components/Counter.vue
new file mode 100644
index 000000000..11f2bf1b5
--- /dev/null
+++ b/examples/framework-vue/src/components/Counter.vue
@@ -0,0 +1,32 @@
+<script setup lang="ts">
+import { ref } from 'vue';
+
+const count = ref(0);
+const add = () => count.value++;
+const subtract = () => count.value--;
+</script>
+
+<template>
+ <div class="counter">
+ <button @click="subtract">-</button>
+ <pre>{{ count }}</pre>
+ <button @click="add">+</button>
+ </div>
+ <div class="counter-message">
+ <slot />
+ </div>
+</template>
+
+<style>
+.counter {
+ display: grid;
+ font-size: 2em;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ margin-top: 2em;
+ place-items: center;
+}
+
+.counter-message {
+ text-align: center;
+}
+</style>
diff --git a/examples/framework-vue/src/pages/index.astro b/examples/framework-vue/src/pages/index.astro
new file mode 100644
index 000000000..bae5effe8
--- /dev/null
+++ b/examples/framework-vue/src/pages/index.astro
@@ -0,0 +1,33 @@
+---
+// Component Imports
+import Counter from '../components/Counter.vue';
+
+// Full Astro Component Syntax:
+// https://docs.astro.build/basics/astro-components/
+---
+
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width" />
+ <meta name="generator" content={Astro.generator} />
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+ <style>
+ html,
+ body {
+ font-family: system-ui;
+ margin: 0;
+ }
+ body {
+ padding: 2rem;
+ }
+ </style>
+ </head>
+ <body>
+ <main>
+ <Counter client:visible>
+ <h1>Hello, Vue!</h1>
+ </Counter>
+ </main>
+ </body>
+</html>
diff --git a/examples/framework-vue/tsconfig.json b/examples/framework-vue/tsconfig.json
new file mode 100644
index 000000000..a19c58a62
--- /dev/null
+++ b/examples/framework-vue/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "astro/tsconfigs/strict",
+ "include": [".astro/types.d.ts", "**/*"],
+ "exclude": ["dist"],
+ "compilerOptions": {
+ // Needed for TypeScript intellisense in the template inside Vue files
+ "jsx": "preserve"
+ }
+}
diff --git a/examples/hackernews/.codesandbox/Dockerfile b/examples/hackernews/.codesandbox/Dockerfile
new file mode 100644
index 000000000..c3b5c81a1
--- /dev/null
+++ b/examples/hackernews/.codesandbox/Dockerfile
@@ -0,0 +1 @@
+FROM node:18-bullseye
diff --git a/examples/hackernews/.gitignore b/examples/hackernews/.gitignore
new file mode 100644
index 000000000..16d54bb13
--- /dev/null
+++ b/examples/hackernews/.gitignore
@@ -0,0 +1,24 @@
+# build output
+dist/
+# generated types
+.astro/
+
+# dependencies
+node_modules/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
+
+# jetbrains setting folder
+.idea/
diff --git a/examples/hackernews/.vscode/extensions.json b/examples/hackernews/.vscode/extensions.json
new file mode 100644
index 000000000..22a15055d
--- /dev/null
+++ b/examples/hackernews/.vscode/extensions.json
@@ -0,0 +1,4 @@
+{
+ "recommendations": ["astro-build.astro-vscode"],
+ "unwantedRecommendations": []
+}
diff --git a/examples/hackernews/.vscode/launch.json b/examples/hackernews/.vscode/launch.json
new file mode 100644
index 000000000..d64220976
--- /dev/null
+++ b/examples/hackernews/.vscode/launch.json
@@ -0,0 +1,11 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "command": "./node_modules/.bin/astro dev",
+ "name": "Development server",
+ "request": "launch",
+ "type": "node-terminal"
+ }
+ ]
+}
diff --git a/examples/hackernews/README.md b/examples/hackernews/README.md
new file mode 100644
index 000000000..55c657dc9
--- /dev/null
+++ b/examples/hackernews/README.md
@@ -0,0 +1,59 @@
+# Astro Starter Kit: Hackernews
+
+```sh
+npm create astro@latest -- --template hackernews
+```
+
+[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/hackernews)
+[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/hackernews)
+[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/hackernews/devcontainer.json)
+
+> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
+
+## 🚀 Project Structure
+
+Inside of your Astro project, you'll see the following folders and files:
+
+```text
+/
+├── public/
+│ └── favicon.svg
+├── src/
+│ ├── components/
+│ ├── layouts/
+│ │ └── Layout.astro
+│ └── pages/
+ └── stories/
+ └── [id].astro
+ └── users/
+ └── [id].astro
+│ └── [...stories].astro
+└── package.json
+```
+
+Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. Because the list of stories and users is always changing, dynamic routes like `[id].astro` are used to build pages when a specific page is requested.
+
+There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
+
+Any static assets, like images, can be placed in the `public/` directory.
+
+## Server-side rendering (SSR)
+
+This project uses the [`@astrojs/node`](https://docs.astro.build/en/guides/integrations-guide/node/) adapter to deploy the SSR site to Node targets. Check out Astro's [deployment docs](https://docs.astro.build/en/guides/deploy/) for details on other adapters and hosting environments.
+
+## 🧞 Commands
+
+All commands are run from the root of the project, from a terminal:
+
+| Command | Action |
+| :------------------------ | :----------------------------------------------- |
+| `npm install` | Installs dependencies |
+| `npm run dev` | Starts local dev server at `localhost:4321` |
+| `npm run build` | Build your production site to `./dist/` |
+| `npm run preview` | Preview your build locally, before deploying |
+| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
+| `npm run astro -- --help` | Get help using the Astro CLI |
+
+## 👀 Want to learn more?
+
+Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
diff --git a/examples/hackernews/astro.config.mjs b/examples/hackernews/astro.config.mjs
new file mode 100644
index 000000000..bf6f1a022
--- /dev/null
+++ b/examples/hackernews/astro.config.mjs
@@ -0,0 +1,11 @@
+// @ts-check
+import { defineConfig } from 'astro/config';
+import node from '@astrojs/node';
+
+// https://astro.build/config
+export default defineConfig({
+ output: 'server',
+ adapter: node({
+ mode: 'standalone',
+ }),
+});
diff --git a/examples/hackernews/package.json b/examples/hackernews/package.json
new file mode 100644
index 000000000..bbf102312
--- /dev/null
+++ b/examples/hackernews/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "@example/hackernews",
+ "type": "module",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "dev": "astro dev",
+ "build": "astro build",
+ "preview": "astro preview",
+ "astro": "astro"
+ },
+ "dependencies": {
+ "@astrojs/node": "^9.2.2",
+ "astro": "^5.9.0"
+ }
+}
diff --git a/examples/hackernews/public/favicon.svg b/examples/hackernews/public/favicon.svg
new file mode 100644
index 000000000..f157bd1c5
--- /dev/null
+++ b/examples/hackernews/public/favicon.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
+ <path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
+ <style>
+ path { fill: #000; }
+ @media (prefers-color-scheme: dark) {
+ path { fill: #FFF; }
+ }
+ </style>
+</svg>
diff --git a/examples/hackernews/src/components/Comment.astro b/examples/hackernews/src/components/Comment.astro
new file mode 100644
index 000000000..07e55d19b
--- /dev/null
+++ b/examples/hackernews/src/components/Comment.astro
@@ -0,0 +1,59 @@
+---
+import type { IComment } from '../types.js';
+import For from './For.astro';
+import Show from './Show.astro';
+import Toggle from './Toggle.astro';
+
+interface Props {
+ comment: IComment;
+}
+
+const { comment } = Astro.props;
+---
+
+<li>
+ <div class="by">
+ <a href={`/users/${comment.user}`}>{comment.user}</a>{' '}
+ {comment.time_ago}
+ </div>
+ <div class="text" set:html={comment.content} />
+ <Show when={comment.comments.length}>
+ <Toggle open>
+ <For each={comment.comments}>{(comment: IComment) => <Astro.self comment={comment} />}</For>
+ </Toggle>
+ </Show>
+</li>
+
+<style>
+ li {
+ border-top: 1px solid #eee;
+ position: relative;
+ }
+
+ .by,
+ .text {
+ font-size: 0.9em;
+ margin: 1em 0;
+ }
+
+ .by {
+ color: rgb(51 65 85);
+ }
+
+ .by a {
+ color: rgb(51 65 85);
+ text-decoration: underline;
+ }
+
+ .text {
+ overflow-wrap: break-word;
+ }
+
+ .text :global(a:hover) {
+ color: #335d92;
+ }
+
+ .text :global(pre) {
+ white-space: pre-wrap;
+ }
+</style>
diff --git a/examples/hackernews/src/components/For.astro b/examples/hackernews/src/components/For.astro
new file mode 100644
index 000000000..6eae88e27
--- /dev/null
+++ b/examples/hackernews/src/components/For.astro
@@ -0,0 +1,23 @@
+---
+import Show from './Show.astro';
+
+interface Props<T> {
+ each: Iterable<T>;
+}
+
+const { each } = Astro.props;
+---
+
+{
+ (async function* () {
+ for await (const value of each) {
+ let html = await Astro.slots.render('default', [value]);
+ yield <Fragment set:html={html} />;
+ yield '\n';
+ }
+ })()
+}
+
+<Show when={!each.length}>
+ <slot name="fallback" />
+</Show>
diff --git a/examples/hackernews/src/components/Nav.astro b/examples/hackernews/src/components/Nav.astro
new file mode 100644
index 000000000..7eeba2865
--- /dev/null
+++ b/examples/hackernews/src/components/Nav.astro
@@ -0,0 +1,99 @@
+---
+interface Link {
+ href: string;
+ text: string;
+}
+
+const links: Link[] = [
+ { href: '/', text: 'HN' },
+ { href: '/new', text: 'New' },
+ { href: '/show', text: 'Show' },
+ { href: '/ask', text: 'Ask' },
+ { href: '/job', text: 'Jobs' },
+];
+---
+
+<header>
+ <nav aria-label="Main menu">
+ {
+ links.map(({ href, text }) => (
+ <a href={href} aria-current={href === Astro.url.pathname ? 'page' : undefined}>
+ <strong>{text}</strong>
+ </a>
+ ))
+ }
+ <a class="github" href="http://github.com/withastro/astro" target="_blank" rel="noreferrer">
+ Built with Astro
+ </a>
+ </nav>
+</header>
+
+<style>
+ header {
+ background-color: rgb(107 33 168);
+ position: fixed;
+ z-index: 999;
+ height: 55px;
+ top: 0;
+ left: 0;
+ right: 0;
+ }
+
+ nav {
+ max-width: 800px;
+ box-sizing: border-box;
+ margin: 0 auto;
+ padding: 15px 5px;
+ }
+
+ nav a {
+ color: rgba(248, 250, 252, 0.8);
+ line-height: 24px;
+ transition: color 0.15s ease;
+ display: inline-block;
+ vertical-align: middle;
+ font-weight: 300;
+ letter-spacing: 0.075em;
+ margin-right: 1.8em;
+ }
+
+ nav a:hover {
+ color: rgb(248 250 252);
+ }
+
+ nav [aria-current='page'] {
+ color: rgb(248 250 252);
+ font-weight: 400;
+ }
+
+ nav a:last-of-type {
+ margin-right: 0;
+ }
+
+ .github {
+ color: rgb(248 250 252);
+ font-size: 0.9em;
+ margin: 0;
+ float: right;
+ }
+
+ @media (max-width: 860px) {
+ nav {
+ padding: 15px 30px;
+ }
+ }
+
+ @media (max-width: 600px) {
+ nav {
+ padding: 15px;
+ }
+
+ a {
+ margin-right: 1em;
+ }
+
+ .github {
+ display: none;
+ }
+ }
+</style>
diff --git a/examples/hackernews/src/components/Show.astro b/examples/hackernews/src/components/Show.astro
new file mode 100644
index 000000000..ccb642fd7
--- /dev/null
+++ b/examples/hackernews/src/components/Show.astro
@@ -0,0 +1,9 @@
+---
+interface Props<T> {
+ when: T | number | boolean | undefined | null;
+}
+
+const { when } = Astro.props;
+---
+
+{!!when ? <slot /> : <slot name="fallback" />}
diff --git a/examples/hackernews/src/components/Story.astro b/examples/hackernews/src/components/Story.astro
new file mode 100644
index 000000000..e91748a30
--- /dev/null
+++ b/examples/hackernews/src/components/Story.astro
@@ -0,0 +1,77 @@
+---
+import type { IStory } from '../types.js';
+import Show from './Show.astro';
+
+interface Props {
+ story: IStory;
+}
+
+const { story } = Astro.props;
+---
+
+<li>
+ <span class="score">{story.points}</span>
+ <span class="title">
+ <Show when={story.url}>
+ <a href={story.url} target="_blank" rel="noreferrer">
+ {story.title}
+ </a>
+ <span class="host"> ({story.domain})</span>
+ <a slot="fallback" href={`/item/${story.id}`}>{story.title}</a>
+ </Show>
+ </span>
+ <br />
+ <span class="meta">
+ <Show when={story.type !== 'job'}>
+ by <a href={`/users/${story.user}`}>{story.user}</a>{' '}
+ {story.time_ago}{' '}|{' '}
+ <a href={`/stories/${story.id}`}>
+ {story.comments_count ? `${story.comments_count} comments` : 'discuss'}
+ </a>
+ <a slot="fallback" href={`/stories/${story.id}`}>{story.time_ago}</a>
+ </Show>
+ </span>
+ <Show when={story.type !== 'link'}>
+ &nbsp;
+ <span class="label">{story.type}</span>
+ </Show>
+</li>
+
+<style>
+ li {
+ background-color: rgb(248 250 252);
+ padding: 20px 30px 20px 80px;
+ border-bottom: 1px solid #eee;
+ position: relative;
+ line-height: 20px;
+ }
+
+ .score {
+ color: rgb(88 28 135);
+ font-size: 1.1em;
+ font-weight: 700;
+ position: absolute;
+ top: 50%;
+ left: 0;
+ width: 80px;
+ text-align: center;
+ margin-top: -10px;
+ }
+
+ .host,
+ .meta {
+ font-size: 0.85em;
+ color: rgb(51 65 85);
+ }
+
+ .host a,
+ .meta a {
+ color: rgb(51 65 85);
+ text-decoration: underline;
+ }
+
+ .host a:hover,
+ .meta a:hover {
+ color: #335d92;
+ }
+</style>
diff --git a/examples/hackernews/src/components/Toggle.astro b/examples/hackernews/src/components/Toggle.astro
new file mode 100644
index 000000000..799fca08c
--- /dev/null
+++ b/examples/hackernews/src/components/Toggle.astro
@@ -0,0 +1,78 @@
+---
+interface Props {
+ open?: boolean;
+}
+
+const { open = false } = Astro.props;
+---
+
+<hn-toggle open={open ? '' : undefined}>
+ <div class="toggle">
+ <a>{open ? '[-]' : '[+] comments collapsed'}</a>
+ </div>
+ <ul class="comment-children">
+ <slot />
+ </ul>
+</hn-toggle>
+
+<style>
+ hn-toggle[open] > .toggle {
+ padding: 0;
+ background-color: transparent;
+ margin-bottom: -0.5em;
+ }
+
+ hn-toggle:not([open]) > .toggle {
+ background-color: rgb(255 247 237);
+ }
+ hn-toggle:not([open]) ul {
+ display: none;
+ }
+
+ .toggle {
+ font-size: 0.9em;
+ margin: 1em 0;
+ padding: 0.3em 0.5em;
+ border-radius: 4px;
+ }
+
+ a {
+ color: rgb(51 65 85);
+ cursor: pointer;
+ }
+</style>
+
+<script>
+ class HnToggle extends HTMLElement {
+ #btn = this.querySelector<HTMLAnchorElement>('a')!;
+ #toggleOpen = this.toggleOpen.bind(this);
+
+ connectedCallback() {
+ this.#btn.addEventListener('click', this.#toggleOpen, false);
+ }
+
+ disconnectedCallback() {
+ this.#btn.addEventListener('click', this.#toggleOpen);
+ }
+
+ get open() {
+ return this.hasAttribute('open');
+ }
+
+ set open(value: boolean) {
+ if (value) {
+ this.setAttribute('open', '');
+ this.#btn.textContent = '[-]';
+ } else {
+ this.removeAttribute('open');
+ this.#btn.textContent = '[+] comments collapsed';
+ }
+ }
+
+ toggleOpen() {
+ this.open = !this.open;
+ }
+ }
+
+ customElements.define('hn-toggle', HnToggle);
+</script>
diff --git a/examples/hackernews/src/layouts/Layout.astro b/examples/hackernews/src/layouts/Layout.astro
new file mode 100644
index 000000000..47bc1ab1a
--- /dev/null
+++ b/examples/hackernews/src/layouts/Layout.astro
@@ -0,0 +1,36 @@
+---
+import Nav from '../components/Nav.astro';
+---
+
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <meta name="generator" content={Astro.generator} />
+ <title>Astro - Hacker News</title>
+ <meta name="description" content="Hacker News Clone built with Astro" />
+ </head>
+ <body>
+ <Nav />
+ <slot />
+ <style is:global>
+ body {
+ font-family:
+ -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
+ 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
+ font-size: 15px;
+ background-color: rgb(226 232 240);
+ margin: 0;
+ padding-top: 55px;
+ color: rgb(15 23 42);
+ overflow-y: scroll;
+ }
+
+ a {
+ color: rgb(15 23 42);
+ text-decoration: none;
+ }
+ </style>
+ </body>
+</html>
diff --git a/examples/hackernews/src/lib/api.ts b/examples/hackernews/src/lib/api.ts
new file mode 100644
index 000000000..49fd0c333
--- /dev/null
+++ b/examples/hackernews/src/lib/api.ts
@@ -0,0 +1,24 @@
+const story = (path: string) => `https://node-hnapi.herokuapp.com/${path}`;
+const user = (path: string) => `https://hacker-news.firebaseio.com/v0/${path}.json`;
+
+export default async function fetchAPI(path: string) {
+ const url = path.startsWith('user') ? user(path) : story(path);
+ const headers = { 'User-Agent': 'chrome' };
+
+ try {
+ let response = await fetch(url, { headers });
+ let text = await response.text();
+ try {
+ if (text === null) {
+ return { error: 'Not found' };
+ }
+ return JSON.parse(text);
+ } catch (e) {
+ console.error(`Received from API: ${text}`);
+ console.error(e);
+ return { error: e };
+ }
+ } catch (error) {
+ return { error };
+ }
+}
diff --git a/examples/hackernews/src/pages/[...stories].astro b/examples/hackernews/src/pages/[...stories].astro
new file mode 100644
index 000000000..fa227e0c1
--- /dev/null
+++ b/examples/hackernews/src/pages/[...stories].astro
@@ -0,0 +1,105 @@
+---
+import For from '../components/For.astro';
+import Show from '../components/Show.astro';
+import Story from '../components/Story.astro';
+import Layout from '../layouts/Layout.astro';
+import fetchAPI from '../lib/api';
+import type { IStory } from '../types.js';
+
+const mapStories = {
+ top: 'news',
+ new: 'newest',
+ show: 'show',
+ ask: 'ask',
+ job: 'jobs',
+};
+
+function safeParseInt(value: any, fallback: number) {
+ try {
+ return parseInt(value) || fallback;
+ } catch {
+ return fallback;
+ }
+}
+
+const page = safeParseInt(Astro.url.searchParams.get('page'), 1);
+const type =
+ Astro.params.stories && Astro.params.stories in mapStories
+ ? (Astro.params.stories.toString() as keyof typeof mapStories)
+ : 'top';
+
+const stories = (await fetchAPI(`${mapStories[type]}?page=${page}`)) as IStory[];
+---
+
+<Layout>
+ <section>
+ <nav aria-labelledby="current-page">
+ <Show when={page > 1}>
+ <a href={`/${type}?page=${page - 1}`} aria-label="Previous Page"> &lt; prev</a>
+ <span slot="fallback" aria-disabled="true"> &lt; prev</span>
+ </Show>
+ <span id="current-page">page {page}</span>
+ <Show when={stories?.length >= 29}>
+ <a href={`/${type}?page=${page + 1}`} aria-label="Next Page">more &gt;</a>
+ <span slot="fallback" aria-disabled="true"> more &gt;</span>
+ </Show>
+ </nav>
+ <main>
+ <Show when={stories}>
+ <ul>
+ <For each={stories}>{(story: IStory) => <Story story={story} />}</For>
+ </ul>
+ </Show>
+ </main>
+ </section>
+</Layout>
+
+<style>
+ section {
+ padding-top: 45px;
+ }
+
+ nav,
+ main {
+ background-color: rgb(248 250 252);
+ border-radius: 2px;
+ }
+
+ nav {
+ padding: 15px 30px;
+ position: fixed;
+ text-align: center;
+ top: 55px;
+ left: 0;
+ right: 0;
+ z-index: 998;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
+ }
+
+ nav a {
+ margin: 0 1em;
+ }
+
+ [aria-disabled='true'] {
+ color: rgb(71 85 105);
+ margin: 0 1em;
+ }
+
+ main {
+ position: absolute;
+ margin: 30px 0;
+ width: 100%;
+ }
+
+ ul {
+ list-style-type: none;
+ padding: 0;
+ margin: 0;
+ }
+
+ @media (max-width: 600px) {
+ main {
+ margin: 10px 0;
+ }
+ }
+</style>
diff --git a/examples/hackernews/src/pages/stories/[id].astro b/examples/hackernews/src/pages/stories/[id].astro
new file mode 100644
index 000000000..84383aa9e
--- /dev/null
+++ b/examples/hackernews/src/pages/stories/[id].astro
@@ -0,0 +1,96 @@
+---
+import Comment from '../../components/Comment.astro';
+import For from '../../components/For.astro';
+import Show from '../../components/Show.astro';
+import Layout from '../../layouts/Layout.astro';
+import fetchAPI from '../../lib/api';
+import type { IComment, IStory } from '../../types.js';
+
+const { id } = Astro.params as { id: string };
+
+const story = (await fetchAPI(`item/${id}`)) as IStory;
+---
+
+<Layout>
+ <div>
+ <header>
+ <a href={story.url} target="_blank">
+ <h1>{story.title}</h1>
+ </a>
+ <Show when={story.domain}>
+ <span class="host">({story.domain})</span>
+ </Show>
+ <p class="meta">
+ {story.points} points | by
+ <a href={`/users/${story.user}`}>
+ {story.user}
+ </a>
+ &nbsp;{story.time_ago}
+ </p>
+ </header>
+ <main>
+ <p>
+ {story.comments_count ? story.comments_count + ' comments' : 'No comments yet.'}
+ </p>
+ <ul class="comment-children">
+ <For each={story.comments}>
+ {(comment: IComment) => <Comment comment={comment} />}
+ </For>
+ </ul>
+ </main>
+ </div>
+</Layout>
+
+<style>
+ header {
+ background-color: rgb(248 250 252);
+ padding: 1.8em 2em 1em;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
+ }
+
+ h1 {
+ display: inline;
+ font-size: 1.5em;
+ margin: 0;
+ margin-right: 0.5em;
+ }
+
+ .host,
+ .meta,
+ .host a {
+ color: rgb(51 65 85);
+ }
+
+ .meta a {
+ text-decoration: underline;
+ }
+
+ main {
+ background-color: rgb(248 250 252);
+ margin-top: 10px;
+ padding: 0 2em 0.5em;
+ }
+
+ main p {
+ margin: 0;
+ font-size: 1.1em;
+ padding: 1em 0;
+ position: relative;
+ }
+
+ main :global(ul) {
+ list-style-type: none;
+ padding: 0;
+ margin: 0;
+ }
+
+ @media (max-width: 600px) {
+ h1 {
+ font-size: 1.25em;
+ }
+ }
+
+ ul :global(ul) {
+ margin-left: 1.5em;
+ }
+</style>
diff --git a/examples/hackernews/src/pages/users/[id].astro b/examples/hackernews/src/pages/users/[id].astro
new file mode 100644
index 000000000..e56085992
--- /dev/null
+++ b/examples/hackernews/src/pages/users/[id].astro
@@ -0,0 +1,69 @@
+---
+import Show from '../../components/Show.astro';
+import Layout from '../../layouts/Layout.astro';
+import fetchAPI from '../../lib/api';
+import type { IUser } from '../../types.js';
+
+const { id } = Astro.params as { id: string };
+
+const user = (await fetchAPI(`user/${id}`)) as IUser;
+---
+
+<Layout>
+ <main>
+ <Show when={user}>
+ <Show when={!user.error}>
+ <h1 slot="fallback">User not found.</h1>
+ <h1>User : {user.id}</h1>
+ <ul class="meta">
+ <li>
+ <span class="label">Created:</span>
+ {user.created}
+ </li>
+ <li>
+ <span class="label">Karma:</span>
+ {user.karma}
+ </li>
+ <Show when={user.about}>
+ <li set:html={user.about} class="about" />{' '}
+ </Show>
+ </ul>
+ <p>
+ <a href={`https://news.ycombinator.com/submitted?id=${user.id}`}>submissions</a> |{' '}
+ <a href={`https://news.ycombinator.com/threads?id=${user.id}`}>comments</a>
+ </p>
+ </Show>
+ </Show>
+ </main>
+</Layout>
+
+<style>
+ main {
+ background-color: rgb(248 250 252);
+ box-sizing: border-box;
+ padding: 2em 3em;
+ }
+
+ h1 {
+ margin: 0;
+ font-size: 1.5em;
+ }
+
+ .meta {
+ list-style-type: none;
+ padding: 0;
+ }
+
+ .label {
+ display: inline-block;
+ min-width: 4em;
+ }
+
+ .about {
+ margin: 1em 0;
+ }
+
+ p a {
+ text-decoration: underline;
+ }
+</style>
diff --git a/examples/hackernews/src/types.ts b/examples/hackernews/src/types.ts
new file mode 100644
index 000000000..e27ee85e4
--- /dev/null
+++ b/examples/hackernews/src/types.ts
@@ -0,0 +1,27 @@
+export interface IComment {
+ user: string;
+ time_ago: string;
+ content: string;
+ comments: IComment[];
+}
+
+export interface IStory {
+ id: string;
+ points: string;
+ url: string;
+ title: string;
+ domain: string;
+ type: string;
+ time_ago: string;
+ user: string;
+ comments_count: number;
+ comments: IComment[];
+}
+
+export interface IUser {
+ error: string;
+ id: string;
+ created: string;
+ karma: number;
+ about: string;
+}
diff --git a/examples/hackernews/tsconfig.json b/examples/hackernews/tsconfig.json
new file mode 100644
index 000000000..8bf91d3bb
--- /dev/null
+++ b/examples/hackernews/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "extends": "astro/tsconfigs/strict",
+ "include": [".astro/types.d.ts", "**/*"],
+ "exclude": ["dist"]
+}
diff --git a/examples/integration/.gitignore b/examples/integration/.gitignore
new file mode 100644
index 000000000..16d54bb13
--- /dev/null
+++ b/examples/integration/.gitignore
@@ -0,0 +1,24 @@
+# build output
+dist/
+# generated types
+.astro/
+
+# dependencies
+node_modules/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
+
+# jetbrains setting folder
+.idea/
diff --git a/examples/integration/README.md b/examples/integration/README.md
new file mode 100644
index 000000000..4f0880031
--- /dev/null
+++ b/examples/integration/README.md
@@ -0,0 +1,33 @@
+# Astro Starter Kit: Integration Package
+
+This is a template for an Astro integration. Use this template for writing integrations to use in multiple projects or publish to NPM.
+
+```sh
+npm create astro@latest -- --template integration
+```
+
+[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/integration)
+[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/integration)
+[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/integration/devcontainer.json)
+
+## 🚀 Project Structure
+
+Inside of your Astro project, you'll see the following folders and files:
+
+```text
+/
+├── index.ts
+├── tsconfig.json
+├── package.json
+```
+
+The `index.ts` file is the "entry point" for your integration. Export your integration in `index.ts` to make them importable from your package.
+
+## 🧞 Commands
+
+All commands are run from the root of the project, from a terminal:
+
+| Command | Action |
+| :------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `npm link` | Registers this package locally. Run `npm link my-integration` in an Astro project to install your integration |
+| `npm publish` | [Publishes](https://docs.npmjs.com/creating-and-publishing-unscoped-public-packages#publishing-unscoped-public-packages) this package to NPM. Requires you to be [logged in](https://docs.npmjs.com/cli/v8/commands/npm-adduser) |
diff --git a/examples/integration/index.ts b/examples/integration/index.ts
new file mode 100644
index 000000000..1fafd4e15
--- /dev/null
+++ b/examples/integration/index.ts
@@ -0,0 +1,23 @@
+import type { AstroIntegration } from 'astro';
+
+export default function createIntegration(): AstroIntegration {
+ // See the Integration API docs for full details
+ // https://docs.astro.build/en/reference/integrations-reference/
+ return {
+ name: '@example/my-integration',
+ hooks: {
+ 'astro:config:setup': () => {
+ // See the @astrojs/react integration for an example
+ // https://github.com/withastro/astro/blob/main/packages/integrations/react/src/index.ts
+ },
+ 'astro:build:setup': () => {
+ // See the @astrojs/react integration for an example
+ // https://github.com/withastro/astro/blob/main/packages/integrations/react/src/index.ts
+ },
+ 'astro:build:done': () => {
+ // See the @astrojs/partytown integration for an example
+ // https://github.com/withastro/astro/blob/main/packages/integrations/partytown/src/index.ts
+ },
+ },
+ };
+}
diff --git a/examples/integration/package.json b/examples/integration/package.json
new file mode 100644
index 000000000..e6a3866fb
--- /dev/null
+++ b/examples/integration/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "@example/integration",
+ "private": true,
+ "version": "0.0.1",
+ "type": "module",
+ "exports": {
+ ".": "./index.ts"
+ },
+ "files": [
+ "src",
+ "index.ts"
+ ],
+ "keywords": [
+ "withastro"
+ ],
+ "scripts": {},
+ "devDependencies": {
+ "astro": "^5.9.0"
+ },
+ "peerDependencies": {
+ "astro": "^4.0.0"
+ }
+}
diff --git a/examples/integration/tsconfig.json b/examples/integration/tsconfig.json
new file mode 100644
index 000000000..bcbf8b509
--- /dev/null
+++ b/examples/integration/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "astro/tsconfigs/strict"
+}
diff --git a/examples/minimal/.codesandbox/Dockerfile b/examples/minimal/.codesandbox/Dockerfile
new file mode 100644
index 000000000..c3b5c81a1
--- /dev/null
+++ b/examples/minimal/.codesandbox/Dockerfile
@@ -0,0 +1 @@
+FROM node:18-bullseye
diff --git a/examples/minimal/.gitignore b/examples/minimal/.gitignore
new file mode 100644
index 000000000..16d54bb13
--- /dev/null
+++ b/examples/minimal/.gitignore
@@ -0,0 +1,24 @@
+# build output
+dist/
+# generated types
+.astro/
+
+# dependencies
+node_modules/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
+
+# jetbrains setting folder
+.idea/
diff --git a/examples/minimal/.vscode/extensions.json b/examples/minimal/.vscode/extensions.json
new file mode 100644
index 000000000..22a15055d
--- /dev/null
+++ b/examples/minimal/.vscode/extensions.json
@@ -0,0 +1,4 @@
+{
+ "recommendations": ["astro-build.astro-vscode"],
+ "unwantedRecommendations": []
+}
diff --git a/examples/minimal/.vscode/launch.json b/examples/minimal/.vscode/launch.json
new file mode 100644
index 000000000..d64220976
--- /dev/null
+++ b/examples/minimal/.vscode/launch.json
@@ -0,0 +1,11 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "command": "./node_modules/.bin/astro dev",
+ "name": "Development server",
+ "request": "launch",
+ "type": "node-terminal"
+ }
+ ]
+}
diff --git a/examples/minimal/README.md b/examples/minimal/README.md
new file mode 100644
index 000000000..e34a99b44
--- /dev/null
+++ b/examples/minimal/README.md
@@ -0,0 +1,47 @@
+# Astro Starter Kit: Minimal
+
+```sh
+npm create astro@latest -- --template minimal
+```
+
+[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/minimal)
+[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/minimal)
+[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/minimal/devcontainer.json)
+
+> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
+
+## 🚀 Project Structure
+
+Inside of your Astro project, you'll see the following folders and files:
+
+```text
+/
+├── public/
+├── src/
+│ └── pages/
+│ └── index.astro
+└── package.json
+```
+
+Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
+
+There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
+
+Any static assets, like images, can be placed in the `public/` directory.
+
+## 🧞 Commands
+
+All commands are run from the root of the project, from a terminal:
+
+| Command | Action |
+| :------------------------ | :----------------------------------------------- |
+| `npm install` | Installs dependencies |
+| `npm run dev` | Starts local dev server at `localhost:4321` |
+| `npm run build` | Build your production site to `./dist/` |
+| `npm run preview` | Preview your build locally, before deploying |
+| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
+| `npm run astro -- --help` | Get help using the Astro CLI |
+
+## 👀 Want to learn more?
+
+Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
diff --git a/examples/minimal/astro.config.mjs b/examples/minimal/astro.config.mjs
new file mode 100644
index 000000000..e762ba5cf
--- /dev/null
+++ b/examples/minimal/astro.config.mjs
@@ -0,0 +1,5 @@
+// @ts-check
+import { defineConfig } from 'astro/config';
+
+// https://astro.build/config
+export default defineConfig({});
diff --git a/examples/minimal/package.json b/examples/minimal/package.json
new file mode 100644
index 000000000..eaf6c2f11
--- /dev/null
+++ b/examples/minimal/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "@example/minimal",
+ "type": "module",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "dev": "astro dev",
+ "build": "astro build",
+ "preview": "astro preview",
+ "astro": "astro"
+ },
+ "dependencies": {
+ "astro": "^5.9.0"
+ }
+}
diff --git a/examples/minimal/public/favicon.svg b/examples/minimal/public/favicon.svg
new file mode 100644
index 000000000..f157bd1c5
--- /dev/null
+++ b/examples/minimal/public/favicon.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
+ <path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
+ <style>
+ path { fill: #000; }
+ @media (prefers-color-scheme: dark) {
+ path { fill: #FFF; }
+ }
+ </style>
+</svg>
diff --git a/examples/minimal/src/pages/index.astro b/examples/minimal/src/pages/index.astro
new file mode 100644
index 000000000..2d1410736
--- /dev/null
+++ b/examples/minimal/src/pages/index.astro
@@ -0,0 +1,16 @@
+---
+
+---
+
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+ <meta name="viewport" content="width=device-width" />
+ <meta name="generator" content={Astro.generator} />
+ <title>Astro</title>
+ </head>
+ <body>
+ <h1>Astro</h1>
+ </body>
+</html>
diff --git a/examples/minimal/tsconfig.json b/examples/minimal/tsconfig.json
new file mode 100644
index 000000000..8bf91d3bb
--- /dev/null
+++ b/examples/minimal/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "extends": "astro/tsconfigs/strict",
+ "include": [".astro/types.d.ts", "**/*"],
+ "exclude": ["dist"]
+}
diff --git a/examples/portfolio/.codesandbox/Dockerfile b/examples/portfolio/.codesandbox/Dockerfile
new file mode 100644
index 000000000..c3b5c81a1
--- /dev/null
+++ b/examples/portfolio/.codesandbox/Dockerfile
@@ -0,0 +1 @@
+FROM node:18-bullseye
diff --git a/examples/portfolio/.gitignore b/examples/portfolio/.gitignore
new file mode 100644
index 000000000..16d54bb13
--- /dev/null
+++ b/examples/portfolio/.gitignore
@@ -0,0 +1,24 @@
+# build output
+dist/
+# generated types
+.astro/
+
+# dependencies
+node_modules/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
+
+# jetbrains setting folder
+.idea/
diff --git a/examples/portfolio/.vscode/extensions.json b/examples/portfolio/.vscode/extensions.json
new file mode 100644
index 000000000..22a15055d
--- /dev/null
+++ b/examples/portfolio/.vscode/extensions.json
@@ -0,0 +1,4 @@
+{
+ "recommendations": ["astro-build.astro-vscode"],
+ "unwantedRecommendations": []
+}
diff --git a/examples/portfolio/.vscode/launch.json b/examples/portfolio/.vscode/launch.json
new file mode 100644
index 000000000..d64220976
--- /dev/null
+++ b/examples/portfolio/.vscode/launch.json
@@ -0,0 +1,11 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "command": "./node_modules/.bin/astro dev",
+ "name": "Development server",
+ "request": "launch",
+ "type": "node-terminal"
+ }
+ ]
+}
diff --git a/examples/portfolio/README.md b/examples/portfolio/README.md
new file mode 100644
index 000000000..312d18b1c
--- /dev/null
+++ b/examples/portfolio/README.md
@@ -0,0 +1,30 @@
+# Astro Starter Kit: Portfolio
+
+```sh
+npm create astro@latest -- --template portfolio
+```
+
+[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/portfolio)
+[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/portfolio)
+[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/portfolio/devcontainer.json)
+
+> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
+
+![portfolio](https://user-images.githubusercontent.com/357379/210779178-a98f0fb7-6b1a-4068-894c-8e1403e26654.jpg)
+
+## 🧞 Commands
+
+All commands are run from the root of the project, from a terminal:
+
+| Command | Action |
+| :------------------------ | :----------------------------------------------- |
+| `npm install` | Installs dependencies |
+| `npm run dev` | Starts local dev server at `localhost:4321` |
+| `npm run build` | Build your production site to `./dist/` |
+| `npm run preview` | Preview your build locally, before deploying |
+| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
+| `npm run astro -- --help` | Get help using the Astro CLI |
+
+## 👀 Want to learn more?
+
+Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
diff --git a/examples/portfolio/astro.config.mjs b/examples/portfolio/astro.config.mjs
new file mode 100644
index 000000000..e762ba5cf
--- /dev/null
+++ b/examples/portfolio/astro.config.mjs
@@ -0,0 +1,5 @@
+// @ts-check
+import { defineConfig } from 'astro/config';
+
+// https://astro.build/config
+export default defineConfig({});
diff --git a/examples/portfolio/package.json b/examples/portfolio/package.json
new file mode 100644
index 000000000..926d2cc16
--- /dev/null
+++ b/examples/portfolio/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "@example/portfolio",
+ "type": "module",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "dev": "astro dev",
+ "build": "astro build",
+ "preview": "astro preview",
+ "astro": "astro"
+ },
+ "dependencies": {
+ "astro": "^5.9.0"
+ }
+}
diff --git a/examples/portfolio/public/assets/at-work.jpg b/examples/portfolio/public/assets/at-work.jpg
new file mode 100644
index 000000000..82c29fe56
--- /dev/null
+++ b/examples/portfolio/public/assets/at-work.jpg
Binary files differ
diff --git a/examples/portfolio/public/assets/backgrounds/bg-footer-dark-1440w.jpg b/examples/portfolio/public/assets/backgrounds/bg-footer-dark-1440w.jpg
new file mode 100644
index 000000000..659ad7501
--- /dev/null
+++ b/examples/portfolio/public/assets/backgrounds/bg-footer-dark-1440w.jpg
Binary files differ
diff --git a/examples/portfolio/public/assets/backgrounds/bg-footer-dark-800w.jpg b/examples/portfolio/public/assets/backgrounds/bg-footer-dark-800w.jpg
new file mode 100644
index 000000000..2ec737c75
--- /dev/null
+++ b/examples/portfolio/public/assets/backgrounds/bg-footer-dark-800w.jpg
Binary files differ
diff --git a/examples/portfolio/public/assets/backgrounds/bg-footer-light-1440w.jpg b/examples/portfolio/public/assets/backgrounds/bg-footer-light-1440w.jpg
new file mode 100644
index 000000000..9a5af1a51
--- /dev/null
+++ b/examples/portfolio/public/assets/backgrounds/bg-footer-light-1440w.jpg
Binary files differ
diff --git a/examples/portfolio/public/assets/backgrounds/bg-footer-light-800w.jpg b/examples/portfolio/public/assets/backgrounds/bg-footer-light-800w.jpg
new file mode 100644
index 000000000..2652dff17
--- /dev/null
+++ b/examples/portfolio/public/assets/backgrounds/bg-footer-light-800w.jpg
Binary files differ
diff --git a/examples/portfolio/public/assets/backgrounds/bg-main-dark-1440w.jpg b/examples/portfolio/public/assets/backgrounds/bg-main-dark-1440w.jpg
new file mode 100644
index 000000000..704388734
--- /dev/null
+++ b/examples/portfolio/public/assets/backgrounds/bg-main-dark-1440w.jpg
Binary files differ
diff --git a/examples/portfolio/public/assets/backgrounds/bg-main-dark-800w.jpg b/examples/portfolio/public/assets/backgrounds/bg-main-dark-800w.jpg
new file mode 100644
index 000000000..ccffe3c6f
--- /dev/null
+++ b/examples/portfolio/public/assets/backgrounds/bg-main-dark-800w.jpg
Binary files differ
diff --git a/examples/portfolio/public/assets/backgrounds/bg-main-dark.svg b/examples/portfolio/public/assets/backgrounds/bg-main-dark.svg
new file mode 100644
index 000000000..7c3d656f2
--- /dev/null
+++ b/examples/portfolio/public/assets/backgrounds/bg-main-dark.svg
@@ -0,0 +1 @@
+<svg height="640" width="1440" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a"><stop offset=".58" stop-opacity="0"/><stop offset="1"/></linearGradient><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="793.5" x2="759.5" xlink:href="#a" y1="261.5" y2="149.5"/><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="644.19" x2="645.54" xlink:href="#a" y1="398.02" y2="267.7"/><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="547" x2="522.36" xlink:href="#a" y1="457.27" y2="342.85"/><g clip-rule="evenodd" fill-rule="evenodd" opacity=".15"><path d="m439.57 249.55a2149.47 2149.47 0 0 1 1193.87-182.45l-12.48 93.17a2055.46 2055.46 0 0 0 -1141.66 174.47l-454.24 211.86-39.73-85.2z" fill="url(#b)"/><path d="m272.3 266.93a2393.36 2393.36 0 0 1 1328.96 205.6l-44.42 94.78a2288.7 2288.7 0 0 0 -1270.84-196.61l-553.29 73.05-13.7-103.77z" fill="url(#c)" opacity=".56"/><path d="m195.26 416.13a2149.46 2149.46 0 0 1 1204.86-83.21l-20.13 91.82a2055.46 2055.46 0 0 0 -1152.17 79.56l-470.18 173.62-32.56-88.18 470.18-173.62z" fill="url(#d)"/></g><path d="m-258.15 719.56 1743.12-517.56 182.93 616.12-1743.1 517.56z" fill="#090b11"/></svg> \ No newline at end of file
diff --git a/examples/portfolio/public/assets/backgrounds/bg-main-light-1440w.jpg b/examples/portfolio/public/assets/backgrounds/bg-main-light-1440w.jpg
new file mode 100644
index 000000000..915e7a6c9
--- /dev/null
+++ b/examples/portfolio/public/assets/backgrounds/bg-main-light-1440w.jpg
Binary files differ
diff --git a/examples/portfolio/public/assets/backgrounds/bg-main-light-800w.jpg b/examples/portfolio/public/assets/backgrounds/bg-main-light-800w.jpg
new file mode 100644
index 000000000..185eaadfa
--- /dev/null
+++ b/examples/portfolio/public/assets/backgrounds/bg-main-light-800w.jpg
Binary files differ
diff --git a/examples/portfolio/public/assets/backgrounds/bg-main-light.svg b/examples/portfolio/public/assets/backgrounds/bg-main-light.svg
new file mode 100644
index 000000000..19410d6ad
--- /dev/null
+++ b/examples/portfolio/public/assets/backgrounds/bg-main-light.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1440" height="640"><g opacity=".15"><path fill="url(#a)" d="M439.57 249.55A2149.47 2149.47 0 0 1 1633.44 67.1l-12.48 93.17A2055.46 2055.46 0 0 0 479.3 334.74L25.06 546.6l-39.73-85.2z"/><path fill="url(#b)" d="M272.3 265.93a2393.36 2393.36 0 0 1 1328.96 205.6l-44.42 94.78A2288.7 2288.7 0 0 0 286 369.7l-553.29 73.05-13.7-103.77z" opacity=".56"/><path fill="url(#c)" d="M195.26 416.13a2149.47 2149.47 0 0 1 1204.86-83.21l-20.13 91.82A2055.46 2055.46 0 0 0 227.82 504.3l-470.18 173.62-32.56-88.18 470.18-173.62z"/></g><path fill="#fff" d="M-258 718.56 1485.12 201l182.93 616.12-1743.11 517.56z"/><defs><linearGradient id="d"><stop offset=".58" stop-opacity="0"/><stop offset="1"/></linearGradient><linearGradient xlink:href="#d" id="a" x1="793.5" x2="759.5" y1="261.5" y2="149.5" gradientUnits="userSpaceOnUse"/><linearGradient xlink:href="#d" id="b" x1="644.19" x2="645.54" y1="397.02" y2="266.7" gradientUnits="userSpaceOnUse"/><linearGradient xlink:href="#d" id="c" x1="547" x2="522.36" y1="457.27" y2="342.85" gradientUnits="userSpaceOnUse"/></defs></svg>
diff --git a/examples/portfolio/public/assets/backgrounds/bg-subtle-1-dark-1440w.jpg b/examples/portfolio/public/assets/backgrounds/bg-subtle-1-dark-1440w.jpg
new file mode 100644
index 000000000..f1471ce14
--- /dev/null
+++ b/examples/portfolio/public/assets/backgrounds/bg-subtle-1-dark-1440w.jpg
Binary files differ
diff --git a/examples/portfolio/public/assets/backgrounds/bg-subtle-1-dark-800w.jpg b/examples/portfolio/public/assets/backgrounds/bg-subtle-1-dark-800w.jpg
new file mode 100644
index 000000000..c213ea05f
--- /dev/null
+++ b/examples/portfolio/public/assets/backgrounds/bg-subtle-1-dark-800w.jpg
Binary files differ
diff --git a/examples/portfolio/public/assets/backgrounds/bg-subtle-1-light-1440w.jpg b/examples/portfolio/public/assets/backgrounds/bg-subtle-1-light-1440w.jpg
new file mode 100644
index 000000000..0aaad4af2
--- /dev/null
+++ b/examples/portfolio/public/assets/backgrounds/bg-subtle-1-light-1440w.jpg
Binary files differ
diff --git a/examples/portfolio/public/assets/backgrounds/bg-subtle-1-light-800w.jpg b/examples/portfolio/public/assets/backgrounds/bg-subtle-1-light-800w.jpg
new file mode 100644
index 000000000..d8dab2b5f
--- /dev/null
+++ b/examples/portfolio/public/assets/backgrounds/bg-subtle-1-light-800w.jpg
Binary files differ
diff --git a/examples/portfolio/public/assets/backgrounds/bg-subtle-2-dark-1440w.jpg b/examples/portfolio/public/assets/backgrounds/bg-subtle-2-dark-1440w.jpg
new file mode 100644
index 000000000..52991fa25
--- /dev/null
+++ b/examples/portfolio/public/assets/backgrounds/bg-subtle-2-dark-1440w.jpg
Binary files differ
diff --git a/examples/portfolio/public/assets/backgrounds/bg-subtle-2-dark-800w.jpg b/examples/portfolio/public/assets/backgrounds/bg-subtle-2-dark-800w.jpg
new file mode 100644
index 000000000..3df6bef53
--- /dev/null
+++ b/examples/portfolio/public/assets/backgrounds/bg-subtle-2-dark-800w.jpg
Binary files differ
diff --git a/examples/portfolio/public/assets/backgrounds/bg-subtle-2-light-1440w.jpg b/examples/portfolio/public/assets/backgrounds/bg-subtle-2-light-1440w.jpg
new file mode 100644
index 000000000..3154d1b5d
--- /dev/null
+++ b/examples/portfolio/public/assets/backgrounds/bg-subtle-2-light-1440w.jpg
Binary files differ
diff --git a/examples/portfolio/public/assets/backgrounds/bg-subtle-2-light-800w.jpg b/examples/portfolio/public/assets/backgrounds/bg-subtle-2-light-800w.jpg
new file mode 100644
index 000000000..1fa2d3a47
--- /dev/null
+++ b/examples/portfolio/public/assets/backgrounds/bg-subtle-2-light-800w.jpg
Binary files differ
diff --git a/examples/portfolio/public/assets/backgrounds/noise.png b/examples/portfolio/public/assets/backgrounds/noise.png
new file mode 100644
index 000000000..1e7976c1f
--- /dev/null
+++ b/examples/portfolio/public/assets/backgrounds/noise.png
Binary files differ
diff --git a/examples/portfolio/public/assets/portrait.jpg b/examples/portfolio/public/assets/portrait.jpg
new file mode 100644
index 000000000..f1c8984bd
--- /dev/null
+++ b/examples/portfolio/public/assets/portrait.jpg
Binary files differ
diff --git a/examples/portfolio/public/assets/stock-1.jpg b/examples/portfolio/public/assets/stock-1.jpg
new file mode 100644
index 000000000..c8dec6b96
--- /dev/null
+++ b/examples/portfolio/public/assets/stock-1.jpg
Binary files differ
diff --git a/examples/portfolio/public/assets/stock-2.jpg b/examples/portfolio/public/assets/stock-2.jpg
new file mode 100644
index 000000000..3ad4b7150
--- /dev/null
+++ b/examples/portfolio/public/assets/stock-2.jpg
Binary files differ
diff --git a/examples/portfolio/public/assets/stock-3.jpg b/examples/portfolio/public/assets/stock-3.jpg
new file mode 100644
index 000000000..27068541c
--- /dev/null
+++ b/examples/portfolio/public/assets/stock-3.jpg
Binary files differ
diff --git a/examples/portfolio/public/assets/stock-4.jpg b/examples/portfolio/public/assets/stock-4.jpg
new file mode 100644
index 000000000..6942cc2c6
--- /dev/null
+++ b/examples/portfolio/public/assets/stock-4.jpg
Binary files differ
diff --git a/examples/portfolio/public/favicon.svg b/examples/portfolio/public/favicon.svg
new file mode 100644
index 000000000..f157bd1c5
--- /dev/null
+++ b/examples/portfolio/public/favicon.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
+ <path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
+ <style>
+ path { fill: #000; }
+ @media (prefers-color-scheme: dark) {
+ path { fill: #FFF; }
+ }
+ </style>
+</svg>
diff --git a/examples/portfolio/src/components/CallToAction.astro b/examples/portfolio/src/components/CallToAction.astro
new file mode 100644
index 000000000..a1ca69750
--- /dev/null
+++ b/examples/portfolio/src/components/CallToAction.astro
@@ -0,0 +1,56 @@
+---
+interface Props {
+ href: string;
+}
+
+const { href } = Astro.props;
+---
+
+<a href={href}><slot /></a>
+
+<style>
+ a {
+ position: relative;
+ display: flex;
+ place-content: center;
+ text-align: center;
+ padding: 0.56em 2em;
+ gap: 0.8em;
+ color: var(--accent-text-over);
+ text-decoration: none;
+ line-height: 1.1;
+ border-radius: 999rem;
+ overflow: hidden;
+ background: var(--gradient-accent-orange);
+ box-shadow: var(--shadow-md);
+ white-space: nowrap;
+ }
+
+ @media (min-width: 20em) {
+ a {
+ font-size: var(--text-lg);
+ }
+ }
+
+ /* Overlay for hover effects. */
+ a::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ transition: background-color var(--theme-transition);
+ mix-blend-mode: overlay;
+ }
+
+ a:focus::after,
+ a:hover::after {
+ background-color: hsla(var(--gray-999-basis), 0.3);
+ }
+
+ @media (min-width: 50em) {
+ a {
+ padding: 1.125rem 2.5rem;
+ font-size: var(--text-xl);
+ }
+ }
+</style>
diff --git a/examples/portfolio/src/components/ContactCTA.astro b/examples/portfolio/src/components/ContactCTA.astro
new file mode 100644
index 000000000..6986bd740
--- /dev/null
+++ b/examples/portfolio/src/components/ContactCTA.astro
@@ -0,0 +1,46 @@
+---
+import CallToAction from './CallToAction.astro';
+import Icon from './Icon.astro';
+---
+
+<aside>
+ <h2>Interested in working together?</h2>
+ <CallToAction href="mailto:me@example.com">
+ Send Me a Message
+ <Icon icon="paper-plane-tilt" size="1.2em" />
+ </CallToAction>
+</aside>
+
+<style>
+ aside {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 3rem;
+ border-top: 1px solid var(--gray-800);
+ border-bottom: 1px solid var(--gray-800);
+ padding: 5rem 1.5rem;
+ background-color: var(--gray-999_40);
+ box-shadow: var(--shadow-sm);
+ }
+
+ h2 {
+ font-size: var(--text-xl);
+ text-align: center;
+ max-width: 15ch;
+ }
+
+ @media (min-width: 50em) {
+ aside {
+ padding: 7.5rem;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ }
+
+ h2 {
+ font-size: var(--text-3xl);
+ text-align: left;
+ }
+ }
+</style>
diff --git a/examples/portfolio/src/components/Footer.astro b/examples/portfolio/src/components/Footer.astro
new file mode 100644
index 000000000..9d1878dad
--- /dev/null
+++ b/examples/portfolio/src/components/Footer.astro
@@ -0,0 +1,74 @@
+---
+import Icon from './Icon.astro';
+const currentYear = new Date().getFullYear();
+---
+
+<footer>
+ <div class="group">
+ <p>
+ Designed & Developed in Portland with <a href="https://astro.build/">Astro</a>
+ <Icon icon="rocket-launch" size="1.2em" />
+ </p>
+ <p>&copy; {currentYear} Jeanine White</p>
+ </div>
+ <p class="socials">
+ <a href="https://twitter.com/me"> Twitter</a>
+ <a href="https://github.com/me"> GitHub</a>
+ <a href="https://codepen.io/me"> CodePen</a>
+ </p>
+</footer>
+<style>
+ footer {
+ display: flex;
+ flex-direction: column;
+ gap: 3rem;
+ margin-top: auto;
+ padding: 3rem 2rem 3rem;
+ text-align: center;
+ color: var(--gray-400);
+ font-size: var(--text-sm);
+ }
+
+ footer a {
+ color: var(--gray-400);
+ text-decoration: 1px solid underline transparent;
+ text-underline-offset: 0.25em;
+ transition: text-decoration-color var(--theme-transition);
+ }
+
+ footer a:hover,
+ footer a:focus {
+ text-decoration-color: currentColor;
+ }
+
+ .group {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ }
+
+ .socials {
+ display: flex;
+ justify-content: center;
+ gap: 1rem;
+ flex-wrap: wrap;
+ }
+
+ @media (min-width: 50em) {
+ footer {
+ flex-direction: row;
+ justify-content: space-between;
+ padding: 2.5rem 5rem;
+ }
+
+ .group {
+ flex-direction: row;
+ gap: 1rem;
+ flex-wrap: wrap;
+ }
+
+ .socials {
+ justify-content: flex-end;
+ }
+ }
+</style>
diff --git a/examples/portfolio/src/components/Grid.astro b/examples/portfolio/src/components/Grid.astro
new file mode 100644
index 000000000..24a5ea79d
--- /dev/null
+++ b/examples/portfolio/src/components/Grid.astro
@@ -0,0 +1,65 @@
+---
+interface Props {
+ variant?: 'offset' | 'small';
+}
+
+const { variant } = Astro.props;
+---
+
+<ul class:list={['grid', { offset: variant === 'offset', small: variant === 'small' }]}>
+ <slot />
+</ul>
+
+<style>
+ .grid {
+ display: grid;
+ grid-auto-rows: 1fr;
+ gap: 1rem;
+ list-style: none;
+ padding: 0;
+ }
+
+ .grid.small {
+ grid-template-columns: 1fr 1fr;
+ gap: 1.5rem;
+ }
+
+ /* If last row contains only one item, make it span both columns. */
+ .grid.small > :global(:last-child:nth-child(odd)) {
+ grid-column: 1 / 3;
+ }
+
+ @media (min-width: 50em) {
+ .grid {
+ grid-template-columns: 1fr 1fr;
+ gap: 4rem;
+ }
+
+ .grid.offset {
+ --row-offset: 7.5rem;
+ padding-bottom: var(--row-offset);
+ }
+
+ /* Shift first item in each row vertically to create staggered effect. */
+ .grid.offset > :global(:nth-child(odd)) {
+ transform: translateY(var(--row-offset));
+ }
+
+ /* If last row contains only one item, display it in the second column. */
+ .grid.offset > :global(:last-child:nth-child(odd)) {
+ grid-column: 2 / 3;
+ transform: none;
+ }
+
+ .grid.small {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 2rem;
+ }
+
+ .grid.small > :global(*) {
+ flex-basis: 20rem;
+ }
+ }
+</style>
diff --git a/examples/portfolio/src/components/Hero.astro b/examples/portfolio/src/components/Hero.astro
new file mode 100644
index 000000000..30460420a
--- /dev/null
+++ b/examples/portfolio/src/components/Hero.astro
@@ -0,0 +1,54 @@
+---
+interface Props {
+ title: string;
+ tagline?: string;
+ align?: 'start' | 'center';
+}
+
+const { align = 'center', tagline, title } = Astro.props;
+---
+
+<div class:list={['hero stack gap-4', align]}>
+ <div class="stack gap-2">
+ <h1 class="title">{title}</h1>
+ {tagline && <p class="tagline">{tagline}</p>}
+ </div>
+ <slot />
+</div>
+
+<style>
+ .hero {
+ font-size: var(--text-lg);
+ text-align: center;
+ }
+
+ .title,
+ .tagline {
+ max-width: 37ch;
+ margin-inline: auto;
+ }
+
+ .title {
+ font-size: var(--text-3xl);
+ color: var(--gray-0);
+ }
+
+ @media (min-width: 50em) {
+ .hero {
+ font-size: var(--text-xl);
+ }
+
+ .start {
+ text-align: start;
+ }
+
+ .start .title,
+ .start .tagline {
+ margin-inline: unset;
+ }
+
+ .title {
+ font-size: var(--text-5xl);
+ }
+ }
+</style>
diff --git a/examples/portfolio/src/components/Icon.astro b/examples/portfolio/src/components/Icon.astro
new file mode 100644
index 000000000..92cff492a
--- /dev/null
+++ b/examples/portfolio/src/components/Icon.astro
@@ -0,0 +1,56 @@
+---
+import type { HTMLAttributes } from 'astro/types';
+import { iconPaths } from './IconPaths';
+
+interface Props {
+ icon: keyof typeof iconPaths;
+ color?: string;
+ gradient?: boolean;
+ size?: string;
+}
+
+const { color = 'currentcolor', gradient, icon, size } = Astro.props;
+const iconPath = iconPaths[icon];
+
+const attrs: HTMLAttributes<'svg'> = {};
+if (size) attrs.style = { '--size': size };
+
+const gradientId = 'icon-gradient-' + Math.round(Math.random() * 10e12).toString(36);
+---
+
+<svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="40"
+ height="40"
+ viewBox="0 0 256 256"
+ aria-hidden="true"
+ stroke={gradient ? `url(#${gradientId})` : color}
+ fill={gradient ? `url(#${gradientId})` : color}
+ {...attrs}
+>
+ <g set:html={iconPath} />
+ {
+ gradient && (
+ <linearGradient
+ id={gradientId}
+ x1="23"
+ x2="235"
+ y1="43"
+ y2="202"
+ gradientUnits="userSpaceOnUse"
+ >
+ <stop stop-color="var(--gradient-stop-1)" />
+ <stop offset=".5" stop-color="var(--gradient-stop-2)" />
+ <stop offset="1" stop-color="var(--gradient-stop-3)" />
+ </linearGradient>
+ )
+ }
+</svg>
+
+<style>
+ svg {
+ vertical-align: middle;
+ width: var(--size, 1em);
+ height: var(--size, 1em);
+ }
+</style>
diff --git a/examples/portfolio/src/components/IconPaths.ts b/examples/portfolio/src/components/IconPaths.ts
new file mode 100644
index 000000000..f2e959f62
--- /dev/null
+++ b/examples/portfolio/src/components/IconPaths.ts
@@ -0,0 +1,38 @@
+/**
+ * Icons adapted from https://phosphoricons.com/
+ *
+ * Want to add more?
+ * 1. Find the icon you want on Phosphor Icons.
+ * 2. Click “Copy SVG”.
+ * 3. Paste the SVG code in your editor.
+ * 4. Remove the `<svg>` wrapper so you only have elements like `<path>`, `<circle>`, `<rect>` etc.
+ * 5. Remove any `stroke="#000000"` attributes
+ * 6. Replace any `fill="#000000"` attributes with `stroke="none"`
+ * (or add `stroke="none"` on shapes with no `fill` or `stroke` specified).
+ */
+export const iconPaths = {
+ 'terminal-window': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m80 96 40 32-40 32m56 0h40"/><rect width="192" height="160" x="32" y="48" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16.97" rx="8.5"/>`,
+ trophy: `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M56 56v55.1c0 39.7 31.8 72.6 71.5 72.9a72 72 0 0 0 72.5-72V56a8 8 0 0 0-8-8H64a8 8 0 0 0-8 8Zm40 168h64m-32-40v40"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M198.2 128h9.8a32 32 0 0 0 32-32V80a8 8 0 0 0-8-8h-32M58 128H47.9a32 32 0 0 1-32-32V80a8 8 0 0 1 8-8h32"/>`,
+ strategy: `<circle cx="68" cy="188" r="28" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m40 72 40 40m0-40-40 40m136 56 40 40m0-40-40 40M136 80V40h40"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m136 40 16 16c40 40 8 88-24 96"/>`,
+ 'paper-plane-tilt': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M210.3 35.9 23.9 88.4a8 8 0 0 0-1.2 15l85.6 40.5a7.8 7.8 0 0 1 3.8 3.8l40.5 85.6a8 8 0 0 0 15-1.2l52.5-186.4a7.9 7.9 0 0 0-9.8-9.8Zm-99.4 109.2 45.2-45.2"/>`,
+ 'arrow-right': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M40 128h176m-72-72 72 72-72 72"/>`,
+ 'arrow-left': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M216 128H40m72-72-72 72 72 72"/>`,
+ code: `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m64 88-48 40 48 40m128-80 48 40-48 40M160 40 96 216"/>`,
+ 'microphone-stage': `<circle cx="168" cy="88" r="64" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m213.3 133.3-90.6-90.6M100 156l-12 12m16.8-70.1L28.1 202.5a7.9 7.9 0 0 0 .8 10.4l14.2 14.2a7.9 7.9 0 0 0 10.4.8l104.6-76.7"/>`,
+ 'pencil-line': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M96 216H48a8 8 0 0 1-8-8v-44.7a7.9 7.9 0 0 1 2.3-5.6l120-120a8 8 0 0 1 11.4 0l44.6 44.6a8 8 0 0 1 0 11.4Zm40-152 56 56"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M216 216H96l-55.5-55.5M164 92l-96 96"/>`,
+ 'rocket-launch': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M94.1 184.6c-11.4 33.9-56.6 33.9-56.6 33.9s0-45.2 33.9-56.6m124.5-56.5L128 173.3 82.7 128l67.9-67.9C176.3 34.4 202 34.7 213 36.3a7.8 7.8 0 0 1 6.7 6.7c1.6 11 1.9 36.7-23.8 62.4Z"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M184.6 116.7v64.6a8 8 0 0 1-2.4 5.6l-32.3 32.4a8 8 0 0 1-13.5-4.1l-8.4-41.9m11.3-101.9H74.7a8 8 0 0 0-5.6 2.4l-32.4 32.3a8 8 0 0 0 4.1 13.5l41.9 8.4"/>`,
+ list: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M40 128h176M40 64h176M40 192h176"/>`,
+ heart: `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M128 216S28 160 28 92a52 52 0 0 1 100-20h0a52 52 0 0 1 100 20c0 68-100 124-100 124Z"/>`,
+ 'moon-stars': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M216 112V64m24 24h-48m-24-64v32m16-16h-32m65 113A92 92 0 0 1 103 39h0a92 92 0 1 0 114 114Z"/>`,
+ sun: `<circle cx="128" cy="128" r="60" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M128 36V16M63 63 49 49m-13 79H16m47 65-14 14m79 13v20m65-47 14 14m13-79h20m-47-65 14-14"/>`,
+ 'twitter-logo': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M128 88c0-22 18.5-40.3 40.5-40a40 40 0 0 1 36.2 24H240l-32.3 32.3A127.9 127.9 0 0 1 80 224c-32 0-40-12-40-12s32-12 48-36c0 0-64-32-48-120 0 0 40 40 88 48Z"/>`,
+ 'codepen-logo': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m232 101-104 59-104-59 100.1-56.8a8.3 8.3 0 0 1 7.8 0Z"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m232 165-100.1 56.8a8.3 8.3 0 0 1-7.8 0L24 165l104-59Zm0-64v64M24 101v64m104-5v62.8m0-179.6V106"/>`,
+ 'github-logo': `<g stroke-linecap="round" stroke-linejoin="round"><path fill="none" stroke-width="14.7" d="M55.7 167.2c13.9 1 21.3 13.1 22.2 14.6 4.2 7.2 10.4 9.6 18.3 7.1l1.1-3.4a60.3 60.3 0 0 1-25.8-11.9c-12-10.1-18-25.6-18-46.3"/><path fill="none" stroke-width="16" d="M61.4 205.1a24.5 24.5 0 0 1-3-6.1c-3.2-7.9-7.1-10.6-7.8-11.1l-1-.6c-2.4-1.6-9.5-6.5-7.2-13.9 1.4-4.5 6-7.2 12.3-7.2h.8c4 .3 7.6 1.5 10.7 3.2-9.1-10.1-13.6-24.3-13.6-42.3 0-11.3 3.5-21.7 10.1-30.4A46.7 46.7 0 0 1 65 67.3a8.3 8.3 0 0 1 5-4.7c2.8-.9 13.3-2.7 33.2 9.9a105 105 0 0 1 50.5 0c19.9-12.6 30.4-10.8 33.2-9.9 2.3.7 4.1 2.4 5 4.7 5 12.7 4 23.2 2.6 29.4 6.7 8.7 10 18.9 10 30.4 0 42.6-25.8 54.7-43.6 58.7 1.4 4.1 2.2 8.8 2.2 13.7l-.1 23.4v2.3"/><path fill="none" stroke-width="16" d="M160.9 185.7c1.4 4.1 2.2 8.8 2.2 13.7l-.1 23.4v2.3A98.6 98.6 0 1 0 61.4 205c-1.4-2.1-11.3-17.5-11.8-17.8-2.4-1.6-9.5-6.5-7.2-13.9 1.4-4.5 6-7.2 12.3-7.2h.8c4 .3 7.6 1.5 10.7 3.2-9.1-10.1-13.6-24.3-13.6-42.3 0-11.3 3.5-21.7 10.1-30.4A46.4 46.4 0 0 1 65 67.3a8.3 8.3 0 0 1 5-4.7c2.8-.9 13.3-2.7 33.2 9.9a105 105 0 0 1 50.5 0c19.9-12.6 30.4-10.8 33.2-9.9 2.3.7 4.1 2.4 5 4.7 5 12.7 4 23.2 2.6 29.4 6.7 8.7 10 18.9 10 30.4.1 42.6-25.8 54.7-43.6 58.6z"/><path fill="none" stroke-width="18.7" d="m170.1 203.3 17.3-12 17.2-18.7 9.5-26.6v-27.9l-9.5-27.5" /><path fill="none" stroke-width="22.7" d="m92.1 57.3 23.3-4.6 18.7-1.4 29.3 5.4m-110 32.6-8 16-4 21.4.6 20.3 3.4 13" /><path fill="none" stroke-width="13.3" d="M28.8 133a100 100 0 0 0 66.9 94.4v-8.7c-22.4 1.8-33-11.5-35.6-19.8-3.4-8.6-7.8-11.4-8.5-11.8"/></g>`,
+ 'twitch-logo': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M165 200h-42a8 8 0 0 0-5 2l-46 38v-40H48a8 8 0 0 1-8-8V48a8 8 0 0 1 8-8h160a8 8 0 0 1 8 8v108a8 8 0 0 1-3 6l-43 36a8 8 0 0 1-5 2Zm3-112v48m-48-48v48"/>`,
+ 'youtube-logo': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m160 128-48-32v64l48-32z"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M24 128c0 30 3 47 5 56a16 16 0 0 0 10 11c34 13 89 13 89 13s56 0 89-13a16 16 0 0 0 10-11c2-9 5-26 5-56s-3-47-5-56a16 16 0 0 0-10-11c-33-13-89-13-89-13s-55 0-89 13a16 16 0 0 0-10 11c-2 9-5 26-5 56Z"/>`,
+ 'dribbble-logo': `<circle cx="128" cy="128" r="96" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M71 205a160 160 0 0 1 137-77l16 1m-36-76a160 160 0 0 1-124 59 165 165 0 0 1-30-3"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M86 42a161 161 0 0 1 74 177"/>`,
+ 'discord-logo': `<circle stroke="none" cx="96" cy="144" r="12"/><circle stroke="none" cx="160" cy="144" r="12"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M74 80a175 175 0 0 1 54-8 175 175 0 0 1 54 8m0 96a175 175 0 0 1-54 8 175 175 0 0 1-54-8"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m155 182 12 24a8 8 0 0 0 9 4c25-6 46-16 61-30a8 8 0 0 0 3-8L206 59a8 8 0 0 0-5-5 176 176 0 0 0-30-9 8 8 0 0 0-9 5l-8 24m-53 108-12 24a8 8 0 0 1-9 4c-25-6-46-16-61-30a8 8 0 0 1-3-8L50 59a8 8 0 0 1 5-5 176 176 0 0 1 30-9 8 8 0 0 1 9 5l8 24"/>`,
+ 'linkedin-logo': `<rect width="184" height="184" x="36" y="36" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" rx="8"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M120 112v64m-32-64v64m32-36a28 28 0 0 1 56 0v36"/><circle stroke="none" cx="88" cy="80" r="12"/>`,
+ 'instagram-logo': `<circle cx="128" cy="128" r="40" fill="none" stroke-miterlimit="10" stroke-width="16"/><rect width="184" height="184" x="36" y="36" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" rx="48"/><circle cx="180" cy="76" r="12" stroke="none" />`,
+ 'tiktok-logo': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M168 106a96 96 0 0 0 56 18V84a56 56 0 0 1-56-56h-40v128a28 28 0 1 1-40-25V89a68 68 0 1 0 80 67Z"/>`,
+};
diff --git a/examples/portfolio/src/components/MainHead.astro b/examples/portfolio/src/components/MainHead.astro
new file mode 100644
index 000000000..b4c7263ff
--- /dev/null
+++ b/examples/portfolio/src/components/MainHead.astro
@@ -0,0 +1,47 @@
+---
+import '../styles/global.css';
+
+interface Props {
+ title?: string | undefined;
+ description?: string | undefined;
+}
+
+const {
+ title = 'Jeanine White: Personal Site',
+ description = 'The personal site of Jeanine White',
+} = Astro.props;
+---
+
+<meta charset="UTF-8" />
+<meta name="description" property="og:description" content={description} />
+<meta name="viewport" content="width=device-width" />
+<meta name="generator" content={Astro.generator} />
+<title>{title}</title>
+
+<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+<link rel="preconnect" href="https://fonts.googleapis.com" />
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
+<link
+ href="https://fonts.googleapis.com/css2?family=Public+Sans:ital,wght@0,400;0,700;1,400&family=Rubik:wght@500;600&display=swap"
+ rel="stylesheet"
+/>
+<script is:inline>
+ // This code is inlined in the head to make dark mode instant & blocking.
+ const getThemePreference = () => {
+ if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
+ return localStorage.getItem('theme');
+ }
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
+ };
+ const isDark = getThemePreference() === 'dark';
+ document.documentElement.classList[isDark ? 'add' : 'remove']('theme-dark');
+
+ if (typeof localStorage !== 'undefined') {
+ // Watch the document element and persist user preference when it changes.
+ const observer = new MutationObserver(() => {
+ const isDark = document.documentElement.classList.contains('theme-dark');
+ localStorage.setItem('theme', isDark ? 'dark' : 'light');
+ });
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
+ }
+</script>
diff --git a/examples/portfolio/src/components/Nav.astro b/examples/portfolio/src/components/Nav.astro
new file mode 100644
index 000000000..1d1938aec
--- /dev/null
+++ b/examples/portfolio/src/components/Nav.astro
@@ -0,0 +1,354 @@
+---
+import Icon from './Icon.astro';
+import ThemeToggle from './ThemeToggle.astro';
+import type { iconPaths } from './IconPaths';
+
+/** Main menu items */
+const textLinks: { label: string; href: string }[] = [
+ { label: 'Home', href: '/' },
+ { label: 'Work', href: '/work/' },
+ { label: 'About', href: '/about/' },
+];
+
+/** Icon links to social media — edit these with links to your profiles! */
+const iconLinks: { label: string; href: string; icon: keyof typeof iconPaths }[] = [
+ { label: 'Twitter', href: 'https://twitter.com/me', icon: 'twitter-logo' },
+ { label: 'Twitch', href: 'https://twitch.tv/me', icon: 'twitch-logo' },
+ { label: 'GitHub', href: 'https://github.com/me', icon: 'github-logo' },
+ { label: 'CodePen', href: 'https://codepen.io/me', icon: 'codepen-logo' },
+ { label: 'dribbble', href: 'https://dribbble.com/me', icon: 'dribbble-logo' },
+ { label: 'YouTube', href: 'https://www.youtube.com/@me/', icon: 'youtube-logo' },
+];
+
+/** Test if a link is pointing to the current page. */
+const isCurrentPage = (href: string) => {
+ let pathname = Astro.url.pathname.replace(import.meta.env.BASE_URL, '');
+ if (pathname.at(0) !== '/') pathname = '/' + pathname;
+ if (pathname.at(-1) !== '/') pathname += '/';
+ return pathname === href || (href !== '/' && pathname.startsWith(href));
+};
+---
+
+<nav>
+ <div class="menu-header">
+ <a href="/" class="site-title">
+ <Icon icon="terminal-window" color="var(--accent-regular)" size="1.6em" gradient />
+ Jeanine White
+ </a>
+ <menu-button>
+ <template>
+ <button class="menu-button" aria-expanded="false">
+ <span class="sr-only">Menu</span>
+ <Icon icon="list" />
+ </button>
+ </template>
+ </menu-button>
+ </div>
+ <noscript>
+ <ul class="nav-items">
+ {
+ textLinks.map(({ label, href }) => (
+ <li>
+ <a aria-current={isCurrentPage(href) ? 'page' : null} class="link" href={href}>
+ {label}
+ </a>
+ </li>
+ ))
+ }
+ </ul>
+ </noscript>
+ <noscript>
+ <div class="menu-footer">
+ <div class="socials">
+ {
+ iconLinks.map(({ href, icon, label }) => (
+ <a href={href} class="social">
+ <span class="sr-only">{label}</span>
+ <Icon icon={icon} />
+ </a>
+ ))
+ }
+ </div>
+ </div>
+ </noscript>
+ <div id="menu-content" hidden>
+ <ul class="nav-items">
+ {
+ textLinks.map(({ label, href }) => (
+ <li>
+ <a aria-current={isCurrentPage(href) ? 'page' : null} class="link" href={href}>
+ {label}
+ </a>
+ </li>
+ ))
+ }
+ </ul>
+ <div class="menu-footer">
+ <div class="socials">
+ {
+ iconLinks.map(({ href, icon, label }) => (
+ <a href={href} class="social">
+ <span class="sr-only">{label}</span>
+ <Icon icon={icon} />
+ </a>
+ ))
+ }
+ </div>
+
+ <div class="theme-toggle">
+ <ThemeToggle />
+ </div>
+ </div>
+ </div>
+</nav>
+
+<script>
+ class MenuButton extends HTMLElement {
+ constructor() {
+ super();
+
+ // Inject menu toggle button when JS runs.
+ this.appendChild(this.querySelector('template')!.content.cloneNode(true));
+ const btn = this.querySelector('button')!;
+
+ // Hide menu (shown by default to support no-JS browsers).
+ const menu = document.getElementById('menu-content')!;
+ menu.hidden = true;
+ // Add "menu-content" class in JS to avoid covering content in non-JS browsers.
+ menu.classList.add('menu-content');
+
+ /** Set whether the menu is currently expanded or collapsed. */
+ const setExpanded = (expand: boolean) => {
+ btn.setAttribute('aria-expanded', expand ? 'true' : 'false');
+ menu.hidden = !expand;
+ };
+
+ // Toggle menu visibility when the menu button is clicked.
+ btn.addEventListener('click', () => setExpanded(menu.hidden));
+
+ // Hide menu button for large screens.
+ const handleViewports = (e: MediaQueryList | MediaQueryListEvent) => {
+ setExpanded(e.matches);
+ btn.hidden = e.matches;
+ };
+ const mediaQueries = window.matchMedia('(min-width: 50em)');
+ handleViewports(mediaQueries);
+ mediaQueries.addEventListener('change', handleViewports);
+ }
+ }
+ customElements.define('menu-button', MenuButton);
+</script>
+
+<style>
+ nav {
+ z-index: 9999;
+ position: relative;
+ font-family: var(--font-brand);
+ font-weight: 500;
+ margin-bottom: 3.5rem;
+ }
+
+ .menu-header {
+ display: flex;
+ justify-content: space-between;
+ gap: 0.5rem;
+ padding: 1.5rem;
+ }
+
+ .site-title {
+ display: flex;
+ gap: 0.5rem;
+ align-items: center;
+ line-height: 1.1;
+ color: var(--gray-0);
+ text-decoration: none;
+ }
+
+ .menu-button {
+ position: relative;
+ display: flex;
+ border: 0;
+ border-radius: 999rem;
+ padding: 0.5rem;
+ font-size: 1.5rem;
+ color: var(--gray-300);
+ background: radial-gradient(var(--gray-900), var(--gray-800) 150%);
+ box-shadow: var(--shadow-md);
+ }
+
+ .menu-button[aria-expanded='true'] {
+ color: var(--gray-0);
+ background:
+ linear-gradient(180deg, var(--gray-600), transparent),
+ radial-gradient(var(--gray-900), var(--gray-800) 150%);
+ }
+
+ .menu-button[hidden] {
+ display: none;
+ }
+
+ .menu-button::before {
+ position: absolute;
+ inset: -1px;
+ content: '';
+ background: var(--gradient-stroke);
+ border-radius: 999rem;
+ z-index: -1;
+ }
+
+ .menu-content {
+ position: absolute;
+ left: 0;
+ right: 0;
+ }
+
+ .nav-items {
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ font-size: var(--text-md);
+ line-height: 1.2;
+ list-style: none;
+ padding: 2rem;
+ background-color: var(--gray-999);
+ border-bottom: 1px solid var(--gray-800);
+ }
+
+ .link {
+ display: inline-block;
+ color: var(--gray-300);
+ text-decoration: none;
+ }
+
+ .link[aria-current] {
+ color: var(--gray-0);
+ }
+
+ .menu-footer {
+ --icon-size: var(--text-xl);
+ --icon-padding: 0.5rem;
+
+ display: flex;
+ justify-content: space-between;
+ gap: 0.75rem;
+ padding: 1.5rem 2rem 1.5rem 1.5rem;
+ background-color: var(--gray-999);
+ border-radius: 0 0 0.75rem 0.75rem;
+ box-shadow: var(--shadow-lg);
+ }
+
+ .socials {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.625rem;
+ font-size: var(--icon-size);
+ }
+
+ .social {
+ display: flex;
+ padding: var(--icon-padding);
+ text-decoration: none;
+ color: var(--accent-dark);
+ transition: color var(--theme-transition);
+ }
+
+ .social:hover,
+ .social:focus {
+ color: var(--accent-text-over);
+ }
+
+ .theme-toggle {
+ display: flex;
+ align-items: center;
+ height: calc(var(--icon-size) + 2 * var(--icon-padding));
+ }
+
+ @media (min-width: 50em) {
+ nav {
+ display: grid;
+ grid-template-columns: 1fr auto 1fr;
+ align-items: center;
+ padding: 2.5rem 5rem;
+ gap: 1rem;
+ }
+
+ .menu-header {
+ padding: 0;
+ }
+
+ .site-title {
+ font-size: var(--text-lg);
+ }
+
+ .menu-content {
+ display: contents;
+ }
+
+ .nav-items {
+ position: relative;
+ flex-direction: row;
+ font-size: var(--text-sm);
+ border-radius: 999rem;
+ border: 0;
+ padding: 0.5rem 0.5625rem;
+ background: radial-gradient(var(--gray-900), var(--gray-800) 150%);
+ box-shadow: var(--shadow-md);
+ }
+
+ .nav-items::before {
+ position: absolute;
+ inset: -1px;
+ content: '';
+ background: var(--gradient-stroke);
+ border-radius: 999rem;
+ z-index: -1;
+ }
+
+ .link {
+ padding: 0.5rem 1rem;
+ border-radius: 999rem;
+ transition:
+ color var(--theme-transition),
+ background-color var(--theme-transition);
+ }
+
+ .link:hover,
+ .link:focus {
+ color: var(--gray-100);
+ background-color: var(--accent-subtle-overlay);
+ }
+
+ .link[aria-current='page'] {
+ color: var(--accent-text-over);
+ background-color: var(--accent-regular);
+ }
+
+ .menu-footer {
+ --icon-padding: 0.375rem;
+
+ justify-self: flex-end;
+ align-items: center;
+ padding: 0;
+ background-color: transparent;
+ box-shadow: none;
+ }
+
+ .socials {
+ display: none;
+ }
+ }
+
+ @media (min-width: 60em) {
+ .socials {
+ display: flex;
+ justify-content: flex-end;
+ gap: 0;
+ }
+ }
+ @media (forced-colors: active) {
+ .link[aria-current='page'] {
+ color: SelectedItem;
+ }
+ }
+</style>
diff --git a/examples/portfolio/src/components/Pill.astro b/examples/portfolio/src/components/Pill.astro
new file mode 100644
index 000000000..2c410faa0
--- /dev/null
+++ b/examples/portfolio/src/components/Pill.astro
@@ -0,0 +1,16 @@
+<div class="pill"><slot /></div>
+
+<style>
+ .pill {
+ display: flex;
+ padding: 0.5rem 1rem;
+ gap: 0.5rem;
+ color: var(--accent-text-over);
+ border: 1px solid var(--accent-regular);
+ background-color: var(--accent-regular);
+ border-radius: 999rem;
+ font-size: var(--text-md);
+ line-height: 1.35;
+ white-space: nowrap;
+ }
+</style>
diff --git a/examples/portfolio/src/components/PortfolioPreview.astro b/examples/portfolio/src/components/PortfolioPreview.astro
new file mode 100644
index 000000000..f26bae0e2
--- /dev/null
+++ b/examples/portfolio/src/components/PortfolioPreview.astro
@@ -0,0 +1,64 @@
+---
+import type { CollectionEntry } from 'astro:content';
+
+interface Props {
+ project: CollectionEntry<'work'>;
+}
+
+const { data, id } = Astro.props.project;
+---
+
+<a class="card" href={`/work/${id}`}>
+ <span class="title">{data.title}</span>
+ <img src={data.img} alt={data.img_alt || ''} loading="lazy" decoding="async" />
+</a>
+
+<style>
+ .card {
+ display: grid;
+ grid-template: auto 1fr / auto 1fr;
+ height: 11rem;
+ background: var(--gradient-subtle);
+ border: 1px solid var(--gray-800);
+ border-radius: 0.75rem;
+ overflow: hidden;
+ box-shadow: var(--shadow-sm);
+ text-decoration: none;
+ font-family: var(--font-brand);
+ font-size: var(--text-lg);
+ font-weight: 500;
+ transition: box-shadow var(--theme-transition);
+ }
+
+ .card:hover {
+ box-shadow: var(--shadow-md);
+ }
+
+ .title {
+ grid-area: 1 / 1 / 2 / 2;
+ z-index: 1;
+ margin: 0.5rem;
+ padding: 0.5rem 1rem;
+ background: var(--gray-999);
+ color: var(--gray-200);
+ border-radius: 0.375rem;
+ }
+
+ img {
+ grid-area: 1 / 1 / 3 / 3;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ }
+
+ @media (min-width: 50em) {
+ .card {
+ height: 22rem;
+ border-radius: 1.5rem;
+ }
+
+ .title {
+ border-radius: 0.9375rem;
+ }
+ }
+</style>
diff --git a/examples/portfolio/src/components/Skills.astro b/examples/portfolio/src/components/Skills.astro
new file mode 100644
index 000000000..5df5bb0d3
--- /dev/null
+++ b/examples/portfolio/src/components/Skills.astro
@@ -0,0 +1,62 @@
+---
+import Icon from './Icon.astro';
+---
+
+<section class="box skills">
+ <div class="stack gap-2 lg:gap-4">
+ <Icon icon="terminal-window" color="var(--accent-regular)" size="2.5rem" gradient />
+ <h2>Full Stack</h2>
+ <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod.</p>
+ </div>
+ <div class="stack gap-2 lg:gap-4">
+ <Icon icon="trophy" color="var(--accent-regular)" size="2.5rem" gradient />
+ <h2>Industry Leader</h2>
+ <p>Neque viverra justo nec ultrices dui. Est ultricies integer quis auctor elit.</p>
+ </div>
+ <div class="stack gap-2 lg:gap-4">
+ <Icon icon="strategy" color="var(--accent-regular)" size="2.5rem" gradient />
+ <h2>Strategy-Minded</h2>
+ <p>Urna porttitor rhoncus dolor purus non enim praesent ornare.</p>
+ </div>
+</section>
+
+<style>
+ .box {
+ border: 1px solid var(--gray-800);
+ border-radius: 0.75rem;
+ padding: 1.5rem;
+ background-color: var(--gray-999_40);
+ box-shadow: var(--shadow-sm);
+ }
+
+ .skills {
+ display: flex;
+ flex-direction: column;
+ gap: 3rem;
+ }
+
+ .skills h2 {
+ font-size: var(--text-lg);
+ }
+
+ .skills p {
+ color: var(--gray-400);
+ }
+
+ @media (min-width: 50em) {
+ .box {
+ border-radius: 1.5rem;
+ padding: 2.5rem;
+ }
+
+ .skills {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 5rem;
+ }
+
+ .skills h2 {
+ font-size: var(--text-2xl);
+ }
+ }
+</style>
diff --git a/examples/portfolio/src/components/ThemeToggle.astro b/examples/portfolio/src/components/ThemeToggle.astro
new file mode 100644
index 000000000..88f7bf67c
--- /dev/null
+++ b/examples/portfolio/src/components/ThemeToggle.astro
@@ -0,0 +1,95 @@
+---
+import Icon from './Icon.astro';
+---
+
+<theme-toggle>
+ <button>
+ <span class="sr-only">Dark theme</span>
+ <span class="icon light"><Icon icon="sun" /></span>
+ <span class="icon dark"><Icon icon="moon-stars" /></span>
+ </button>
+</theme-toggle>
+
+<style>
+ button {
+ display: flex;
+ border: 0;
+ border-radius: 999rem;
+ padding: 0;
+ background-color: var(--gray-999);
+ box-shadow: inset 0 0 0 1px var(--accent-overlay);
+ cursor: pointer;
+ }
+
+ .icon {
+ z-index: 1;
+ position: relative;
+ display: flex;
+ padding: 0.5rem;
+ width: 2rem;
+ height: 2rem;
+ font-size: 1rem;
+ color: var(--accent-overlay);
+ }
+
+ .icon.light::before {
+ content: '';
+ z-index: -1;
+ position: absolute;
+ inset: 0;
+ background-color: var(--accent-regular);
+ border-radius: 999rem;
+ }
+
+ :global(.theme-dark) .icon.light::before {
+ transform: translateX(100%);
+ }
+
+ :global(.theme-dark) .icon.dark,
+ :global(html:not(.theme-dark)) .icon.light,
+ button[aria-pressed='false'] .icon.light {
+ color: var(--accent-text-over);
+ }
+
+ @media (prefers-reduced-motion: no-preference) {
+ .icon,
+ .icon.light::before {
+ transition:
+ transform var(--theme-transition),
+ color var(--theme-transition);
+ }
+ }
+
+ @media (forced-colors: active) {
+ .icon.light::before {
+ background-color: SelectedItem;
+ }
+ }
+</style>
+
+<script>
+ class ThemeToggle extends HTMLElement {
+ constructor() {
+ super();
+
+ const button = this.querySelector('button')!;
+
+ /** Set the theme to dark/light mode. */
+ const setTheme = (dark: boolean) => {
+ document.documentElement.classList[dark ? 'add' : 'remove']('theme-dark');
+ button.setAttribute('aria-pressed', String(dark));
+ };
+
+ // Toggle the theme when a user clicks the button.
+ button.addEventListener('click', () => setTheme(!this.isDark()));
+
+ // Initialize button state to reflect current theme.
+ setTheme(this.isDark());
+ }
+
+ isDark() {
+ return document.documentElement.classList.contains('theme-dark');
+ }
+ }
+ customElements.define('theme-toggle', ThemeToggle);
+</script>
diff --git a/examples/portfolio/src/content.config.ts b/examples/portfolio/src/content.config.ts
new file mode 100644
index 000000000..689ddde5a
--- /dev/null
+++ b/examples/portfolio/src/content.config.ts
@@ -0,0 +1,17 @@
+import { glob } from 'astro/loaders';
+import { defineCollection, z } from 'astro:content';
+
+export const collections = {
+ work: defineCollection({
+ // Load Markdown files in the src/content/work directory.
+ loader: glob({ base: './src/content/work', pattern: '**/*.md', }),
+ schema: z.object({
+ title: z.string(),
+ description: z.string(),
+ publishDate: z.coerce.date(),
+ tags: z.array(z.string()),
+ img: z.string(),
+ img_alt: z.string().optional(),
+ }),
+ }),
+};
diff --git a/examples/portfolio/src/content/work/bloom-box.md b/examples/portfolio/src/content/work/bloom-box.md
new file mode 100644
index 000000000..d0e5371cf
--- /dev/null
+++ b/examples/portfolio/src/content/work/bloom-box.md
@@ -0,0 +1,23 @@
+---
+title: Bloom Box
+publishDate: 2019-12-01 00:00:00
+img: /assets/stock-2.jpg
+img_alt: A bright pink sheet of paper used to wrap flowers curves in front of rich blue background
+description: |
+ We paired with a cutting-edge music API and a team of horticulturalists
+ to build AI-generated playlists that maximize houseplant health.
+tags:
+ - Dev
+ - Branding
+ - Backend
+---
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur posuere commodo venenatis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam non ligula vel metus efficitur hendrerit. In hac habitasse platea dictumst. Praesent et mauris ut mi dapibus semper. Curabitur tortor justo, efficitur sit amet pretium cursus, porta eget odio. Cras ac venenatis dolor. Donec laoreet posuere malesuada. Curabitur nec mi tempor, placerat leo sit amet, tincidunt est. Quisque pellentesque venenatis magna, eget tristique nibh pulvinar in. Vestibulum vitae volutpat arcu. Aenean ut malesuada odio, sit amet pellentesque odio. Suspendisse nunc elit, blandit nec hendrerit non, aliquet at magna. Donec id leo ut nulla sagittis sodales.
+
+Integer vitae nibh elit. Suspendisse eget urna eu neque bibendum pharetra. Sed interdum lectus sem, in pulvinar magna dignissim vel. Quisque maximus at urna nec laoreet. Suspendisse potenti. Vestibulum rhoncus sem ut mi pellentesque, in vestibulum erat blandit. Aliquam sodales dui ac maximus consectetur. Duis quis est vehicula, imperdiet nisl nec, fermentum erat. Duis tortor diam, pharetra eu euismod in, vehicula non eros. Curabitur facilisis dui at erat ultrices gravida. In at nunc ultricies, pulvinar mi vel, sagittis mauris. Praesent pharetra posuere purus ac imperdiet. Nulla facilisi.
+
+Sed pulvinar porttitor mi in ultricies. Etiam non dolor gravida eros pulvinar pellentesque et dictum ex. Proin eu ornare ligula, sed condimentum dui. Vivamus tincidunt tellus mi, sed semper ipsum pharetra a. Suspendisse sollicitudin at sapien nec volutpat. Etiam justo urna, laoreet ac lacus sed, ultricies facilisis dolor. Integer posuere, metus vel viverra gravida, risus elit ornare magna, id feugiat erat risus ullamcorper libero. Proin vitae diam auctor, laoreet lorem vitae, varius tellus.
+
+Mauris sed eros in ex maximus volutpat. Suspendisse potenti. Donec lacinia justo consectetur sagittis tempor. Proin ullamcorper nisi vitae auctor rhoncus. Sed tristique aliquam augue. Pellentesque vitae fringilla ligula. Nulla arcu elit, efficitur eu nunc malesuada, eleifend tincidunt orci. Interdum et malesuada fames ac ante ipsum primis in faucibus. Integer mattis orci in bibendum ultricies. Quisque a dui erat. Phasellus et vulputate ipsum. Proin metus ex, lobortis nec ornare eget, bibendum ut sapien. Aliquam in dolor lobortis, aliquam tellus a, congue augue. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+
+Aenean pretium purus augue, ut bibendum erat convallis quis. Cras condimentum quis velit ac mollis. Suspendisse non purus fringilla, venenatis nisl porta, finibus odio. Curabitur aliquet metus faucibus libero interdum euismod. Morbi sed magna nisl. Morbi odio nibh, facilisis vel sapien eu, tempus tincidunt erat. Nullam erat velit, sagittis at purus quis, tristique scelerisque tortor. Pellentesque lacinia tortor id est aliquam viverra. Vestibulum et diam ac ipsum mollis fringilla.
diff --git a/examples/portfolio/src/content/work/h20.md b/examples/portfolio/src/content/work/h20.md
new file mode 100644
index 000000000..521bbbe07
--- /dev/null
+++ b/examples/portfolio/src/content/work/h20.md
@@ -0,0 +1,22 @@
+---
+title: h2.0
+publishDate: 2019-10-02 00:00:00
+img: /assets/stock-4.jpg
+img_alt: Soft pink and baby blue water ripples together in a subtle texture.
+description: |
+ We developed brand positioning and design assets for the launch
+ of a new colored water product.
+tags:
+ - Design
+ - Branding
+---
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur posuere commodo venenatis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam non ligula vel metus efficitur hendrerit. In hac habitasse platea dictumst. Praesent et mauris ut mi dapibus semper. Curabitur tortor justo, efficitur sit amet pretium cursus, porta eget odio. Cras ac venenatis dolor. Donec laoreet posuere malesuada. Curabitur nec mi tempor, placerat leo sit amet, tincidunt est. Quisque pellentesque venenatis magna, eget tristique nibh pulvinar in. Vestibulum vitae volutpat arcu. Aenean ut malesuada odio, sit amet pellentesque odio. Suspendisse nunc elit, blandit nec hendrerit non, aliquet at magna. Donec id leo ut nulla sagittis sodales.
+
+Integer vitae nibh elit. Suspendisse eget urna eu neque bibendum pharetra. Sed interdum lectus sem, in pulvinar magna dignissim vel. Quisque maximus at urna nec laoreet. Suspendisse potenti. Vestibulum rhoncus sem ut mi pellentesque, in vestibulum erat blandit. Aliquam sodales dui ac maximus consectetur. Duis quis est vehicula, imperdiet nisl nec, fermentum erat. Duis tortor diam, pharetra eu euismod in, vehicula non eros. Curabitur facilisis dui at erat ultrices gravida. In at nunc ultricies, pulvinar mi vel, sagittis mauris. Praesent pharetra posuere purus ac imperdiet. Nulla facilisi.
+
+Sed pulvinar porttitor mi in ultricies. Etiam non dolor gravida eros pulvinar pellentesque et dictum ex. Proin eu ornare ligula, sed condimentum dui. Vivamus tincidunt tellus mi, sed semper ipsum pharetra a. Suspendisse sollicitudin at sapien nec volutpat. Etiam justo urna, laoreet ac lacus sed, ultricies facilisis dolor. Integer posuere, metus vel viverra gravida, risus elit ornare magna, id feugiat erat risus ullamcorper libero. Proin vitae diam auctor, laoreet lorem vitae, varius tellus.
+
+Mauris sed eros in ex maximus volutpat. Suspendisse potenti. Donec lacinia justo consectetur sagittis tempor. Proin ullamcorper nisi vitae auctor rhoncus. Sed tristique aliquam augue. Pellentesque vitae fringilla ligula. Nulla arcu elit, efficitur eu nunc malesuada, eleifend tincidunt orci. Interdum et malesuada fames ac ante ipsum primis in faucibus. Integer mattis orci in bibendum ultricies. Quisque a dui erat. Phasellus et vulputate ipsum. Proin metus ex, lobortis nec ornare eget, bibendum ut sapien. Aliquam in dolor lobortis, aliquam tellus a, congue augue. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+
+Aenean pretium purus augue, ut bibendum erat convallis quis. Cras condimentum quis velit ac mollis. Suspendisse non purus fringilla, venenatis nisl porta, finibus odio. Curabitur aliquet metus faucibus libero interdum euismod. Morbi sed magna nisl. Morbi odio nibh, facilisis vel sapien eu, tempus tincidunt erat. Nullam erat velit, sagittis at purus quis, tristique scelerisque tortor. Pellentesque lacinia tortor id est aliquam viverra. Vestibulum et diam ac ipsum mollis fringilla.
diff --git a/examples/portfolio/src/content/work/markdown-mystery-tour.md b/examples/portfolio/src/content/work/markdown-mystery-tour.md
new file mode 100644
index 000000000..5fbc2cac5
--- /dev/null
+++ b/examples/portfolio/src/content/work/markdown-mystery-tour.md
@@ -0,0 +1,35 @@
+---
+title: Markdown Mystery Tour
+publishDate: 2020-03-02 00:00:00
+img: /assets/stock-1.jpg
+img_alt: Iridescent ripples of a bright blue and pink liquid
+description: |
+ We designed a whodunnit-style game to introduce Markdown formatting. Suspense — suspicion — syntax!
+tags:
+ - Design
+ - Dev
+ - User Testing
+---
+
+## Level-two heading
+
+> Tell me and I forget. Teach me and I remember. Involve me and I learn.
+
+Lorem ipsum dolor sit amet, <a href="https://astro.build/">Astro</a> makes people happy. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Proin nibh nisl condimentum id venenatis a condimentum vitae. Dapibus ultrices in iaculis nunc. Arcu odio ut sem nulla pharetra diam sit amet. Diam quis enim lobortis scelerisque fermentum dui faucibus in ornare.
+
+Arcu dui vivamus arcu felis bibendum ut tristique et egestas. Eget gravida cum sociis natoque penatibus. Cras fermentum odio eu feugiat pretium nibh. Proin nibh nisl condimentum id venenatis. Porta nibh venenatis cras sed felis eget velit. Id diam vel quam elementum pulvinar etiam non.
+
+### Level-three heading
+
+Ultrices tincidunt arcu non sodales neque sodales ut. Sed enim ut sem viverra aliquet eget sit amet. Lacus luctus accumsan tortor posuere ac ut consequat semper viverra. Viverra accumsan in nisl nisi scelerisque eu ultrices. In massa tempor nec feugiat nisl pretium fusce.
+
+### Level-three heading
+
+Sed pulvinar porttitor mi in ultricies. Etiam non dolor gravida eros pulvinar pellentesque et dictum ex. Proin eu ornare ligula, sed condimentum dui. Vivamus tincidunt tellus mi, sed semper ipsum pharetra a. Suspendisse sollicitudin at sapien nec volutpat. Etiam justo urna, laoreet ac lacus sed, ultricies facilisis dolor. Integer posuere, metus vel viverra gravida, risus elit ornare magna, id feugiat erat risus ullamcorper libero. Proin vitae diam auctor, laoreet lorem vitae, varius tellus.
+
+Aenean pretium purus augue, ut bibendum erat convallis quis. Cras condimentum quis velit ac mollis. Suspendisse non purus fringilla, venenatis nisl porta, finibus odio. Curabitur aliquet metus faucibus libero interdum euismod. Morbi sed magna nisl. Morbi odio nibh, facilisis vel sapien eu, tempus tincidunt erat. Nullam erat velit, sagittis at purus quis, tristique scelerisque tortor. Pellentesque lacinia tortor id est aliquam viverra. Vestibulum et diam ac ipsum mollis fringilla.
+
+#### Level-four heading
+
+- We noted this
+- And also this other point
diff --git a/examples/portfolio/src/content/work/nested/duvet-genius.md b/examples/portfolio/src/content/work/nested/duvet-genius.md
new file mode 100644
index 000000000..e377ede69
--- /dev/null
+++ b/examples/portfolio/src/content/work/nested/duvet-genius.md
@@ -0,0 +1,22 @@
+---
+title: Duvet Genius
+publishDate: 2020-03-04 00:00:00
+img: /assets/stock-3.jpg
+img_alt: Pearls of silky soft white cotton, bubble up under vibrant lighting
+description: |
+ We developed a virtual showcase for the softest bedding imaginable.
+tags:
+ - Design
+ - Dev
+ - Branding
+---
+
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur posuere commodo venenatis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam non ligula vel metus efficitur hendrerit. In hac habitasse platea dictumst. Praesent et mauris ut mi dapibus semper. Curabitur tortor justo, efficitur sit amet pretium cursus, porta eget odio. Cras ac venenatis dolor. Donec laoreet posuere malesuada. Curabitur nec mi tempor, placerat leo sit amet, tincidunt est. Quisque pellentesque venenatis magna, eget tristique nibh pulvinar in. Vestibulum vitae volutpat arcu. Aenean ut malesuada odio, sit amet pellentesque odio. Suspendisse nunc elit, blandit nec hendrerit non, aliquet at magna. Donec id leo ut nulla sagittis sodales.
+
+Integer vitae nibh elit. Suspendisse eget urna eu neque bibendum pharetra. Sed interdum lectus sem, in pulvinar magna dignissim vel. Quisque maximus at urna nec laoreet. Suspendisse potenti. Vestibulum rhoncus sem ut mi pellentesque, in vestibulum erat blandit. Aliquam sodales dui ac maximus consectetur. Duis quis est vehicula, imperdiet nisl nec, fermentum erat. Duis tortor diam, pharetra eu euismod in, vehicula non eros. Curabitur facilisis dui at erat ultrices gravida. In at nunc ultricies, pulvinar mi vel, sagittis mauris. Praesent pharetra posuere purus ac imperdiet. Nulla facilisi.
+
+Sed pulvinar porttitor mi in ultricies. Etiam non dolor gravida eros pulvinar pellentesque et dictum ex. Proin eu ornare ligula, sed condimentum dui. Vivamus tincidunt tellus mi, sed semper ipsum pharetra a. Suspendisse sollicitudin at sapien nec volutpat. Etiam justo urna, laoreet ac lacus sed, ultricies facilisis dolor. Integer posuere, metus vel viverra gravida, risus elit ornare magna, id feugiat erat risus ullamcorper libero. Proin vitae diam auctor, laoreet lorem vitae, varius tellus.
+
+Mauris sed eros in ex maximus volutpat. Suspendisse potenti. Donec lacinia justo consectetur sagittis tempor. Proin ullamcorper nisi vitae auctor rhoncus. Sed tristique aliquam augue. Pellentesque vitae fringilla ligula. Nulla arcu elit, efficitur eu nunc malesuada, eleifend tincidunt orci. Interdum et malesuada fames ac ante ipsum primis in faucibus. Integer mattis orci in bibendum ultricies. Quisque a dui erat. Phasellus et vulputate ipsum. Proin metus ex, lobortis nec ornare eget, bibendum ut sapien. Aliquam in dolor lobortis, aliquam tellus a, congue augue. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+
+Aenean pretium purus augue, ut bibendum erat convallis quis. Cras condimentum quis velit ac mollis. Suspendisse non purus fringilla, venenatis nisl porta, finibus odio. Curabitur aliquet metus faucibus libero interdum euismod. Morbi sed magna nisl. Morbi odio nibh, facilisis vel sapien eu, tempus tincidunt erat. Nullam erat velit, sagittis at purus quis, tristique scelerisque tortor. Pellentesque lacinia tortor id est aliquam viverra. Vestibulum et diam ac ipsum mollis fringilla.
diff --git a/examples/portfolio/src/layouts/BaseLayout.astro b/examples/portfolio/src/layouts/BaseLayout.astro
new file mode 100644
index 000000000..235599abd
--- /dev/null
+++ b/examples/portfolio/src/layouts/BaseLayout.astro
@@ -0,0 +1,118 @@
+---
+// Learn about using Astro layouts:
+// https://docs.astro.build/en/core-concepts/layouts/
+
+// Component Imports
+import MainHead from '../components/MainHead.astro';
+import Nav from '../components/Nav.astro';
+import Footer from '../components/Footer.astro';
+
+interface Props {
+ title?: string | undefined;
+ description?: string | undefined;
+}
+
+const { title, description } = Astro.props;
+---
+
+<html lang="en">
+ <head>
+ <MainHead title={title} description={description} />
+ </head>
+ <body>
+ <div class="stack backgrounds">
+ <Nav />
+ <slot />
+ <Footer />
+ </div>
+
+ <script>
+ // Add “loaded” class once the document has completely loaded.
+ addEventListener('load', () => document.documentElement.classList.add('loaded'));
+ </script>
+
+ <style>
+ :root {
+ --_placeholder-bg: linear-gradient(transparent, transparent);
+ --bg-image-main: url('/assets/backgrounds/bg-main-light-800w.jpg');
+ --bg-image-main-curves: url('/assets/backgrounds/bg-main-light.svg');
+ --bg-image-subtle-1: var(--_placeholder-bg);
+ --bg-image-subtle-2: var(--_placeholder-bg);
+ --bg-image-footer: var(--_placeholder-bg);
+ --bg-svg-blend-mode: overlay;
+ --bg-blend-mode: darken;
+ --bg-image-aspect-ratio: 2.25;
+ --bg-scale: 1.68;
+ --bg-aspect-ratio: calc(var(--bg-image-aspect-ratio) / var(--bg-scale));
+ --bg-gradient-size: calc(var(--bg-scale) * 100%);
+ }
+
+ :root.theme-dark {
+ --bg-image-main: url('/assets/backgrounds/bg-main-dark-800w.jpg');
+ --bg-image-main-curves: url('/assets/backgrounds/bg-main-dark.svg');
+ --bg-svg-blend-mode: darken;
+ --bg-blend-mode: lighten;
+ }
+
+ /* These backgrounds are displayed below the fold, so we lazy load them
+ once the `.loaded` class has been set. */
+ :root.loaded {
+ --bg-image-subtle-1: url('/assets/backgrounds/bg-subtle-1-light-800w.jpg');
+ --bg-image-subtle-2: url('/assets/backgrounds/bg-subtle-2-light-800w.jpg');
+ --bg-image-footer: url('/assets/backgrounds/bg-footer-light-800w.jpg');
+ }
+ :root.loaded.theme-dark {
+ --bg-image-subtle-1: url('/assets/backgrounds/bg-subtle-1-dark-800w.jpg');
+ --bg-image-subtle-2: url('/assets/backgrounds/bg-subtle-2-dark-800w.jpg');
+ --bg-image-footer: url('/assets/backgrounds/bg-footer-dark-800w.jpg');
+ }
+
+ @media (min-width: 50em) {
+ :root {
+ --bg-scale: 1;
+ --bg-image-main: url('/assets/backgrounds/bg-main-light-1440w.jpg');
+ }
+ :root.theme-dark {
+ --bg-image-main: url('/assets/backgrounds/bg-main-dark-1440w.jpg');
+ }
+
+ :root.loaded {
+ --bg-image-subtle-1: url('/assets/backgrounds/bg-subtle-1-light-1440w.jpg');
+ --bg-image-subtle-2: url('/assets/backgrounds/bg-subtle-2-light-1440w.jpg');
+ --bg-image-footer: url('/assets/backgrounds/bg-footer-light-1440w.jpg');
+ }
+ :root.loaded.theme-dark {
+ --bg-image-subtle-1: url('/assets/backgrounds/bg-subtle-1-dark-1440w.jpg');
+ --bg-image-subtle-2: url('/assets/backgrounds/bg-subtle-2-dark-1440w.jpg');
+ --bg-image-footer: url('/assets/backgrounds/bg-footer-dark-1440w.jpg');
+ }
+ }
+
+ .backgrounds {
+ min-height: 100%;
+ isolation: isolate;
+ background:
+ /*noise*/
+ url('/assets/backgrounds/noise.png') top center/220px repeat,
+ /*footer*/ var(--bg-image-footer) bottom center/var(--bg-gradient-size) no-repeat,
+ /*header1*/ var(--bg-image-main-curves) top center/var(--bg-gradient-size) no-repeat,
+ /*header2*/ var(--bg-image-main) top center/var(--bg-gradient-size) no-repeat,
+ /*base*/ var(--gray-999);
+ background-blend-mode: /*noise*/
+ overlay,
+ /*footer*/ var(--bg-blend-mode),
+ /*header1*/ var(--bg-svg-blend-mode),
+ /*header2*/ normal,
+ /*base*/ normal;
+ }
+ @media (forced-colors: active) {
+ /* Deactivate custom backgrounds for high contrast users. */
+ .backgrounds {
+ background: none;
+ background-blend-mode: none;
+ --bg-gradient-size: none;
+ }
+ }
+ </style>
+ </body>
+</html>
diff --git a/examples/portfolio/src/pages/404.astro b/examples/portfolio/src/pages/404.astro
new file mode 100644
index 000000000..e3995899a
--- /dev/null
+++ b/examples/portfolio/src/pages/404.astro
@@ -0,0 +1,8 @@
+---
+import Hero from '../components/Hero.astro';
+import BaseLayout from '../layouts/BaseLayout.astro';
+---
+
+<BaseLayout title="Not Found" description="404 Error — this page was not found">
+ <Hero title="Page Not Found" tagline="Not found" />
+</BaseLayout>
diff --git a/examples/portfolio/src/pages/about.astro b/examples/portfolio/src/pages/about.astro
new file mode 100644
index 000000000..14e34ba81
--- /dev/null
+++ b/examples/portfolio/src/pages/about.astro
@@ -0,0 +1,120 @@
+---
+import BaseLayout from '../layouts/BaseLayout.astro';
+
+import ContactCTA from '../components/ContactCTA.astro';
+import Hero from '../components/Hero.astro';
+---
+
+<BaseLayout title="About | Jeanine White" description="About Jeanine White Lorem Ipsum">
+ <div class="stack gap-20">
+ <main class="wrapper about">
+ <Hero
+ title="About"
+ tagline="Thanks for stopping by. Read below to learn more about myself and my background."
+ >
+ <img
+ width="1553"
+ height="873"
+ src="/assets/at-work.jpg"
+ alt="Jeanine White at work with a colleague"
+ />
+ </Hero>
+
+ <section>
+ <h2 class="section-title">Background</h2>
+ <div class="content">
+ <p>
+ Lorem ipsum dolor sit amet, <a href="https://astro.build/">Astro</a> makes people happy.
+ Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Proin nibh nisl condimentum
+ id venenatis a condimentum vitae. Dapibus ultrices in iaculis nunc. Arcu odio ut sem nulla
+ pharetra diam sit amet. Diam quis enim lobortis scelerisque fermentum dui faucibus in ornare.
+ </p>
+ <p>
+ Arcu dui vivamus arcu felis bibendum ut tristique et egestas. Eget gravida cum sociis
+ natoque penatibus. Cras fermentum odio eu feugiat pretium nibh. Proin nibh nisl
+ condimentum id venenatis. Porta nibh venenatis cras sed felis eget velit. Id diam vel
+ quam elementum pulvinar etiam non.
+ </p>
+ <p>
+ Ultrices tincidunt arcu non sodales neque sodales ut. Sed enim ut sem viverra aliquet
+ eget sit amet. Lacus luctus accumsan tortor posuere ac ut consequat semper viverra.
+ Viverra accumsan in nisl nisi scelerisque eu ultrices. In massa tempor nec feugiat nisl
+ pretium fusce.
+ </p>
+ </div>
+ </section>
+ <section>
+ <h2 class="section-title">Education</h2>
+ <div class="content">
+ <p>Corporis voluptates tenetur laudantium.</p>
+ </div>
+ </section>
+ <section>
+ <h2 class="section-title">Skills</h2>
+ <div class="content">
+ <p>officia unde omnis</p>
+ </div>
+ </section>
+ </main>
+
+ <ContactCTA />
+ </div>
+</BaseLayout>
+
+<style>
+ .about {
+ display: flex;
+ flex-direction: column;
+ gap: 3.5rem;
+ }
+
+ img {
+ margin-top: 1.5rem;
+ border-radius: 1.5rem;
+ box-shadow: var(--shadow-md);
+ }
+
+ section {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ color: var(--gray-200);
+ }
+
+ .section-title {
+ grid-column-start: 1;
+ font-size: var(--text-xl);
+ color: var(--gray-0);
+ }
+
+ .content {
+ grid-column: 2 / 4;
+ }
+
+ .content :global(a) {
+ text-decoration: 1px solid underline transparent;
+ text-underline-offset: 0.25em;
+ transition: text-decoration-color var(--theme-transition);
+ }
+
+ .content :global(a:hover),
+ .content :global(a:focus) {
+ text-decoration-color: currentColor;
+ }
+
+ @media (min-width: 50em) {
+ .about {
+ display: grid;
+ grid-template-columns: 1fr 60% 1fr;
+ }
+
+ .about > :global(:first-child) {
+ grid-column-start: 2;
+ }
+
+ section {
+ display: contents;
+ font-size: var(--text-lg);
+ }
+ }
+</style>
diff --git a/examples/portfolio/src/pages/index.astro b/examples/portfolio/src/pages/index.astro
new file mode 100644
index 000000000..5f67e3860
--- /dev/null
+++ b/examples/portfolio/src/pages/index.astro
@@ -0,0 +1,252 @@
+---
+import { getCollection } from 'astro:content';
+
+// Layout import — provides basic page elements: <head>, <nav>, <footer> etc.
+import BaseLayout from '../layouts/BaseLayout.astro';
+
+// Component Imports
+import CallToAction from '../components/CallToAction.astro';
+import Grid from '../components/Grid.astro';
+import Hero from '../components/Hero.astro';
+import Icon from '../components/Icon.astro';
+import Pill from '../components/Pill.astro';
+import PortfolioPreview from '../components/PortfolioPreview.astro';
+
+// Page section components
+import ContactCTA from '../components/ContactCTA.astro';
+import Skills from '../components/Skills.astro';
+
+// Content Fetching: List four most recent work projects
+const projects = (await getCollection('work'))
+ .sort((a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf())
+ .slice(0, 4);
+
+// Full Astro Component Syntax:
+// https://docs.astro.build/basics/astro-components/
+---
+
+<BaseLayout>
+ <div class="stack gap-20 lg:gap-48">
+ <div class="wrapper stack gap-8 lg:gap-20">
+ <header class="hero">
+ <Hero
+ title="Hello, my name is Jeanine White"
+ tagline="I am a Creative Developer who is currently based in Portland, Oregon."
+ align="start"
+ >
+ <div class="roles">
+ <Pill><Icon icon="code" size="1.33em" /> Developer</Pill>
+ <Pill><Icon icon="microphone-stage" size="1.33em" /> Speaker</Pill>
+ <Pill><Icon icon="pencil-line" size="1.33em" /> Writer</Pill>
+ </div>
+ </Hero>
+
+ <img
+ alt="Jeanine White smiling in a red plaid shirt and tortoise shell glasses"
+ width="480"
+ height="620"
+ src="/assets/portrait.jpg"
+ />
+ </header>
+
+ <Skills />
+ </div>
+
+ <main class="wrapper stack gap-20 lg:gap-48">
+ <section class="section with-background with-cta">
+ <header class="section-header stack gap-2 lg:gap-4">
+ <h3>Selected Work</h3>
+ <p>Take a look below at some of my featured work for clients from the past few years.</p>
+ </header>
+
+ <div class="gallery">
+ <Grid variant="offset">
+ {
+ projects.map((project) => (
+ <li>
+ <PortfolioPreview project={project} />
+ </li>
+ ))
+ }
+ </Grid>
+ </div>
+
+ <div class="cta">
+ <CallToAction href="/work/">
+ View All
+ <Icon icon="arrow-right" size="1.2em" />
+ </CallToAction>
+ </div>
+ </section>
+
+ <section class="section with-background bg-variant">
+ <header class="section-header stack gap-2 lg:gap-4">
+ <h3>Mentions</h3>
+ <p>
+ I have been fortunate enough to receive praise for my work in several publications. Take
+ a look below to learn more.
+ </p>
+ </header>
+
+ <div class="gallery">
+ <Grid variant="small">
+ {
+ ['Medium', 'BuzzFeed', 'The Next Web', 'awwwards.', 'TechCrunch'].map((brand) => (
+ <li class="mention-card">
+ <p>{brand}</p>
+ </li>
+ ))
+ }
+ </Grid>
+ </div>
+ </section>
+ </main>
+
+ <ContactCTA />
+ </div>
+</BaseLayout>
+
+<style>
+ .hero {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 2rem;
+ }
+
+ .roles {
+ display: none;
+ }
+
+ .hero img {
+ aspect-ratio: 5 / 4;
+ object-fit: cover;
+ object-position: top;
+ border-radius: 1.5rem;
+ box-shadow: var(--shadow-md);
+ }
+
+ @media (min-width: 50em) {
+ .hero {
+ display: grid;
+ grid-template-columns: 6fr 4fr;
+ padding-inline: 2.5rem;
+ gap: 3.75rem;
+ }
+
+ .roles {
+ margin-top: 0.5rem;
+ display: flex;
+ gap: 0.5rem;
+ }
+
+ .hero img {
+ aspect-ratio: 3 / 4;
+ border-radius: 4.5rem;
+ object-fit: cover;
+ }
+ }
+
+ /* ====================================================== */
+
+ .section {
+ display: grid;
+ gap: 2rem;
+ }
+
+ .with-background {
+ position: relative;
+ }
+
+ .with-background::before {
+ --hero-bg: var(--bg-image-subtle-2);
+
+ content: '';
+ position: absolute;
+ pointer-events: none;
+ left: 50%;
+ width: 100vw;
+ aspect-ratio: calc(2.25 / var(--bg-scale));
+ top: 0;
+ transform: translateY(-75%) translateX(-50%);
+ background:
+ url('/assets/backgrounds/noise.png') top center/220px repeat,
+ var(--hero-bg) center center / var(--bg-gradient-size) no-repeat,
+ var(--gray-999);
+ background-blend-mode: overlay, normal, normal, normal;
+ mix-blend-mode: var(--bg-blend-mode);
+ z-index: -1;
+ }
+
+ .with-background.bg-variant::before {
+ --hero-bg: var(--bg-image-subtle-1);
+ }
+
+ .section-header {
+ justify-self: center;
+ text-align: center;
+ max-width: 50ch;
+ font-size: var(--text-md);
+ color: var(--gray-300);
+ }
+
+ .section-header h3 {
+ font-size: var(--text-2xl);
+ }
+
+ @media (min-width: 50em) {
+ .section {
+ grid-template-columns: repeat(4, 1fr);
+ grid-template-areas: 'header header header header' 'gallery gallery gallery gallery';
+ gap: 5rem;
+ }
+
+ .section.with-cta {
+ grid-template-areas: 'header header header cta' 'gallery gallery gallery gallery';
+ }
+
+ .section-header {
+ grid-area: header;
+ font-size: var(--text-lg);
+ }
+
+ .section-header h3 {
+ font-size: var(--text-4xl);
+ }
+
+ .with-cta .section-header {
+ justify-self: flex-start;
+ text-align: left;
+ }
+
+ .gallery {
+ grid-area: gallery;
+ }
+
+ .cta {
+ grid-area: cta;
+ }
+ }
+
+ /* ====================================================== */
+
+ .mention-card {
+ display: flex;
+ height: 7rem;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+ border: 1px solid var(--gray-800);
+ border-radius: 1.5rem;
+ color: var(--gray-300);
+ background: var(--gradient-subtle);
+ box-shadow: var(--shadow-sm);
+ }
+
+ @media (min-width: 50em) {
+ .mention-card {
+ border-radius: 1.5rem;
+ height: 9.5rem;
+ }
+ }
+</style>
diff --git a/examples/portfolio/src/pages/work.astro b/examples/portfolio/src/pages/work.astro
new file mode 100644
index 000000000..233c760bf
--- /dev/null
+++ b/examples/portfolio/src/pages/work.astro
@@ -0,0 +1,39 @@
+---
+import { getCollection } from 'astro:content';
+
+import BaseLayout from '../layouts/BaseLayout.astro';
+
+import ContactCTA from '../components/ContactCTA.astro';
+import PortfolioPreview from '../components/PortfolioPreview.astro';
+import Hero from '../components/Hero.astro';
+import Grid from '../components/Grid.astro';
+
+const projects = (await getCollection('work')).sort(
+ (a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf(),
+);
+---
+
+<BaseLayout
+ title="My Work | Jeanine White"
+ description="Learn about Jeanine White's most recent projects"
+>
+ <div class="stack gap-20">
+ <main class="wrapper stack gap-8">
+ <Hero
+ title="My Work"
+ tagline="See my most recent projects below to get an idea of my past experience."
+ align="start"
+ />
+ <Grid variant="offset">
+ {
+ projects.map((project) => (
+ <li>
+ <PortfolioPreview project={project} />
+ </li>
+ ))
+ }
+ </Grid>
+ </main>
+ <ContactCTA />
+ </div>
+</BaseLayout>
diff --git a/examples/portfolio/src/pages/work/[...slug].astro b/examples/portfolio/src/pages/work/[...slug].astro
new file mode 100644
index 000000000..90eb3ba8d
--- /dev/null
+++ b/examples/portfolio/src/pages/work/[...slug].astro
@@ -0,0 +1,152 @@
+---
+import { type CollectionEntry, getCollection } from 'astro:content';
+
+import BaseLayout from '../../layouts/BaseLayout.astro';
+
+import ContactCTA from '../../components/ContactCTA.astro';
+import Hero from '../../components/Hero.astro';
+import Icon from '../../components/Icon.astro';
+import Pill from '../../components/Pill.astro';
+import { render } from 'astro:content';
+
+interface Props {
+ entry: CollectionEntry<'work'>;
+}
+
+// This is a dynamic route that generates a page for every Markdown file in src/content/
+// Read more about dynamic routes and this `getStaticPaths` function in the Astro docs:
+// https://docs.astro.build/en/core-concepts/routing/#dynamic-routes
+export async function getStaticPaths() {
+ const work = await getCollection('work');
+ return work.map((entry) => ({
+ params: { slug: entry.id },
+ props: { entry },
+ }));
+}
+
+const { entry } = Astro.props;
+const { Content } = await render(entry);
+---
+
+<BaseLayout title={entry.data.title} description={entry.data.description}>
+ <div class="stack gap-20">
+ <div class="stack gap-15">
+ <header>
+ <div class="wrapper stack gap-2">
+ <a class="back-link" href="/work/"><Icon icon="arrow-left" /> Work</a>
+ <Hero title={entry.data.title} align="start">
+ <div class="details">
+ <div class="tags">
+ {entry.data.tags.map((t) => <Pill>{t}</Pill>)}
+ </div>
+ <p class="description">{entry.data.description}</p>
+ </div>
+ </Hero>
+ </div>
+ </header>
+ <main class="wrapper">
+ <div class="stack gap-10 content">
+ {entry.data.img && <img src={entry.data.img} alt={entry.data.img_alt || ''} />}
+ <div class="content">
+ <Content />
+ </div>
+ </div>
+ </main>
+ </div>
+ <ContactCTA />
+ </div>
+</BaseLayout>
+
+<style>
+ header {
+ padding-bottom: 2.5rem;
+ border-bottom: 1px solid var(--gray-800);
+ }
+
+ .back-link {
+ display: none;
+ }
+
+ .details {
+ display: flex;
+ flex-direction: column;
+ padding: 0.5rem;
+ gap: 1.5rem;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ .tags {
+ display: flex;
+ gap: 0.5rem;
+ }
+
+ .description {
+ font-size: var(--text-lg);
+ max-width: 54ch;
+ }
+
+ .content {
+ max-width: 65ch;
+ margin-inline: auto;
+ }
+
+ .content > :global(* + *) {
+ margin-top: 1rem;
+ }
+
+ .content :global(h1),
+ .content :global(h2),
+ .content :global(h3),
+ .content :global(h4),
+ .content :global(h5) {
+ margin: 1.5rem 0;
+ }
+
+ .content :global(img) {
+ border-radius: 1.5rem;
+ box-shadow: var(--shadow-sm);
+ background: var(--gradient-subtle);
+ border: 1px solid var(--gray-800);
+ }
+
+ .content :global(blockquote) {
+ font-size: var(--text-lg);
+ font-family: var(--font-brand);
+ font-weight: 600;
+ line-height: 1.1;
+ padding-inline-start: 1.5rem;
+ border-inline-start: 0.25rem solid var(--accent-dark);
+ color: var(--gray-0);
+ }
+
+ .back-link,
+ .content :global(a) {
+ text-decoration: 1px solid underline transparent;
+ text-underline-offset: 0.25em;
+ transition: text-decoration-color var(--theme-transition);
+ }
+
+ .back-link:hover,
+ .back-link:focus,
+ .content :global(a:hover),
+ .content :global(a:focus) {
+ text-decoration-color: currentColor;
+ }
+
+ @media (min-width: 50em) {
+ .back-link {
+ display: block;
+ align-self: flex-start;
+ }
+
+ .details {
+ flex-direction: row;
+ gap: 2.5rem;
+ }
+
+ .content :global(blockquote) {
+ font-size: var(--text-2xl);
+ }
+ }
+</style>
diff --git a/examples/portfolio/src/styles/global.css b/examples/portfolio/src/styles/global.css
new file mode 100644
index 000000000..12d49fcfb
--- /dev/null
+++ b/examples/portfolio/src/styles/global.css
@@ -0,0 +1,262 @@
+/* Global variables */
+:root {
+ /* Colors */
+ --gray-0: #090b11;
+ --gray-50: #141925;
+ --gray-100: #283044;
+ --gray-200: #3d4663;
+ --gray-300: #505d84;
+ --gray-400: #6474a2;
+ --gray-500: #8490b5;
+ --gray-600: #a3acc8;
+ --gray-700: #c3cadb;
+ --gray-800: #e3e6ee;
+ --gray-900: #f3f4f7;
+ --gray-999-basis: 0, 0%, 100%;
+ --gray-999_40: hsla(var(--gray-999-basis), 0.4);
+ --gray-999: #ffffff;
+
+ --accent-light: #c561f6;
+ --accent-regular: #7611a6;
+ --accent-dark: #1c0056;
+ --accent-overlay: hsla(280, 89%, 67%, 0.33);
+ --accent-subtle-overlay: var(--accent-overlay);
+ --accent-text-over: var(--gray-999);
+
+ --link-color: var(--accent-regular);
+
+ /* Gradients */
+ --gradient-stop-1: var(--accent-light);
+ --gradient-stop-2: var(--accent-regular);
+ --gradient-stop-3: var(--accent-dark);
+ --gradient-subtle: linear-gradient(150deg, var(--gray-900) 19%, var(--gray-999) 150%);
+ --gradient-accent: linear-gradient(
+ 150deg,
+ var(--gradient-stop-1),
+ var(--gradient-stop-2),
+ var(--gradient-stop-3)
+ );
+ --gradient-accent-orange: linear-gradient(
+ 150deg,
+ #ca7879,
+ var(--accent-regular),
+ var(--accent-dark)
+ );
+ --gradient-stroke: linear-gradient(180deg, var(--gray-900), var(--gray-700));
+
+ /* Shadows */
+ --shadow-sm:
+ 0px 6px 3px rgba(9, 11, 17, 0.01), 0px 4px 2px rgba(9, 11, 17, 0.01),
+ 0px 2px 2px rgba(9, 11, 17, 0.02), 0px 0px 1px rgba(9, 11, 17, 0.03);
+ --shadow-md:
+ 0px 28px 11px rgba(9, 11, 17, 0.01), 0px 16px 10px rgba(9, 11, 17, 0.03),
+ 0px 7px 7px rgba(9, 11, 17, 0.05), 0px 2px 4px rgba(9, 11, 17, 0.06);
+ --shadow-lg:
+ 0px 62px 25px rgba(9, 11, 17, 0.01), 0px 35px 21px rgba(9, 11, 17, 0.05),
+ 0px 16px 16px rgba(9, 11, 17, 0.1), 0px 4px 9px rgba(9, 11, 17, 0.12);
+
+ /* Text Sizes */
+ --text-sm: 0.875rem;
+ --text-base: 1rem;
+ --text-md: 1.125rem;
+ --text-lg: 1.25rem;
+ --text-xl: 1.625rem;
+ --text-2xl: 2.125rem;
+ --text-3xl: 2.625rem;
+ --text-4xl: 3.5rem;
+ --text-5xl: 4.5rem;
+
+ /* Fonts */
+ --font-system:
+ system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
+ 'Open Sans', 'Helvetica Neue', sans-serif;
+ --font-body: 'Public Sans', var(--font-system);
+ --font-brand: Rubik, var(--font-system);
+
+ /* Transitions */
+ --theme-transition: 0.2s ease-in-out;
+}
+
+:root.theme-dark {
+ --gray-0: #ffffff;
+ --gray-50: #f3f4f7;
+ --gray-100: #e3e6ee;
+ --gray-200: #c3cadb;
+ --gray-300: #a3acc8;
+ --gray-400: #8490b5;
+ --gray-500: #6474a2;
+ --gray-600: #505d84;
+ --gray-700: #3d4663;
+ --gray-800: #283044;
+ --gray-900: #141925;
+ --gray-999-basis: 225, 31%, 5%;
+ --gray-999: #090b11;
+
+ --accent-light: #1c0056;
+ --accent-regular: #7611a6;
+ --accent-dark: #c561f6;
+ --accent-overlay: hsla(280, 89%, 67%, 0.33);
+ --accent-subtle-overlay: hsla(281, 81%, 36%, 0.33);
+ --accent-text-over: var(--gray-0);
+
+ --link-color: var(--accent-dark);
+
+ --gradient-stop-1: #4c11c6;
+ --gradient-subtle: linear-gradient(150deg, var(--gray-900) 19%, var(--gray-999) 81%);
+ --gradient-accent-orange: linear-gradient(
+ 150deg,
+ #ca7879,
+ var(--accent-regular),
+ var(--accent-light)
+ );
+ --gradient-stroke: linear-gradient(180deg, var(--gray-600), var(--gray-800));
+
+ --shadow-sm:
+ 0px 6px 3px rgba(255, 255, 255, 0.01), 0px 4px 2px rgba(255, 255, 255, 0.01),
+ 0px 2px 2px rgba(255, 255, 255, 0.02), 0px 0px 1px rgba(255, 255, 255, 0.03);
+ --shadow-md:
+ 0px 28px 11px rgba(255, 255, 255, 0.01), 0px 16px 10px rgba(255, 255, 255, 0.03),
+ 0px 7px 7px rgba(255, 255, 255, 0.05), 0px 2px 4px rgba(255, 255, 255, 0.06);
+ --shadow-lg:
+ 0px 62px 25px rgba(255, 255, 255, 0.01), 0px 35px 21px rgba(255, 255, 255, 0.05),
+ 0px 16px 16px rgba(255, 255, 255, 0.1), 0px 4px 9px rgba(255, 255, 255, 0.12);
+}
+
+html,
+body {
+ min-height: 100%;
+ overflow-x: hidden;
+}
+
+body {
+ background-color: var(--gray-999);
+ color: var(--gray-200);
+ font-family: var(--font-body);
+ -webkit-font-smoothing: antialiased;
+ line-height: 1.5;
+}
+
+*,
+*::after,
+*::before {
+ box-sizing: border-box;
+ margin: 0;
+}
+
+img {
+ max-width: 100%;
+ height: auto;
+}
+
+a {
+ color: var(--link-color);
+}
+
+h1,
+h2,
+h3,
+h4,
+h5 {
+ line-height: 1.1;
+ font-family: var(--font-brand);
+ font-weight: 600;
+ color: var(--gray-100);
+}
+
+h1 {
+ font-size: var(--text-5xl);
+}
+
+h2 {
+ font-size: var(--text-4xl);
+}
+
+h3 {
+ font-size: var(--text-3xl);
+}
+
+h4 {
+ font-size: var(--text-2xl);
+}
+
+h5 {
+ font-size: var(--text-xl);
+}
+
+/* Utilities */
+
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border-width: 0;
+}
+
+.wrapper {
+ width: 100%;
+ max-width: 83rem;
+ margin-inline: auto;
+ padding-inline: 1.5rem;
+}
+
+.stack {
+ display: flex;
+ flex-direction: column;
+}
+
+.gap-2 {
+ gap: 0.5rem;
+}
+.gap-4 {
+ gap: 1rem;
+}
+.gap-8 {
+ gap: 2rem;
+}
+.gap-10 {
+ gap: 2.5rem;
+}
+.gap-15 {
+ gap: 3.75rem;
+}
+.gap-20 {
+ gap: 5rem;
+}
+.gap-30 {
+ gap: 7.5rem;
+}
+.gap-48 {
+ gap: 12rem;
+}
+
+@media (min-width: 50em) {
+ .lg\:gap-2 {
+ gap: 0.5rem;
+ }
+ .lg\:gap-4 {
+ gap: 1rem;
+ }
+ .lg\:gap-8 {
+ gap: 2rem;
+ }
+ .lg\:gap-10 {
+ gap: 2.5rem;
+ }
+ .lg\:gap-15 {
+ gap: 3.75rem;
+ }
+ .lg\:gap-20 {
+ gap: 5rem;
+ }
+ .lg\:gap-30 {
+ gap: 7.5rem;
+ }
+ .lg\:gap-48 {
+ gap: 12rem;
+ }
+}
diff --git a/examples/portfolio/tsconfig.json b/examples/portfolio/tsconfig.json
new file mode 100644
index 000000000..8bf91d3bb
--- /dev/null
+++ b/examples/portfolio/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "extends": "astro/tsconfigs/strict",
+ "include": [".astro/types.d.ts", "**/*"],
+ "exclude": ["dist"]
+}
diff --git a/examples/ssr/.codesandbox/Dockerfile b/examples/ssr/.codesandbox/Dockerfile
new file mode 100644
index 000000000..c3b5c81a1
--- /dev/null
+++ b/examples/ssr/.codesandbox/Dockerfile
@@ -0,0 +1 @@
+FROM node:18-bullseye
diff --git a/examples/ssr/.gitignore b/examples/ssr/.gitignore
new file mode 100644
index 000000000..16d54bb13
--- /dev/null
+++ b/examples/ssr/.gitignore
@@ -0,0 +1,24 @@
+# build output
+dist/
+# generated types
+.astro/
+
+# dependencies
+node_modules/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
+
+# jetbrains setting folder
+.idea/
diff --git a/examples/ssr/.vscode/extensions.json b/examples/ssr/.vscode/extensions.json
new file mode 100644
index 000000000..22a15055d
--- /dev/null
+++ b/examples/ssr/.vscode/extensions.json
@@ -0,0 +1,4 @@
+{
+ "recommendations": ["astro-build.astro-vscode"],
+ "unwantedRecommendations": []
+}
diff --git a/examples/ssr/.vscode/launch.json b/examples/ssr/.vscode/launch.json
new file mode 100644
index 000000000..d64220976
--- /dev/null
+++ b/examples/ssr/.vscode/launch.json
@@ -0,0 +1,11 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "command": "./node_modules/.bin/astro dev",
+ "name": "Development server",
+ "request": "launch",
+ "type": "node-terminal"
+ }
+ ]
+}
diff --git a/examples/ssr/astro.config.mjs b/examples/ssr/astro.config.mjs
new file mode 100644
index 000000000..78d88cb1a
--- /dev/null
+++ b/examples/ssr/astro.config.mjs
@@ -0,0 +1,13 @@
+// @ts-check
+import { defineConfig } from 'astro/config';
+import svelte from '@astrojs/svelte';
+import node from '@astrojs/node';
+
+// https://astro.build/config
+export default defineConfig({
+ output: 'server',
+ adapter: node({
+ mode: 'standalone',
+ }),
+ integrations: [svelte()],
+});
diff --git a/examples/ssr/package.json b/examples/ssr/package.json
new file mode 100644
index 000000000..1989e6193
--- /dev/null
+++ b/examples/ssr/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "@example/ssr",
+ "type": "module",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "dev": "astro dev",
+ "build": "astro build",
+ "preview": "astro preview",
+ "astro": "astro",
+ "server": "node dist/server/entry.mjs"
+ },
+ "dependencies": {
+ "@astrojs/node": "^9.2.2",
+ "@astrojs/svelte": "^7.1.0",
+ "astro": "^5.9.0",
+ "svelte": "^5.25.7"
+ }
+}
diff --git a/examples/ssr/public/favicon.svg b/examples/ssr/public/favicon.svg
new file mode 100644
index 000000000..f157bd1c5
--- /dev/null
+++ b/examples/ssr/public/favicon.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
+ <path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
+ <style>
+ path { fill: #000; }
+ @media (prefers-color-scheme: dark) {
+ path { fill: #FFF; }
+ }
+ </style>
+</svg>
diff --git a/examples/ssr/public/images/products/cereal.jpg b/examples/ssr/public/images/products/cereal.jpg
new file mode 100644
index 000000000..35601a789
--- /dev/null
+++ b/examples/ssr/public/images/products/cereal.jpg
Binary files differ
diff --git a/examples/ssr/public/images/products/muffins.jpg b/examples/ssr/public/images/products/muffins.jpg
new file mode 100644
index 000000000..ced2d9a91
--- /dev/null
+++ b/examples/ssr/public/images/products/muffins.jpg
Binary files differ
diff --git a/examples/ssr/public/images/products/oats.jpg b/examples/ssr/public/images/products/oats.jpg
new file mode 100644
index 000000000..54ae1ebdb
--- /dev/null
+++ b/examples/ssr/public/images/products/oats.jpg
Binary files differ
diff --git a/examples/ssr/public/images/products/yogurt.jpg b/examples/ssr/public/images/products/yogurt.jpg
new file mode 100644
index 000000000..73c1b9a85
--- /dev/null
+++ b/examples/ssr/public/images/products/yogurt.jpg
Binary files differ
diff --git a/examples/ssr/src/api.ts b/examples/ssr/src/api.ts
new file mode 100644
index 000000000..2d10a85f9
--- /dev/null
+++ b/examples/ssr/src/api.ts
@@ -0,0 +1,72 @@
+export interface Product {
+ id: number;
+ name: string;
+ price: number;
+ image: string;
+}
+
+interface User {
+ id: number;
+}
+
+interface Cart {
+ items: Array<{
+ id: number;
+ name: string;
+ count: number;
+ }>;
+}
+
+async function getJson<T>(
+ incomingReq: Request,
+ endpoint: string,
+): Promise<T> {
+ const origin = new URL(incomingReq.url).origin;
+ try {
+ const response = await fetch(`${origin}${endpoint}`, {
+ credentials: 'same-origin',
+ headers: incomingReq.headers,
+ });
+ if (!response.ok) {
+ throw new Error(`GET ${endpoint} failed: ${response.statusText}`);
+ }
+ return response.json() as Promise<T>;
+ } catch (error) {
+ if (error instanceof DOMException || error instanceof TypeError) {
+ throw new Error(`GET ${endpoint} failed: ${error.message}`);
+ }
+ throw error;
+ }
+}
+
+export async function getProducts(incomingReq: Request): Promise<Product[]> {
+ return getJson<Product[]>(incomingReq, '/api/products');
+}
+
+export async function getProduct(incomingReq: Request, id: number): Promise<Product> {
+ return getJson<Product>(incomingReq, `/api/products/${id}`);
+}
+
+export async function getUser(incomingReq: Request): Promise<User> {
+ return getJson<User>(incomingReq, `/api/user`);
+}
+
+export async function getCart(incomingReq: Request): Promise<Cart> {
+ return getJson<Cart>(incomingReq, `/api/cart`);
+}
+
+export async function addToUserCart(id: number | string, name: string): Promise<void> {
+ await fetch(`${location.origin}/api/cart`, {
+ credentials: 'same-origin',
+ method: 'POST',
+ mode: 'no-cors',
+ headers: {
+ 'Content-Type': 'application/json',
+ Cache: 'no-cache',
+ },
+ body: JSON.stringify({
+ id,
+ name,
+ }),
+ });
+}
diff --git a/examples/ssr/src/components/AddToCart.svelte b/examples/ssr/src/components/AddToCart.svelte
new file mode 100644
index 000000000..bae888b6b
--- /dev/null
+++ b/examples/ssr/src/components/AddToCart.svelte
@@ -0,0 +1,53 @@
+<script>
+ import { addToUserCart } from '../api';
+ let { id, name } = $props()
+
+ function notifyCartItem(id) {
+ window.dispatchEvent(new CustomEvent('add-to-cart', {
+ detail: id
+ }));
+ }
+
+ async function addToCart() {
+ await addToUserCart(id, name);
+ notifyCartItem(id);
+ }
+</script>
+<style>
+ button {
+ display:block;
+ padding:0.5em 1em 0.5em 1em;
+ border-radius:100px;
+ border:none;
+ font-size: 1.4em;
+ position:relative;
+ background:#0652DD;
+ cursor:pointer;
+ height:2em;
+ width:10em;
+ overflow:hidden;
+ transition:transform 0.1s;
+ z-index:1;
+}
+button:hover {
+ transform:scale(1.1);
+}
+
+.pretext {
+ color:#fff;
+ background:#0652DD;
+ position:absolute;
+ top:0;
+ left:0;
+ height:100%;
+ width:100%;
+ display:flex;
+ justify-content:center;
+ align-items:center;
+ font-family: 'Quicksand', sans-serif;
+ text-transform: uppercase;
+}
+</style>
+<button click={addToCart}>
+ <span class="pretext">Add to cart</span>
+</button>
diff --git a/examples/ssr/src/components/Cart.svelte b/examples/ssr/src/components/Cart.svelte
new file mode 100644
index 000000000..5d4b7d251
--- /dev/null
+++ b/examples/ssr/src/components/Cart.svelte
@@ -0,0 +1,34 @@
+<script>
+ let { count } = $props()
+ let items = new Set();
+
+ function onAddToCart(ev) {
+ const id = ev.detail;
+ items.add(id);
+ count++;
+ }
+</script>
+<style>
+ .cart {
+ display: flex;
+ align-items: center;
+ text-decoration: none;
+ color: inherit;
+ }
+ .cart :first-child {
+ margin-right: 5px;
+ }
+
+ .cart-icon {
+ font-size: 36px;
+ }
+
+ .count {
+ font-size: 24px;
+ }
+</style>
+<svelte:window onadd-to-cart={onAddToCart}/>
+<a href="/cart" class="cart">
+ <span class="material-icons cart-icon">shopping_cart</span>
+ <span class="count">{count}</span>
+</a>
diff --git a/examples/ssr/src/components/Container.astro b/examples/ssr/src/components/Container.astro
new file mode 100644
index 000000000..f1741156c
--- /dev/null
+++ b/examples/ssr/src/components/Container.astro
@@ -0,0 +1,13 @@
+---
+const { tag = 'div' } = Astro.props;
+const Tag = tag;
+---
+
+<style>
+ .container {
+ width: 1248px; /** TODO: responsive */
+ margin-left: auto;
+ margin-right: auto;
+ }
+</style>
+<Tag class="container"><slot /></Tag>
diff --git a/examples/ssr/src/components/Header.astro b/examples/ssr/src/components/Header.astro
new file mode 100644
index 000000000..d266733e9
--- /dev/null
+++ b/examples/ssr/src/components/Header.astro
@@ -0,0 +1,49 @@
+---
+import TextDecorationSkip from './TextDecorationSkip.astro';
+import Cart from './Cart.svelte';
+import { getCart } from '../api';
+
+const cart = await getCart(Astro.request);
+const cartCount = cart.items.reduce((sum, item) => sum + item.count, 0);
+---
+
+<style>
+ @import url('https://fonts.googleapis.com/css2?family=Lobster&display=swap');
+
+ header {
+ margin: 1rem 2rem;
+ display: flex;
+ justify-content: space-between;
+ }
+
+ h1 {
+ margin: 0;
+ font-family: 'Lobster', cursive;
+ color: black;
+ }
+
+ a,
+ a:visited {
+ color: inherit;
+ text-decoration: none;
+ }
+
+ .right-pane {
+ display: flex;
+ }
+
+ .material-icons {
+ font-size: 36px;
+ margin-right: 1rem;
+ }
+</style>
+<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
+<header>
+ <h1><a href="/"><TextDecorationSkip text="Online Store" /></a></h1>
+ <div class="right-pane">
+ <a href="/login">
+ <span class="material-icons"> login</span>
+ </a>
+ <Cart client:idle count={cartCount} />
+ </div>
+</header>
diff --git a/examples/ssr/src/components/ProductListing.astro b/examples/ssr/src/components/ProductListing.astro
new file mode 100644
index 000000000..14e6e1d8c
--- /dev/null
+++ b/examples/ssr/src/components/ProductListing.astro
@@ -0,0 +1,70 @@
+---
+import type { Product } from '../api';
+
+interface Props {
+ products: Product[];
+}
+
+const { products } = Astro.props;
+---
+
+<style>
+ ul {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ }
+
+ figure {
+ width: 200px;
+ padding: 7px;
+ border: 1px solid black;
+ display: flex;
+ flex-direction: column;
+ }
+
+ figure figcaption {
+ text-align: center;
+ line-height: 1.6;
+ }
+
+ figure img {
+ width: 100%;
+ height: 250px;
+ object-fit: cover;
+ }
+
+ .product a {
+ display: block;
+ text-decoration: none;
+ color: inherit;
+ }
+
+ .name {
+ font-weight: 500;
+ }
+
+ .price {
+ font-size: 90%;
+ color: #787878;
+ }
+</style>
+<slot name="title" />
+<ul>
+ {
+ products.map((product) => (
+ <li class="product">
+ <a href={`/products/${product.id}`}>
+ <figure>
+ <img src={product.image} />
+ <figcaption>
+ <div class="name">{product.name}</div>
+ <div class="price">${product.price}</div>
+ </figcaption>
+ </figure>
+ </a>
+ </li>
+ ))
+ }
+</ul>
diff --git a/examples/ssr/src/components/TextDecorationSkip.astro b/examples/ssr/src/components/TextDecorationSkip.astro
new file mode 100644
index 000000000..707027763
--- /dev/null
+++ b/examples/ssr/src/components/TextDecorationSkip.astro
@@ -0,0 +1,23 @@
+---
+interface Props {
+ text: string;
+}
+
+const { text } = Astro.props;
+const words = text.split(' ');
+const last = words.length - 1;
+---
+
+<style>
+ span {
+ text-decoration: underline;
+ }
+</style>
+{
+ words.map((word, i) => (
+ <Fragment>
+ <span>{word}</span>
+ {i !== last && <Fragment>&#32;</Fragment>}
+ </Fragment>
+ ))
+}
diff --git a/examples/ssr/src/models/db.json b/examples/ssr/src/models/db.json
new file mode 100644
index 000000000..76f9e4da3
--- /dev/null
+++ b/examples/ssr/src/models/db.json
@@ -0,0 +1,28 @@
+{
+ "products": [
+ {
+ "id": 1,
+ "name": "Cereal",
+ "price": 3.99,
+ "image": "/images/products/cereal.jpg"
+ },
+ {
+ "id": 2,
+ "name": "Yogurt",
+ "price": 3.97,
+ "image": "/images/products/yogurt.jpg"
+ },
+ {
+ "id": 3,
+ "name": "Rolled Oats",
+ "price": 2.89,
+ "image": "/images/products/oats.jpg"
+ },
+ {
+ "id": 4,
+ "name": "Muffins",
+ "price": 4.39,
+ "image": "/images/products/muffins.jpg"
+ }
+ ]
+}
diff --git a/examples/ssr/src/models/db.ts b/examples/ssr/src/models/db.ts
new file mode 100644
index 000000000..0ec181f9a
--- /dev/null
+++ b/examples/ssr/src/models/db.ts
@@ -0,0 +1,6 @@
+import db from './db.json';
+
+const products = db.products;
+const productMap = new Map(products.map((product) => [product.id, product]));
+
+export { products, productMap };
diff --git a/examples/ssr/src/models/session.ts b/examples/ssr/src/models/session.ts
new file mode 100644
index 000000000..16dce00b4
--- /dev/null
+++ b/examples/ssr/src/models/session.ts
@@ -0,0 +1,2 @@
+// Normally this would be in a database.
+export const userCartItems = new Map();
diff --git a/examples/ssr/src/pages/api/cart.ts b/examples/ssr/src/pages/api/cart.ts
new file mode 100644
index 000000000..8d64ec7d8
--- /dev/null
+++ b/examples/ssr/src/pages/api/cart.ts
@@ -0,0 +1,38 @@
+import type { APIContext } from 'astro';
+import { userCartItems } from '../../models/session';
+
+export function GET({ cookies }: APIContext) {
+ let userId = cookies.get('user-id')?.value;
+
+ if (!userId || !userCartItems.has(userId)) {
+ return Response.json({ items: [] });
+ }
+ let items = userCartItems.get(userId);
+ let array = Array.from(items.values());
+
+ return Response.json({ items: array });
+}
+
+interface AddToCartItem {
+ id: number;
+ name: string;
+}
+
+export async function POST({ cookies, request }: APIContext) {
+ const item: AddToCartItem = await request.json();
+
+ let userId = cookies.get('user-id')?.value;
+
+ if (!userCartItems.has(userId)) {
+ userCartItems.set(userId, new Map());
+ }
+
+ let cart = userCartItems.get(userId);
+ if (cart.has(item.id)) {
+ cart.get(item.id).count++;
+ } else {
+ cart.set(item.id, { id: item.id, name: item.name, count: 1 });
+ }
+
+ return Response.json({ ok: true });
+}
diff --git a/examples/ssr/src/pages/api/products.ts b/examples/ssr/src/pages/api/products.ts
new file mode 100644
index 000000000..8bf02a03d
--- /dev/null
+++ b/examples/ssr/src/pages/api/products.ts
@@ -0,0 +1,5 @@
+import { products } from '../../models/db';
+
+export function GET() {
+ return new Response(JSON.stringify(products));
+}
diff --git a/examples/ssr/src/pages/api/products/[id].ts b/examples/ssr/src/pages/api/products/[id].ts
new file mode 100644
index 000000000..f0f6fa89f
--- /dev/null
+++ b/examples/ssr/src/pages/api/products/[id].ts
@@ -0,0 +1,16 @@
+import { productMap } from '../../../models/db';
+import type { APIContext } from 'astro';
+
+export function GET({ params }: APIContext) {
+ const id = Number(params.id);
+ if (productMap.has(id)) {
+ const product = productMap.get(id);
+
+ return new Response(JSON.stringify(product));
+ } else {
+ return new Response(null, {
+ status: 400,
+ statusText: 'Not found',
+ });
+ }
+}
diff --git a/examples/ssr/src/pages/cart.astro b/examples/ssr/src/pages/cart.astro
new file mode 100644
index 000000000..40e5cf126
--- /dev/null
+++ b/examples/ssr/src/pages/cart.astro
@@ -0,0 +1,51 @@
+---
+import Header from '../components/Header.astro';
+import Container from '../components/Container.astro';
+import { getCart } from '../api';
+
+if (!Astro.cookies.get('user-id')) {
+ return Astro.redirect('/');
+}
+
+// They must be logged in.
+
+const user = { name: 'test' }; // getUser?
+const cart = await getCart(Astro.request);
+---
+
+<html lang="en">
+ <head>
+ <title>Cart | Online Store</title>
+ <style>
+ h1 {
+ font-size: 36px;
+ }
+ </style>
+ </head>
+ <body>
+ <Header />
+
+ <Container tag="main">
+ <h1>Cart</h1>
+ <p>Hi {user.name}! Here are your cart items:</p>
+ <table>
+ <thead>
+ <tr>
+ <th>Item</th>
+ <th>Count</th>
+ </tr>
+ </thead>
+ <tbody>
+ {
+ cart.items.map((item) => (
+ <tr>
+ <td>{item.name}</td>
+ <td>{item.count}</td>
+ </tr>
+ ))
+ }
+ </tbody>
+ </table>
+ </Container>
+ </body>
+</html>
diff --git a/examples/ssr/src/pages/index.astro b/examples/ssr/src/pages/index.astro
new file mode 100644
index 000000000..1ce70bc81
--- /dev/null
+++ b/examples/ssr/src/pages/index.astro
@@ -0,0 +1,33 @@
+---
+import Header from '../components/Header.astro';
+import Container from '../components/Container.astro';
+import ProductListing from '../components/ProductListing.astro';
+import { getProducts } from '../api';
+import '../styles/common.css';
+
+const products = await getProducts(Astro.request);
+---
+
+<html lang="en">
+ <head>
+ <title>Online Store</title>
+ <style>
+ h1 {
+ font-size: 36px;
+ }
+
+ .product-listing-title {
+ text-align: center;
+ }
+ </style>
+ </head>
+ <body>
+ <Header />
+
+ <Container tag="main">
+ <ProductListing products={products}>
+ <h2 class="product-listing-title" slot="title">Product Listing</h2>
+ </ProductListing>
+ </Container>
+ </body>
+</html>
diff --git a/examples/ssr/src/pages/login.astro b/examples/ssr/src/pages/login.astro
new file mode 100644
index 000000000..030838a64
--- /dev/null
+++ b/examples/ssr/src/pages/login.astro
@@ -0,0 +1,58 @@
+---
+import Header from '../components/Header.astro';
+import Container from '../components/Container.astro';
+---
+
+<html lang="en">
+ <head>
+ <title>Online Store</title>
+ <style>
+ h1 {
+ font-size: 36px;
+ }
+ </style>
+
+ <script type="module" is:inline>
+ document.addEventListener('DOMContentLoaded', () => {
+ const form = document.querySelector('form');
+ if (!form) throw new Error('Form not found');
+ form.addEventListener('submit', (e) => {
+ e.preventDefault();
+ const formData = new FormData(form);
+ const data = Object.fromEntries(formData);
+
+ fetch('/login.form.async', {
+ method: 'POST',
+ body: JSON.stringify(data),
+ })
+ .then((res) => res.json())
+ .then(() => {
+ const result = document.querySelector('#result');
+ if (result) {
+ result.innerHTML =
+ 'Progressive login was successful! you will be redirected to the store in 3 seconds';
+ setTimeout(() => (location.href = '/'), 3000);
+ }
+ });
+ });
+ });
+ </script>
+ </head>
+ <body>
+ <Header />
+
+ <Container tag="main">
+ <h1>Login</h1>
+ <form action="/login.form" method="POST">
+ <label for="name">Name</label>
+ <input type="text" name="name" />
+
+ <label for="password">Password</label>
+ <input type="password" name="password" />
+
+ <input type="submit" value="Submit" />
+ </form>
+ <div id="result"></div>
+ </Container>
+ </body>
+</html>
diff --git a/examples/ssr/src/pages/login.form.async.ts b/examples/ssr/src/pages/login.form.async.ts
new file mode 100644
index 000000000..94020d9c9
--- /dev/null
+++ b/examples/ssr/src/pages/login.form.async.ts
@@ -0,0 +1,14 @@
+import type { APIContext, APIRoute } from 'astro';
+
+export const POST: APIRoute = ({ cookies }: APIContext) => {
+ // add a new cookie
+ cookies.set('user-id', '1', {
+ path: '/',
+ maxAge: 2592000,
+ });
+
+ return Response.json({
+ ok: true,
+ user: 1,
+ });
+};
diff --git a/examples/ssr/src/pages/login.form.ts b/examples/ssr/src/pages/login.form.ts
new file mode 100644
index 000000000..f3cd50db4
--- /dev/null
+++ b/examples/ssr/src/pages/login.form.ts
@@ -0,0 +1,16 @@
+import type { APIContext } from 'astro';
+
+export function POST({ cookies }: APIContext) {
+ // add a new cookie
+ cookies.set('user-id', '1', {
+ path: '/',
+ maxAge: 2592000,
+ });
+
+ return new Response(null, {
+ status: 301,
+ headers: {
+ Location: '/',
+ },
+ });
+}
diff --git a/examples/ssr/src/pages/products/[id].astro b/examples/ssr/src/pages/products/[id].astro
new file mode 100644
index 000000000..e90900e45
--- /dev/null
+++ b/examples/ssr/src/pages/products/[id].astro
@@ -0,0 +1,45 @@
+---
+import Header from '../../components/Header.astro';
+import Container from '../../components/Container.astro';
+import AddToCart from '../../components/AddToCart.svelte';
+import { getProduct } from '../../api';
+import '../../styles/common.css';
+
+const id = Number(Astro.params.id);
+const product = await getProduct(Astro.request, id);
+---
+
+<html lang="en">
+ <head>
+ <title>{product.name} | Online Store</title>
+ <style>
+ h2 {
+ text-align: center;
+ font-size: 3.5rem;
+ }
+
+ figure {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ }
+
+ img {
+ width: 400px;
+ }
+ </style>
+ </head>
+ <body>
+ <Header />
+
+ <Container tag="article">
+ <h2>{product.name}</h2>
+ <figure>
+ <img src={product.image} />
+ <figcaption>
+ <AddToCart client:idle id={id} name={product.name} />
+ <p>Description here...</p>
+ </figcaption>
+ </figure>
+ </Container>
+ </body>
+</html>
diff --git a/examples/ssr/src/styles/common.css b/examples/ssr/src/styles/common.css
new file mode 100644
index 000000000..9d73ad1a4
--- /dev/null
+++ b/examples/ssr/src/styles/common.css
@@ -0,0 +1,3 @@
+body {
+ font-family: 'GT America Standard', 'Helvetica Neue', Helvetica, Arial, sans-serif;
+}
diff --git a/examples/ssr/tsconfig.json b/examples/ssr/tsconfig.json
new file mode 100644
index 000000000..8bf91d3bb
--- /dev/null
+++ b/examples/ssr/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "extends": "astro/tsconfigs/strict",
+ "include": [".astro/types.d.ts", "**/*"],
+ "exclude": ["dist"]
+}
diff --git a/examples/starlog/.gitignore b/examples/starlog/.gitignore
new file mode 100644
index 000000000..16d54bb13
--- /dev/null
+++ b/examples/starlog/.gitignore
@@ -0,0 +1,24 @@
+# build output
+dist/
+# generated types
+.astro/
+
+# dependencies
+node_modules/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
+
+# jetbrains setting folder
+.idea/
diff --git a/examples/starlog/README.md b/examples/starlog/README.md
new file mode 100644
index 000000000..da5cb142c
--- /dev/null
+++ b/examples/starlog/README.md
@@ -0,0 +1,7 @@
+# Starlog
+
+## Release notes theme for Astro
+
+![starlog-gh](https://github.com/doodlemarks/starlog/assets/2244813/9c5c2e46-665a-437e-a971-053db4dbff63)
+
+Built with Astro and Sass. Supports both dark and light modes.
diff --git a/examples/starlog/astro.config.mjs b/examples/starlog/astro.config.mjs
new file mode 100644
index 000000000..759bb082c
--- /dev/null
+++ b/examples/starlog/astro.config.mjs
@@ -0,0 +1,7 @@
+// @ts-check
+import { defineConfig } from 'astro/config';
+
+// https://astro.build/config
+export default defineConfig({
+ site: 'https://example.com',
+});
diff --git a/examples/starlog/package.json b/examples/starlog/package.json
new file mode 100644
index 000000000..51561ab3e
--- /dev/null
+++ b/examples/starlog/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "@example/starlog",
+ "type": "module",
+ "version": "0.0.1",
+ "scripts": {
+ "dev": "astro dev",
+ "build": "astro build",
+ "preview": "astro preview",
+ "astro": "astro"
+ },
+ "dependencies": {
+ "astro": "^5.9.0",
+ "sass": "^1.86.3",
+ "sharp": "^0.33.3"
+ }
+}
diff --git a/examples/starlog/public/favicon.svg b/examples/starlog/public/favicon.svg
new file mode 100644
index 000000000..f157bd1c5
--- /dev/null
+++ b/examples/starlog/public/favicon.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
+ <path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
+ <style>
+ path { fill: #000; }
+ @media (prefers-color-scheme: dark) {
+ path { fill: #FFF; }
+ }
+ </style>
+</svg>
diff --git a/examples/starlog/src/assets/starlog-placeholder-1.jpg b/examples/starlog/src/assets/starlog-placeholder-1.jpg
new file mode 100644
index 000000000..846bdcd45
--- /dev/null
+++ b/examples/starlog/src/assets/starlog-placeholder-1.jpg
Binary files differ
diff --git a/examples/starlog/src/assets/starlog-placeholder-14.jpg b/examples/starlog/src/assets/starlog-placeholder-14.jpg
new file mode 100644
index 000000000..e2e0acb9e
--- /dev/null
+++ b/examples/starlog/src/assets/starlog-placeholder-14.jpg
Binary files differ
diff --git a/examples/starlog/src/assets/starlog-placeholder-18.jpg b/examples/starlog/src/assets/starlog-placeholder-18.jpg
new file mode 100644
index 000000000..211c85cf5
--- /dev/null
+++ b/examples/starlog/src/assets/starlog-placeholder-18.jpg
Binary files differ
diff --git a/examples/starlog/src/assets/starlog-placeholder-2.jpg b/examples/starlog/src/assets/starlog-placeholder-2.jpg
new file mode 100644
index 000000000..2251c1c49
--- /dev/null
+++ b/examples/starlog/src/assets/starlog-placeholder-2.jpg
Binary files differ
diff --git a/examples/starlog/src/components/BaseHead.astro b/examples/starlog/src/components/BaseHead.astro
new file mode 100644
index 000000000..901b042cf
--- /dev/null
+++ b/examples/starlog/src/components/BaseHead.astro
@@ -0,0 +1,21 @@
+---
+import { ClientRouter } from 'astro:transitions';
+import SEO, { type Props as SEOProps } from './SEO.astro';
+import { SiteTitle, SiteDescription } from '../consts';
+
+export type Props = Partial<SEOProps>;
+const { title = SiteTitle, name = SiteTitle, description = SiteDescription, ...seo } = Astro.props;
+---
+
+<meta charset="utf-8" />
+<meta name="viewport" content="width=device-width,initial-scale=1.0" />
+<SEO {title} {description} {name} {...seo} />
+
+<link rel="preconnect" href="https://fonts.googleapis.com" />
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
+<link
+ href="https://fonts.googleapis.com/css2?family=Lato:wght@400;700&family=Source+Code+Pro&display=swap"
+ rel="stylesheet"
+/>
+
+<ClientRouter />
diff --git a/examples/starlog/src/components/Footer.astro b/examples/starlog/src/components/Footer.astro
new file mode 100644
index 000000000..276550d4a
--- /dev/null
+++ b/examples/starlog/src/components/Footer.astro
@@ -0,0 +1,12 @@
+---
+import '../styles/global.scss';
+---
+
+<footer>
+ <p>© 2023</p>
+ <div class="footer_links">
+ <a href="#">Discord</a>
+ <a href="#">X</a>
+ <a href="#">GitHub</a>
+ </div>
+</footer>
diff --git a/examples/starlog/src/components/FormattedDate.astro b/examples/starlog/src/components/FormattedDate.astro
new file mode 100644
index 000000000..377286d9f
--- /dev/null
+++ b/examples/starlog/src/components/FormattedDate.astro
@@ -0,0 +1,25 @@
+---
+import type { HTMLAttributes } from 'astro/types';
+
+type Props = HTMLAttributes<'time'> & {
+ date: Date;
+};
+
+const { date, ...attrs } = Astro.props;
+---
+
+<time datetime={date.toISOString()} {...attrs}>
+ {
+ date.toLocaleDateString('en-us', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ })
+ }
+</time>
+
+<style>
+ time {
+ display: block;
+ }
+</style>
diff --git a/examples/starlog/src/components/Header.astro b/examples/starlog/src/components/Header.astro
new file mode 100644
index 000000000..bbdaf1e70
--- /dev/null
+++ b/examples/starlog/src/components/Header.astro
@@ -0,0 +1,54 @@
+---
+import '../styles/global.scss';
+import { SiteTitle } from '../consts';
+---
+
+<header>
+ <nav>
+ <h2 id="site_title">
+ <a href="/">
+ <svg
+ aria-hidden="true"
+ xmlns="http://www.w3.org/2000/svg"
+ width="24"
+ height="24"
+ fill="none"
+ ><path
+ fill="url(#a)"
+ fill-rule="evenodd"
+ d="M.654 3.276C0 4.56 0 6.24 0 9.6v4.8c0 3.36 0 5.04.654 6.324a6 6 0 0 0 2.622 2.622C4.56 24 6.24 24 9.6 24h4.8c3.36 0 5.04 0 6.324-.654a6 6 0 0 0 2.622-2.622C24 19.44 24 17.76 24 14.4V9.6c0-3.36 0-5.04-.654-6.324A6 6 0 0 0 20.724.654C19.44 0 17.76 0 14.4 0H9.6C6.24 0 4.56 0 3.276.654A6 6 0 0 0 .654 3.276Zm10.875 16.41a.5.5 0 0 0 .942 0l.628-1.754a8 8 0 0 1 4.833-4.833l1.754-.628a.5.5 0 0 0 0-.942l-1.754-.628A8 8 0 0 1 13.1 6.068l-.628-1.754a.5.5 0 0 0-.942 0l-.628 1.754A8 8 0 0 1 6.068 10.9l-1.754.628a.5.5 0 0 0 0 .942l1.754.628a8 8 0 0 1 4.833 4.833l.628 1.754Z"
+ clip-rule="evenodd"></path><path
+ stroke="url(#b)"
+ stroke-opacity=".5"
+ stroke-width=".5"
+ d="M.25 9.6c0-1.684 0-2.932.08-3.92.081-.985.24-1.69.547-2.29A5.75 5.75 0 0 1 3.39.877C3.99.57 4.695.41 5.68.33 6.668.25 7.916.25 9.6.25h4.8c1.684 0 2.932 0 3.92.08.985.081 1.69.24 2.29.547a5.75 5.75 0 0 1 2.513 2.513c.306.6.466 1.305.546 2.29.08.988.081 2.236.081 3.92v4.8c0 1.684 0 2.932-.08 3.92-.081.985-.24 1.69-.547 2.29a5.75 5.75 0 0 1-2.513 2.513c-.6.306-1.305.466-2.29.546-.988.08-2.236.081-3.92.081H9.6c-1.684 0-2.932 0-3.92-.08-.985-.081-1.69-.24-2.29-.547A5.75 5.75 0 0 1 .877 20.61C.57 20.01.41 19.305.33 18.32.25 17.332.25 16.084.25 14.4V9.6Zm11.044 10.17c.237.663 1.175.663 1.412 0l.628-1.753a7.75 7.75 0 0 1 4.683-4.683l1.753-.628c.663-.237.663-1.175 0-1.412l-1.753-.628a7.75 7.75 0 0 1-4.683-4.683l-.628-1.753c-.237-.663-1.175-.663-1.412 0l-.628 1.753a7.75 7.75 0 0 1-4.683 4.683l-1.753.628c-.663.237-.663 1.175 0 1.412l1.753.628a7.75 7.75 0 0 1 4.683 4.683l.628 1.753Z"
+ ></path><defs
+ ><radialGradient
+ id="a"
+ cx="0"
+ cy="0"
+ r="1"
+ gradientTransform="rotate(-40.136 32.164 11.75) scale(33.3542)"
+ gradientUnits="userSpaceOnUse"
+ ><stop offset=".639" stop-color="#9818E7"></stop><stop offset="1" stop-color="#DF7F4F"
+ ></stop></radialGradient
+ ><linearGradient id="b" x1="12" x2="12" y1="0" y2="24" gradientUnits="userSpaceOnUse"
+ ><stop stop-color="#fff"></stop><stop offset="1" stop-color="#fff" stop-opacity="0"
+ ></stop></linearGradient
+ ></defs
+ ></svg
+ >
+ {SiteTitle}
+ </a>
+ </h2>
+ <div class="links">
+ <a href="mailto:contactus@yourwebsite.com">Contact</a>
+ </div>
+ </nav>
+</header>
+
+<style>
+ .links a {
+ text-decoration: none;
+ }
+</style>
diff --git a/examples/starlog/src/components/SEO.astro b/examples/starlog/src/components/SEO.astro
new file mode 100644
index 000000000..1e2ddcca2
--- /dev/null
+++ b/examples/starlog/src/components/SEO.astro
@@ -0,0 +1,87 @@
+---
+import type { ImageMetadata } from 'astro';
+type Image = {
+ src: string | ImageMetadata;
+ alt: string;
+};
+
+type SEOMetadata = {
+ name: string;
+ title: string;
+ description: string;
+ image?: Image | undefined;
+ canonicalURL?: URL | string | undefined;
+ locale?: string;
+};
+
+type OpenGraph = Partial<SEOMetadata> & {
+ type?: string;
+};
+
+type Twitter = Partial<SEOMetadata> & {
+ handle?: string;
+ card?: 'summary' | 'summary_large_image';
+};
+
+export type Props = SEOMetadata & {
+ og?: OpenGraph;
+ twitter?: Twitter;
+};
+
+const {
+ name,
+ title,
+ description,
+ image,
+ locale = 'en',
+ canonicalURL = new URL(Astro.url.pathname, Astro.site),
+} = Astro.props;
+
+const og = {
+ name,
+ title,
+ description,
+ canonicalURL,
+ image,
+ locale,
+ type: 'website',
+ ...(Astro.props.og ?? {}),
+} satisfies OpenGraph;
+
+const twitter = {
+ name,
+ title,
+ description,
+ canonicalURL,
+ image,
+ locale,
+ card: 'summary_large_image',
+ ...Astro.props.twitter,
+};
+
+function normalizeImageUrl(image: string | ImageMetadata) {
+ return typeof image === 'string' ? image : image.src;
+}
+---
+
+<!-- Page Metadata -->
+<link rel="canonical" href={canonicalURL} />
+<meta name="description" content={description} />
+
+<!-- OpenGraph Tags -->
+<meta property="og:title" content={og.title} />
+<meta property="og:type" content={og.type} />
+<meta property="og:url" content={canonicalURL} />
+<meta property="og:locale" content={og.locale} />
+<meta property="og:description" content={og.description} />
+<meta property="og:site_name" content={og.name} />
+{og.image && <meta property="og:image" content={normalizeImageUrl(og.image.src)} />}
+{og.image && <meta property="og:image:alt" content={og.image.alt} />}
+
+<!-- Twitter Tags -->
+<meta name="twitter:card" content={twitter.card} />
+<meta name="twitter:site" content={twitter.handle} />
+<meta name="twitter:title" content={twitter.title} />
+<meta name="twitter:description" content={twitter.description} />
+{twitter.image && <meta name="twitter:image" content={normalizeImageUrl(twitter.image.src)} />}
+{twitter.image && <meta name="twitter:image:alt" content={twitter.image.alt} />}
diff --git a/examples/starlog/src/consts.ts b/examples/starlog/src/consts.ts
new file mode 100644
index 000000000..3e91752e1
--- /dev/null
+++ b/examples/starlog/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 SiteTitle = 'Starlog';
+export const SiteDescription = 'Welcome to my website!';
diff --git a/examples/starlog/src/content.config.ts b/examples/starlog/src/content.config.ts
new file mode 100644
index 000000000..26986525a
--- /dev/null
+++ b/examples/starlog/src/content.config.ts
@@ -0,0 +1,22 @@
+import { glob } from 'astro/loaders';
+import { defineCollection, z } from 'astro:content';
+
+const releases = defineCollection({
+ // Load Markdown files in the src/content/releases directory.
+ loader: glob({ base: './src/content/releases', pattern: '**/*.md' }),
+ // Type-check frontmatter using a schema
+ schema: ({ image }) =>
+ z.object({
+ title: z.string(),
+ description: z.string(),
+ versionNumber: z.string(),
+ image: z.object({
+ src: image(),
+ alt: z.string(),
+ }),
+ // Transform string to Date object
+ date: z.date({ coerce: true }),
+ }),
+});
+
+export const collections = { releases };
diff --git a/examples/starlog/src/content/releases/1_0.md b/examples/starlog/src/content/releases/1_0.md
new file mode 100644
index 000000000..a5eeff437
--- /dev/null
+++ b/examples/starlog/src/content/releases/1_0.md
@@ -0,0 +1,29 @@
+---
+title: 'Introducing Nebulous 1.0!'
+date: '2022-03-21'
+versionNumber: '1.0'
+description: 'This is the first post of my new Astro blog.'
+image:
+ src: '../../assets/starlog-placeholder-1.jpg'
+ alt: 'The full Astro logo.'
+---
+
+## A New World with 1.0
+
+![Nebulous 2.0 Release](../../assets/starlog-placeholder-1.jpg)
+
+Hey there, Nebulous users! We're back with some exciting updates that will turbocharge your Nebulous experience. Here's the lowdown:
+
+### 🍿 New Features & Enhancements
+
+- **NebulaProtect Supercharged:** Enjoy beefed-up security and real-time monitoring to keep your digital fortress unbreachable.
+- **NebulaConnect for Teams:** Collaboration is a breeze with integrated project management tools.
+- **Speed Boost Galore:** We've fine-tuned Nebulous for ultimate speed and responsiveness.
+
+### 🐞 Bug Fixes
+
+- Kicked pesky crashes out the door for NebulaSync.
+- Fixed rare data hiccups during file transfers.
+- Nebulous is now even friendly with older devices.
+
+Thank you for making Nebulous your tech partner. We thrive on your feedback, so if you have ideas or run into bumps, don't hesitate to drop a line to our support wizards. Together, we're taking Nebulous to the next level!
diff --git a/examples/starlog/src/content/releases/1_4.md b/examples/starlog/src/content/releases/1_4.md
new file mode 100644
index 000000000..aacab8a84
--- /dev/null
+++ b/examples/starlog/src/content/releases/1_4.md
@@ -0,0 +1,29 @@
+---
+title: 'Introducing Nebulous 1.8!'
+date: '2022-04-16'
+versionNumber: '1.4'
+description: 'This is the first post of my new Astro blog.'
+image:
+ src: '../../assets/starlog-placeholder-14.jpg'
+ alt: 'The full Astro logo.'
+---
+
+## Go further with 1.4
+
+![Nebulous 1.4 Release](../../assets/starlog-placeholder-14.jpg)
+
+Hello, Nebulous enthusiasts! It's that time again—time for us to unveil the latest and greatest in our tech universe. Buckle up as we introduce you to the future of Nebulous:
+
+### 🍿 New Features & Enhancements
+
+- **NebulaSync Quantum:** Prepare for a mind-blowing file syncing experience. It's faster, smarter, and more intuitive than ever before.
+- **NebulaAI Odyssey:** Welcome to the era of NebulaAI Odyssey—a journey into the boundless possibilities of artificial intelligence. From image manipulation to language translation, Odyssey empowers you like never before.
+
+### 🐞 Bug Fixes
+
+- Squashed even more bugs, making NebulaSync and other features more reliable than ever.
+- Streamlined data transfer processes for flawless file exchanges.
+- Extended support for older devices to ensure everyone enjoys Nebulous.
+- Elevating error handling to the next level, ensuring a hiccup-free experience.
+
+Thank you for being a part of the Nebulous journey. Your feedback fuels our innovation, so don't hesitate to share your thoughts or report any hiccups with our dedicated support team. Together, we're shaping the future of tech with Nebulous!
diff --git a/examples/starlog/src/content/releases/1_8.md b/examples/starlog/src/content/releases/1_8.md
new file mode 100644
index 000000000..d300f964c
--- /dev/null
+++ b/examples/starlog/src/content/releases/1_8.md
@@ -0,0 +1,29 @@
+---
+title: 'Introducing Nebulous 1.8!'
+date: '2022-06-01'
+versionNumber: '1.8'
+description: 'This is the first post of my new Astro blog.'
+image:
+ src: '../../assets/starlog-placeholder-18.jpg'
+ alt: 'The full Astro logo.'
+---
+
+## Faster, Stronger, Betterer
+
+![Nebulous 2.0 Release](../../assets/starlog-placeholder-18.jpg)
+
+Hey there, Nebulous users! We're back with some exciting updates that will turbocharge your Nebulous experience. Here's the lowdown:
+
+### New Features & Enhancements
+
+- **NebulaProtect Supercharged:** Enjoy beefed-up security and real-time monitoring to keep your digital fortress unbreachable.
+- **NebulaConnect for Teams:** Collaboration is a breeze with integrated project management tools.
+- **Speed Boost Galore:** We've fine-tuned Nebulous for ultimate speed and responsiveness.
+
+### 🐞 Bug Fixes
+
+- Kicked pesky crashes out the door for NebulaSync.
+- Fixed rare data hiccups during file transfers.
+- Nebulous is now even friendly with older devices.
+
+Thank you for making Nebulous your tech partner. We thrive on your feedback, so if you have ideas or run into bumps, don't hesitate to drop a line to our support wizards. Together, we're taking Nebulous to the next level!
diff --git a/examples/starlog/src/content/releases/2_0.md b/examples/starlog/src/content/releases/2_0.md
new file mode 100644
index 000000000..6a01d76fc
--- /dev/null
+++ b/examples/starlog/src/content/releases/2_0.md
@@ -0,0 +1,38 @@
+---
+title: 'Introducing Nebulous 2.0!'
+date: '2022-07-01'
+versionNumber: '2.0'
+description: 'This is the first post of my new Astro blog.'
+image:
+ src: '../../assets/starlog-placeholder-2.jpg'
+ alt: 'The full Astro logo.'
+---
+
+## Introducing Nebulous 2.0!
+
+![Nebulous 2.0 Release](../../assets/starlog-placeholder-2.jpg)
+
+Greetings, Nebulous users! We're excited to bring you the latest updates in our [ever-evolving tech ecosystem](#). In this release, we're introducing some exciting new features and squashing a few pesky bugs. Let's dive in!
+
+### 🍿 New Features & Enhancements
+
+- **NebulaSync v2.0:** We're thrilled to introduce NebulaSync 2.0, our revamped file synchronization tool. It now offers blazing-fast sync speeds, improved reliability, and enhanced cross-device compatibility.
+- **Enhanced NebulaProtect:** NebulaProtect, our comprehensive security suite, has received a major update. Enjoy advanced threat detection, and real-time monitoring.
+- **NebulaConnect for Teams:** Collaborate effortlessly with NebulaConnect for Teams. This powerful feature allows seamless integration with your favorite project management tools, enabling you to manage tasks, share documents, and track progress in real-time.
+
+### 🐞 Bug Fixes
+
+- Resolved occasional crashing issues when using NebulaSync.
+- Fixed a bug causing data corruption in rare cases during file transfers.
+- Improved compatibility with older devices to ensure a seamless experience for all users.
+- Enhanced error handling and reporting for a smoother user experience.
+
+### 👀 Coming Soon
+
+We can't spill all the beans just yet, but we're thrilled to give you a sneak peek of what's coming in the next Nebulous release:
+
+- **NebulaWallet:** A secure and user-friendly cryptocurrency wallet integrated directly into Nebulous for seamless digital asset management.
+- **NebulaConnect Mobile:** Take your collaboration to the next level with our upcoming mobile app, enabling you to work on the go.
+- **NebulaLabs:** Our developer tools and API enhancements, providing you with even more customization options and possibilities.
+
+If you have any suggestions or encounter any issues, don't hesitate to reach out to our support team. Together, we'll continue to make Nebulous the ultimate tech solution for you.
diff --git a/examples/starlog/src/layouts/IndexLayout.astro b/examples/starlog/src/layouts/IndexLayout.astro
new file mode 100644
index 000000000..117c57f0b
--- /dev/null
+++ b/examples/starlog/src/layouts/IndexLayout.astro
@@ -0,0 +1,22 @@
+---
+import BaseHead, { type Props as HeadProps } from '../components/BaseHead.astro';
+import Header from '../components/Header.astro';
+import Footer from '../components/Footer.astro';
+
+type Props = HeadProps;
+
+const { ...head } = Astro.props;
+---
+
+<!doctype html>
+<html lang="en">
+ <head>
+ <BaseHead {...head} />
+ </head>
+ <body>
+ <div class="glow"></div>
+ <Header />
+ <slot />
+ <Footer />
+ </body>
+</html>
diff --git a/examples/starlog/src/layouts/PostLayout.astro b/examples/starlog/src/layouts/PostLayout.astro
new file mode 100644
index 000000000..9c87a7b88
--- /dev/null
+++ b/examples/starlog/src/layouts/PostLayout.astro
@@ -0,0 +1,39 @@
+---
+import type { CollectionEntry } from 'astro:content';
+import BaseHead from '../components/BaseHead.astro';
+import FormattedDate from '../components/FormattedDate.astro';
+import Header from '../components/Header.astro';
+import Footer from '../components/Footer.astro';
+
+type Props = {
+ release: CollectionEntry<'releases'>;
+};
+
+const { release } = Astro.props;
+---
+
+<!doctype html>
+<html lang="en">
+ <head>
+ <BaseHead
+ title={release.data.title}
+ description={release.data.description}
+ image={release.data.image}
+ />
+ </head><body>
+ <div class="glow"></div>
+ <Header />
+ <div class="post single" transition:persist transition:name="post">
+ <div class="version_wrapper">
+ <div class="version_info">
+ <div class="version_number">{release.data.versionNumber}</div>
+ <FormattedDate class="date" date={release.data.date} />
+ </div>
+ </div>
+ <div class="content">
+ <slot />
+ </div>
+ </div>
+ <Footer />
+ </body>
+</html>
diff --git a/examples/starlog/src/pages/index.astro b/examples/starlog/src/pages/index.astro
new file mode 100644
index 000000000..3cf04af62
--- /dev/null
+++ b/examples/starlog/src/pages/index.astro
@@ -0,0 +1,36 @@
+---
+import { getCollection, render } from 'astro:content';
+import FormattedDate from '../components/FormattedDate.astro';
+import Layout from '../layouts/IndexLayout.astro';
+
+const posts = await getCollection('releases');
+posts.sort((a, b) => +b.data.date - +a.data.date);
+---
+
+<Layout>
+ <main>
+ <h1 class="page_title">Changelog</h1>
+ <hr />
+ <ul class="posts" transition:name="post">
+ {
+ posts.map((post) => (
+ <li class="post">
+ <div class="version_wrapper">
+ <div class="version_info">
+ <a href={`/releases/${post.id}`}>
+ <div class="version_number">{post.data.versionNumber}</div>
+ <FormattedDate class="date" date={post.data.date} />
+ </a>
+ </div>
+ </div>
+ <div class="content">
+ {render(post).then(({ Content }) => (
+ <Content />
+ ))}
+ </div>
+ </li>
+ ))
+ }
+ </ul>
+ </main>
+</Layout>
diff --git a/examples/starlog/src/pages/releases/[slug].astro b/examples/starlog/src/pages/releases/[slug].astro
new file mode 100644
index 000000000..8c3119a8f
--- /dev/null
+++ b/examples/starlog/src/pages/releases/[slug].astro
@@ -0,0 +1,21 @@
+---
+import { getCollection, render } from 'astro:content';
+import Layout from '../../layouts/PostLayout.astro';
+
+export async function getStaticPaths() {
+ const releases = await getCollection('releases');
+
+ return releases.map((release) => ({
+ params: { slug: release.id },
+ props: { release },
+ }));
+}
+
+const { release } = Astro.props;
+
+const { Content } = await render(release);
+---
+
+<Layout {release}>
+ <Content />
+</Layout>
diff --git a/examples/starlog/src/styles/colors.scss b/examples/starlog/src/styles/colors.scss
new file mode 100644
index 000000000..43b9ee957
--- /dev/null
+++ b/examples/starlog/src/styles/colors.scss
@@ -0,0 +1,63 @@
+@use 'sass:map';
+
+@function color($color, $tone) {
+ // @warn map.get($palette,$color);
+
+ @if map.has-key($palette, $color) {
+ $color: map.get($palette, $color);
+
+ @if map.has-key($color, $tone) {
+ $tone: map.get($color, $tone);
+ @return $tone;
+ }
+
+ @warn "unknown tone `#{$tone}` in color";
+ @return null;
+ }
+
+ @warn "unknown color `#{$color}` in palette";
+ @return null;
+}
+
+$white: #ffffff;
+$palette: (
+ purple: (
+ 50: #f2e8fd,
+ 100: #e6d1fa,
+ 200: #cfa3f5,
+ 300: #ba75f0,
+ 400: #a846ec,
+ 500: #9818e7,
+ 600: #7b13b4,
+ 700: #5b0e81,
+ 800: #3a084e,
+ 900: #15031c,
+ 950: #020002,
+ ),
+ orange: (
+ 50: #fbf0ea,
+ 100: #f8e3d9,
+ 200: #f2cab7,
+ 300: #ecb194,
+ 400: #e59872,
+ 500: #df7f4f,
+ 600: #d05f26,
+ 700: #a1491d,
+ 800: #713315,
+ 900: #421e0c,
+ 950: #2a1308,
+ ),
+ gray: (
+ 50: #f6f6f9,
+ 100: #e6e7ef,
+ 200: #c7c9db,
+ 300: #a8abc7,
+ 400: #898eb4,
+ 500: #6a71a0,
+ 600: #545b83,
+ 700: #404664,
+ 800: #2c3145,
+ 900: #181b26,
+ 950: #0e1016,
+ ),
+);
diff --git a/examples/starlog/src/styles/global.scss b/examples/starlog/src/styles/global.scss
new file mode 100644
index 000000000..39b21e91b
--- /dev/null
+++ b/examples/starlog/src/styles/global.scss
@@ -0,0 +1,3 @@
+@use 'colors.scss';
+@use 'type.scss';
+@use 'layout.scss';
diff --git a/examples/starlog/src/styles/layout.scss b/examples/starlog/src/styles/layout.scss
new file mode 100644
index 000000000..3a5f8d173
--- /dev/null
+++ b/examples/starlog/src/styles/layout.scss
@@ -0,0 +1,295 @@
+@use 'sass:color';
+@use './colors.scss' as colors;
+@use './type.scss' as type;
+
+$container: 1040px;
+$tablet: 768px;
+$mobile: 420px;
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0 auto;
+ padding: 0 1em;
+ width: 1040px;
+ max-width: 100%;
+ background-color: colors.$white;
+ @media (prefers-color-scheme: dark) {
+ background-color: colors.color(gray, 950);
+ }
+ @media (max-width: $tablet) {
+ font-size: 16px;
+ }
+}
+
+.glow {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ z-index: -1;
+ top: 0;
+ left: 0;
+ overflow: hidden;
+
+ &:after {
+ content: '';
+ display: block;
+ position: absolute;
+ top: -120px;
+ left: calc(50% - 360px);
+ width: 720px;
+ height: 240px;
+ background: radial-gradient(
+ 50% 50% at 50% 50%,
+ rgba(colors.color(orange, 500), 0.2) 0%,
+ rgba(colors.color(orange, 500), 0) 100%
+ );
+ @media (prefers-color-scheme: dark) {
+ background: radial-gradient(
+ 50% 50% at 50% 50%,
+ rgba(255, 255, 255, 0.06) 0%,
+ rgba(255, 255, 255, 0) 100%
+ );
+ }
+ }
+}
+
+::selection {
+ background: colors.color(orange, 200);
+ @media (prefers-color-scheme: dark) {
+ background: colors.color(orange, 600);
+ }
+}
+
+a,
+a:visited {
+ color: colors.color(orange, 600);
+ transition: 0.1s ease;
+ @media (prefers-color-scheme: dark) {
+ color: colors.color(orange, 300);
+ }
+
+ &:hover {
+ color: colors.color(orange, 500);
+ }
+}
+
+hr {
+ margin: 1em 0;
+ border: 0;
+ border-bottom: 1px solid colors.color(gray, 100);
+ @media (prefers-color-scheme: dark) {
+ border-color: colors.color(gray, 900);
+ }
+}
+
+nav {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin: 0 0 2em 0;
+ padding: 2em 0;
+
+ a {
+ transition: 0.1s ease;
+ &:hover {
+ opacity: 0.6;
+ }
+ }
+
+ #site_title {
+ margin: 0;
+ }
+ #site_title a {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ color: colors.color(gray, 950);
+ font-size: 16px;
+ font-weight: 700;
+ letter-spacing: 2px;
+ line-height: 1;
+ text-decoration: none;
+ text-transform: uppercase;
+ @media (prefers-color-scheme: dark) {
+ color: colors.$white;
+ }
+ }
+ .links a {
+ margin-left: 1em;
+ color: colors.color(gray, 800);
+ @media (prefers-color-scheme: dark) {
+ color: colors.color(gray, 200);
+ }
+ }
+}
+
+.content {
+ ol,
+ ul {
+ padding-left: 2em;
+ margin-bottom: 1em;
+ }
+
+ ul {
+ list-style: none;
+
+ li {
+ position: relative;
+ margin-bottom: 0.75em;
+
+ &:before {
+ content: '';
+ display: block;
+ position: absolute;
+ left: -1em;
+ top: 0.63em;
+ width: 8px;
+ height: 8px;
+ background: linear-gradient(25deg, colors.color(purple, 500), colors.color(orange, 500));
+ border-radius: 99px;
+ }
+ }
+ }
+}
+
+.page_title {
+ margin: 1.5em 0;
+ @media (max-width: $tablet) {
+ margin: 0.5em 0;
+ }
+}
+
+.posts {
+ list-style: none;
+ padding: 0;
+}
+
+.post {
+ display: flex;
+ width: 100%;
+ @media (max-width: $tablet) {
+ flex-flow: column;
+ }
+
+ &:last-child .content,
+ &.single .content {
+ border-bottom: 0;
+ }
+}
+
+.version_wrapper {
+ flex-basis: 260px;
+ flex-grow: 0;
+ flex-shrink: 0;
+ margin: 4.5em 0 0 0;
+ @media (max-width: $container) {
+ flex-basis: 140px;
+ }
+ @media (max-width: $tablet) {
+ flex-basis: 0;
+ margin-top: 2em;
+ }
+
+ .version_info {
+ position: sticky;
+ top: 1em;
+ @media (max-width: $tablet) {
+ position: relative;
+ top: 0;
+ }
+ }
+
+ a {
+ float: left;
+ color: colors.$white;
+ text-decoration: none;
+ transition: 0.1s ease;
+
+ &:hover {
+ opacity: 0.6;
+ }
+ }
+}
+
+.version_number {
+ display: inline-block;
+ font-family: type.$codeFont;
+ line-height: 1;
+ margin-bottom: 8px;
+ padding: 4px 12px;
+ color: colors.$white;
+ background: linear-gradient(
+ 25deg,
+ colors.color(purple, 800),
+ colors.color(purple, 700),
+ color.mix(colors.color(purple, 500), colors.color(orange, 500)),
+ colors.color(orange, 500)
+ );
+ border-radius: 8px;
+}
+
+.date {
+ clear: both;
+ color: colors.color(gray, 800);
+ font-family: type.$codeFont;
+ font-size: type.$fontSizeSmall;
+ @media (max-width: $tablet) {
+ display: inline;
+ margin-left: 1em;
+ }
+ @media (prefers-color-scheme: dark) {
+ color: colors.color(gray, 200);
+ }
+}
+
+.content {
+ margin: 0;
+ padding: 4em 0;
+ border-bottom: 1px solid colors.color(gray, 100);
+ @media (max-width: $tablet) {
+ margin: 1em 0;
+ padding: 0 0 2em 0;
+ }
+ @media (prefers-color-scheme: dark) {
+ border-color: colors.color(gray, 900);
+ }
+ *:first-child {
+ margin-top: 0;
+ }
+ img {
+ max-width: 100%;
+ height: auto;
+ border-radius: 12px;
+ border: 1px solid colors.color(gray, 200);
+ @media (prefers-color-scheme: dark) {
+ border-color: colors.color(gray, 800);
+ }
+ }
+}
+
+footer {
+ display: flex;
+ padding: 2em 0;
+ color: colors.color(gray, 500);
+ justify-content: space-between;
+ border-top: 1px solid colors.color(gray, 100);
+ @media (max-width: $tablet) {
+ padding: 1em 0;
+ }
+ @media (prefers-color-scheme: dark) {
+ border-color: colors.color(gray, 900);
+ }
+
+ a {
+ margin-left: 1em;
+ color: colors.color(gray, 500);
+ text-decoration: none;
+ &:hover {
+ color: colors.color(gray, 500);
+ opacity: 0.6;
+ }
+ }
+}
diff --git a/examples/starlog/src/styles/type.scss b/examples/starlog/src/styles/type.scss
new file mode 100644
index 000000000..0bb5a86d1
--- /dev/null
+++ b/examples/starlog/src/styles/type.scss
@@ -0,0 +1,70 @@
+@use './colors.scss' as colors;
+
+$baseFont: 'Lato', sans-serif;
+$codeFont: 'Source Code Pro', monospace;
+$fontSizeSmall: 15px;
+
+body {
+ font-family: $baseFont;
+ font-size: 18px;
+ line-height: 1.65;
+ font-weight: 400;
+ color: colors.color(gray, 800);
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ text-rendering: optimizeLegibility;
+
+ @media (prefers-color-scheme: dark) {
+ color: colors.color(gray, 200);
+ }
+}
+
+h1,
+h2,
+h3,
+h4,
+h5 {
+ line-height: 1.2;
+ margin: 1em 0 0.5em 0;
+ color: colors.color(gray, 950);
+ font-weight: 700;
+
+ @media (prefers-color-scheme: dark) {
+ color: colors.$white;
+ }
+}
+
+h1 {
+ font-size: 3.052em;
+}
+h2 {
+ font-size: 2.441em;
+}
+h3 {
+ font-size: 1.953em;
+}
+h4 {
+ font-size: 1.563em;
+}
+h5 {
+ font-size: 1.25em;
+}
+
+p {
+ margin: 0 0 1em 0;
+}
+
+code {
+ font-family: $codeFont;
+}
+
+b,
+strong {
+ font-weight: 700;
+ color: #fff;
+ color: colors.color(gray, 950);
+
+ @media (prefers-color-scheme: dark) {
+ color: colors.$white;
+ }
+}
diff --git a/examples/starlog/tsconfig.json b/examples/starlog/tsconfig.json
new file mode 100644
index 000000000..8bf91d3bb
--- /dev/null
+++ b/examples/starlog/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "extends": "astro/tsconfigs/strict",
+ "include": [".astro/types.d.ts", "**/*"],
+ "exclude": ["dist"]
+}
diff --git a/examples/toolbar-app/.codesandbox/Dockerfile b/examples/toolbar-app/.codesandbox/Dockerfile
new file mode 100644
index 000000000..c3b5c81a1
--- /dev/null
+++ b/examples/toolbar-app/.codesandbox/Dockerfile
@@ -0,0 +1 @@
+FROM node:18-bullseye
diff --git a/examples/toolbar-app/.gitignore b/examples/toolbar-app/.gitignore
new file mode 100644
index 000000000..abe03335e
--- /dev/null
+++ b/examples/toolbar-app/.gitignore
@@ -0,0 +1,21 @@
+# dependencies
+node_modules/
+
+# production build
+dist
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
+
+# jetbrains setting folder
+.idea/
diff --git a/examples/toolbar-app/README.md b/examples/toolbar-app/README.md
new file mode 100644
index 000000000..8b879a01e
--- /dev/null
+++ b/examples/toolbar-app/README.md
@@ -0,0 +1,40 @@
+# Astro Starter Kit: Toolbar App
+
+```sh
+npm create astro@latest -- --template toolbar-app
+```
+
+[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/toolbar-app)
+[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/toolbar-app)
+[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/toolbar-app/devcontainer.json)
+
+> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
+
+## 🚀 Project Structure
+
+Inside of your Astro project, you'll see the following folders and files:
+
+```text
+/
+├── app.ts
+├── integration.ts
+└── package.json
+```
+
+The logic of your app is in the appropriately named `app.ts` file. This is where the vast majority of your toolbar app logic will live.
+
+The `integration.ts` file is a simple Astro integration file that will be used to add your app into the toolbar.
+
+## 🧞 Commands
+
+All commands are run from the root of the project, from a terminal:
+
+| Command | Action |
+| :-------------- | :------------------------------------------------- |
+| `npm install` | Installs dependencies |
+| `npm run dev` | Watch for changes and build your app automatically |
+| `npm run build` | Build your app to `./dist/` |
+
+## 👀 Want to learn more?
+
+Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
diff --git a/examples/toolbar-app/package.json b/examples/toolbar-app/package.json
new file mode 100644
index 000000000..56c232081
--- /dev/null
+++ b/examples/toolbar-app/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "@example/toolbar-app",
+ "type": "module",
+ "version": "0.0.1",
+ "peerDependencies": {
+ "astro": "^4.6.1"
+ },
+ "scripts": {
+ "build": "tsc",
+ "dev": "tsc --watch",
+ "prepublish": "npm run build"
+ },
+ "exports": {
+ ".": "./dist/integration.js",
+ "./app": "./dist/app.js"
+ },
+ "devDependencies": {
+ "@types/node": "^18.17.8",
+ "astro": "^5.9.0"
+ }
+}
diff --git a/examples/toolbar-app/src/app.ts b/examples/toolbar-app/src/app.ts
new file mode 100644
index 000000000..72bd4772d
--- /dev/null
+++ b/examples/toolbar-app/src/app.ts
@@ -0,0 +1,16 @@
+import { defineToolbarApp } from 'astro/toolbar';
+
+// Guide: https://docs.astro.build/en/recipes/making-toolbar-apps/
+// API Reference: https://docs.astro.build/en/reference/dev-toolbar-app-reference/
+export default defineToolbarApp({
+ init(canvas) {
+ const astroWindow = document.createElement('astro-dev-toolbar-window');
+
+ const text = document.createElement('p');
+ text.textContent = 'Hello, Astro!';
+
+ astroWindow.append(text);
+
+ canvas.append(astroWindow);
+ },
+});
diff --git a/examples/toolbar-app/src/integration.ts b/examples/toolbar-app/src/integration.ts
new file mode 100644
index 000000000..81597cf6e
--- /dev/null
+++ b/examples/toolbar-app/src/integration.ts
@@ -0,0 +1,17 @@
+import { fileURLToPath } from 'node:url';
+import type { AstroIntegration } from 'astro';
+
+// API Reference: https://docs.astro.build/en/reference/integrations-reference/
+export default {
+ name: 'my-astro-integration',
+ hooks: {
+ 'astro:config:setup': ({ addDevToolbarApp }) => {
+ addDevToolbarApp({
+ id: "my-toolbar-app",
+ name: "My Toolbar App",
+ icon: "🚀",
+ entrypoint: fileURLToPath(new URL('./app.js', import.meta.url))
+ });
+ },
+ },
+} satisfies AstroIntegration;
diff --git a/examples/toolbar-app/tsconfig.json b/examples/toolbar-app/tsconfig.json
new file mode 100644
index 000000000..281fb7f92
--- /dev/null
+++ b/examples/toolbar-app/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "astro/tsconfigs/strict",
+ "compilerOptions": {
+ "outDir": "dist",
+ "rootDir": "src"
+ }
+}
diff --git a/examples/with-markdoc/.codesandbox/Dockerfile b/examples/with-markdoc/.codesandbox/Dockerfile
new file mode 100644
index 000000000..c3b5c81a1
--- /dev/null
+++ b/examples/with-markdoc/.codesandbox/Dockerfile
@@ -0,0 +1 @@
+FROM node:18-bullseye
diff --git a/examples/with-markdoc/.gitignore b/examples/with-markdoc/.gitignore
new file mode 100644
index 000000000..16d54bb13
--- /dev/null
+++ b/examples/with-markdoc/.gitignore
@@ -0,0 +1,24 @@
+# build output
+dist/
+# generated types
+.astro/
+
+# dependencies
+node_modules/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
+
+# jetbrains setting folder
+.idea/
diff --git a/examples/with-markdoc/.vscode/extensions.json b/examples/with-markdoc/.vscode/extensions.json
new file mode 100644
index 000000000..22a15055d
--- /dev/null
+++ b/examples/with-markdoc/.vscode/extensions.json
@@ -0,0 +1,4 @@
+{
+ "recommendations": ["astro-build.astro-vscode"],
+ "unwantedRecommendations": []
+}
diff --git a/examples/with-markdoc/.vscode/launch.json b/examples/with-markdoc/.vscode/launch.json
new file mode 100644
index 000000000..d64220976
--- /dev/null
+++ b/examples/with-markdoc/.vscode/launch.json
@@ -0,0 +1,11 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "command": "./node_modules/.bin/astro dev",
+ "name": "Development server",
+ "request": "launch",
+ "type": "node-terminal"
+ }
+ ]
+}
diff --git a/examples/with-markdoc/README.md b/examples/with-markdoc/README.md
new file mode 100644
index 000000000..35afb0f3f
--- /dev/null
+++ b/examples/with-markdoc/README.md
@@ -0,0 +1,56 @@
+# Astro Example: Markdoc (experimental)
+
+This starter showcases the experimental Markdoc integration.
+
+```sh
+npm create astro@latest -- --template with-markdoc
+```
+
+[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/with-markdoc)
+[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/with-markdoc)
+
+> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
+
+## 🚀 Project Structure
+
+Inside of your Astro project, you'll see the following folders and files:
+
+```text
+/
+├── public/
+├── src/
+│ └── content/
+ └── docs/
+│ └── intro.mdoc
+| └── config.ts
+│ └── components/Aside.astro
+│ └── layouts/Layout.astro
+│ └── pages/index.astro
+| └── env.d.ts
+├── astro.config.mjs
+├── markdoc.config.mjs
+├── README.md
+├── package.json
+└── tsconfig.json
+```
+
+Markdoc (`.mdoc`) files can be used in content collections. See `src/content/docs/` for an example file.
+
+You can also render Astro components from your Markdoc files using [tags](https://markdoc.dev/docs/tags). See the `markdoc.config.mjs` file for an example configuration.
+
+## 🧞 Commands
+
+All commands are run from the root of the project, from a terminal:
+
+| Command | Action |
+| :------------------------ | :----------------------------------------------- |
+| `npm install` | Installs dependencies |
+| `npm run dev` | Starts local dev server at `localhost:4321` |
+| `npm run build` | Build your production site to `./dist/` |
+| `npm run preview` | Preview your build locally, before deploying |
+| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
+| `npm run astro -- --help` | Get help using the Astro CLI |
+
+## 👀 Want to learn more?
+
+Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
diff --git a/examples/with-markdoc/astro.config.mjs b/examples/with-markdoc/astro.config.mjs
new file mode 100644
index 000000000..517f5a62e
--- /dev/null
+++ b/examples/with-markdoc/astro.config.mjs
@@ -0,0 +1,8 @@
+// @ts-check
+import { defineConfig } from 'astro/config';
+import markdoc from '@astrojs/markdoc';
+
+// https://astro.build/config
+export default defineConfig({
+ integrations: [markdoc()],
+});
diff --git a/examples/with-markdoc/markdoc.config.mjs b/examples/with-markdoc/markdoc.config.mjs
new file mode 100644
index 000000000..90608d564
--- /dev/null
+++ b/examples/with-markdoc/markdoc.config.mjs
@@ -0,0 +1,13 @@
+import { defineMarkdocConfig, component } from '@astrojs/markdoc/config';
+
+export default defineMarkdocConfig({
+ tags: {
+ aside: {
+ render: component('./src/components/Aside.astro'),
+ attributes: {
+ type: { type: String },
+ title: { type: String },
+ },
+ },
+ },
+});
diff --git a/examples/with-markdoc/package.json b/examples/with-markdoc/package.json
new file mode 100644
index 000000000..f9be2cf9a
--- /dev/null
+++ b/examples/with-markdoc/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "@example/with-markdoc",
+ "type": "module",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "dev": "astro dev",
+ "build": "astro build",
+ "preview": "astro preview",
+ "astro": "astro"
+ },
+ "dependencies": {
+ "@astrojs/markdoc": "^0.15.0",
+ "astro": "^5.9.0"
+ }
+}
diff --git a/examples/with-markdoc/public/favicon.svg b/examples/with-markdoc/public/favicon.svg
new file mode 100644
index 000000000..f157bd1c5
--- /dev/null
+++ b/examples/with-markdoc/public/favicon.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
+ <path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
+ <style>
+ path { fill: #000; }
+ @media (prefers-color-scheme: dark) {
+ path { fill: #FFF; }
+ }
+ </style>
+</svg>
diff --git a/examples/with-markdoc/src/components/Aside.astro b/examples/with-markdoc/src/components/Aside.astro
new file mode 100644
index 000000000..be15e8c0a
--- /dev/null
+++ b/examples/with-markdoc/src/components/Aside.astro
@@ -0,0 +1,116 @@
+---
+// Inspired by the `Aside` component from docs.astro.build
+// https://github.com/withastro/starlight/blob/main/packages/starlight/integrations/asides.ts
+
+interface Props {
+ type?: 'note' | 'tip' | 'caution' | 'danger';
+ title?: string;
+}
+
+const labelByType = {
+ note: 'Note',
+ tip: 'Tip',
+ caution: 'Caution',
+ danger: 'Danger',
+};
+const { type = 'note' } = Astro.props as Props;
+const title = Astro.props.title ?? labelByType[type] ?? '';
+
+// SVG icon paths based on GitHub Octicons
+const icons: Record<NonNullable<Props['type']>, { viewBox: string; d: string }> = {
+ note: {
+ viewBox: '0 0 18 18',
+ d: 'M0 3.75C0 2.784.784 2 1.75 2h12.5c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0114.25 14H1.75A1.75 1.75 0 010 12.25v-8.5zm1.75-.25a.25.25 0 00-.25.25v8.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25v-8.5a.25.25 0 00-.25-.25H1.75zM3.5 6.25a.75.75 0 01.75-.75h7a.75.75 0 010 1.5h-7a.75.75 0 01-.75-.75zm.75 2.25a.75.75 0 000 1.5h4a.75.75 0 000-1.5h-4z',
+ },
+ tip: {
+ viewBox: '0 0 18 18',
+ d: 'M14 0a8.8 8.8 0 0 0-6 2.6l-.5.4-.9 1H3.3a1.8 1.8 0 0 0-1.5.8L.1 7.6a.8.8 0 0 0 .4 1.1l3.1 1 .2.1 2.4 2.4.1.2 1 3a.8.8 0 0 0 1 .5l2.9-1.7a1.8 1.8 0 0 0 .8-1.5V9.5l1-1 .4-.4A8.8 8.8 0 0 0 16 2v-.1A1.8 1.8 0 0 0 14.2 0h-.1zm-3.5 10.6-.3.2L8 12.3l.5 1.8 2-1.2a.3.3 0 0 0 .1-.2v-2zM3.7 8.1l1.5-2.3.2-.3h-2a.3.3 0 0 0-.3.1l-1.2 2 1.8.5zm5.2-4.5a7.3 7.3 0 0 1 5.2-2.1h.1a.3.3 0 0 1 .3.3v.1a7.3 7.3 0 0 1-2.1 5.2l-.5.4a15.2 15.2 0 0 1-2.5 2L7.1 11 5 9l1.5-2.3a15.3 15.3 0 0 1 2-2.5l.4-.5zM12 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm-8.4 9.6a1.5 1.5 0 1 0-2.2-2.2 7 7 0 0 0-1.1 3 .2.2 0 0 0 .3.3c.6 0 2.2-.4 3-1.1z',
+ },
+ caution: {
+ viewBox: '-1 1 18 18',
+ d: 'M8.9 1.5C8.7 1.2 8.4 1 8 1s-.7.2-.9.5l-7 12a1 1 0 0 0 0 1c.2.3.6.5 1 .5H15c.4 0 .7-.2.9-.5a1 1 0 0 0 0-1l-7-12zM9 13H7v-2h2v2zm0-3H7V6h2v4z',
+ },
+ danger: {
+ viewBox: '0 1 14 17',
+ d: 'M5 .3c.9 2.2.5 3.4-.5 4.3C3.5 5.6 2 6.5 1 8c-1.5 2-1.7 6.5 3.5 7.7-2.2-1.2-2.6-4.5-.3-6.6-.6 2 .6 3.3 2 2.8 1.4-.4 2.3.6 2.2 1.7 0 .8-.3 1.4-1 1.8A5.6 5.6 0 0 0 12 10c0-2.9-2.5-3.3-1.3-5.7-1.5.2-2 1.2-1.8 2.8 0 1-1 1.8-2 1.3-.6-.4-.6-1.2 0-1.8C8.2 5.3 8.7 2.5 5 .3z',
+ },
+};
+const { viewBox, d } = icons[type];
+---
+
+<aside class={`content ${type}`} aria-label={title}>
+ <p class="title" aria-hidden="true">
+ <span class="icon">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox={viewBox} width={16} height={16}>
+ <path fill-rule="evenodd" d={d}></path>
+ </svg>
+ </span>
+ {title}
+ </p>
+ <section>
+ <slot />
+ </section>
+</aside>
+
+<style>
+ aside {
+ --color-base-purple: 269, 79%;
+ --color-base-teal: 180, 80%;
+ --color-base-red: 351, 100%;
+ --color-base-yellow: 41, 100%;
+
+ --aside-color-base: var(--color-base-purple);
+ --aside-color-lightness: 54%;
+ --aside-accent-color: hsl(var(--aside-color-base), var(--aside-color-lightness));
+ --aside-text-lightness: 20%;
+ --aside-text-accent-color: hsl(var(--aside-color-base), var(--aside-text-lightness));
+
+ border-inline-start: 4px solid var(--aside-accent-color);
+ padding: 1rem;
+ background-color: hsla(var(--aside-color-base), var(--aside-color-lightness), 0.1);
+ /* Indicates the aside boundaries for forced colors users, transparent is changed to a solid color */
+ outline: 1px solid transparent;
+ }
+
+ .title {
+ line-height: 1;
+ margin-bottom: 0.5rem;
+ font-size: 0.9rem;
+ letter-spacing: 0.05em;
+ font-weight: bold;
+ text-transform: uppercase;
+ color: var(--aside-text-accent-color);
+ }
+
+ .icon svg {
+ width: 1.5em;
+ height: 1.5em;
+ vertical-align: middle;
+ fill: currentcolor;
+ }
+
+ aside :global(a),
+ aside :global(a > code:not([class*='language'])) {
+ color: var(--aside-text-accent-color);
+ }
+
+ aside :global(pre) {
+ margin-left: 0;
+ margin-right: 0;
+ }
+
+ .tip {
+ --aside-color-lightness: 42%;
+ --aside-color-base: var(--color-base-teal);
+ }
+
+ .caution {
+ --aside-color-lightness: 59%;
+ --aside-color-base: var(--color-base-yellow);
+ }
+
+ .danger {
+ --aside-color-lightness: 54%;
+ --aside-color-base: var(--color-base-red);
+ }
+</style>
diff --git a/examples/with-markdoc/src/content.config.ts b/examples/with-markdoc/src/content.config.ts
new file mode 100644
index 000000000..79743326e
--- /dev/null
+++ b/examples/with-markdoc/src/content.config.ts
@@ -0,0 +1,5 @@
+import { defineCollection } from 'astro:content';
+
+export const collections = {
+ docs: defineCollection({})
+};
diff --git a/examples/with-markdoc/src/content/docs/intro.mdoc b/examples/with-markdoc/src/content/docs/intro.mdoc
new file mode 100644
index 000000000..c8fcf5675
--- /dev/null
+++ b/examples/with-markdoc/src/content/docs/intro.mdoc
@@ -0,0 +1,39 @@
+---
+title: Welcome to Markdoc 👋
+---
+
+This simple starter showcases Markdoc with Content Collections. All Markdoc features are supported, including this nifty built-in `{% table %}` tag:
+
+{% table %}
+* Feature
+* Supported
+---
+* `.mdoc` in Content Collections
+* ✅
+---
+* Markdoc transform configuration
+* ✅
+---
+* Astro components
+* ✅
+{% /table %}
+
+{% aside title="Code Challenge" type="tip" %}
+
+Reveal the secret message below by adding `revealSecret` to your list of Markdoc variables.
+
+_Hint: Try passing as a prop to the `<Content />` component in the `src/pages/index.astro` file._
+
+{% if $revealSecret %}
+
+Maybe the real secret was the Rick Rolls we shared along the way.
+
+![Rick Astley dancing](https://media.tenor.com/x8v1oNUOmg4AAAAM/rickroll-roll.gif)
+
+{% /if %}
+
+{% /aside %}
+
+Check out [the `@astrojs/markdoc` integration][astro-markdoc] for complete documentation and usage examples.
+
+[astro-markdoc]: https://docs.astro.build/en/guides/integrations-guide/markdoc/
diff --git a/examples/with-markdoc/src/layouts/Layout.astro b/examples/with-markdoc/src/layouts/Layout.astro
new file mode 100644
index 000000000..aaa6e0ce0
--- /dev/null
+++ b/examples/with-markdoc/src/layouts/Layout.astro
@@ -0,0 +1,42 @@
+---
+interface Props {
+ title: string;
+}
+
+const { title } = Astro.props;
+---
+
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width" />
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+ <meta name="generator" content={Astro.generator} />
+ <title>{title}</title>
+ </head>
+ <body>
+ <slot />
+ </body>
+</html>
+<style is:global>
+ html {
+ font-family: system-ui, sans-serif;
+ background-color: #f6f6f6;
+ }
+ code {
+ font-family:
+ Menlo,
+ Monaco,
+ Lucida Console,
+ Liberation Mono,
+ DejaVu Sans Mono,
+ Bitstream Vera Sans Mono,
+ Courier New,
+ monospace;
+ }
+ main {
+ margin: auto;
+ max-width: 60ch;
+ }
+</style>
diff --git a/examples/with-markdoc/src/pages/index.astro b/examples/with-markdoc/src/pages/index.astro
new file mode 100644
index 000000000..891b344de
--- /dev/null
+++ b/examples/with-markdoc/src/pages/index.astro
@@ -0,0 +1,28 @@
+---
+import { getEntry } from 'astro:content';
+import Layout from '../layouts/Layout.astro';
+
+const intro = await getEntry('docs', 'intro');
+if (!intro) {
+ return Astro.redirect('/404');
+}
+const { Content } = await intro.render();
+---
+
+<Layout title={intro.data.title}>
+ <main>
+ <h1>{intro.data.title}</h1>
+ <Content />
+ </main>
+</Layout>
+
+<style is:global>
+ table {
+ margin-block: 2rem;
+ margin-inline: auto;
+ }
+ table td {
+ padding-block: 0.3rem;
+ padding-inline: 0.5rem;
+ }
+</style>
diff --git a/examples/with-markdoc/tsconfig.json b/examples/with-markdoc/tsconfig.json
new file mode 100644
index 000000000..0dc098dd7
--- /dev/null
+++ b/examples/with-markdoc/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "astro/tsconfigs/strict",
+ "include": [".astro/types.d.ts", "**/*"],
+ "exclude": ["dist"],
+ "compilerOptions": {
+ "strictNullChecks": true
+ }
+}
diff --git a/examples/with-mdx/.codesandbox/Dockerfile b/examples/with-mdx/.codesandbox/Dockerfile
new file mode 100644
index 000000000..c3b5c81a1
--- /dev/null
+++ b/examples/with-mdx/.codesandbox/Dockerfile
@@ -0,0 +1 @@
+FROM node:18-bullseye
diff --git a/examples/with-mdx/.gitignore b/examples/with-mdx/.gitignore
new file mode 100644
index 000000000..16d54bb13
--- /dev/null
+++ b/examples/with-mdx/.gitignore
@@ -0,0 +1,24 @@
+# build output
+dist/
+# generated types
+.astro/
+
+# dependencies
+node_modules/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
+
+# jetbrains setting folder
+.idea/
diff --git a/examples/with-mdx/.vscode/extensions.json b/examples/with-mdx/.vscode/extensions.json
new file mode 100644
index 000000000..56f043d30
--- /dev/null
+++ b/examples/with-mdx/.vscode/extensions.json
@@ -0,0 +1,4 @@
+{
+ "recommendations": ["astro-build.astro-vscode", "unifiedjs.vscode-mdx"],
+ "unwantedRecommendations": []
+}
diff --git a/examples/with-mdx/.vscode/launch.json b/examples/with-mdx/.vscode/launch.json
new file mode 100644
index 000000000..d64220976
--- /dev/null
+++ b/examples/with-mdx/.vscode/launch.json
@@ -0,0 +1,11 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "command": "./node_modules/.bin/astro dev",
+ "name": "Development server",
+ "request": "launch",
+ "type": "node-terminal"
+ }
+ ]
+}
diff --git a/examples/with-mdx/README.md b/examples/with-mdx/README.md
new file mode 100644
index 000000000..55fe589cf
--- /dev/null
+++ b/examples/with-mdx/README.md
@@ -0,0 +1,11 @@
+# Astro Example: MDX
+
+```sh
+npm create astro@latest -- --template with-mdx
+```
+
+[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/with-mdx)
+[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/with-mdx)
+[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/with-mdx/devcontainer.json)
+
+This example showcases using [`@astrojs/mdx`](https://www.npmjs.com/package/@astrojs/mdx) to author content using [MDX](https://mdxjs.com/).
diff --git a/examples/with-mdx/astro.config.mjs b/examples/with-mdx/astro.config.mjs
new file mode 100644
index 000000000..93aeffbc5
--- /dev/null
+++ b/examples/with-mdx/astro.config.mjs
@@ -0,0 +1,9 @@
+// @ts-check
+import { defineConfig } from 'astro/config';
+import mdx from '@astrojs/mdx';
+import preact from '@astrojs/preact';
+
+// https://astro.build/config
+export default defineConfig({
+ integrations: [mdx(), preact()],
+});
diff --git a/examples/with-mdx/package.json b/examples/with-mdx/package.json
new file mode 100644
index 000000000..1eab43093
--- /dev/null
+++ b/examples/with-mdx/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "@example/with-mdx",
+ "type": "module",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "dev": "astro dev",
+ "build": "astro build",
+ "preview": "astro preview",
+ "astro": "astro"
+ },
+ "dependencies": {
+ "@astrojs/mdx": "^4.3.0",
+ "@astrojs/preact": "^4.1.0",
+ "astro": "^5.9.0",
+ "preact": "^10.26.5"
+ }
+}
diff --git a/examples/with-mdx/public/favicon.svg b/examples/with-mdx/public/favicon.svg
new file mode 100644
index 000000000..f157bd1c5
--- /dev/null
+++ b/examples/with-mdx/public/favicon.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
+ <path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
+ <style>
+ path { fill: #000; }
+ @media (prefers-color-scheme: dark) {
+ path { fill: #FFF; }
+ }
+ </style>
+</svg>
diff --git a/examples/with-mdx/src/components/Counter.jsx b/examples/with-mdx/src/components/Counter.jsx
new file mode 100644
index 000000000..801eefe73
--- /dev/null
+++ b/examples/with-mdx/src/components/Counter.jsx
@@ -0,0 +1,18 @@
+import { useState } from 'preact/hooks';
+
+export default function Counter({ children }) {
+ const [count, setCount] = useState(0);
+ const add = () => setCount((i) => i + 1);
+ const subtract = () => setCount((i) => i - 1);
+
+ return (
+ <>
+ <div class="counter">
+ <button onClick={subtract}>-</button>
+ <pre>{count}</pre>
+ <button onClick={add}>+</button>
+ </div>
+ <div class="counter-message">{children}</div>
+ </>
+ );
+}
diff --git a/examples/with-mdx/src/components/Title.astro b/examples/with-mdx/src/components/Title.astro
new file mode 100644
index 000000000..6d0dcb86c
--- /dev/null
+++ b/examples/with-mdx/src/components/Title.astro
@@ -0,0 +1,7 @@
+<h1><slot /></h1>
+
+<style>
+ h1 {
+ color: red;
+ }
+</style>
diff --git a/examples/with-mdx/src/pages/index.mdx b/examples/with-mdx/src/pages/index.mdx
new file mode 100644
index 000000000..df0dd47c3
--- /dev/null
+++ b/examples/with-mdx/src/pages/index.mdx
@@ -0,0 +1,29 @@
+import Counter from '../components/Counter.jsx';
+import Title from '../components/Title.astro';
+export const components = { h1: Title };
+
+export const authors = [
+ { name: 'Jane', email: 'hi@jane.com' },
+ { name: 'John', twitter: '@john2002' },
+];
+export const published = new Date('2022-02-01');
+
+# Hello world!
+
+Written by: {new Intl.ListFormat('en').format(authors.map(d => d.name))}.
+
+Published on: {new Intl.DateTimeFormat('en', {dateStyle: 'long'}).format(published)}.
+
+<Counter client:idle>This is a **counter**!</Counter>
+
+## Syntax highlighting
+
+We also support syntax highlighting in MDX out-of-the-box! This example uses the default [Shiki](https://shiki.style) theme. See the [MDX integration docs](https://docs.astro.build/en/guides/integrations-guide/mdx/#syntax-highlighting) for configuration options.
+
+```astro
+---
+const weSupportAstro = true;
+---
+
+<h1>Hey, what theme is that? Looks nice!</h1>
+```
diff --git a/examples/with-mdx/tsconfig.json b/examples/with-mdx/tsconfig.json
new file mode 100644
index 000000000..8bf91d3bb
--- /dev/null
+++ b/examples/with-mdx/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "extends": "astro/tsconfigs/strict",
+ "include": [".astro/types.d.ts", "**/*"],
+ "exclude": ["dist"]
+}
diff --git a/examples/with-nanostores/.codesandbox/Dockerfile b/examples/with-nanostores/.codesandbox/Dockerfile
new file mode 100644
index 000000000..c3b5c81a1
--- /dev/null
+++ b/examples/with-nanostores/.codesandbox/Dockerfile
@@ -0,0 +1 @@
+FROM node:18-bullseye
diff --git a/examples/with-nanostores/.gitignore b/examples/with-nanostores/.gitignore
new file mode 100644
index 000000000..16d54bb13
--- /dev/null
+++ b/examples/with-nanostores/.gitignore
@@ -0,0 +1,24 @@
+# build output
+dist/
+# generated types
+.astro/
+
+# dependencies
+node_modules/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
+
+# jetbrains setting folder
+.idea/
diff --git a/examples/with-nanostores/README.md b/examples/with-nanostores/README.md
new file mode 100644
index 000000000..163c9129a
--- /dev/null
+++ b/examples/with-nanostores/README.md
@@ -0,0 +1,11 @@
+# Astro Example: Nanostores
+
+```sh
+npm create astro@latest -- --template with-nanostores
+```
+
+[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/with-nanostores)
+[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/with-nanostores)
+[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/with-nanostores/devcontainer.json)
+
+This example showcases using [`nanostores`](https://github.com/nanostores/nanostores) to provide shared state between components of any framework. [**Read our documentation on sharing state**](https://docs.astro.build/en/core-concepts/sharing-state/) for a complete breakdown of this project, along with guides to use React, Vue, Svelte, or Solid!
diff --git a/examples/with-nanostores/astro.config.mjs b/examples/with-nanostores/astro.config.mjs
new file mode 100644
index 000000000..9f7dbd219
--- /dev/null
+++ b/examples/with-nanostores/astro.config.mjs
@@ -0,0 +1,9 @@
+// @ts-check
+import { defineConfig } from 'astro/config';
+import preact from '@astrojs/preact';
+
+// https://astro.build/config
+export default defineConfig({
+ // Enable many frameworks to support all different kinds of components.
+ integrations: [preact()],
+});
diff --git a/examples/with-nanostores/package.json b/examples/with-nanostores/package.json
new file mode 100644
index 000000000..cb7cab954
--- /dev/null
+++ b/examples/with-nanostores/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "@example/with-nanostores",
+ "type": "module",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "dev": "astro dev",
+ "build": "astro build",
+ "preview": "astro preview",
+ "astro": "astro"
+ },
+ "dependencies": {
+ "@astrojs/preact": "^4.1.0",
+ "@nanostores/preact": "^0.5.2",
+ "astro": "^5.9.0",
+ "nanostores": "^0.11.4",
+ "preact": "^10.26.5"
+ }
+}
diff --git a/examples/with-nanostores/public/favicon.svg b/examples/with-nanostores/public/favicon.svg
new file mode 100644
index 000000000..f157bd1c5
--- /dev/null
+++ b/examples/with-nanostores/public/favicon.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
+ <path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
+ <style>
+ path { fill: #000; }
+ @media (prefers-color-scheme: dark) {
+ path { fill: #FFF; }
+ }
+ </style>
+</svg>
diff --git a/examples/with-nanostores/public/images/astronaut-figurine.png b/examples/with-nanostores/public/images/astronaut-figurine.png
new file mode 100644
index 000000000..aac9b445e
--- /dev/null
+++ b/examples/with-nanostores/public/images/astronaut-figurine.png
Binary files differ
diff --git a/examples/with-nanostores/src/cartStore.ts b/examples/with-nanostores/src/cartStore.ts
new file mode 100644
index 000000000..a57a6ce87
--- /dev/null
+++ b/examples/with-nanostores/src/cartStore.ts
@@ -0,0 +1,31 @@
+import { atom, map } from 'nanostores';
+
+export const isCartOpen = atom(false);
+
+export type CartItem = {
+ id: string;
+ name: string;
+ imageSrc: string;
+ quantity: number;
+};
+
+export type CartItemDisplayInfo = Pick<CartItem, 'id' | 'name' | 'imageSrc'>;
+
+export const cartItems = map<Record<string, CartItem>>({});
+
+export function addCartItem({ id, name, imageSrc }: CartItemDisplayInfo) {
+ const existingEntry = cartItems.get()[id];
+ if (existingEntry) {
+ cartItems.setKey(id, {
+ ...existingEntry,
+ quantity: existingEntry.quantity + 1,
+ });
+ } else {
+ cartItems.setKey(id, {
+ id,
+ name,
+ imageSrc,
+ quantity: 1,
+ });
+ }
+}
diff --git a/examples/with-nanostores/src/components/AddToCartForm.tsx b/examples/with-nanostores/src/components/AddToCartForm.tsx
new file mode 100644
index 000000000..7498443f6
--- /dev/null
+++ b/examples/with-nanostores/src/components/AddToCartForm.tsx
@@ -0,0 +1,18 @@
+import { isCartOpen, addCartItem } from '../cartStore';
+import type { CartItemDisplayInfo } from '../cartStore';
+import type { ComponentChildren } from 'preact';
+
+type Props = {
+ item: CartItemDisplayInfo;
+ children: ComponentChildren;
+};
+
+export default function AddToCartForm({ item, children }: Props) {
+ function addToCart(e: SubmitEvent) {
+ e.preventDefault();
+ isCartOpen.set(true);
+ addCartItem(item);
+ }
+
+ return <form onSubmit={addToCart}>{children}</form>;
+}
diff --git a/examples/with-nanostores/src/components/CartFlyout.module.css b/examples/with-nanostores/src/components/CartFlyout.module.css
new file mode 100644
index 000000000..cee43dd4c
--- /dev/null
+++ b/examples/with-nanostores/src/components/CartFlyout.module.css
@@ -0,0 +1,29 @@
+.container {
+ position: fixed;
+ right: 0;
+ top: var(--nav-height);
+ height: 100vh;
+ background: var(--color-bg-2);
+ padding-inline: 2rem;
+ min-width: min(90vw, 300px);
+ border-left: 3px solid var(--color-bg-3);
+}
+
+.list {
+ list-style: none;
+ padding: 0;
+}
+
+.listItem {
+ display: flex;
+ gap: 1rem;
+ align-items: center;
+}
+
+.listItem * {
+ margin-block: 0.3rem;
+}
+
+.listItemImg {
+ width: 4rem;
+}
diff --git a/examples/with-nanostores/src/components/CartFlyout.tsx b/examples/with-nanostores/src/components/CartFlyout.tsx
new file mode 100644
index 000000000..98fd8cbfb
--- /dev/null
+++ b/examples/with-nanostores/src/components/CartFlyout.tsx
@@ -0,0 +1,28 @@
+import { useStore } from '@nanostores/preact';
+import { cartItems, isCartOpen } from '../cartStore';
+import styles from './CartFlyout.module.css';
+
+export default function CartFlyout() {
+ const $isCartOpen = useStore(isCartOpen);
+ const $cartItems = useStore(cartItems);
+
+ return (
+ <aside hidden={!$isCartOpen} className={styles.container}>
+ {Object.values($cartItems).length ? (
+ <ul className={styles.list} role="list">
+ {Object.values($cartItems).map((cartItem) => (
+ <li className={styles.listItem}>
+ <img className={styles.listItemImg} src={cartItem.imageSrc} alt={cartItem.name} />
+ <div>
+ <h3>{cartItem.name}</h3>
+ <p>Quantity: {cartItem.quantity}</p>
+ </div>
+ </li>
+ ))}
+ </ul>
+ ) : (
+ <p>Your cart is empty!</p>
+ )}
+ </aside>
+ );
+}
diff --git a/examples/with-nanostores/src/components/CartFlyoutToggle.tsx b/examples/with-nanostores/src/components/CartFlyoutToggle.tsx
new file mode 100644
index 000000000..14ce1c70d
--- /dev/null
+++ b/examples/with-nanostores/src/components/CartFlyoutToggle.tsx
@@ -0,0 +1,7 @@
+import { useStore } from '@nanostores/preact';
+import { isCartOpen } from '../cartStore';
+
+export default function CartFlyoutToggle() {
+ const $isCartOpen = useStore(isCartOpen);
+ return <button onClick={() => isCartOpen.set(!$isCartOpen)}>Cart</button>;
+}
diff --git a/examples/with-nanostores/src/components/FigurineDescription.astro b/examples/with-nanostores/src/components/FigurineDescription.astro
new file mode 100644
index 000000000..1294b1510
--- /dev/null
+++ b/examples/with-nanostores/src/components/FigurineDescription.astro
@@ -0,0 +1,44 @@
+<h1>Astronaut Figurine</h1>
+<p class="limited-edition-badge">Limited Edition</p>
+<p>
+ The limited edition Astronaut Figurine is the perfect gift for any Astro contributor. This
+ fully-poseable action figurine comes equipped with:
+</p>
+<ul>
+ <li>A fabric space suit with adjustable straps</li>
+ <li>Boots lightly dusted by the lunar surface *</li>
+ <li>An adjustable space visor</li>
+</ul>
+<p>
+ <sub>* Dust not actually from the lunar surface</sub>
+</p>
+
+<style>
+ h1 {
+ margin: 0;
+ margin-block-start: 2rem;
+ }
+
+ .limited-edition-badge {
+ font-weight: 700;
+ text-transform: uppercase;
+ background-image: linear-gradient(0deg, var(--astro-blue), var(--astro-pink));
+ background-size: 100% 200%;
+ background-position-y: 100%;
+ border-radius: 0.4rem;
+ animation: pulse 4s ease-in-out infinite;
+ display: inline-block;
+ color: white;
+ padding: 0.2rem 0.4rem;
+ }
+
+ @keyframes pulse {
+ 0%,
+ 100% {
+ background-position-y: 0%;
+ }
+ 50% {
+ background-position-y: 80%;
+ }
+ }
+</style>
diff --git a/examples/with-nanostores/src/layouts/Layout.astro b/examples/with-nanostores/src/layouts/Layout.astro
new file mode 100644
index 000000000..aa8c34773
--- /dev/null
+++ b/examples/with-nanostores/src/layouts/Layout.astro
@@ -0,0 +1,113 @@
+---
+import CartFlyout from '../components/CartFlyout';
+import CartFlyoutToggle from '../components/CartFlyoutToggle';
+import { withBase } from '../utils';
+
+interface Props {
+ title: string;
+}
+
+const { title } = Astro.props;
+---
+
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width" />
+ <meta name="generator" content={Astro.generator} />
+ <link rel="icon" type="image/svg+xml" href={withBase('/favicon.svg')} />
+ <title>{title}</title>
+ </head>
+ <body>
+ <header>
+ <nav>
+ <a href={withBase('/')} class="nav-header">
+ <span style="color: var(--astro-blue)">Astro</span> storefront
+ </a>
+ <CartFlyoutToggle client:load />
+ </nav>
+ </header>
+ <slot />
+ <CartFlyout client:load />
+ </body>
+</html>
+
+<style is:global>
+ :root {
+ --font-family: system-ui, sans-serif;
+ --font-size-base: clamp(1rem, 0.34vw + 0.91rem, 1.19rem);
+ --font-size-lg: clamp(1.2rem, 0.7vw + 1.2rem, 1.5rem);
+ --font-size-xl: clamp(2rem, 1.75vw + 1.35rem, 2.75rem);
+
+ --color-text: hsl(12, 5%, 4%);
+ --color-bg: hsl(17, 20%, 97%);
+ --color-bg-2: hsl(17, 20%, 94%);
+ --color-bg-3: hsl(17, 20%, 88%);
+ --astro-blue: #4f39fa;
+ --astro-pink: #da62c4;
+
+ --content-max-width: 90ch;
+ --nav-height: clamp(2.44rem, 2.38vw + 1.85rem, 3.75rem);
+ }
+
+ h1 {
+ font-size: var(--font-size-xl);
+ }
+
+ button {
+ border: none;
+ color: var(--astro-blue);
+ border: 2px solid var(--astro-blue);
+ transition:
+ color 0.2s,
+ background-color 0.2s;
+ background-color: transparent;
+ padding: 0.4rem 0.8rem;
+ border-radius: 0.4rem;
+ font-family: var(--font-family);
+ font-size: var(--font-size-base);
+ font-weight: bold;
+ cursor: pointer;
+ }
+
+ button:hover {
+ background-color: var(--astro-blue);
+ color: white;
+ }
+</style>
+
+<style>
+ html {
+ font-family: var(--font-family);
+ font-size: var(--font-size-base);
+ color: var(--color-text);
+ background-color: var(--color-bg);
+ }
+
+ body {
+ margin: 0;
+ }
+
+ header {
+ background: var(--color-bg-2);
+ }
+
+ nav {
+ max-width: var(--content-max-width);
+ height: var(--nav-height);
+ margin: auto;
+ padding-inline: 1rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ .nav-header,
+ .nav-header:visited {
+ font-size: var(--font-size-base);
+ font-weight: bold;
+ color: inherit;
+ text-decoration: none;
+ }
+</style>
diff --git a/examples/with-nanostores/src/pages/index.astro b/examples/with-nanostores/src/pages/index.astro
new file mode 100644
index 000000000..c90609595
--- /dev/null
+++ b/examples/with-nanostores/src/pages/index.astro
@@ -0,0 +1,50 @@
+---
+import type { CartItemDisplayInfo } from '../cartStore';
+import Layout from '../layouts/Layout.astro';
+import AddToCartForm from '../components/AddToCartForm';
+import FigurineDescription from '../components/FigurineDescription.astro';
+import { withBase } from '../utils';
+
+const item: CartItemDisplayInfo = {
+ id: 'astronaut-figurine',
+ name: 'Astronaut Figurine',
+ imageSrc: withBase('/images/astronaut-figurine.png'),
+};
+---
+
+<Layout title={item.name}>
+ <main>
+ <div class="product-layout">
+ <div>
+ <FigurineDescription />
+ <AddToCartForm item={item} client:load>
+ <button type="submit">Add to cart</button>
+ </AddToCartForm>
+ </div>
+ <img src={item.imageSrc} alt={item.name} />
+ </div>
+ </main>
+</Layout>
+
+<style>
+ main {
+ margin: auto;
+ padding: 1em;
+ max-width: var(--content-max-width);
+ }
+
+ .product-layout {
+ display: grid;
+ gap: 2rem;
+ grid-template-columns: repeat(auto-fit, minmax(20rem, max-content));
+ }
+
+ .product-layout img {
+ width: 100%;
+ max-width: 26rem;
+ }
+
+ button[type='submit'] {
+ margin-block-start: 1rem;
+ }
+</style>
diff --git a/examples/with-nanostores/src/utils.ts b/examples/with-nanostores/src/utils.ts
new file mode 100644
index 000000000..cbe38f5f3
--- /dev/null
+++ b/examples/with-nanostores/src/utils.ts
@@ -0,0 +1,4 @@
+const base = import.meta.env.BASE_URL.replace(/\/$/, '');
+
+/** Prefix a URL path with the site’s base path if set. */
+export const withBase = (path: string) => base + path;
diff --git a/examples/with-nanostores/tsconfig.json b/examples/with-nanostores/tsconfig.json
new file mode 100644
index 000000000..c8983c2ef
--- /dev/null
+++ b/examples/with-nanostores/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "astro/tsconfigs/strict",
+ "include": [".astro/types.d.ts", "**/*"],
+ "exclude": ["dist"],
+ "compilerOptions": {
+ // Preact specific settings
+ "jsx": "react-jsx",
+ "jsxImportSource": "preact"
+ }
+}
diff --git a/examples/with-tailwindcss/.codesandbox/Dockerfile b/examples/with-tailwindcss/.codesandbox/Dockerfile
new file mode 100644
index 000000000..c3b5c81a1
--- /dev/null
+++ b/examples/with-tailwindcss/.codesandbox/Dockerfile
@@ -0,0 +1 @@
+FROM node:18-bullseye
diff --git a/examples/with-tailwindcss/.gitignore b/examples/with-tailwindcss/.gitignore
new file mode 100644
index 000000000..16d54bb13
--- /dev/null
+++ b/examples/with-tailwindcss/.gitignore
@@ -0,0 +1,24 @@
+# build output
+dist/
+# generated types
+.astro/
+
+# dependencies
+node_modules/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
+
+# jetbrains setting folder
+.idea/
diff --git a/examples/with-tailwindcss/README.md b/examples/with-tailwindcss/README.md
new file mode 100644
index 000000000..8b512bb7f
--- /dev/null
+++ b/examples/with-tailwindcss/README.md
@@ -0,0 +1,13 @@
+# Astro with Tailwind
+
+```sh
+npm create astro@latest -- --template with-tailwindcss
+```
+
+[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/with-tailwindcss)
+[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/with-tailwindcss)
+[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/with-tailwindcss/devcontainer.json)
+
+Astro comes with [Tailwind](https://tailwindcss.com) support out of the box. This example showcases how to style your Astro project with Tailwind.
+
+For complete setup instructions, please see our [Tailwind Integration Guide](https://docs.astro.build/en/guides/integrations-guide/tailwind).
diff --git a/examples/with-tailwindcss/astro.config.mjs b/examples/with-tailwindcss/astro.config.mjs
new file mode 100644
index 000000000..832a62b06
--- /dev/null
+++ b/examples/with-tailwindcss/astro.config.mjs
@@ -0,0 +1,10 @@
+// @ts-check
+import { defineConfig } from 'astro/config';
+import tailwindcss from '@tailwindcss/vite';
+
+// https://astro.build/config
+export default defineConfig({
+ vite: {
+ plugins: [tailwindcss()]
+ }
+});
diff --git a/examples/with-tailwindcss/package.json b/examples/with-tailwindcss/package.json
new file mode 100644
index 000000000..4e86ff23e
--- /dev/null
+++ b/examples/with-tailwindcss/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "@example/with-tailwindcss",
+ "type": "module",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "dev": "astro dev",
+ "build": "astro build",
+ "preview": "astro preview",
+ "astro": "astro"
+ },
+ "dependencies": {
+ "@astrojs/mdx": "^4.3.0",
+ "@tailwindcss/vite": "^4.1.3",
+ "@types/canvas-confetti": "^1.9.0",
+ "astro": "^5.9.0",
+ "canvas-confetti": "^1.9.3",
+ "tailwindcss": "^4.1.3"
+ }
+}
diff --git a/examples/with-tailwindcss/public/favicon.svg b/examples/with-tailwindcss/public/favicon.svg
new file mode 100644
index 000000000..f157bd1c5
--- /dev/null
+++ b/examples/with-tailwindcss/public/favicon.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
+ <path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
+ <style>
+ path { fill: #000; }
+ @media (prefers-color-scheme: dark) {
+ path { fill: #FFF; }
+ }
+ </style>
+</svg>
diff --git a/examples/with-tailwindcss/src/components/Button.astro b/examples/with-tailwindcss/src/components/Button.astro
new file mode 100644
index 000000000..167927fb3
--- /dev/null
+++ b/examples/with-tailwindcss/src/components/Button.astro
@@ -0,0 +1,19 @@
+---
+// Click button, get confetti!
+// Styled by Tailwind :)
+---
+
+<button
+ class="appearance-none py-2 px-4 bg-purple-500 text-white font-semibold rounded-lg shadow-md hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-400 focus:ring-opacity-75"
+>
+ <slot />
+</button>
+
+<script>
+ import confetti from 'canvas-confetti';
+ const button = document.body.querySelector('button');
+
+ if (button) {
+ button.addEventListener('click', () => confetti());
+ }
+</script>
diff --git a/examples/with-tailwindcss/src/layouts/main.astro b/examples/with-tailwindcss/src/layouts/main.astro
new file mode 100644
index 000000000..0a9423176
--- /dev/null
+++ b/examples/with-tailwindcss/src/layouts/main.astro
@@ -0,0 +1,16 @@
+---
+import '../styles/global.css';
+const { content } = Astro.props;
+---
+
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width" />
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+ <title>{content.title}</title>
+ </head>
+ <body>
+ <slot />
+ </body>
+</html>
diff --git a/examples/with-tailwindcss/src/pages/index.astro b/examples/with-tailwindcss/src/pages/index.astro
new file mode 100644
index 000000000..6350a3330
--- /dev/null
+++ b/examples/with-tailwindcss/src/pages/index.astro
@@ -0,0 +1,25 @@
+---
+import '../styles/global.css';
+// Component Imports
+import Button from '../components/Button.astro';
+
+// Full Astro Component Syntax:
+// https://docs.astro.build/basics/astro-components/
+---
+
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width" />
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+ <meta name="generator" content={Astro.generator} />
+ <title>Astro + TailwindCSS</title>
+ </head>
+
+ <body>
+ <div class="grid place-items-center h-screen content-center">
+ <Button>Tailwind Button in Astro!</Button>
+ <a href="/markdown-page" class="p-4 underline">Markdown is also supported...</a>
+ </div>
+ </body>
+</html>
diff --git a/examples/with-tailwindcss/src/pages/markdown-page.md b/examples/with-tailwindcss/src/pages/markdown-page.md
new file mode 100644
index 000000000..de11d381b
--- /dev/null
+++ b/examples/with-tailwindcss/src/pages/markdown-page.md
@@ -0,0 +1,16 @@
+---
+title: 'Markdown + Tailwind'
+layout: ../layouts/main.astro
+---
+
+<div class="grid place-items-center h-screen content-center">
+ <div class="py-2 px-4 bg-purple-500 text-white font-semibold rounded-lg shadow-md">
+ Tailwind classes also work in Markdown!
+ </div>
+ <a
+ href="/"
+ class="p-4 underline hover:text-purple-500 transition-colors ease-in-out duration-200"
+ >
+ Go home
+ </a>
+</div>
diff --git a/examples/with-tailwindcss/src/styles/global.css b/examples/with-tailwindcss/src/styles/global.css
new file mode 100644
index 000000000..d4b507858
--- /dev/null
+++ b/examples/with-tailwindcss/src/styles/global.css
@@ -0,0 +1 @@
+@import 'tailwindcss';
diff --git a/examples/with-tailwindcss/tsconfig.json b/examples/with-tailwindcss/tsconfig.json
new file mode 100644
index 000000000..8bf91d3bb
--- /dev/null
+++ b/examples/with-tailwindcss/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "extends": "astro/tsconfigs/strict",
+ "include": [".astro/types.d.ts", "**/*"],
+ "exclude": ["dist"]
+}
diff --git a/examples/with-vitest/.codesandbox/Dockerfile b/examples/with-vitest/.codesandbox/Dockerfile
new file mode 100644
index 000000000..c3b5c81a1
--- /dev/null
+++ b/examples/with-vitest/.codesandbox/Dockerfile
@@ -0,0 +1 @@
+FROM node:18-bullseye
diff --git a/examples/with-vitest/.gitignore b/examples/with-vitest/.gitignore
new file mode 100644
index 000000000..16d54bb13
--- /dev/null
+++ b/examples/with-vitest/.gitignore
@@ -0,0 +1,24 @@
+# build output
+dist/
+# generated types
+.astro/
+
+# dependencies
+node_modules/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
+
+# jetbrains setting folder
+.idea/
diff --git a/examples/with-vitest/README.md b/examples/with-vitest/README.md
new file mode 100644
index 000000000..8b831d88c
--- /dev/null
+++ b/examples/with-vitest/README.md
@@ -0,0 +1,11 @@
+# Astro + [Vitest](https://vitest.dev/) Example
+
+```sh
+npm create astro@latest -- --template with-vitest
+```
+
+[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/with-vitest)
+[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/with-vitest)
+[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/with-vitest/devcontainer.json)
+
+This example showcases Astro working with [Vitest](https://vitest.dev/).
diff --git a/examples/with-vitest/astro.config.ts b/examples/with-vitest/astro.config.ts
new file mode 100644
index 000000000..e762ba5cf
--- /dev/null
+++ b/examples/with-vitest/astro.config.ts
@@ -0,0 +1,5 @@
+// @ts-check
+import { defineConfig } from 'astro/config';
+
+// https://astro.build/config
+export default defineConfig({});
diff --git a/examples/with-vitest/package.json b/examples/with-vitest/package.json
new file mode 100644
index 000000000..ff7a90261
--- /dev/null
+++ b/examples/with-vitest/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "@example/with-vitest",
+ "type": "module",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "dev": "astro dev",
+ "build": "astro build",
+ "preview": "astro preview",
+ "astro": "astro",
+ "test": "vitest"
+ },
+ "dependencies": {
+ "astro": "^5.9.0",
+ "vitest": "^3.1.1"
+ }
+}
diff --git a/examples/with-vitest/public/favicon.svg b/examples/with-vitest/public/favicon.svg
new file mode 100644
index 000000000..f157bd1c5
--- /dev/null
+++ b/examples/with-vitest/public/favicon.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
+ <path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
+ <style>
+ path { fill: #000; }
+ @media (prefers-color-scheme: dark) {
+ path { fill: #FFF; }
+ }
+ </style>
+</svg>
diff --git a/examples/with-vitest/src/pages/index.astro b/examples/with-vitest/src/pages/index.astro
new file mode 100644
index 000000000..2d1410736
--- /dev/null
+++ b/examples/with-vitest/src/pages/index.astro
@@ -0,0 +1,16 @@
+---
+
+---
+
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+ <meta name="viewport" content="width=device-width" />
+ <meta name="generator" content={Astro.generator} />
+ <title>Astro</title>
+ </head>
+ <body>
+ <h1>Astro</h1>
+ </body>
+</html>
diff --git a/examples/with-vitest/test/basic.test.ts b/examples/with-vitest/test/basic.test.ts
new file mode 100644
index 000000000..f4677c6cb
--- /dev/null
+++ b/examples/with-vitest/test/basic.test.ts
@@ -0,0 +1,21 @@
+import { assert, expect, test } from 'vitest';
+
+// Edit an assertion and save to see HMR in action
+
+test('Math.sqrt()', () => {
+ expect(Math.sqrt(4)).toBe(2);
+ expect(Math.sqrt(144)).toBe(12);
+ expect(Math.sqrt(2)).toBe(Math.SQRT2);
+});
+
+test('JSON', () => {
+ const input = {
+ foo: 'hello',
+ bar: 'world',
+ };
+
+ const output = JSON.stringify(input);
+
+ expect(output).eq('{"foo":"hello","bar":"world"}');
+ assert.deepEqual(JSON.parse(output), input, 'matches original');
+});
diff --git a/examples/with-vitest/tsconfig.json b/examples/with-vitest/tsconfig.json
new file mode 100644
index 000000000..8bf91d3bb
--- /dev/null
+++ b/examples/with-vitest/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "extends": "astro/tsconfigs/strict",
+ "include": [".astro/types.d.ts", "**/*"],
+ "exclude": ["dist"]
+}
diff --git a/examples/with-vitest/vitest.config.ts b/examples/with-vitest/vitest.config.ts
new file mode 100644
index 000000000..a34f19bb1
--- /dev/null
+++ b/examples/with-vitest/vitest.config.ts
@@ -0,0 +1,9 @@
+/// <reference types="vitest" />
+import { getViteConfig } from 'astro/config';
+
+export default getViteConfig({
+ test: {
+ /* for example, use global to avoid globals imports (describe, test, expect): */
+ // globals: true,
+ },
+});