diff --git a/app/stores/products.ts b/app/stores/products.ts
index 0c159d9a6..d77e02133 100644
--- a/app/stores/products.ts
+++ b/app/stores/products.ts
@@ -1,4 +1,9 @@
export const products = {
+ 'deno': {
+ id: 'deno',
+ price: 'price_1QIkM0BF7AptWZlctgYjiOTL',
+ amount: 20,
+ },
'linux': {
id: 'linux',
price: 'price_1PY9DmBF7AptWZlcpkAbgZlP',
diff --git a/content/courses/deno/_index.md b/content/courses/deno/_index.md
new file mode 100644
index 000000000..62c603c08
--- /dev/null
+++ b/content/courses/deno/_index.md
@@ -0,0 +1,62 @@
+---
+lastmod: 2024-11-07T11:11:30-09:00
+title: Deno Full Course
+description: Master TypeScript and Backend WebDev with Deno
+weight: 0
+type: courses
+vimeo: 1027549123
+author: Jeff Delaney
+tags:
+ - typescript
+ - deno
+ - pro
+
+stack:
+ - deno
+ - ts
+ - js
+---
+
+**Deno - The Full Course** is a hands-on tutorial where you will build a complete web app with [Deno](https://kit.svelte.dev/), using zero 3rd-party denpendices to master TypeScript and Web Platform APIs.
+
+## What will I learn?
+
+- π¦ Everything you need to be productive with Deno
+- πͺ TypeScript fundamentals in fast to-the-point videos
+- πΈοΈ Web Platform APIs that work everywhere
+- π₯ Build a full-stack web app with zero-dependencies
+- π§ͺ Test-Driven Development & Benchmarking
+- πͺ Roll your own cookie-based user authentication
+- πΉ Manage data with Deno KV
+- β‘ Stream database changes in realtime
+- βοΈ Server-rendered HTML with JSX
+- π Deno Deployment
+
+
+## π¦ What will I build?
+
+You will build a **Realtime Link Shortener** inspired by [π΄ Bit.ly](https://bit.ly/) where users can create sharable links and. The goal of this project is to help you master web development fundamentals and learn advanced TypeScript patterns.
+
+### π Try it out!
+
+Visit the demo app and give it a test drive before you enroll.
+
+
+This course is intermediate level π¦ and expects some familiarity with JavaScript and web development. The content is fast-paced and similar to my style on YouTube, but far more in-depth and should be followed in a linear format.
+
+
+
+## When was the course last updated?
+
+Updated November 7th, 2024Deno 2
+
+## How do I enroll?
+
+The first few videos are *free*, so just give it try. When you reach a paid module, you will be asked to pay for a single course or upgrade to PRO.
+
diff --git a/content/courses/deno/app-atomic.md b/content/courses/deno/app-atomic.md
new file mode 100644
index 000000000..9c8065595
--- /dev/null
+++ b/content/courses/deno/app-atomic.md
@@ -0,0 +1,97 @@
+---
+title: Atomic Writes
+description: ACID compliant transactions
+weight: 49
+lastmod: 2024-11-05T11:11:30-09:00
+draft: false
+vimeo: 1027526766
+emoji: π₯
+video_length: 3:25
+---
+
+
+
+## Query Records by Username
+
+
+{{< file "ts" "main.ts" >}}
+```typescript
+export async function storeShortLink(
+ longUrl: string,
+ shortCode: string,
+ userId: string,
+) {
+ const shortLinkKey = ["shortlinks", shortCode];
+ const data: ShortLink = {
+ shortCode,
+ longUrl,
+ userId,
+ createdAt: Date.now(),
+ clickCount: 0,
+ };
+
+ const userKey = [userId, shortCode];
+
+ const res = await kv.atomic()
+ .set(shortLinkKey, data)
+ .set(userKey, shortCode)
+ .commit()
+
+
+ return res;
+}
+
+export async function getUserLinks(userId: string) {
+
+ const list = kv.list({ prefix: [userId]});
+ const res = await Array.fromAsync(list);
+ const userShortLinkKeys = res.map((v) => ['shortlinks', v.value]);
+
+ const userRes = await kv.getMany(userShortLinkKeys)
+ const userShortLinks = await Array.fromAsync(userRes)
+
+ return userShortLinks.map(v => v.value);
+}
+```
+
+## Increment a Count
+
+{{< file "ts" "main.ts" >}}
+```typescript
+export async function incrementClickCount(
+ shortCode: string,
+ data?: Partial,
+) {
+ const shortLinkKey = ["shortlinks", shortCode];
+ const shortLink = await kv.get(shortLinkKey);
+ const shortLinkData = shortLink.value as ShortLink;
+
+ const newClickCount = shortLinkData?.clickCount + 1;
+
+ const analyicsKey = ["analytics", shortCode, newClickCount];
+ const analyticsData = {
+ shortCode,
+ createdAt: Date.now(),
+ ...data,
+ // ipAddress: "192.168.1.1",
+ // userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
+ // country: "United States"
+ };
+
+ const res = await kv.atomic()
+ .check(shortLink)
+ .set(shortLinkKey, {
+ ...shortLinkData,
+ clickCount: shortLinkData?.clickCount + 1,
+ })
+ .set(analyicsKey, analyticsData)
+ .commit();
+ if (res.ok) {
+ console.log("Logged click");
+ } else {
+ console.error("Not logged");
+ }
+
+ return res;
+}
+```
\ No newline at end of file
diff --git a/content/courses/deno/app-auth.md b/content/courses/deno/app-auth.md
new file mode 100644
index 000000000..be7b17774
--- /dev/null
+++ b/content/courses/deno/app-auth.md
@@ -0,0 +1,175 @@
+---
+title: User Auth
+description: Roll your own user authentication flow
+weight: 47
+lastmod: 2024-11-05T11:11:30-09:00
+draft: false
+vimeo: 1027526785
+emoji: π«
+video_length: 5:35
+---
+
+
+
+## Setup Environment Variables
+
+{{< file "cog" ".env" >}}
+```bash
+GITHUB_CLIENT_ID=abc
+GITHUB_CLIENT_SECRET=xyz
+REDIRECT_URI=http://localhost:8000/oauth/callback
+```
+
+Update the dev task to include the `--env` flag to load the env vars:
+
+{{< file "deno" "deno.json" >}}
+```json
+{
+ "tasks": {
+ "dev": "deno serve --watch --unstable-kv --env -A src/main.ts",
+ },
+}
+```
+
+## Authentication with User Profile
+
+
+Add database logic to store and get the user profile from the Deno KV database.
+
+{{< file "ts" "db.ts" >}}
+```typescript
+export type GitHubUser = {
+ login: string; // username
+ avatar_url: string;
+ html_url: string;
+};
+
+
+export async function storeUser(sessionId: string, userData: GitHubUser) {
+ const key = ["sessions", sessionId];
+ const res = await kv.set(key, userData);
+ return res;
+}
+
+export async function getUser(sessionId: string) {
+ const key = ["sessions", sessionId];
+ const res = await kv.get(key);
+ return res.value;
+}
+```
+
+Create a new file to handle authentication logic
+
+{{< file "ts" "auth.ts" >}}
+```typescript
+import { createGitHubOAuthConfig, createHelpers } from "jsr:@deno/kv-oauth";
+import { pick } from "jsr:@std/collections/pick";
+import { type GitHubUser, getUser, storeUser } from "./db.ts";
+
+const oauthConfig = createGitHubOAuthConfig();
+const {
+ handleCallback,
+ getSessionId,
+} = createHelpers(oauthConfig);
+
+
+export async function getCurrentUser(req: Request) {
+ const sessionId = await getSessionId(req);
+ console.log(sessionId)
+ return sessionId ? await getUser(sessionId) : null;
+}
+
+export async function getGitHubProfile(accessToken: string) {
+ const response = await fetch("https://api.github.com/user", {
+ headers: { authorization: `Bearer ${accessToken}` },
+ });
+
+ if (!response.ok) {
+ response.body?.cancel();
+ throw new Error("Failed to fetch GitHub user");
+ }
+
+ return response.json() as Promise;
+}
+
+export async function handleGithubCallback(req: Request) {
+ const { response, tokens, sessionId } = await handleCallback(req);
+ const userData = await getGitHubProfile(tokens?.accessToken);
+ const filteredData = pick(userData, ["avatar_url", "html_url", "login"]);
+ await storeUser(sessionId, filteredData);
+ return response;
+}
+
+```
+
+Add the current user as a property on the router
+
+{{< file "ts" "router.ts" >}}
+```typescript
+import type { GitHubUser } from "./db.ts";
+import { getCurrentUser } from "./auth.ts";
+
+export class Router {
+ #routes: Route[] = [];
+
+ currentUser?: GitHubUser | null; // <-- HERE
+
+ #addRoute(method: string, path: string, handler: Handler) {
+ const pattern = new URLPattern({ pathname: path });
+
+ this.#routes.push({
+ pattern,
+ method,
+ handler: async (req, info, params) => {
+ try {
+ this.currentUser = await getCurrentUser(req); // <-- HERE
+ return await handler(req, info!, params!);
+ } catch (error) {
+ console.error("Error handling request:", error);
+ return new Response("Internal Server Error", { status: 500 });
+ }
+ },
+ });
+ }
+
+ get handler() {
+ return route(this.#routes, () => new Response("Not Found", { status: 404 }))
+ }
+
+}
+```
+
+Configure the OAuth 2.0 routes
+
+{{< file "ts" "main.ts" >}}
+```typescript
+import { createGitHubOAuthConfig, createHelpers } from "jsr:@deno/kv-oauth";
+import { handleGithubCallback } from "./auth.ts";
+
+const app = new Router();
+
+const oauthConfig = createGitHubOAuthConfig({
+ redirectUri: Deno.env.get('REDIRECT_URI')
+});
+const {
+ signIn,
+ signOut,
+} = createHelpers(oauthConfig);
+
+
+app.get("/oauth/signin", (req: Request) => signIn(req));
+app.get("/oauth/signout", signOut);
+app.get("/oauth/callback", handleGithubCallback);
+
+
+app.get("/", () => {
+ return new Response(
+ render(HomePage({ user: app.currentUser })),
+ {
+ status: 200,
+ headers: {
+ "content-type": "text/html",
+ },
+ });
+});
+```
\ No newline at end of file
diff --git a/content/courses/deno/app-custom-router.md b/content/courses/deno/app-custom-router.md
new file mode 100644
index 000000000..dd7d07fea
--- /dev/null
+++ b/content/courses/deno/app-custom-router.md
@@ -0,0 +1,78 @@
+---
+title: Custom Router
+description: Build a custom express-like router
+weight: 43
+lastmod: 2024-11-05T11:11:30-09:00
+draft: false
+vimeo: 1027117748
+emoji: π
+video_length: 4:50
+---
+
+
+## Custom Deno Express-like Router
+
+{{< file "ts" "router.ts" >}}
+```typescript
+import { type Route, route, Handler } from "jsr:@std/http"
+
+export class Router {
+ #routes: Route[] = [];
+
+ get(path: string, handler: Handler) {
+ this.#addRoute("GET", path, handler);
+ }
+
+ post(path: string, handler: Handler) {
+ this.#addRoute("POST", path, handler);
+ }
+
+ put(path: string, handler: Handler) {
+ this.#addRoute("PUT", path, handler);
+ }
+
+ delete(path: string, handler: Handler) {
+ this.#addRoute("DELETE", path, handler);
+ }
+
+ #addRoute(method: string, path: string, handler: Handler) {
+ const pattern = new URLPattern({ pathname: path });
+ this.#routes.push({
+ pattern,
+ method,
+ handler: async (req, info, params) => {
+ try {
+ return await handler(req, info!, params!);
+ } catch (error) {
+ console.error("Error handling request:", error);
+ return new Response("Internal Server Error", { status: 500 });
+ }
+ },
+ });
+ }
+
+ get handler() {
+ return route(this.#routes, () => new Response("Not Found", { status: 404 }))
+ }
+
+}
+```
+
+
+## Using the Custom Router
+
+{{< file "ts" "main.ts" >}}
+```typescript
+import { Router } from "./router.ts";
+const app = new Router();
+
+app.get('/', () => new Response('Hi Mom!'))
+
+app.post('/health-check', () => new Response("It's ALIVE!"))
+
+export default {
+ fetch(req) {
+ return app.handler(req);
+ },
+} satisfies Deno.ServeDefaultExport;
+```
diff --git a/content/courses/deno/app-deno-kv.md b/content/courses/deno/app-deno-kv.md
new file mode 100644
index 000000000..f38aae8fd
--- /dev/null
+++ b/content/courses/deno/app-deno-kv.md
@@ -0,0 +1,116 @@
+---
+title: Deno KV
+description: Using Deno's built-in database
+weight: 45
+lastmod: 2024-11-05T11:11:30-09:00
+draft: false
+vimeo: 1027305799
+emoji: ποΈ
+video_length: 3:38
+---
+
+## Generate a Shortcode
+
+{{< file "ts" "db.ts" >}}
+```typescript
+const kv = await Deno.openKv();
+
+export type ShortLink = {
+ shortCode: string;
+ longUrl: string;
+ createdAt: number;
+ userId: string;
+ clickCount: number;
+ lastClickEvent?: string;
+}
+
+export async function storeShortLink(
+ longUrl: string,
+ shortCode: string,
+ userId: string,
+) {
+ const shortLinkKey = ["shortlinks", shortCode];
+ const data: ShortLink = {
+ shortCode,
+ longUrl,
+ userId,
+ createdAt: Date.now(),
+ clickCount: 0,
+ };
+
+ const res = await kv.set(shortLinkKey, data);
+
+ if (!res.ok) {
+ // handle errors
+ }
+
+ return res;
+
+
+}
+
+export async function getShortLink(shortCode: string) {
+ const link = await kv.get(["shortlinks", shortCode]);
+ return link.value;
+}
+
+
+// Temporary example to try it out
+// deno run -A --unstable-kv src/db.ts
+const longUrl = 'https://fireship.io';
+const shortCode = await generateShortCode(longUrl)
+const userId = 'test';
+
+console.log(shortCode)
+
+
+await storeShortLink(longUrl, shortCode, userId);
+
+const linkData = await getShortLink(shortCode)
+console.log(linkData)
+```
+
+
+## Updated Deno Task
+
+{{< file "deno" "deno.json" >}}
+```json
+{
+ "tasks": {
+ "dev": "deno serve --watch -A --unstable-kv src/main.ts",
+ }
+}
+```
+
+## Example usage in JSON API
+
+```typescript
+app.post("/links", async (req) => {
+
+ const { longUrl } = await req.json()
+
+ const shortCode = await generateShortCode(longUrl);
+ await storeShortLink(longUrl, shortCode, 'testUser');
+
+ return new Response("success!", {
+ status: 201,
+ });
+});
+
+
+app.get("/links/:id", async (_req, _info, params) => {
+
+ const shortCode = params?.pathname.groups.id;
+
+ const data = await getShortLink(shortCode!)
+
+ return new Response(JSON.stringify(data), {
+ status: 201,
+ headers: {
+ "content-type": "application/json",
+ },
+
+ });
+
+})
+```
diff --git a/content/courses/deno/app-deploy.md b/content/courses/deno/app-deploy.md
new file mode 100644
index 000000000..39f1078f7
--- /dev/null
+++ b/content/courses/deno/app-deploy.md
@@ -0,0 +1,34 @@
+---
+title: Ship it
+description: How to deploy a Deno app
+weight: 52
+lastmod: 2024-11-05T11:11:30-09:00
+draft: false
+vimeo:
+emoji: π
+video_length: 3:38
+---
+
+
+{{< file "terminal" "command line" >}}
+```bash
+deno install -A jsr:@deno/deployctl --global
+
+deployctl deploy
+```
+
+{{< file "deno" "deno.json" >}}
+```json
+{
+ "tasks": {
+ "dev": "deno serve --watch --unstable-kv --env -A src/main.ts",
+ "test": "deno test tests/",
+ "deploy": "deployctl deploy --project YOUR_PROJECT"
+ },
+
+ "deploy": {
+ "entrypoint": "src/main.ts"
+ }
+
+}
+```
\ No newline at end of file
diff --git a/content/courses/deno/app-form.md b/content/courses/deno/app-form.md
new file mode 100644
index 000000000..03b92bb66
--- /dev/null
+++ b/content/courses/deno/app-form.md
@@ -0,0 +1,91 @@
+---
+title: Form Submission
+description: Submit HTML forms to the backend
+weight: 48
+lastmod: 2024-11-05T11:11:30-09:00
+draft: false
+vimeo: 1027526776
+emoji: π
+video_length: 2:36
+---
+
+
+{{< file "ts" "main.ts" >}}
+```typescript
+app.get("/links/new", (_req) => {
+ if (!app.currentUser) return unauthorizedResponse();
+
+ return new Response(render(CreateShortlinkPage()), {
+ status: 200,
+ headers: {
+ "content-type": "text/html",
+ },
+ });
+});
+
+
+app.post("/links", async (req) => {
+ if (!app.currentUser) return unauthorizedResponse();
+
+ // Parse form data
+ const formData = await req.formData();
+ const longUrl = formData.get("longUrl") as string;
+
+ if (!longUrl) {
+ return new Response("Missing longUrl", { status: 400 });
+ }
+
+ const shortCode = await generateShortCode(longUrl);
+ await storeShortLink(longUrl, shortCode, app.currentUser.login);
+
+ // Redirect to the links list page after successful creation
+ return new Response(null, {
+ status: 303,
+ headers: {
+ "Location": "/links",
+ },
+ });
+
+ app.get("/links", async () => {
+ if (!app.currentUser) return unauthorizedResponse();
+
+ const shortLinks = await getUserLinks(app.currentUser.login);
+
+ return new Response(render(LinksPage({ shortLinkList: shortLinks })), {
+ status: 200,
+ headers: {
+ "content-type": "text/html",
+ },
+ });
+});
+```
+
+## Form UI
+
+{{< file "react" "ui.tsx" >}}
+```tsx
+export function CreateShortlinkPage() {
+ return (
+
+
Create a New Shortlink
+
+
+ );
+}
+```
+
diff --git a/content/courses/deno/app-hash.md b/content/courses/deno/app-hash.md
new file mode 100644
index 000000000..591967389
--- /dev/null
+++ b/content/courses/deno/app-hash.md
@@ -0,0 +1,84 @@
+---
+title: Deno Crypto
+description: Create a cryptographic shortcode
+weight: 44
+lastmod: 2024-11-05T11:11:30-09:00
+draft: false
+vimeo: 1027305830
+emoji: π
+video_length: 2:57
+---
+
+
+## Generate a Shortcode
+
+{{< file "ts" "db.ts" >}}
+```typescript
+
+import { encodeBase64Url, encodeHex } from "jsr:@std/encoding";
+import { crypto } from "jsr:@std/crypto/crypto";
+
+export async function generateShortCode(longUrl: string) {
+
+ try {
+ new URL(longUrl);
+ } catch (error) {
+ console.log(error);
+ throw new Error("Invalid URL provided");
+ }
+
+ // Generate a unique identifier for the URL
+ const urlData = new TextEncoder().encode(longUrl + Date.now());
+ const hash = await crypto.subtle.digest("SHA-256", urlData);
+ const hashArray = new Uint8Array(hash);
+ const hashHex = encodeHex(hashArray);
+
+ // Take the first 8 characters of the hash for the short URL
+ const shortCode = encodeBase64Url(hashHex.slice(0, 8));
+
+ return shortCode;
+}
+```
+
+
+## Optional: Testing the ShortCode Function
+
+{{< file "ts" "db.ts" >}}
+```typescript
+import { assertEquals, assertNotEquals, assertRejects } from "@std/assert";
+import { delay } from "jsr:@std/async/delay";
+import { generateShortCode } from "../src/db.ts";
+
+Deno.test("URL Shortener ", async (t) => {
+ await t.step("should generate a short code for a valid URL", async () => {
+ const longUrl = "https://www.example.com/some/long/path";
+ const shortCode = await generateShortCode(longUrl);
+
+ assertEquals(typeof shortCode, "string");
+ assertEquals(shortCode.length, 11);
+ });
+
+ await t.step("should be unique for each timestamp", async () => {
+ const longUrl = "https://www.example.com";
+ const a = await generateShortCode(longUrl);
+ await delay(5)
+ const b = await generateShortCode(longUrl);
+
+ assertNotEquals(a, b)
+ });
+
+ await t.step("throw error on bad URL", () => {
+ const longUrl = "this aint no url";
+
+ assertRejects(async () => {
+ await generateShortCode(longUrl);
+ })
+ });
+});
+```
+
+## Bonus Video
+
+
+{{< youtube NuyzuNBFWxQ >}}
+
\ No newline at end of file
diff --git a/content/courses/deno/app-init-structure.md b/content/courses/deno/app-init-structure.md
new file mode 100644
index 000000000..daeb7652a
--- /dev/null
+++ b/content/courses/deno/app-init-structure.md
@@ -0,0 +1,22 @@
+---
+title: Project Structure
+description: How to structure a deno project
+weight: 41
+lastmod: 2024-11-05T11:11:30-09:00
+draft: false
+vimeo: 1027117775
+emoji: π§±
+video_length: 2:59
+---
+
+## Updated Deno Config
+
+{{< file "deno" "deno.json" >}}
+```json
+{
+ "tasks": {
+ "dev": "deno serve --watch -A src/main.ts",
+ "test": "deno test tests/",
+ }
+}
+```
\ No newline at end of file
diff --git a/content/courses/deno/app-jsx.md b/content/courses/deno/app-jsx.md
new file mode 100644
index 000000000..c4cc46bf1
--- /dev/null
+++ b/content/courses/deno/app-jsx.md
@@ -0,0 +1,114 @@
+---
+title: Built-in JSX
+description: Write and render HTML on the server
+weight: 46
+lastmod: 2024-11-05T11:11:30-09:00
+draft: false
+vimeo: 1027305746
+emoji: π½
+video_length: 4:57
+---
+
+
+## Deno Config Update
+
+{{< file "deno" "deno.json" >}}
+```json
+{
+ //...
+ "compilerOptions": {
+ "jsx": "react-jsx",
+ "jsxImportSource": "npm:preact"
+ }
+}
+
+```
+
+## Home Page Route
+
+{{< file "ts" "main.ts" >}}
+```typescript
+import { HomePage } from "./ui.tsx";
+import { render } from "npm:preact-render-to-string";
+
+app.get("/", () => {
+ return new Response(
+ render(HomePage({user: null })),
+ {
+ status: 200,
+ headers: {
+ "content-type": "text/html",
+ },
+ });
+});
+```
+
+## Home Page UI with JSX
+
+Find all the Tailwind CSS styles in the [full source code]()
+
+{{< file "react" "ui.tsx" >}}
+```tsx
+
+/** @jsxImportSource https://esm.sh/preact */
+
+import type { ComponentChildren } from "npm:preact";
+
+export function Layout({ children }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+ );
+}
+
+export function HomePage({ user }) {
+ return (
+
+