docs: add Astro how-to reference from crash course analysis #6

Merged
clawdie merged 1 commit from docs/astro-howto into main 2026-05-31 14:35:08 +02:00
36 changed files with 1786 additions and 0 deletions

View file

@ -0,0 +1,52 @@
# Astro How-To — Project Index
Local documentation generated from the Astro Web Framework Crash Course
by James Q Quick (freeCodeCamp, 1h 16m).
## How This Was Built
1. **Audio extracted** from source video (ffmpeg, 16kHz mono WAV, 141 MB)
2. **Transcribed locally** with faster-whisper base model (CPU, 8.7× realtime)
3. **Structured by GLM-5-turbo** into topics, steps, commands, and screenshot candidates
4. **Contact sheet generated** (77 frames, one per 60s, 5-column grid)
Zero API keys used for media processing. All local.
## Files
| File | Description |
|------|-------------|
| [transcript_local.txt](transcript_local.txt) | Full local transcript (805 lines, timestamped) |
| [transcript.txt](transcript.txt) | YouTube transcript (fallback reference) |
| [docs/SUMMARY.md](docs/SUMMARY.md) | Course overview with all 33 topics |
| [docs/OUTLINE.md](docs/OUTLINE.md) | Timestamped topic index |
| [docs/HOWTO.md](docs/HOWTO.md) | 15-step how-to guide + key concepts |
| [docs/COMMANDS.md](docs/COMMANDS.md) | CLI commands, extensions, packages |
| [docs/QUESTIONS.md](docs/QUESTIONS.md) | Open questions and unclear sections |
| [docs/SCREENSHOTS.md](docs/SCREENSHOTS.md) | 17 recommended frame capture points |
| [contact-sheet/contact_sheet.jpg](contact-sheet/contact_sheet.jpg) | 77-frame grid (2408×4350px, 2.8 MB) |
| [contact-sheet/report.json](contact-sheet/report.json) | Contact sheet generation report |
| [screenshots/index.md](screenshots/index.md) | 17 targeted screenshots index |
| [screenshots/report.json](screenshots/report.json) | Screenshot extraction report |
| [run_manifest.json](run_manifest.json) | Machine-readable run summary |
| [artifacts.sha256](artifacts.sha256) | Checksums for key outputs |
## Scripts
| Script | Purpose |
|--------|---------|
| [scripts/generate_contact_sheet.py](scripts/generate_contact_sheet.py) | Contact sheet generator (OpenCV + Pillow) |
| [scripts/transcribe_local.py](scripts/transcribe_local.py) | Local transcription (faster-whisper) |
| [scripts/extract_screenshots.sh](scripts/extract_screenshots.sh) | Targeted frame extraction |
## Source
Video: /home/samob/Videos/Astro/Astro Web Framework Crash Course [e-hTm5VmofI].mp4
Size: 450 MB | Codec: vp9 1080p | Duration: 1:16:47 | Frames: 138,228
Original: untouched (read-only access only)
## Next Steps
- [x] Extract targeted screenshots from SCREENSHOTS.md (17 frames, 3.2 MB)
- [ ] Consider faster-whisper small/medium for better transcript quality
- [ ] Use this as base for Clawdie CMS skill

View file

@ -0,0 +1,30 @@
982de689f99ba79bb3d5f118894d5857ce9d5fe678da91211f21402ce1549543 transcript_local.txt
3e120745f2244506a9fd784adddab3b5a8a6b0a02192fa0b9f5c29d91de257da docs/COMMANDS.md
80699244254dcf48b58a04ef74a1c026c6ef9e9f47f186179097e0409806c1e1 docs/HOWTO.md
e9a0e30e444fdf30d05e60be4c49ea159b88c4d6bac094640334a50a0ec742af docs/OUTLINE.md
b8fd24369c2d2b44879579aacef52e924dde87c6484088d0c18df8e202a6d715 docs/QUESTIONS.md
10683c95bec327161459f332b5e4c29bd51f21f27adaacfd9d8e1fae8c5cf84b docs/SCREENSHOTS.md
c453000a320de466052b81a258514c88e259cf7d5b60257eddf883bf49b33ed4 docs/SUMMARY.md
f397c075ea0eef43dabfcbe1417ba1b3edd5d5cead290888d1080658ae989d90 contact-sheet/contact_sheet.jpg
46a40dc690775c171e61cee43befde037e33842cffcdb89d89356805654f667e contact-sheet/report.json
e81b5cc8f4984a560f157f80172101f021c0b87cd99aa85653f1ec799aa1be20 run_manifest.json
865605c167527ac5574b94ed9e3538efae4b9e628768592467e009dd8b9784cc README.md
fa47355b542c9d7ac0a2adacbe249cc4fd2eb23148e0e5cc75296fd9f9e638f1 screenshots/001_00-01-05_astro.new_website_showing_starter_template_options.jpg
14147682d20f9b178b3e64e45c2507a33339697d41d5b9d5726dd6f5f9abc37d screenshots/002_00-01-25_documentation_search_modal_opened_with_command+k.jpg
55af0d5e12e36bd625ab570273f412b45fde062c5cde68bedd375792598929fd screenshots/003_00-02-39_animated_astro_project_creation_wizard_in_terminal.jpg
c7c1e2f7895b3e88020e1532eed0c44fb86b0a72513e7d2d1c8a34ee5122091d screenshots/004_00-03-00_houston_mascot_exit_animation_after_project_creation.jpg
9e0fe78f8fe0f91ef434eaefa9feb7b35a103fcc4aacbff3938edcf2bf1123d3 screenshots/005_00-04-10_starter_application_at_localhost:4321.jpg
c15616f3480936157efab008df8f1c95d5e76cef88ccf0a83b32da2fb1b34b00 screenshots/006_00-08-38_vs_code_.astro_file_without_extension_showing_as_plain_text.jpg
8d05b01f54df4c246668c3369be4be58630d28fdf8e044523a1b9811bbe90986 screenshots/007_00-09-00_vs_code_.astro_file_with_astro_extension_showing_syntax_high.jpg
d6715f20d18d1a884bdd0008a64a7a50815833e8326e91b520fa1aca2b22c9d3 screenshots/008_00-10-17_astro.config.mjs_showing_tailwind()_in_integrations_array.jpg
a513e292082c55daa11a0a684debaa9bdcf228765589accaca992e7f7a4d974d screenshots/009_00-31-33_blog_listing_page_with_grid_of_post_cards.jpg
cc984dcd88af14110852a3c586ddb1f522debc67bf33a2c1c26411067caa7df4 screenshots/010_00-34-20_individual_blog_post_page_with_rendered_markdown.jpg
36feb459535eb2e2e439398e195a608ebbfc0f4f25a696b89148aece74056116 screenshots/011_00-51-01_browser_network_tab_showing_webp_images.jpg
8b0951795a815198446f857d97aa254849414f4f753f17db1f54b2b32bd8df06 screenshots/012_00-52-43_view_transitions_animation_between_pages.jpg
2158295d3643173e616ef584749561162b550e5bce055b856256586ffe81dcfa screenshots/013_00-54-10_mdx_file_with_inline_components.jpg
36ba3db82b03d8830f3e29eefb610c2b121db58dabacaf631cbbebe32ee7b70b screenshots/014_00-57-58_netlify_deploy_dashboard_showing_successful_build.jpg
b1ee2f3e1699c72d916f6b5b66f05fef468438ac38156048f4f31c7099c04df3 screenshots/015_00-58-50_vercel_deploy_dashboard.jpg
f1bf4a879fd507d3315caeb9e1540e849b823f131481d1b9e78b285d61c648f8 screenshots/016_01-05-17_api_endpoint_returning_json_in_browser.jpg
f7cac0aa05747c741e2da053c6ec4fa1610accf4f95c27e1ddf6281eab503173 screenshots/017_01-09-17_ssr_individual_post_page_loading_dynamically.jpg
a237b6dfb5a6d2f574bf68622f74397fd4957e10ee2087aaaca6eb452492199c screenshots/index.md
e00e12cbd2b9fed7a422d68629e6d3bf992a502046261dd7dfc895ba1f3eb5fa screenshots/report.json

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

View file

@ -0,0 +1,143 @@
#!/usr/bin/env python3
import os
import sys
import json
import time
import math
import argparse
import threading
import cv2
from PIL import Image
def read_with_timeout(cap, timeout=5.0):
result = [None, None]
exception = [None]
def target():
try:
result[0], result[1] = cap.read()
except Exception as e:
exception[0] = e
result[0] = False
thread = threading.Thread(target=target)
thread.daemon = True
thread.start()
thread.join(timeout=timeout)
if thread.is_alive():
return False, None
if exception[0]:
return False, None
return result[0], result[1]
def main():
parser = argparse.ArgumentParser(description="Generate a video contact sheet.")
parser.add_argument("--save-frames", action="store_true", help="Save individual frames")
args = parser.parse_args()
video_path = "/home/samob/Videos/Astro/Astro Web Framework Crash Course [e-hTm5VmofI].mp4"
output_dir = "/home/samob/ai/astro-howto/contact-sheet/"
frames_dir = os.path.join(output_dir, "contact_sheet_frames")
os.makedirs(output_dir, exist_ok=True)
if args.save_frames:
os.makedirs(frames_dir, exist_ok=True)
if not os.path.exists(video_path):
print(f"Error: Source video not found at {video_path}")
sys.exit(1)
start_time = time.time()
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
print("Error: Could not open video file.")
sys.exit(1)
fps = cap.get(cv2.CAP_PROP_FPS)
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
interval_frames = int(60 * fps)
targets = list(range(0, total_frames, interval_frames))
thumbnails = []
skipped = 0
count = 0
for target_pos in targets:
cap.set(cv2.CAP_PROP_POS_FRAMES, target_pos)
ret, frame = read_with_timeout(cap, timeout=5.0)
if not ret or frame is None:
skipped += 1
continue
try:
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
h, w = frame_rgb.shape[:2]
new_w = 480
new_h = int((new_w / w) * h)
resized = cv2.resize(frame_rgb, (new_w, new_h), interpolation=cv2.INTER_AREA)
pil_img = Image.fromarray(resized)
if args.save_frames:
frame_filename = f"frame_{count:03d}.jpg"
pil_img.save(os.path.join(frames_dir, frame_filename), quality=90)
thumbnails.append(pil_img)
count += 1
except Exception:
skipped += 1
cap.release()
if not thumbnails:
print("Error: No frames could be extracted.")
sys.exit(1)
cols = 5
thumb_w, thumb_h = thumbnails[0].size
padding = 2
rows = math.ceil(len(thumbnails) / cols)
sheet_w = cols * thumb_w + (cols - 1) * padding
sheet_h = rows * thumb_h + (rows - 1) * padding
sheet = Image.new('RGB', (sheet_w, sheet_h), (255, 255, 255))
for i, img in enumerate(thumbnails):
r = i // cols
c = i % cols
x = c * (thumb_w + padding)
y = r * (thumb_h + padding)
sheet.paste(img, (x, y))
output_path = os.path.join(output_dir, "contact_sheet.jpg")
sheet.save(output_path, quality=95)
extraction_time = time.time() - start_time
report = {
"frame_count": count,
"path": output_path,
"dimensions_px": [sheet_w, sheet_h],
"skipped_frames": skipped,
"extraction_time_s": round(extraction_time, 2)
}
report_path = os.path.join(output_dir, "report.json")
with open(report_path, 'w') as f:
json.dump(report, f, indent=4)
if not os.path.exists(output_path) or os.path.getsize(output_path) == 0:
print("Error: Output validation failed - file missing or empty.")
sys.exit(1)
print(f"Frames extracted: {count}")
print(f"Skipped: {skipped}")
print(f"Output path: {output_path}")
print(f"Dimensions: {sheet_w}x{sheet_h}px")
print(f"Time: {report['extraction_time_s']}s")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,10 @@
{
"frame_count": 77,
"path": "/home/samob/ai/astro-howto/contact-sheet/contact_sheet.jpg",
"dimensions_px": [
2408,
4350
],
"skipped_frames": 0,
"extraction_time_s": 7.49
}

View file

@ -0,0 +1,45 @@
# Astro — Commands & Tools Reference
## CLI Commands
- `code -r`
- `git init`
- `git push`
- `git remote add origin`
- `npm create astro@latest`
- `npm install -D @tailwindcss/typography`
- `npm run dev`
- `npx astro add mdx`
- `npx astro add netlify`
- `npx astro add tailwind`
- `npx astro add vercel`
- `nvm install 20`
- `nvm use 18`
## VS Code Extensions
| Extension | Purpose |
|-----------|---------|
| Astro (official) | Syntax highlighting, IntelliSense for .astro files |
| Astro Snippets | Code snippets for faster component creation |
| Houston | Theme + animated mascot icon in file explorer |
| Tailwind CSS IntelliSense | Autocomplete for Tailwind classes |
## Key Packages
| Package | Use |
|---------|-----|
| `astro` | Core framework |
| `@astrojs/tailwind` | Tailwind CSS integration |
| `@astrojs/mdx` | MDX support (JSX in Markdown) |
| `@astrojs/netlify` | Netlify SSR adapter (serverless functions) |
| `@astrojs/vercel` | Vercel SSR adapter (serverless functions) |
| `@tailwindcss/typography` | Markdown content prose styling |
| `zod` | Schema validation for content collections |
## Keyboard Shortcuts
| Shortcut | Action |
|----------|--------|
| Cmd+K (Mac) / Ctrl+K (Windows) | Open docs search modal |
| Cmd+Shift+F (Mac) / Ctrl+Shift+F (Windows) | Find and replace across files |

View file

