JavaRush /Курси /ChatGPT Apps /Як влаштований шаблон: структура проєкту та ключові файли...

Як влаштований шаблон: структура проєкту та ключові файли

ChatGPT Apps
Рівень 2 , Лекція 1
Відкрита

1. Вступ

Проєкт ChatGPT App HelloWorld — це не «магічна чорна скринька від CodeGym, у якій краще нічого не чіпати». Це звичайний Next.js‑проєкт. У ньому просто одночасно співіснують:

  • фронтенд, який відображається всередині ChatGPT,
  • MCP‑сервер, що відповідає на виклики інструментів (tools),
  • налаштування, які поєднують усе це з ChatGPT.

Якщо не розуміти, де і що розташовано, зазвичай трапляються три класичні сценарії:

  1. Розробник випадково пише window у серверному файлі, отримує збій — і починає ненавидіти весь набір технологій.
  2. Намагається додати кнопку в UI, але редагує не той page.tsx (наприклад, корінь застосунку, а не віджет) і не бачить змін у ChatGPT.
  3. Випадково кладе OPENAI_API_KEY у клієнтську частину — і ключ «витікає» в браузер.

Тож сьогоднішня мета — побудувати мапу: де UI, де MCP, де конфіги та куди звертатися, коли ви хочете:

  • змінити зовнішній вигляд віджета;
  • додати новий tool;
  • налаштувати якесь платформне обмеження (CORS, assetPrefix тощо).

2. Високорівнева анатомія проєкту

Next.js‑проєкт ChatGPT App HelloWorld використовує App Router і організований навколо теки app/. Усередині цього дерева сторінок співіснують:

  • UI віджета, який відображається всередині ChatGPT,
  • MCP‑endpoint, що обробляє виклики інструментів.

Типове дерево (спрощене; назви тек у вашому шаблоні можуть відрізнятися, але підхід той самий):

my-chatgpt-app/
├─ app/
│  ├─ api/                          // REST API
│  │  └─ time/                      // GET /api/time повертає час на сервері
│  │     └─ route.ts
│  ├─ hooks/                        // Набір хуків з офіційного Apps SDK
│  │  ├─ use-call-tool.ts
│  │  ├─ use-display-mode.ts
│  │  └─ use-open-external.ts
│  ├─ mcp/                          // MCP-сервер: сюди стукає ChatGPT, коли викликає tools
│  │  └─ route.ts
│  ├─ globals.css                   // Кореневий globals.css усього застосунку
│  ├─ layout.tsx                    // Кореневий layout усього застосунку
│  └─ page.tsx                      // Сторінка віджета всередині ChatGPT
├─ public/                          // Статика: іконки, маніфест тощо
├─ next.config.ts                   // Конфіг Next.js і Apps-специфічні налаштування (assetPrefix тощо)
├─ proxy.ts                         // CORS/заголовки для роботи всередині iframe (колишній middleware.ts)
├─ package.json                     // Залежності проєкту
├─ tsconfig.json                    // Конфігурація TypeScript
└─ .env.local                       // Секрети: OPENAI_API_KEY тощо

Якщо віджетів кілька, то зазвичай їх кладуть не в app/page.tsx, а в app/widget/page.tsx. Але логіка від цього не змінюється: усе одно є одна сторінка‑віджет і один endpoint, який виконує роль MCP‑сервера.

Зручно мислити так: ваш репозиторій — це «дволикий Янус»:

  • одне «обличчя» — шлях /mcp, куди звертається ChatGPT, коли хоче викликати інструмент;
  • інше «обличчя» — шлях /widget (або /), який завантажується в iframe, коли модель вирішує показати ваш UI.

Щоб не плутатися, зафіксуймо три групи файлів:

  1. UI‑шар — усе, що повʼязано з React/Next‑сторінками (app/widget, компоненти, стилі).
  2. MCP‑шарapp/mcp/route.ts і файли, які він використовує.
  3. «Клейовий» шар і конфігиnext.config.ts, proxy.ts, .env.local, package.json, tsconfig.json.

Трохи нижче ми пройдемося по кожному з цих шарів.

3. Де «живе» віджет: тека app/widget та/або app/page.tsx

Почнімо з того, що ви змінюватимете найчастіше, — віджета, тобто UI, який видно всередині ChatGPT.

