diff --git a/README.md b/README.md index ef07a9f..49ca153 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # Genkit by Example -This repo contains the source code for the examples at [examples.genkit.dev](https://examples.genkit.dev). It is intended as an educational resource for developers wanting to add GenAI to their applications using Genkit. +This repo contains the source code for the examples at [genkit.dev/examples](https://genkit.dev/examples). It is intended as an educational resource for developers wanting to add GenAI to their applications using Genkit. diff --git a/next.config.ts b/next.config.ts index 4796dd1..9f03d26 100644 --- a/next.config.ts +++ b/next.config.ts @@ -17,7 +17,11 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + basePath: '/examples', + images: { + loader: 'custom', + loaderFile: './src/lib/image-loader.ts', + }, }; export default nextConfig; diff --git a/src/app/action-context/app.tsx b/src/app/action-context/app.tsx index a6cd548..2315881 100644 --- a/src/app/action-context/app.tsx +++ b/src/app/action-context/app.tsx @@ -19,13 +19,14 @@ import React, { useContext } from "react"; import Chat from "@/components/chat"; import DemoConfig from "@/lib/demo-config"; import CodeBlock from "@/components/code-block"; +import { withBasePath } from "@/lib/constants"; export default function ActionContextDemoApp() { const { config } = useContext(DemoConfig); return ( { if (info.message.role === "system") return <>; switch (part.toolResponse?.name) { diff --git a/src/app/api/og/route.tsx b/src/app/api/og/route.tsx index 6be4339..79d82ba 100644 --- a/src/app/api/og/route.tsx +++ b/src/app/api/og/route.tsx @@ -16,6 +16,7 @@ import { ImageResponse } from "@vercel/og"; import { NextRequest } from "next/server"; +import { SITE_ORIGIN } from "@/lib/constants"; export const runtime = "edge"; @@ -39,9 +40,7 @@ export async function GET(req: NextRequest) { padding: "40px", color: "white", fontFamily: "Nunito Sans", // Use Nunito Sans font (default) - backgroundImage: `url(${ - process.env.SITE_ORIGIN || "http://localhost:3000" - }/og_bg.png)`, + backgroundImage: `url(${SITE_ORIGIN}/og_bg.png)`, }} >

