# Project export: Textify

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: OpenAI Build Week
- Tagline: This is a program that will take the confusing parts of the textbook and "simplify" them based on how you would like to understand them: 5th grader tone, Gen Z slang, or cheat sheet (bullet points).
- Devpost: https://devpost.com/software/textbook-simplifier
- GitHub: https://github.com/giamenon4144/Textify
- Demo: https://textify-transform.giamenon4144.chatgpt.site/
- Video: https://www.youtube.com/embed/1xdpOchZqVQ?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — giamenon4144 (5 commits)

## Devpost submission (written by the team)

### Inspiration

When I read textbooks, I always get confused by parts where they use too many complicated words, so I made a program that helps any student understand the textbook of their choice.

### What it does

It simplifies the textbook into words that the user can better understand, the choices being: 5th-grade tone, Gen Z slang terms, and bullet points.

### How we built it

I built it by using Codex to help create the code.

### Challenges we ran into

I got a working app, but then I realized that the app just took text and didn't change it, just returned it as it was given.

### Accomplishments we're proud of

I'm proud of being able to create my first-ever app.

### What we learned

I learned how to utilize GitHub and Codex and how it can be used to benefit me in certain situations.

### What's next

More tones will be provided, allowing more variation for people to select.

## README (from the GitHub repository)

# vinext-starter