У більшості сучасних проєктів є або:

  • файл app/widget/page.tsx — віджет «живе» за окремим префіксом /widget,
  • або кореневий app/page.tsx — віджет збігається з кореневою сторінкою.

Головні ознаки файлу з віджетом:

  • угорі стоїть 'use client', тому що компонент працює в браузері, спілкується з window і Apps SDK;
  • це звичайний React‑компонент, який відображає розмітку і (трохи пізніше в курсі) взаємодіє з window.openai.

Найпростіший приклад навчального віджета (щось дуже схоже ви вже можете побачити у себе в проєкті):

// app/widget/page.tsx
'use client';

import React from 'react';

export default function WidgetPage() {
  return (
    <main className="p-4">
      <h1 className="text-xl font-semibold">
        HelloWorld — ChatGPT App
      </h1>
      <p className="text-sm text-gray-500">
        Тут ми будемо будувати UI нашого віджета.
      </p>
    </main>
  );
}

Якщо у вашому шаблоні віджет лежить прямо в app/page.tsx, код буде приблизно таким самим — лише без проміжної теки widget.

Зверніть увагу на кілька моментів.

По‑перше, директива 'use client' обовʼязкова. Віджет читає й записує дані в window.openai, слухає події тощо. А це можливо лише в клієнтському компоненті. Якщо її прибрати, Next.js спробує зробити сторінку серверною — і ви отримаєте помилки на кшталт «window is not defined».

По‑друге, це звичайний, зовсім не магічний React‑компонент. Ви можете:

  • розбивати його на підкомпоненти в components/,
  • використовувати Tailwind або будь‑яку іншу CSS‑систему,
  • підʼєднувати контексти, хуки тощо.

По‑третє, пізніше саме тут ви будете:

  • читати window.openai.toolInput і window.openai.toolOutput, щоб намалювати реальні дані,
  • зберігати widgetState через window.openai.setWidgetState,
  • викликати openExternal, callTool та інші методи рантайму.

Наразі достатньо знати одне: якщо ви хочете змінити вигляд інтерфейсу, вам майже напевно потрібно відкрити app/widget/page.tsx або app/page.tsx.

4. Кореневий layout: app/layout.tsx як «рамка» для всього застосунку

Наступний важливий файл — app/layout.tsx. Він:

  • визначає HTML‑структуру (<html>, <body>),
  • підʼєднує глобальні стилі (globals.css),
  • часто ініціалізує «bootstrap» для Apps SDK (обгортку, яка слухає window.openai і передає дані в React).

Спрощений приклад:

// app/layout.tsx
import './globals.css';
import type { ReactNode } from 'react';
import { OpenAIAppProvider } from '@/lib/openai-app-provider';

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head>
        <NextChatSDKBootstrap baseUrl={baseURL} />
      </head>
      <body className={`${geistSans.variable} ${geistMono.variable} antialiased h-full overflow-hidden`}>
        {children}
      </body>
    </html>
  );
}

Назва NextChatSDKBootstrap тут умовна: у вашому шаблоні це може бути OpenAIAppProvider або інший компонент. Його завдання зазвичай одне: налаштувати звʼязок між React‑деревом і рантаймом Apps SDK, підписатися на глобальні дані (theme, displayMode, toolInput тощо) і передати їх дочірнім компонентам.

Важливий практичний висновок: якщо вам потрібно підʼєднати глобальний контекст, стилі або UI‑бібліотеку (наприклад, shadcn/ui), місце для цього майже завжди app/layout.tsx (або layout усередині app/widget — для налаштувань і компонентів, специфічних саме для віджета).

Розбір NextChatSDKBootstrap

NextChatSDKBootstrap я бачив в офіційному шаблоні від Vercel. Це команда, яка створила Next і активно його розвиває. На їхньому сайті є гарний матеріал про ChatGPT App на Next. Також є Starter Template. Хоча в кількох місцях він уже трохи застарів, є всі шанси, що його й надалі підтримуватимуть актуальним.