@ -0,0 +1,140 @@
# Astro — Step-by-Step How-To Guide
Based on the freeCodeCamp crash course by James Q Quick.
## 1. Create New Project
From the Astro docs getting started page. Choose folder name, sample files,
Yes to dependencies, TypeScript (strict/recommended), Yes to Git.
`npm create astro@latest`
Requires Node.js 18+. Use NVM to manage:
`nvm use 18`
`nvm install 20`
## 2. Open in VS Code
`code -r <project-folder>`
## 3. Start Dev Server
Runs on http://localhost:4321
`npm run dev`
## 4. Add Tailwind CSS
Auto-generates tailwind.config and updates astro.config.mjs.
`npx astro add tailwind`
## 5. Create Content Config
`src/content/config.ts` with `defineCollection` and Zod schema for your
content type (e.g., posts with author, date, image, title).
## 6. Query Posts
```ts
import { getCollection } from 'astro:content';
const posts = await getCollection('posts');
```
## 7. Dynamic Routes
`pages/blog/[slug].astro` + export `getStaticPaths` function that returns
`{params: {slug}, props: {post}}` for each page.
## 8. Render Markdown Content
```ts
const { Content } = await post.render();
// In template: <Content />
```
## 9. Install Typography Plugin
For proper Markdown content styling:
`npm install -D @tailwindcss/typography`
Add to tailwind.config plugins array.
## 10. Use Astro Image Component
```ts
import { Image } from 'astro:assets';
```
Auto-converts to webp, resizes, lazy loads. Much smaller than raw images.
## 11. Add View Transitions
```ts
import { ViewTransitions } from 'astro:transitions';
```
Add `<ViewTransitions />` to layout head. One line for smooth page animations.
## 12. Add MDX Support
Enables .mdx files with inline JSX components and exported variables.
`npx astro add mdx`
## 13. Initialize Git & Push
`git init && git add . && git commit -m 'init'`
`git remote add origin <github-url> && git push -u origin main`
## 14. Deploy Static Site
- **Netlify:** Import GitHub repo → auto-detects Astro build
- **Vercel:** Import GitHub repo → auto-detects Astro settings
## 15. Deploy SSR
Add the appropriate adapter:
`npx astro add netlify` (deploys as Netlify Functions)
`npx astro add vercel` (deploys as serverless functions)
## Key Concepts
### File-Based Routing
Files in `src/pages/` map directly to URL routes:
- `index.astro``/`
- `blog.astro``/blog`
- `blog/[slug].astro``/blog/post-title`
### Component Structure
```
---
// Frontmatter: JS/TypeScript, imports, logic
---
<!-- Template: HTML, markdown, components -->
```
### Content Collections
`src/content/config.ts` defines schemas. Query with `getCollection()`.
### SSG vs SSR vs Hybrid
- **SSG** (default): HTML at build time, served from CDN
- **SSR**: HTML per request, can query databases, check auth
- **Hybrid**: Set `output: 'hybrid'`, add `export const prerender = true` for static pages
### API Endpoints
Create `.ts` files in `pages/api/` exporting `GET`/`POST` handlers.
Full backend capability — form submissions, JSON APIs, auth checks.
## Deployment Matrix
| Host | Static | SSR |
|------|--------|-----|
| Netlify | Import repo | `npx astro add netlify` |
| Vercel | Import repo | `npx astro add vercel` |

View file

@ -0,0 +1,39 @@
# Astro Crash Course — Timestamped Outline
| Timestamp | Topic |
|-----------|-------|
| 00:00 | Introduction to Astro |
| 00:29 | Course Overview |
| 01:05 | Project Creation |
| 02:01 | Node.js Requirement |
| 03:38 | Opening in VS Code |
| 03:42 | Dev Server |
| 04:10 | Project Structure |
| 04:54 | Astro Component Anatomy |
| 05:23 | Slots & Props |
| 06:17 | CSS in Astro |
| 07:37 | Astro Config |
| 08:38 | VS Code Setup |
| 09:55 | Tailwind CSS |
| 24:27 | Page Cleanup |
| 24:59 | Reusable Components |
| 26:17 | Content Collections |
| 29:12 | Zod Schema Config |
| 30:06 | Blog Listing Page |
| 31:33 | Post Components |
| 32:49 | Dynamic Routes |
| 34:20 | Rendering Content |
| 35:08 | Typography Plugin |
| 51:01 | Image Optimization |
| 52:43 | View Transitions |
| 54:10 | MDX Support |
| 56:31 | Git + GitHub |
| 57:58 | Netlify Deploy |
| 58:50 | Vercel Deploy |
| 59:50 | SSG vs SSR |
| 63:52 | Hybrid Mode |
| 65:17 | API Endpoints |
| 69:17 | SSR Deployment |
| 73:10 | Auth Possibilities |
33 topics | 0:00 → 1:16:43

View file

@ -0,0 +1,8 @@
# Astro Crash Course — Open Questions
- Header component code was pasted, not typed — exact Tailwind classes unclear
- Content config export syntax cut off in transcript — full object structure unclear
- The exact og:image meta tag implementation is vague
- "Versome"/"Vercel" pronunciation artifact at 00:47
- NVM install vs use — which versions are pre-installed?
- MDX component import path conventions not fully shown

View file

@ -0,0 +1,21 @@
# Suggested Screenshots
For visual documentation (extract frames from the video at these moments):
1. Astro.new website showing starter template options
2. Documentation search modal (Cmd+K / Ctrl+K)
3. Animated project creation wizard in terminal
4. Houston mascot exit animation after project creation
5. Starter app at localhost:4321
6. VS Code .astro file without extension (plain text)
7. VS Code .astro file with Astro extension (syntax highlighted)
8. astro.config.mjs showing integrations array with tailwind()
9. Blog listing page with grid of post cards
10. Individual blog post page with rendered Markdown
11. Browser Network tab showing webp images vs original JPEGs
12. View Transitions animation between pages
13. MDX file with inline components
14. Netlify deploy dashboard showing successful build
15. Vercel deploy dashboard
16. API endpoint returning JSON in browser
17. SSR individual post page loading dynamically

View file

@ -0,0 +1,56 @@
# Astro Web Framework — Crash Course Summary
**Source:** Astro Web Framework Crash Course by James Q Quick (freeCodeCamp)
**Duration:** 1h 16m | **Transcription:** local faster-whisper (base)
## What Astro Is
Astro is an all-in-one web framework for building fast, content-focused websites.
It defaults to static site generation (zero JS shipped by default) but can scale
up to server-side rendering, API endpoints, and full-stack applications.
## Course Topics
| Time | Topic | Description |
|------|-------|-------------|
| 00:00 | Introduction to Astro | All-in-one framework for content-focused websites |
| 00:29 | Course Overview | File-based routing, Markdown/MDX, TypeScript, SSR, view transitions, deployment |
| 01:05 | Project Creation | npm create astro@latest scaffolds new project interactively |
| 02:01 | Node.js Requirement | Astro 3.0 requires Node.js 18+, NVM recommended |
| 03:38 | Opening in VS Code | code -r reuses existing window |
| 03:42 | Dev Server | npm run dev starts on port 4321 |
| 04:10 | Project Structure | public/ for static assets, src/ for pages and components |
| 04:54 | Astro Component Anatomy | Frontmatter (--- fences) for JS + template for HTML |
| 05:23 | Slots & Props | Slot for child content, TypeScript interfaces for typed props |
| 06:17 | CSS in Astro | Scoped by default, is:global for global styles |
| 07:37 | Astro Config | astro.config.mjs for integrations, build settings, SSR |
| 08:38 | VS Code Setup | Astro extension (required), Snippets, Houston theme, Tailwind IntelliSense |
| 09:55 | Tailwind CSS | npx astro add tailwind auto-installs and configures |
| 24:27 | Page Cleanup | Removing default styles, updating page title |
| 24:59 | Reusable Components | H1 and Main wrapper components with Tailwind |
| 26:17 | Content Collections | Organized Markdown/MDX in src/content/ with type-safe schemas |
| 29:12 | Zod Schema Config | src/content/config.ts with defineCollection and Zod |
| 30:06 | Blog Listing Page | getCollection('posts') to query and display all posts |
| 31:33 | Post Components | PostList grid + Post card with image, title, preview |
| 32:49 | Dynamic Routes | pages/blog/[slug].astro with getStaticPaths |
| 34:20 | Rendering Content | post.render() → <Content /> for Markdown |
| 35:08 | Typography Plugin | @tailwindcss/typography for Markdown styling |
| 51:01 | Image Optimization | Astro Image component: auto webp, resize, lazy load |
| 52:43 | View Transitions | One component adds smooth page navigation animations |
| 54:10 | MDX Support | npx astro add mdx — inline JSX components in Markdown |
| 56:31 | Git + GitHub | git init, commit, push to remote |
| 57:58 | Netlify Deploy | Import GitHub repo, auto-detect build, deploy static |
| 58:50 | Vercel Deploy | Same import flow as Netlify |
| 59:50 | SSG vs SSR | Static (build-time HTML) vs Server-rendered (request-time) |
| 63:52 | Hybrid Mode | SSR default + export const prerender = true for static pages |
| 65:17 | API Endpoints | .ts files in pages/ export GET/POST handlers |
| 69:17 | SSR Deployment | npx astro add netlify or vercel adapters |
| 73:10 | Auth Possibilities | Cookie-based auth with SSR for full-stack apps |
**33 topics** spanning 0:00 → 1:16:43
## Key Takeaway
Astro lets you start with pure static HTML/Markdown (fast, simple) and
progressively add interactivity (SSR, API routes, auth) only when needed.
The `npx astro add` command makes integrations nearly one-click.

View file

