Введение

Разработка браузерного расширения началась с простой идеи: упростить поиск по закладкам и открытым вкладкам, а также попробовать свои силы в создании проекта с элементами монетизации. В этой статье я расскажу, как за две недели прошёл путь от прототипа до публикации в Chrome Web Store и Firefox Add-ons, какие технологии использовал и с какими трудностями столкнулся. Надеюсь, мой опыт вдохновит других разработчиков попробовать свои силы в создании подобных проектов. Полный код проекта доступен на GitHub.


Подготовка и исследование

Изучение рынка

Прежде чем приступить к разработке, я изучил доступные ресурсы, чтобы понять процесс создания и публикации Chrome-расширений. Информации оказалось не так много, как ожидалось, но я нашёл официальный кабинет разработчика Chrome Web Store [1]. Оказалось, что для публикации расширения необходимо зарегистрироваться в качестве разработчика и оплатить единовременный взнос в размере 5 долларов. Этот процесс оказался небольшим квестом, так как требовалось подобрать подходящий способ оплаты, но я решил отложить его до завершения разработки, чтобы сначала сосредоточиться на создании прототипа.

Также я изучил документацию WebExtensions API [2], которая лежит в основе разработки расширений для Chrome и других браузеров. Это позволило мне понять основные требования к структуре расширения, включая работу с манифестом и доступ к API браузера, таким как chrome.bookmarks для поиска по закладкам. Исследование подтвердило, что идея создания поиска по закладкам технически реализуема и не требует сложной инфраструктуры.

Первый прототип

Для быстрого старта я выбрал фреймворк Quasar [3], с которым уже работал на одном из текущих проектов. Quasar позволил быстро создать прототип с минималистичным интерфейсом: поле ввода для поискового запроса и динамический вывод результатов по мере ввода. Прототип выглядел просто, но работал корректно, что дало уверенность в жизнеспособности идеи. Однако я заметил, что интерфейсу не хватает изюминки — визуальной привлекательности и дополнительного функционала, который сделал бы расширение более удобным.

Дизайн и расширение функционала

Чтобы улучшить пользовательский опыт, я приступил к проработке дизайна. Имея опыт прохождения курсов по UI/UX-дизайну, я создал макет интерфейса, который был бы удобным и эстетичным. В процессе работы я понял, что поиск только по закладкам ограничивает возможности расширения. Глядя на свой браузер с множеством открытых вкладок, я решил добавить поиск по вкладкам — функцию, которая могла бы решить проблему навигации по десяткам открытых страниц. У меня, как и у многих пользователей, часто накапливается множество вкладок: увидел интересную статью, открыл её, но времени прочитать нет, и вкладка остаётся открытой на неопределённый срок. Найти что-то среди них становится сложно, несмотря на возможность группировки или закрепления вкладок в браузере. Эта идея показалась перспективной, так как решает реальную проблему повседневной работы.

На основе нового видения я обновил дизайн, добавив поддержку поиска по вкладкам, и приступил к реализации. Для этого потребовалось изучить API браузера для работы с вкладками (chrome.tabs) [2], что стало следующим шагом в разработке.

[1] Chrome Web Store Developer Dashboard, https://developer.chrome.com/webstore
[2] WebExtensions API, https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions
[3] Quasar Framework, https://quasar.dev/

Монетизация

Давно хотелось попробовать реализовать проект с элементами монетизации, который приносил бы пользу пользователям и, возможно, небольшой доход, даже если он будет символическим. Такой подход мог бы вдохновить других разработчиков, которые хотят создать собственный продукт, но не знают, с чего начать. В процессе работы над FindMyLink я понял, что функционал поиска по вкладкам идеально подходит для монетизации: поиск по закладкам можно оставить бесплатным, а поиск по вкладкам сделать доступным по подписке. Это позволило бы выделить премиум-функцию, сохранив базовый функционал для всех пользователей.

Вопрос реализации монетизации оказался ключевым. Стало понятно, что для взаимодействия бота с расширением необходима авторизация, чтобы отслеживать, кто оплатил подписку, а кто нет. После изучения различных платёжных систем, таких как Stripe, я остановился на Telegram Payments API [4] из-за его простоты и популярности среди пользователей. Telegram Payments API позволяет создавать инвойсы через бота, что идеально подходило для небольшого проекта. Пользователь оплачивает подписку через Telegram, а бот подтверждает платёж и активирует доступ к премиум-функциям. Для управления подписками была разработана серверная часть, включающая Telegram-бота и API, что описано в следующих разделах.