Виділимо 5 ключових речей, які нам дає NextChatSDKBootstrap:

  • 1. Прибирає проблеми з гідратацією
    Річ у тім, що ChatGPT спочатку завантажує HTML вашого віджета на свій сервер, очищує й «патчить» його. У результаті механізм гідратації свариться і сипле попередження (warnings) у консоль. Це може завадити вам пройти ревʼю.
  • 2. «Патчить» історію браузера
    Річ у тім, що ваш віджет завантажується в iframe зі спеціального домену ChatGPT. Якщо ви використовуватимете власний домен, ви порушите пісочницю. Тому в історії браузера зберігається лише шлях — без домену.
  • 3. Переписує функцію fetch()
    У віджеті не працюватимуть запити fetch() на відносні адреси без домену, адже домен у iframe інший. Тому ми підміняємо функцію fetch() на свою — вона надсилає запити без домену на правильний URL. Якщо домен указано, усе працює без змін.
  • 4. Переходи за посиланнями працюють коректно
    Якщо посилання відкриватимуться всередині iframe, ChatGPT цього не схвалить. Тому додано код, який відстежує натискання на посилання і відкриває їх у зовнішньому вікні через openExternal().
  • 5. Додавання head base (DEPRECATED)
    Також цей код додавав <base> у <head>, але це вже не працює. Пісочниця скидає будь‑який встановлений base, тож раджу використовувати абсолютні посилання для всього, що є: скрипти, ресурси, шрифти, API тощо.

5. MCP‑сервер: app/mcp/route.ts

Тепер переходимо до другої половини «дволикого Януса» — сервера, який спілкується із ChatGPT через MCP.

Файл app/mcp/route.ts — це звичайний Route Handler для App Router, який:

  • приймає HTTP‑запити від ChatGPT (зазвичай POST із JSON‑payload у форматі MCP),
  • передає їх у MCP‑сервер (на базі @modelcontextprotocol/sdk або тонкої обгортки),
  • повертає назад JSON‑відповідь у форматі MCP.

Є два варіанти: можна писати на «чистому» MCP SDK, а можна згладити гострі кути й використати кілька класів від того ж Next/Vercel.

Ось варіант на чистому TS MCP SDK:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

// 1. Створюємо MCP-сервер
const server = new McpServer({
  name: "simple-mcp-server",
  version: "1.0.0",
});

// 2. Реєструємо MCP Resources
// 3. Реєструємо MCP Tools

// 4. HTTP транспорт
const transport = new HttpServerTransport({
  port: 3001,
  path: "/mcp",
});

// 5. Запуск сервера
await server.connect(transport);

Але зручніше взяти кілька готових класів — так працювати приємніше:

// app/mcp/route.ts
import { NextRequest } from 'next/server';
import { createMcpHandler } from "mcp-handler";

const handler = createMcpHandler(async (server) => {
  const gateway = new McpGateway(server);
  await gateway.initialize();
  gateway.registerResources();
  gateway.registerTools();
});

export const GET = handler;
export const POST = handler;

Тут McpGateway — це клас‑обгортка навколо McpServer, який ви десь створюєте (наприклад, у lib/mcp/server.ts) за допомогою SDK. У нашому випадку він повністю міститься в app/mcp/route.ts. Розберімо докладно, що саме є в цьому файлі.

type ContentWidget

На початку файлу в нас описаний тип ContentWidget. Він описує всі дані віджета і використовується у двох місцях: під час реєстрації віджета як mcp‑resource і коли mcp‑tool повертає metadata, де вказує, який віджет використати для відображення даних, які він повернув.

type ContentWidget = {
  id: string;            // Унікальне ім’я/key
  title: string;         // Title
  description: string;   // Description
  templateUri: string;   // Унікальний URI віджета, може бути будь-яким. Ні на що не впливає.
  invoking: string;      // Напис над віджетом, поки він завантажується
  invoked: string;       // Напис над віджетом, коли він уже завантажився
  html: string;          // Увесь html-код віджета.
  widgetDomain: string;  // «Домен» віджета. Ні на що не впливає.
};

class McpGateway

Це клас‑обгортка над McpServer, який спрощує деякі речі. Він має 6 методів:

  • initialize() — тут ми завантажуємо HTML нашого віджета;
  • registerResources() — реєструємо віджети як mcp‑resources;
  • registerTools() — реєструємо функції як mcp‑tools;
  • widgetMeta() — повертає метадані віджета;
  • getAppsSdkCompatibleHtml() — завантажує HTML‑код віджета й трохи «патчить» його;
  • makeImgUrlsAbsolute() — «патчить» HTML: змінює посилання на зображення на абсолютні.

Пройдімося по них докладніше:

public async initialize()

Цей метод завантажує з інтернету HTML‑код віджетів і заповнює обʼєкт типу ContentWidget.

