Как новый сайт попал в ответы ChatGPT за 17 дней

Sergey Nesmachny
Sergey Nesmachny
11.09.2026
6 мин чтения
Стилист в Лиссабоне: 17 дней до ChatGPT, 5 визитов, 2 новых клиента

6 августа 2026 года мы запустили svetstylist.com — сайт персонального стилиста в Лиссабоне. Новый домен, нулевая история, ни одной входящей ссылки.

23 августа пришёл первый посетитель из ChatGPT. Семнадцать дней.

К середине сентября сайт цитировали ChatGPT и Perplexity, а Google показывал его в ответах, сгенерированных ИИ. Двое посетителей из ChatGPT стали платящими клиентами.

  • 17 дней — до первого визита из ChatGPT
  • 5 визитов из ChatGPT за месяц
  • 2 из них стали клиентами
  • 7 показов в AI-ответах Google

Ниже — что для этого делали, чем это отличается от обычного SEO и как проверить у себя. И сразу оговорка про масштаб, без которой этот текст был бы нечестным.

Сначала о масштабе

За месяц с запуска сайт собрал 161 посетителя. Из поиска Google: 43 клика при 1050 показах, средняя позиция 11,1. Из ChatGPT — пять визитов. В отчёте Search Console «Функции на основе генеративного ИИ» — семь показов.

Это не история про десять тысяч визитов. Это история про скорость и про качество.

Если бы я показал вам скриншот с десятками тысяч сессий, честный вопрос был бы: сколько из них что-то купили. Здесь я могу ответить точно: двое из пяти. И именно поэтому кейс стоит того, чтобы его разобрать: посетитель, пришедший из ответа ассистента, приходит с наполовину принятым решением.

Ранжироваться и быть процитированным — это не одно и то же

Вот главное, что мы поняли на этом проекте.

Поисковик ранжирует страницы. Ассистент цитирует предложения.

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

Ассистент не смог их процитировать. Цена жила в бейдже, длительность в строке с иконкой, формат в заголовке колонки. Человек эту сетку читает за секунду. Машина видит набор чисел без утверждения, которое можно взять целиком и вставить в ответ.

Мы переписали так, чтобы каждый факт существовал ещё и в виде фразы:

«Разбор гардероба стоит 226 евро и занимает три-четыре часа — дома в Лиссабоне или по видеосвязи. После остаётся лукбук готовых образов из своих же вещей и список покупок по приоритетам.»

Это цитируемо. Сетка из иконок — нет. Обе версии живут на странице одновременно: человеку карточки, машине предложения.

Что делали

Без пошагового рецепта, но по существу. Пять вещей.

1. Технический минимум, который все считают скучным

Быстрая отдача страниц, корректный sitemap, hreflang между языковыми версиями, мгновенное уведомление поисковиков о новых страницах. Ничего изобретательного. Но пока это не сделано, остальное не имеет смысла: ассистент не процитирует страницу, которую не увидел.

2. Разметка, описывающая бизнес, а не страницу

Большинство сайтов размечает статьи и хлебные крошки. Мы описали саму услугу: что это, сколько стоит, где оказывается, на каких языках, кем. Ассистенту нужен не документ — ему нужен субъект, о котором можно сделать утверждение.

3. Вопросы в тех формулировках, которыми их задают

Не «Услуги и цены», а «Сколько стоит стилист в Лиссабоне». Не «Форматы работы», а «Я не в Лиссабоне — это работает?». Мы взяли реальные первые сообщения клиентов и сделали из них заголовки. Одна деталь оттуда: та самая клиентка назвала услугу словом, которого на сайте не было ни разу. Теперь оно там есть.

4. Однозначная личность

Прежде чем кого-то рекомендовать, ассистент должен быть уверен, что понимает, о ком речь. Кто это, где работает, на каких языках, чем именно занимается — и чтобы это совпадало во всех местах, где о человеке есть данные. Расхождения между сайтом и внешними профилями стоят дорого: это причина не назвать вас в ответе.

5. Текстовая версия для машин

Отдельный файл, где всё главное о бизнесе изложено простым текстом со ссылками. Дёшево делается, легко читается и не устаревает, потому что генерируется из той же базы, что и сайт.

Отдельно скажу, чего мы не делали: не покупали ссылки, не писали статьи «под ключевые слова», не ждали, пока «наберётся возраст домена». Последнее особенно важно — в классическом SEO новый домен полгода сидит в песочнице. У ассистентов этого механизма нет.

Как проверить это у себя

