@operstack/audit 0.33.1 → 0.33.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/prompts.mjs +141 -10
- package/src/test-prompts.mjs +31 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@operstack/audit",
|
|
3
|
-
"version": "0.33.
|
|
3
|
+
"version": "0.33.2",
|
|
4
4
|
"description": "SEO, AEO and GEO audit generator: collect public signals from a site into audit.json, let an analyst or an agent fill the narrative, render the OperStack report as HTML and PDF.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/prompts.mjs
CHANGED
|
@@ -114,6 +114,140 @@ const P = {
|
|
|
114
114
|
ru: { task: 'Возьми страницы короче трёхсот слов и допиши их фактами, которые знает владелец: условия, цены, сроки, ограничения, частые вопросы покупателей с ответами. Не разбавляй текст водой ради объёма. Если фактов нет, выпиши владельцу список вопросов и оставь страницу как есть.', verify: 'Страница стала длиннее трёхсот слов, и каждый добавленный абзац содержит факт, а не общие слова.' },
|
|
115
115
|
en: { task: 'Take the pages under three hundred words and fill them with facts the owner has: terms, prices, timings, limits, the questions buyers actually ask with the answers. Do not pad for length. Where the facts are missing, write the owner a list of questions and leave the page alone.', verify: 'The page is over three hundred words and every added paragraph carries a fact rather than filler.' },
|
|
116
116
|
},
|
|
117
|
+
/*
|
|
118
|
+
* Ниже задания для проверок, у которых до 0.33.2 своего текста не было: «Задача» в файле для
|
|
119
|
+
* клиента повторяла название проверки или пояснение «это не наша работа». Найдено 01.10.2026 на
|
|
120
|
+
* первых настоящих выдачах (dial93.ru, bristolpropertycentre.co.uk). Тест test-prompts.mjs
|
|
121
|
+
* теперь требует задание у каждой проверки, которая может выйти находкой.
|
|
122
|
+
*/
|
|
123
|
+
https: {
|
|
124
|
+
ru: { task: 'Сделай так, чтобы сайт открывался по https:// с действующим сертификатом, а адрес с http:// постоянно (кодом 301) переводил на https://. Сертификат выпускает хостинг: у большинства это одна галочка или бесплатный Let\'s Encrypt в панели. Если сайт сейчас не отвечает вовсе, сначала выясни у хостинга, почему, остальные задачи без этого бессмысленны.', verify: 'Главная открывается по https:// без предупреждения браузера, а http:// отвечает кодом 301 на тот же адрес с https://.' },
|
|
125
|
+
en: { task: 'Make the site open over https:// with a valid certificate, and make the http:// address redirect permanently (301) to https://. The host issues the certificate: on most hosts it is one switch or a free Let\'s Encrypt option in the panel. If the site does not answer at all right now, find out from the host why first; nothing else here matters until it does.', verify: 'The home page opens over https:// with no browser warning, and http:// answers 301 to the same address on https://.' },
|
|
126
|
+
},
|
|
127
|
+
www: {
|
|
128
|
+
ru: { task: 'Выбери один основной адрес сайта (с www или без, тот, что уже стоит в canonical и в карте сайта) и настрой постоянную переадресацию (код 301) со второго варианта на основной, с сохранением пути страницы. Если второй вариант сейчас не открывается вовсе, добавь для него запись в DNS и ту же переадресацию: люди набирают адрес по-разному.', verify: 'Второй вариант адреса, главная и любая внутренняя страница, отвечает кодом 301 на тот же путь на основном адресе.' },
|
|
129
|
+
en: { task: 'Pick one main address for the site (with www or without, whichever is already in the canonical tags and the sitemap) and redirect the other variant to it permanently (301), keeping the page path. If the other variant does not resolve at all, add a DNS record for it and the same redirect: people type the address both ways.', verify: 'The other variant, for the home page and for any inner page, answers 301 to the same path on the main address.' },
|
|
130
|
+
},
|
|
131
|
+
ttfb: {
|
|
132
|
+
ru: { task: 'Сократи время первого ответа главной до одной секунды: включи кеширование готовых страниц (плагин кеша, кеш на уровне хостинга или CDN), убери с главной тяжёлые запросы к базе при каждом открытии. Если сайт на дешёвом общем хостинге и кеш не помогает, напиши владельцу, что нужен тариф быстрее: менять хостинг без его решения нельзя.', verify: 'Три замера подряд (curl -o /dev/null -w "%{time_starttransfer}" адрес главной) показывают меньше 1,2 секунды.' },
|
|
133
|
+
en: { task: 'Bring the home page time to first byte under one second: turn on full-page caching (a cache plugin, host-level cache or a CDN) and remove heavy database work done on every home page load. If the site sits on cheap shared hosting and caching does not help, tell the owner a faster plan is needed: do not move hosts without their decision.', verify: 'Three runs in a row of curl -o /dev/null -w "%{time_starttransfer}" on the home page show under 1.2 seconds.' },
|
|
134
|
+
},
|
|
135
|
+
'robots-block': {
|
|
136
|
+
ru: { task: 'Убери из robots.txt строку Disallow: / в блоке User-agent: *, она запрещает поисковикам читать весь сайт. Закрытыми оставь только служебные адреса (корзина, личный кабинет, поиск по сайту). Если запрет стоял специально, например на тестовой копии, уточни у владельца, прежде чем снимать.', verify: 'В блоке User-agent: * файла /robots.txt нет строки Disallow: /, а главная открыта для чтения роботами.' },
|
|
137
|
+
en: { task: 'Remove Disallow: / from the User-agent: * block in robots.txt: it tells search engines not to read the whole site. Keep only utility addresses closed (cart, account, site search). If the block was put there on purpose, for a staging copy say, check with the owner before lifting it.', verify: 'The User-agent: * block of /robots.txt has no Disallow: / line, and the home page is open to crawlers.' },
|
|
138
|
+
},
|
|
139
|
+
engine: {
|
|
140
|
+
ru: { task: 'Найди в robots.txt правила, которые закрывают роботов, названных в строке «Сейчас», и убери для них Disallow: / (или весь их блок, если он только запрещает). Это роботы, которые достают страницу в момент ответа, а не обучающие: пока они закрыты, этот помощник не может сослаться на сайт. Если запрет поставлен осознанно, уточни у владельца.', verify: 'В /robots.txt для названных роботов нет запрета на весь сайт, и повторная проверка показывает этот помощник открытым.' },
|
|
141
|
+
en: { task: 'Find the robots.txt rules that block the crawlers named under Now and remove their Disallow: / (or their whole block if all it does is block). These crawlers fetch a page while answering, they are not training crawlers: while they are blocked, this assistant cannot cite the site. If the block is deliberate, check with the owner.', verify: '/robots.txt has no site-wide block for the named crawlers, and a fresh check shows this assistant as allowed.' },
|
|
142
|
+
},
|
|
143
|
+
'sitemap-foreign': {
|
|
144
|
+
ru: { task: 'Убери из карты сайта все адреса, которые ведут на другие домены: в sitemap.xml должны быть только страницы этого сайта на его основном адресе. Если карту собирает плагин или сборщик, поправь его настройку, а не файл руками, иначе адреса вернутся при следующей сборке.', verify: 'Все адреса в sitemap.xml начинаются с основного адреса сайта.' },
|
|
145
|
+
en: { task: 'Remove every URL that points at another domain from the sitemap: sitemap.xml should hold only this site\'s pages on its main address. If a plugin or the build generates the sitemap, fix its setting rather than the file by hand, or the URLs come back on the next build.', verify: 'Every URL in sitemap.xml starts with the site\'s main address.' },
|
|
146
|
+
},
|
|
147
|
+
'sitemap-health': {
|
|
148
|
+
ru: { task: 'Пройди по адресам из карты сайта и оставь в ней только те, что отвечают кодом 200. Адреса с переадресацией замени на конечные, мёртвые (404, 410, 5xx) убери. Если мёртвая страница должна существовать, не создавай её сама, а сообщи владельцу списком.', verify: 'Каждый адрес из sitemap.xml отвечает кодом 200 без переадресации.' },
|
|
149
|
+
en: { task: 'Go through the sitemap URLs and keep only those that answer 200. Replace redirecting URLs with their final address and drop dead ones (404, 410, 5xx). If a dead page ought to exist, do not invent it: list it for the owner.', verify: 'Every URL in sitemap.xml answers 200 without a redirect.' },
|
|
150
|
+
},
|
|
151
|
+
'api-catalog': {
|
|
152
|
+
ru: { task: 'Спроси владельца, есть ли у компании публичный API, который могут вызывать агенты. Если нет, ничего не делай: это необязательная строка, и выдумывать каталог ради галочки нельзя. Если есть, положи в /.well-known/api-catalog файл в формате linkset (RFC 9727) со ссылкой на описание API (OpenAPI) и документацию.', verify: 'Либо владелец подтвердил, что API нет, либо /.well-known/api-catalog отвечает кодом 200 с типом application/linkset+json и ссылки в нём открываются.' },
|
|
153
|
+
en: { task: 'Ask the owner whether the business has a public API that agents can call. If not, do nothing: this line is optional, and inventing a catalogue for the tick is wrong. If it does, serve /.well-known/api-catalog as a linkset (RFC 9727) pointing at the API description (OpenAPI) and the docs.', verify: 'Either the owner confirmed there is no API, or /.well-known/api-catalog answers 200 as application/linkset+json and its links open.' },
|
|
154
|
+
},
|
|
155
|
+
'title-all': {
|
|
156
|
+
ru: { task: 'Добавь тег title на страницы, где его нет (их адреса в пояснении к проверке). Заголовок до 60 знаков, свой у каждой страницы, говорит, что на ней, словами, которыми это ищут. Если страницы собраны из одного шаблона, почини шаблон: пусть он берёт заголовок из поля страницы, а не оставляет пустым.', verify: 'На каждой проверенной странице есть непустой title, и он не совпадает с title других страниц.' },
|
|
157
|
+
en: { task: 'Add a title tag to the pages that lack one (their paths are in the note to this check). Keep it under 60 characters, unique per page, saying what is on the page in the words people search with. If the pages share a template, fix the template so it takes the title from the page field instead of leaving it empty.', verify: 'Every sampled page has a non-empty title that no other page shares.' },
|
|
158
|
+
},
|
|
159
|
+
'description-all': {
|
|
160
|
+
ru: { task: 'Добавь meta description на страницы, где его нет: 120-160 знаков своими словами о том, что человек найдёт именно на этой странице. Бери смысл из текста страницы, обещаний, которых на ней нет, не пиши. Если описание пустое из-за шаблона, почини шаблон, чтобы он брал поле описания страницы.', verify: 'На каждой проверенной странице есть meta description длиной от 70 до 160 знаков, и описания не повторяются.' },
|
|
161
|
+
en: { task: 'Add a meta description to the pages that lack one: 120-160 characters in your own words on what a visitor finds on that page. Take the meaning from the page text and promise nothing the page does not have. If the template is why it is empty, fix the template to use the page description field.', verify: 'Every sampled page has a meta description of 70 to 160 characters, and no two are the same.' },
|
|
162
|
+
},
|
|
163
|
+
'h1-sample': {
|
|
164
|
+
ru: { task: 'На страницах, где больше одного H1, оставь один: главный заголовок страницы. Остальные H1 переведи в H2 или в обычный текст. Чаще всего лишний H1 сидит в шапке (логотип или название сайта) или в блоке шаблона, тогда правь шаблон, а не каждую страницу.', verify: 'На каждой проверенной странице ровно один H1.' },
|
|
165
|
+
en: { task: 'On pages with more than one H1, keep one: the main heading of the page. Turn the others into H2 or plain text. The extra H1 usually lives in the header (logo or site name) or in a template block; then fix the template, not every page.', verify: 'Every sampled page has exactly one H1.' },
|
|
166
|
+
},
|
|
167
|
+
'dup-titles': {
|
|
168
|
+
ru: { task: 'Найди страницы с одинаковым title (пример в строке «Сейчас») и дай каждой свой: что отличает эту страницу от соседних (город, услуга, модель, номер страницы списка). Если одинаковый title ставит шаблон, научи его брать заголовок страницы. Если страницы и правда дублируют друг друга, не переименовывай, а сообщи владельцу.', verify: 'Среди проверенных страниц нет двух с одинаковым title.' },
|
|
169
|
+
en: { task: 'Find the pages sharing one title (the example is under Now) and give each its own: what sets the page apart from its neighbours (city, service, model, list page number). If a template stamps the same title, make it use the page heading. If the pages really are duplicates of each other, do not rename them: tell the owner.', verify: 'No two sampled pages share a title.' },
|
|
170
|
+
},
|
|
171
|
+
'og-title': {
|
|
172
|
+
ru: { task: 'Добавь мета-тег og:title на страницы, где его нет. Значение бери из заголовка страницы (H1 или title без названия сайта). Лучше поставить его один раз в общем шаблоне, чтобы он заполнялся у всех страниц сам.', verify: 'На каждой проверенной странице есть <meta property="og:title"> с непустым значением, совпадающим по смыслу с заголовком страницы.' },
|
|
173
|
+
en: { task: 'Add an og:title meta tag to the pages that lack it. Take the value from the page heading (the H1, or the title without the site name). Best set it once in the shared template so every page gets it.', verify: 'Every sampled page has <meta property="og:title"> with a non-empty value matching the page heading.' },
|
|
174
|
+
},
|
|
175
|
+
'og-image': {
|
|
176
|
+
ru: { task: 'Добавь мета-тег og:image на страницы, где его нет: полный адрес картинки размером не меньше 1200 на 630 точек. Для страниц со своей главной картинкой бери её, для остальных одну общую (логотип на фоне или фото компании, которое уже есть на сайте). Новых изображений не рисуй и чужих не бери.', verify: 'На каждой проверенной странице есть og:image с полным адресом, и картинка по нему открывается с кодом 200.' },
|
|
177
|
+
en: { task: 'Add an og:image meta tag to the pages that lack it: the full URL of an image at least 1200 by 630 pixels. Pages with their own main image use it; the rest use one shared image (the logo on a background, or a company photo already on the site). Do not make up new images or take other people\'s.', verify: 'Every sampled page has og:image with a full URL, and the image opens with a 200.' },
|
|
178
|
+
},
|
|
179
|
+
'twitter-card': {
|
|
180
|
+
ru: { task: 'Добавь в общий шаблон мета-тег twitter:card со значением summary_large_image. Заголовок, описание и картинку X возьмёт из og-тегов, отдельно их дублировать не нужно, если og-теги уже стоят.', verify: 'В HTML главной есть <meta name="twitter:card" content="summary_large_image">.' },
|
|
181
|
+
en: { task: 'Add a twitter:card meta tag with the value summary_large_image to the shared template. X takes the title, description and image from the og tags, so they need no duplicates if the og tags are in place.', verify: 'The home page HTML has <meta name="twitter:card" content="summary_large_image">.' },
|
|
182
|
+
},
|
|
183
|
+
'schema-all': {
|
|
184
|
+
ru: { task: 'Добавь разметку JSON-LD на страницы, где её нет (адреса в пояснении). Тип выбирай по тому, что на странице на самом деле: статья (Article), услуга (Service), товар с ценой (Product), страница контактов (ContactPage), остальные хотя бы WebPage с BreadcrumbList. Значения бери только со страницы, ничего не дописывай от себя.', verify: 'На каждой проверенной странице есть хотя бы один блок JSON-LD, и валидатор schema.org не показывает ошибок.' },
|
|
185
|
+
en: { task: 'Add JSON-LD markup to the pages that lack it (paths in the note). Choose the type from what the page really is: an article (Article), a service (Service), a product with a price (Product), a contact page (ContactPage), and at least WebPage with BreadcrumbList for the rest. Take every value from the page itself, add nothing of your own.', verify: 'Every sampled page has at least one JSON-LD block, and the schema.org validator shows no errors.' },
|
|
186
|
+
},
|
|
187
|
+
'breadcrumbs-schema': {
|
|
188
|
+
ru: { task: 'Добавь на внутренние страницы разметку BreadcrumbList в JSON-LD: путь от главной до текущей страницы, каждый шаг с названием и полным адресом. Путь строй по настоящей структуре разделов сайта; если на странице уже видны хлебные крошки, разметка должна повторять их слово в слово.', verify: 'На внутренних проверенных страницах есть JSON-LD с типом BreadcrumbList, все адреса в нём открываются, валидатор не показывает ошибок.' },
|
|
189
|
+
en: { task: 'Add BreadcrumbList JSON-LD to the inner pages: the path from the home page to the current one, each step with a name and a full URL. Build the path from the site\'s real section structure; if the page already shows visible breadcrumbs, the markup must match them word for word.', verify: 'Inner sampled pages carry JSON-LD of type BreadcrumbList, every URL in it opens, and the validator shows no errors.' },
|
|
190
|
+
},
|
|
191
|
+
'org-schema': {
|
|
192
|
+
ru: { task: 'Добавь в общий шаблон (или хотя бы на главную) разметку организации в JSON-LD: Organization или более точный тип (LocalBusiness, RealEstateAgent и подобные), с названием, адресом сайта, логотипом, контактами и ссылками на профили в sameAs. Заполняй только тем, что есть на сайте или что подтвердил владелец, остальные поля оставь пустыми.', verify: 'На главной есть JSON-LD с типом Organization или его подтипом, в нём name и url, и валидатор не показывает ошибок.' },
|
|
193
|
+
en: { task: 'Add organisation JSON-LD to the shared template (or at least the home page): Organization or a more precise type (LocalBusiness, RealEstateAgent and the like), with the name, site address, logo, contacts and profile links in sameAs. Fill in only what is on the site or confirmed by the owner; leave the other fields out.', verify: 'The home page has JSON-LD of type Organization or a subtype, with name and url, and the validator shows no errors.' },
|
|
194
|
+
},
|
|
195
|
+
favicon: {
|
|
196
|
+
ru: { task: 'Добавь значок сайта: файл favicon (лучше SVG и запасной PNG 32 на 32 или ICO) из логотипа компании, и ссылку <link rel="icon"> на него в общем шаблоне. Логотип бери тот, что уже есть на сайте, новый не рисуй.', verify: 'В HTML главной есть <link rel="icon">, и файл по ссылке открывается с кодом 200.' },
|
|
197
|
+
en: { task: 'Add a site icon: a favicon file (SVG, plus a 32 by 32 PNG or an ICO as a fallback) made from the company logo, and a <link rel="icon"> to it in the shared template. Use the logo already on the site, do not draw a new one.', verify: 'The home page HTML has <link rel="icon">, and the file it points at opens with a 200.' },
|
|
198
|
+
},
|
|
199
|
+
'redirect-chain': {
|
|
200
|
+
ru: { task: 'Сделай так, чтобы адрес главной, который набирает человек, вёл на конечный адрес одной переадресацией, а не цепочкой (например http://, потом www, потом косая черта). Замени цепочку одним правилом 301 сразу на конечный адрес для каждого варианта.', verify: 'curl -sIL на адрес без www и без https показывает не больше одной переадресации до ответа 200.' },
|
|
201
|
+
en: { task: 'Make the home page address people type reach its final address in one redirect instead of a chain (http://, then www, then a trailing slash, say). Replace the chain with one 301 rule straight to the final address for each variant.', verify: 'curl -sIL on the address without www and without https shows at most one redirect before the 200.' },
|
|
202
|
+
},
|
|
203
|
+
resources: {
|
|
204
|
+
ru: { task: 'Сократи число скриптов, которые грузит главная: убери подключения неиспользуемых плагинов и виджетов, объедини мелкие файлы при сборке, несрочные скрипты подключи с defer или после загрузки. Ничего не удаляй вслепую: что нужно для форм, оплаты и счётчиков, оставь и перечисли владельцу, что убрано.', verify: 'Главная грузит не больше 15 файлов скриптов, а формы, меню и счётчики продолжают работать.' },
|
|
205
|
+
en: { task: 'Cut the number of scripts the home page loads: drop plugins and widgets nobody uses, bundle small files at build, load non-urgent scripts with defer or after load. Remove nothing blindly: keep whatever forms, payments and counters need, and list for the owner what you removed.', verify: 'The home page loads at most 15 script files, and forms, menus and counters still work.' },
|
|
206
|
+
},
|
|
207
|
+
xmlrpc: {
|
|
208
|
+
ru: { task: 'Закрой точку xmlrpc.php в WordPress: плагином безопасности, фильтром xmlrpc_enabled в теме или правилом сервера, которое отдаёт 403. Сначала уточни у владельца, не публикует ли он записи через мобильное приложение WordPress или Jetpack: им xmlrpc нужен.', verify: 'Запрос к /xmlrpc.php отвечает кодом 403 или 404.' },
|
|
209
|
+
en: { task: 'Close WordPress xmlrpc.php: with a security plugin, the xmlrpc_enabled filter in the theme, or a server rule that returns 403. First ask the owner whether they publish through the WordPress mobile app or Jetpack: those need xmlrpc.', verify: 'A request to /xmlrpc.php answers 403 or 404.' },
|
|
210
|
+
},
|
|
211
|
+
utility: {
|
|
212
|
+
ru: { task: 'Исключи из карты сайта служебные адреса: корзину, избранное, личный кабинет, страницы тегов и авторов, результаты поиска. Людям они пусть остаются доступны, убрать их нужно только из sitemap.xml (а страницам тегов и поиска ещё поставить noindex). Правь настройку плагина или сборщика карты, а не файл руками.', verify: 'В sitemap.xml нет адресов корзины, избранного, кабинета, тегов, авторов и поиска.' },
|
|
213
|
+
en: { task: 'Drop utility URLs from the sitemap: cart, wishlist, account, tag and author pages, search results. Leave them reachable for people; only take them out of sitemap.xml (and set noindex on tag and search pages). Change the sitemap plugin or build setting, not the file by hand.', verify: 'sitemap.xml lists no cart, wishlist, account, tag, author or search URLs.' },
|
|
214
|
+
},
|
|
215
|
+
viewport: {
|
|
216
|
+
ru: { task: 'Поставь в общий шаблон мета-тег <meta name="viewport" content="width=device-width, initial-scale=1">. Если в нём сейчас есть user-scalable=no или maximum-scale=1, убери их: запрет масштабирования мешает людям со слабым зрением и вредит мобильной выдаче.', verify: 'В HTML главной стоит viewport с width=device-width и без user-scalable=no и maximum-scale=1.' },
|
|
217
|
+
en: { task: 'Put <meta name="viewport" content="width=device-width, initial-scale=1"> in the shared template. If it currently has user-scalable=no or maximum-scale=1, remove them: blocking zoom hurts people with weak eyesight and mobile rankings.', verify: 'The home page HTML has a viewport with width=device-width and no user-scalable=no or maximum-scale=1.' },
|
|
218
|
+
},
|
|
219
|
+
'trust-profiles': {
|
|
220
|
+
ru: { task: 'Спроси владельца, где у компании есть настоящие профили: соцсети, каталоги, карты, отраслевые площадки. Впиши их адреса в поле sameAs разметки организации и поставь ссылки на них в подвал. Профили, которых нет или которые заброшены, не добавляй.', verify: 'В JSON-LD организации есть sameAs хотя бы с двумя адресами, и каждый открывает профиль именно этой компании.' },
|
|
221
|
+
en: { task: 'Ask the owner where the business really has profiles: social networks, directories, maps, industry sites. Put those addresses in the sameAs field of the organisation markup and link them from the footer. Do not add profiles that do not exist or are abandoned.', verify: 'The organisation JSON-LD has sameAs with at least two addresses, and each opens a profile of this very business.' },
|
|
222
|
+
},
|
|
223
|
+
'trust-about': {
|
|
224
|
+
ru: { task: 'Сделай страницу «О компании» (или «Команда») и поставь на неё ссылку в меню или подвал, чтобы она была видна со всех страниц. На странице: кто стоит за сайтом, чем занимается, с какого года, где находится, как связаться. Всё только со слов владельца или из того, что уже опубликовано; если страница уже есть, просто добавь ссылку.', verify: 'Со всех проверенных страниц есть ссылка на страницу о компании, и она открывается с кодом 200.' },
|
|
225
|
+
en: { task: 'Make an About (or Team) page and link it from the menu or footer so it is reachable from every page. On it: who is behind the site, what they do, since when, where, how to get in touch. Use only what the owner tells you or what is already published; if the page exists, just add the link.', verify: 'Every sampled page links to the about page, and it opens with a 200.' },
|
|
226
|
+
},
|
|
227
|
+
'trust-policies': {
|
|
228
|
+
ru: { task: 'Поставь в подвал ссылки на политику конфиденциальности и на условия (оферту или пользовательское соглашение). Если документов нет, не сочиняй их: юридический текст за владельцем, сообщи ему, каких страниц не хватает. Если документы есть, но не связаны с сайтом, просто добавь ссылки.', verify: 'В подвале каждой страницы есть ссылки на политику конфиденциальности и на условия, и обе открываются с кодом 200.' },
|
|
229
|
+
en: { task: 'Link the privacy policy and the terms from the footer. If the documents do not exist, do not write them yourself: legal text is the owner\'s call, so tell them which pages are missing. If they exist but are not linked, just add the links.', verify: 'Every page footer links to a privacy policy and to terms, and both open with a 200.' },
|
|
230
|
+
},
|
|
231
|
+
'conv-contact-path': {
|
|
232
|
+
ru: { task: 'Сделай так, чтобы с любой страницы можно было связаться: ссылка «Контакты» в меню или подвале, телефон или мессенджер в шапке, либо короткая форма. Используй только те контакты, которые уже есть на сайте или которые дал владелец.', verify: 'На каждой проверенной странице есть форма, ссылка на контакты или прямой контакт (tel:, mailto:, мессенджер).' },
|
|
233
|
+
en: { task: 'Make it possible to get in touch from any page: a Contact link in the menu or footer, a phone or messenger in the header, or a short form. Use only contacts already on the site or given by the owner.', verify: 'Every sampled page has a form, a contact link or a direct contact (tel:, mailto:, a messenger).' },
|
|
234
|
+
},
|
|
235
|
+
'conv-form-depth': {
|
|
236
|
+
ru: { task: 'Подними форму заявки выше на длинных страницах: либо перенеси её в первую треть, либо поставь вверху кнопку, которая прокручивает к форме. Сами поля и текст формы не меняй.', verify: 'На проверенных страницах форма или кнопка, ведущая к ней, видна в первой половине страницы, в том числе на телефоне.' },
|
|
237
|
+
en: { task: 'Move the enquiry form higher on long pages: either into the first third, or add a button near the top that scrolls to it. Do not change the form fields or wording.', verify: 'On the sampled pages the form, or a button leading to it, sits in the first half of the page, on a phone too.' },
|
|
238
|
+
},
|
|
239
|
+
'conv-form-fields': {
|
|
240
|
+
ru: { task: 'Сократи форму заявки до четырёх полей или меньше: имя и один способ связи (телефон или мессенджер, на выбор), остальное необязательным или убрать. Какие поля нужны для работы, решает владелец: перечисли ему, что предлагаешь убрать, и убирай после его ответа.', verify: 'В форме не больше четырёх полей, и заявка с заполненными обязательными полями доходит до владельца.' },
|
|
241
|
+
en: { task: 'Cut the enquiry form to four fields or fewer: a name and one way to reach them (phone or messenger, their choice), the rest optional or gone. The owner decides which fields the business needs: list what you propose to drop and drop it once they agree.', verify: 'The form has at most four fields, and a submission with the required fields reaches the owner.' },
|
|
242
|
+
},
|
|
243
|
+
sections: {
|
|
244
|
+
ru: { task: 'Разбей длинные страницы на разделы: каждый начинается заголовком H2, который звучит как вопрос или тема раздела. На странице должно быть не меньше трёх таких разделов. Ради объёма ничего не дописывай, только структурируй то, что есть.', verify: 'На каждой проверенной странице не меньше трёх заголовков H2, и по ним понятно, о чём страница, без чтения текста.' },
|
|
245
|
+
en: { task: 'Split the long pages into sections, each opening with an H2 that reads as the question or topic of that section. A page needs at least three. Add nothing for length, only structure what is there.', verify: 'Every sampled page has at least three H2 headings, and they alone tell what the page is about.' },
|
|
246
|
+
},
|
|
247
|
+
tables: {
|
|
248
|
+
ru: { task: 'Там, где на странице есть цифры, которые сравнивают (цены, сроки, площади, условия), собери их в таблицу с подписанными столбцами. Строй её только из данных, которые уже есть на странице или у владельца.', verify: 'На страницах со сравниваемыми цифрами есть хотя бы одна настоящая таблица (тег table), а не картинка и не список.' },
|
|
249
|
+
en: { task: 'Where a page has figures that get compared (prices, timings, areas, terms), put them in a table with labelled columns. Build it only from data already on the page or held by the owner.', verify: 'Pages with comparable figures carry at least one real table (a table tag), not an image and not a list.' },
|
|
250
|
+
},
|
|
117
251
|
};
|
|
118
252
|
|
|
119
253
|
const withLang = (v, lang) => v[lang === 'ru' ? 'ru' : 'en'];
|
|
@@ -202,10 +336,13 @@ function whereLine(id, platform, lang) {
|
|
|
202
336
|
return `**${head}:** ${Object.entries(map).map(([k, v]) => `${PLATFORM_LABEL[k]}, ${v[lang]}`).join('; ')}`;
|
|
203
337
|
}
|
|
204
338
|
|
|
339
|
+
/** Строки «помощник может читать сайт» идут по одной на помощника (engine-chatgpt и т.д.), задание у них общее. */
|
|
340
|
+
const promptKey = (id) => (String(id || '').startsWith('engine-') ? 'engine' : id);
|
|
341
|
+
|
|
205
342
|
export function agentPrompt(check, opts = {}) {
|
|
206
343
|
const lang = opts.lang === 'ru' ? 'ru' : 'en';
|
|
207
344
|
const h = HEAD[lang];
|
|
208
|
-
const own = P[check.id] ? withLang(P[check.id], lang) : null;
|
|
345
|
+
const own = P[promptKey(check.id)] ? withLang(P[promptKey(check.id)], lang) : null;
|
|
209
346
|
const rule = FIX_ACTIONS[check.id];
|
|
210
347
|
const fallback = rule && rule.action ? withLang(rule.action, lang) : (rule && rule.why ? withLang(rule.why, lang) : check.label);
|
|
211
348
|
|
|
@@ -230,7 +367,7 @@ export function agentPrompts(audit, opts = {}) {
|
|
|
230
367
|
const ordered = [...checks.filter((c) => c.status === 'bad'), ...checks.filter((c) => c.status === 'warn')];
|
|
231
368
|
const chosen = limit > 0 ? ordered.slice(0, limit) : ordered;
|
|
232
369
|
const platform = opts.platform || detectPlatform(audit);
|
|
233
|
-
return chosen.map((c) => ({ id: c.id, hasOwnText: Boolean(P[c.id]), text: agentPrompt(c, { lang, platform }) }));
|
|
370
|
+
return chosen.map((c) => ({ id: c.id, hasOwnText: Boolean(P[promptKey(c.id)]), text: agentPrompt(c, { lang, platform }) }));
|
|
234
371
|
}
|
|
235
372
|
|
|
236
373
|
/** Файл, который отдаётся клиенту вместе с отчётом. */
|
|
@@ -276,6 +413,8 @@ const FIRST_FIX_BY_PROMPT = {
|
|
|
276
413
|
'answer-first-missing': 'answer-first',
|
|
277
414
|
'sources-missing': 'sources',
|
|
278
415
|
'thin-pages': 'thin',
|
|
416
|
+
'few-h2': 'sections',
|
|
417
|
+
'no-tables': 'tables',
|
|
279
418
|
};
|
|
280
419
|
|
|
281
420
|
const FIRST_FIX_OWN = {
|
|
@@ -299,14 +438,6 @@ const FIRST_FIX_OWN = {
|
|
|
299
438
|
ru: { task: 'Добавь на страницы статей и записей разметку Article или BlogPosting в JSON-LD: заголовок, дата публикации, дата изменения, автор, издатель. Бери данные со страницы, ничего не придумывай.', verify: 'На странице статьи есть JSON-LD с типом Article или BlogPosting, и проверка разметки не показывает ошибок.' },
|
|
300
439
|
en: { task: 'Add Article or BlogPosting markup in JSON-LD to the article pages: headline, date published, date modified, author, publisher. Take the data from the page, invent nothing.', verify: 'An article page carries JSON-LD of type Article or BlogPosting and the markup validator shows no errors.' },
|
|
301
440
|
},
|
|
302
|
-
'few-h2': {
|
|
303
|
-
ru: { task: 'Разбей длинные страницы на разделы: каждый начинается заголовком H2, который звучит как вопрос или тема раздела. На странице должно быть не меньше трёх таких разделов. Ради объёма ничего не дописывай, только структурируй то, что есть.', verify: 'На каждой проверенной странице не меньше трёх заголовков H2, и по ним понятно, о чём страница, без чтения текста.' },
|
|
304
|
-
en: { task: 'Split the long pages into sections, each opening with an H2 that reads as the question or topic of that section. A page needs at least three. Add nothing for length, only structure what is there.', verify: 'Every sampled page has at least three H2 headings, and they alone tell what the page is about.' },
|
|
305
|
-
},
|
|
306
|
-
'no-tables': {
|
|
307
|
-
ru: { task: 'Там, где на странице есть цифры, которые сравнивают (цены, сроки, площади, условия), собери их в таблицу с подписанными столбцами. Строй её только из данных, которые уже есть на странице или у владельца.', verify: 'На страницах со сравниваемыми цифрами есть хотя бы одна настоящая таблица (тег table), а не картинка и не список.' },
|
|
308
|
-
en: { task: 'Where a page has figures that get compared (prices, timings, areas, terms), put them in a table with labelled columns. Build it only from data already on the page or held by the owner.', verify: 'Pages with comparable figures carry at least one real table (a table tag), not an image and not a list.' },
|
|
309
|
-
},
|
|
310
441
|
'sitemap-no-lastmod': {
|
|
311
442
|
ru: { task: 'Проставь в карте сайта (sitemap.xml) каждой странице поле lastmod с настоящей датой последнего изменения содержимого. Дату сборки или сегодняшнее число во все строки не ставь: это хуже, чем пустое поле.', verify: 'У каждого url в sitemap.xml есть lastmod, даты различаются между страницами и совпадают с датой изменения на самой странице.' },
|
|
312
443
|
en: { task: 'Give every page in sitemap.xml a lastmod with the real date its content last changed. Do not stamp the build date or today on every line: that is worse than leaving it empty.', verify: 'Every url in sitemap.xml has a lastmod, the dates differ between pages and match the modified date on the page itself.' },
|
package/src/test-prompts.mjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
4
|
import { agentPrompt, agentPrompts, renderAgentPrompts, firstFixParts, FIRST_FIX_IDS } from './prompts.mjs';
|
|
5
5
|
import { FIX_ACTIONS } from './fix.mjs';
|
|
6
|
+
import { localiseChecks } from './i18n.mjs';
|
|
6
7
|
|
|
7
8
|
let bad = 0;
|
|
8
9
|
const is = (n, a, b) => { const ok = JSON.stringify(a) === JSON.stringify(b); if (!ok) { bad++; console.error(`FAIL ${n}\n ждали ${JSON.stringify(b)}\n вышло ${JSON.stringify(a)}`); } else console.log(`ok ${n}`); };
|
|
@@ -20,7 +21,7 @@ ok('в задании есть запрет выдумывать', one.includes(
|
|
|
20
21
|
// Даже там, где своего текста нет, все три части на месте
|
|
21
22
|
const generic = agentPrompt(c('xmlrpc', 'warn', 'xmlrpc.php', 'открыт'), { lang: 'ru' });
|
|
22
23
|
ok('общее задание тоже с проверкой', generic.includes('**Как проверить:**'));
|
|
23
|
-
ok('и
|
|
24
|
+
ok('и со своим действием', generic.includes('Закрой точку xmlrpc.php'));
|
|
24
25
|
|
|
25
26
|
// Порядок: сначала провалы, потом предупреждения
|
|
26
27
|
const list = agentPrompts(audit([c('og', 'warn', 'OG', 'нет'), c('robots', 'bad', 'robots', 'закрыт')]), { lang: 'ru' });
|
|
@@ -108,5 +109,34 @@ ok('в файле нет пустых сдвоенных строк', !/\n\n\n/.
|
|
|
108
109
|
ok('незнакомый язык падает в английский', firstFixParts({ id: 'llms-missing', text: 'x' }, { lang: 'de' }).labels.now === 'Now');
|
|
109
110
|
}
|
|
110
111
|
|
|
112
|
+
/*
|
|
113
|
+
* У каждой проверки, которая может выйти находкой, своё задание. 01.10.2026 в первых настоящих
|
|
114
|
+
* выдачах (dial93.ru, bristolpropertycentre.co.uk) «Задача» у части проверок просто повторяла
|
|
115
|
+
* название, а «Как проверить» была общей фразой «прогони проверку заново». Список проверок берём
|
|
116
|
+
* прямо из collect.mjs, чтобы новая проверка без задания роняла этот тест.
|
|
117
|
+
*/
|
|
118
|
+
{
|
|
119
|
+
const src = readFileSync(new URL('./collect.mjs', import.meta.url), 'utf8');
|
|
120
|
+
const rows = [...src.matchAll(/row\(\s*['`]([^'`]+)['`]\s*,\s*'([a-z]+)'\s*,\s*['`"]([^'`"]*)['`"]/g)]
|
|
121
|
+
.map((m) => ({ id: m[1].replace('${e.id}', 'chatgpt'), label: m[3].replace('${e.label}', 'ChatGPT') }));
|
|
122
|
+
rows.push({ id: 'js-content', label: 'Text visible without running JavaScript' });
|
|
123
|
+
// Эти строки выходят только со статусом na (справка или осознанный выбор), находкой не бывают.
|
|
124
|
+
const NEVER_OPEN = new Set(['cms', 'robots-ai', 'howto-schema', 'product-schema', 'hreflang', 'links-profile']);
|
|
125
|
+
ok('список проверок из collect.mjs прочитан', rows.length > 40);
|
|
126
|
+
const field = (text, name) => (text.match(new RegExp(`\\*\\*${name}:\\*\\* (.*)`)) || [])[1] || '';
|
|
127
|
+
const empty = [];
|
|
128
|
+
for (const r of rows.filter((x) => !NEVER_OPEN.has(x.id))) {
|
|
129
|
+
for (const lang of ['ru', 'en']) {
|
|
130
|
+
const [chk] = lang === 'ru' ? localiseChecks([c(r.id, 'warn', r.label, 'x')], 'ru') : [c(r.id, 'warn', r.label, 'x')];
|
|
131
|
+
const t = agentPrompt(chk, { lang });
|
|
132
|
+
const task = field(t, lang === 'ru' ? 'Задача' : 'Task').trim();
|
|
133
|
+
const verify = field(t, lang === 'ru' ? 'Как проверить' : 'How to check').trim();
|
|
134
|
+
const generic = lang === 'ru' ? /^Прогони проверку сайта заново/ : /^Run the site check again/;
|
|
135
|
+
if (!task || task === chk.label || task === r.label || task.length < 60 || !verify || generic.test(verify)) empty.push(`${r.id}/${lang}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
is('у каждой проверки своё задание и своя проверка, не название', empty, []);
|
|
139
|
+
}
|
|
140
|
+
|
|
111
141
|
if (bad) { console.error(`\n${bad} тест(ов) упало`); process.exit(1); }
|
|
112
142
|
console.log('\nвсе тесты заданий прошли');
|