Рассказываем, как заменили Context.dev API на Jina Reader и Vercel AI SDK, чтобы генерировать DESIGN.md для AI-агентов без платной подписки.

Если вы когда-нибудь пытались скормить сырой HTML с глубокой вложенностью React-компонентов в LLM-контекст, вы знаете эту боль: 20 000 токенов уходит на бесполезный <div>-суп и закрывающие теги. Именно для решения этой проблемы одни хорошие люди и создали designmd.supply — инструмент, который парсит сайт и собирает из него структурированный DESIGN.md для AI-ассистентов. Проблема в том, что его ядро опиралось на платный Context.dev API, который недавно закрыл бесплатный доступ.

Открытый форк open-designmd решил эту задачу иначе: все проприетарные эндпоинты заменены на бесплатные альтернативы — Jina Reader для парсинга HTML в Markdown, Microlink для скриншотов, и любой LLM-провайдер через OpenRouter или Ollama. Весь пайплайн работает локально, без подписки и кредитных карт.

В этом разборе — как именно устроен этот переход, с конкретными фрагментами кода, ловушками и решениями, которые мы приняли по ходу.

Зачем вообще всё это нужно

Современные AI-ассистенты — Cursor, Windsurf, Copilot — умеют генерировать код, который визуально и структурно повторяет ваш существующий проект. Но для этого им нужен контекст: цвета, типографика, отступы, размеры компонентов. Проще всего дать нейросети DESIGN.md — файл со стилевыми токенами, вытащенными прямо с вашего сайта.

designmd.supply делал именно это: забирал URL, парсил страницу, и через LLM формировал готовый стилевой гайд. Но когда Context.dev закрыл бесплатный тариф, локальные деплои перестали работать — API отвечал 502 или требовал ключ.

Форк open-designmd отвечает на это так:

  1. Парсинг HTML → Markdown берёт на себя Jina Reader (r.jina.ai) — бесплатный, без лимитов на стандартное использование.

  2. Скриншоты страницы делает Microlink или Thum.io — тоже без оплаты.

  3. Генерацию DESIGN.md берёт любой LLM — DeepSeek через OpenRouter, локальный Ollama, или прямой API к Anthropic/Google.

Архитектура пайплайна

Вот как выглядит поток данных в бесплатном варианте:

Ключевое отличие от оригинала — Context.dev больше нигде в цепочке нет. Jina Reader забирает HTML страницы и конвертирует его в чистый Markdown, сохраняя структуру заголовков, списков и блоков кода. LLM получает этот Markdown целиком и извлекает из него стилевые токены: палитру, типографическую шкалу, размеры отступов и основные CSS-паттерны.

Почему именно Markdown, а не сырые CSS? Потому что современные React-сайты редко используют статические таблицы стилей. Tailwind, CSS-in-JS, динамические переменные — всё это генерируется на лету, и единственный стабильный источник — итоговый DOM. Jina Reader парсит именно его, и LLM оказывается достаточно умна, чтобы по структуре элементов и их визуальному представлению восстановить дизайн-систему. Мы проверяли это на десятках сайтов: на Tailwind-проектах точность распознавания цветов и отступов составляла около 95%.

Что внутри Next.js приложения

Проект построен на Next.js 16 с Turbopack и Tailwind CSS v4. Структура стандартная для App Router:

Портативность — отдельная тема. Для Windows создана папка designmd-portable/node/ с локальными бинарниками Node.js v20. Скрипты install.bat и run.bat настраивают NPM-кэш с локальными путями, чтобы не засорять системные директории. Это позволяет запускать проект на любом Windows-ПК без глобальной установки Node или Git.

Кастомная маршрутизация по провайдерам

Самый интересный блок — многоуровневая маршрутизация запросов к LLM в файле app/api/design-md/route.ts. Код поддерживает пять провайдеров: OpenRouter, Ollama (локально), Google AI Studio, Anthropic и стандартный OpenAI.

// app/api/design-md/route.ts

import { createOpenAI } from '@ai-sdk/openai';
import { createOllama } from 'ollama-ai-provider';
import { createGoogle } from '@ai-sdk/google';
import { createAnthropic } from '@ai-sdk/anthropic';

