1. Qué es una prueba de humo para una ChatGPT App
En el mundo habitual del desarrollo web, una prueba de humo es la verificación mínima de «¿el sistema está vivo?». La página se abre, los botones no se caen, nada crítico arde.
En el mundo de las ChatGPT Apps la prueba de humo es un poco más interesante, porque en la cadena participan varios eslabones a la vez:
- Tu código del widget (React/Next.js).
- Servidor de desarrollo de Next.js.
- Túnel (ngrok/Cloudflare).
- ChatGPT, que crea un iframe y carga tu widget dentro del chat.
Para nosotros, una buena prueba de humo es la situación en la que:
- el widget se renderiza dentro de ChatGPT sin errores;
- la interactividad básica funciona (por ejemplo, pulsas un botón y se abre un enlace externo);
- ni en la consola del navegador ni en los logs del servidor de desarrollo hay una avalancha roja de errores.
Importante: en esta etapa aún no probamos las herramientas MCP, no hacemos pruebas de carga y no contamos dinero por tokens. Nuestra tarea es modesta y muy práctica: demostrar que la cadena «código → Next.js → túnel → ChatGPT → usuario» realmente se cierra.
Es útil imaginarlo como una tabla:
| Qué comprobamos | Cómo saber que todo está bien |
|---|---|
| Renderizado del widget | En ChatGPT se ve nuestra UI, no un «iframe roto» |
| Conexión entre ChatGPT ↔ nuestro servidor | No hay errores como «no se puede cargar la aplicación» |
| Ejecución de JS en la sandbox | Los manejadores onClick realmente se ejecutan |
| Posibilidad de abrir un enlace externo | El botón abre una pestaña/ventana nueva con la URL indicada |
2. Nuestra app de aprendizaje: un sencillo «Hello GiftGenius»
En este curso vamos construyendo la aplicación GiftGenius — un asistente para elegir regalos. En este paso aún no elige nada, pero ya puede al menos saludar educadamente y mostrar un enlace de «más información».
Necesitamos un widget mínimo pero honesto: sin lógica compleja, pero con código React real.
La versión más simple del componente del widget puede verse así (el nombre y los estilos puedes adaptarlos a tu gusto, pero tomemos la base del plan del curso):
// app/widget/page.tsx
'use client';
export default function GiftGeniusWidget() {
return (
<main style={{ padding: 16, fontFamily: 'system-ui, sans-serif' }}>
<h1 style={{ fontSize: 24, marginBottom: 8 }}>
Hello from GiftGenius
</h1>
<p style={{ marginBottom: 16 }}>
Este es tu primer ChatGPT App. Más adelante le enseñaremos a elegir regalos.
</p>
</main>
);
}
Un par de puntos importantes.
Primero, la directiva 'use client'; al principio del archivo hace que el componente sea del lado del cliente. Sin ella, Next.js trata el archivo como un componente del lado del servidor y no podrás usar window, manejadores onClick ni ninguna API del navegador.
En segundo lugar, es un componente React normal. No se ve ninguna «magia del Apps SDK», y eso es honesto. Toda la magia de que acabe dentro de ChatGPT está oculta en la configuración del servidor MCP y de la herramienta que devuelve el enlace al URL del widget. Eso lo veremos más adelante; ahora solo nos interesa la UI.
3. Integrar el widget en la plantilla y lanzarlo
En la plantilla oficial de Next.js para Apps SDK la página del widget suele existir ya; o bien la editas, o creas la tuya bajo la ruta necesaria (por ejemplo, /widget).
Supongamos que tienes app/widget/page.tsx y sustituyes su contenido por el código anterior. A partir de ahí, la cadena se ve así:
- Guardas el archivo.
- El servidor de desarrollo de Next.js (ya lanzado con npm run dev) reinicia los módulos necesarios y HMR actualiza la página.
- A través del túnel, tu URL público HTTPS por la misma ruta /widget empieza a servir la UI actualizada.
Puedes comprobarlo de dos maneras.
Primero, a la antigua: en el navegador local. Abres:
http://localhost:3000/widget
y ves el mismo Hello from GiftGenius. Sí, aún no es ChatGPT; simplemente te aseguras de que la UI de tu aplicación Next.js está viva.
Luego, a través del túnel. Tomas la URL proporcionada (algo como https://witty-cat.ngrok-free.app), añades /widget y la abres en un navegador normal:
https://witty-cat.ngrok-free.app/widget
Si todo va bien, la página debería verse igual. Significa que la cadena «Next.js → túnel → tu navegador» funciona; solo falta insertar ChatGPT entre ellos.
4. Comprobar el widget dentro de ChatGPT
En Dev Mode, ChatGPT básicamente realiza tres pasos: crea un iframe, pone en él el src con tu URL público y deja vivir ese iframe dentro del mensaje del chat.
Simplificado, el flujo se ve así:
sequenceDiagram
participant Dev as Tú (Dev)
participant Next as Servidor de desarrollo de Next.js
participant Tun as Túnel (HTTPS)
participant GPT as ChatGPT
participant User as Usuario
Dev->>Next: npm run dev (http://localhost:3000)
Dev->>Tun: Iniciar el túnel hacia el puerto 3000
GPT->>Tun: GET https://.../widget
Tun->>Next: Proxy hacia http://localhost:3000/widget
Next-->>Tun: HTML + JS del widget
Tun-->>GPT: Respuesta con HTML/JS
GPT->>User: Renderizar iframe con el widget
Para ver el resultado, debes:
- Abrir ChatGPT en el navegador y elegir el modelo necesario (normalmente GPT‑5.1 o lo que esté por defecto para Dev Mode).
- Seleccionar explícitamente tu aplicación (mediante el menú Apps/Developer) o «invocarla» con una frase como: «Inicia la aplicación GiftGenius».
- ChatGPT invoca tu App, el servidor MCP devuelve una respuesta que incluye un enlace a la UI (ese /widget), y en el mensaje del chat aparece tu widget.
Si todo va bien, verás el conocido encabezado «Hello from GiftGenius» directamente dentro de ChatGPT. En este punto la prueba de humo está casi superada: el iframe se renderiza y la cadena «Next.js → túnel → ChatGPT» está viva. Queda comprobar el último punto de nuestra tabla: que el widget puede abrir un enlace externo de forma predecible. Para ello necesitaremos openExternal.
Un poco más adelante, cuando empieces a cambiar el código, el ciclo de desarrollo normal tendrá este aspecto:
- Cambias el JSX.
- Guardas.
- O bien recargas la pestaña de ChatGPT o (a veces) basta con «mover» el widget — por ejemplo, enviar un mensaje nuevo o lanzar de nuevo la App (según cómo esté configurada tu plantilla y el caché).
Si no ves los cambios, piensa primero en tres sospechosos: el servidor de desarrollo no está arrancado, el túnel se ha caído o ChatGPT está conectado a una URL antigua. En la sección «Dónde buscar errores si algo no ha ido bien» veremos este escenario con más detalle.
5. Por qué no basta con poner <a href> y olvidarse
Para cumplir el último punto de nuestra prueba de humo — un botón que abre una página externa — tendremos que entender openExternal. La pregunta lógica: «¿Y para qué sirve openExternal? ¿Qué impide usar un enlace normal?»
El problema es que tu widget no vive «simplemente en el navegador», sino en un iframe bajo el control de ChatGPT. Ese iframe funciona en una sandbox bastante estricta: pueden aplicarse restricciones de Content Security Policy, atributos sandbox, rarezas con target="_blank" y bloqueo de ventanas emergentes. Como resultado, el comportamiento de <ahref="…"> o de window.open() dentro de ese iframe puede resultar impredecible: desde ignorarlo por completo hasta avisos emergentes no controlados por tu código.
Además, desde el punto de vista de UX, OpenAI quiere controlar cuándo y cómo abres páginas externas. Por eso Apps SDK proporciona un puente unificado window.openai: tu código no toca directamente la ventana padre, sino que delega la acción a la aplicación anfitriona mediante una API bien definida.
6. API window.openai.openExternal: qué es y cómo funciona
En la sandbox del widget está disponible el objeto global window.openai. Es el «puente» principal entre tu UI y ChatGPT: con él puedes invocar herramientas, enviar mensajes de seguimiento, cambiar el modo de visualización, gestionar el estado del widget y, por supuesto, abrir enlaces externos.
En esta lección nos interesa un método concreto:
window.openai.openExternal({ href: string }): void;
Cuando llamas a window.openai.openExternal({ href: 'https://example.com' }), ChatGPT:
- Verifica que la URL esté permitida por las políticas.
- Puede mostrar al usuario un aviso (por ejemplo, que es un sitio externo).
- Abre el enlace en una pestaña/ventana nueva del navegador del usuario.
Hay dos cosas importantes que entender.
En primer lugar, es una operación puramente del lado del cliente. No invoca herramientas MCP, no toca tu backend y no gasta tokens de OpenAI. Es simplemente una señal a la aplicación anfitriona: «por favor, abre esta URL».
En segundo lugar, este método es compatible con la sandbox. ChatGPT decide por sí mismo cómo abrir el enlace, sin permitir que tu iframe se exceda con window.open().
7. Añadir un botón con openExternal a nuestro widget
Aprendamos a abrir un enlace externo desde nuestro «Hello GiftGenius». El escenario más simple: un botón «Abrir enlace de demostración», que lleve, por ejemplo, a la documentación o al landing de tu servicio.
Para empezar, escribamos un pequeño helper, para que TypeScript no se queje y para que el widget no se rompa si abres /widget directamente en el navegador (donde aún no existe window.openai):
// app/widget/openExternalSafe.ts
export function openExternalSafe(href: string) {
if (typeof window !== 'undefined' && (window as any).openai?.openExternal) {
(window as any).openai.openExternal({ href });
} else {
// Fallback para la visualización local sin ChatGPT
window.open(href, '_blank', 'noopener,noreferrer');
}
}
Aquí uso intencionadamente (window as any) para no cargarte con la tipificación de window.openai. Más adelante en el curso describiremos con cuidado la interfaz de este objeto. Por ahora nos basta con que el código compile y funcione.
Ahora conectemos el helper en nuestro widget y añadamos el botón:
// app/widget/page.tsx
'use client';
import { openExternalSafe } from './openExternalSafe';
export default function GiftGeniusWidget() {
return (
<main style={{ padding: 16, fontFamily: 'system-ui, sans-serif' }}>
<h1 style={{ fontSize: 24, marginBottom: 8 }}>
Hello from GiftGenius
</h1>
<p style={{ marginBottom: 16 }}>
Este es tu primer ChatGPT App. Más adelante le enseñaremos a elegir regalos.
</p>
<button
type="button"
onClick={() => openExternalSafe('https://example.com')}
style={{
padding: '8px 16px',
borderRadius: 8,
border: '1px solid #ccc',
cursor: 'pointer',
}}
>
Abrir enlace de demostración
</button>
</main>
);
}
Qué ocurrirá al hacer clic.
Si el widget se ejecuta dentro de ChatGPT, window.openai.openExternal existe y ChatGPT abrirá https://example.com como corresponda según sus reglas.
Si has abierto http://localhost:3000/widget en un navegador normal, window.openai no existe y se activará el fallback: se abrirá una pestaña nueva con los medios habituales del navegador. Aquí window.open se usa solo cuando abres directamente /widget en un navegador normal, es decir, ya fuera de la sandbox de ChatGPT. En ese contexto funciona con normalidad y no crea problemas.
Veremos openExternal con más detalle en el módulo 3 (lección aparte sobre el widget y la sandbox), así que ahora puedes pasar sin miedo a lanzar la aplicación.
8. Mini prueba de humo end‑to‑end
Ahora podemos hacer una pasada «de combate» completa. Intentemos recorrer todos los pasos:
- Asegúrate de que el servidor de desarrollo está en marcha (npm run dev) y de que ves Hello from GiftGenius en http://localhost:3000/widget.
- Asegúrate de que el túnel hacia el puerto 3000 está levantado y que la URL pública se abre desde un navegador externo.
- Abre ChatGPT, activa Dev Mode y comprueba que tu App está conectada a la URL correcta (pública, no localhost).
- Abre un chat, elige la App (o pide al modelo que la inicie).
- Comprueba que en el widget incrustado se ve «Hello from GiftGenius».
- Pulsa el botón «Abrir enlace de demostración» y confirma que se abrió en el navegador https://example.com (o tu dirección).
Si todo eso ha funcionado, significa que:
- El HTML/JS del widget se compila correctamente y lo sirve el servidor de Next.
- El túnel HTTPS proxifica las solicitudes correctamente.
- ChatGPT confía en tu URL y sabe cargar el widget.
- window.openai funciona y transmite el comando para abrir el enlace externo.
Eso es exactamente lo que queríamos de la primera prueba de humo.
9. Dónde buscar errores si algo no ha ido bien
A diferencia del front‑end «habitual», aquí tienes solo tres lugares principales para diagnosticar. Es importante entender rápidamente en cuál de ellos se ha roto todo:
- Primero mira la UI en ChatGPT. Si en lugar del widget ves un mensaje de error como «Error loading app» o «We had trouble talking to your app», probablemente el problema esté en el túnel o en la disponibilidad de tu servidor de desarrollo. Intenta abrir la URL pública directamente en el navegador: si no se abre o se abre con un error de Next.js, soluciona eso primero.
- Luego abre las DevTools del navegador en la pestaña donde funciona ChatGPT. Hay un iframe separado para tu widget y, dentro de él, la conocida pestaña Console. Si al hacer clic en el botón con openExternal no pasa nada, mira si hay errores del tipo «window.openai is undefined» u otros errores de JS. Si existe ese error, probablemente estás probando el widget fuera de ChatGPT (directamente por la URL del túnel) o te has olvidado de la directiva 'use client';.
- En paralelo, mira el terminal con npm run dev. Si ahí caen errores de build (TypeScript, ESLint, compilación), en el mejor de los casos ChatGPT verá una versión antigua del código, y en el peor no verá nada. Si no hay errores pero no ves actualizaciones, asegúrate de que el túnel sigue activo: muchos servicios de túneles cierran sesiones por timeout de inactividad.
Hay otro caso típico: todo funciona en localhost, pero al acceder por el túnel obtienes 404 o una página extraña. Entonces revisa con atención la ruta base (/widget vs /), los ajustes de basePath/assetPrefix (si ya los tocaste) y la dirección indicada en Dev Mode.
10. Un poco de «limpieza»: detener procesos
Es un detalle, pero muy útil en la práctica. Los principiantes a menudo olvidan que tanto el servidor de desarrollo como el túnel son procesos separados que siguen vivos en segundo plano.
Si de repente «el puerto 3000 ya está en uso», quizá haya un viejo npm run dev escondido en alguna terminal. En Windows a veces esto se convierte en un pequeño baile con el Administrador de tareas; en macOS y Linux te salva Ctrl + C en la terminal donde el proceso está lanzado.
Lo mismo con el túnel: si has experimentado con varios túneles seguidos o te olvidaste de cerrar el antiguo, es fácil confundirse sobre a qué URL está apuntada tu App en Dev Mode ahora mismo. Mejor crear el hábito: cuando vayas a terminar la sesión, desconecta el túnel, detén el servidor de desarrollo y, en el próximo arranque, empieza desde cero.
11. Errores típicos en la primera prueba de humo
Error n.º 1: usar localhost en lugar de una URL pública HTTPS.
Una historia común: en Dev Mode indicas por accidente http://localhost:3000 o directamente te olvidas del túnel. En tu máquina todo funciona, pero ChatGPT, que vive en la nube, físicamente no puede alcanzar localhost. El remedio es simple: comprueba que en la configuración de la App figure la dirección pública HTTPS del túnel, y con la ruta correcta (/mcp o la raíz, según la plantilla).
Error n.º 2: olvidar la directiva 'use client'; en el archivo del widget.
Escribes un bonito código React, añades onClick, accedes a window.openai, y Next.js silenciosamente convierte la página en un componente del lado del servidor. En el mejor de los casos obtendrás un error «window is not defined», y en el peor el componente ni siquiera se compilará. Para tener acceso a las APIs del navegador, el widget debe ser un componente del lado del cliente, y de eso se encarga la primera línea 'use client';.
Error n.º 3: llamar directamente a window.open() en lugar de openExternal.
A veces parece más fácil hacer window.open('https://example.com'). En un navegador normal puede funcionar, pero dentro de la sandbox de ChatGPT tendrás un comportamiento impredecible: desde ignorarlo completamente hasta bloquearlo. El camino correcto para las ChatGPT Apps es window.openai.openExternal({ href }), que delega la apertura del enlace al host y respeta todas las políticas de seguridad.
Error n.º 4: TypeScript se queja de window.openai, y el desarrollador lo «cura» desactivando los tipos.
A veces, en un arrebato, la gente escribe // @ts-nocheck al principio del archivo. Eso elimina los errores de compilación, pero a la vez desactiva TypeScript por completo en ese archivo. Es mucho más seguro usar un as any puntual alrededor de window, o describir en un archivo aparte la interfaz mínima para window.openai. En este módulo elegimos un pequeño helper openExternalSafe con (window as any), y más tarde añadiremos la tipificación adecuada.
Error n.º 5: ver el resultado solo en localhost, pero no dentro de ChatGPT.
Puede resultar tentador conformarse con que http://localhost:3000/widget se abre y dar la tarea por resuelta. Pero el sentido de este módulo es precisamente ver la App dentro de ChatGPT. Que en un navegador normal todo vaya bien no garantiza que ChatGPT cree correctamente el iframe, obtenga los recursos a través del túnel y no choque con CORS/CSP. Una prueba de humo completa siempre incluye el paso de ejecutar la App en la interfaz real de ChatGPT.
Error n.º 6: túnel olvidado o caído.
Has actualizado el código, pero en ChatGPT sigue colgada una versión antigua del widget o directamente no se carga nada. A menudo resulta que el túnel se cerró por timeout, pero Developer Mode sigue apuntando a la URL antigua. Si al abrir la URL del túnel en un navegador normal ves un error, primero restaura el túnel y solo después eches la culpa al Apps SDK.
Error n.º 7: ignorar la consola del iframe.
Los desarrolladores acostumbrados a SPA miran el console.log en las DevTools de su aplicación, pero dentro de ChatGPT esto es un iframe y hay que elegir el frame correcto en las DevTools. Si miras solo el nivel superior, puede que no veas ni un error, aunque dentro del widget todo esté rojo desde hace rato. El hábito de «abrir las DevTools precisamente en el iframe del widget» ahorra muchos nervios.
GO TO FULL VERSION