in
- * cf/TypeScriptExample, which recovers the raw source from the rendered
- * block (its slot-based contract) rather than a `code` prop.
- */
-import { z } from "astro/zod";
-import { Code } from "../ui/code";
-import TypeScriptExample from "./TypeScriptExample.astro";
-
-// `lang` is authored freeform in MDX (z.string()); Code's `lang` prop is
-// typed against shiki's narrower CodeLanguage union. Cast at the render
-// site — shiki already falls back gracefully for unrecognized values.
-type CodeLang = Parameters[0]["lang"];
-
-type Props = z.infer;
-
-const props = z
- .object({
- repo: z.string(),
- commit: z.string(),
- file: z.string(),
- lang: z.string(),
- lines: z
- .string()
- .transform((val) => val.split("-").map(Number))
- .optional(),
- tag: z.string().optional(),
- useTypeScriptExample: z.boolean().default(false),
- code: z.custom>().optional(),
- })
- .strict()
- .refine((val) => !(val.lines && val.tag), {
- message: "Lines and tag are mutually exclusive filters.",
- })
- .refine((val) => !val.useTypeScriptExample || val.lang === "ts", {
- message: "useTypeScriptExample can only be used when 'lang' is set to 'ts'",
- });
-
-const { repo, commit, file, lang, lines, tag, useTypeScriptExample, code } =
- props.parse(Astro.props);
-
-const res = await fetch(
- `https://gh-code.developers.cloudflare.com/${repo}/${commit}/${file}`,
-);
-
-if (!res.ok) {
- throw new Error(`[GitHubCode] Received ${res.status} from Worker.`);
-}
-
-const content = await res.text();
-let contentLines = content.split("\n");
-
-if (lines) {
- const [start, end] = lines;
-
- if (contentLines.length < end - 1) {
- throw new Error(
- `[GitHubCode] End line requested is beyond content length (${contentLines.length}).`,
- );
- }
-
- contentLines = contentLines.slice(start - 1, end);
-} else if (tag) {
- const startTag = contentLines.findIndex((x) =>
- x.includes(``),
- );
- const endTag = contentLines.findIndex((x) =>
- x.includes(` `),
- );
-
- if (startTag === -1 || endTag === -1) {
- throw new Error(`[GitHubCode] Unable to find a region using tag "${tag}".`);
- }
-
- contentLines = contentLines.slice(startTag + 1, endTag);
-}
-
-contentLines = contentLines.filter(
- (line) => !/<[/]?docs-tag name=".*">/.test(line),
-);
-
-const finalCode = contentLines.join("\n");
-
-const { title, ...restCode } = (code ?? {}) as Record;
-const meta = title ? `title="${title}"` : undefined;
----
-
-{
- useTypeScriptExample ? (
-
-
-
- ) : (
-
- )
-}
diff --git a/src/components/cf/WorkersTemplates.astro b/src/components/cf/WorkersTemplates.astro
deleted file mode 100644
index da15d515159..00000000000
--- a/src/components/cf/WorkersTemplates.astro
+++ /dev/null
@@ -1,72 +0,0 @@
----
-import { AnchorHeading, PackageManagers } from "~/components";
-import { fetchWithToken } from "~/util/github";
-
-const REPO = "cloudflare/templates";
-
-const latestCommit = await fetchWithToken(
- `https://api.github.com/repos/${REPO}/commits?sha=main&per_page=1`,
-)
- .then((r) => r.json())
- .then((r) => r[0].sha);
-
-const contents = await fetchWithToken(
- `https://api.github.com/repos/${REPO}/contents/?ref=${latestCommit}`,
-).then((r) => r.json());
-
-const dirs = contents.filter((ent: any) => ent.type === "dir");
----
-
-{
- dirs
- .filter((dir: any) => dir.name !== ".github")
- .map(async (dir: any) => {
- const packageJson = await fetch(
- `https://gh-code.developers.cloudflare.com/${REPO}/${latestCommit}/${dir.path}/package.json`,
- )
- .then((r) => r.json())
- .catch((reason) => {
- console.warn(
- `[WorkersTemplates] Failed to parse JSON for ${dir.path}`,
- reason,
- );
- });
-
- if (!packageJson) return;
-
- return (
- <>
-
-
-
-
- >
- );
- })
-}
diff --git a/src/content/docs/agents/concepts/agentic-patterns/index.mdx b/src/content/docs/agents/concepts/agentic-patterns/index.mdx
index 61701e33f5b..8545f41a04c 100644
--- a/src/content/docs/agents/concepts/agentic-patterns/index.mdx
+++ b/src/content/docs/agents/concepts/agentic-patterns/index.mdx
@@ -9,8 +9,6 @@ products:
- agents
---
-import { GitHubCode } from "~/components";
-
This page lists and defines common patterns for implementing AI agents, based on [Anthropic's patterns for building effective agents](https://www.anthropic.com/research/building-effective-agents).
Code samples use the [AI SDK](https://ai-sdk.dev/docs/foundations/agents), running in [Durable Objects](/durable-objects).
@@ -21,12 +19,57 @@ Decomposes tasks into a sequence of steps, where each LLM call processes the out

-
+```ts
+import { openai } from "@ai-sdk/openai";
+import { generateText, generateObject } from "ai";
+import { z } from "zod";
+
+export default async function generateMarketingCopy(input: string) {
+ const model = openai("gpt-4o");
+
+ // First step: Generate marketing copy
+ const { text: copy } = await generateText({
+ model,
+ prompt: `Write persuasive marketing copy for: ${input}. Focus on benefits and emotional appeal.`,
+ });
+
+ // Perform quality check on copy
+ const { object: qualityMetrics } = await generateObject({
+ model,
+ schema: z.object({
+ hasCallToAction: z.boolean(),
+ emotionalAppeal: z.number().min(1).max(10),
+ clarity: z.number().min(1).max(10),
+ }),
+ prompt: `Evaluate this marketing copy for:
+ 1. Presence of call to action (true/false)
+ 2. Emotional appeal (1-10)
+ 3. Clarity (1-10)
+
+ Copy to evaluate: ${copy}`,
+ });
+
+ // If quality check fails, regenerate with more specific instructions
+ if (
+ !qualityMetrics.hasCallToAction ||
+ qualityMetrics.emotionalAppeal < 7 ||
+ qualityMetrics.clarity < 7
+ ) {
+ const { text: improvedCopy } = await generateText({
+ model,
+ prompt: `Rewrite this marketing copy with:
+ ${!qualityMetrics.hasCallToAction ? "- A clear call to action" : ""}
+ ${qualityMetrics.emotionalAppeal < 7 ? "- Stronger emotional appeal" : ""}
+ ${qualityMetrics.clarity < 7 ? "- Improved clarity and directness" : ""}
+
+ Original copy: ${copy}`,
+ });
+ return { copy: improvedCopy, qualityMetrics };
+ }
+
+ return { copy, qualityMetrics };
+}
+```
## Routing
@@ -34,12 +77,52 @@ Classifies input and directs it to specialized followup tasks, allowing for sepa

-
+```ts
+import { openai } from '@ai-sdk/openai';
+import { generateObject, generateText } from 'ai';
+import { z } from 'zod';
+
+async function handleCustomerQuery(query: string) {
+ const model = openai('gpt-4o');
+
+ // First step: Classify the query type
+ const { object: classification } = await generateObject({
+ model,
+ schema: z.object({
+ reasoning: z.string(),
+ type: z.enum(['general', 'refund', 'technical']),
+ complexity: z.enum(['simple', 'complex']),
+ }),
+ prompt: `Classify this customer query:
+ ${query}
+
+ Determine:
+ 1. Query type (general, refund, or technical)
+ 2. Complexity (simple or complex)
+ 3. Brief reasoning for classification`,
+ });
+
+ // Route based on classification
+ // Set model and system prompt based on query type and complexity
+ const { text: response } = await generateText({
+ model:
+ classification.complexity === 'simple'
+ ? openai('gpt-4o-mini')
+ : openai('o1-mini'),
+ system: {
+ general:
+ 'You are an expert customer service agent handling general inquiries.',
+ refund:
+ 'You are a customer service agent specializing in refund requests. Follow company policy and collect necessary information.',
+ technical:
+ 'You are a technical support specialist with deep product knowledge. Focus on clear step-by-step troubleshooting.',
+ }[classification.type],
+ prompt: query,
+ });
+
+ return { response, classification };
+}
+```
## Parallelization
@@ -47,12 +130,75 @@ Enables simultaneous task processing through sectioning or voting mechanisms.

-
+```ts
+import { openai } from '@ai-sdk/openai';
+import { generateText, generateObject } from 'ai';
+import { z } from 'zod';
+
+// Example: Parallel code review with multiple specialized reviewers
+async function parallelCodeReview(code: string) {
+ const model = openai('gpt-4o');
+
+ // Run parallel reviews
+ const [securityReview, performanceReview, maintainabilityReview] =
+ await Promise.all([
+ generateObject({
+ model,
+ system:
+ 'You are an expert in code security. Focus on identifying security vulnerabilities, injection risks, and authentication issues.',
+ schema: z.object({
+ vulnerabilities: z.array(z.string()),
+ riskLevel: z.enum(['low', 'medium', 'high']),
+ suggestions: z.array(z.string()),
+ }),
+ prompt: `Review this code:
+ ${code}`,
+ }),
+
+ generateObject({
+ model,
+ system:
+ 'You are an expert in code performance. Focus on identifying performance bottlenecks, memory leaks, and optimization opportunities.',
+ schema: z.object({
+ issues: z.array(z.string()),
+ impact: z.enum(['low', 'medium', 'high']),
+ optimizations: z.array(z.string()),
+ }),
+ prompt: `Review this code:
+ ${code}`,
+ }),
+
+ generateObject({
+ model,
+ system:
+ 'You are an expert in code quality. Focus on code structure, readability, and adherence to best practices.',
+ schema: z.object({
+ concerns: z.array(z.string()),
+ qualityScore: z.number().min(1).max(10),
+ recommendations: z.array(z.string()),
+ }),
+ prompt: `Review this code:
+ ${code}`,
+ }),
+ ]);
+
+ const reviews = [
+ { ...securityReview.object, type: 'security' },
+ { ...performanceReview.object, type: 'performance' },
+ { ...maintainabilityReview.object, type: 'maintainability' },
+ ];
+
+ // Aggregate results using another model instance
+ const { text: summary } = await generateText({
+ model,
+ system: 'You are a technical lead summarizing multiple code reviews.',
+ prompt: `Synthesize these code review results into a concise summary with key actions:
+ ${JSON.stringify(reviews, null, 2)}`,
+ });
+
+ return { reviews, summary };
+}
+```
## Orchestrator-Workers
@@ -60,12 +206,71 @@ A central LLM dynamically breaks down tasks, delegates to Worker LLMs, and synth

-
+```ts
+import { openai } from '@ai-sdk/openai';
+import { generateObject } from 'ai';
+import { z } from 'zod';
+
+async function implementFeature(featureRequest: string) {
+ // Orchestrator: Plan the implementation
+ const { object: implementationPlan } = await generateObject({
+ model: openai('o1'),
+ schema: z.object({
+ files: z.array(
+ z.object({
+ purpose: z.string(),
+ filePath: z.string(),
+ changeType: z.enum(['create', 'modify', 'delete']),
+ }),
+ ),
+ estimatedComplexity: z.enum(['low', 'medium', 'high']),
+ }),
+ system:
+ 'You are a senior software architect planning feature implementations.',
+ prompt: `Analyze this feature request and create an implementation plan:
+ ${featureRequest}`,
+ });
+
+ // Workers: Execute the planned changes
+ const fileChanges = await Promise.all(
+ implementationPlan.files.map(async file => {
+ // Each worker is specialized for the type of change
+ const workerSystemPrompt = {
+ create:
+ 'You are an expert at implementing new files following best practices and project patterns.',
+ modify:
+ 'You are an expert at modifying existing code while maintaining consistency and avoiding regressions.',
+ delete:
+ 'You are an expert at safely removing code while ensuring no breaking changes.',
+ }[file.changeType];
+
+ const { object: change } = await generateObject({
+ model: openai('gpt-4o'),
+ schema: z.object({
+ explanation: z.string(),
+ code: z.string(),
+ }),
+ system: workerSystemPrompt,
+ prompt: `Implement the changes for ${file.filePath} to support:
+ ${file.purpose}
+
+ Consider the overall feature context:
+ ${featureRequest}`,
+ });
+
+ return {
+ file,
+ implementation: change,
+ };
+ }),
+ );
+
+ return {
+ plan: implementationPlan,
+ changes: fileChanges,
+ };
+}
+```
## Evaluator-Optimizer
@@ -73,9 +278,81 @@ One LLM generates responses while another provides evaluation and feedback in a

-
+```ts
+import { openai } from '@ai-sdk/openai';
+import { generateText, generateObject } from 'ai';
+import { z } from 'zod';
+
+async function translateWithFeedback(text: string, targetLanguage: string) {
+ let currentTranslation = '';
+ let iterations = 0;
+ const MAX_ITERATIONS = 3;
+
+ // Initial translation
+ const { text: translation } = await generateText({
+ model: openai('gpt-4o-mini'), // use small model for first attempt
+ system: 'You are an expert literary translator.',
+ prompt: `Translate this text to ${targetLanguage}, preserving tone and cultural nuances:
+ ${text}`,
+ });
+
+ currentTranslation = translation;
+
+ // Evaluation-optimization loop
+ while (iterations < MAX_ITERATIONS) {
+ // Evaluate current translation
+ const { object: evaluation } = await generateObject({
+ model: openai('gpt-4o'), // use a larger model to evaluate
+ schema: z.object({
+ qualityScore: z.number().min(1).max(10),
+ preservesTone: z.boolean(),
+ preservesNuance: z.boolean(),
+ culturallyAccurate: z.boolean(),
+ specificIssues: z.array(z.string()),
+ improvementSuggestions: z.array(z.string()),
+ }),
+ system: 'You are an expert in evaluating literary translations.',
+ prompt: `Evaluate this translation:
+
+ Original: ${text}
+ Translation: ${currentTranslation}
+
+ Consider:
+ 1. Overall quality
+ 2. Preservation of tone
+ 3. Preservation of nuance
+ 4. Cultural accuracy`,
+ });
+
+ // Check if quality meets threshold
+ if (
+ evaluation.qualityScore >= 8 &&
+ evaluation.preservesTone &&
+ evaluation.preservesNuance &&
+ evaluation.culturallyAccurate
+ ) {
+ break;
+ }
+
+ // Generate improved translation based on feedback
+ const { text: improvedTranslation } = await generateText({
+ model: openai('gpt-4o'), // use a larger model
+ system: 'You are an expert literary translator.',
+ prompt: `Improve this translation based on the following feedback:
+ ${evaluation.specificIssues.join('\n')}
+ ${evaluation.improvementSuggestions.join('\n')}
+
+ Original: ${text}
+ Current Translation: ${currentTranslation}`,
+ });
+
+ currentTranslation = improvedTranslation;
+ iterations++;
+ }
+
+ return {
+ finalTranslation: currentTranslation,
+ iterationsRequired: iterations,
+ };
+}
+```
diff --git a/src/content/docs/d1/best-practices/read-replication.mdx b/src/content/docs/d1/best-practices/read-replication.mdx
index daa2d866001..dcbb1af7675 100644
--- a/src/content/docs/d1/best-practices/read-replication.mdx
+++ b/src/content/docs/d1/best-practices/read-replication.mdx
@@ -9,7 +9,7 @@ products:
- d1
---
-import { GlossaryTooltip, Details, GitHubCode, APIRequest, Tabs, TabItem, TypeScriptExample, DashButton } from "~/components"
+import { GlossaryTooltip, Details, APIRequest, Tabs, TabItem, TypeScriptExample, DashButton } from "~/components"
D1 read replication can lower latency for read queries and scale read throughput by adding read-only database copies, called read replicas, across regions globally closer to clients.
@@ -25,14 +25,50 @@ To checkout D1 read replication, deploy the following Worker code using Sessions
To simulate how read replication can improve a worst case latency scenario, set your D1 database location hint to be in a farther away region. For example, if you are in Europe create your database in Western North America (WNAM).
:::
-
+
+
+```ts
+export default {
+ async fetch(request, env, ctx): Promise {
+ const url = new URL(request.url);
+
+ // A. Create the Session.
+ // When we create a D1 Session, we can continue where we left off from a previous
+ // Session if we have that Session's last bookmark or use a constraint.
+ const bookmark =
+ request.headers.get("x-d1-bookmark") ?? "first-unconstrained";
+ const session = env.DB01.withSession(bookmark);
+
+ try {
+ // Use this Session for all our Workers' routes.
+ const response = await withTablesInitialized(
+ request,
+ session,
+ handleRequest,
+ );
+
+ // B. Return the bookmark so we can continue the Session in another request.
+ response.headers.set("x-d1-bookmark", session.getBookmark() ?? "");
+
+ return response;
+ } catch (e) {
+ console.error({
+ message: "Failed to handle request",
+ error: String(e),
+ errorProps: e,
+ url,
+ bookmark,
+ });
+ return Response.json(
+ { error: String(e), errorDetails: e },
+ { status: 500 },
+ );
+ }
+ },
+} satisfies ExportedHandler;
+```
+
+
## Primary database instance vs read replicas
diff --git a/src/content/docs/d1/best-practices/retry-queries.mdx b/src/content/docs/d1/best-practices/retry-queries.mdx
index 02bc218ed30..12b186d11af 100644
--- a/src/content/docs/d1/best-practices/retry-queries.mdx
+++ b/src/content/docs/d1/best-practices/retry-queries.mdx
@@ -8,8 +8,6 @@ products:
- d1
---
-import { GitHubCode } from "~/components";
-
It is useful to retry write queries from your application when you encounter a transient [error](/d1/observability/debug-d1/#error-list). From the list of `D1_ERROR`s, refer to the Recommended action column to determine if a query should be retried.
:::note
diff --git a/src/content/docs/d1/tutorials/d1-and-prisma-orm.mdx b/src/content/docs/d1/tutorials/d1-and-prisma-orm.mdx
index 64bb7a1593d..8c9494a3c01 100644
--- a/src/content/docs/d1/tutorials/d1-and-prisma-orm.mdx
+++ b/src/content/docs/d1/tutorials/d1-and-prisma-orm.mdx
@@ -16,7 +16,7 @@ import {
WranglerConfig,
FileTree,
PackageManagers,
- GitHubCode,
+ TypeScriptExample,
Tabs,
TabItem,
} from "~/components";
@@ -201,16 +201,13 @@ Next, you need to add the SQL statement that will create a `User` table to that
Open the `schema.prisma` file and add the following `User` model to your schema:
-
+```prisma title="schema.prisma"
+model User {
+ id Int @id @default(autoincrement())
+ email String @unique
+ name String?
+}
+```
Now, run the following command in your terminal to generate the SQL statement that creates a `User` table equivalent to the `User` model above:
@@ -233,16 +230,17 @@ npx prisma migrate diff --from-empty --to-schema-datamodel ./prisma/schema.prism
This stores a SQL statement to create a new `User` table in your migration file from before, here is what it looks like:
-
+```sql title="0001_create_user_table.sql"
+-- CreateTable
+CREATE TABLE "User" (
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ "email" TEXT NOT NULL,
+ "name" TEXT
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
+```
`UNIQUE INDEX` on `email` was created because the `User` model in your Prisma schema is using the [`@unique`](https://www.prisma.io/docs/orm/reference/prisma-schema-reference#unique) attribute on its `email` field.
@@ -304,16 +302,29 @@ To query your database from the Worker using Prisma ORM, you need to:
Open `src/index.ts` and replace the entire content with the following:
-
+
+
+```ts
+import { PrismaClient } from './generated/prisma/';
+import { PrismaD1 } from '@prisma/adapter-d1';
+
+export interface Env {
+ DB: D1Database;
+}
+
+export default {
+ async fetch(request, env, ctx): Promise {
+ const adapter = new PrismaD1(env.DB);
+ const prisma = new PrismaClient({ adapter });
+
+ const users = await prisma.user.findMany();
+ const result = JSON.stringify(users);
+ return new Response(result);
+ },
+} satisfies ExportedHandler;
+```
+
+
Before running the Worker, generate Prisma Client with the following command:
diff --git a/src/content/docs/kv/get-started.mdx b/src/content/docs/kv/get-started.mdx
index de961789320..1daef895dbb 100644
--- a/src/content/docs/kv/get-started.mdx
+++ b/src/content/docs/kv/get-started.mdx
@@ -11,7 +11,7 @@ products:
- kv
---
-import { Render, PackageManagers, Steps, FileTree, Details, Tabs, TabItem, WranglerConfig, GitHubCode, DashButton } from "~/components";
+import { Render, PackageManagers, Steps, FileTree, Details, Tabs, TabItem, WranglerConfig, TypeScriptExample, DashButton } from "~/components";
Workers KV provides low-latency, high-throughput global storage to your [Cloudflare Workers](/workers/) applications. Workers KV is ideal for storing user configuration data, routing data, A/B testing configurations and authentication tokens, and is well suited for read-heavy workloads.
@@ -375,14 +375,38 @@ Also refer to [KV binding docs](/kv/concepts/kv-bindings/#use-kv-bindings-when-d
Your Worker code should look like this:
-
+
+
+```ts
+export interface Env {
+ USERS_NOTIFICATION_CONFIG: KVNamespace;
+}
+
+export default {
+ async fetch(request, env, ctx): Promise {
+ try {
+ await env.USERS_NOTIFICATION_CONFIG.put("user_2", "disabled");
+ const value = await env.USERS_NOTIFICATION_CONFIG.get("user_2");
+ if (value === null) {
+ return new Response("Value not found", { status: 404 });
+ }
+ return new Response(value);
+ } catch (err) {
+ console.error(`KV returned error:`, err);
+ const errorMessage =
+ err instanceof Error
+ ? err.message
+ : "An unknown error occurred when accessing KV storage";
+ return new Response(errorMessage, {
+ status: 500,
+ headers: { "Content-Type": "text/plain" },
+ });
+ }
+ },
+} satisfies ExportedHandler;
+```
+
+
The code above:
@@ -402,14 +426,38 @@ The code above:
3. Select **Edit Code**.
4. Clear the contents of the `workers.js` file, then paste the following code.
-
+
+
+ ```ts
+ export interface Env {
+ USERS_NOTIFICATION_CONFIG: KVNamespace;
+ }
+
+ export default {
+ async fetch(request, env, ctx): Promise {
+ try {
+ await env.USERS_NOTIFICATION_CONFIG.put("user_2", "disabled");
+ const value = await env.USERS_NOTIFICATION_CONFIG.get("user_2");
+ if (value === null) {
+ return new Response("Value not found", { status: 404 });
+ }
+ return new Response(value);
+ } catch (err) {
+ console.error(`KV returned error:`, err);
+ const errorMessage =
+ err instanceof Error
+ ? err.message
+ : "An unknown error occurred when accessing KV storage";
+ return new Response(errorMessage, {
+ status: 500,
+ headers: { "Content-Type": "text/plain" },
+ });
+ }
+ },
+ } satisfies ExportedHandler;
+ ```
+
+
The code above:
diff --git a/src/content/docs/style-guide/components/github-code.mdx b/src/content/docs/style-guide/components/github-code.mdx
deleted file mode 100644
index a7af6ff49ee..00000000000
--- a/src/content/docs/style-guide/components/github-code.mdx
+++ /dev/null
@@ -1,159 +0,0 @@
----
-title: GitHubCode
-styleGuide:
- component: GitHubCode
-description: Fetch and display code from a GitHub repository.
-products:
- - style-guide
----
-
-import { GitHubCode } from "~/components";
-
-The `GitHubCode` component allows you to include files from Cloudflare repositories.
-
-The remote content can be filtered by lines or a region enclosed in tags.
-
-## Import
-
-```mdx
-import { GitHubCode } from "~/components";
-```
-
-## Usage
-
-```mdx
-import { GitHubCode } from "~/components";
-
-
-```
-
-### Filtering by lines
-
-```mdx
-import { GitHubCode } from "~/components";
-
-{/*
-import { foo } from "bar";
-
-const baz = foo();
-
-console.log(baz);
-*/}
-
-{/*
-import { foo } from "bar";
-
-const baz = foo();
-*/}
-```
-
-### Filtering by tag
-
-```mdx
-import { GitHubCode } from "~/components";
-
-{/*
-
-import { foo } from "bar";
-
-const baz = foo();
-
-
-console.log(baz);
-*/}
-
-{/*
-import { foo } from "bar";
-
-const baz = foo();
-*/}
-```
-
-## `` Props
-
-### `repo`
-
-**required**
-**type:** `string`
-
-The owner and repository to pull from, in the form of `cloudflare/`
-
-For example:
-
-- `cloudflare/workers-rs`.
-- `cloudflare/templates`.
-
-### `file`
-
-**required**
-**type:** `string`
-
-The file path to pull from, in the form of `path/to/filename-including-extensions`. This path excludes the repo name.
-
-For example:
-
-- `templates/hello-world/src/lib.rs`.
-- `d1-starter-sessions-api/src/index.ts`.
-
-### `commit`
-
-**required**
-**type:** `string`
-
-The long (40-characters) Git commit hash to pull from, for example `ab3951b5c95329a600a7baa9f9bb1a7a95f1aeaa`.
-
-### `lang`
-
-**required**
-**type:** `string`
-
-The language to use for the code block, for example `rs`.
-
-### `useTypeScriptExample`
-
-**type:** `boolean`
-
-
-If the `lang` is `"ts"` and `useTypeScriptExample` is `true`, the [`TypeScriptExample`](/style-guide/components/typescript-example/) component will be used to provide a JavaScript tab as well.
-
-### `lines`
-
-**type:** `string`
-
-A range of lines to filter the content using, for example `1-3`.
-
-### `tag`
-
-**type:** `string`
-
-A region to filter the content with, for example `no-logging`.
-
-This should be represented as starting `` and closing ` ` comments in the source file.
-
-### `code`
-
-**type**: `object`
-
-Props to pass to the [Astro `Code` component](https://docs.astro.build/en/reference/api-reference/#code-).
-
-## Associated content types
-
-- [Tutorial](/style-guide/documentation-content-strategy/content-types/tutorial/)
\ No newline at end of file
diff --git a/src/content/docs/style-guide/components/typescript-example.mdx b/src/content/docs/style-guide/components/typescript-example.mdx
index b7e6aa093d2..cbfa82137fb 100644
--- a/src/content/docs/style-guide/components/typescript-example.mdx
+++ b/src/content/docs/style-guide/components/typescript-example.mdx
@@ -11,8 +11,6 @@ products:
The `TypeScriptExample` component uses [`ts-blank-space`](https://github.com/bloomberg/ts-blank-space) to remove TypeScript-specific syntax from your example and provide a JavaScript tab. This reduces maintenance burden by only having a single example to maintain.
-This component is automatically used in the [`GitHubCode`](/style-guide/components/github-code/) component when the `lang` is set to `ts`.
-
:::note
Some TypeScript syntax influences runtime behaviour, and cannot be stripped.
diff --git a/src/content/docs/style-guide/documentation-content-strategy/content-types/tutorial.mdx b/src/content/docs/style-guide/documentation-content-strategy/content-types/tutorial.mdx
index a76fdb23d94..0ff55623a61 100644
--- a/src/content/docs/style-guide/documentation-content-strategy/content-types/tutorial.mdx
+++ b/src/content/docs/style-guide/documentation-content-strategy/content-types/tutorial.mdx
@@ -58,7 +58,6 @@ For more details, refer to [`pcx_content_type`](/style-guide/frontmatter/custom-
#### Most used
-- [`GitHubCode`](/style-guide/components/github-code/)
- [`ListTutorials`](/style-guide/components/list-tutorials/)
#### Required
diff --git a/src/content/docs/style-guide/how-we-docs/image-maintenance.mdx b/src/content/docs/style-guide/how-we-docs/image-maintenance.mdx
index 2e27e05adfd..8ae3333b43c 100644
--- a/src/content/docs/style-guide/how-we-docs/image-maintenance.mdx
+++ b/src/content/docs/style-guide/how-we-docs/image-maintenance.mdx
@@ -8,8 +8,6 @@ products:
- style-guide
---
-import { GitHubCode } from "~/components";
-
Though valuable for user understanding, images are difficult to maintain. We have a few strategies that we use to help make this easier.
## Guidelines
@@ -43,14 +41,23 @@ What the GitHub action does is:
In combination with [flagging unused images](#flag-unused-images), we also have logic in our [build process](https://github.com/cloudflare/cloudflare-docs/blob/production/astro.config.ts) to validate image paths.
-
+```ts title="astro.config.ts" {5}
+export default defineConfig({
+ site: "https://developers.cloudflare.com",
+ markdown: {
+ smartypants: false,
+ remarkPlugins: [remarkValidateImages],
+ rehypePlugins: [
+ rehypeMermaid,
+ rehypeExternalLinks,
+ rehypeHeadingSlugs,
+ rehypeAutolinkHeadings,
+ // @ts-expect-error plugins types are outdated but functional
+ rehypeTitleFigure,
+ rehypeShiftHeadings,
+ ],
+ },
+```
This ensures that the build-time `nimbus/image-ref` lint rule validates all image paths. If the path does not exist, we throw an error and prevent the site from building.
diff --git a/src/content/docs/style-guide/how-we-docs/links.mdx b/src/content/docs/style-guide/how-we-docs/links.mdx
index 34143cbc612..b134a86d7ab 100644
--- a/src/content/docs/style-guide/how-we-docs/links.mdx
+++ b/src/content/docs/style-guide/how-we-docs/links.mdx
@@ -8,7 +8,7 @@ products:
- style-guide
---
-import { GitHubCode, Render } from "~/components";
+import { Render } from "~/components";
Though [links](/style-guide/documentation-content-strategy/component-attributes/links/) are an important part of documentation, they also have their own maintenance cost.
diff --git a/src/content/docs/style-guide/how-we-docs/metadata.mdx b/src/content/docs/style-guide/how-we-docs/metadata.mdx
index 88a42e32eed..e53a277cefa 100644
--- a/src/content/docs/style-guide/how-we-docs/metadata.mdx
+++ b/src/content/docs/style-guide/how-we-docs/metadata.mdx
@@ -8,8 +8,6 @@ products:
- style-guide
---
-import { GitHubCode } from "~/components";
-
Page-level metadata - content type, associated products, last updated, word count - lets you take a broader, more strategic view of your content.
It helps you answer questions like the following:
@@ -54,13 +52,25 @@ We set two values at a folder level, `Product` and `Product Group`. We take this
For example, here's the content from our [DNS folder](https://github.com/cloudflare/cloudflare-docs/blob/production/src/content/products/dns.yaml).
-
+```yaml title="dns.yaml" {4,6}
+name: DNS
+
+product:
+ title: DNS
+ url: /dns/
+ group: Application performance
+
+meta:
+ title: Cloudflare DNS docs
+ description: Cloudflare DNS provides the fastest, most resilient, and simplest
+ managed DNS platform to meet your needs.
+ author: "@cloudflare"
+
+resources:
+ community: https://community.cloudflare.com/tags/c/reliability/7/none
+ dashboard_link: https://dash.cloudflare.com/?to=/:account/:zone/dns
+ learning_center: https://www.cloudflare.com/learning/dns/what-is-dns/
+```
### Page-level attributes
@@ -68,14 +78,18 @@ We primarily set page-level attributes through the [page's frontmatter](/style-g
For example, here are the values set for our [Build a Slackbot tutorial](/workers/tutorials/build-a-slackbot/).
-
+```mdx title="build-a-slackbot.mdx" {2,4,6,7}
+---
+updated: 2024-06-05
+difficulty: Beginner
+pcx_content_type: tutorial
+title: Build a Slackbot
+tags:
+ - Hono
+languages:
+ - TypeScript
+---
+```
However, the `last_modified` value is pulled automatically from the git history of a file.
@@ -99,14 +113,16 @@ For example, these are the `meta` properties and values on the [AI Crawl Control
We render these values using a custom override for our [`Head.astro`](https://github.com/cloudflare/cloudflare-docs/blob/production/src/components/overrides/Head.astro) file. If specific values are set, we then add them as meta tags onto the page.
-
+```ts title="Head.astro"
+ if (product.data.product.title) {
+ ["pcx_product", "algolia_product_filter"].map((name) => {
+ metaTags.push({
+ name,
+ content: product.data.product.title,
+ });
+ });
+ }
+```
### Benefits
diff --git a/src/content/docs/style-guide/how-we-docs/redirects.mdx b/src/content/docs/style-guide/how-we-docs/redirects.mdx
index 70ad39d19ef..7a15c1e878e 100644
--- a/src/content/docs/style-guide/how-we-docs/redirects.mdx
+++ b/src/content/docs/style-guide/how-we-docs/redirects.mdx
@@ -8,7 +8,7 @@ products:
- style-guide
---
-import { Details, GitHubCode, GlossaryTooltip } from "~/components";
+import { Details, GlossaryTooltip } from "~/components";
As your content changes (and it will change), redirects preserve continuity for your users and (friendly) bots.
@@ -76,13 +76,67 @@ We trigger this check _after_ we build our site. What it does it then call [`val
- Redirect targets with anchor links in them
-
+
+```ts title="validate-redirects.ts"
+import { readFile } from "fs/promises";
+
+async function main() {
+ const redirects = await readFile("public/__redirects", { encoding: "utf-8" });
+
+ let numInfiniteRedirects = 0;
+ let numUrlsWithFragment = 0;
+ let numDuplicateRedirects = 0;
+
+ const redirectSourceUrls: string[] = [];
+
+ for (const line of redirects.split("\n")) {
+ if (line.startsWith("#") || line.trim() === "") continue;
+
+ const [from, to] = line.split(" ");
+
+ if (from === to) {
+ console.log(`✘ Found infinite redirect:\n ${from} -> ${to}`);
+ numInfiniteRedirects++;
+ }
+
+ if (from.includes("#")) {
+ console.log(`✘ Found source URL with fragment:\n ${from}`);
+ numUrlsWithFragment++;
+ }
+
+ if (redirectSourceUrls.includes(from)) {
+ console.log(`✘ Found repeated source URL:\n ${from}`);
+ numDuplicateRedirects++;
+ } else {
+ redirectSourceUrls.push(from);
+ }
+ }
+
+ if (numInfiniteRedirects || numUrlsWithFragment || numDuplicateRedirects) {
+ console.log("\nDetected errors:");
+
+ if (numInfiniteRedirects > 0) {
+ console.log(`- ${numInfiniteRedirects} infinite redirect(s)`);
+ }
+
+ if (numUrlsWithFragment > 0) {
+ console.log(`- ${numUrlsWithFragment} source URL(s) with a fragment`);
+ }
+
+ if (numDuplicateRedirects > 0) {
+ console.log(`- ${numDuplicateRedirects} repeated source URL(s)`);
+ }
+
+ console.log("\nPlease fix the errors above before merging :)");
+ process.exit(1);
+ } else {
+ console.log("\nDone!");
+ }
+}
+
+main();
+```
+
### Potential redirects
diff --git a/src/content/docs/workers/get-started/quickstarts.mdx b/src/content/docs/workers/get-started/quickstarts.mdx
index c4c9aaa383a..7abd42bdc02 100644
--- a/src/content/docs/workers/get-started/quickstarts.mdx
+++ b/src/content/docs/workers/get-started/quickstarts.mdx
@@ -11,8 +11,10 @@ products:
- workers
---
-import { LinkButton, WorkersTemplates } from "~/components";
+import { LinkButton } from "~/components";
-Templates are GitHub repositories that are designed to be a starting point for building a new Cloudflare Workers project. To start any of the projects below, run:
+Templates are GitHub repositories that are designed to be a starting point for building a new Cloudflare Workers project. Browse the full collection of templates in the Cloudflare dashboard, then deploy the one that best fits your use case.
-
+
+ Deploy a template
+
diff --git a/src/content/docs/workers/platform/infrastructure-as-code.mdx b/src/content/docs/workers/platform/infrastructure-as-code.mdx
index 345e55a62e9..a6523ff9688 100644
--- a/src/content/docs/workers/platform/infrastructure-as-code.mdx
+++ b/src/content/docs/workers/platform/infrastructure-as-code.mdx
@@ -8,7 +8,7 @@ products:
- workers
---
-import { GitHubCode, TabItem, Tabs } from "~/components";
+import { TypeScriptExample, TabItem, Tabs } from "~/components";
While [Wrangler](/workers/wrangler/configuration) makes it easy to upload and manage Workers, there are times when you need a more programmatic approach. This could involve using Infrastructure as Code (IaC) tools or interacting directly with the [Workers API](/api/resources/workers/). Examples include build and deploy scripts, CI/CD pipelines, custom developer tools, and automated testing.
@@ -382,13 +382,167 @@ resource "cloudflare_worker_version" "my_worker_version" {
This example uses the [cloudflare-typescript](https://github.com/cloudflare/cloudflare-typescript) SDK which provides convenient access to the Cloudflare REST API from server-side JavaScript or TypeScript.
-
+
+
+```ts
+#!/usr/bin/env -S npm run tsn -T
+
+/**
+ * Create and deploy a Worker
+ *
+ * Docs:
+ * - https://developers.cloudflare.com/workers/configuration/versions-and-deployments/
+ * - https://developers.cloudflare.com/workers/platform/infrastructure-as-code/
+ *
+ * Prerequisites:
+ * 1. Generate an API token: https://developers.cloudflare.com/fundamentals/api/get-started/create-token/
+ * 2. Find your account ID: https://developers.cloudflare.com/fundamentals/setup/find-account-and-zone-ids/
+ * 3. Find your workers.dev subdomain: https://developers.cloudflare.com/workers/configuration/routing/workers-dev/
+ *
+ * Environment variables:
+ * - CLOUDFLARE_API_TOKEN (required)
+ * - CLOUDFLARE_ACCOUNT_ID (required)
+ * - CLOUDFLARE_SUBDOMAIN (optional)
+ *
+ * Usage:
+ * Run this script to deploy a simple "Hello World" Worker.
+ * Access it at: my-hello-world-worker.$subdomain.workers.dev
+ */
+
+import { exit } from 'node:process';
+
+import Cloudflare from 'cloudflare';
+
+interface Config {
+ apiToken: string;
+ accountId: string;
+ subdomain: string | undefined;
+ workerName: string;
+}
+
+const WORKER_NAME = 'my-hello-world-worker';
+const SCRIPT_FILENAME = `${WORKER_NAME}.mjs`;
+
+function loadConfig(): Config {
+ const apiToken = process.env['CLOUDFLARE_API_TOKEN'];
+ if (!apiToken) {
+ throw new Error('Missing required environment variable: CLOUDFLARE_API_TOKEN');
+ }
+
+ const accountId = process.env['CLOUDFLARE_ACCOUNT_ID'];
+ if (!accountId) {
+ throw new Error('Missing required environment variable: CLOUDFLARE_ACCOUNT_ID');
+ }
+
+ const subdomain = process.env['CLOUDFLARE_SUBDOMAIN'];
+
+ return {
+ apiToken,
+ accountId,
+ subdomain: subdomain || undefined,
+ workerName: WORKER_NAME,
+ };
+}
+
+const config = loadConfig();
+const client = new Cloudflare({
+ apiToken: config.apiToken,
+});
+
+async function main(): Promise {
+ try {
+ console.log('🚀 Starting Worker creation and deployment...');
+
+ const scriptContent = `
+ export default {
+ async fetch(request, env, ctx) {
+ return new Response(env.MESSAGE, { status: 200 });
+ },
+ }`.trim();
+
+ let worker;
+ try {
+ worker = await client.workers.beta.workers.get(config.workerName, {
+ account_id: config.accountId,
+ });
+ console.log(`♻️ Worker ${config.workerName} already exists. Using it.`);
+ } catch (error) {
+ if (!(error instanceof Cloudflare.NotFoundError)) { throw error; }
+ console.log(`✏️ Creating Worker ${config.workerName}...`);
+ worker = await client.workers.beta.workers.create({
+ account_id: config.accountId,
+ name: config.workerName,
+ subdomain: {
+ enabled: config.subdomain !== undefined,
+ },
+ observability: {
+ enabled: true,
+ },
+ });
+ }
+
+ console.log(`⚙️ Worker id: ${worker.id}`);
+ console.log('✏️ Creating Worker version...');
+
+ // Create the first version of the Worker
+ const version = await client.workers.beta.workers.versions.create(worker.id, {
+ account_id: config.accountId,
+ main_module: SCRIPT_FILENAME,
+ compatibility_date: new Date().toISOString().split('T')[0]!,
+ bindings: [
+ {
+ type: 'plain_text',
+ name: 'MESSAGE',
+ text: 'Hello World!',
+ },
+ ],
+ modules: [
+ {
+ name: SCRIPT_FILENAME,
+ content_type: 'application/javascript+module',
+ content_base64: Buffer.from(scriptContent).toString('base64'),
+ },
+ ],
+ });
+
+ console.log(`⚙️ Version id: ${version.id}`);
+ console.log('🚚 Creating Worker deployment...');
+
+ // Create a deployment and point all traffic to the version we created
+ await client.workers.scripts.deployments.create(config.workerName, {
+ account_id: config.accountId,
+ strategy: 'percentage',
+ versions: [
+ {
+ percentage: 100,
+ version_id: version.id,
+ },
+ ],
+ });
+
+ console.log('✅ Deployment successful!');
+
+ if (config.subdomain) {
+ console.log(`
+🌍 Your Worker is live!
+📍 URL: https://${config.workerName}.${config.subdomain}.workers.dev/
+`);
+ } else {
+ console.log(`
+⚠️ Set up a route, custom domain, or workers.dev subdomain to access your Worker.
+Add CLOUDFLARE_SUBDOMAIN to your environment variables to set one up automatically.
+`);
+ }
+ } catch (error) {
+ console.error('❌ Deployment failed:', error);
+ exit(1);
+ }
+}
+
+main();
+```
+
+
## Cloudflare REST API
diff --git a/src/content/docs/workers/static-assets/direct-upload.mdx b/src/content/docs/workers/static-assets/direct-upload.mdx
index aab6097c0ec..f47159bb0fb 100644
--- a/src/content/docs/workers/static-assets/direct-upload.mdx
+++ b/src/content/docs/workers/static-assets/direct-upload.mdx
@@ -13,7 +13,6 @@ import {
Badge,
Description,
FileTree,
- GitHubCode,
InlineBadge,
Render,
TabItem,
@@ -217,10 +216,420 @@ Optionally, an assets binding can be provided if you wish to fetch and serve ass
This example is from [cloudflare-typescript](https://github.com/cloudflare/cloudflare-typescript/blob/main/examples/workers/script-with-assets-upload.ts).
-
+
+
+```ts
+#!/usr/bin/env -S npm run tsn -T
+
+/**
+ * Create a Worker that serves static assets
+ *
+ * This example demonstrates how to:
+ * - Upload static assets to Cloudflare Workers
+ * - Create and deploy a Worker that serves those assets
+ *
+ * Docs:
+ * - https://developers.cloudflare.com/workers/static-assets/direct-upload
+ *
+ * Prerequisites:
+ * 1. Generate an API token: https://developers.cloudflare.com/fundamentals/api/get-started/create-token/
+ * 2. Find your account ID: https://developers.cloudflare.com/fundamentals/setup/find-account-and-zone-ids/
+ * 3. Find your workers.dev subdomain: https://developers.cloudflare.com/workers/configuration/routing/workers-dev/
+ *
+ * Environment variables:
+ * - CLOUDFLARE_API_TOKEN (required)
+ * - CLOUDFLARE_ACCOUNT_ID (required)
+ * - ASSETS_DIRECTORY (required)
+ * - CLOUDFLARE_SUBDOMAIN (optional)
+ *
+ * Usage:
+ * Place your static files in the ASSETS_DIRECTORY, then run this script.
+ * Assets will be available at: my-script-with-assets.$subdomain.workers.dev/$filename
+ */
+
+import crypto from 'crypto';
+import fs from 'fs';
+import { readFile } from 'node:fs/promises';
+import { extname } from 'node:path';
+import path from 'path';
+import { exit } from 'node:process';
+
+import Cloudflare from 'cloudflare';
+
+interface Config {
+ apiToken: string;
+ accountId: string;
+ assetsDirectory: string;
+ subdomain: string | undefined;
+ workerName: string;
+}
+
+interface AssetManifest {
+ [path: string]: {
+ hash: string;
+ size: number;
+ };
+}
+
+interface UploadPayload {
+ [hash: string]: string; // base64 encoded content
+}
+
+const WORKER_NAME = 'my-worker-with-assets';
+const SCRIPT_FILENAME = `${WORKER_NAME}.mjs`;
+
+function loadConfig(): Config {
+ const apiToken = process.env['CLOUDFLARE_API_TOKEN'];
+ if (!apiToken) {
+ throw new Error('Missing required environment variable: CLOUDFLARE_API_TOKEN');
+ }
+
+ const accountId = process.env['CLOUDFLARE_ACCOUNT_ID'];
+ if (!accountId) {
+ throw new Error('Missing required environment variable: CLOUDFLARE_ACCOUNT_ID');
+ }
+
+ const assetsDirectory = process.env['ASSETS_DIRECTORY'];
+ if (!assetsDirectory) {
+ throw new Error('Missing required environment variable: ASSETS_DIRECTORY');
+ }
+
+ if (!fs.existsSync(assetsDirectory)) {
+ throw new Error(`Assets directory does not exist: ${assetsDirectory}`);
+ }
+
+ const subdomain = process.env['CLOUDFLARE_SUBDOMAIN'];
+
+ return {
+ apiToken,
+ accountId,
+ assetsDirectory,
+ subdomain: subdomain || undefined,
+ workerName: WORKER_NAME,
+ };
+}
+
+const config = loadConfig();
+const client = new Cloudflare({
+ apiToken: config.apiToken,
+});
+
+/**
+ * Recursively reads all files from a directory and creates a manifest
+ * mapping file paths to their hash and size.
+ */
+function createManifest(directory: string): AssetManifest {
+ const manifest: AssetManifest = {};
+
+ function processDirectory(currentDir: string, basePath = ''): void {
+ try {
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
+
+ for (const entry of entries) {
+ const fullPath = path.join(currentDir, entry.name);
+ const relativePath = path.join(basePath, entry.name);
+
+ if (entry.isDirectory()) {
+ processDirectory(fullPath, relativePath);
+ } else if (entry.isFile()) {
+ try {
+ const fileContent = fs.readFileSync(fullPath);
+ const extension = extname(relativePath).substring(1);
+
+ // Generate a hash for the file
+ const hash = crypto
+ .createHash('sha256')
+ .update(fileContent.toString('base64') + extension)
+ .digest('hex')
+ .slice(0, 32);
+
+ // Normalize path separators to forward slashes
+ const manifestPath = `/${relativePath.replace(/\\/g, '/')}`;
+
+ manifest[manifestPath] = {
+ hash,
+ size: fileContent.length,
+ };
+
+ console.log(`Added to manifest: ${manifestPath} (${fileContent.length} bytes)`);
+ } catch (error) {
+ console.warn(`Failed to process file ${fullPath}:`, error);
+ }
+ }
+ }
+ } catch (error) {
+ throw new Error(`Failed to read directory ${currentDir}: ${error}`);
+ }
+ }
+
+ processDirectory(directory);
+
+ if (Object.keys(manifest).length === 0) {
+ throw new Error(`No files found in assets directory: ${directory}`);
+ }
+
+ console.log(`Created manifest with ${Object.keys(manifest).length} files`);
+ return manifest;
+}
+
+/**
+ * Generates the Worker script content that serves static assets
+ */
+function generateWorkerScript(exampleFile: string): string {
+ return `
+export default {
+ async fetch(request, env, ctx) {
+ const url = new URL(request.url);
+
+ // Serve a simple index page at the root
+ if (url.pathname === '/') {
+ return new Response(
+ \`
+
+
+ Static Assets Worker
+
+
+
+ This Worker serves static assets!
+
+ To access your assets, add /filename to the URL.
+ Try visiting /${exampleFile}
+
+
+\`,
+ {
+ status: 200,
+ headers: { 'Content-Type': 'text/html' }
+ }
+ );
+ }
+
+ // Serve static assets for all other paths
+ return env.ASSETS.fetch(request);
+ }
+};
+ `.trim();
+}
+
+/**
+ * Creates upload payloads from buckets and manifest
+ */
+async function createUploadPayloads(
+ buckets: string[][],
+ manifest: AssetManifest,
+ assetsDirectory: string
+): Promise {
+ const payloads: UploadPayload[] = [];
+
+ for (const bucket of buckets) {
+ const payload: UploadPayload = {};
+
+ for (const hash of bucket) {
+ // Find the file path for this hash
+ const manifestEntry = Object.entries(manifest).find(
+ ([_, data]) => data.hash === hash
+ );
+
+ if (!manifestEntry) {
+ throw new Error(`Could not find file for hash: ${hash}`);
+ }
+
+ const [relativePath] = manifestEntry;
+ const fullPath = path.join(assetsDirectory, relativePath);
+
+ try {
+ const fileContent = await readFile(fullPath);
+ payload[hash] = fileContent.toString('base64');
+ console.log(`Prepared for upload: ${relativePath}`);
+ } catch (error) {
+ throw new Error(`Failed to read file ${fullPath}: ${error}`);
+ }
+ }
+
+ payloads.push(payload);
+ }
+
+ return payloads;
+}
+
+/**
+ * Uploads asset payloads
+ */
+async function uploadAssets(
+ payloads: UploadPayload[],
+ uploadJwt: string,
+ accountId: string
+): Promise {
+ let completionJwt: string | undefined;
+
+ console.log(`Uploading ${payloads.length} payload(s)...`);
+
+ for (let i = 0; i < payloads.length; i++) {
+ const payload = payloads[i]!;
+ console.log(`Uploading payload ${i + 1}/${payloads.length}...`);
+
+ try {
+ const response = await client.workers.assets.upload.create(
+ {
+ account_id: accountId,
+ base64: true,
+ body: payload,
+ },
+ {
+ headers: { Authorization: `Bearer ${uploadJwt}` },
+ }
+ );
+
+ if (response?.jwt) {
+ completionJwt = response.jwt;
+ }
+ } catch (error) {
+ throw new Error(`Failed to upload payload ${i + 1}: ${error}`);
+ }
+ }
+
+ if (!completionJwt) {
+ throw new Error('Upload completed but no completion JWT received');
+ }
+
+ console.log('✅ All assets uploaded successfully');
+ return completionJwt;
+}
+
+async function main(): Promise {
+ try {
+ console.log('🚀 Starting Worker creation and deployment with static assets...');
+ console.log(`📁 Assets directory: ${config.assetsDirectory}`);
+
+ console.log('📝 Creating asset manifest...');
+ const manifest = createManifest(config.assetsDirectory);
+ const exampleFile = Object.keys(manifest)[0]?.replace(/^\//, '') || 'file.txt';
+
+ const scriptContent = generateWorkerScript(exampleFile);
+
+ let worker;
+ try {
+ worker = await client.workers.beta.workers.get(config.workerName, {
+ account_id: config.accountId,
+ });
+ console.log(`♻️ Worker ${config.workerName} already exists. Using it.`);
+ } catch (error) {
+ if (!(error instanceof Cloudflare.NotFoundError)) { throw error; }
+ console.log(`✏️ Creating Worker ${config.workerName}...`);
+ worker = await client.workers.beta.workers.create({
+ account_id: config.accountId,
+ name: config.workerName,
+ subdomain: {
+ enabled: config.subdomain !== undefined,
+ },
+ observability: {
+ enabled: true,
+ },
+ });
+ }
+
+ console.log(`⚙️ Worker id: ${worker.id}`);
+ console.log('🔄 Starting asset upload session...');
+
+ const uploadResponse = await client.workers.scripts.assets.upload.create(
+ config.workerName,
+ {
+ account_id: config.accountId,
+ manifest,
+ }
+ );
+
+ const { buckets, jwt: uploadJwt } = uploadResponse;
+
+ if (!uploadJwt || !buckets) {
+ throw new Error('Failed to start asset upload session');
+ }
+
+ let completionJwt: string;
+
+ if (buckets.length === 0) {
+ console.log('✅ No new assets to upload!');
+ // Use the initial upload JWT as completion JWT when no uploads are needed
+ completionJwt = uploadJwt;
+ } else {
+ const payloads = await createUploadPayloads(
+ buckets,
+ manifest,
+ config.assetsDirectory
+ );
+
+ completionJwt = await uploadAssets(
+ payloads,
+ uploadJwt,
+ config.accountId
+ );
+ }
+
+ console.log('✏️ Creating Worker version...');
+
+ // Create a new version with assets
+ const version = await client.workers.beta.workers.versions.create(worker.id, {
+ account_id: config.accountId,
+ main_module: SCRIPT_FILENAME,
+ compatibility_date: new Date().toISOString().split('T')[0]!,
+ bindings: [
+ {
+ type: 'assets',
+ name: 'ASSETS',
+ },
+ ],
+ assets: {
+ jwt: completionJwt,
+ },
+ modules: [
+ {
+ name: SCRIPT_FILENAME,
+ content_type: 'application/javascript+module',
+ content_base64: Buffer.from(scriptContent).toString('base64'),
+ },
+ ],
+ });
+
+ console.log('🚚 Creating Worker deployment...');
+
+ // Create a deployment and point all traffic to the version we created
+ await client.workers.scripts.deployments.create(config.workerName, {
+ account_id: config.accountId,
+ strategy: 'percentage',
+ versions: [
+ {
+ percentage: 100,
+ version_id: version.id,
+ },
+ ],
+ });
+
+ console.log('✅ Deployment successful!');
+
+ if (config.subdomain) {
+ console.log(`
+🌍 Your Worker is live!
+📍 Base URL: https://${config.workerName}.${config.subdomain}.workers.dev/
+📄 Try accessing: https://${config.workerName}.${config.subdomain}.workers.dev/${exampleFile}
+`);
+ } else {
+ console.log(`
+⚠️ Set up a route, custom domain, or workers.dev subdomain to access your Worker.
+Add CLOUDFLARE_SUBDOMAIN to your environment variables to set one up automatically.
+`);
+ }
+ } catch (error) {
+ console.error('❌ Deployment failed:', error);
+ exit(1);
+ }
+}
+
+main();
+```
+
+
diff --git a/src/content/docs/workers/tutorials/deploy-an-express-app.mdx b/src/content/docs/workers/tutorials/deploy-an-express-app.mdx
index 63a565491ae..9a516b3ef59 100644
--- a/src/content/docs/workers/tutorials/deploy-an-express-app.mdx
+++ b/src/content/docs/workers/tutorials/deploy-an-express-app.mdx
@@ -10,12 +10,7 @@ description: >-
Learn how to deploy an Express.js application on Cloudflare Workers.
---
-import {
- Render,
- WranglerConfig,
- PackageManagers,
- GitHubCode,
-} from "~/components";
+import { Render, WranglerConfig, PackageManagers } from "~/components";
In this tutorial, you will learn how to deploy an [Express.js](https://expressjs.com/) application on Cloudflare Workers using the [Cloudflare Workers platform](/workers/) and [D1 database](/d1/). You will build a Members Registry API with basic Create, Read, Update, and Delete (CRUD) operations. You will use D1 as the database for storing and retrieving member data.
@@ -134,16 +129,21 @@ The binding will be added to your Wrangler configuration file.
Create a directory called `schemas` in your project root, and inside it, create a file called `schema.sql`:
-
+```sql title="schemas/schema.sql"
+DROP TABLE IF EXISTS members;
+CREATE TABLE IF NOT EXISTS members (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ email TEXT NOT NULL UNIQUE,
+ joined_date TEXT NOT NULL
+);
+
+-- Insert sample data
+INSERT INTO members (name, email, joined_date) VALUES
+ ('Alice Johnson', 'alice@example.com', '2024-01-15'),
+ ('Bob Smith', 'bob@example.com', '2024-02-20'),
+ ('Carol Williams', 'carol@example.com', '2024-03-10');
+```
This schema creates a `members` table with an auto-incrementing ID, name, email, and join date fields. It also inserts three sample members.
@@ -190,16 +190,35 @@ npm run cf-typegen
Add endpoints to retrieve members from the database. Update your `src/index.ts` file by adding the following routes after the health check endpoint:
-
+```ts title="src/index.ts"
+// GET all members
+app.get('/api/members', async (req, res) => {
+ try {
+ const { results } = await env.DB.prepare('SELECT * FROM members ORDER BY joined_date DESC').all();
+
+ res.json({ success: true, members: results });
+ } catch (error) {
+ res.status(500).json({ success: false, error: 'Failed to fetch members' });
+ }
+});
+
+// GET a single member by ID
+app.get('/api/members/:id', async (req, res) => {
+ try {
+ const { id } = req.params;
+
+ const { results } = await env.DB.prepare('SELECT * FROM members WHERE id = ?').bind(id).all();
+
+ if (results.length === 0) {
+ return res.status(404).json({ success: false, error: 'Member not found' });
+ }
+
+ res.json({ success: true, member: results[0] });
+ } catch (error) {
+ res.status(500).json({ success: false, error: 'Failed to fetch member' });
+ }
+});
+```
These routes use the D1 binding (`env.DB`) to prepare SQL statements and execute them. Since you imported `env` from `cloudflare:workers` at the top of the file, it is accessible throughout your application. The `prepare`, `bind`, and `all` methods on the D1 binding allow you to safely query the database. Refer to [D1 Workers Binding API](/d1/worker-api/) for all available methods.
@@ -207,16 +226,60 @@ These routes use the D1 binding (`env.DB`) to prepare SQL statements and execute
Add an endpoint to create new members. Add the following route to your `src/index.ts` file:
-
+```ts title="src/index.ts"
+// POST - Create a new member
+app.post("/api/members", async (req, res) => {
+ try {
+ const { name, email } = req.body;
+
+ // Validate input
+ if (!name || !email) {
+ return res.status(400).json({
+ success: false,
+ error: "Name and email are required",
+ });
+ }
+
+ // Basic email validation (simplified for tutorial purposes)
+ // For production, consider using a validation library or more comprehensive checks
+ if (!email.includes("@") || !email.includes(".")) {
+ return res.status(400).json({
+ success: false,
+ error: "Invalid email format",
+ });
+ }
+
+ const joined_date = new Date().toISOString().split("T")[0];
+
+ const result = await env.DB.prepare(
+ "INSERT INTO members (name, email, joined_date) VALUES (?, ?, ?)"
+ )
+ .bind(name, email, joined_date)
+ .run();
+
+ if (result.success) {
+ res.status(201).json({
+ success: true,
+ message: "Member created successfully",
+ id: result.meta.last_row_id,
+ });
+ } else {
+ res
+ .status(500)
+ .json({ success: false, error: "Failed to create member" });
+ }
+ } catch (error: any) {
+ // Handle unique constraint violation
+ if (error.message?.includes("UNIQUE constraint failed")) {
+ return res.status(409).json({
+ success: false,
+ error: "Email already exists",
+ });
+ }
+ res.status(500).json({ success: false, error: "Failed to create member" });
+ }
+});
+```
This endpoint validates the input, checks the email format, and inserts a new member into the database. It also handles duplicate email addresses by checking for unique constraint violations.
@@ -224,16 +287,68 @@ This endpoint validates the input, checks the email format, and inserts a new me
Add an endpoint to update existing members. Add the following route to your `src/index.ts` file:
-
+```ts title="src/index.ts"
+app.put("/api/members/:id", async (req, res) => {
+ try {
+ const { id } = req.params;
+ const { name, email } = req.body;
+
+ // Validate input
+ if (!name && !email) {
+ return res.status(400).json({
+ success: false,
+ error: "At least one field (name or email) is required",
+ });
+ }
+
+ // Basic email validation if provided (simplified for tutorial purposes)
+ // For production, consider using a validation library or more comprehensive checks
+ if (email && (!email.includes("@") || !email.includes("."))) {
+ return res.status(400).json({
+ success: false,
+ error: "Invalid email format",
+ });
+ }
+
+ // Build dynamic update query
+ const updates: string[] = [];
+ const values: any[] = [];
+
+ if (name) {
+ updates.push("name = ?");
+ values.push(name);
+ }
+ if (email) {
+ updates.push("email = ?");
+ values.push(email);
+ }
+
+ values.push(id);
+
+ const result = await env.DB.prepare(
+ `UPDATE members SET ${updates.join(", ")} WHERE id = ?`
+ )
+ .bind(...values)
+ .run();
+
+ if (result.meta.changes === 0) {
+ return res
+ .status(404)
+ .json({ success: false, error: "Member not found" });
+ }
+
+ res.json({ success: true, message: "Member updated successfully" });
+ } catch (error: any) {
+ if (error.message?.includes("UNIQUE constraint failed")) {
+ return res.status(409).json({
+ success: false,
+ error: "Email already exists",
+ });
+ }
+ res.status(500).json({ success: false, error: "Failed to update member" });
+ }
+});
+```
This endpoint allows updating either the name, email, or both fields of an existing member. It builds a dynamic SQL query based on the provided fields.
@@ -241,16 +356,28 @@ This endpoint allows updating either the name, email, or both fields of an exist
Add an endpoint to delete members. Add the following route to your `src/index.ts` file:
-
+```ts title="src/index.ts"
+// DELETE - Delete a member
+app.delete("/api/members/:id", async (req, res) => {
+ try {
+ const { id } = req.params;
+
+ const result = await env.DB.prepare("DELETE FROM members WHERE id = ?")
+ .bind(id)
+ .run();
+
+ if (result.meta.changes === 0) {
+ return res
+ .status(404)
+ .json({ success: false, error: "Member not found" });
+ }
+
+ res.json({ success: true, message: "Member deleted successfully" });
+ } catch (error) {
+ res.status(500).json({ success: false, error: "Failed to delete member" });
+ }
+});
+```
This endpoint deletes a member by their ID and returns an error if the member does not exist.
diff --git a/src/content/docs/workflows/examples/wait-for-event.mdx b/src/content/docs/workflows/examples/wait-for-event.mdx
index 95cc28394e9..0bc09f622bc 100644
--- a/src/content/docs/workflows/examples/wait-for-event.mdx
+++ b/src/content/docs/workflows/examples/wait-for-event.mdx
@@ -13,7 +13,7 @@ products:
- workflows
---
-import { GitHubCode, WranglerConfig } from "~/components"
+import { TypeScriptExample, WranglerConfig } from "~/components"
This example demonstrates how to use the `waitForEvent()` API in Cloudflare Workflows to introduce a human-in-the-loop step. The Workflow is triggered by an image upload, during which metadata is stored in a D1 database. The Workflow then waits for user approval, and upon approval, it uses Workers AI to generate image tags, which are stored in the database. An accompanying Next.js frontend application facilitates the image upload and approval process.
@@ -44,14 +44,53 @@ The `index.ts` file defines the core logic of the Cloudflare Workflow responsibl
For the complete implementation of the `index.ts` file, please refer to the [GitHub repository](https://github.com/cloudflare/docs-examples/blob/main/workflows/waitForEvent/workflow/src/index.ts).
-
+
+
+```ts
+export class MyWorkflow extends WorkflowEntrypoint {
+ private db!: DatabaseService;
+
+ async run(event: WorkflowEvent, step: WorkflowStep) {
+ this.db = new DatabaseService(this.env.DB);
+ const { imageKey } = event.payload;
+
+ await step.do('Insert image name into database', async () => {
+ await this.db.insertImage(imageKey, event.instanceId);
+ });
+
+ const waitForApproval = await step.waitForEvent('Wait for AI Image tagging approval', {
+ type: 'approval-for-ai-tagging',
+ timeout: '5 minute',
+ });
+
+ const approvalPayload = waitForApproval.payload as ApprovalRequest;
+ if (approvalPayload?.approved) {
+ const aiTags = await step.do('Generate AI tags', async () => {
+ const image = await this.env.workflow_demo_bucket.get(imageKey);
+ if (!image) throw new Error('Image not found');
+
+ const arrayBuffer = await image.arrayBuffer();
+ const uint8Array = new Uint8Array(arrayBuffer);
+
+ const input = {
+ image: Array.from(uint8Array),
+ prompt: AI_CONFIG.PROMPT,
+ max_tokens: AI_CONFIG.MAX_TOKENS,
+ };
+
+ const response = await this.env.AI.run(AI_CONFIG.MODEL, input);
+ return response.description;
+ });
+
+ await step.do('Update DB with AI tags', async () => {
+ await this.db.updateImageTags(event.instanceId, aiTags);
+ });
+ }
+ }
+}
+```
+
+
## Workflow wrangler.jsonc
diff --git a/src/mdx-components.ts b/src/mdx-components.ts
index bff2deb24ed..99cf191283e 100644
--- a/src/mdx-components.ts
+++ b/src/mdx-components.ts
@@ -33,7 +33,6 @@ import YouTube from "./components/cf/YouTube.astro";
import Example from "./components/cf/Example.astro";
import Markdown from "./components/cf/Markdown.astro";
import CURL from "./components/cf/CURL.astro";
-import GitHubCode from "./components/cf/GitHubCode.astro";
import Width from "./components/cf/Width.astro";
import RuleID from "./components/cf/RuleID.astro";
import PublicStats from "./components/cf/PublicStats.astro";
@@ -65,7 +64,6 @@ export const components = {
Feature,
FeatureTable,
FileTree,
- GitHubCode,
Glossary,
GlossaryDefinition,
GlossaryTooltip,
diff --git a/src/util/api.ts b/src/util/api.ts
index b6927f55efb..beb3b1f3df7 100644
--- a/src/util/api.ts
+++ b/src/util/api.ts
@@ -4,8 +4,8 @@
* CF source: cloudflare-docs/src/util/api.ts — 1:1 port.
*
* Fetches the Cloudflare API OpenAPI document from the gh-code worker at a
- * PINNED commit (same reproducibility model as cf/GitHubCode.astro) and
- * dereferences all `$ref`s. Memoized at module scope so the fetch + deref run
+ * PINNED commit and dereferences all `$ref`s. Memoized at module scope so the
+ * fetch + deref run
* once per build, not per component instance.
*
* Reproducibility (migration WS9): the COMMIT below MUST match upstream's pin
diff --git a/src/util/github.ts b/src/util/github.ts
deleted file mode 100644
index 32ae1362b2e..00000000000
--- a/src/util/github.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-export async function fetchWithToken(
- req: Parameters[0],
- init?: Parameters[1],
-) {
- if (!import.meta.env.GITHUB_TOKEN) {
- const res = await fetch(req);
-
- if (res.status === 403 || res.status === 429) {
- throw new Error(
- `A request to the GitHub API (${res.url}) was made without a token and was rate limited.\nIf you have the "gh" CLI installed, you can get a token like so: "GITHUB_TOKEN=$(gh auth token) npx astro dev"`,
- );
- }
-
- return res;
- }
-
- const request = new Request(req, init);
-
- return fetch(request, {
- ...request,
- headers: {
- ...request.headers,
- Authorization: `Bearer ${import.meta.env.GITHUB_TOKEN}`,
- },
- });
-}