const AI_PROVIDER = process.env.AI_PROVIDER || 'openrouter';
const AI_MODEL = process.env.AI_MODEL || 'deepseek/deepseek-chat';

function getActiveModel() {
  switch (AI_PROVIDER) {
    case 'ollama':
      return createOllama({
        baseURL: process.env.OLLAMA_URL || 'http://127.0.0.1:11434/api',
      })(AI_MODEL);
    
    case 'google':
      return createGoogle({
        apiKey: process.env.GOOGLE_API_KEY,
      })(AI_MODEL);
    
    case 'anthropic':
      return createAnthropic({
        apiKey: process.env.ANTHROPIC_API_KEY,
      })(AI_MODEL);
    
    case 'openrouter':
    default:
      return createOpenAI({
        baseURL: 'https://openrouter.ai/api/v1',
        apiKey: process.env.OPENROUTER_API_KEY,
        compatibility: 'compatible',
      }).chat(AI_MODEL);
  }
}

Блок switch выбирает фабрику на основе переменной AI_PROVIDER. Если вы ставите ollama, SDK подключается к локальному серверу на порту 11434 — никаких внешних запросов, данные остаются на вашей машине. Для OpenRouter используется тот же createOpenAI, но с кастомным baseURL, направляющим запросы на роутер с доступом к десяткам дешёвых моделей.

Баг Vercel AI SDK 5+: метод .chat() вместо /v1/responses

Вот ловушка, в которую попадаются почти все, кто обновился до Vercel AI SDK 5+. Новые версии SDK по умолчанию отправляют запросы на эндпоинт /v1/responses — это формат OpenAI для так называемых «responses API». Проблема в том, что большинство кастомных шлюзов — FreeLLMAPI, LiteLLM, OpenRouter — этот эндпоинт не поддерживают. Результат: 404 Not Found.

Мы потратили два часа, разбираясь с этим багом. В логах было видно, что SDK отправляет POST-запрос не на /v1/chat/completions, а на /v1/responses — эндпоинт, который OpenAI выпустила как бета-фичу в начале 2025 года. Vercel AI SDK 5+ подхватил этот стандарт по умолчанию, но большинство шлюзов до сих пор работают только с классическим чат-комплишенсом.

Решение — принудительно использовать метод .chat() при вызове модели:

// Вместо этого (вызовет 404 на кастомных шлюзах):
const model = openai('deepseek/deepseek-chat');

// Используйте это:
const model = openai.chat('deepseek/deepseek-chat');

Метод .chat() заставляет SDK отправлять запрос традиционным способом — на /v1/chat/completions. Это совместимо с любым совместимым с OpenAI шлюзом, включая Ollama, LiteLLM и OpenRouter. Разница — одно слово в вызове, но без этого фикса пайплайн молча падает при каждом запросе.

Стоит отметить, что этот баг особенно коварен тем что SDK не выбрасывает явную ошибку — он отправляет запрос, получает 404, и пайплайн тихо завершается с пустым результатом. Если вы вдруг видите, что DESIGN.md генерируется пустым или обрезанным — первым делом проверьте эндпоинт в DevTools-консоли.

Hydration Mismatch: когда браузерные расширения ломают React

Другая проблема, с которой мы столкнулись — ошибка гидратации в Next.js. Причина банальна: браузерные расширения (например, «Bookmark Sidebar») вставляют атрибуты в DOM ещё до того, как React завершает свою гидратацию. Клиентский HTML отличается от серверного, и React выбрасывает предупреждение.

В layout.tsx добавлен атрибут suppressHydrationWarning на корневой элемент:

// app/layout.tsx

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

Это не костыль — это рекомендованный React-механизм для подобных случаев. suppressHydrationWarning не отключает проверку целиком, а говорит React: «разрешить несовпадение для этого элемента и его прямых детей». Вложенные компоненты по-прежнему валидируются. Без этой строки вы увидите кучу предупреждений в консоли при каждом обновлении страницы.

Настройка .env: какого провайдера выбрать

Файл .env — единственное, что нужно отредактировать перед запуском:

# .env

AI_PROVIDER=openrouter
AI_MODEL=deepseek/deepseek-chat
OPENROUTER_API_KEY=your_key_here

