Manager / Engineer: Platform team
---
import Counter from '../components/Counter.jsx'
const title = 'Counter'
---
<html lang="en">
<head>
<title>{ title }</title>
</head>
<body>
<h1>Counter app</h1>
<Counter />
</body>
</html>
---
import Counter from '../components/Counter.jsx'
const title = 'Counter'
---
<html lang="en">
<head>
<title>{ title }</title>
</head>
<body>
<h1>Counter app</h1>
<Counter client:idle />
</body>
</html>
.
└── my-company-site/
└── src/
├── components/
│ ├── Counter.jsx
│ ├── Header.jsx
│ ├── Footer.vue
│ └── Sidebar.astro
└── pages/
├── index.astro
├── about.astro
└── blog/
├── first-post.md
├── second-post.md
└── fancy.mdx
.
└── my-company-site/
└── src/
├── layouts/
└── main.astro
├── pages/
│ ├── index.astro
├── blog/
│ ├── introduction.md
│ ├── project-status.mdx
│ └── migrating-to-astro.mdx
└── team/
├── rachel.md
├── timothy.md
└── dominique.md
.
└── my-company-site/
└── src/
├── pages/
│ ├── index.astro
│ └── blog/
│ └── [...slug].astro
└── content/
├── blog/
│ ├── introduction.md
│ ├── project-status.mdx
│ ├── migrating-to-astro.mdx
│ └── another-article.mdoc
└── team/
├── rachel.md
├── timothy.md
└── dominique.md
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
schema: z.object({
// Define your expected frontmatter properties
title: z.string(),
// Mark certain properties as optional
draft: z.boolean().optional(),
// Transform datestrings to full Date objects
publishDate: z.string().transform(
(val) => new Date(val)
),
// Improve SEO with descriptive warnings
description: z.string().max(
160,
'Short descriptions have better SEO!'
),
// ...
}),
});
export const collections = { blog };
---
title: Astro is Amazing
publishDate: "6/5/2023"
---
# Astro is Amazing
This is a blog post about how amazing Astro is.
---
import { getCollection } from 'astro:content';
// Get all `src/content/blog/` entries
const blogPosts = await getCollection('blog');
---
<ul>
{ blogPosts.map(post => (
<li>
<a href={post.slug}>{ post.data.title }</a>
</li>
))}
</ul>
---
import { Image } from 'astro:assets';
import localImage from '../assets/logo.png';
const localAlt = 'The Astro Logo';
---
<Image
src={localImage}
width={300}
height={350}
alt={localAlt}
/>
---
import { getImage } from "astro:assets"
import myImage from "../assets/penguin.png"
const bg = await getImage({
src: myImage
})
---
<div style={`background-image: url(${bg.src})`}></div>
Important metric for Core Web Vitals
post.md
---
title: "My first blog post"
cover: "./firstpostcover.jpeg"
coverAlt: "A photograph of a sunset behind a mountain range"
---
This is a blog post
const blogCollection = defineCollection({
schema: ({ image }) => z.object({
title: z.string(),
cover: image(),
coverAlt: z.string(),
}),
});
config.ts
post.md
---
title: "My first blog post"
cover: "./firstpostcover.jpeg"
coverAlt: "A photograph of a sunset behind a mountain range"
---
This is a blog post
const blogCollection = defineCollection({
schema: ({ image }) => z.object({
title: z.string(),
cover: image().refine(
(img) => img.width == 800 && img.height == 600, {
message: "Cover needs to be 800x600 pixels!",
}),
coverAlt: z.string(),
}),
});
config.ts
---
import { getEntryBySlug } from 'astro:content'
import { Image } from 'astro:assets'
const post = await getEntryBySlug('blog', Astro.params.slug)
---
<h1>{ post.data.title }</h1>
<Image
src={post.data.cover}
alt={post.data.coverAlt} />
Middleware
Redirects
CDN hosted assets
HTML minification
CSS specifity control
Custom client directives
Hybrid rendering
Vercel image service
Images in MDX
Markdoc
Image build cache
Data collections
Collection references
CSS inlining
SSR in sitemaps
---
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" %}
Markdoc
import { defineMarkdocConfig } from '@astrojs/markdoc/config';
import Aside from './src/components/Aside.astro';
export default defineMarkdocConfig({
tags: {
aside: {
render: Aside,
attributes: {
type: { type: String },
title: { type: String },
},
},
},
});
Markdoc
markdoc.config.mjs
<link href="/_astro/assets/home.css" rel="stylesheet">
<link href="https://cdn.example.com/_astro/assets/home.css" rel="stylesheet">
export default defineConfig({
build: {
assetsPrefix: 'https://cdn.example.com'
}
});
<html lang="en" class="astro-J7PV25F6"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>minimum html</title><link rel="stylesheet" href="/_astro/index.1a1a72db.css" /></head><body class="astro-J7PV25F6"><div>2</div><main class="astro-J7PV25F6"></main><div>3</div></body></html>
---
import Aside from './aside.astro'
import Page from './page.astro'
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>minimum html</title>
<body>
<Aside/>
<Page />
</body>
</html>
---
import AsyncComponent from '../AsyncComponent.astro'
---
<AsyncComponent delay={1000} />
<AsyncComponent delay={1000} />
<AsyncComponent delay={1000} />
import { defineConfig } from 'astro/config';
import sitemaps from '@astrojs/sitemaps';
export default defineConfig({
integrations: [sitemaps()]
});
<style>
article {
margin-block: 2rem;
}
</style>
<article></article>
article:where(.astro-1234) {
margin-block: 2rem;
}
Current default
article.astro-1234 {
margin-block: 2rem;
}
Class strategy
# Favorite paintings
![A starry night sky](../../assets/stars.png)
// astro.config.mjs
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/serverless';
export default defineConfig({
output: 'server',
adapter: vercel({
imageService: true
})
});
// astro.config.mjs
import { defineConfig } from 'astro/config';
import vercel from '@astrojs/vercel/serverless';
export default defineConfig({
output: 'server',
adapter: vercel({
imageConfig: {
sizes: [320, 640, 1280]
}
})
});
.
└── my-company-site/
└── src/
├── pages/
│ ├── index.astro
│ └── blog/
│ └── [...slug].astro
└── content/
├── blog/
│ ├── introduction.md
│ ├── project-status.mdx
│ ├── migrating-to-astro.mdx
│ └── another-article.mdoc
└── team/
├── rachel.json
├── timothy.json
└── dominique.json
import { defineCollection, z } from 'astro:content';
const authors = defineCollection({
type: 'data',
schema: z.object({
name: z.string(),
portfolio: z.string().url(),
}),
});
export const collections = { authors };
Data collections
{
"name": "Matthew Phillips",
"portfolio": "https://matthewphillips.info"
}
Data collections
.
└── my-company-site/
└── src/
└── content/
├── blog/
└── authors/
├── matthew.json
├── fred.json
└── erika.json
import { defineCollection, z, reference } from 'astro:content'
const authors = defineCollection({
type: 'data',
schema: z.object({
name: z.string(),
portfolio: z.string().url(),
}),
})
const blog = defineCollection({
schema: z.object({
title: z.string(),
author: reference('authors'),
})
})
export const collections = { authors, blog }
One-to-one
---
title: Welcome Post
author: matthew
---
# Welcome to the Site!
One-to-one
---
import { getEntry, getEntries } from 'astro:content';
const welcomePost = await getEntry('blog', 'welcome');
const author = await getEntry(welcomePost.data.author);
---
<h1>Author: {author.data.name}</h1>
<a href={author.data.portfolio}>Portfolio</a>
One-to-one
import { defineCollection, z, reference } from 'astro:content'
const blog = defineCollection({
schema: z.object({
title: z.string(),
relatedPosts: z.array(reference('blog')).optional(),
})
})
export const collections = { authors, blog }
One-to-many
---
title: Welcome Post
relatedPosts:
- related-1
- related-2
---
# Welcome to the Future!
One-to-many
---
import { getEntry, getEntries } from 'astro:content';
const welcomePost = await getEntry('blog', 'welcome');
const relatedPosts = await getEntries(welcomePost.data.relatedPosts ?? []);
---
<h1>Related posts</h1>
<ul class="related-posts">
{
relatedPosts.map((post) => (
<li>
<a href={`/blog/${post.slug}`}>{post.data.title}</a>
</li>
))
}
</ul>
One-to-many
import { defineConfig } from 'astro/config';
export default defineConfig({
output: 'hybrid'
});
Hybrid rendering
---
export const prerender = false
---
<!-- ... -->
Hybrid rendering
index.astro
export const prerender = false
export function post({ request }) {
/* ... */
}
api.ts
Modify the request and response
export const onRequest = ({ url }, next) => {
if(url.pathname === '/testing' && import.meta.env.MODE !== 'development') {
return new Response(null, {
status: 405
});
}
return next();
};
Middleware
const auth = async ({ cookies, locals }, next) => {
if(!cookies.has('sid')) {
return new Response(null, {
status: 405 // Not allowed
});
}
let sessionId = cookies.get('sid');
let user = await getUserFromSession(sessionId);
if(!user) {
return new Response(null, {
status: 405 // Not allowed
});
}
locals.user = user;
return next();
};
export {
auth as onRequest
};
Middleware
---
import UserProfile from '../components/UserProfile.tsx';
const { user } = Astro.locals;
---
<UserProfile user={user} client:load />
(Usage in component)
Middleware
import { sequence } from 'astro/middleware';
import { auth } from './auth';
const allowed = async({ locals, url }, next) => {
if(url.pathname === '/admin') {
if(locals.user.isAdmin) {
return next();
} else {
return new Response('Not allowed', {
status: 405
});
}
}
return next();
};
export const onRequest = sequence(auth, allowed);
Middleware
export const onRequest = async (context, next) {
let response = await next();
if(response.headers.get('content-type') === 'text/html') {
let html = await response.text();
let minified = await minifyHTML(html);
return new Response(minified, {
status: 200,
headers: response.headers
});
}
return response;
}
Middleware
Custom client directives
<Counter client:load />
<Counter client:idle />
<Counter client:visible />
<Counter client:media="(max-width: 50em)" />
Custom client directives
<Counter client:click />
export default (load, opts, el) => {
addEventListener('click', async () => {
const hydrate = await load()
await hydrate()
}, { once: true })
}
1. Define behavior
import { defineConfig } from 'astro/config'
import click from '@matthewp/astro-click'
export default defineConfig({
integrations: [click()]
})
2. Add integration
3. Use anywhere
import { defineConfig } from 'astro/config';
export default defineConfig({
redirects: {
'/home': '/welcome',
'/blog/[..slug]': '/articles/[...slug]'
}
});
Redirects
/other / 301
/home /welcome 301
/team/articles/* /blog/* 301
Redirects
{"version":3,"routes":[
{"src":"/\\/blog(?:\\/(.*?))?","headers":{"Location":"/team/articles/$1"},"status":301},
{"src":"/\\/two","headers":{"Location":"/"},"status":301},
{"src":"/\\/one","headers":{"Location":"/"},"status":301}
]}
_redirects
config.json
<link rel="stylesheet" href="/main.css">
<link rel="stylesheet" href="/shared-one.css">
<link rel="stylesheet" href="/shared-two.css">
<link rel="stylesheet" href="/shared-two.css">
<link rel="stylesheet" href="/main.css">
<link rel="stylesheet" href="/shared-one.css">
<style>.shared-two{/* ... */}</style>
<style>.shared-three{/* ... */}</style>
inlineStylesheets: 'never'
inlineStylesheets: 'auto'
<style>body{/* ... */}</style>
<style>.shared-one{/* ... */}</style>
<style>.shared-two{/* ... */}</style>
<style>.shared-three{/* ... */}</style>
inlineStylesheets: 'always'