[4] Telegram Payments API, https://core.telegram.org/bots/payments

Бэкенд: Telegram-бот для платежей

Выбор технологии

Для реализации монетизации через Telegram Payments API [4] я решил использовать Telegram-бота, который обрабатывает инвойсы и платежи. После изучения различных платёжных систем, таких как Stripe, я выбрал Telegram из-за его простоты интеграции и отсутствия необходимости в сложной инфраструктуре. Для базы данных я остановился на SQLite с использованием библиотеки aiosqlite и SQLAlchemy[asyncio] [5], так как проект небольшой, и SQLite обеспечивает достаточную производительность для хранения данных о пользователях и подписках. Для кэширования использовался KeyDB [6], совместимый с Redis, но более быстрый и лёгкий в настройке. Бот реализован с использованием библиотеки python-telegram-bot [7], которая поддерживает асинхронные операции и упрощает работу с Telegram API. Для управления конфигурацией применялись библиотеки msgspec [8] и config-lib-msgspec [9], последняя из которых была разработана коллегой и позволила удобно загружать настройки из YAML, переменных окружения или аргументов командной строки.

Технологический стек бота:

  • Framework: python-telegram-bot (асинхронный) для взаимодействия с Telegram API.

  • База данных: SQLite + aiosqlite + SQLAlchemy[asyncio] для хранения данных.

  • Кэш: KeyDB (Redis-совместимый) для быстрого доступа к данным.

  • Конфигурация: msgspec + config-lib-msgspec для гибкого управления настройками.

  • Платежи: Telegram Payments API для обработки подписок.

Конфигурация с config-lib-msgspec

# bot/src/configs/config.py
from typing import List
from config_lib.base import BaseConfig
from msgspec import Struct, field

class TelegramConfig(Struct):
    bot_token: str | None = None
    payment_provider_token: str | None = None
    admin_ids: List[int] = 0,

class DatabaseConfig(Struct):
    path: str | None = None
    default_lang: str | None = None
    default_trial_used: bool = False
    default_auto_renewal: bool = True

class BotConfig(BaseConfig):
    telegram: TelegramConfig = field(default_factory=TelegramConfig)
    database: DatabaseConfig = field(default_factory=DatabaseConfig)
    redis: RedisConfig = field(default_factory=RedisConfig)
    rate_limit: RateLimitConfig = field(default_factory=RateLimitConfig)
    subscription: SubscriptionConfig = field(default_factory=SubscriptionConfig)
    logging: LoggingConfig = field(default_factory=LoggingConfig)

# Загружаем конфиг из YAML/ENV/CLI
config = BotConfig.load()

Основная логика бота

# bot/src/bot.py
from telegram import Update
from telegram.ext import Application, CommandHandler, MessageHandler, filters
from configs.config import config

async def start_command(update: Update, context):
    """Обработчик команды /start"""
    user_id = update.effective_user.id
    await context.bot.send_message(
        chat_id=update.effective_chat.id,
        text=f"Привет! Я бот для управления подпиской FindMyLink.\n"
             f"Ваш ID: {user_id}"
    )

async def subscribe_command(update: Update, context):
    """Обработчик команды /subscribe"""
    # Создаём инвойс для оплаты
    invoice = await context.bot.create_invoice(
        title="Подписка FindMyLink",
        description="Доступ к поиску по вкладкам на 30 дней",
        payload="subscription_30_days",
        provider_token=config.telegram.payment_provider_token,
        currency="RUB",
        prices=[{"label": "Подписка", "amount": 29900}]  # 299 рублей
    )
    
    await context.bot.send_invoice(
        chat_id=update.effective_chat.id,
        **invoice
    )

def main():
    application = Application.builder().token(config.telegram.bot_token).build()
    
    application.add_handler(CommandHandler("start", start_command))
    application.add_handler(CommandHandler("subscribe", subscribe_command))
    
    application.run_polling()

[4] Telegram Payments API, https://core.telegram.org/bots/payments
[5] SQLAlchemy, https://www.sqlalchemy.org/
[6] KeyDB, https://keydb.dev/
[7] python-telegram-bot, https://python-telegram-bot.org/
[8] msgspec, https://jcristharif.com/msgspec/
[9] config-lib-msgspec, https://github.com/rastaclaus/config-lib-msgspec