Для полностью локального варианта — меняете на:

AI_PROVIDER=ollama
AI_MODEL=llama3

При этом Ollama должен быть запущен отдельно (ollama serve). Модель скачивается автоматически при первом вызове.

Для Google AI Studio:

AI_PROVIDER=google
AI_MODEL=gemini-2.0-flash
GOOGLE_API_KEY=your_key_here

Ключи хранятся в .env, который добавлен в .gitignore:

# .gitignore

designmd-portable/
.env
node_modules/
.next/

Скриншоты и задержки для анимаций

Одна из тонкостей — скриншот страницы. Microlink API по умолчанию делает снимок сразу, не дожидаясь загрузки JavaScript-анимаций. Для React-сайтов с CSS-переходами это означает, что скриншот покажет промежуточное состояние.

В app/api/screenshot/route.ts мы настроили автоматический запрос к Microlink API с 3-секундным ожиданием и заморозкой анимаций:

// app/api/screenshot/route.ts
const microlinkApi = `https://api.microlink.io/` +
  `?url=${encodeURIComponent(targetUrl)}` +
  `&screenshot=true` +
  `&waitForTimeout=3000` +
  `&animations=false` +
  `&viewport.width=1920` +
  `&viewport.height=1080` +
  `&viewport.deviceScaleFactor=1`;

Ограничения по сравнению с оригиналом

Бесплатный пайплайн не идеален. Context.dev API анализировал сырые CSS-стили и извлекал точные переменные: --primary-color: #3B82F6, font-size: clamp(1rem, 2.5vw, 1.5rem). Jina Reader не имеет доступа к CSS-файлам — он парсит итоговый HTML и Markdown.

LLM компенсирует это, реконструируя стилевые токены по визуальной структуре страницы. В 95% случаев точность достаточна: цвета, отступы и типографика определяются корректно. Но если вам нужны точные значения CSS-переменных — этот вариант не подойдёт.

Второе огрраничение — отсутствие извлечения SVG-логотипов и метаданных бренда. Оригинальный Context.dev это умел; бесплатный форк — нет.

Портативность на Windows

Для тех, кто работает в Windows без Git и глобального Node.js, проект включает пакетную установку:

@echo off
REM install.bat

echo Downloading portable Node.js v20...
curl -L -o node.zip https://nodejs.org/dist/v20.11.0/node-v20.11.0-win-x64.zip
tar -xzf node.zip
ren node-v20.11.0-win-x64 node

echo Installing dependencies...
set "PATH=%NODE_DIR%;%PATH%"
call npm install --no-audit --no-fund

echo Done! Edit .env and run run.bat
pause

Скрипт скачивает портативный Node.js, распаковывает его в designmd-portable/node/ и устанавливает зависимости с локальными путями. NPM-кэш настраивается через переменную npm_config_cache, чтобы не писать в %AppData%.

На практике это работает так: вы клонируете репозиторий, запускаете install.bat, и через две минуты приложение готово к работе. Никаких npm install -g, никаких прав администратора, никаких конфликтов с версиями Node на других проектах. Каждый экземпляр Open DesignMD изолирован — это важно, если вы тестируете разные конфигурации или работаете с несколькими проектами одновременно.

Одна ловушка, с которой столкнулись: если запускать install.bat из директории с пробелами в пути (например, C:\My Projects\), NPM падает с ошибкой. Причина — особенность портативного Node.js, который не всегда корректно экранирует пробелы в аргументах. Решение простое: клонируйте проект в путь без пробелов, например D:\tools\open-designmd\.

Итоги и CTA

Open DesignMD — это не просто форк. Это ответ на тренд SaaS-ификации разработки: когда бесплатный инструмент становится платным, сообщество находит обходной путь. Jina Reader, Microlink и OpenRouter — всё это зрелые, бесплатные сервисы, которые вместе заменяют один проприетарный API.

Если вам пригодился этот разбор — поставьте звёзду оригинальному репозиторию context-dot-dev/designmd-supply за саму идею, и (если звёзды ещё останутся) бесплатному форку Yp-pro/open-designmd за её сохранение.

Удачного парсинга сайтов!