diff --git a/docs/astro-howto/README.md b/docs/astro-howto/README.md new file mode 100644 index 0000000..639f4b4 --- /dev/null +++ b/docs/astro-howto/README.md @@ -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 diff --git a/docs/astro-howto/artifacts.sha256 b/docs/astro-howto/artifacts.sha256 new file mode 100644 index 0000000..619a6e4 --- /dev/null +++ b/docs/astro-howto/artifacts.sha256 @@ -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 diff --git a/docs/astro-howto/contact-sheet/contact_sheet.jpg b/docs/astro-howto/contact-sheet/contact_sheet.jpg new file mode 100644 index 0000000..21e3a9a Binary files /dev/null and b/docs/astro-howto/contact-sheet/contact_sheet.jpg differ diff --git a/docs/astro-howto/contact-sheet/generate_contact_sheet.py b/docs/astro-howto/contact-sheet/generate_contact_sheet.py new file mode 100644 index 0000000..5617920 --- /dev/null +++ b/docs/astro-howto/contact-sheet/generate_contact_sheet.py @@ -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() diff --git a/docs/astro-howto/contact-sheet/report.json b/docs/astro-howto/contact-sheet/report.json new file mode 100644 index 0000000..9ff1959 --- /dev/null +++ b/docs/astro-howto/contact-sheet/report.json @@ -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 +} \ No newline at end of file diff --git a/docs/astro-howto/docs/COMMANDS.md b/docs/astro-howto/docs/COMMANDS.md new file mode 100644 index 0000000..1d2597c --- /dev/null +++ b/docs/astro-howto/docs/COMMANDS.md @@ -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 | diff --git a/docs/astro-howto/docs/HOWTO.md b/docs/astro-howto/docs/HOWTO.md new file mode 100644 index 0000000..76ced1c --- /dev/null +++ b/docs/astro-howto/docs/HOWTO.md @@ -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 ` + +## 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: +``` + +## 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 `` 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 && 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 +--- + +``` + +### 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` | diff --git a/docs/astro-howto/docs/OUTLINE.md b/docs/astro-howto/docs/OUTLINE.md new file mode 100644 index 0000000..f0ddff8 --- /dev/null +++ b/docs/astro-howto/docs/OUTLINE.md @@ -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 diff --git a/docs/astro-howto/docs/QUESTIONS.md b/docs/astro-howto/docs/QUESTIONS.md new file mode 100644 index 0000000..7444af3 --- /dev/null +++ b/docs/astro-howto/docs/QUESTIONS.md @@ -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 diff --git a/docs/astro-howto/docs/SCREENSHOTS.md b/docs/astro-howto/docs/SCREENSHOTS.md new file mode 100644 index 0000000..bea04f0 --- /dev/null +++ b/docs/astro-howto/docs/SCREENSHOTS.md @@ -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 diff --git a/docs/astro-howto/docs/SUMMARY.md b/docs/astro-howto/docs/SUMMARY.md new file mode 100644 index 0000000..326b365 --- /dev/null +++ b/docs/astro-howto/docs/SUMMARY.md @@ -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() → 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. diff --git a/docs/astro-howto/run_manifest.json b/docs/astro-howto/run_manifest.json new file mode 100644 index 0000000..08559eb --- /dev/null +++ b/docs/astro-howto/run_manifest.json @@ -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." +} diff --git a/docs/astro-howto/screenshots/001_00-01-05_astro.new_website_showing_starter_template_options.jpg b/docs/astro-howto/screenshots/001_00-01-05_astro.new_website_showing_starter_template_options.jpg new file mode 100644 index 0000000..5e846ac Binary files /dev/null and b/docs/astro-howto/screenshots/001_00-01-05_astro.new_website_showing_starter_template_options.jpg differ diff --git a/docs/astro-howto/screenshots/002_00-01-25_documentation_search_modal_opened_with_command+k.jpg b/docs/astro-howto/screenshots/002_00-01-25_documentation_search_modal_opened_with_command+k.jpg new file mode 100644 index 0000000..09de58e Binary files /dev/null and b/docs/astro-howto/screenshots/002_00-01-25_documentation_search_modal_opened_with_command+k.jpg differ diff --git a/docs/astro-howto/screenshots/003_00-02-39_animated_astro_project_creation_wizard_in_terminal.jpg b/docs/astro-howto/screenshots/003_00-02-39_animated_astro_project_creation_wizard_in_terminal.jpg new file mode 100644 index 0000000..cf0eb80 Binary files /dev/null and b/docs/astro-howto/screenshots/003_00-02-39_animated_astro_project_creation_wizard_in_terminal.jpg differ diff --git a/docs/astro-howto/screenshots/004_00-03-00_houston_mascot_exit_animation_after_project_creation.jpg b/docs/astro-howto/screenshots/004_00-03-00_houston_mascot_exit_animation_after_project_creation.jpg new file mode 100644 index 0000000..029fb95 Binary files /dev/null and b/docs/astro-howto/screenshots/004_00-03-00_houston_mascot_exit_animation_after_project_creation.jpg differ diff --git a/docs/astro-howto/screenshots/005_00-04-10_starter_application_at_localhost:4321.jpg b/docs/astro-howto/screenshots/005_00-04-10_starter_application_at_localhost:4321.jpg new file mode 100644 index 0000000..e8341ae Binary files /dev/null and b/docs/astro-howto/screenshots/005_00-04-10_starter_application_at_localhost:4321.jpg differ diff --git a/docs/astro-howto/screenshots/006_00-08-38_vs_code_.astro_file_without_extension_showing_as_plain_text.jpg b/docs/astro-howto/screenshots/006_00-08-38_vs_code_.astro_file_without_extension_showing_as_plain_text.jpg new file mode 100644 index 0000000..32657bf Binary files /dev/null and b/docs/astro-howto/screenshots/006_00-08-38_vs_code_.astro_file_without_extension_showing_as_plain_text.jpg differ diff --git a/docs/astro-howto/screenshots/007_00-09-00_vs_code_.astro_file_with_astro_extension_showing_syntax_high.jpg b/docs/astro-howto/screenshots/007_00-09-00_vs_code_.astro_file_with_astro_extension_showing_syntax_high.jpg new file mode 100644 index 0000000..8dda9d6 Binary files /dev/null and b/docs/astro-howto/screenshots/007_00-09-00_vs_code_.astro_file_with_astro_extension_showing_syntax_high.jpg differ diff --git a/docs/astro-howto/screenshots/008_00-10-17_astro.config.mjs_showing_tailwind()_in_integrations_array.jpg b/docs/astro-howto/screenshots/008_00-10-17_astro.config.mjs_showing_tailwind()_in_integrations_array.jpg new file mode 100644 index 0000000..3dd40b4 Binary files /dev/null and b/docs/astro-howto/screenshots/008_00-10-17_astro.config.mjs_showing_tailwind()_in_integrations_array.jpg differ diff --git a/docs/astro-howto/screenshots/009_00-31-33_blog_listing_page_with_grid_of_post_cards.jpg b/docs/astro-howto/screenshots/009_00-31-33_blog_listing_page_with_grid_of_post_cards.jpg new file mode 100644 index 0000000..46cc86f Binary files /dev/null and b/docs/astro-howto/screenshots/009_00-31-33_blog_listing_page_with_grid_of_post_cards.jpg differ diff --git a/docs/astro-howto/screenshots/010_00-34-20_individual_blog_post_page_with_rendered_markdown.jpg b/docs/astro-howto/screenshots/010_00-34-20_individual_blog_post_page_with_rendered_markdown.jpg new file mode 100644 index 0000000..76a30f8 Binary files /dev/null and b/docs/astro-howto/screenshots/010_00-34-20_individual_blog_post_page_with_rendered_markdown.jpg differ diff --git a/docs/astro-howto/screenshots/011_00-51-01_browser_network_tab_showing_webp_images.jpg b/docs/astro-howto/screenshots/011_00-51-01_browser_network_tab_showing_webp_images.jpg new file mode 100644 index 0000000..c8addac Binary files /dev/null and b/docs/astro-howto/screenshots/011_00-51-01_browser_network_tab_showing_webp_images.jpg differ diff --git a/docs/astro-howto/screenshots/012_00-52-43_view_transitions_animation_between_pages.jpg b/docs/astro-howto/screenshots/012_00-52-43_view_transitions_animation_between_pages.jpg new file mode 100644 index 0000000..c586204 Binary files /dev/null and b/docs/astro-howto/screenshots/012_00-52-43_view_transitions_animation_between_pages.jpg differ diff --git a/docs/astro-howto/screenshots/013_00-54-10_mdx_file_with_inline_components.jpg b/docs/astro-howto/screenshots/013_00-54-10_mdx_file_with_inline_components.jpg new file mode 100644 index 0000000..44802ec Binary files /dev/null and b/docs/astro-howto/screenshots/013_00-54-10_mdx_file_with_inline_components.jpg differ diff --git a/docs/astro-howto/screenshots/014_00-57-58_netlify_deploy_dashboard_showing_successful_build.jpg b/docs/astro-howto/screenshots/014_00-57-58_netlify_deploy_dashboard_showing_successful_build.jpg new file mode 100644 index 0000000..d764b10 Binary files /dev/null and b/docs/astro-howto/screenshots/014_00-57-58_netlify_deploy_dashboard_showing_successful_build.jpg differ diff --git a/docs/astro-howto/screenshots/015_00-58-50_vercel_deploy_dashboard.jpg b/docs/astro-howto/screenshots/015_00-58-50_vercel_deploy_dashboard.jpg new file mode 100644 index 0000000..44f5822 Binary files /dev/null and b/docs/astro-howto/screenshots/015_00-58-50_vercel_deploy_dashboard.jpg differ diff --git a/docs/astro-howto/screenshots/016_01-05-17_api_endpoint_returning_json_in_browser.jpg b/docs/astro-howto/screenshots/016_01-05-17_api_endpoint_returning_json_in_browser.jpg new file mode 100644 index 0000000..20b8aef Binary files /dev/null and b/docs/astro-howto/screenshots/016_01-05-17_api_endpoint_returning_json_in_browser.jpg differ diff --git a/docs/astro-howto/screenshots/017_01-09-17_ssr_individual_post_page_loading_dynamically.jpg b/docs/astro-howto/screenshots/017_01-09-17_ssr_individual_post_page_loading_dynamically.jpg new file mode 100644 index 0000000..afcca87 Binary files /dev/null and b/docs/astro-howto/screenshots/017_01-09-17_ssr_individual_post_page_loading_dynamically.jpg differ diff --git a/docs/astro-howto/screenshots/index.md b/docs/astro-howto/screenshots/index.md new file mode 100644 index 0000000..8998e47 --- /dev/null +++ b/docs/astro-howto/screenshots/index.md @@ -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 diff --git a/docs/astro-howto/screenshots/report.json b/docs/astro-howto/screenshots/report.json new file mode 100644 index 0000000..c1967c8 --- /dev/null +++ b/docs/astro-howto/screenshots/report.json @@ -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"} + ] +} diff --git a/docs/astro-howto/scripts/extract_screenshots.sh b/docs/astro-howto/scripts/extract_screenshots.sh new file mode 100755 index 0000000..bfd3f19 --- /dev/null +++ b/docs/astro-howto/scripts/extract_screenshots.sh @@ -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." diff --git a/docs/astro-howto/scripts/extract_screenshots_fast.sh b/docs/astro-howto/scripts/extract_screenshots_fast.sh new file mode 100644 index 0000000..db3a35e --- /dev/null +++ b/docs/astro-howto/scripts/extract_screenshots_fast.sh @@ -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." diff --git a/docs/astro-howto/scripts/generate_contact_sheet.py b/docs/astro-howto/scripts/generate_contact_sheet.py new file mode 100644 index 0000000..5617920 --- /dev/null +++ b/docs/astro-howto/scripts/generate_contact_sheet.py @@ -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() diff --git a/docs/astro-howto/scripts/transcribe_local.py b/docs/astro-howto/scripts/transcribe_local.py new file mode 100644 index 0000000..67a6a30 --- /dev/null +++ b/docs/astro-howto/scripts/transcribe_local.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +""" +Local audio transcription using faster-whisper. +Usage: python3 transcribe_local.py [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() diff --git a/docs/astro-howto/transcript_local.txt b/docs/astro-howto/transcript_local.txt new file mode 100644 index 0000000..e50c59d --- /dev/null +++ b/docs/astro-howto/transcript_local.txt @@ -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.