@ -0,0 +1,103 @@
{
"run_id": "astro-howto-20260531",
"created": "2026-05-31T12:21:00+02:00",
"source": {
"path": "/home/samob/Videos/Astro/Astro Web Framework Crash Course [e-hTm5VmofI].mp4",
"size_bytes": 471704471,
"size_human": "449.9 MB",
"mtime_unix": 1750287293,
"mtime_iso": "2025-06-19T00:54:53+02:00",
"codec": "vp9",
"resolution": "1920x1080",
"fps": 30.0,
"duration_s": 4607.6,
"duration_human": "1:16:47",
"frame_count": 138228,
"audio_codec": "aac",
"audio_channels": 2,
"original_untouched": true
},
"pipeline": [
{
"stage": "audio_extraction",
"tool": "ffmpeg",
"command": "ffmpeg -i video.mp4 -vn -acodec pcm_s16le -ar 16000 -ac 1 audio.wav",
"output": "audio.wav",
"size_bytes": 147443712,
"size_human": "140.6 MB",
"format": "WAV 16kHz mono",
"duration_s": 5
},
{
"stage": "transcription",
"tool": "faster-whisper",
"model": "base",
"device": "cpu",
"compute_type": "int8",
"language": "en",
"vad_filter": true,
"beam_size": 5,
"output": "transcript_local.txt",
"lines": 805,
"size_bytes": 85547,
"audio_duration_s": 4608,
"processing_duration_s": 533,
"realtime_factor": 8.7
},
{
"stage": "topic_extraction",
"tool": "zot_rpc_adapter.py",
"model": "glm-5-turbo",
"provider": "zai",
"chunks": 3,
"output": "merged_structure.json",
"topics_extracted": 33,
"steps_extracted": 18,
"commands_extracted": 13
},
{
"stage": "documentation_generation",
"outputs": [
"docs/SUMMARY.md",
"docs/OUTLINE.md",
"docs/HOWTO.md",
"docs/COMMANDS.md",
"docs/QUESTIONS.md",
"docs/SCREENSHOTS.md"
]
},
{
"stage": "contact_sheet",
"tool": "generate_contact_sheet.py (OpenCV + Pillow)",
"script_path": "scripts/generate_contact_sheet.py",
"frames_extracted": 77,
"frames_skipped": 0,
"interval_s": 60,
"columns": 5,
"thumbnail_width_px": 480,
"output": "contact-sheet/contact_sheet.jpg",
"dimensions_px": [2408, 4350],
"size_bytes": 2932736,
"size_human": "2.8 MB",
"processing_duration_s": 7.49
},
{
"stage": "screenshot_extraction",
"tool": "ffmpeg",
"method": "hybrid (-ss after -i for frames 1-8, -ss before -i for 9-17)",
"frames_requested": 17,
"frames_extracted": 17,
"frames_failed": 0,
"output": "screenshots/",
"total_size_human": "3.2 MB",
"index": "screenshots/index.md",
"report": "screenshots/report.json"
}
],
"models_used": [
{"model": "faster-whisper-base", "purpose": "local transcription", "api_key_used": false},
{"model": "glm-5-turbo", "purpose": "topic extraction + script generation", "api_key_used": true, "provider": "zai"}
],
"api_keys_used_for_media": false,
"notes": "All media processing (ffmpeg, faster-whisper, OpenCV, Pillow) ran locally. Only GLM-5-turbo (topic extraction and script generation) used an API key via zot_rpc_adapter."
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

View file

@ -0,0 +1,26 @@
# Screenshots Index
17 frames extracted from Astro Web Framework Crash Course.
Source video: `/home/samob/Videos/Astro/Astro Web Framework Crash Course [e-hTm5VmofI].mp4`
| # | Timestamp | Description | File |
|---|-----------|-------------|------|
| 01 | 00:01:05 | Astro.new website showing starter template options | [001_00-01-05_astro.new_website_showing_starter_template_options.jpg](001_00-01-05_astro.new_website_showing_starter_template_options.jpg) |
| 02 | 00:01:25 | Documentation search modal opened with Command+K | [002_00-01-25_documentation_search_modal_opened_with_command+k.jpg](002_00-01-25_documentation_search_modal_opened_with_command+k.jpg) |
| 03 | 00:02:39 | Animated Astro project creation wizard in terminal | [003_00-02-39_animated_astro_project_creation_wizard_in_terminal.jpg](003_00-02-39_animated_astro_project_creation_wizard_in_terminal.jpg) |
| 04 | 00:03:00 | Houston mascot exit animation after project creation | [004_00-03-00_houston_mascot_exit_animation_after_project_creation.jpg](004_00-03-00_houston_mascot_exit_animation_after_project_creation.jpg) |
| 05 | 00:04:10 | Starter application at localhost:4321 | [005_00-04-10_starter_application_at_localhost:4321.jpg](005_00-04-10_starter_application_at_localhost:4321.jpg) |
| 06 | 00:08:38 | VS Code .astro file without extension showing as plain text | [006_00-08-38_vs_code_.astro_file_without_extension_showing_as_plain_text.jpg](006_00-08-38_vs_code_.astro_file_without_extension_showing_as_plain_text.jpg) |
| 07 | 00:09:00 | VS Code .astro file with Astro extension showing syntax highlighting | [007_00-09-00_vs_code_.astro_file_with_astro_extension_showing_syntax_high.jpg](007_00-09-00_vs_code_.astro_file_with_astro_extension_showing_syntax_high.jpg) |
| 08 | 00:10:17 | astro.config.mjs showing tailwind() in integrations array | [008_00-10-17_astro.config.mjs_showing_tailwind()_in_integrations_array.jpg](008_00-10-17_astro.config.mjs_showing_tailwind()_in_integrations_array.jpg) |
| 09 | 00:31:33 | Blog listing page with grid of post cards | [009_00-31-33_blog_listing_page_with_grid_of_post_cards.jpg](009_00-31-33_blog_listing_page_with_grid_of_post_cards.jpg) |
| 10 | 00:34:20 | Individual blog post page with rendered Markdown | [010_00-34-20_individual_blog_post_page_with_rendered_markdown.jpg](010_00-34-20_individual_blog_post_page_with_rendered_markdown.jpg) |
| 11 | 00:51:01 | Browser Network tab showing webp images vs original JPEGs | [011_00-51-01_browser_network_tab_showing_webp_images.jpg](011_00-51-01_browser_network_tab_showing_webp_images.jpg) |
| 12 | 00:52:43 | View Transitions animation between pages | [012_00-52-43_view_transitions_animation_between_pages.jpg](012_00-52-43_view_transitions_animation_between_pages.jpg) |
| 13 | 00:54:10 | MDX file with inline components | [013_00-54-10_mdx_file_with_inline_components.jpg](013_00-54-10_mdx_file_with_inline_components.jpg) |
| 14 | 00:57:58 | Netlify deploy dashboard showing successful build | [014_00-57-58_netlify_deploy_dashboard_showing_successful_build.jpg](014_00-57-58_netlify_deploy_dashboard_showing_successful_build.jpg) |
| 15 | 00:58:50 | Vercel deploy dashboard | [015_00-58-50_vercel_deploy_dashboard.jpg](015_00-58-50_vercel_deploy_dashboard.jpg) |
| 16 | 01:05:17 | API endpoint returning JSON in browser | [016_01-05-17_api_endpoint_returning_json_in_browser.jpg](016_01-05-17_api_endpoint_returning_json_in_browser.jpg) |
| 17 | 01:09:17 | SSR individual post page loading dynamically | [017_01-09-17_ssr_individual_post_page_loading_dynamically.jpg](017_01-09-17_ssr_individual_post_page_loading_dynamically.jpg) |
**Total:** 17 frames, 3.2 MB

View file

@ -0,0 +1,31 @@
{
"stage": "screenshot_extraction",
"tool": "ffmpeg",
"source_video": "/home/samob/Videos/Astro/Astro Web Framework Crash Course [e-hTm5VmofI].mp4",
"output_dir": "screenshots/",
"frames_requested": 17,
"frames_extracted": 17,
"frames_failed": 0,
"total_size_bytes": 3355443,
"total_size_human": "3.2 MB",
"seeking_method": "hybrid (frames 1-8: -ss after -i for accuracy; frames 9-17: -ss before -i for speed)",
"extraction_timestamps": [
{"num": 1, "timestamp": "00:01:05", "file": "001_00-01-05_astro.new_website_showing_starter_template_options.jpg"},
{"num": 2, "timestamp": "00:01:25", "file": "002_00-01-25_documentation_search_modal_opened_with_command+k.jpg"},
{"num": 3, "timestamp": "00:02:39", "file": "003_00-02-39_animated_astro_project_creation_wizard_in_terminal.jpg"},
{"num": 4, "timestamp": "00:03:00", "file": "004_00-03-00_houston_mascot_exit_animation_after_project_creation.jpg"},
{"num": 5, "timestamp": "00:04:10", "file": "005_00-04-10_starter_application_at_localhost:4321.jpg"},
{"num": 6, "timestamp": "00:08:38", "file": "006_00-08-38_vs_code_.astro_file_without_extension_showing_as_plain_text.jpg"},
{"num": 7, "timestamp": "00:09:00", "file": "007_00-09-00_vs_code_.astro_file_with_astro_extension_showing_syntax_high.jpg"},
{"num": 8, "timestamp": "00:10:17", "file": "008_00-10-17_astro.config.mjs_showing_tailwind()_in_integrations_array.jpg"},
{"num": 9, "timestamp": "00:31:33", "file": "009_00-31-33_blog_listing_page_with_grid_of_post_cards.jpg"},
{"num": 10, "timestamp": "00:34:20", "file": "010_00-34-20_individual_blog_post_page_with_rendered_markdown.jpg"},
{"num": 11, "timestamp": "00:51:01", "file": "011_00-51-01_browser_network_tab_showing_webp_images.jpg"},
{"num": 12, "timestamp": "00:52:43", "file": "012_00-52-43_view_transitions_animation_between_pages.jpg"},
{"num": 13, "timestamp": "00:54:10", "file": "013_00-54-10_mdx_file_with_inline_components.jpg"},
{"num": 14, "timestamp": "00:57:58", "file": "014_00-57-58_netlify_deploy_dashboard_showing_successful_build.jpg"},
{"num": 15, "timestamp": "00:58:50", "file": "015_00-58-50_vercel_deploy_dashboard.jpg"},
{"num": 16, "timestamp": "01:05:17", "file": "016_01-05-17_api_endpoint_returning_json_in_browser.jpg"},
{"num": 17, "timestamp": "01:09:17", "file": "017_01-09-17_ssr_individual_post_page_loading_dynamically.jpg"}
]
}

View file

@ -0,0 +1,63 @@
#!/bin/bash
# Extract targeted screenshots from Astro video
# Generated by Hermes — read-only, preserves original
set -euo pipefail
VIDEO="/home/samob/Videos/Astro/Astro Web Framework Crash Course [e-hTm5VmofI].mp4"
OUT="/home/samob/ai/astro-howto/screenshots"
mkdir -p "$OUT"
echo "[1/17] 00:01:05 — Astro.new website showing starter template options"
ffmpeg -y -i "$VIDEO" -ss 65 -frames:v 1 -q:v 2 "$OUT/001_00-01-05_astro.new_website_showing_starter_template_options.jpg" 2>&1 | tail -1
echo ""
echo "[2/17] 00:01:25 — Documentation search modal opened with Command+K"
ffmpeg -y -i "$VIDEO" -ss 85 -frames:v 1 -q:v 2 "$OUT/002_00-01-25_documentation_search_modal_opened_with_command+k.jpg" 2>&1 | tail -1
echo ""
echo "[3/17] 00:02:39 — Animated Astro project creation wizard in terminal"
ffmpeg -y -i "$VIDEO" -ss 159 -frames:v 1 -q:v 2 "$OUT/003_00-02-39_animated_astro_project_creation_wizard_in_terminal.jpg" 2>&1 | tail -1
echo ""
echo "[4/17] 00:03:00 — Houston mascot exit animation after project creation"
ffmpeg -y -i "$VIDEO" -ss 180 -frames:v 1 -q:v 2 "$OUT/004_00-03-00_houston_mascot_exit_animation_after_project_creation.jpg" 2>&1 | tail -1
echo ""
echo "[5/17] 00:04:10 — Starter application at localhost:4321"
ffmpeg -y -i "$VIDEO" -ss 250 -frames:v 1 -q:v 2 "$OUT/005_00-04-10_starter_application_at_localhost:4321.jpg" 2>&1 | tail -1
echo ""
echo "[6/17] 00:08:38 — VS Code .astro file without extension showing as plain text"
ffmpeg -y -i "$VIDEO" -ss 518 -frames:v 1 -q:v 2 "$OUT/006_00-08-38_vs_code_.astro_file_without_extension_showing_as_plain_text.jpg" 2>&1 | tail -1
echo ""
echo "[7/17] 00:09:00 — VS Code .astro file with Astro extension showing syntax highlighting"
ffmpeg -y -i "$VIDEO" -ss 540 -frames:v 1 -q:v 2 "$OUT/007_00-09-00_vs_code_.astro_file_with_astro_extension_showing_syntax_high.jpg" 2>&1 | tail -1
echo ""
echo "[8/17] 00:10:17 — astro.config.mjs showing tailwind() in integrations array"
ffmpeg -y -i "$VIDEO" -ss 617 -frames:v 1 -q:v 2 "$OUT/008_00-10-17_astro.config.mjs_showing_tailwind()_in_integrations_array.jpg" 2>&1 | tail -1
echo ""
echo "[9/17] 00:31:33 — Blog listing page with grid of post cards"
ffmpeg -y -i "$VIDEO" -ss 1893 -frames:v 1 -q:v 2 "$OUT/009_00-31-33_blog_listing_page_with_grid_of_post_cards.jpg" 2>&1 | tail -1
echo ""
echo "[10/17] 00:34:20 — Individual blog post page with rendered Markdown"
ffmpeg -y -i "$VIDEO" -ss 2060 -frames:v 1 -q:v 2 "$OUT/010_00-34-20_individual_blog_post_page_with_rendered_markdown.jpg" 2>&1 | tail -1
echo ""
echo "[11/17] 00:51:01 — Browser Network tab showing webp images vs original JPEGs"
ffmpeg -y -i "$VIDEO" -ss 3061 -frames:v 1 -q:v 2 "$OUT/011_00-51-01_browser_network_tab_showing_webp_images_vs_original_jpegs.jpg" 2>&1 | tail -1
echo ""
echo "[12/17] 00:52:43 — View Transitions animation between pages"
ffmpeg -y -i "$VIDEO" -ss 3163 -frames:v 1 -q:v 2 "$OUT/012_00-52-43_view_transitions_animation_between_pages.jpg" 2>&1 | tail -1
echo ""
echo "[13/17] 00:54:10 — MDX file with inline components"
ffmpeg -y -i "$VIDEO" -ss 3250 -frames:v 1 -q:v 2 "$OUT/013_00-54-10_mdx_file_with_inline_components.jpg" 2>&1 | tail -1
echo ""
echo "[14/17] 00:57:58 — Netlify deploy dashboard showing successful build"
ffmpeg -y -i "$VIDEO" -ss 3478 -frames:v 1 -q:v 2 "$OUT/014_00-57-58_netlify_deploy_dashboard_showing_successful_build.jpg" 2>&1 | tail -1
echo ""
echo "[15/17] 00:58:50 — Vercel deploy dashboard"
ffmpeg -y -i "$VIDEO" -ss 3530 -frames:v 1 -q:v 2 "$OUT/015_00-58-50_vercel_deploy_dashboard.jpg" 2>&1 | tail -1
echo ""
echo "[16/17] 01:05:17 — API endpoint returning JSON in browser"
ffmpeg -y -i "$VIDEO" -ss 3917 -frames:v 1 -q:v 2 "$OUT/016_01-05-17_api_endpoint_returning_json_in_browser.jpg" 2>&1 | tail -1
echo ""
echo "[17/17] 01:09:17 — SSR individual post page loading dynamically"
ffmpeg -y -i "$VIDEO" -ss 4157 -frames:v 1 -q:v 2 "$OUT/017_01-09-17_ssr_individual_post_page_loading_dynamically.jpg" 2>&1 | tail -1
echo ""
echo "Done. All 17 screenshots extracted."

View file

@ -0,0 +1,26 @@
#!/bin/bash
# Fast extract remaining screenshots (9-17)
set -euo pipefail
VIDEO="/home/samob/Videos/Astro/Astro Web Framework Crash Course [e-hTm5VmofI].mp4"
OUT="/home/samob/ai/astro-howto/screenshots"
# 9-17
ffmpeg -y -ss 1893 -i "$VIDEO" -frames:v 1 -q:v 2 "$OUT/009_00-31-33_blog_listing_page_with_grid_of_post_cards.jpg" 2>&1 | tail -1
echo "[9/17] done"
ffmpeg -y -ss 2060 -i "$VIDEO" -frames:v 1 -q:v 2 "$OUT/010_00-34-20_individual_blog_post_page_with_rendered_markdown.jpg" 2>&1 | tail -1
echo "[10/17] done"
ffmpeg -y -ss 3061 -i "$VIDEO" -frames:v 1 -q:v 2 "$OUT/011_00-51-01_browser_network_tab_showing_webp_images.jpg" 2>&1 | tail -1
echo "[11/17] done"
ffmpeg -y -ss 3163 -i "$VIDEO" -frames:v 1 -q:v 2 "$OUT/012_00-52-43_view_transitions_animation_between_pages.jpg" 2>&1 | tail -1
echo "[12/17] done"
ffmpeg -y -ss 3250 -i "$VIDEO" -frames:v 1 -q:v 2 "$OUT/013_00-54-10_mdx_file_with_inline_components.jpg" 2>&1 | tail -1
echo "[13/17] done"
ffmpeg -y -ss 3478 -i "$VIDEO" -frames:v 1 -q:v 2 "$OUT/014_00-57-58_netlify_deploy_dashboard_showing_successful_build.jpg" 2>&1 | tail -1
echo "[14/17] done"
ffmpeg -y -ss 3530 -i "$VIDEO" -frames:v 1 -q:v 2 "$OUT/015_00-58-50_vercel_deploy_dashboard.jpg" 2>&1 | tail -1
echo "[15/17] done"
ffmpeg -y -ss 3917 -i "$VIDEO" -frames:v 1 -q:v 2 "$OUT/016_01-05-17_api_endpoint_returning_json_in_browser.jpg" 2>&1 | tail -1
echo "[16/17] done"
ffmpeg -y -ss 4157 -i "$VIDEO" -frames:v 1 -q:v 2 "$OUT/017_01-09-17_ssr_individual_post_page_loading_dynamically.jpg" 2>&1 | tail -1
echo "[17/17] done"
echo "All remaining screenshots extracted."

View file

@ -0,0 +1,143 @@
#!/usr/bin/env python3
import os
import sys
import json
import time
import math
import argparse
import threading
import cv2
from PIL import Image
def read_with_timeout(cap, timeout=5.0):
result = [None, None]
exception = [None]
def target():
try:
result[0], result[1] = cap.read()
except Exception as e:
exception[0] = e
result[0] = False
thread = threading.Thread(target=target)
thread.daemon = True
thread.start()
thread.join(timeout=timeout)
if thread.is_alive():
return False, None
if exception[0]:
return False, None
return result[0], result[1]
def main():
parser = argparse.ArgumentParser(description="Generate a video contact sheet.")
parser.add_argument("--save-frames", action="store_true", help="Save individual frames")
args = parser.parse_args()
video_path = "/home/samob/Videos/Astro/Astro Web Framework Crash Course [e-hTm5VmofI].mp4"
output_dir = "/home/samob/ai/astro-howto/contact-sheet/"
frames_dir = os.path.join(output_dir, "contact_sheet_frames")
os.makedirs(output_dir, exist_ok=True)
if args.save_frames:
os.makedirs(frames_dir, exist_ok=True)
if not os.path.exists(video_path):
print(f"Error: Source video not found at {video_path}")
sys.exit(1)
start_time = time.time()
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
print("Error: Could not open video file.")
sys.exit(1)
fps = cap.get(cv2.CAP_PROP_FPS)
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
interval_frames = int(60 * fps)
targets = list(range(0, total_frames, interval_frames))
thumbnails = []
skipped = 0
count = 0
for target_pos in targets:
cap.set(cv2.CAP_PROP_POS_FRAMES, target_pos)
ret, frame = read_with_timeout(cap, timeout=5.0)
if not ret or frame is None:
skipped += 1
continue
try:
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
h, w = frame_rgb.shape[:2]
new_w = 480
new_h = int((new_w / w) * h)
resized = cv2.resize(frame_rgb, (new_w, new_h), interpolation=cv2.INTER_AREA)
pil_img = Image.fromarray(resized)
if args.save_frames:
frame_filename = f"frame_{count:03d}.jpg"
pil_img.save(os.path.join(frames_dir, frame_filename), quality=90)
thumbnails.append(pil_img)
count += 1
except Exception:
skipped += 1
cap.release()
if not thumbnails:
print("Error: No frames could be extracted.")
sys.exit(1)
cols = 5
thumb_w, thumb_h = thumbnails[0].size
padding = 2
rows = math.ceil(len(thumbnails) / cols)
sheet_w = cols * thumb_w + (cols - 1) * padding
sheet_h = rows * thumb_h + (rows - 1) * padding
sheet = Image.new('RGB', (sheet_w, sheet_h), (255, 255, 255))
for i, img in enumerate(thumbnails):
r = i // cols
c = i % cols
x = c * (thumb_w + padding)
y = r * (thumb_h + padding)
sheet.paste(img, (x, y))
output_path = os.path.join(output_dir, "contact_sheet.jpg")
sheet.save(output_path, quality=95)
extraction_time = time.time() - start_time
report = {
"frame_count": count,
"path": output_path,
"dimensions_px": [sheet_w, sheet_h],
"skipped_frames": skipped,
"extraction_time_s": round(extraction_time, 2)
}
report_path = os.path.join(output_dir, "report.json")
with open(report_path, 'w') as f:
json.dump(report, f, indent=4)
if not os.path.exists(output_path) or os.path.getsize(output_path) == 0:
print("Error: Output validation failed - file missing or empty.")
sys.exit(1)
print(f"Frames extracted: {count}")
print(f"Skipped: {skipped}")
print(f"Output path: {output_path}")
print(f"Dimensions: {sheet_w}x{sheet_h}px")
print(f"Time: {report['extraction_time_s']}s")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""
Local audio transcription using faster-whisper.
Usage: python3 transcribe_local.py <audio.wav> [model]
Models: tiny, base (default), small, medium, large-v3
Smaller = faster, larger = more accurate.
"""
import sys
import time
from faster_whisper import WhisperModel
def main():
audio_path = sys.argv[1] if len(sys.argv) > 1 else "audio.wav"
model_size = sys.argv[2] if len(sys.argv) > 2 else "base"
output_path = audio_path.rsplit(".", 1)[0] + "_transcript.txt"
print(f"Loading {model_size} model...")
model = WhisperModel(model_size, device='cpu', compute_type='int8', num_workers=8)
print(f"Transcribing {audio_path}...")
start = time.time()
segments, info = model.transcribe(
audio_path,
language='en',
beam_size=5,
vad_filter=True,
vad_parameters=dict(min_silence_duration_ms=500),
)
with open(output_path, 'w') as f:
for seg in segments:
ts = (
f'[{seg.start:.0f}s]'
if seg.start < 3600
else f'[{int(seg.start // 3600)}:{int(seg.start % 3600 // 60):02d}:{int(seg.start % 60):02d}]'
)
f.write(f'{ts} {seg.text.strip()}\n')
elapsed = time.time() - start
print(f"Done. {info.duration:.0f}s audio in {elapsed:.0f}s ({info.duration / elapsed:.1f}x realtime)")
print(f"Output: {output_path}")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,805 @@
[0s] Astro is an all-in-one web framework for building fast content-focused websites,
[6s] like landing pages, blogs, technical documentation, and more.
[10s] In this course, James QQuick will teach you about this increasingly popular framework.
[16s] James is a popular instructor and keynote conference speaker.
[20s] He's the perfect person to teach you about Astro.
[24s] Let's learn all about Astro, one of the most exciting and up-and-coming JavaScript frameworks.
[29s] My name is James QQuick, and I absolutely love Astro.
[32s] So in this crash course, we're going to cover some of the core concepts of Astro along the way.
[36s] We'll talk about file-based routing, creating and managing markdown and MDX content
[41s] with content collections and TypeScript.
[44s] We'll talk about dynamic routes, and we'll even, at the end of this video,
[47s] get into deploying this to Netlify and Versome.
[50s] We'll also talk about the server side capabilities of Astro going from a statically generated site
[55s] to a server side rendered site and show you how you can add server endpoints to your
[59s] Astro applications as well.
[61s] Lastly, we'll talk about a few neat features of Astro along the way,
[64s] like view transitions, which allows you to add beautiful transitions between your pages
[69s] and your Astro application with just one line of code.
[71s] Now, if you enjoy what we cover in this crash course and you want to learn more,
[75s] you can check out my full course at astrocourse.dev to learn all the ins and outs of Astro 3.0.
[82s] That said, let's go ahead and get started.
[85s] Let's start on the Astro documentation page where we can see the getting started
[88s] instructions for creating a brand new project with Astro version 3, which is the most recent version
[93s] of Astro. Now, on this documentation page, they kind of give you an overview, some of the things
[98s] that we've talked about, some key features, etc. And then if you scroll down, they'll give you
[102s] a couple of ways to get started with Astro. One cool way to do that is with the Astro.new website,
[107s] and what this is, is a collection of different Astro projects that you can open up that have already
[112s] been created and kind of get started working with them right inside of stacklets.
[116s] Now, in this case, we're going to create our own project from scratch. So, we're going to do this
[120s] inside of our own VS code window and terminal to be able to create the project and then go and work
[125s] and build out the tutorial or the blog that we're going to build. Now, one thing I do want to show
[130s] and we'll kind of reference this throughout this crash course is the ability to search for anything
[134s] that we might want in this course on the Astro documentation page. So, once you get to the Astro
[140s] docs, you can come up to the top left and you can click to open the search window and you can search for
[145s] server, anything, server, side, rendering, for example. And then, click on one of these and go
[151s] straight to those pages. The other thing I want to show you that's pretty neat is on this page,
[155s] you can hold Command K on Mac or Control K on Windows to be able to pull that up as well. So,
[161s] you don't have to go and click that yourself. You can just open that with the shortcut window.
[165s] That said, what we're going to do is copy this command to be able to create a brand new Astro project
[170s] and then run this in our terminal so that we can get started. So, I'm going to scroll over to my
[174s] VS code window and this is an open MTVS code window and I'm going to first switch over to the directory
[180s] that I want to create the project. So, I'm going to switch into code and then my demos directory.
[186s] And then from here, I'm going to paste in that command from the Astro docs to create the new project.
[191s] So, once I press enter, this will ask me a few different things. First of all, are we okay installing
[195s] the package that's needed to generate our project? And this is actually a good error to have here.
[201s] So, with the latest version of Astro, you're required to have a version 18 or higher of Node.js to be
[207s] able to run the installer. So, I wanted to leave this in as a reminder that you'll need to be working
[212s] with a version 18 or higher. Now, for me personally, I use NVM to manage my different versions of Node
[220s] to handle this. This is the easiest way that I found to handle working with different versions of Node.
[225s] So, I can use my NVM command and then NVM use and then type 18. And this will let me use a version
[231s] of Node that is 18. Now, I can also use NVM to install a version of Node like 20, for example.
[239s] And that will go in install a different version of Node that I could use at any time. But in this case,
[243s] since I'm now using version 18, I can scroll back through my commands and run that same create command
[249s] inside of my terminal. Now, you see, we get the really awesome animated experience where Astro is
[255s] going to walk us through creating this new project. So, in this case, we need to give it a folder
[260s] that we're going to create the project in. I'm going to call this FCC Astro Crash Course. So,
[267s] you'll just do .slash and then whatever the folder is of whatever the name is of the folder that you
[272s] want it to create in. In this case, we're going to start with sample files. You could also choose a
[277s] blog template where they'll have a lot of the stuff done that we will be building from scratch. You
[282s] could also additionally choose an empty project to just start completely from scratch.
[286s] In this case, we'll accept the sample files. And what it's doing now is it's going to start to copy
[290s] over the sample files, which is done. And then it's going to ask whether or not we want it to install
[294s] the dependencies and we can press Enter on Yes and let it go and run and install all of these dependencies.
[302s] So, we'll let that run for a second and come back when it's finished. All right, so those dependencies
[306s] have been installed. And the next question is, do we plan to write TypeScript, which in this case,
[310s] we do. So, we'll click Yes. And then we'll choose the strict or recommended version of how strict
[315s] with TypeScript we should be. Lastly, the question is, do we want this to initialize a new Git repository?
[321s] In this case, we'll say yes, because we're going to use this repository to deploy this later on to
[326s] Netlify and Versel. So, we'll click Yes here. And then we get our out animation from Houston, which
[333s] is the Astro mascot. We'll come back to Houston in a little bit. And the next thing I want to do is
[338s] open this project inside of VS Code. So, I can use the command Code-R, which means to reuse
[344s] the window that I'm currently in. And then I'm going to choose my FCC Astro Crash Course window.
[350s] Notice it pops up here with this intelligence. If you're curious where this little window is coming
[354s] from, this is coming from an extension called Fig.io, which is really great to kind of supercharge
[360s] your powers inside of your terminal. So, I'm going to press Enter and then we'll open this up inside
[365s] of that same window in VS Code. And now I'm going to go to the bottom and open my terminal again.
[370s] And inside of here, I'm going to run NPM Run Dev. And this should now start our Astro project.
[376s] And it will run it at port 4321. They chose this port because it is kind of like 4321 blast off,
[383s] which is kind of neat. So, I'm going to open a new tab in my browser and open up my browser to
[388s] localhost 4321. And you see, this is the starter application that we get with Astro, or we have
[395s] a welcome to Astro. We have a little code challenge of how to make an update to this, which is right here.
[400s] Then we have links to documentation, integration, themes, community, etc. So, we have our beginner
[406s] Astro application created. Let's go and walk through the code and talk a little bit about what all
[410s] is there. So, first off, we have our public directory. This is where we would store any public assets
[416s] like images or other things that we want to be publicly available from our site. Now,
[422s] these are going to be directly available after the end of the URL. So, what this means is,
[426s] since we have favicon.svg, we could come to the end of our URL and type in favicon.svg.
[434s] And now that'll take us to that file, which is not going to show a whole lot because it's an SVG.
[438s] And we're not going to have good visibility here. But we do have the access to be able to access
[442s] that directly. So, anything that you put inside of the public directory will just be shipped with
[447s] your built version of your site and included and would be available after the slash in your URL.
[454s] After that, we have our source directory. This is where all of our code is really going to live.
[458s] And what Astro really depends on is this page-based routing where we have different files under
[464s] the pages directory are going to represent, as you might have guessed it, different pages in your
[468s] application. So, let's just open the index.astro file and take a look. The first thing we'll
[475s] notice is that it is a .astro file. It's not a .js. It's not a .ts. This is .astro. And this obviously
[482s] is going to signify to the developer to yourself and to your editor. We'll talk about this in a minute
[488s] that this is an astro component. Now, astro components are made up of two different sections. There is the
[494s] kind of JavaScript section which goes in these three-blocks. We can call these front-matter. We'll
[500s] talk more about front-matter inside of Markdown files. But inside of these three-blocks, we can add
[508s] any sort of JavaScript that we want, including importing other components. So, you can see we import
[513s] our layout and our card. And in this case, we can find both of these in their appropriate
[518s] directories. Under layout is the first one. If we scroll down, we see a reference to slot.
[523s] Now, slot is where we're going to take whatever information is in between this component when we use it,
[528s] the layout component, and then inject that right inside of the component that we have defined here.
[534s] So, what this looks like, if we come down to our index.astro, since we wrap this entire page,
[539s] which has a lot of code in it, since we wrap this entire page with this layout component, all of
[545s] the stuff in between the layout tags, which is here. All of that is considered the slot. And that
[551s] will be rendered inside of this layout component right here. So, inside of the body. Now, inside of the
[557s] layout, you'll also see a few other things. You'll see that we can define TypeScript interfaces for
[563s] our props. After we define those, we can then destructure those properties and then use them inside
[568s] of our application, just like we're using the title inside of the title tag inside of the head
[574s] for our application. Now, notice we can use these JavaScript variables by putting them in between
[581s] the two brackets. So, inside of these two brackets, we're able to basically write JavaScript here,
[587s] which enables us to use variables that we've defined up above. So, in this layout file, you'll also see
[593s] the other components that make up a basic HTML file. You'll see the doc type defined at the top.
[599s] You'll see the HTML tag. You'll see head and we'll see a few different of our meta tags here,
[606s] like description, viewport, etc. We'll also see a reference to our title. And then if we scroll down,
[612s] we'll see a lot of CSS in here. Now, Ashter has a few different ways that you can write CSS. One of the
[617s] ways that you can do that is just by adding a style tag right inside of your Ashtero components.
[623s] Now, this may or may not make you excited. This is something that gets debated about whether or not your
[628s] style should be co-located with your actual markdown and with your JavaScript. But in Ashtero,
[632s] you have the ability to write all three together. So, your JavaScript, your markup, or your HTML,
[639s] and then your styles in here as well. Now, styles typically in here are scoped to a given component.
[646s] So, you can see here that we have an is global tag that's associated with this style,
[651s] which means all of these styles are going to be applied to every single page on our application,
[656s] versus if we go to our index.astero component and scroll down, there's going to be styles here.
[662s] And these styles are not global. They're only referencing material that's inside of the
[668s] component that it's in, which in this case is a page component, which is the index.astero. Now,
[673s] we can see another good example of this with the actual card component. You can see we define our
[678s] props here. We destructure those props. We have markup. We reference those properties inside of our
[684s] markup. And then we also have style tags down here as well. Now, again, these styles are only
[689s] applying to things that are inside of this card component and won't be applied anywhere else.
[695s] So, an example of this is if I were to select the main tag and did a background color of red,
[704s] this actually won't appear to have any difference or make any difference on our application.
[709s] And that's because there's no main tag inside of this card component and these styles are only
[714s] applied to that. However, if I went to the layout and I now chose to select the main tag and did a
[721s] background of red, now we'll see that this red color is going to come into play because these styles
[729s] are global and are going to be applied anywhere there is a main tag. So, really important to remember
[735s] that the styles inside of astro by default are scoped to that component and won't be interfering
[741s] with other styles that you have in other components. Now, in this case and this crash course,
[745s] we're going to use tailwind CSS to style our application. So, we're actually not going to worry about
[750s] all of these styles that are defined inside of here. We'll come back and clean these out in a minute
[754s] and kind of reset this with some beginner styles for us to work with. But if you were building an
[759s] astro project yourself, you do have a few different ways that you could choose to do CSS. In this case,
[764s] we are just going to use tailwind CSS, which has become incredibly popular. So, that's the majority
[771s] of the basics of the layout for your code inside of the source directory. There's a few other files
[777s] that get ignore file, which is pretty standard. There's also the astro.config file. And this is
[782s] really important because this is where we can add integrations and astro. We can also define different
[788s] things about our project like how this project is going to be built and where it's going to be hosted.
[794s] So, by default, astro is a statically generated site. We'll talk more and more about this.
[799s] We don't have to configure anything for that to be supported. But if we wanted to convert this to be
[803s] a server side rendered site, we could configure this in here and then configure where and how we want
[808s] to deploy this. At the end of this video, we'll talk about deploying this to both Netlify and Versel.
[813s] But in this case, we don't have anything to change yet in the astro config, although we will come back
[818s] to this shortly. Next up, we have our package.json with a few commands on how to run the project. We have
[823s] the readme and then we have a TS config, which just extends the TypeScript config that comes from
[829s] astro. So, this is going to give us all the basic rules for working with TypeScript inside of our
[833s] astro project. You could go and customize that in any way that you want to, but in this case, we don't
[838s] need to. Now, I want to take a few minutes to talk about setting up your VS Code instance to work with
[844s] astro in the best way. Now, the most important thing you'll need to install is the actual astro
[850s] extension, which comes from astro themselves. Now, what this does is it allows these astro components,
[856s] these astro files to be recognized as astro files so that we get appropriate intelligence, coloring,
[862s] etc. So, notice down at the bottom here that VS Code is recognizing this as an astro file and then
[868s] based on that and based on this extension knows what to do and how to color this. Now, if we were to
[873s] disable this just to show you what this looks like and we restart this, all of our code highlighting
[880s] our syntax, our intelligence, etc. goes away inside of these astro components and VS Code considers
[886s] this to be a plain text file, which is obviously not what we want. So, you'll want to make sure to install
[892s] the astro extension to get all of the benefits that come along with it. It's by far the best way to work
[898s] with astro. So, I'm going to enable that and notice all of that comes back. Now, another one that I
[902s] have installed is an astro snippets extension. There's lots that you can do with astro in terms of
[907s] different types of files, different things you might want to do. This is a great set of snippets that
[912s] you could start with to kind of help generate components quicker and easier for you as you're going along.
[917s] Now, there's one more extension from the astro team, which is the Houston extension. Now,
[922s] Houston is the actual mascot for astro and they've built a lot of fun things around this. So, with the
[927s] Houston extension, you get the astro VS Code theme mimicking the colors from Houston, which is
[933s] pretty neat. I like this a lot. In addition to that inside of the file explorer, you get a little
[938s] Houston tab and you get kind of an animated Houston icon that shows you whether or not your application
[944s] is running well or not based on it being happy or sad. So, it's a nice little touch to kind of feel
[949s] like you're inside of the astro community. So, you can install this and kind of have some fun with
[954s] that if you're interested. Now, me personally, I'm using my personal James Q quick themes. So,
[959s] if you're interested in having your colors look exactly like mine, you can search James Q quick to
[963s] get set up there. The last thing I want to show you is the tailwind CSS IntelliSense extension. This is
[970s] one that I use all the time when working with tailwind so that it helps me kind of auto-complete or
[975s] remember what all the different styles are that I'm trying to work with. So, you'll see this more as we
[980s] work work through this and start writing some code. Now, with all of that set up, let's go back
[985s] to the astro documentation and see how to install tailwind. So, let's just search in the documentation
[990s] for tailwind and we'll be taken to the astro JS tailwind extension or integration that we can add
[996s] which is one of the really cool things about astro is that it comes with integrations which makes
[1001s] it really easy to add support for other UI frameworks, for example, to be able to deploy SSR to different
[1007s] places and a bunch of other really neat things. So, let's scroll down. We can kind of skip the
[1011s] wide tailwind and let's come down to the npx astro add tailwind command which allows us to add tailwind
[1017s] in one command and be able to work with it right after that. So, let's stop our running application.
[1022s] Let's paste in our npx astro add tailwind command and this will kind of walk us through what it's
[1028s] going to do to make sure we're okay with it doing all of these things. So, do we want to allow it to
[1033s] install the tailwind CSS extension and the astro JS tailwind package. Yes, absolutely. In this case,
[1039s] it says it's going to generate a tailwind config file which we absolutely want. So, we'll say yes
[1044s] and then lastly, it says it's going to update the astro config file to be able to support tailwind.
[1049s] So, we'll say yes to that as well. Now, just to confirm what this did, let's search for the astro
[1053s] config file. Let's open this up and what it did is it added an integration section here and an inside
[1059s] of that array it added a call to the tailwind function that gets imported from the astro JS tailwind
[1066s] package. So, this in theory is how we would manually install integrations into our applications.
[1072s] But in this case, astro gives us this command, the astro add command to be able to do all this
[1077s] a force which is really, really nice. So, let's go into our layout file and let's get rid of all
[1083s] of the styles that are defined in here because we're going to use tailwind for our styles and not use
[1087s] the built-in styles that come with the application. Now, just to make sure let's go ahead and run
[1092s] this to have this running, we can make a few changes inside of this layout file to start to get the
[1099s] base of our application going. Now, the first thing to notice is that we're only defining one
[1104s] property that could be passed in as props which is the title. Now, we could additionally add extra ones
[1110s] like the description. We can have this be optional. So, we'll have that be defined as a string. So,
[1115s] the optional question mark or the question mark denotes this as optional. And we can also define an
[1121s] image in here as well. So, now that we get all three of those, we can de-structure them so that we can
[1127s] be able to use these as well. All right, so we have these three properties, but we're not using all
[1132s] of them just yet. We're only referencing the title and not the description or the image yet.
[1139s] So, inside of the content for the description, we could reset this
[1145s] to be description. And we could also reference our image by using it for the OG image type.
[1152s] Now, we're not going to get all the way into all of the different OG tags that we could use.
[1156s] Let's start with a meta tag with a property of OG image. And then we'll say the content
[1165s] is going to be that image property that we pass in. So, we'll put this in here as image.
[1171s] Now, one thing you might be wondering is what if the description and the image are not pass in here,
[1177s] we should probably have a default property that we can use. So, in this case, for the description,
[1182s] we can set the default right in line in here by doing equals and then assigning this to a string.
[1187s] Now, what we're building is an application called Rhythm Nation. And this is a community
[1193s] of music producers and enthusiasts. And then we want to give a default value to the image as well.
[1202s] Now, we'll come back to this in the images section. But I'm going to set this to a default of slash
[1208s] images slash band.jpeg. Now, remember, we talked about the public directory. What this is referring to
[1215s] would be a file band.jpeg inside of an images directory inside of public. We'll come back to that
[1222s] in a little bit. For now, we're just kind of setting these by default. Now, just to show that these
[1226s] are coming up, we can come back to our running application, which now looks a little bit different
[1232s] because we got rid of those styles. And if we look inside of the head, we should see that we now see
[1237s] our description here. We also see our OG image, which if we try to access will not be available yet.
[1242s] Then we still have our same title, which is great. So, all those things are working well. There's a lot
[1247s] more that you could dive into with OG tags for helping your website show up on social media.
[1254s] Post, for example, or embeds and Slack or discord. But that's the conversation for another day.
[1259s] Just know that you have complete control to add all of those inside of here. Now, inside of the layout,
[1264s] what I want to do is add some tailwind classes in here. And what we can start with is a men height
[1270s] of screen. So, before we save that, let's go and look at the body tag in here.
[1277s] If we see this body tag is not taking up the entire height. So, we can start to style that a little bit
[1283s] with men h of screen. And now we should see that this body is now taking up the entire height, which
[1288s] also is confirming that our tailwind classes are working. Now, from here, I want to add a header
[1295s] component that can show the basics of our application. So, in this case, I'm going to copy a little bit of
[1300s] code, but inside of components, I'm going to create the header dot astro component. And I'm going to
[1306s] paste in some starter code for us to work with. Now, we'll walk through the code that's here. So,
[1311s] we define our header. We give some tailwind CSS classes. Again, this is not a crash course on tailwind
[1316s] specifically, but we have some classes for our header. We then have an image icon that we can have
[1322s] to show in the top left. We'll come back to that in a second. Then we have a few links to the
[1326s] different pages on this application, like the homepage, the about and the blog. So, from here,
[1332s] what I want to do is import this into our body or into the body of the layout component. So, I can
[1338s] actually open bracket and start to type our header. And then oftentimes, I'll get IntelliSense for
[1343s] this, but it looks like it's not opening for me. So, I can go and do a manual import up here instead.
[1349s] So, at the top of this, we can import header from and then we'll go back a directory into the
[1355s] components directory and then grab the header.astro. So, we can save this and we should now see the
[1362s] basics of our application starting to come together. We have our header up here. We have a missing
[1366s] icon up here, and we'll need to add that inside of our source code so that we can actually have this
[1372s] show up. So, one of the things that we can do is go ahead and go to the astro course demo,
[1379s] final source code, and then inside of the public directory, we have an images directory,
[1384s] and we have a heartbeat.png, both of which we're going to need. So, we can click on the heartbeat.png,
[1391s] and we can download this. So, we can download that file. And then additionally, we're going to need
[1397s] the images directory as well. So, from the images directory, we can download this directory as well.
[1405s] All right. So, that should download all of those files. And so, what you'll need to do is go and find
[1410s] the heartbeat image and then that directory that we just downloaded, and we'll add that into our
[1415s] source code. So, in this case, I'm going to take the heartbeat.png and I'm going to add this into the
[1419s] public directory. So, there that is there. And if we look inside of our header, it's referencing
[1425s] slash heartbeat.png, which should reference a heartbeat image right inside of that public directory.
[1430s] So, now if we come back and go to our application and refresh, we should see that heartbeat
[1436s] icon is starting to show up, which is great. Now, the other thing we wanted to do is take those
[1440s] images that we downloaded in the zip folder and add those to the public directory as well.
[1445s] So, inside of the public directory, I'm going to create an images folder. And I'm going to drag
[1450s] all those images that we just downloaded into that folder. So, into the images directory here.
[1456s] Now, one additional benefit of that, if we remember, is back in the layout component,
[1461s] we defined a default image for our OG image to be inside of the images directory just like we did,
[1469s] and in the band.jpeg. So, this now should be the default image that shows up for our OG image tags.
[1475s] And actually, we can test this by going directly to this inside of the URL. So, we go to slash images,
[1480s] and then slash band.jog, but jpeg. And we should now see this entire image showing up. So, we know that
[1488s] that is working as well. So, we have our images copied over, which is exactly what we want. We have our
[1494s] header showing up here above. We have links to our different pages, which we haven't created yet.
[1500s] And now, we can go into our index component, that root page. And we can get rid of all of the styles.
[1507s] So, we can delete all of these styles. And we can delete all of the main content that's in here as well.
[1512s] So, let's just scroll all the way through and get rid of everything inside of main. And in this case,
[1518s] we're going to update this title to be relevant to the blog post, the blog site that we're working on,
[1525s] which is the rhythm nation blog. So, it's just the demo idea here. But, let's go ahead and type that
[1532s] in rhythm nation blog. And now, we can add a title to this as well. So, I'm going to actually create a new
[1539s] component for an h1 that we can reuse. So, we'll create an h1 component dot astro. I'm going to
[1544s] copy over the tiny bit of code that we have for this, where we define our props. We take a text
[1550s] property as our prop. We destructure that. And then we put it inside of an h1 that already has the tail
[1556s] one styles created. So, I'm going to save this. And then back inside of the index, we can now reference
[1563s] our h1 component. And we'll pass in a property of rhythm nation. And then we'll need to import this
[1572s] component so that we can reference it. So, we'll import h1 from. And then we'll go into that
[1577s] components directory. And then we'll grab the h1 dot astro. And now, we should see a basic title
[1583s] showing up. Now, one thing you might notice is that we need some spacing on the outside of this.
[1588s] So, one more component that we're going to create is the main dot astro component. And what we're
[1593s] going to do is use this component to wrap all of the other things that we do. So, in this case,
[1598s] I'm going to open up a main tag. And then we'll pass in everything in between as the slot. And then
[1604s] we'll just add some tail one classes in here. So, we'll have a px of 24, which is padding x. We'll
[1612s] set a max width of 7 xl. We'll set mx to be auto, which is going to automatically center everything
[1618s] horizontally. And then we'll set the width to be full. And then lastly, we can set the padding to
[1625s] be five on screens that are at max, small size. So, this is saying that we'll have a padding x of
[1634s] five on screens extra small and small. And then above that, anything bigger will have a padding x of
[1639s] 24. Now, the last thing we need to do, we can actually duplicate this import. We can import the main
[1646s] component from that component's directory. And we can now use our main component instead of just
[1652s] the main tag. And that should wrap everything and give some spacing on the outside. So, now we have
[1657s] a clean section here for our main content that has that padding on the outside. We have our header,
[1662s] we have our images loaded. And we can start to do more with this application by building out blog
[1667s] functionality and leveraging the content collection feature in Astro, which is one of my favorite features
[1672s] of Astro. Now, if we look at the final demo from the full Astro course, we can see we have a homepage
[1678s] where we show a bunch of blog posts, we have tags, etc. But what I want to show you is that if we go
[1683s] to the slash blog page, we can see a list of all these blog posts, which is what we're going to start to
[1688s] work on now. So, we can see a list of all the blog posts in addition to all the images that are
[1694s] associated with them. So, if we scroll through, we can see these blog posts. And then if we click on one,
[1699s] we'll actually be taken to the specific route for the individual page. So, notice inside of the
[1705s] URL, we have our route URL slash blog and then slash the title of that blog post in a slugified
[1711s] version, which means having dashes in between all of the words. So, let's start to work on setting up
[1717s] our application in this crash course to be able to work with Markdown using content collections in
[1722s] Astro. So, let's go over to the Astro documentation and let's search for content collections. So, what
[1729s] content collections are are ways to organize and manage and author content in any Astro project.
[1735s] And in my personal opinion, this is the best experience for working with content specifically
[1739s] Markdown and or MDX content that I've ever seen across any platform, which gets me really, really
[1745s] excited. So, our content collections again give us a way to organize all these inside of a special
[1751s] directory in Astro called content directory. So, under source slash content, we can then create a
[1757s] directory for each different type of content that we want to create. In this case, their demo,
[1761s] they have newsletter. In our case, we will have blog and that's where all of our blog posts will
[1766s] live as Markdown files inside of there. Now, you can scroll down and find a lot more about this
[1772s] with multiple collections, et cetera. But the one thing I do want to show you is how to define
[1776s] collections inside of the content config file, which is a TypeScript file and allows you to define
[1783s] types for your individual collections like the blog collection using ZOD to have TypeScript type
[1791s] associated with each of your collections. So, you define a collection, you define a schema,
[1796s] and you can define all of the different properties that are going to be associated with each piece of
[1800s] content. Now, before we actually get into the code, the one thing we will need to do is go back to the
[1806s] Astro course demo and we'll need to download some sample Markdown files that we haven't here to
[1811s] reference inside of our application. So, inside of the Astro course demo, there is the source directory
[1816s] and an inside of there just like we talked about is a content directory and an inside of there is the
[1822s] post directory. So, what you'll want to do is go and download this entire directory of all of these
[1828s] posts. Now, after you do that, you'll want to make sure to extract all of those and then we'll go inside
[1835s] of our source directory. We'll now create our content directory and an inside of there will create
[1841s] another new folder called Post and then we'll take all of that that we just copied and drag it into the
[1849s] post directory. Now, one thing I did skip from copying over is the images directory that you can see
[1856s] here, but that's something we'll come back to when we get into image optimizations. So, let's just take
[1861s] a real quick look at what we have inside of this content. So, inside of here we have our front
[1867s] matter at the top of each one of these Markdown files and we have an author, we have categories,
[1872s] we have a date, we have whether or not this blog post is featured, we have a cover image to be
[1877s] referenced and then we have a title. Now, this is all the front matter and what we're going to do with
[1881s] content collections is define a data type that represents this and stores or gives us an
[1888s] intelligence inside of our editor to know which of these properties to associate with our given
[1893s] blog post specifically with a given collection, which in this case is our blog post. Now, at the bottom
[1898s] of this, you can see all of the sample Markdown that's included here. So, this is just some getting
[1903s] started Markdown. So, we have something to render. You could obviously go and create your own Markdown
[1908s] with your own content if you wanted to. Now, one thing I do want to change is the reference to where
[1914s] these images are stored. So, actually, I'm going to select this whole thing and I'm going to do
[1919s] Command Shift F on Mac or Control Shift F on Windows and I'm going to change this slightly
[1925s] and I'm going to get rid of this leading dot in each one of these blog posts or each one of these
[1929s] Markdown files and we'll come back to that again when we talk about updating our images to work with
[1935s] the image component that comes with Astro to optimize our images. But right now, I want this to point
[1941s] to the public images directory where those different images are. Again, we'll come back to this
[1945s] in a minute. So, we have our sample Markdown. Now, we need to go into the content directory and we
[1952s] need to create our config dot TS file. So, let's start to work on defining a content collection inside
[1959s] of this config file. Now, to start, we're going to import the define collection function and then the
[1965s] Z for Zod from the Astro content import. So, this is giving a little bit of an issue saying it cannot
[1975s] be found. This should be okay. So, as we go through this, we'll make sure to run it just to make sure.
[1980s] And then from here, I want to define my post collection and this is going to call the define collection
[1986s] function. We'll call this and we'll pass it a config object. Now, this config object will then have
[1992s] a schema and we'll say that the schema is going to be a Z dot object, which is a function and we'll
[2000s] pass that a configuration object as well. So, we're using Z dot object to say that this schema is going
[2006s] to be an object and then now we can define the different properties that it's going to have. So, we
[2011s] can define it to have a property of author and then using Z, which is Z, we can say where this is
[2016s] going to be a string. Then we'll have a date, which in this case is also a string. We'll have an
[2023s] image, which is a string. We'll come back to this in a minute and then we'll have a title, which
[2028s] is a string as well. Now, the cool thing about Zod is that it has other data types that you can work
[2034s] with where you can add a lot of customization on what exactly these types should look like. And then
[2040s] lastly, what we want to do is export a variable called collections and this is going to be an object
[2046s] and we'll say a key is going to be posts and then it's going to have a value of posts collection
[2052s] and it's really important that this word here match up with the name of the directory that
[2056s] that content is in. So, those two things should match, which means our post collection should be inside
[2062s] of this post directory inside of content. So, now that we have our definitions for our content,
[2067s] we want to start to query this content so that we can start to display this inside of our slash blog
[2073s] page. So, to do this, we'll need to create another component inside of our pages directory and we're
[2078s] going to create the blog dot astro component. Now, in here, we can start to query our content by
[2085s] referencing the get collection function that comes from that astro content name space. So, this is going
[2093s] to be a function that we can call to get the content associated with a specific collection. So,
[2100s] in this case, we're going to sign this to a variable called posts. We're going to await a call to get
[2106s] collection and then inside of a string, we're going to pass it the name of the content that we're
[2110s] looking for. Now, notice it gives me an intelligence in here because it knows what the different content
[2115s] collections are that I've defined. So, I can now query these posts inside of here and now we can be able,
[2121s] we should be able to log the post to the console. So, important to note with astro is all of this code
[2130s] is going to be run statically at build time. So, it won't quite look this way when we run this now,
[2136s] but when this is deployed, all of this content is going to be queried and generated at build time
[2141s] and then deployed statically. We'll talk more about this as we convert to SSR later in this video.
[2146s] But in this case, we should be able to go back to our site. We should be able to click on the blog
[2153s] page. Nothing will show up, but if we go back to our logs, we should see that this is actually querying
[2160s] all of this content, which is pretty nice. So, what I want to do is start to be able to display
[2165s] the basics here. So, one thing I'm going to do is copy over the structure of a page from that root page.
[2172s] And now, we'll say this title is going to be blog and then rhythm nation, maybe not rhythm nation
[2177s] blog because that's repetitive. We also have missing imports. So, I can do command and period and go to
[2183s] add all missing imports. This would be control period if you're on Windows machine. Now, we have all of our
[2188s] imports and then we can also update this to be blog. So, now we should at least have the basics of a
[2194s] page kind of showing here. So, rhythm nation blog. So, that's great. But now we want to actually be able
[2199s] to display that content. So, one of the things that we could do is we could iterate through our post.
[2206s] So, we could say post. And then map and then get a reference to each post. And then for each post,
[2212s] what do we want to return? So, inside of here, we could start with an h2 and then reference the post.
[2222s] Data, that's going to be all of our front matter. And then inside of here, when we press enter,
[2227s] we now get intelligence for all those properties, which in this case, I'm going to choose the title.
[2231s] So, this is not going to look great, but at least we have the ability to show that all these post titles
[2235s] are being queried here. Now, the other thing we might want to do is wrap this all in an anchor tag. So,
[2241s] if we kind of stub out an anchor tag here and wrap our h2, what we want to do is we want to set the
[2249s] href to a particular URL that will take the user to that blog post. So, in this case, we can define this
[2257s] ourselves by using an ES6 template literal string. And we could say this is going to take the user
[2262s] to slash blog slash. And then inside of our template literal string, we can reference the post.slug.
[2269s] So, this is going to be the slugified version of that based on the name of that actual file.
[2275s] So, now, each one of these should be a link to that blog post even though that page doesn't exist yet.
[2281s] So, if we hover on this on the bottom left, you can see it links to slash blog slash blah blah.
[2286s] If we click on this, it doesn't exist and that's our responsibility to go and create that.
[2290s] So, we want to do a couple of things in here to make this look a little bit better. We'll cheat a
[2294s] little bit and copy in some components to help us. We'll start with the post list.astro component.
[2301s] And now, in this case, what we're going to do is define our props to take in a prop of post,
[2308s] which is an array of a collection entry of the type of post. Now, again, post is going back to that
[2315s] collection that we define. And we're just saying we have an array of those posts that we're passing
[2321s] inside of here. Now, then we have our tailwind CSS to be able to display a CSS grid here with two
[2328s] columns on bigger screens and then go down to one column on smaller screens. And then we display each
[2333s] individual post with a post component that we haven't created yet. So, inside of our posts or inside
[2339s] of components, we'll create one more component and this is going to be the post component that we can
[2345s] paste in. Alright, so very similar. We define a prop in here where we're going to take one property,
[2352s] which is a collection entry of posts. So, it's one post. We then destructure that and now we can
[2358s] reference each piece of that data. So, notice we also have the same kind of link in here with an H2
[2364s] where we have the post.data.title. And then we have the link that's linking to slash blog and then
[2370s] the slug. We also are referencing the body of our blog posts, but we're using a few CSS or tailwind CSS
[2377s] classes or one in here to say line clamp of two. This will give it a maximum line, maximum display of
[2385s] two lines and then use ellipses to finish it out. And then at the top of this, we're also referencing
[2390s] our image, which will come back to in a minute as we go and optimize these in a second. So, we can
[2396s] save this. We can save the post list component. Let's go back to our blog page and let's get rid of
[2401s] this log and just make sure all this stuff looks good. So, let's scroll to the bottom of these logs
[2406s] and the terminal. There we go. And then now if we refresh our page here, nothing looks different because
[2412s] we actually need to use that post list component. So, we'll replace the anchor tag that we wrote,
[2418s] we'll reference the post list component and then we'll pass into that our post property that we
[2424s] queried above. And then we'll need to also import this. So, I'm going to copy the layout import,
[2431s] paste in post list, and then paste in our type in post list here as well. And this is from the
[2439s] components, not the layouts directory. So, now we should see that we're actually loading each of
[2446s] these posts and it's linking to the individual page for that post. So, notice this doesn't display
[2451s] yet because we haven't generated those pages, but we do have the ability to link to each individual
[2456s] blog post, which is pretty neat. Now, let's go ahead and generate the pages for each one of these.
[2460s] Now, to do this, let's go to the Astro documentation really quick to kind of show you how we're going to.
[2466s] So, we can search for dynamic routes. And in this case, what we do is define a file that basically
[2472s] is going to have a placeholder in the file name that tells us some property that we can use to then
[2478s] query and display the appropriate information for that post. Now, in our case, what we're going to
[2484s] reference is the slug of the blog post. So, inside of our pages directory, we can create a new folder,
[2490s] slash our blog. And then inside of here, we're going to create a new file that says inside of brackets,
[2496s] slug, and then dot Astro. Now, what this means again is that we're going to be able to get the slug
[2503s] for each one of these posts by defining each one of the different routes that we have. Now, the way we
[2509s] define each one of these routes is we open up our JavaScript snippet here. And then we're going to
[2514s] export a function called get static paths. This is going to be an async function. And then inside of
[2521s] here, we want to query all of our posts. So, just like we did before, we'll call get collection.
[2526s] And we'll pass in the post name. And we'll need to import that get collection function. So, we'll
[2533s] import this get collection. And we'll also import collection entry from Astro content. And once we
[2545s] have each one of our posts, what we want to you, what we want to do is use those posts to be able to
[2550s] generate the path that should be created by Astro for each one of these different individual blog posts.
[2555s] So, we're going to create a path's property. We'll take our post variable, we'll map through it,
[2561s] we'll get a reference to each post. And then inside of here, we want to return an object. And
[2568s] this is going to have a couple of different properties. The first one is params. So now inside of our
[2574s] params, these are params that we can pass directly to this component. So, we want to pass in that
[2579s] slug property. And it's going to come from post.slug. And then we want to pass in our props. So, our props
[2586s] is going to be the post itself. So by exporting this get static pass function, we're basically
[2592s] defining a path and a property for each one of these blog posts that will generate statically for
[2599s] application. Now, from here, we can kind of define how this component is going to work. So, we'll define
[2604s] the props type. This is going to have one property of posts that's going to be a collection entry.
[2611s] It's also going to be referencing that post collection. So, we'll have one post is being passed in.
[2617s] We can then destructure this. So, we can get the post from astro.props. And then from that post,
[2625s] we can grab the contents. We can destructure the content itself from the post.render function,
[2634s] which is an async function. Now, from here, we want to kind of lay out a blog post page,
[2640s] just like we've done a few times before. So, let's copy over a few of these different components.
[2645s] So, let's just paste this in. So, I have our layout. We'll have our main and we'll have our H1.
[2651s] And we can import all of these at the top. So, we can import layout. And we can import the H1.
[2659s] And lastly, our main. And so, we've imported all three of the components that we're going to use
[2666s] in here. And just to start, we can now start to update a bit of information based on this individual
[2671s] blog post. So, in this title, we want this to actually be the title of the blog post that we're on.
[2676s] So, we can take post.data.title. And then inside of our H1 on the page, we can do the same thing.
[2685s] So, we want to reference our post.data.title. So, what we should have done now is we should have
[2691s] generated a page for each one of our blog posts that will, in this case, just display the title
[2696s] of that blog post. So, if we click on one of these blog posts from the slash blog page,
[2701s] it should take us to this page, but it looks like we have some sort of air in how we defined
[2706s] our git static pass function. So, it is expecting an array, but got undefined. So, let's go back up
[2712s] and double-check that. So, it looks like we defined our pass, but we didn't return this in the end.
[2719s] So, the most important part about the git static pass function is it has to return those paths so
[2724s] that Astro knows what to do with them to generate the individual pages. So, hopefully that will handle this.
[2730s] Now, if we refresh, we see the title now coming up at the top and we have our individual blog
[2736s] post pages for each of these individual blog posts. So, that's great. We have each of those defined.
[2741s] We can now kind of copy a little bit of code from the post component. So, if we go back to the post
[2747s] component and look at the image, we can now copy this end just so we're starting to display some
[2753s] more things on here. So, if we paste this in under the H1, we should now see our images popping up.
[2761s] So, it looks like it's not quite the size that we want, because we're now shrinking this to be a
[2766s] width of 600. So, we can update this to be 1024. And I think that should give us now kind of the full
[2773s] screen or almost full screen page that we're looking for. And then lastly, we can render this
[2780s] content component that we got from the render function of our post. So, up here we called
[2787s] post.render. We got the content component. We can render this, but it's not going to look great.
[2792s] So, you can see we have all of our content in here, but this doesn't quite look great. And that's
[2796s] because we don't have any styles to find for this. Now, in our case, what we're going to use is the
[2801s] tailwind CSS typography package to handle this for us. So, tailwind CSS typography. You can search
[2808s] this. Basically, what we're going to do is install this plugin. And then we'll be able to use this
[2811s] inside of our page to be able to display this stuff appropriately. So, I'm going to copy this install
[2817s] command. Let's go back, let's paste this in. And this is going to install the tailwind CSS typography
[2825s] package. Then inside of our tailwind config, we need to make sure to reference this. So, we're going
[2833s] to inside of this array require and then we're going to require the tailwind CSS typography package.
[2843s] And then lastly, for this to work inside of where we render our content, we're going to need to wrap
[2847s] this in a class or a div that has a few classes. Primarily, the pros and pros to Excel class.
[2856s] So, those are the classes that kind of activate this extension or to be able to use that plugin to
[2860s] be able to render all of our content. So, let's go ahead and run this with our run dev command.
[2865s] We can now come back to our application. We can refresh this. And we should see that this now is
[2870s] looking a lot better. And this is starting to feel like a real blog. So, we have, if we go back,
[2875s] we have a list of all of our blog posts. And then we can click on the individual pages, see the image
[2882s] and see all the content, which is pretty neat. Now, one thing that's interesting that's not very
[2887s] optimized on here is the way we're referencing our images. So, if we go into our network tab
[2893s] and just look at our images as they load in, we'll notice a couple of things. A couple of things.
[2898s] We're loading all of these images even before we scroll down to see them. So, that's a little bit
[2903s] unnecessary. It would be more optimal if we were only loading images as we're getting close to
[2908s] scrolling down to them. And then we'll also see that these are being loaded as JPEG files,
[2912s] which are not the most optimal format, where P would be a better format. And see that these are
[2917s] really big images. So, six megabytes, four megabytes, et cetera. So, we can use the Asher image
[2923s] component to make this a lot better and much more optimal. So, let's go back to the Asher documentation.
[2928s] And let's just search for image. And let's just go to the top level images here. And let's go down to
[2934s] the actual image component, which is what we're going to use to be able to do a lot of optimizations
[2939s] with our images. So, we can import the image component from the image assets namespace and
[2946s] basically just replace the regular IMG tag that we were already using. We'll have to do a few more
[2951s] things in here, but let's start with that. So, inside of our post component, we can copy in the
[2956s] import for this image component. And we can now replace that IMG with our image component.
[2963s] Now, if we come back to our application, we can see that this is going to work,
[2967s] but nothing is really changed. So, we're still loading all of these files and they're still JPEGs
[2973s] and they're still pretty big. So, one of the things we want to do is actually move this images
[2978s] directory into our content directory. And actually, specifically, we're going to move this into
[2984s] our slash post directory because these are going to be all the images that are associated with these
[2988s] posts. Now, then inside of our markdown, we're going to update. If you remember, we changed this
[2994s] at the beginning, we're going to select this little bit and we'll do command shift F or control
[2999s] shift F to select all of that. And what we want to do is we want to update this to go from slash to
[3005s] dot slash. So, slash implies that it should look at the root of the application, which is at the end
[3010s] of the URL. Dot slash now means we should look relative to where the actual file is. So, in this case,
[3017s] it's going to be relative to where this markdown file is. And that's going to be inside of that post
[3021s] directory. So, I'm going to update each one of these posts to reference dot slash image.
[3028s] And we would think that would work, but we actually have an error in here of something isn't working.
[3035s] And that's because we need to go back to our config for content collections. And we need to update
[3041s] our image property to actually use an image object that Astro gives to us. So, what we're going to do
[3047s] is turn instead of returning this object directly, we're going to turn this schema value into a function.
[3055s] And so, this is going to then return that z dot object. But by defining this as an function,
[3061s] we can now destructure a property called image. And now we can reference our image type to be of
[3069s] type image or call that image function. So, this is going to more explicitly references as an image
[3075s] in a way that Astro can understand in a way that it can also import those images from the content
[3080s] or inside of the source content directory. So, let's try this one more time. Run this again.
[3087s] So, the first thing that you notice, so you might notice, is that the URL for these images are looking
[3092s] kind of weird. And that's because Astro is using kind of internal URLs to define how to render these
[3098s] images. So, if you look really closely, you can see it defines the format and width and height, etc.
[3104s] But we can see that these are loading web p images and that these are much smaller than what they
[3108s] were originally. So, our page now is going to load much faster because these images are much more
[3113s] optimized and they're a better format and they're much, much smaller. Now, if we click on one of these
[3118s] individual pages, notice that this isn't working. And that's because we're referencing this using the
[3123s] old image IMG tag, which can't be referenced here. So, let's go into the slug page. And let's just
[3131s] update this to use the image component from Astro assets. And we should be able to just save this as
[3137s] is and have this be working. So, now we're able to load this image and this should also be choosing
[3143s] a web p version of this which should be smaller than what it was originally. So, now we have a working
[3148s] list of all the blog posts. We can then go to the individual page for a blog post. We see an optimized
[3154s] image and we can see all the content associated with that blog post as well. Now, with all this in place,
[3159s] there's one really neat thing that we can add that's really easy and Astro and pretty amazing.
[3163s] If we look at navigating between these pages, we see kind of this page refresh. We actually don't
[3168s] have the about page created, but we see kind of the page refresh as we go between individual pages.
[3174s] And we can actually make this a little bit easier by using the View Transitions API in Astro.
[3179s] So, so, only take it a second to add, but it does make a big difference in how you can view and navigate
[3185s] through your application. So, inside of the View Transitions API, you can you can read a lot more
[3191s] about this. We can basically import this component and then use it inside of the head of any page that
[3197s] we want to have those transitions between. So, since we want to by default use this on every single
[3202s] page, we can actually just import this inside of our layout file. And then somewhere in the head,
[3209s] we can just reference this View Transitions component and save. And now, we're actually going
[3215s] to be able to see a difference between how we navigate our pages. This is pretty neat. So, let's go
[3220s] from this page to home. Notice we get kind of the animation. We go to blog. We get animation.
[3226s] We see this page. We get the animation. So, it looks like a much, much better experience of
[3231s] navigating between pages with just one component that we can add. Now, there's some additional ways
[3236s] that you can customize this. You can also pass state from one page to another, which is pretty neat.
[3240s] We won't go any deeper into this, but it is nice to know that we can add this pretty easily to make
[3245s] the transitions in our application look a lot better. Now, one additional thing I wanted to show you
[3250s] is that you have the ability to not only use Markdown inside of Astero for your content, you can also
[3256s] add MDX. And the support for this comes with an integration that we can add with one of those
[3262s] NPX Astero add commands. So, let's go and add support for Markdown by pacing in this command.
[3269s] This will make a change to the Astero config and install that package. So, we'll just go ahead and
[3274s] say yes to all the things that it needs to do. Yes. All right, so that should be added.
[3279s] And now we can do an MPM run dev to start this again. And what we should see, if we come back to our
[3286s] Markdown files, for example, the behind the scenes, is we can now rename this file,
[3293s] and we can rename this to an MDX file. And now, all of this should still stay the same. So,
[3299s] if we come back to the application. So, if we refresh this, we see this stays the exact same,
[3304s] which is exactly what we wanted. But now we can harness the power of MDX in addition to just the
[3309s] Markdown that we were already using. Now, if we scroll back up, there's a quick section on Y MDX.
[3315s] So, now, we're not going to dive deep into this, but there are lots of really cool things that you
[3319s] can do, like MDX only features. So, you can use exported variables inside of MDX. So, if you wanted
[3326s] to create variables at the top, you could then reference that. You can also use your front-matter variables,
[3332s] so you could use those directly inside of here as well. And the last thing is you can reference
[3336s] Astero components and UI components of other frameworks like React View, etc., inside of this as well.
[3344s] So, if we look in the example in here inside of the MDX part, we're importing two different
[3350s] components, one Astero component and one React component that we can then display right in line inside
[3356s] of our Markdown content. Now, this is really useful as an example to do like a callout inside of your
[3362s] blog post. If you want to customize a callout to send somebody to a newsletter or something else,
[3367s] you could define those components and bring those into your MDX files anytime that you want or
[3374s] need. So, we're not going to dive any deeper into MDX. That's kind of a section on its own,
[3379s] but it is nice to know that you have the ability to work with both Markdown and MDX files in your
[3384s] content with Astero. Now, we can start to work on deploying this application. So, we initialize this
[3391s] initially as a GitHub or a Git repository. So, we can now do a Git status and we can see all
[3398s] the things that we've changed. Now, in this case, we can add everything with Git add star,
[3402s] then we can do a Git commit-m to say initial commit and all of this stuff has been committed to this
[3410s] local Git repository. Now, the next step is we need to connect this to a GitHub repository that we can
[3416s] then use to deploy to Netlify or Verselm. So, on GitHub.com, you can go to the top right. You can
[3424s] click New Repository and then we can call this FCC Astro Crash Course Test. We'll have this be a
[3434s] public directory. We don't want to add a readme because we'll take care of that ourselves. We don't
[3438s] want to add a Git Ignore. So, we can create this very blank repository and then what we'll do is just
[3444s] take the code into our terminal that pushes from an existing repository to our local Git repository
[3453s] to this GitHub repository. So, you can copy this section where it adds the origin and the remote, or
[3460s] it adds the remote origin and then pushes everything locally to that remote project inside of GitHub.
[3468s] All right. So, it looks like it pushed all of that code up. If we come back to the GitHub repository,
[3473s] we can come in here and see that this has been added. And so, now our next step is to go to,
[3478s] we'll start with Netlify. We can do Netlify and Verselm. These are both free. So, you'll sign up with a
[3482s] free account after you do. You can log in on Netlify. And basically, what we're going to do is add a
[3487s] new site where we're going to import from an existing project. We'll choose to deploy with GitHub.
[3493s] And then what we need to do is go and choose that project that we just created. So, I can search FCC
[3498s] dash and this should be enough to pull up that project. All right. So, we can now pull this in.
[3504s] We don't need to customize anything. It should pick up on what the build command is automatically.
[3509s] So, we can go ahead and deploy this and it should run a build and then have this site ready for us to
[3513s] use after it's done. All right. So, it looked like this build has finished. You can now kind of choose
[3520s] the random URL that they gave you. And we should see that this is deployed our application successfully.
[3525s] So, we can see our blog page. We can go and click on individual ones as well. So, that is on
[3530s] Netlify. We could also choose to deploy this on Versel almost the exact same process. You'll sign up
[3536s] for a free account so you can then come and add a new project. You're going to import this from a
[3541s] Git repository. Choose from that FCC crash course project and GitHub. Choose all the defaults and then
[3547s] go into deploy. And then after this is finished, we should have this deployed on Versel as well.
[3554s] All right. So, it looks like this has finished on Versel. We can continue to the dashboard.
[3558s] Then we can visit this at the random URL that it's generated for us also.
[3562s] Now, so far everything that we've done with Astro has been statically generated pages.
[3567s] Well, we can start to look into the SSR capabilities of Astro. So, Astro actually has the ability to do
[3574s] a full backend if you so decide. And you have the ability to define what type of output your site
[3581s] is going to have. So, by default, it is static, which takes no additional configuration for us to do.
[3587s] But there also is the ability to define it as a server rendered application by default.
[3591s] It says to use this one most or all of your site should be server rendered. You can also opt in to
[3596s] pre-rendering or static pages for individual pages. You also have the option to do hybrid, which is
[1:00:02] basically saying it's going to pre-rendered by default. And then you can define for individual pages
[1:00:07] to opt out of pre-rendering. Before we make this transition into server-side rendering in our code,
[1:00:13] let's actually take a couple of minutes to talk about the difference between static site generation
[1:00:17] or SSG versus SSR, which is server-side rendering. And we'll talk about this while using diagrams to
[1:00:23] kind of explain the differences between the two. So, let's start with what we've already been using,
[1:00:27] which is SSG or static site generation. And this is what Astro does by default.
[1:00:32] Quick reminder, if you want to follow up on this diagram later on, you can find the link in the
[1:00:35] description below. So, what happens here is when we deploy our application, we deploy this
[1:00:40] as something like Netlify or Versel, or there's lots of other hosts that you could use as well.
[1:00:45] So, when this thing is deploying, it actually runs a build. And during that build process, what happens
[1:00:49] is for each one of those individual pages that we have in our Astro application, it actually generates
[1:00:54] the HTML file at build time for each one of those. So, we have one for our index.html page.
[1:01:00] We have one for our blog.html page. And then additionally, we have an HTML page created for each
[1:01:06] one of our blog posts. And that's where we define that export or we exported that get static pass
[1:01:11] function, where we defined each one of the pass that we wanted to be able to support and then
[1:01:16] to generate the content for. So, the important part about this is SSG at build time is going to go
[1:01:22] ahead and create the content or the HTML pages for each one of these pages at build time so that it's
[1:01:28] ready and accessible by the time someone comes and tries to view one of these blog posts for example
[1:01:33] or one of these other pages. And that's an important next step to talk about is the actual request time.
[1:01:38] So, what happens? Well, these individual files that are generated during the build process are then
[1:01:43] saved to a CDN or a content delivery network that are replicated all across the world. What this
[1:01:49] means is that those files now are very fast to access and return when a request comes in from the
[1:01:55] browser. So, let's say that you go to the browser and you type in local host, but you type in
[1:01:59] the URL of the application that you're trying to work on and you go to the index page. That's going
[1:02:04] to make a request to the CDN. It's going to now return that index.html page. Now, let's say you then
[1:02:09] want to go to the blog page. Well, you make another request to the CDN. The CDN now is going to return
[1:02:14] blog.html. Or if you're going to one of the specific pages for an individual blog post, it's going
[1:02:19] to return those pages as well. Again, because they've already been predefined. Now, this starts to
[1:02:25] differ a lot when we look at SSR or server side rendering. So, I've got almost an empty diagram here
[1:02:31] for SSR build time. So, with server side rendering, you're still going to have a build process to go through
[1:02:36] and run all of your code. You may run test if you have them, but basically this is going to go through and
[1:02:41] do the build of your application. And you may have some static pages. We'll talk about how to mix these
[1:02:47] in a second. But, for the most part, what this is going to do is now kind of have that server configured
[1:02:52] so that it can handle those requests as it comes in. So, notice there's no predefined HTML pages
[1:02:58] that are already calculated for us. That means if we scroll down now that something has to happen
[1:03:03] at request time when this request comes in from the browser. So, notice instead of having a CDN,
[1:03:09] we now have an application server. So, requests will go from the browser to the application server.
[1:03:14] Now, for this application server to respond back, most likely specifically in this case with our
[1:03:19] blog post, individual blog post pages, it's going to need to get the information necessary for those
[1:03:24] blog posts from the database. Now, in our case, we're not using a traditional database. We're using
[1:03:29] embedded markdown in our source code, but it basically works the same way. So, let's say that we make
[1:03:34] a request to slash blog slash blog dash one, for example, that's the website that we're trying to go to.
[1:03:40] Well, this is going to now make a request to the database. Let's add a new piece of text in here.
[1:03:45] And it's going to say, give me all the information that you have about blog one. So, from the database,
[1:03:51] the database is going to return back the data for blog one to the application server. The application
[1:03:57] server is now going to turn that back into an HTML page, which will look like if we add our corresponding
[1:04:04] piece of text to your slash blog slash blog dash one dot HTML. We can move this up a little bit for
[1:04:13] readability. So basically with SSR or server side generated pages or applications, every request that
[1:04:20] comes in is going to go to an application server. It is it is then going to query the database or in
[1:04:25] whatever format it is, which might be embedded markdown that will return the data. The application
[1:04:30] server will then take that content and turn it into an HTML page that can be rendered on the browser
[1:04:35] and viewed by the user. So that's a quick overview of the difference between SSG and SSR. Let's go back
[1:04:41] to our code and start to make this work inside of Astro. So in this case, we can start by going into
[1:04:48] the Astro config and we can choose the output property to be server. Now, if we try to run this,
[1:04:54] we should see that this is going to break. And that's because we're doing a couple of things that
[1:04:59] are specifically geared towards statically generated pages. So let's go to our running application and
[1:05:05] refresh and we see that we now are having an issue on the individual blog posts pages. And that's
[1:05:11] because the way that we're generating those pages is using this get static paths function. And
[1:05:17] that doesn't exist inside of an SSR deployed application. Now one thing we could do is we could
[1:05:24] look in the documentation and we could see how to define this as a pre rendered page. That means
[1:05:30] it's going to generate this page statically. So if we add this at the top of this file and refresh,
[1:05:35] this actually will go back to working as we expect. So now we see we have that blog post. We can go back
[1:05:40] to all of them and go to another one, etc. But just for practice and kind of experience, let's go back
[1:05:47] and get rid of the pre render and let's see what it would take to actually figure out how to generate
[1:05:52] these pages inside of an SSR environment. Now in this case, what we're going to do is go back to
[1:05:58] our page and we can actually get rid of this entire get static pass function. And we can
[1:06:06] start at the top here. And most importantly, what we're going to do is destructure a property called
[1:06:11] slug from astro.params. Now what astro was going to do is because of this slug definition up here,
[1:06:18] it's going to pass this slug into the astro.params object to let us reference it and use that in here to
[1:06:26] dynamically query that post from astro. We can also get rid of our definition for the post and our
[1:06:33] prop types. And then what we're going to do is we're going to get that post from an awaited call
[1:06:40] to get entry by slug. Now this is a function up here that comes from astro content. It's a function
[1:06:47] that they give us and we can say what content collection we want to get this from, which in this case
[1:06:52] is post. And then we can pass in the slug. Now in this case, it's going to throw an error or show us
[1:06:57] the typescript error because slug could be potentially undefined. So we're just going to say this is
[1:07:02] going to be a string so that we get our appropriate type in here. Now in this case, it's throwing
[1:07:07] an error because it's saying that we might query for a post that also doesn't exist. And what we
[1:07:12] could do, we could say if that post doesn't exist, we could do an astro redirect so we could
[1:07:17] return an astro redirect to the slash four or four or four page just just to show that that thing
[1:07:26] wasn't found. Now we could go and customize this and do anything that we wanted to handle it. But in
[1:07:31] this case, this ought to be enough just to get this working and now have these dynamically generated
[1:07:36] pages be dynamically generated with server side rendering instead of statically generated pages.
[1:07:41] So if we go back, we should see that we have all these showing up in our blog index page and then
[1:07:46] clicking on one should be able to show all the details for this blog post as well. So we've now
[1:07:51] completely flipped how we're rendering these blog post pages. Now they're server side rendered and
[1:07:57] what this means just to clarify is as the request comes into this URL, it's going to send a request to
[1:08:02] the server, the server is going to query based on that URL, the individual blog post, return that back
[1:08:07] and then use that to render the page that shows up on the screen as opposed to previously.
[1:08:13] We had each of these pages generated statically at build time for all the blog posts that we have.
[1:08:19] Now the cool thing about this is we can still go back and configure individual pages to be configured
[1:08:25] as static. So as an example on the homepage, there's no reason that this shouldn't just be a static
[1:08:31] page. So we can still export a const pre render variable that's set to true and that will mark this index
[1:08:40] page as static. So if we go back here, this will be a static page versus this is server side rendered
[1:08:48] and this individual page or all the individual pages for our blog post are server side rendered as well.
[1:08:55] Now what I do want to show one more thing that you can do is when you have server side rendered
[1:09:00] enabled, you can define API endpoints. So we can search for server endpoints in here and basically
[1:09:07] what that allows us to do is have a file inside of our pages directory that just basically serves as an
[1:09:13] API endpoint instead of actually returning an astro component. So if we go into our pages directory,
[1:09:19] we could create a new folder slash or called API and then we could just create a test.ts. Now
[1:09:26] notice this is a TS file instead of an astro component again, because it only runs on the server.
[1:09:32] And if we look in here, just copy kind of the basic starter code that they give us, but we're not
[1:09:36] going to reference any of this. So we can kind of get rid of all of this information about products.
[1:09:43] And then we can return back an object with a message that says hello world. So this is how we define a
[1:09:51] starter function for API endpoints. So in this case, we're also not referencing this parameter.
[1:09:58] So what we defined is a get endpoint where we're basically just going to return hello world as JSON.
[1:10:04] So we can save this. We can then go back to our application. We can then open the URL and go back
[1:10:10] to the root and then slash API slash test. And we should get back that message with hello world.
[1:10:16] Now, what's really cool is we can define all of our HTTP endpoints with this as well. So we could also
[1:10:23] export a post function if we wanted to. We could return with the same thing. Now,
[1:10:28] unfortunately, there's not a way to be able to test this inside of the browser, because the browser
[1:10:33] can only send get requests. So I have the postman extension inside of VS code installed. So if you
[1:10:38] wanted to follow along, you could install the postman extension. This has been kind of my default way
[1:10:43] of doing testing APIs for a long time. But now they have the the VS code extension to go with it.
[1:10:49] So we can create a new HTTP request and we can now send a post request to the same general idea.
[1:10:56] So local host for three to one. And this will be slash API slash test. And we'll send that and we'll
[1:11:03] get the same response that we just got back with the message of hello world. Now inside of handling our
[1:11:08] post request, we could also destructure the request as well. And then we could get the body
[1:11:16] from that request by calling a weight request dot JSON. And then we could just return this just to
[1:11:24] show that we're actually getting it. So let's just return that body, which is going to be an object.
[1:11:29] So we're destructuring this request. Notice we also don't have TypeScript types around this.
[1:11:33] So we could define this a little bit differently. If we wanted to, I'm just going to copy in a new
[1:11:38] kind of function definition here. So this is going to use an arrow function syntax where we define
[1:11:44] now our post to be an API route so we can add the missing import for that. Now it's going to give us
[1:11:51] intelligence for the request. So if we do dot, we can see all the things that we have access to there.
[1:11:56] We also then can see things that we might have on our programs in here as well. So we can now save this
[1:12:02] and what we should see is if we go back to our request, this is a post request, but it doesn't have a
[1:12:07] body. So we can now inject in here a raw body with JSON and we could have an object and we could
[1:12:15] have a property of name and we could say astro crash course. And then what this should return back with
[1:12:23] is that same object that we can see down here. So we have the ability in astro to define any and all
[1:12:30] kind of server capabilities that we wanted. We could handle form submissions. We could define API
[1:12:34] endpoints for all of our different methods, which is really, really cool and really, really powerful.
[1:12:38] So I think the only next step is to show how do we actually deploy this to Netlify and Versel now that
[1:12:44] we're doing server side rendering. So inside of here, we have a plugin or an integration for both
[1:12:50] Netlify and Versel. So I'm going to copy and paste this command. So here's the Netlify one.
[1:12:57] And let's paste this in here for Netlify. And this is going to add that package and then it's going to
[1:13:03] make an addition to our astro.config file to use Netlify as the host. So notice it says adapter is
[1:13:10] Netlify. We'll say yes. And then if we look inside of the astro config, we should see that it's
[1:13:16] referencing adapter Netlify here. So now what we want to do is we will add everything. We'll do a
[1:13:24] git commit with a message of added SSR and deploy to Netlify. And then since this is already
[1:13:34] connected to our Netlify site, we can push this and Netlify ought to automatically pick this up,
[1:13:39] pick up that change. Let's just log back in. It should pick up that change automatically. And it
[1:13:44] should be building a new version of that that now is going to be our server side rendered version.
[1:13:50] So we'll let this go through our build and then we'll open this up to make sure everything looks
[1:13:53] okay. Now as this is building one thing to notice, it's referencing Netlify functions actually
[1:13:59] just missed it. But inside of building, you can see that it references deploying this to Netlify
[1:14:03] function. So that's how it's actually able to deploy this. It looks like everything is complete. We
[1:14:08] should be able to open this production deploy. Hopefully everything now continues to look okay just
[1:14:14] like it did before. And we can see the individual blog post pages as well. Now next we'll need to do this
[1:14:20] for Versel. So we can copy in that same command and then add in Versel. Now this is going to go
[1:14:27] through that same process, add the Versel package, and then it's going to update the astro config to
[1:14:32] reference that Versel adapter. So we can say yes to that as well. So now this is updated. We can add
[1:14:39] everything again. We can commit and say hosting on Versel. Then we can push this. Now do note that
[1:14:48] deploy in Netlify. We're still connected in Netlify. So this will kick off another build.
[1:14:52] That next build will fail in Netlify. But what we do want to see is inside of Versel, we should see
[1:14:59] that this is kicking off a new build in Versel. So we can see under the building tab that it's going
[1:15:05] through and it's doing this. So when that finishes, we should see that we have the same deployed
[1:15:11] application hosted now on Versel using SSR. All right. So it looks like it's finished. We should now
[1:15:17] be able to visit this and we have the same experience where we can go to blog. We can see all the pages.
[1:15:22] We can go to the individual page, etc. Now going back to the idea of SSR inside of astro,
[1:15:30] one of the coolest things that you could do in addition to API endpoints and other things is you
[1:15:35] could start to incorporate authentication into your applications. So you have the ability with astro to
[1:15:40] create full-fledged full stack applications and you can do authentication here by referencing cookies
[1:15:46] as an example. So you could track a session in a cookie for a user and you could gate pages to prevent
[1:15:52] users from getting to certain pages if they're not logged in or if they don't have certain permissions
[1:15:57] or anything like that with authentication in that full astro course. We actually build a basic
[1:16:02] authentication strategy using SSR and astro as well as taking advantages of taking advantage of cookies
[1:16:11] and then having that reference users that are saved inside of a data database. So another really
[1:16:16] cool full stack implication or example of what you can build with astro. Now if you want a full
[1:16:22] overview of what we build in that course, you can go to astrocourse.dev and it breaks down everything
[1:16:28] that we're going to build inside of this full application, including all the topics that are
[1:16:32] covered, the pricing, etc. So if you're interested in that, you can find that at astrocourse.dev.
[1:16:38] All in all, I hope you're as excited about astro as a framework as I am. Obviously, I'm pretty
[1:16:43] excited. So thanks for checking out this crash course and I really hope that you enjoyed it.