{
  id: "hello_world",                         // Унікальний key віджета
  templateUri: "ui://widget/hello_world.html", // Унікальний URI віджета. "ui:" нічого не означає.
  title: "HelloWorld Widget",               // Ім’я віджета
  description: "Displays the HelloWorld widget", // Пояснення для LLM, що робить віджет
  invoking: "Loading widget...",            // Напис над віджетом під час його завантаження
  invoked: "Widget loaded",                 // Напис над віджетом після його завантаження
  html: htmlWidget,                         // HTML віджета
  widgetDomain: baseURL,                    // «Домен» віджета. Зараз ні на що не впливає.
}

public registerResources()

Реєструє віджети як mcp‑resources. Викликає метод server.registerResource(), у який передаються 4 параметри:

  • id (key) MCP‑ресурсу;
  • URI ресурсу (це потрібно саме для MCP‑протоколу; для віджета фактично синонім унікальної адреси);
  • метадані MCP‑ресурсу;
  • функція, що повертає MCP‑ресурс.

Метадані віджета

{
  title: widget.title,                 // Ім’я ресурсу/віджета
  description: widget.description,     // Опис ресурсу/віджета
  mimeType: "text/html+skybridge",     // Важливо! Лише такий html буде відображатися як віджет
  _meta: {
    "openai/widgetDescription": widget.description, // Опис віджета
    "openai/widgetPrefersBorder": true,            // Просимо ChatGPT відмалювати рамку віджета
  },
}

Віджет як MCP‑ресурс

{
  uri: uri.href,                        // Наш URI (береться з параметра uri)
  mimeType: "text/html+skybridge",      // Важливо! Лише такий html буде відображатися як віджет
  text: widget.html,                    // HTML віджета
  _meta: {
    "openai/widgetDescription": widget.description, // Опис віджета
    "openai/widgetPrefersBorder": true,            // Просимо ChatGPT відмалювати рамку віджета
    "openai/widgetDomain": widget.widgetDomain,    // «Домен» віджета. Зараз ні на що не впливає.
    "openai/widgetCSP": {                          // Важливо! Домени, доступні віджету:
      connect_domains: [                           // Домени для з’єднань (fetch тощо)
        baseURL,
        "https://codegym.cc",
      ],
      resource_domains: [                          // Домени для ресурсів (css/fonts/img)
        baseURL,
        "https://codegym.cc",
        "https://cdn.tailwindcss.com",
        "https://persistent.oaistatic.com",
        "https://fonts.googleapis.com",
        "https://fonts.gstatic.com"
      ]
    }
  },
}

У майбутньому ми ще не раз торкнемося openai/widgetCSP, але зараз варто відзначити два моменти:

  • connect_domains — список доменів для:
    • fetch()
    • завантаження скриптів
    • openExternal()
  • resource_domains — список доменів для:
    • зображень
    • CSS
    • шрифтів

Теоретично ви можете додати хоч 200 доменів. Але чи пройдете ви з таким списком ревʼю — питання відкрите.

Також я подивився ці параметри в уже опублікованих застосунках і знайшов там amplitude.com. Це теж хороша новина: якісна аналітика рідко буває зайвою.

public registerTools()

Реєструє функції як mcp‑tools. Викликає метод server.registerTool(), у який передаються 3 параметри:

  • id (key) MCP‑tool;
  • метадані MCP‑tool;
  • функція, що повертає MCP‑tool.

Метадані інструмента

Усі параметри в цьому списку важливі. Докладніше про них — у наступних лекціях.

{
  title: widget.title,                               // Ім’я інструмента
  description: "Returns HelloWorld widget",          // Важливо! Опис того, що робить інструмент
  inputSchema: z.object({}).describe("No inputs"),   // Схема параметрів інструмента. Можна Zod
  _meta: this.widgetMeta(widget),                    // Метадані віджета: який віджет відображати
  annotations: {
    destructiveHint: false,                          // Метод робить щось важливе — потрібен confirm
    openWorldHint: false,                            // Метод змінює щось у third party services
    readOnlyHint: true                               // Метод нічого не змінює
  },
}

Функція, яка робить щось важливе

