diff --git a/examples/hackernews/.gitignore b/examples/hackernews/.gitignore
new file mode 100644
index 000000000000..7329a851d0ac
--- /dev/null
+++ b/examples/hackernews/.gitignore
@@ -0,0 +1,20 @@
+# build output
+dist/
+.output/
+
+# 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
diff --git a/examples/hackernews/.vscode/extensions.json b/examples/hackernews/.vscode/extensions.json
new file mode 100644
index 000000000000..22a15055d638
--- /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 000000000000..d6422097621f
--- /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 000000000000..aeb16745ab51
--- /dev/null
+++ b/examples/hackernews/README.md
@@ -0,0 +1,57 @@
+# Astro Starter Kit: Hackernews
+
+```
+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)
+
+> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
+
+## 🚀 Project Structure
+
+Inside of your Astro project, you'll see the following folders and files:
+
+```
+/
+├── 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:3000` |
+| `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 000000000000..68ba7fac5876
--- /dev/null
+++ b/examples/hackernews/astro.config.mjs
@@ -0,0 +1,10 @@
+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 000000000000..ae79e4bfda7c
--- /dev/null
+++ b/examples/hackernews/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "@example/hackernews",
+ "type": "module",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "dev": "astro dev",
+ "start": "astro dev",
+ "check": "astro check && tsc",
+ "build": "astro build",
+ "preview": "astro preview",
+ "astro": "astro"
+ },
+ "dependencies": {
+ "@astrojs/node": "^2.0.1",
+ "astro": "^1.5.2"
+ }
+}
diff --git a/examples/hackernews/public/favicon.svg b/examples/hackernews/public/favicon.svg
new file mode 100644
index 000000000000..0f3906297879
--- /dev/null
+++ b/examples/hackernews/public/favicon.svg
@@ -0,0 +1,13 @@
+
diff --git a/examples/hackernews/sandbox.config.json b/examples/hackernews/sandbox.config.json
new file mode 100644
index 000000000000..9178af77d7de
--- /dev/null
+++ b/examples/hackernews/sandbox.config.json
@@ -0,0 +1,11 @@
+{
+ "infiniteLoopProtection": true,
+ "hardReloadOnChange": false,
+ "view": "browser",
+ "template": "node",
+ "container": {
+ "port": 3000,
+ "startScript": "start",
+ "node": "14"
+ }
+}
diff --git a/examples/hackernews/src/components/Comment.astro b/examples/hackernews/src/components/Comment.astro
new file mode 100644
index 000000000000..50fa3e9e4217
--- /dev/null
+++ b/examples/hackernews/src/components/Comment.astro
@@ -0,0 +1,59 @@
+---
+import For from './For.astro';
+import Show from './Show.astro';
+import Toggle from './Toggle.astro';
+import type { IComment } from '../types.js';
+
+export interface Props {
+ comment: IComment;
+}
+
+const { comment } = Astro.props;
+---
+
+
+
+
+
+
+ {(comment: IComment) => }
+
+
+
+
+
diff --git a/examples/hackernews/src/components/For.astro b/examples/hackernews/src/components/For.astro
new file mode 100644
index 000000000000..b784f8f8a39e
--- /dev/null
+++ b/examples/hackernews/src/components/For.astro
@@ -0,0 +1,21 @@
+---
+import Show from './Show.astro';
+
+export interface Props {
+ each: Iterable;
+}
+
+const { each } = Astro.props;
+---
+
+{(async function* () {
+ for await (const value of each) {
+ let html = await Astro.slots.render('default', [value]);
+ yield ;
+ yield '\n';
+ }
+})()}
+
+
+
+
diff --git a/examples/hackernews/src/components/Nav.astro b/examples/hackernews/src/components/Nav.astro
new file mode 100644
index 000000000000..10bf0f899150
--- /dev/null
+++ b/examples/hackernews/src/components/Nav.astro
@@ -0,0 +1,97 @@
+---
+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' },
+];
+---
+
+
+
+
diff --git a/examples/hackernews/src/components/Show.astro b/examples/hackernews/src/components/Show.astro
new file mode 100644
index 000000000000..7e08877849ff
--- /dev/null
+++ b/examples/hackernews/src/components/Show.astro
@@ -0,0 +1,9 @@
+---
+export interface Props {
+ when: T | number | boolean | undefined | null;
+}
+
+const { when } = Astro.props;
+---
+
+{!!when ? : }
diff --git a/examples/hackernews/src/components/Story.astro b/examples/hackernews/src/components/Story.astro
new file mode 100644
index 000000000000..ee43bab17b3a
--- /dev/null
+++ b/examples/hackernews/src/components/Story.astro
@@ -0,0 +1,77 @@
+---
+import Show from './Show.astro';
+import type { IStory } from '../types.js';
+
+export interface Props {
+ story: IStory;
+}
+
+const { story } = Astro.props;
+---
+
+
+ {story.points}
+
+
+
+ {story.title}
+
+ ({story.domain})
+ {story.title}
+
+
+
+
+
+ by {story.user}{' '}
+ {story.time_ago}{' '}|{' '}
+
+ {story.comments_count ? `${story.comments_count} comments` : 'discuss'}
+
+ {story.time_ago}
+
+
+
+
+ {story.type}
+
+
+
+
diff --git a/examples/hackernews/src/components/Toggle.astro b/examples/hackernews/src/components/Toggle.astro
new file mode 100644
index 000000000000..87b686981ed0
--- /dev/null
+++ b/examples/hackernews/src/components/Toggle.astro
@@ -0,0 +1,78 @@
+---
+export interface Props {
+ open?: boolean;
+}
+
+const { open = false } = Astro.props;
+---
+
+
+
+
+
+
+
+
+
diff --git a/examples/hackernews/src/env.d.ts b/examples/hackernews/src/env.d.ts
new file mode 100644
index 000000000000..f964fe0cffd8
--- /dev/null
+++ b/examples/hackernews/src/env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/examples/hackernews/src/layouts/Layout.astro b/examples/hackernews/src/layouts/Layout.astro
new file mode 100644
index 000000000000..b1630da8aa34
--- /dev/null
+++ b/examples/hackernews/src/layouts/Layout.astro
@@ -0,0 +1,35 @@
+---
+import Nav from '../components/Nav.astro';
+---
+
+
+
+
+
+
+
+ Astro - Hacker News
+
+
+
+
+
+
+
+
diff --git a/examples/hackernews/src/lib/api.ts b/examples/hackernews/src/lib/api.ts
new file mode 100644
index 000000000000..61fc2f9abc0f
--- /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(`Recevied 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 000000000000..fa227e0c12ca
--- /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[];
+---
+
+
+
+
+
+
+
+ {(story: IStory) => }
+
+
+
+
+
+
+
diff --git a/examples/hackernews/src/pages/stories/[id].astro b/examples/hackernews/src/pages/stories/[id].astro
new file mode 100644
index 000000000000..6cd17ea456bf
--- /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;
+---
+
+
+
+
+
+
+ {story.comments_count ? story.comments_count + ' comments' : 'No comments yet.'}
+
+
+
+
+
+
+
diff --git a/examples/hackernews/src/pages/users/[id].astro b/examples/hackernews/src/pages/users/[id].astro
new file mode 100644
index 000000000000..9b43c695852d
--- /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;
+---
+
+
+
+
+
+ User not found.
+ User : {user.id}
+
+
+ submissions |{' '}
+ comments
+
+
+
+
+
+
+
diff --git a/examples/hackernews/src/types.ts b/examples/hackernews/src/types.ts
new file mode 100644
index 000000000000..e27ee85e461a
--- /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 000000000000..d78f81ec4e8e
--- /dev/null
+++ b/examples/hackernews/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "astro/tsconfigs/base"
+}
diff --git a/package.json b/package.json
index 1b4808a042e6..aa33623a82df 100644
--- a/package.json
+++ b/package.json
@@ -91,7 +91,7 @@
"execa": "^6.1.0",
"organize-imports-cli": "^0.10.0",
"prettier": "^2.7.0",
- "prettier-plugin-astro": "^0.3.0",
+ "prettier-plugin-astro": "^0.4.0",
"pretty-bytes": "^6.0.0",
"tiny-glob": "^0.2.9",
"turbo": "1.2.5",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index fa4760019953..f2191a9a43ac 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -30,7 +30,7 @@ importers:
execa: ^6.1.0
organize-imports-cli: ^0.10.0
prettier: ^2.7.0
- prettier-plugin-astro: ^0.3.0
+ prettier-plugin-astro: ^0.4.0
pretty-bytes: ^6.0.0
tiny-glob: ^0.2.9
turbo: 1.2.5
@@ -53,7 +53,7 @@ importers:
execa: 6.1.0
organize-imports-cli: 0.10.0
prettier: 2.7.1
- prettier-plugin-astro: 0.3.0
+ prettier-plugin-astro: 0.4.1
pretty-bytes: 6.0.0
tiny-glob: 0.2.9
turbo: 1.2.5
@@ -230,6 +230,14 @@ importers:
astro: link:../../packages/astro
vue: 3.2.41
+ examples/hackernews:
+ specifiers:
+ '@astrojs/node': ^2.0.1
+ astro: ^1.5.2
+ dependencies:
+ '@astrojs/node': link:../../packages/integrations/node
+ astro: link:../../packages/astro
+
examples/integration:
specifiers:
astro: ^1.6.2
@@ -15850,8 +15858,8 @@ packages:
fast-diff: 1.2.0
dev: true
- /prettier-plugin-astro/0.3.0:
- resolution: {integrity: sha512-ApEy5U3FPdylBa72L8k7W/h/zgegAyuHppDu9geVwFHzaaEQpaRCrT3Wt0nc6tNcbDaOgotBfcaUzMr0Cw6a8w==}
+ /prettier-plugin-astro/0.4.1:
+ resolution: {integrity: sha512-AvYyg4WDnhmqH5S85EFgJAMU20p1lLypVt4sn4tuGW+lnPfWr70SaHZD9gevx20o3FnALgZxDu1enFSkN/MSwQ==}
engines: {node: ^14.15.0 || >=16.0.0, npm: '>=6.14.0'}
dependencies:
'@astrojs/compiler': 0.19.0