De-SaaSifying Next.js: как мы обошли платный Design API и собрали бесплатный генератор стилевых гайдов

от автора

Рассказываем, как заменили 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, который недавно закрыл бесплатный доступ.

<!— cut —>

Открытый форк 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.

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

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

[ Target URL ]       │      ├───► [ Jina Reader (Free Markdown) ] ──────┐      └───► [ Microlink (Full HD Screenshot) ] ───┼─► [ Open DesignMD Backend ] ───► [ Local LLM / OpenRouter ] ───► [ DESIGN.md ]

Ключевое отличие от оригинала — 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:

open-designmd/├── app/│   ├── api/│   │   └── design-md/│   │       └── route.ts       ← основной API-роут│   ├── layout.tsx│   └── page.tsx├── components/├── lib/├── .env                       ← конфигурация провайдера├── .gitignore├── package.json└── install.bat                ← Windows-портативность

Портативность — отдельная тема. Для 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=openrouterAI_MODEL=deepseek/deepseek-chatOPENROUTER_API_KEY=your_key_here

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

AI_PROVIDER=ollamaAI_MODEL=llama3

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

Для Google AI Studio:

AI_PROVIDER=googleAI_MODEL=gemini-2.0-flashGOOGLE_API_KEY=your_key_here

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

# .gitignore​designmd-portable/.envnode_modules/.next/

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

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

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

// app/api/screenshot/route.tsconst 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 offREM 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.ziptar -xzf node.zipren 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.batpause

Скрипт скачивает портативный 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 за её сохранение.

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

ссылка на оригинал статьи https://habr.com/ru/articles/1042406/