Три места, куда стоит посмотреть прямо сегодня.

  1. Search Console, «Эффективность» → «Функции на основе генеративного ИИ». Отчёт в бете, и большинство владельцев сайтов не знают, что он существует. Он показывает, сколько раз ваш сайт попал в ответы Google, сгенерированные ИИ.
  2. Фильтр по источнику перехода в аналитике. chatgpt.com, perplexity.ai, copilot.microsoft.com. Если там ноль, вас не цитируют. Если единицы, процесс пошёл.
  3. Спросите сам ассистент. Задайте ChatGPT вопрос, который задал бы ваш клиент, и посмотрите, кого он назовёт. Это грубая проверка, ответ меняется от запроса к запросу, но она честнее любых прогнозов.

Ловушка, в которую вы попадёте при замерах

А теперь то, о чём почти не пишут.

Вот как выглядит трафик из ChatGPT в аналитике этого проекта: пять визитов, показатель отказов 100%, средняя длительность визита ноль секунд.

По любому дашборду это худший трафик на сайте. Мусор, от которого надо избавляться.

Двое из этих пяти стали платящими клиентами.

Объяснение простое: человек уже прочитал ответ в чате. Он приходит на сайт не изучать, а сверить — убедиться, что бизнес настоящий, посмотреть на лицо и на цену. Потом закрывает вкладку и пишет в мессенджер. Аналитика видит одну страницу, ноль секунд и отказ. Бизнес видит клиента.

Если вы будете оценивать AI-трафик обычными метриками вовлечённости, вы его отключите как бесполезный. Мерить надо по обращениям, а не по глубине просмотра.

Что это значит для вашего бизнеса

Окно открыто именно сейчас, и устроено оно не так, как поисковое.

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

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

Хотите так же

Я делаю такие проекты под ключ: техническая база, структура, разметка, тексты, которые можно процитировать, и измеримость, чтобы через месяц вы видели не «трафик вырос», а откуда именно пришли обращения.

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

Напишите мне — разберём ваш случай и скажу честно, есть ли там что брать.


Данные в тексте: Google Search Console и аналитика проекта, период 5 августа — 10 сентября 2026 года. Кейс опубликован с согласия клиента.

Sergey Nesmachny

Автор

Sergey Nesmachny

Поделиться:

Другие статьи

Продолжайте читать блог

7 GDPR-Compliant Google Analytics Alternatives in Europe (2026)

7 GDPR-Compliant Google Analytics Alternatives in Europe (2026)

EuroMetrics, Plausible, Fathom, Simple Analytics, Matomo, Umami and Piwik PRO compared: EU hosting, cookieless tracking, open source, free tiers and who each suits.

06.09.2026
Is There Finally a Real Alternative to WordPress? (2026)

Is There Finally a Real Alternative to WordPress? (2026)

I built a client site on Cloudflare + EmDash CMS instead of WordPress: D1 database, R2 storage, built-in business email and CRM, all for $5/month.

11.08.2026
Создайте RAG-чатбот на основе контента WordPress (2026)

Создайте RAG-чатбот на основе контента WordPress (2026)