- + ); } diff --git a/src/app/image-analysis/app.tsx b/src/app/image-analysis/app.tsx index 5c10059..25044ff 100644 --- a/src/app/image-analysis/app.tsx +++ b/src/app/image-analysis/app.tsx @@ -26,6 +26,7 @@ import ImageField from "./image-field"; import HighlightArea from "./highlight-area"; import DemoConfig from "@/lib/demo-config"; import useAgent from "@/lib/use-agent"; +import { withBasePath } from "@/lib/constants"; function fileToDataUri(file: File): Promise { return new Promise((resolve, reject) => { @@ -57,8 +58,8 @@ export default function ImageAnalysisApp() { // Sample images const sampleImages = [ - { name: "Bird in Tree", path: "/image-analysis/bird_in_tree.png" }, - { name: "Desk Items", path: "/image-analysis/desk_items.jpg" }, + { name: "Bird in Tree", path: withBasePath("/image-analysis/bird_in_tree.png") }, + { name: "Desk Items", path: withBasePath("/image-analysis/desk_items.jpg") }, ]; function borderColor(o: Partial) { @@ -80,7 +81,7 @@ export default function ImageAnalysisApp() { const analysis = await simplePost< { imageUrl: string; system: any }, Analysis - >("/image-analysis/api", { + >(withBasePath("/image-analysis/api"), { system: config?.system, imageUrl: await fileToDataUri(file), }); diff --git a/src/app/mcp/app.tsx b/src/app/mcp/app.tsx index 437430e..cfb7e0d 100644 --- a/src/app/mcp/app.tsx +++ b/src/app/mcp/app.tsx @@ -19,6 +19,7 @@ import React from "react"; import Chat from "@/components/chat"; import { Sun, Cloud, CloudRain, CloudSnow } from "lucide-react"; import Dice from "./dice-roll"; +import { withBasePath } from "@/lib/constants"; function WeatherResponse({ temperature, @@ -78,7 +79,7 @@ function DiceResponse({ output }: { output: number }) { export default function ToolCallingChatbotApp() { return ( { if (part.toolResponse?.name === "getWeather") return ; diff --git a/src/app/page.tsx b/src/app/page.tsx index 1f2de23..0cdf27f 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -19,6 +19,7 @@ import { demos } from "@/data"; import { Metadata } from "next"; import Image from "next/image"; import Link from "next/link"; +import { SITE_ORIGIN } from "@/lib/constants"; export const metadata: Metadata = { title: "Add GenAI to your web/mobile app", @@ -26,9 +27,7 @@ export const metadata: Metadata = { "Pre-built examples of using Genkit to build a variety of AI-powered features for Next.js applications.", openGraph: { images: [ - `${ - process.env.SITE_ORIGIN || "http://localhost:3000" - }/api/og?title=Practical%20GenAI%20 Demos`, + `${SITE_ORIGIN}/api/og?title=Practical%20GenAI%20 Demos`, ], }, }; diff --git a/src/app/robots.txt b/src/app/robots.txt index a03bdcc..e78e7ea 100644 --- a/src/app/robots.txt +++ b/src/app/robots.txt @@ -1,4 +1,4 @@ User-Agent: * Allow: / -Sitemap: https://examples.genkit.dev/sitemap.xml \ No newline at end of file +Sitemap: https://genkit.dev/examples/sitemap.xml \ No newline at end of file diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index 109a543..1183e03 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -1,5 +1,6 @@ import { demos } from "@/data"; import type { MetadataRoute } from "next"; +import { SITE_ORIGIN } from "@/lib/constants"; const latestUpdate = demos .map((d) => d.added) @@ -8,15 +9,16 @@ const latestUpdate = demos .at(-1); export default function sitemap(): MetadataRoute.Sitemap { + const baseUrl = SITE_ORIGIN; return [ { - url: "https://examples.genkit.dev", + url: baseUrl, lastModified: latestUpdate, changeFrequency: "daily", priority: 1, }, ...demos.map((d) => ({ - url: `https://examples.genkit.dev/${d.id}`, + url: `${baseUrl}/${d.id}`, lastModified: d.added, changeFrequency: "weekly" as const, priority: 0.8, diff --git a/src/app/structured-output/app.tsx b/src/app/structured-output/app.tsx index ae3be7f..27bdf25 100644 --- a/src/app/structured-output/app.tsx +++ b/src/app/structured-output/app.tsx @@ -24,6 +24,7 @@ import type { CharacterSheet } from "./schema"; import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; import React from "react"; +import { withBasePath } from "@/lib/constants"; export default function StructuredOutputApp() { const [prompt, setPrompt] = useState(""); @@ -47,7 +48,7 @@ export default function StructuredOutputApp() { const stream = post< { prompt: string }, { output: Partial } - >("/structured-output/api", { + >(withBasePath("/structured-output/api"), { prompt, }); for await (const chunk of stream) { diff --git a/src/app/tool-calling/app.tsx b/src/app/tool-calling/app.tsx index 0648bd5..ce1cde2 100644 --- a/src/app/tool-calling/app.tsx +++ b/src/app/tool-calling/app.tsx @@ -19,6 +19,7 @@ import React from "react"; import Chat from "@/components/chat"; import { Sun, Cloud, CloudRain, CloudSnow } from "lucide-react"; import Dice from "./dice-roll"; +import { withBasePath } from "@/lib/constants"; function WeatherResponse({ temperature, @@ -78,7 +79,7 @@ function DiceResponse({ output }: { output: number }) { export default function ToolCallingChatbotApp() { return ( { if (part.toolResponse?.name === "getWeather") return ; diff --git a/src/lib/constants.ts b/src/lib/constants.ts new file mode 100644 index 0000000..d7a5d84 --- /dev/null +++ b/src/lib/constants.ts @@ -0,0 +1,40 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Base path for the application - must match next.config.ts basePath + */ +export const BASE_PATH = "/examples"; + +/** + * Default base URL for the application (used in development) + * In production, SITE_ORIGIN env var should be set in apphosting.yaml + */ +export const DEFAULT_BASE_URL = `http://localhost:3000${BASE_PATH}`; + +/** + * Get the site origin (production or development) + */ +export const SITE_ORIGIN = process.env.SITE_ORIGIN || DEFAULT_BASE_URL; + +/** + * Helper to create paths with the basePath prefix + * Use this for any hardcoded paths that need the basePath added + */ +export function withBasePath(path: string): string { + return `${BASE_PATH}${path}`; +} + diff --git a/src/lib/demo-metadata.ts b/src/lib/demo-metadata.ts index c41ae63..9415c77 100644 --- a/src/lib/demo-metadata.ts +++ b/src/lib/demo-metadata.ts @@ -16,6 +16,7 @@ import { findDemo } from "@/data"; import { Metadata } from "next"; +import { SITE_ORIGIN } from "./constants"; export function demoMetadata(id: string): () => Promise { const demo = findDemo(id); @@ -26,9 +27,7 @@ export function demoMetadata(id: string): () => Promise { description: demo.description, openGraph: { images: [ - `${process.env.SITE_ORIGIN || "http://localhost:3000"}/api/og?title=${ - demo.name - }`, + `${SITE_ORIGIN}/api/og?title=${demo.name}`, ], description: demo.description, title: `Genkit by Example - ${demo.name}`, diff --git a/src/lib/image-loader.ts b/src/lib/image-loader.ts new file mode 100644 index 0000000..f2ce2df --- /dev/null +++ b/src/lib/image-loader.ts @@ -0,0 +1,24 @@ +/** + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * TODO: Ran into issues with builtin NextJS image optimization serving the wrong image after configuring custom basePath. + * Custom loader lets us control that but turns off the automatic optimziation. + */ +export default function imageLoader({ src }: { src: string }) { + return `/examples${src}`; +} +