Блог

Инсайты о SaaS, технологиях, маркетинге и создании цифровых продуктов.

Публикации

Бумажная записка «Вы в ответе ИИ?» и два чат-пузыря: ChatGPT назвал три бренда, вас там нет

В ответе ИИ три названия. Как узнать, есть ли там вы

Видимость в ответах Алисы, ChatGPT, Perplexity и нейро-блоках Яндекса можно измерить, а не угадывать. Семь показателей, правило трёх повторов и причина, по которой цифра такая.

02.10.2026
Переезд с WordPress на EmDash: $5 в месяц, страница за 60–100 мс, Lighthouse 100

Cloudflare выпустила EmDash, и это начало конца эпохи WordPress

Cloudflare выпустила EmDash 1.0 — бесплатную CMS на TypeScript, которая метит на место WordPress. Почему это важно, что дал переезд nesmachny.ru и почему мы делаем на ней новые сайты.

29.09.2026
Стилист в Лиссабоне: 17 дней до ChatGPT, 5 визитов, 2 новых клиента

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

Новый домен без ссылок попал в ответы ChatGPT через 17 дней после запуска. Что делали, чем это отличается от SEO и почему AI-трафик нельзя мерить отказами.

11.09.2026
7 альтернатив Google Analytics: аналитика без cookie, данные остаются в России

7 альтернатив Google Analytics с учётом 152-ФЗ (2026)

Сравниваем Web-Metrics, Plausible, Fathom, Simple Analytics, Matomo, Umami и Piwik PRO: хостинг в России, работа без cookies, открытый код, бесплатные тарифы и кому что подходит.

06.09.2026
Клиентский сайт на Cloudflare и EmDash за $5 в месяц: правите сами, почта и CRM встроены

Появилась ли наконец настоящая альтернатива WordPress? (2026)

Я сделал клиентский сайт на Cloudflare и EmDash CMS вместо WordPress: база D1, хранилище R2, встроенные деловая почта и CRM, и всё это за $5 в месяц.

11.08.2026
RAG-чатбот на WordPress за 6 шагов: ответы по вашим статьям со ссылками на источники

Создайте 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
Ваш сайт не видит ChatGPT? 5 проверок и ремонт, а не абонемент

Почему ваш сайт невидим для ChatGPT (и как это исправить за 14 дней)

ИИ-ассистенты теперь решают, кого рекомендовать. Большинство сайтов для них нечитаемы. Вот 5 бесплатных проверок и как работает разовое техническое исправление.

15.06.2026

WordPress как бэкенд для RAG: Как мы это сделали и почему это работает

Как мы сделали из WordPress базу знаний для ИИ-ассистента: публичные и приватные страницы, вебхук, Qdrant и гибридный поиск. Что сработало на практике.

02.05.2026

Как я настроил бесплатную профессиональную почту для своего стартапа с помощью Cloudflare (и вы тоже можете)

Каждый стартап нуждается в профессиональном адресе электронной почты. Никто не воспринимает support@gmail.com серьезно. Но платить $6–12 за пользователя в месяц за Google Workspace или Microsoft 365 только для того, чтобы получать письма на собственном домене, кажется ранней налогом на стартап, который вам не следует…

17.03.2026