# Build a RAG Chatbot on WordPress Content: Complete Step-by-Step Guide ## What You'll Build A RAG (Retrieval-Augmented Generation) chatbot that: - Fetches WordPress posts via REST API - Converts posts into embeddings - Stores embeddings in Qdrant vector database - Retrieves relevant posts for user queries - Generates answers with proper citations ## Prerequisites - WordPress site with REST API enabled (WordPress 4.7+) - Python 3.10+ - Qdrant instance (local or cloud) - OpenAI API key (or alternative LLM) - Basic knowledge of Python and APIs ## Architecture Overview ``` WordPress REST API ↓ Python Script ↓ Embeddings (OpenAI) ↓ Qdrant Storage ↓ RAG Pipeline ↓ LLM Response + Citations ``` ## Step 1: Set Up Your Environment ### Install Required Dependencies ```bash pip install requests python-dotenv qdrant-client openai langchain python-dotenv ``` ### Create Environment File Create `.env`: ``` WORDPRESS_URL=https://your-wordpress-site.com WORDPRESS_USERNAME=your_username WORDPRESS_PASSWORD=your_password OPENAI_API_KEY=your_openai_key QDRANT_URL=http://localhost:6333 QDRANT_API_KEY=your_qdrant_key_or_leave_empty ``` ## Step 2: Fetch WordPress Posts via REST API ### Create WordPress Data Fetcher Create `fetch_wordpress.py`: ```python import requests import json from typing import List, Dict import os from dotenv import load_dotenv load_dotenv() class WordPressFetcher: def __init__(self, base_url: str): self.base_url = base_url.rstrip('/') self.rest_endpoint = f"{self.base_url}/wp-json/wp/v2" def fetch_all_posts(self, per_page: int = 100) -> List[Dict]: """ Fetch all posts from WordPress using pagination """ posts = [] page = 1 while True: url = f"{self.rest_endpoint}/posts" params = { 'per_page': per_page, 'page': page, 'status': 'publish' } response = requests.get(url, params=params) if response.status_code != 200: print(f"Error fetching page {page}: {response.status_code}") break page_posts = response.json() if not page_posts: break posts.extend(page_posts) page += 1 print(f"Fetched page {page - 1}, total posts: {len(posts)}") return posts def extract_post_content(self, post: Dict) -> Dict: """ Extract relevant content from WordPress post """ return { 'id': str(post['id']), 'title': post['title']['rendered'], 'content': post['content']['rendered'], 'excerpt': post['excerpt']['rendered'], 'url': post['link'], 'date': post['date'], 'author': post['author'], 'categories': post.get('categories', []) } def clean_html(self, html_content: str) -> str: """ Remove HTML tags from content """ from html.parser import HTMLParser class MLStripper(HTMLParser): def __init__(self): super().__init__() self.reset() self.strict = False self.convert_charrefs = True self.text = [] def handle_data(self, d): self.text.append(d) def get_data(self): return ''.join(self.text) stripper = MLStripper() stripper.feed(html_content) return stripper.get_data() # Usage if __name__ == "__main__": wordpress_url = os.getenv('WORDPRESS_URL') fetcher = WordPressFetcher(wordpress_url) posts = fetcher.fetch_all_posts() print(f"Total posts fetched: {len(posts)}") # Save to file for inspection with open('wordpress_posts.json', 'w') as f: json.dump([fetcher.extract_post_content(p) for p in posts], f, indent=2) ``` ### Run the Fetcher ```bash python fetch_wordpress.py ``` This creates `wordpress_posts.json` with all your posts. ## Step 3: Create Embeddings ### Set Up Embedding Generator Create `embeddings.py`: ```python from openai import OpenAI import os from dotenv import load_dotenv load_dotenv() class EmbeddingGenerator: def __init__(self, api_key: str = None): self.client = OpenAI(api_key=api_key or os.getenv('OPENAI_API_KEY')) self.model = "text-embedding-3-small" def generate_embedding(self, text: str) -> list: """ Generate embedding for a single text """ response = self.client.embeddings.create( input=text, model=self.model ) return response.data[0].embedding def generate_batch_embeddings(self, texts: list) -> list: """ Generate embeddings for multiple texts (batch is more efficient) """ response = self.client.embeddings.create( input=texts, model=self.model ) return [item.embedding for item in response.data] # Test if __name__ == "__main__": generator = EmbeddingGenerator() test_embedding = generator.generate_embedding("Test content for embedding") print(f"Embedding dimension: {len(test_embedding)}") print(f"First 5 values: {test_embedding[:5]}") ``` ## Step 4: Set Up Qdrant Vector Database ### Start Qdrant Locally (Docker) ```bash docker run -p 6333:6333 -p 6334:6334 \ -v /path/to/qdrant/storage:/qdrant/storage:z \ qdrant/qdrant:latest ``` Access Qdrant dashboard at `http://localhost:6333/dashboard` ### Create Qdrant Manager Create `qdrant_manager.py`: ```python from qdrant_client import QdrantClient from qdrant_client.models import Distance, VectorParams, PointStruct from typing import List, Dict, Tuple import os from dotenv import load_dotenv load_dotenv() class QdrantManager: def __init__(self, url: str = None, api_key: str = None): self.url = url or os.getenv('QDRANT_URL', 'http://localhost:6333') self.api_key = api_key or os.getenv('QDRANT_API_KEY') self.client = QdrantClient( url=self.url, api_key=self.api_key if self.api_key else None ) self.collection_name = "wordpress_posts" def create_collection(self, vector_size: int = 1536): """ Create collection for storing embeddings 1536 is the dimension for text-embedding-3-small """ try: self.client.recreate_collection( collection_name=self.collection_name, vectors_config=VectorParams(size=vector_size, distance=Distance.COSINE), ) print(f"Collection '{self.collection_name}' created") except Exception as e: print(f"Error creating collection: {e}") def add_posts(self, posts: List[Dict], embeddings: List[List[float]]): """ Add posts with their embeddings to Qdrant """ points = [] for idx, (post, embedding) in enumerate(zip(posts, embeddings)): point = PointStruct( id=int(post['id']), vector=embedding, payload={ 'title': post['title'], 'content': post['content'], 'excerpt': post['excerpt'], 'url': post['url'], 'date': post['date'], } ) points.append(point) self.client.upsert( collection_name=self.collection_name, points=points, ) print(f"Added {len(points)} posts to Qdrant") def search(self, query_embedding: List[float], limit: int = 5) -> List[Dict]: """ Search for similar posts """ results = self.client.search( collection_name=self.collection_name, query_vector=query_embedding, limit=limit, ) retrieved_posts = [] for result in results: retrieved_posts.append({ 'id': result.id, 'score': result.score, 'title': result.payload['title'], 'content': result.payload['content'], 'url': result.payload['url'], 'date': result.payload['date'], }) return retrieved_posts def get_collection_info(self): """ Get collection statistics """ info = self.client.get_collection(self.collection_name) return info # Test if __name__ == "__main__": manager = QdrantManager() info = manager.get_collection_info() print(f"Collection info: {info}") ``` ## Step 5: Index WordPress Posts into Qdrant ### Create Indexing Script Create `index_posts.py`: ```python import json from fetch_wordpress import WordPressFetcher from embeddings import EmbeddingGenerator from qdrant_manager import QdrantManager import os from dotenv import load_dotenv from html.parser import HTMLParser load_dotenv() class HTMLStripper(HTMLParser): def __init__(self): super().__init__() self.reset() self.strict = False self.convert_charrefs = True self.text = [] def handle_data(self, d): self.text.append(d) def get_data(self): return ''.join(self.text) def strip_html(html): stripper = HTMLStripper() stripper.feed(html) return stripper.get_data() def index_wordpress_posts(): """ Main indexing pipeline """ print("Step 1: Fetching WordPress posts...") wordpress_url = os.getenv('WORDPRESS_URL') fetcher = WordPressFetcher(wordpress_url) raw_posts = fetcher.fetch_all_posts() # Extract and clean content posts = [] for raw_post in raw_posts: post = fetcher.extract_post_content(raw_post) post['content'] = strip_html(post['content']) post['excerpt'] = strip_html(post['excerpt']) posts.append(post) print(f"Extracted {len(posts)} posts") print("\nStep 2: Generating embeddings...") generator = EmbeddingGenerator() # Combine title and content for embedding texts_to_embed = [ f"{post['title']}\n{post['content'][:500]}" # Use first 500 chars for post in posts ] embeddings = generator.generate_batch_embeddings(texts_to_embed) print(f"Generated {len(embeddings)} embeddings") print("\nStep 3: Indexing into Qdrant...") manager = QdrantManager() manager.create_collection(vector_size=1536) manager.add_posts(posts, embeddings) print("\nStep 4: Verifying...") info = manager.get_collection_info() print(f"Collection has {info.points_count} points") print("✓ Indexing complete!") if __name__ == "__main__": index_wordpress_posts() ``` ### Run the Indexer ```bash python index_posts.py ``` This will: - Fetch all posts - Generate embeddings - Store in Qdrant - Display progress ## Step 6: Build the RAG Chatbot ### Create RAG Pipeline Create `rag_chatbot.py`: ```python from embeddings import EmbeddingGenerator from qdrant_manager import QdrantManager from openai import OpenAI from typing import List, Dict, Tuple import os from dotenv import load_dotenv load_dotenv() class RAGChatbot: def __init__(self): self.embedding_generator = EmbeddingGenerator() self.qdrant_manager = QdrantManager() self.llm_client = OpenAI(api_key=os.getenv('OPENAI_API_KEY')) self.model = "gpt-4o-mini" def retrieve_relevant_posts(self, query: str, top_k: int = 5) -> List[Dict]: """ Retrieve relevant posts for a query """ # Generate embedding for the query query_embedding = self.embedding_generator.generate_embedding(query) # Search in Qdrant results = self.qdrant_manager.search(query_embedding, limit=top_k) return results def generate_answer_with_citations( self, query: str, retrieved_posts: List[Dict] ) -> Tuple[str, List[Dict]]: """ Generate answer using LLM with citations """ # Format context from retrieved posts context = self._format_context(retrieved_posts) # Create prompt system_prompt = """You are a helpful assistant answering questions about WordPress blog posts. Use the provided post content to answer questions accurately. Always cite the source posts by title and URL in your response. If you can't find relevant information, say so explicitly.""" user_prompt = f"""Based on the following posts, answer the question: "{query}" POSTS: {context} Please provide a comprehensive answer with citations to the source posts.""" # Call LLM response = self.llm_client.chat.completions.create( model=self.model, messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt} ], temperature=0.7, max_tokens=1000 ) answer = response.choices[0].message.content return answer, retrieved_posts def _format_context(self, posts: List[Dict]) -> str: """ Format retrieved posts into a context string """ context_parts = [] for i, post in enumerate(posts, 1): context_parts.append(f""" Post {i}: {post['title']} URL: {post['url']} Relevance Score: {post['score']:.3f} Content: {post['content'][:800]}... ---""") return "\n".join(context_parts) def chat(self, query: str, top_k: int = 5) ->

26.06.2026