A clean full-stack starter running on
[vinext](https://github.com/cloudflare/vinext), with optional Cloudflare D1 and
Drizzle support.

## Prerequisites

- Node.js `>=22.13.0`

## Quick Start

```bash
npm install
npm run dev
npm run build
```

This starter does not use `wrangler.jsonc`.

## Included Shape

- edit site code under `app/`
- `.openai/hosting.json` declares optional Sites D1 and R2 bindings
- `vite.config.ts` simulates declared bindings for local development
- `db/schema.ts` starts intentionally empty
- `examples/d1/` contains an optional D1 example surface
- `drizzle.config.ts` supports local migration generation when needed

## Workspace Auth Headers

OpenAI workspace sites can read the current user's email from
`oai-authenticated-user-email`.

SIWC-authenticated workspace sites may also receive
`oai-authenticated-user-full-name` when the user's SIWC profile has a non-empty
`name` claim. The full-name value is percent-encoded UTF-8 and is accompanied by
`oai-authenticated-user-full-name-encoding: percent-encoded-utf-8`.

Treat the full name as optional and fall back to email when it is absent:

```tsx
import { headers } from "next/headers";

export default async function Home() {
  const requestHeaders = await headers();
  const email = requestHeaders.get("oai-authenticated-user-email");
  const encodedFullName = requestHeaders.get("oai-authenticated-user-full-name");
  const fullName =
    encodedFullName &&
    requestHeaders.get("oai-authenticated-user-full-name-encoding") ===
      "percent-encoded-utf-8"
      ? decodeURIComponent(encodedFullName)
      : null;

  const displayName = fullName ?? email;
  // ...
}
```

## Optional Dispatch-Owned ChatGPT Sign-In

Import the ready-to-use helpers from `app/chatgpt-auth.ts` when the site needs
optional or required ChatGPT sign-in:

- Use `getChatGPTUser()` for optional signed-in UI.
- Use `requireChatGPTUser(returnTo)` for server-rendered pages that should send
  anonymous visitors through Sign in with ChatGPT.
- Use `chatGPTSignInPath(returnTo)` and `chatGPTSignOutPath(returnTo)` for
  browser links or actions.
- Pass a same-origin relative `returnTo` path for the destination after sign-in
  or sign-out. The helper validates and safely encodes it.
- Mark protected pages with `export const dynamic = "force-dynamic"` because
  they depend on per-request identity headers.

Dispatch owns `/signin-with-chatgpt`, `/signout-with-chatgpt`, `/callback`, the
OAuth cookies, and identity header injection. Do not implement app routes for
those reserved paths. Routes that do not import and call the helper remain
anonymous-compatible.

SIWC establishes identity only; it does not prove workspace membership. Use the
Sites hosting platform's access policy controls for workspace-wide restrictions,
or enforce explicit server-side membership or allowlist checks.

Use SIWC for account pages, user-specific dashboards, saved records, and write
actions tied to the current ChatGPT user. Leave public content anonymous.

## Useful Commands

- `npm run dev`: start local development
- `npm run build`: verify the vinext build output
- `npm test`: build the starter and verify its rendered loading skeleton
- `npm run db:generate`: generate Drizzle migrations after schema changes

## Learn More

- [vinext Documentation](https://github.com/cloudflare/vinext)
- [Drizzle D1 Guide](https://orm.drizzle.team/docs/get-started/d1-new)


## Detected evidence (automated analysis)

Indexed codebase: 15 recognized source files, 40 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- Next.js (technology) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- JavaScript (language) — claimed on Devpost, not found in the code
- OpenAI (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (24 of 24)

```
.gitignore
.openai/hosting.json
app/api/transform/route.ts
app/chatgpt-auth.ts
app/globals.css
app/layout.tsx
app/page.tsx
db/index.ts
db/schema.ts
docs/.nojekyll
docs/index.html
drizzle.config.ts
drizzle/meta/_journal.json
eslint.config.mjs
examples/d1/app/api/notes/route.ts
examples/d1/db/schema.ts
next.config.ts
package.json
postcss.config.mjs
README.md
tests/rendered-html.test.mjs
tsconfig.json
vite.config.ts
worker/index.ts
```

### Dependencies

- package.json: @cloudflare/vite-plugin@1.37.1, @tailwindcss/postcss@4.2.1, @types/node@22.19.19, @types/react@19.2.14, @types/react-dom@19.2.3, @vitejs/plugin-react@6.0.2, @vitejs/plugin-rsc@0.5.26, drizzle-kit@0.31.10, drizzle-orm@0.45.2, eslint@9.39.4, eslint-config-next@16.2.6, next@16.2.6, react@19.2.6, react-dom@19.2.6, react-server-dom-webpack@19.2.6, tailwindcss@4.2.1, typescript@5.9.3, vinext@0.0.50, vite@8.0.13, wrangler@4.92.0

### Recent commits (newest first)

- Add GitHub Pages configuration
- Add Devpost project page
- Remove GitHub Pages configuration
- Remove GitHub Pages landing page
- Add GitHub Pages landing page
- Add AI-powered content transformations
- Rewrite passages for each audience
- Build Textify transformation app

## Key source files (fetched from GitHub, selected and truncated for size)

### package.json

```
{
  "name": "textify",
  "version": "0.1.0",
  "private": true,
  "engines": {
    "node": ">=22.13.0"
  },
  "scripts": {
    "dev": "WRANGLER_LOG_PATH=.wrangler/wrangler.log vinext dev",
    "build": "WRANGLER_LOG_PATH=.wrangler/wrangler.log vinext build",
    "start": "WRANGLER_LOG_PATH=.wrangler/wrangler.log vinext start",
    "test": "npm run build && node --test tests/rendered-html.test.mjs",
    "lint": "eslint . --ignore-pattern dist --ignore-pattern .next",
    "db:generate": "drizzle-kit generate"
  },
  "dependencies": {
    "drizzle-orm": "0.45.2",
    "next": "16.2.6",
    "react": "19.2.6",
    "react-dom": "19.2.6"
  },
  "devDependencies": {
    "@cloudflare/vite-plugin": "1.37.1",
    "@tailwindcss/postcss": "4.2.1",
    "@types/node": "22.19.19",
    "@types/react": "19.2.14",
    "@types/react-dom": "19.2.3",
    "@vitejs/plugin-react": "6.0.2",
    "@vitejs/plugin-rsc": "0.5.26",
    "drizzle-kit": "0.31.10",
    "eslint": "9.39.4",
    "eslint-config-next": "16.2.6",
    "react-server-dom-webpack": "19.2.6",
    "tailwindcss": "4.2.1",
    "typescript": "5.9.3",
    "vinext": "0.0.50",
    "vite": "8.0.13",
    "wrangler": "4.92.0"
  },
  "type": "module"
}

```

### db/index.ts

```typescript
import { env } from "cloudflare:workers";
import { drizzle } from "drizzle-orm/d1";
import * as schema from "./schema";

export function getDb() {
  if (!env.DB) {
    throw new Error(
      "Cloudflare D1 binding `DB` is unavailable. Set the `d1` field in .openai/hosting.json to `DB` or let your control plane inject the real binding values before using the database."
    );
  }

  return drizzle(env.DB, { schema });
}

```

### app/layout.tsx

```typescript
import type { Metadata } from "next";
import "./globals.css";

export const metadata: Metadata = {
  title: "Textify — Same facts. Better fit.",
  description:
    "Transform one summary into Gen Z, simple, and cheat-sheet versions without changing the facts.",
  openGraph: {
    title: "Textify — Same facts. Better fit.",
    description:
      "Transform one summary into three audience-ready versions.",
    images: [{ url: "/og.jpg", width: 1200, height: 630, alt: "Textify" }],
  },
  twitter: {
    card: "summary_large_image",
    title: "Textify — Same facts. Better fit.",
    description:
      "Transform one summary into three audience-ready versions.",
    images: ["/og.jpg"],
  },
  icons: {
    icon: "/favicon.svg",
    shortcut: "/favicon.svg",
  },
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

```

### worker/index.ts

```typescript
/** Cloudflare Worker entry point for the vinext-starter template. */
import { handleImageOptimization, DEFAULT_DEVICE_SIZES, DEFAULT_IMAGE_SIZES } from "vinext/server/image-optimization";
import handler from "vinext/server/app-router-entry";

interface Env {
  ASSETS: Fetcher;
  DB: D1Database;
  IMAGES: {
    input(stream: ReadableStream): {
      transform(options: Record<string, unknown>): {
        output(options: { format: string; quality: number }): Promise<{ response(): Response }>;
      };
    };
  };
}

interface ExecutionContext {
  waitUntil(promise: Promise<unknown>): void;
  passThroughOnException(): void;
}

// Image security config. SVG sources with .svg extension auto-skip the
// optimization endpoint on the client side (served directly, no proxy).
// To route SVGs through the optimizer (with security headers), set
// dangerouslyAllowSVG: true in next.config.js and uncomment below:
// const imageConfig: ImageConfig = { dangerouslyAllowSVG: true };

const worker = {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url);

    if (url.pathname === "/_vinext/image") {
      const allowedWidths = [...DEFAULT_DEVICE_SIZES, ...DEFAULT_IMAGE_SIZES];
      return handleImageOptimization(request, {
        fetchAsset: (path) => env.ASSETS.fetch(new Request(new URL(path, request.url))),
        transformImage: async (body, { width, format, quality }) => {
          const result = await env.IMAGES.input(body).transform(width > 0 ? { width } : {}).output({ format, quality });
          return result.response();
        },
      }, allowedWidths);
    }

    return handler.fetch(request, env, ctx);
  },
};

export default worker;

```

### app/page.tsx

```typescript
"use client";

import { useMemo, useState } from "react";

type Tab = "genz" | "simple" | "cheatsheet";

const starter =
  "Photosynthesis is the process plants use to turn sunlight, water, and carbon dioxide into glucose and oxygen. It mainly happens in the leaves, inside structures called chloroplasts. Chlorophyll absorbs sunlight and gives plants their green color. The glucose stores energy for the plant, while oxygen is released into the air.";

const replacements: Array<[RegExp, string]> = [
  [/\baccording to\b/gi, "as said by"],
  [/\badditional(?:ly)?\b/gi, "more"],
  [/\bapproximately\b/gi, "about"],
  [/\bcommence(?:d|s|ment)?\b/gi, "start"],
  [/\bconsequently\b/gi, "so"],
  [/\bconsiderable\b/gi, "large"],
  [/\bconstitutes?\b/gi, "makes up"],
  [/\bdemonstrate(?:d|s)?\b/gi, "show"],
  [/\bdespite the fact that\b/gi, "even though"],
  [/\bdue to the fact that\b/gi, "because"],
  [/\bfacilitate(?:d|s)?\b/gi, "help"],
  [/\bfundamental\b/gi, "basic"],
  [/\bhowever\b/gi, "but"],
  [/\bimplement(?:ed|s|ing)?\b/gi, "put in place"],
  [/\bin addition\b/gi, "also"],
  [/\bin order to\b/gi, "to"],
  [/\bindicate(?:d|s)?\b/gi, "show"],
  [/\bindividuals\b/gi, "people"],
  [/\binitial(?:ly)?\b/gi, "first"],
  [/\b(?:modify|modified|modification)\b/gi, "change"],
  [/\bnumerous\b/gi, "many"],
  [/\bobtain(?:ed|s)?\b/gi, "get"],
  [/\boccur(?:red|s|ring)?\b/gi, "happen"],
  [/\bparticipate(?:d|s)?\b/gi, "take part"],
  [/\bportion\b/gi, "part"],
  [/\bpossess(?:ed|es)?\b/gi, "have"],
  [/\bprocess\b/gi, "way"],
  [/\bprevious(?:ly)?\b/gi, "before"],
  [/\bprimarily\b/gi, "mostly"],
  [/\bprovide(?:d|s)?\b/gi, "give"],
  [/\bpurchase(?:d|s)?\b/gi, "buy"],
  [/\bregarding\b/gi, "about"],
  [/\brequire(?:d|s)?\b/gi, "need"],
  [/\bretain(?:ed|s)?\b/gi, "keep"],
  [/\bsignificant(?:ly)?\b/gi, "important"],
  [/\bsubsequent(?:ly)?\b/gi, "later"],
  [/\bsufficient\b/gi, "enough"],
  [/\btherefore\b/gi, "so"],
  [/\btransmit(?:ted|s)?\b/gi, "send"],
  [/\butilize(?:d|s)?\b/gi, "use"],
  [/\bwith the exception of\b/gi, "except"],
  [/\bstructures\b/gi, "parts"],
  [/\babsorbs?\b/gi, "takes in"],
  [/\breleased\b/gi, "let out"],
];

function sentences(value: string) {
  return (
    value
      .trim()
      .replace(/\s+/g, " ")
      .match(/[^.!?]+[.!?]+|[^.!?]+$/g)
      ?.map((line) => line.trim())
      .filter(Boolean) ?? []
  );
}

function simplifyWords(value: string) {
  let result = value;
  replacements.forEach(([pattern, replacement]) => {
    result = result.replace(pattern, replacement);
  });
  return result
    .replace(/\b(is|are|was|were) able to\b/gi, "can")
    .replace(/\bhas the ability to\b/gi, "can")
    .replace(/\ba large number of\b/gi, "many")
    .replace(/\bat this point in time\b/gi, "now")
    .replace(/\bfor the purpose of\b/gi, "to")
    .replace(/\s+/g, " ")
    .trim();
}

function splitIdeas(items: string[]) {
  return items
    .flatMap((item) =>
      item
        .replace(/\(([^)]+)\)/g, ", meaning $1,")
        .split(/;\s*|,\s+(?=(?:but|while|whereas|which|and|so|because)\b)/i),
    )
    .map((idea) => simplifyWords(idea).trim())
    .filter(Boolean)
    .map((idea) => {
      const cleaned = idea
        .replace(/^(however|therefore|additionally),?\s*/i, "")
        .replace(/^which\s+/i, "This ")
        .replace(/^whereas\s+/i, "But ")
        .replace(/^while\s+/i, "At the same time, ")
        .replace(/^and\s+/i, "Also, ")
        .replace(/^but\s+/i, "But ")
        .replace(/^so\s+/i, "So ");
      return cleaned.charAt(0).toUpperCase() + cleaned.slice(1).replace(/[.!?]*$/, ".");
    });
}

function shorten(idea: string, maxWords: number) {
  const words = idea.split(/\s+/);
  if (words.length <= maxWords) return idea;
  const pivot = words.findIndex(
    (word, index) =>
      index > 5 && /^(and|but|because|while|which|that)$/i.test(word.replace(/[,.]/g, "")),
  );
  if (pivot > 0) {
    return `${words.slice(0, pivot).join(" ").replace(/[,;]$/, "")}. ${words
      .slice(pivot)
      .join(" ")
      .replace(/^(and|which|that)\s+/i, "This ")}`;
  }
  return idea;
}

function makeGenZ(items: string[]) {
  const ideas = splitIdeas(items).map((idea) => shorten(idea, 22));
  return [
    "Okay, here’s what’s really going on 👀",
    "",
    ...ideas.flatMap((idea, index) => [
      index === 0 ? `The big idea: ${idea}` : index === 1 ? `Here’s the key part: ${idea}` : idea,
      "",
    ]),
    "That’s the breakdown. Same meaning, way less textbook energy. ✨",
  ]
    .join("\n")
    .replace(/\n{3,}/g, "\n\n")
    .trim();
}

function makeSimple(items: string[]) {
  const ideas = splitIdeas(items)
    .map((idea) => shorten(idea, 14))
    .flatMap((idea) => idea.match(/[^.!?]+[.!?]+|[^.!?]+$/g) ?? [idea])
    .map((idea) => idea.trim())
    .filter(Boolean);
  return ["Let’s make this easy.", ...ideas, "That is the big idea."].join(" ");
}

function cleanTerm(sentence: string) {
  const firstPhrase = sentence
    .replace(/[.!?]/g, "")
    .split(/\b(?:is|are|was|were|can|has|have|means|uses|helps|shows)\b/i)[0]
    .trim();
  return firstPhrase.split(/\s+/).slice(0, 3).join(" ") || "Key fact";
}

function makeCheatsheet(items: string[]) {
  const ideas = splitIdeas(items);
  const facts = ideas.slice(0, 7).map((idea) => {
    const term = cleanTerm(idea);
    const detail = shorten(idea, 18).replace(new RegExp(`^${term}\\s*`, "i"), "").trim();
    return `- **${term}** — ${detail || idea}`;
  });
  return [`TL;DR: ${shorten(ideas[0] ?? "Add a summary to see the key idea.", 18)}`, "", ...facts].join(
    "\n",
  );
}

function countWords(value: string) {
  return value.trim() ? value.trim().split(/\s+/).length : 0;
}

export default function Home() {
  const [summary, setSummary] = useState(starter);
  const [activeTab, setActiveTab] = useState<Tab>("genz");
  const [outputs, setOutputs] = useState<Record<Tab, string>>(() => {
    const items = sentences(starter);
    return {
      genz: makeGenZ(items),
      simple: makeSimple(items),
    
[truncated — 5975 more characters]
```

### app/api/transform/route.ts

```typescript
import { NextResponse } from "next/server";

const SYSTEM_PROMPT = `You are the transformation engine for Textify.
Rewrite the user's entire summary three times. Preserve every key fact and do not invent facts.
Treat the supplied summary as the only source of truth. Do not use outside knowledge, even when it is correct.
Every factual claim in every version must be directly traceable to the supplied summary.
You may simplify a technical term only from context already present in the summary.
If the summary does not define a term, keep its name and explain only its stated role.

GENZ:
- Rebuild the passage from scratch for a smart Gen Z audience.
- Split long ideas into punchy, natural lines.
- Replace academic wording with current everyday language.
- Explain necessary technical terms only with information stated in the summary.
- Use light, natural slang only when it helps. Never sound like a parody.
- 80–150 words with caption-style line breaks.

SIMPLE:
- Rebuild the passage so a very young child can understand it.
- Use tiny sentences, common words, and one idea per sentence.
- Replace hard wording with plain wording. Use a comparison only when it restates a relationship already present in the summary.
- Keep a warm, gentle voice.
- 60–100 words.

CHEATSHEET:
- Turn the passage into exam-revision notes, not a shortened paragraph.
- Begin with one line starting "TL;DR:".
- Follow with 5–8 one-line markdown bullets.
- Bold the key term or number in every bullet.
- Include definitions, relationships, causes, effects, steps, or contrasts that matter for recall.
- Make every line useful for active recall before an exam.

Return only the requested structured fields.`;

const schema = {
  type: "object",
  additionalProperties: false,
  properties: {
    genz: { type: "string" },
    simple: { type: "string" },
    cheatsheet: { type: "string" },
  },
  required: ["genz", "simple", "cheatsheet"],
};

export async function POST(request: Request) {
  const apiKey = process.env.OPENAI_API_KEY;
  if (!apiKey) {
    return NextResponse.json(
      { error: "Textify’s language engine is not configured yet." },
      { status: 503 },
    );
  }

  let summary = "";
  try {
    const body = (await request.json()) as { summary?: unknown };
    summary = typeof body.summary === "string" ? body.summary.trim() : "";
  } catch {
    return NextResponse.json({ error: "Please send a valid summary." }, { status: 400 });
  }

  if (!summary) {
    return NextResponse.json({ error: "Please add a summary first." }, { status: 400 });
  }
  if (summary.length > 12_000) {
    return NextResponse.json(
      { error: "That summary is too long. Please keep it under 12,000 characters." },
      { status: 400 },
    );
  }

  const response = await fetch("https://api.openai.com/v1/responses", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      model: "gpt-5.6",
      instructions: SYSTEM_PROMPT,
      input: summary,
      text: {
        format: {
          type: "json_schema",
          name: "textify_transformations",
          strict: true,
          schema,
        },
      },
    }),
  });

  const data = (await response.json()) as {
    error?: { message?: string };
    output?: Array<{
      type?: string;
      content?: Array<{ type?: string; text?: string; refusal?: string }>;
    }>;
  };

  if (!response.ok) {
    console.error("OpenAI transformation failed", response.status, data.error?.message);
    if (response.status === 429) {
      return NextResponse.json(
        {
          error:
            "The OpenAI account has no available API quota. Add billing or credits, then try again.",
        },
        { status: 429 },
      );
    }
    return NextResponse.json(
      { error: "The rewrite could not be completed. Please try again." },
      { status: 502 },
    );
  }

  const content = data.output
    ?.find((item) => item.type === "message")
    ?.content?.find((item) => item.type === "output_text");

  if (!content?.text) {
    const refusal = data.output
      ?.flatMap((item) => item.content ?? [])
      .find((item) => item.type === "refusal")?.refusal;
    return NextResponse.json(
      { error: refusal ?? "The rewrite did not return usable text." },
      { status: 422 },
    );
  }

  try {
    return NextResponse.json(JSON.parse(content.text));
  } catch {
    return NextResponse.json(
      { error: "The rewrite returned an unexpected format. Please try again." },
      { status: 502 },
    );
  }
}

```

### examples/d1/app/api/notes/route.ts

```typescript
import { desc } from "drizzle-orm";
import { getDb } from "../../../../../db";
import { notes } from "../../../db/schema";

function toRouteErrorMessage(error: unknown) {
  const message = error instanceof Error ? error.message : "Unexpected error";
  const detail =
    error instanceof Error && error.cause instanceof Error ? error.cause.message : "";
  const combined = `${message}\n${detail}`;

  if (combined.includes("no such table") || combined.includes('from "notes"')) {
    return "The notes table is unavailable. Generate the migration locally with `npm run db:generate`, then deploy so the platform can apply the generated SQL to the real D1 database.";
  }

  return message;
}

export async function GET() {
  try {
    const db = getDb();
    const rows = await db
      .select()
      .from(notes)
      .orderBy(desc(notes.createdAt), desc(notes.id))
      .limit(20);

    return Response.json({ notes: rows });
  } catch (error) {
    return Response.json(
      { error: toRouteErrorMessage(error) },
      { status: 500 }
    );
  }
}

export async function POST(request: Request) {
  try {
    const payload = (await request.json()) as {
      title?: string;
      content?: string;
    };
    const title = payload.title?.trim() ?? "";
    const content = payload.content?.trim() ?? "";

    if (!title) {
      return Response.json({ error: "title is required" }, { status: 400 });
    }

    const db = getDb();
    const [note] = await db.insert(notes).values({ title, content }).returning();
    return Response.json({ note }, { status: 201 });
  } catch (error) {
    return Response.json(
      { error: toRouteErrorMessage(error) },
      { status: 500 }
    );
  }
}

```

### next.config.ts

```typescript
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  /* config options here */
};

export default nextConfig;

```

### drizzle.config.ts

```typescript
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  out: "./drizzle",
  schema: "./db/schema.ts",
  dialect: "sqlite",
});

```

### vite.config.ts

```typescript
import vinext from "vinext";
import { defineConfig } from "vite";
import hostingConfig from "./.openai/hosting.json";
import { sites } from "./build/sites-vite-plugin";

const SITE_CREATOR_PLACEHOLDER_DATABASE_ID =
  "00000000-0000-4000-8000-000000000000";

const { d1, r2 } = hostingConfig;

// macOS Seatbelt blocks FSEvents, so Codex previews need polling for HMR.
const isCodexSeatbeltSandbox = process.env.CODEX_SANDBOX === "seatbelt";

const localBindingConfig = {
  main: "./worker/index.ts",
  compatibility_flags: ["nodejs_compat"],
  d1_databases: d1
    ? [
        {
          binding: d1,
          database_name: "site-creator-d1",
          database_id: SITE_CREATOR_PLACEHOLDER_DATABASE_ID,
        },
      ]
    : [],
  r2_buckets: r2
    ? [
        {
          binding: r2,
          bucket_name: "site-creator-r2",
        },
      ]
    : [],
};

export default defineConfig(async () => {
  // Keep Wrangler and Miniflare state project-local. These are non-secret tool
  // settings; application environment belongs in ignored `.env*` files.
  process.env.WRANGLER_WRITE_LOGS ??= "false";
  process.env.WRANGLER_LOG_PATH ??= ".wrangler/logs";
  process.env.MINIFLARE_REGISTRY_PATH ??= ".wrangler/registry";

  // Wrangler snapshots its log path while the Cloudflare plugin is imported.
  const { cloudflare } = await import("@cloudflare/vite-plugin");

  return {
    server: isCodexSeatbeltSandbox
      ? { watch: { useFsEvents: false, usePolling: true } }
      : undefined,
    plugins: [
      vinext(),
      sites(),
      cloudflare({
        viteEnvironment: { name: "rsc", childEnvironments: ["ssr"] },
        config: localBindingConfig,
      }),
    ],
  };
});

```

[5 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]