async (input, extra) => {
  // 1. Валідація параметрів
  // 2. Робимо щось важливе
  return {
    content: [{ type: "text", text: "HelloWorld MCP-tool" }], // Опис результату для ІІ
    structuredContent: {                                      // Важливо! Це і є JSON результату.
      timestamp: new Date().toISOString()                     // Може містити будь-які дані.
    },
    _meta: this.widgetMeta(widget),                           // Метадані віджета, який відображає JSON
  };                                                          // Може бути відсутній — тоді віджета не буде
}

private widgetMeta(widget: ContentWidget)

Повертає метадані віджета — за ними ChatGPT зрозуміє, який віджет використати для відображення JSON‑результату.

{
  "openai/outputTemplate": widget.templateUri,            // URI віджета
  "openai/toolInvocation/invoking": widget.invoking,      // Напис над віджетом, поки він завантажується
  "openai/toolInvocation/invoked": widget.invoked,        // Напис над віджетом, коли він завантажився
  "openai/widgetAccessible": true,                        // MCP-tool можна викликати з віджета
  "openai/resultCanProduceWidget": true,                  // MCP-tool поверне віджет
}

Окремо варто обговорити таку просту річ, як "openai/outputTemplate". У MCP‑протоколі є 3 сутності (про які ви докладніше дізнаєтеся в модулі 6):

  • MCP Resources
  • MCP Templates
  • MCP Tools

Отже, цей "openai/outputTemplate" не має жодного стосунку до MCP Templates. MCP Templates узагалі ніяк не використовуються в ChatGPT Apps. Слово template тут узялося звідси:

Віджети задумувалися як шаблон для відображення JSON. MCP‑tool повернув певний JSON, ШІ показав віджет, передав йому JSON через параметр ToolOutput — і віджет гарно відобразив ці дані. outputTemplate — це просто синонім віджета.

Думаю, на цьому все. Докладніше ми розберемо ці речі в модулі 4: як саме описувати інструменти, JSON Schema і обробники. Наразі достатньо розуміти: якщо щось повʼязано з інструментами (tools) і логікою, шукайте поруч із app/mcp/route.ts.

6. Конфігурація і «клей»: next.config.ts, middleware.ts, .env та інші

Тепер розберемо основний набір файлів, потрібних для того, щоб ваш Next.js‑проєкт коректно працював усередині iframe ChatGPT і був доступний ChatGPT через HTTPS‑тунель (ngrok, Cloudflare Tunnel тощо; про тунелі ми ще поговоримо окремо).

next.config.ts

У цьому файлі, окрім стандартних налаштувань Next.js, часто задають:

  • assetPrefix — щоб статика (JS, CSS із /_next/) коректно завантажувалася не з домену ChatGPT, а з вашого dev‑URL (тунелю або Vercel);
  • будь‑які специфічні налаштування, потрібні шаблону (наприклад, експериментальні прапорці під Next 16).

На практиці це виглядає як звичайний експорт nextConfig з потрібними полями. Для лекції нам важливо одне: якщо в ChatGPT віджет не може завантажити CSS/JS, дуже часто винен саме assetPrefix.

proxy.ts (колишній middleware.ts)

Цей файл вставляє шар middleware між запитом із ChatGPT і вашими роутами. У шаблоні він зазвичай:

  • встановлює CORS‑заголовки, щоб iframe ChatGPT узагалі мав право звертатися до вашого сервера;
  • іноді налаштовує додаткові заголовки для React Server Components.

Знати всі тонкощі зараз не обовʼязково. Корисно лише памʼятати: якщо ChatGPT скаржиться на CORS або ви бачите в DevTools дивні помилки про заборону доступу, зазирніть у proxy.ts.

.env

Файл .env (або .env.local) — місце для секретів і змінних середовища:

  • OPENAI_API_KEY (якщо MCP‑сервер сам ходить в OpenAI API),
  • адреси ваших внутрішніх API,
  • токени сторонніх сервісів тощо.

Є важливий нюанс: у Next.js змінні, що починаються з NEXT_PUBLIC_, автоматично потрапляють у JS‑бандл і стають доступними в браузері. Ніколи не робіть так з OPENAI_API_KEY: секрети мають бути лише серверними змінними.

package.json і tsconfig.json

У package.json ви побачите:

  • версії Next.js, React, Apps SDK, MCP SDK та інших залежностей;
  • скрипти dev, build, start, а іноді й допоміжні команди (лінтер, форматер тощо).

У tsconfig.json лежать звичні для вас налаштування TypeScript:

  • шляхи аліасів (@/lib, @/components),
  • строгий режим,
  • таргети компіляції.