Авторизация через Telegram

Проблема с виджетом

Для реализации авторизации я решил использовать Telegram Login Widget [10], который позволяет пользователям входить в приложение через Telegram. После изучения документации я выяснил, что виджет прост в интеграции и требует только наличие Telegram-бота, который у меня уже был создан для монетизации. Изначально я планировал встроить виджет непосредственно в интерфейс расширения, так как это казалось логичным решением для удобного пользовательского опыта.

Однако при тестировании возникла проблема: при открытии попапа с виджетом он оказывался пустым. После анализа я обнаружил, что Telegram Login Widget работает только на страницах с HTTPS [10]. Поскольку на этапе локальной разработки у меня ещё не было домена и VPS, я столкнулся с необходимостью настроить HTTPS для тестирования.

Локальная разработка

Чтобы обеспечить работу Telegram Login Widget в локальной среде, я настроил HTTPS-соединение, выполнив следующие шаги:

  1. Добавление домена в /etc/hosts
    Открыл файл /etc/hosts с правами суперпользователя:

    sudo vim /etc/hosts
    

    Добавил строку, связывающую testdomain.com с локальным IP:

    127.0.0.1    testdomain.com
    

    Сохранил изменения и закрыл файл (:wq).

  2. Создание конфигурации Nginx
    Создал папку для конфигураций Nginx:

    mkdir -p ~/nginx-proxy/conf.d
    

    Создал файл конфигурации:

    vim ~/nginx-proxy/conf.d/testdomain.conf
    

    Вставил следующую конфигурацию, заменив порт на локальный (например, 8000):

    server {
        listen 80;
        server_name testdomain.com;
        return 301 https://$host$request_uri;
    }
    
    server {
        listen 443 ssl;
        server_name testdomain.com;
    
        ssl_certificate     /etc/nginx/ssl/testdomain.com+3.pem;
        ssl_certificate_key /etc/nginx/ssl/testdomain.com+3-key.pem;
    
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers HIGH:!aNULL:!MD5;
    
        location / {
            proxy_pass http://host.docker.internal:8000;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
    
  3. Генерация SSL-сертификата
    Создал папку для SSL-сертификатов:

    mkdir -p ~/nginx-proxy/ssl
    

    Установил утилиту mkcert для генерации доверенных сертификатов:

    sudo apt install mkcert  # Linux
    # или
    brew install mkcert      # Mac
    

    Сгенерировал самоподписанный сертификат:

    mkcert testdomain.com
    

    Скопировал сертификаты в папку Nginx:

    mv testdomain.com.pem ~/nginx-proxy/ssl/findmylinkbot.crt
    mv testdomain.com-key.pem ~/nginx-proxy/ssl/findmylinkbot.key
    

    Перезапустил Nginx:

    docker restart nginx-proxy
    
  4. Запуск Nginx в Docker
    Запустил контейнер Nginx с пробросом портов и подключением конфигураций:

    docker run -d \
        --name nginx-proxy \
        -p 80:80 \
        -p 443:443 \
        -v ~/nginx-proxy/conf.d:/etc/nginx/conf.d \
        -v ~/nginx-proxy/ssl:/etc/nginx/ssl \
        --add-host=host.docker.internal:host-gateway \
        nginx
    

    Если host.docker.internal не работал, использовал опцию --network="host".

  5. Проверка
    Убедился, что локальный сервис запущен на указанном порту (например, 8000). Если сервис не был запущен, использовал команду:

    docker run -d --name myapp --network mynet -p 8000:8000 myapp
    

    Проверил доступность в браузере:

После выполнения этих шагов Telegram Login Widget заработал в локальной среде, что позволило протестировать процесс авторизации.

Подводные камни и решение

Изначально расширение открывалось как стандартный попап под иконкой на панели браузера. Я ожидал, что авторизация через Telegram будет работать следующим образом: пользователь нажимает кнопку “Войти”, открывается попап с виджетом, проходит авторизацию, и данные сохраняются в расширении. Однако при тестировании я столкнулся с неожиданным поведением: при смене фокуса (например, клике вне окна) попап расширения закрывался (это стандартное поведение), из-за чего колбэк от Telegram не доходил до приложения, и данные не сохранялись в localStorage. Понял я это не сразу, а опытным путём: при открытии консоли разработчика для отладки попап оставался открытым, и авторизация проходила успешно, но без консоли при потере фокуса ничего не работало.

Чтобы устранить эту проблему, я изменил подход: вместо стандартного попапа расширение открывает отдельное окно браузера, которое остаётся активным даже при смене фокуса. Это потребовало небольшой доработки кода:

// public/redirect-popup.js
// Если открыли popup как расширение, сразу открываем отдельное окно и закрываем popup
if (window.location.pathname.endsWith('index.html')) {
  window.open(
    chrome.runtime.getURL('normal_popup.html'),
    'findmylink_popup',
    'width=380,height=600'
  );
  window.close();
}

Такое решение изменило привычное поведение расширения, но оказалось удобным в использовании и позволило корректно обрабатывать авторизацию. Проблема с потерей фокуса была решена, и данные пользователя начали сохраняться.

Авторизация через API

// src/components/auth.ts
export function initTelegramLoginButton(container: HTMLElement) {
  const loginButton = document.createElement('button');
  loginButton.className = 'telegram-login-button w-full py-2 px-4 bg-[#0088cc] text-white rounded-lg hover:bg-[#0077b3] transition-colors flex items-center justify-center gap-2';
  loginButton.textContent = t('login.button');
  
  loginButton.onclick = () => {
    // Открываем внешнюю страницу авторизации
    const authUrl = `${CONFIG.API.BASE_URL}${CONFIG.API.ENDPOINTS.EXTENSION_AUTH}`;
    const win = window.open(authUrl, 'telegram_auth', 'width=500,height=600');
    
    // Слушаем postMessage с токеном
    function handler(event: MessageEvent) {
      if (event.data?.type === CONFIG.MESSAGES.TELEGRAM_TOKEN && event.data.token) {
        chrome.runtime.sendMessage({ 
          type: CONFIG.MESSAGES.TELEGRAM_TOKEN, 
          token: event.data.token 
        });
        window.removeEventListener('message', handler);
        if (win) win.close();
      }
    }
    window.addEventListener('message', handler);
  };
  
  container.appendChild(loginButton);
}

Для обеспечения безопасности и разделения ответственности я перенёс обработку авторизации в серверную часть API, что описано в следующем разделе. Расширение отправляет запрос на авторизацию, API возвращает страницу с виджетом, а после успешного входа передаёт токен через postMessage [11]. Это позволило сделать процесс авторизации надёжным и соответствующим требованиям Chrome Web Store.

[10] Telegram Login Widget, https://core.telegram.org/widgets/login
[11] WebExtensions API, https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions

API для расширения

Выбор фреймворка и настройка API

Для реализации серверной части, обеспечивающей взаимодействие между расширением и Telegram-ботом, я выбрал фреймворк Litestar [12], так как он напоминает Django по структуре и удобству, но поддерживает асинхронное программирование, что делает его более производительным для таких задач. Litestar позволил быстро настроить эндпоинты для обработки авторизации и проверки статуса подписки. Для работы с базой данных я использовал SQLite через библиотеки aiosqlite и SQLAlchemy[asyncio] [5], что обеспечивало простоту и достаточную производительность для небольшого проекта. Кэширование реализовал с помощью KeyDB [6], совместимого с Redis, для ускорения доступа к данным о пользователях и их подписках. Для управления конфигурацией использовались библиотеки msgspec [8] и config-lib-msgspec [9], последняя из которых была разработана коллегой и позволила удобно загружать настройки из YAML, переменных окружения или аргументов командной строки.

Технологический стек API:

  • Framework: Litestar для создания асинхронного REST API.

  • База данных: SQLite + aiosqlite + SQLAlchemy[asyncio] для хранения данных.

  • Кэш: KeyDB для быстрого доступа к данным.

  • Конфигурация: msgspec и config-lib-msgspec для гибкого управления настройками.

Выбор Litestar был обусловлен его похожестью на Django, но с поддержкой асинхронного программирования, что идеально подошло для создания быстрого и лёгкого API. SQLite и KeyDB оказались оптимальными для небольшого проекта, где не требовалась сложная инфраструктура.

Структура API

API обрабатывает запросы от расширения, включая авторизацию через Telegram и проверку статуса подписки. Основные эндпоинты:

  • /auth/telegram: Возвращает страницу с Telegram Login Widget для авторизации.

  • /auth/callback: Обрабатывает колбэк от Telegram, проверяет подпись и возвращает токен.

  • /subscription/status: Проверяет статус подписки пользователя.

Пример кода для обработки авторизации:

# api/src/routers/auth.py
from litestar import get, post, Router
from litestar.exceptions import HTTPException
from msgspec import Struct
from sqlalchemy.ext.asyncio import AsyncSession

from models import User
from services import TelegramAuthService

class TelegramAuthResponse(Struct):
    token: str

@get("/auth/telegram")
async def get_auth_page() -> str:
    return f"""
    <!DOCTYPE html>
    <html>
        <body>
            <script async src="https://telegram.org/js/telegram-widget.js?22"
                    data-telegram-login="{config.telegram.bot_name}"
                    data-size="large"
                    data-request-access="write"
                    data-onauth="onTelegramAuth(user)">
            </script>
            <script>
                function onTelegramAuth(user) {{
                    fetch('{config.api.base_url}/auth/callback', {{
                        method: 'POST',
                        headers: {{ 'Content-Type': 'application/json' }},
                        body: JSON.stringify(user)
                    }})
                    .then(response => response.json())
                    .then(data => {{
                        window.opener.postMessage({{
                            type: '{config.messages.telegram_token}',
                            token: data.token
                        }}, '*');
                        window.close();
                    }});
                }}
            </script>
        </body>
    </html>
    """

@post("/auth/callback")
async def telegram_callback(data: dict, db: AsyncSession) -> TelegramAuthResponse:
    if not TelegramAuthService.verify_signature(data):
        raise HTTPException(status_code=400, detail="Invalid signature")
    
    user_id = data["id"]
    user = await User.get(db, user_id)
    if not user:
        user = User(id=user_id, username=data.get("username"))
        await user.save(db)
    
    token = TelegramAuthService.generate_token(user_id)
    return TelegramAuthResponse(token=token)

Обработка авторизации

Для обеспечения безопасности я реализовал проверку подписи Telegram, как указано в документации Telegram Login Widget [10]. После успешной авторизации API генерирует JWT-токен, который передаётся расширению через postMessage [11]. Расширение использует этот токен для последующих запросов к API, например, для проверки статуса подписки. Данные о пользователях и подписках хранятся в SQLite, а KeyDB используется для кэширования токенов и статуса подписки, что снижает нагрузку на базу данных.

Интеграция с Telegram-ботом

API взаимодействует с Telegram-ботом через внутренние вызовы. Например, при создании инвойса бот отправляет запрос к API, чтобы проверить, существует ли пользователь. После успешной оплаты API обновляет статус подписки в базе данных и уведомляет расширение. Это обеспечивает синхронизацию между клиентской частью (расширением), сервером и ботом.

[5] SQLAlchemy, https://www.sqlalchemy.org/
[6] KeyDB, https://keydb.dev/
[8] msgspec, https://jcristharif.com/msgspec/
[9] config-lib-msgspec, https://github.com/rastaclaus/config-lib-msgspec
[10] Telegram Login Widget, https://core.telegram.org/widgets/login
[11] WebExtensions API, https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions
[12] Litestar, https://litestar.dev/

Деплой и инфраструктура

Настройка сервера

Процесс деплоя API и Telegram-бота был стандартным, но требовал внимательной настройки для обеспечения безопасности и производительности. Я арендовал VPS для размещения серверной части и выбрал Docker [13] для контейнеризации приложения, что упростило управление зависимостями и обеспечило воспроизводимость окружения. HTTPS был настроен с использованием Caddy [14], современного веб-сервера, который автоматически управляет SSL-сертификатами через Let’s Encrypt.

Для настройки HTTPS я создал Caddyfile с минимальной конфигурацией:

# Caddyfile
api.findmylink.com {
    reverse_proxy localhost:8000
}

Caddy автоматически получил и настроил SSL-сертификат для домена api.findmylink.com, обеспечив безопасное соединение для взаимодействия с Telegram Login Widget и API. Это избавило меня от необходимости вручную генерировать сертификаты, как это было на этапе локальной разработки.

Настройка конфигурации

Перед запуском я создал .env файлы для API и Telegram-бота, чтобы пробросить необходимые параметры в конфигурацию. Пример .env файла для API:

# api/.env
CFG_DATABASE_URL=sqlite+aiosqlite:///data.db
CFG_REDIS_URL=redis://keydb:6379
CFG_API_BASE_URL=https://api.findmylink.com

Пример .env файла для бота:

# bot/.env
CFG_TELEGRAM_BOT_TOKEN=your_bot_token_here
CFG_PAYMENT_PROVIDER_TOKEN=your_payment_provider_token_here

Эти файлы загружались с помощью библиотеки config-lib-msgspec [9], что позволяло гибко управлять настройками через YAML, переменные окружения или аргументы командной строки.

Развёртывание приложения

Я создал Docker-образы для API (на основе Litestar) и Telegram-бота, используя Dockerfile для каждого компонента. Пример Dockerfile для API:

# api/Dockerfile
FROM python:3.11-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["litestar", "run", "--host", "0.0.0.0", "--port", "8000"]

Для деплоя я использовал docker-compose для оркестрации контейнеров API, бота и KeyDB. Пример конфигурации:

# docker-compose.yml
version: '3.8'
services:
  api:
    build: ./api
    ports:
      - "8000:8000"
    env_file:
      - ./api/.env
    volumes:
      - ./api:/app
    depends_on:
      - keydb

  bot:
    build: ./bot
    env_file:
      - ./bot/.env
    volumes:
      - ./bot:/app
    depends_on:
      - keydb

  keydb:
    image: eqalpha/keydb
    ports:
      - "6379:6379"
    volumes:
      - keydb_data:/data

volumes:
  keydb_data:

После запуска docker-compose up -d все компоненты (API, бот и KeyDB) были развёрнуты и настроены для взаимодействия. Для защиты сервера я настроил базовые правила брандмауэра с помощью ufw, ограничив доступ только к портам 80, 443 и 22.

Интеграция с Cloudflare

Для дополнительной безопасности и оптимизации я подключил домен findmylink.ru к Cloudflare [15]. Это позволило использовать их DNS, защиту от DDoS-атак и кэширование статических ресурсов. Настройка Cloudflare была простой: я добавил домен в их панель управления, обновил DNS-записи у регистратора и включил проксирование через Cloudflare для порта 443.

Проблемы и решения

На этапе деплоя возникла проблема с доступом к API из расширения из-за CORS (Cross-Origin Resource Sharing). Chrome блокировал запросы с локального расширения к внешнему API, так как они считались кросс-доменными. Для решения я добавил заголовки CORS в конфигурацию Litestar:

# api/src/main.py
from litestar import Litestar
from litestar.middleware import DefineMiddleware
from litestar.middleware.cors import CORSConfig

cors_config = CORSConfig(allow_origins=["chrome-extension://*"], allow_methods=["GET", "POST"], allow_headers=["*"])
app = Litestar(route_handlers=[...], middleware=[DefineMiddleware(CORSConfig, cors_config)])

Это позволило расширению корректно взаимодействовать с API. После настройки HTTPS через Caddy и интеграции с Cloudflare приложение работало стабильно, а пользователи могли безопасно авторизоваться и использовать функции подписки.

[13] Docker, https://www.docker.com/
[14] Caddy, https://caddyserver.com/
[15] Cloudflare, https://www.cloudflare.com/

Публикация в Chrome Web Store

Процесс публикации

Когда приложение было готово, я приступил к публикации расширения в Chrome Web Store [1]. Для этого требовалось зарегистрироваться в качестве разработчика и оплатить единовременный регистрационный взнос в размере 5 долларов. Процесс оплаты оказался непростым: я перепробовал несколько способов, но в итоге остановился на использовании Telegram-бота для оплаты, что оказалось наиболее удобным, несмотря на небольшую комиссию. После успешной оплаты доступ к кабинету разработчика открылся сразу.

Для загрузки расширения я заархивировал содержимое папки dist, заполнил необходимые поля в кабинете разработчика, включая описание, название и изображения, а также загрузил архив. На каждое запрошенное разрешение (например, доступ к storage или bookmarks) требовалось предоставить обоснование, что я сделал, описав функциональность расширения и взаимодействие с API.

Проблемы с авторизацией и их решение

Через день-два после отправки расширения на проверку я получил отказ от Chrome Web Store. Причина была в том, что нельзя встраивать в расширение код, который обрабатывается на другом домене, — это касалось авторизации через Telegram Login Widget [10]. Изначально я встроил виджет непосредственно в расширение, что оказалось ошибкой, так как это нарушало политики Chrome по безопасности удалённого кода.

Чтобы обойти это ограничение, я решил перенести авторизацию в API. Теперь расширение открывает попап, который делает запрос к API, а API возвращает страницу с Telegram Login Widget. Это позволило соответствовать требованиям Chrome Web Store, сохранив функциональность авторизации. Я обновил код расширения, чтобы оно взаимодействовало с API следующим образом:

// src/auth.js
function openTelegramAuthPopup() {
  const authUrl = `${CONFIG.API.BASE_URL}${CONFIG.API.ENDPOINTS.EXTENSION_AUTH}`;
  window.open(authUrl, 'telegram_auth', 'width=500,height=600');
}

После внесения изменений я повторно загрузил расширение в Chrome Web Store. На этот раз проверка прошла успешно, и через день расширение было опубликовано.

[1] Chrome Web Store Developer Dashboard,https://chromewebstore.google.com
[10] Telegram Login Widget, https://core.telegram.org/widgets/login

Куда ещё можно выложить?

Firefox Add-ons

После публикации FindMyLink в Chrome Web Store я решил изучить возможность размещения расширения в других магазинах, начиная с Firefox Add-ons [2]. Публикация в Firefox оказалась бесплатной, а процесс модерации — относительно быстрым. Для адаптации потребовалось внести небольшие изменения в файл manifest.json, чтобы соответствовать требованиям Mozilla, но описание, иконки и остальной код остались без изменений, так как расширение основано на WebExtensions API [11].

При загрузке архива в Firefox Add-ons Developer Hub система автоматически анализировала код и указывала на ошибки. После их исправления я отправил расширение на проверку. Модерация заняла около пяти дней, что соответствовало указанному сроку ожидания ревьювера. В итоге я получил письмо с уведомлением: “Your Extension has been automatically screened and tentatively approved.” Статус расширения сменился на “Approved”, и оно было опубликовано. В целом, процесс оказался простым и доступным для разработчиков.

Microsoft Edge Add-ons

Другим вариантом стал Microsoft Edge Add-ons [16]. Процесс публикации выглядел похожим на Chrome Web Store: регистрация в Microsoft Partner Center, загрузка архива, добавление описания, скриншотов и иконок. Манифест не пришлось изменять, так как Edge, как и Chrome, использует WebExtensions API, и документация Microsoft подтверждает их совместимость [17]. Я быстро подготовил черновик, но при попытке опубликовать получил ошибку: “Cannot publish extension. Your developer account verification is either pending or rejected. Please complete your account verification on the Account Settings page before you begin publishing.”

На указанной странице в личном кабинете, кроме базовой информации о профиле, не было ни статуса верификации, ни кнопки для её запуска. Поиск информации в интернете не дал чётких ответов: я нашёл лишь упоминания, что процесс верификации может быть длительным и должен отображаться в интерфейсе, но в моём случае никаких признаков активности не было. Информация об оплате также отсутствовала. В итоге я пришёл к выводу, что, несмотря на простоту загрузки расширения, процесс верификации в Microsoft Partner Center остаётся сложным и не интуитивным, что создаёт значительные препятствия для публикации.

Публикация в GitHub Releases

Ещё одним вариантом было выложить запакованное расширение в релизах GitHub [18]. Это простой способ, не требующий модерации или сложной настройки: достаточно загрузить архив из папки dist в раздел Releases репозитория. Однако этот подход имеет ограничения: расширение будет доступно только тем, кто знает о проекте и следит за репозиторием, что значительно сужает аудиторию по сравнению с публикацией в Chrome Web Store или Firefox Add-ons. Поэтому я рассматривал GitHub как дополнительный, а не основной канал распространения.

По поводу статистики

Пока данных по использованию мало, потому что расширение только-только вышло. Но даже пара установок уже кажется маленькой победой!

[2] Firefox Add-ons, https://addons.mozilla.org/
[11] WebExtensions API, https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions
[16] Microsoft Edge Add-ons, https://microsoftedge.microsoft.com/addons
[17] Microsoft Edge Extensions, https://learn.microsoft.com/en-us/microsoft-edge/extensions-chromium/
[18] GitHub Releases, https://docs.github.com/en/repositories/releasing-projects-on-github

Результаты и выводы

Что получилось

За примерно две недели я смог собрать полноценное расширение для браузера, которое решает реальную проблему — поиск по куче открытых вкладок и закладок. Я доволен результатом: расширение работает, как задумано, я им пользуюсь сам и оно уже доступно в Chrome Web Store [1] и Firefox Add-ons [2]. Попытка выложить его в Microsoft Edge Add-ons пока застопорилась из-за мутного процесса верификации, но сам факт, что я дошёл до публикации, уже радует.

Что конкретно удалось:

  • Сделал удобный поиск по закладкам и вкладкам с помощью WebExtensions API [11].

  • Настроил авторизацию через Telegram Login Widget [10], хотя пришлось повозиться с переносом её в API, чтобы угодить требованиям Chrome.

  • Добавил монетизацию через Telegram Payments API [4], чтобы пользователи могли покупать подписку на премиум-функции.

  • Развернул сервер с Docker, Caddy и Cloudflare — всё летает, HTTPS работает.

Советы для тех, кто хочет попробовать

Пройдя весь этот путь, я понял несколько вещей, которые могут помочь другим разработчикам:

  • Беритесь за небольшие проекты. Этот родился из простой идеи, и за две недели я довёл его до рабочего состояния. Не нужно сразу строить что-то грандиозное.

  • Читайте документацию заранее. WebExtensions API [11] и правила магазинов (особенно Mozilla [19]) спасут от головной боли. Я, например, чуть не попал с авторизацией в Chrome.

  • Настраивайте HTTPS с самого начала. Caddy [14] сильно упрощают жизнь, особенно для простых проектов.

  • Не бойтесь экспериментировать с монетизацией. Telegram Payments API [4] — это просто и быстро, если не хочется заморачиваться с большими платёжными системами.

  • Будьте готовы к сюрпризам. Верификация в Microsoft Edge Add-ons — это какой-то квест без подсказок, так что запаситесь терпением.

Что дальше

Хочется доработать интерфейс, чтобы он был ещё удобнее, может, добавить возможность настраивать цвета. Ещё попробую разобраться с Microsoft Edge Add-ons — вдруг всё-таки получится опубликовать. Плюс, интересно было бы добавить удаление дубликатов, экспорт и может синхронизацию, но это пока только идея. В общем, проект живёт, и я планирую его развивать, опираясь на отзывы пользователей.

[1] Chrome Web Store Developer Dashboard, https://chromewebstore.google.com/
[2] Firefox Add-ons, https://addons.mozilla.org/
[4] Telegram Payments API, https://core.telegram.org/bots/payments
[10] Telegram Login Widget, https://core.telegram.org/widgets/login
[11] WebExtensions API, https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions
[14] Caddy, https://caddyserver.com/
[19] Mozilla Add-on Policies, https://extensionworkshop.com/documentation/publish/add-on-policies/

[20] FindMyLink, https://findmylink.ru

Комментарии (7)


  1. AntonyKozm
    24.07.2025 19:39

    Описано как будто диплом читаю (перекрестные ссылки и оформление), но в целом интересный материал. Тоже есть мысли по разработке расширения, но пока все в стадии идеи. Спасибо за материал)


    1. pulichkin Автор
      24.07.2025 19:39

      Да, пожалуйста, на счёт ссылок подумал так удобно будет, часть какую-то почитал, если что-то заинтересовало посмотрел, как в книге сноски


      1. AntonyKozm
        24.07.2025 19:39

        Немного перебор. Можно сделать ссылку при первом упоминании, и указывать ссылки в конце. Таким образом текст не будет перегружен квадратными скобками

        Это просто мое предложение. Текст написан обстоятельно и подробно, это гуд)


        1. pulichkin Автор
          24.07.2025 19:39

          спасибо, учту в следующих статьях


  1. vikaz
    24.07.2025 19:39

    Спасибо за развёрнутую статью. Хочу сразу извиниться за возможный дилетантский вопрос, но всё же. Как вы собираетесь выводить деньги? Правильно ли я понимаю, что вы находитесь не в России, коль выбрали такую платформу для монетизации?


    1. pulichkin Автор
      24.07.2025 19:39

      Пожалуйста, рад что статья понравилась. Да нормальный вопрос, если будет что выводить, то скорее всего через Fragment и wallet, звезды можно сконвертировать в тонкоины, а их уже в кошельке можно на бирже продать за рубли. Я нахожусь в России, выбрал такой вариант, просто потому что захотелось так попробовать, люблю экспериментировать)


      1. vikaz
        24.07.2025 19:39

        Спасибо. Пусть всё получится! ))