Skip to content

Commit

Permalink
Implements redirects, headers for SSR (#2798)
Browse files Browse the repository at this point in the history
* Implements redirects, headers for SSR

* Move away from an explicit Request

* Properly handle endpoint routes in the build

* chore(lint): ESLint fix

* Update based on review comments

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
matthewp and github-actions[bot] authored Mar 16, 2022
1 parent 8f13b3d commit 4c25a1c
Show file tree
Hide file tree
Showing 33 changed files with 697 additions and 108 deletions.
5 changes: 5 additions & 0 deletions .changeset/fuzzy-lies-nail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Implement APIs for headers for SSR flag
8 changes: 7 additions & 1 deletion examples/ssr/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@ export default defineConfig({
renderers: ['@astrojs/renderer-svelte'],
vite: {
server: {
cors: {
credentials: true
},
proxy: {
'/api': 'http://localhost:8085',
'/api': {
target: 'http://127.0.0.1:8085',
changeOrigin: true,
}
},
},
},
Expand Down
5 changes: 4 additions & 1 deletion examples/ssr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"private": true,
"scripts": {
"dev-api": "node server/dev-api.mjs",
"dev": "npm run dev-api & astro dev --experimental-ssr",
"dev-server": "astro dev --experimental-ssr",
"dev": "concurrently \"npm run dev-api\" \"astro dev --experimental-ssr\"",
"start": "astro dev",
"build": "echo 'Run pnpm run build-ssr instead'",
"build-ssr": "node build.mjs",
Expand All @@ -13,6 +14,8 @@
"devDependencies": {
"@astrojs/renderer-svelte": "^0.5.2",
"astro": "^0.24.3",
"concurrently": "^7.0.0",
"lightcookie": "^1.0.25",
"unocss": "^0.15.6",
"vite-imagetools": "^4.0.3"
}
Expand Down
52 changes: 52 additions & 0 deletions examples/ssr/server/api.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import fs from 'fs';
import lightcookie from 'lightcookie';

const dbJSON = fs.readFileSync(new URL('./db.json', import.meta.url));
const db = JSON.parse(dbJSON);
const products = db.products;
const productMap = new Map(products.map((product) => [product.id, product]));

// Normally this would be in a database.
const userCartItems = new Map();

const routes = [
{
match: /\/api\/products\/([0-9])+/,
Expand Down Expand Up @@ -32,6 +37,53 @@ const routes = [
res.end(JSON.stringify(products));
},
},
{
match: /\/api\/cart/,
async handle(req, res) {
res.writeHead(200, {
'Content-Type': 'application/json'
});
let cookie = req.headers.cookie;
let userId = cookie ? lightcookie.parse(cookie)['user-id'] : '1'; // default for testing
if(!userId || !userCartItems.has(userId)) {
res.end(JSON.stringify({ items: [] }));
return;
}
let items = userCartItems.get(userId);
let array = Array.from(items.values());
res.end(JSON.stringify({ items: array }));
}
},
{
match: /\/api\/add-to-cart/,
async handle(req, res) {
let body = '';
req.on('data', chunk => body += chunk);
return new Promise(resolve => {
req.on('end', () => {
let cookie = req.headers.cookie;
let userId = lightcookie.parse(cookie)['user-id'];
let msg = JSON.parse(body);

if(!userCartItems.has(userId)) {
userCartItems.set(userId, new Map());
}

let cart = userCartItems.get(userId);
if(cart.has(msg.id)) {
cart.get(msg.id).count++;
} else {
cart.set(msg.id, { id: msg.id, name: msg.name, count: 1 });
}

res.writeHead(200, {
'Content-Type': 'application/json',
});
res.end(JSON.stringify({ ok: true }));
});
});
}
}
];

export async function apiHandler(req, res) {
Expand Down
7 changes: 4 additions & 3 deletions examples/ssr/server/server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ async function handle(req, res) {
const route = app.match(req);

if (route) {
const html = await app.render(req, route);

res.writeHead(200, {
/** @type {Response} */
const response = await app.render(req, route);
const html = await response.text();
res.writeHead(response.status, {
'Content-Type': 'text/html; charset=utf-8',
'Content-Length': Buffer.byteLength(html, 'utf-8'),
});
Expand Down
51 changes: 47 additions & 4 deletions examples/ssr/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,25 @@ interface Product {
image: string;
}

//let origin: string;
const { mode } = import.meta.env;
const origin = mode === 'develepment' ? `http://localhost:3000` : `http://localhost:8085`;
interface User {
id: number;
}

interface Cart {
items: Array<{
id: number;
name: string;
count: number;
}>;
}

const { MODE } = import.meta.env;
const origin = MODE === 'development' ? `http://127.0.0.1:3000` : `http://127.0.0.1:8085`;

async function get<T>(endpoint: string, cb: (response: Response) => Promise<T>): Promise<T> {
const response = await fetch(`${origin}${endpoint}`);
const response = await fetch(`${origin}${endpoint}`, {
credentials: 'same-origin'
});
if (!response.ok) {
// TODO make this better...
return null;
Expand All @@ -31,3 +44,33 @@ export async function getProduct(id: number): Promise<Product> {
return product;
});
}

export async function getUser(): Promise<User> {
return get<User>(`/api/user`, async response => {
const user: User = await response.json();
return user;
});
}

export async function getCart(): Promise<Cart> {
return get<Cart>(`/api/cart`, async response => {
const cart: Cart = await response.json();
return cart;
});
}

export async function addToUserCart(id: number | string, name: string): Promise<void> {
await fetch(`${origin}/api/add-to-cart`, {
credentials: 'same-origin',
method: 'POST',
mode: 'no-cors',
headers: {
'Content-Type': 'application/json',
'Cache': 'no-cache'
},
body: JSON.stringify({
id,
name
})
});
}
9 changes: 8 additions & 1 deletion examples/ssr/src/components/AddToCart.svelte
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
<script>
import { addToUserCart } from '../api';
export let id = 0;
export let name = '';
function addToCart() {
function notifyCartItem(id) {
window.dispatchEvent(new CustomEvent('add-to-cart', {
detail: id
}));
}
async function addToCart() {
await addToUserCart(id, name);
notifyCartItem(id);
}
</script>
<style>
button {
Expand Down
6 changes: 4 additions & 2 deletions examples/ssr/src/components/Cart.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
.cart {
display: flex;
align-items: center;
text-decoration: none;
color: inherit;
}
.cart :first-child {
margin-right: 5px;
Expand All @@ -26,7 +28,7 @@
}
</style>
<svelte:window on:add-to-cart={onAddToCart}/>
<div class="cart">
<a href="/cart" class="cart">
<span class="material-icons cart-icon">shopping_cart</span>
<span class="count">{count}</span>
</div>
</a>
20 changes: 19 additions & 1 deletion examples/ssr/src/components/Header.astro
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
---
import TextDecorationSkip from './TextDecorationSkip.astro';
import Cart from './Cart.svelte';
import { getCart } from '../api';
const cart = await getCart();
const cartCount = cart.items.reduce((sum, item) => sum + item.count, 0);
---
<style>
@import url('https://fonts.googleapis.com/css2?family=Lobster&display=swap');
Expand All @@ -21,11 +25,25 @@ import Cart from './Cart.svelte';
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">
<Cart client:idle />
<a href="/login">
<span class="material-icons">
login
</span>
</a>
<Cart client:idle count={cartCount} />
</div>
</header>
8 changes: 8 additions & 0 deletions examples/ssr/src/models/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import lightcookie from 'lightcookie';


export function isLoggedIn(request: Request): boolean {
const cookie = request.headers.get('cookie');
const parsed = lightcookie.parse(cookie);
return 'user-id' in parsed;
}
47 changes: 47 additions & 0 deletions examples/ssr/src/pages/cart.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
import Header from '../components/Header.astro';
import Container from '../components/Container.astro';
import { getCart } from '../api';
import { isLoggedIn } from '../models/user';
if(!isLoggedIn(Astro.request)) {
return Astro.redirect('/');
}
// They must be logged in.
const user = { name: 'test'}; // getUser?
const cart = await getCart();
---
<html>
<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>
30 changes: 30 additions & 0 deletions examples/ssr/src/pages/login.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
import Header from '../components/Header.astro';
import Container from '../components/Container.astro';
---
<html>
<head>
<title>Online Store</title>
<style>
h1 {
font-size: 36px;
}
</style>
</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>
</Container>
</body>
</html>
10 changes: 10 additions & 0 deletions examples/ssr/src/pages/login.form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

export function post(params, request) {
return new Response(null, {
status: 301,
headers: {
'Location': '/',
'Set-Cookie': 'user-id=1; Path=/; Max-Age=2592000'
}
});
}
2 changes: 1 addition & 1 deletion examples/ssr/src/pages/products/[id].astro
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const product = await getProduct(id);
<figure>
<img src={product.image} />
<figcaption>
<AddToCart id={id} client:idle />
<AddToCart client:idle id={id} name={product.name} />
<p>Description here...</p>
</figcaption>
</figure>
Expand Down
8 changes: 8 additions & 0 deletions examples/ssr/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"lib": ["ES2015", "DOM"],
"module": "ES2022",
"moduleResolution": "node",
"types": ["astro/env"]
}
}
Loading

0 comments on commit 4c25a1c

Please sign in to comment.