З погляду цього курсу головне — розуміти, що шаблон використовує звичайний TypeScript‑стек, і ви можете розширювати його стандартним чином.

7. Швидкий «навігатор по проєкту» для розробника

Зафіксуймо, куди йти, коли ви хочете зробити типові речі. Без списків — просто у вигляді міні‑сценаріїв.

Якщо вам хочеться змінити текст або кнопки у віджеті, відкрийте файл UI віджета: це або app/widget/page.tsx, або app/page.tsx — залежно від шаблону. Там ви редагуєте JSX, додаєте нові компоненти, підʼєднуєте дизайн‑систему. І саме тут ви використовуватимете рантайм Apps SDK (window.openai або зручні хуки) для відображення даних.

Якщо потрібно додати нову кнопку, яка щось робить на сервері, ви все одно починаєте з UI‑файла. Кнопка у віджеті на натискання викликатиме window.openai.callTool, а реалізацію цього інструмента ви додасте в конфігурацію MCP‑сервера — тобто в код поруч із app/mcp/route.ts. Звʼязок UI ↔ tool‑логіка якраз будемо розбирати в модулях 4 і далі.

Коли ви хочете навчити ChatGPT нового функціоналу (наприклад, «пошуку турів» або «підбору товарів»), ви йдете в MCP‑шар (файли, що імпортуються з app/mcp/route.ts). Там реєструєте новий tool із JSON Schema, описом і обробником. Віджет може потім читати результат через window.openai.toolOutput і гарно його відображати.

Якщо у вас «злетіла» статика або віджет дивно відображається лише в ChatGPT, а локально все гаразд, згадуємо про «клейовий» шар. Насамперед варто перевірити next.config.ts (особливо assetPrefix) і middleware.ts/proxy.ts (CORS). Якщо ви нещодавно змінювали тунель, URL або розгортали застосунок на Vercel, коректність цих налаштувань критична.

Нарешті, якщо ви підозрюєте проблеми з ключами або змінними середовища, ваша трійка файлів — .env.local, package.json (щоб упевнитися, які залежності та скрипти реально використовуються) і логи dev‑сервера. Саме ця звʼязка відповідає за те, щоб MCP мав доступ до потрібних секретів і сервісів.

8. Міні‑практика: знайомимося з файловою системою руками

Теорія — теорією, але давайте закріпимо все на практиці. Ці кроки можна виконати прямо зараз у редакторі/IDE.

Спробуйте відкрити у своєму проєкті теку app і знайти, який файл відповідає за віджет. Якщо шаблон використовує app/page.tsx, саме там ви побачите знайомий напис на кшталт «HelloWorld — ChatGPT App» або вітальний текст. Якщо окремої теки віджета немає, відкрийте app/page.tsx і переконайтеся, що там є 'use client' і якась JSX‑розмітка.

Далі знайдіть app/mcp/route.ts. Зверніть увагу, які модулі він імпортує: зазвичай ви побачите або пряме використання MCP SDK, або виклик допоміжної функції з lib/mcp/*. Оцініть, наскільки «тонким» зроблено цей прошарок: в ідеалі там майже немає бізнес‑логіки, лише «прийняв JSON → передав серверу → повернув JSON».

Після цього зазирніть у next.config.ts і proxy.ts/middleware.ts. Не потрібно розуміти все, що там написано. Просто зафіксуйте, що:

  • next.config.ts відповідає за конфігурацію Next, зокрема за правила збірки та віддачі асетів;
  • proxy.ts втручається в HTTP‑запити (майже напевно побачите там роботу із заголовками).

І насамкінець відкрийте .env або .env.local і переконайтеся, що ваші ключі лежать саме там, а не в коді. Якщо десь побачите NEXT_PUBLIC_OPENAI_API_KEY — це чудовий привід виправити, поки йдеться лише про локальну розробку.

9. Візуальна схема: як ChatGPT взаємодіє з вашим шаблоном

Щоб картина остаточно склалася, корисно подивитися на простий потік:

flowchart TD
    U[Користувач у ChatGPT] -->|Пише запит| M[Модель ChatGPT]

    M -->|Викликає tool| MCP["Ваш MCP endpoint
app/mcp/route.ts"] MCP -->|"JSON‑відповідь MCP (structuredContent, _meta, UI‑посилання)"| M M -->|Вирішує показати UI| WIDGET_URL["URL віджета
(/widget або /)"] WIDGET_URL -->|iframe| W[Ваш віджет
app/page.tsx] W -->|читає window.openai.toolOutput
+ widgetState| U

Тут важливо помітити, що ініціатором майже завжди є модель ChatGPT, а не браузер користувача, як у класичному веб‑застосунку. Ваш app/mcp/route.ts і app/widget/page.tsx — це просто двоє різних «дверей» в один і той самий Next.js‑проєкт: одна для «робота» (MCP), інша — для UI.

Якщо тримати в голові цю мапу проєкту (віджет → MCP‑шар → конфіги) і свідомо уникати перелічених «граблів», далі за курсом ви зможете зосередитися на логіці та UX свого App, а не на пошуках «того самого файлу, який усе ламає».

10. Типові помилки під час роботи зі структурою шаблону

Помилка № 1: Плутати віджет зі звичайною сторінкою сайту.
Іноді розробник бачить у шаблоні і app/page.tsx, і app/widget/page.tsx, редагує «не той» файл і дивується, чому зміни не зʼявляються в ChatGPT. Віджет — це саме та сторінка, яка використовується як outputTemplate/iframe для MCP‑інструмента. Якщо ви змінюєте інший роут, ChatGPT про це навіть не дізнається. Завжди звіряйтеся з README шаблону і дивіться, який URL указано як віджет.

Помилка № 2: Писати клієнтський код (window, document) у серверних файлах MCP.
Файл app/mcp/route.ts і все, що він імпортує, виконується на сервері. Будь‑яка спроба використати там window або DOM‑API призведе до падіння рантайму. Якщо хочеться щось зробити в UI, це майже напевно має бути у файлах під app/widget або в інших клієнтських компонентах. MCP‑шар — це чистий backend: запити, бази, зовнішні API і формування структурованої відповіді.

Помилка № 3: Ігнорувати assetPrefix і CORS‑налаштування.
На локальному localhost:3000 усе працює чудово, але варто відкрити App через тунель у ChatGPT — і стилі зникають, JS не завантажується, у консолі купа CORS‑помилок. Часто причина в тому, що конфігурація next.config.ts або middleware.ts/proxy.ts не враховує новий публічний URL або була випадково зламана під час рефакторингу. Змінюючи ці файли, завжди тримайте в голові, що ваш код житиме всередині iframe на домені ChatGPT, а не напряму на localhost.

Помилка № 4: Зберігати секрети не в .env, а прямо в коді або в NEXT_PUBLIC_*‑змінних.
Ховати OPENAI_API_KEY у const apiKey = 'sk-...' десь у app/widget/page.tsx — це найгірша ідея: ключ опиниться в JS‑бандлі й стане доступним будь‑якому користувачу. Майже так само погано — зробити змінну NEXT_PUBLIC_OPENAI_API_KEY, адже префікс NEXT_PUBLIC_ гарантує потрапляння в браузер. Завжди кладіть секрети в .env без цього префікса і використовуйте їх лише на серверному боці (MCP‑сервер, backend‑функції).

Помилка № 5: Вважати шаблон «занадто розумним» і боятися його змінювати.
Іноді розробники ставляться до офіційного стартера як до чогось «священного»: «краще туди не лізти, раптом зламаю інтеграцію». У результаті вони пишуть увесь свій код десь збоку, ускладнюють архітектуру і все одно наступають на ті самі граблі. Насправді шаблон — це лише акуратно зібраний Next.js‑проєкт із парою налаштувань під Apps SDK. Розуміння того, що app/ — це UI і MCP, а все інше — звичайні конфіги, дуже «звільняє»: ви починаєте працювати з кодом як зі звичним React/Next‑проєктом, а не як із магічною коробкою.

Помилка № 6: Намагатися вирішити всі проблеми «на рівні віджета».
Іноді хочеться робити все в UI: і бізнес‑логіку, і доступ до баз, і запити до зовнішніх API. У контексті ChatGPT Apps це особливо погана ідея: віджет живе в дуже жорсткій пісочниці, не бачить ваших секретів і сильно залежить від window.openai. Якщо потрібно щось серйозне — місце цьому в MCP‑шарі й backend‑сервісах. А віджет має бути тонким презентаційним шаром: він відображає структуровані дані і, за потреби, запускає інструменти.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