Ворклоги

Проблема: Google требует обязательные поля для Product в структурированных данных

Суть ошибки

Google Search Console выдаёт ошибку:

"Задайте значение для одного из следующих элементов данных: offers, review или aggregateRating"

Согласно документации Google, для разметки Product обязательно наличие хотя бы одного из:

  • offers — информация о цене и наличии
  • review — отзыв о товаре
  • aggregateRating — средний рейтинг

Без этих данных страница не будет показываться в расширенных результатах поиска (rich snippets).

Типичный кейс

Интернет-магазин с товарами "цена по запросу" или "под заказ". У таких товаров нет фиксированной цены, поэтому разработчик не передаёт offers — и получает ошибку от Google.

Решения

1. Использовать offers без конкретной цены

Schema.org позволяет указать priceSpecification без точной цены:

json{
  "@type": "Product",
  "name": "Детская площадка Premium",
  "offers": {
    "@type": "Offer",
    "availability": "https://schema.org/InStock",
    "priceSpecification": {
      "@type": "PriceSpecification",
      "priceCurrency": "RUB"
    }
  }
}

2. Добавить aggregateRating или review

Если есть система отзывов — использовать её:

json{
  "@type": "Product",
  "name": "Детская площадка Premium",
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": "4.5",
    "reviewCount": "12"
  }
}

3. Не использовать разметку Product для товаров без цены

Если товар не имеет цены и отзывов — можно использовать WebPage вместо Product. Это не даст rich snippets, но и не будет ошибок.

Рекомендация

Для товаров "цена по запросу" лучше всего передавать offers с availability, но без price. Google примет такую разметку, а пользователи увидят информацию о наличии товара.

Сделано.

1. На главной странице в блоке Ворклоги добавил ссылку "Смотреть все".

2. В шапке ссылка Журнал стала вести на раздел ворклогов.

3. На странице ворклога добавил ссылку на задачу, в рамках которой ворклог выполнен

4. Добавил отдельный сайтмап для ворклогов

https://fi1osof.ru/sitemap/worklogs.xml

5. На странице проекта под задачами добавил ссылку "Все задачи проекта".

Теперь можно увидеть все задачи проекта, а не только текущие активные.

Кстати, страниц будет больше. Гугл съел сайтмап и там только задач он видит 90+ страниц.

Теперь важно проработать страницу задачи, чтобы она была более информативная (по какому проекту, какие логи были, более проработанные ворклоги (чтобы было более контентополезно и т.п.)). Без этого есть риск попасть под писсимизацию. То есть если будет много типовых страниц с малым поличеством контента, такое не нравится ни гуглу, ни яндексу.

А вот в гугле ситуация получше, он быстрее подхватил последние изменения и начал выводить новые страницы.

Пока что в яндексе ситуация такая:

Добавлена страница с серверным заголовком "Удалена навсегда".

import { useEffect } from 'react'
import { useRouter } from 'next/router'
import { Page } from 'src/components/pages/_App/interfaces'

export const Redirect410Page: Page = () => {
  const router = useRouter()

  useEffect(() => {
    const timer = setTimeout(() => {
      router.push('/')
    }, 3000)

    return () => clearTimeout(timer)
  }, [router])

  return (
    <div>
      <h1>Удалена навсегда</h1>
      <p>
        Страница была удалена.
        <br />
        Вы будете перенаправлены на главную страницу через 3 секунды...
      </p>
    </div>
  )
}

Redirect410Page.getInitialProps = async ({ res }) => {
  if (res) {
    res.statusCode = 410
  }

  return {}
}

Добавил более провокационное. Сейчас задача - получить обратную связь.

Хостинг был выбран вьетнамский, и это тот еще квест. Надо было найти хоть какой-то хостинг с поддержкой хотя бы английского языка. БЫла найден этот: https://www.pavietnam.vn/en/

Он считается крупнейшим вьетнамским хостингом для частных клиентов, но и тут я как будто в начало 2000-ых попал. Несмотря на заявленную поддержку чего угодно, выбрав при установке убунту 24, по факту, получил я только 18-ую, при чем спустя часа полтора, потому что скорее всего все вручную устанавливали. То есть облаками там и не пахнет. Конфигурацую просто так не поменять, поминутной тарификации конечно же нет (только минимум помесячно. Даже не получится просто так в моменте удалить сервер и не тратить бюджеты). В общем, современный хостинг там пока еще не родился.

Адаптивная верстка совсем плохая. Вот такие бывают варианты на некоторых разрешениях:

3. УРЛы на эстонском языке. Тоже не особо критично, но все-таки правильней было бы на англ, тем более что сайт мультиязычный.

А то как-то странно УРЛ выглядит /ru/elektritööd.html

Кстати, next-js такое не кушает. Думаю, любой react-router тоже, потому что там обычно используют регулярки. Тут вот какой адрес получется:

GET /elektrit%C3%B6%C3%B6d.html 404 

4. Несуществующий lang="zxx"

<html class="no-js" lang="zxx">

Попутно замечания по исходному сайту: 1. Нет меню в мобильной версии. При этом основное меню не видно.

2. Тексты слишком СЕОшные. Сайт всего несколько страниц и рассчитывать на органику нет особого смысла, а вот для тех пользователей, что приходят по прямой ссылке, он почти нечитаемый.

Доперенес все и опубликовал.

Теперь можно новые фичи допиливать.

Нет, совсем не заслуживает внимания. Пережиток прошлого. Много всего, и одновременно почти ничего. Это не готовый продукт, а заготовка, которую надо допиливать под себя, при чем весьма монструозная и неудобная. И даже в докере запустить проблематично. Одна огромная куча легаси.

Пока что просто можно сколько угодно заказов оформлять на один емейл.

Сейчас большая путаница с путями картинок.

В тв-полях картинки когда выбираешь, записывается относительный путь от корня самого медеасурса, а не от корня сайта. Вот к примеру, полный путь: /userfiles/Dopolnitelnoe_oborydovanie/balka-s-kachelyami-i-kolczami-samson.png а записывает Dopolnitelnoe_oborydovanie/balka-s-kachelyami-i-kolczami-samson.png

Логика MODX-а: "А чо такова? Медиасурс по-умолчанию, бери из него".

А то, что медиасурс по-умолчанию можно сменить (и он менялся), это их не волнует. В итоге картинка пока на фронте держится на кеше phpthumb ,а по прямой ссылке не доступна.

В итоге в ресайзере пришлось накостылять

  // Поиск файла с альтернативными префиксами для старых картинок
  if (!fs.existsSync(absPath)) {
    const altPrefixes = ['images_old/', 'userfiles/', 'images_old/userfiles/']
    for (const prefix of altPrefixes) {
      const altPath = resolve(`/uploads/`, prefix + src)
      const altAbsPath = process.cwd() + altPath
      if (fs.existsSync(altAbsPath)) {
        absPath = altAbsPath
        break
      }
    }
  }

Но надо будет еще выпиливать префикс images_old из УРЛов, а для этого вероятно придется и редиректы прописать.

Фронт в общих чертах сделан.

Обновил пакеты, полетела ошибка

server/schema/types/Analyra/resolvers/analyzeWebPageAccesibility/index.ts:199:40 - error TS2739: Type 'Page' is missing the following properties from type 'Page': localStorage, sessionStorage

199 const axe = await new AxeBuilder({ page }).analyze()
~~~~

node_modules/@axe-core/playwright/dist/index.d.ts:5:5
5 page: Page;
~~~~
The expected type comes from property 'page' which is declared here on type 'AxePlaywrightParams'


Found 1 error in server/schema/types/Analyra/resolvers/analyzeWebPageAccesibility/index.ts:199

Проблему не сразу получилось локализовать. Оказывается, обновился axe-core, который входит в состав @axe-core/playwright, хотя сам @axe-core/playwright остался той же версии, и этот axe-core ожидал на вход объект Page с новыми полями, а объект этот создается другой зависимостью - playwright, которая в свою очередь устанавливается вместе с @playwright/test.

npm list playwright
site-boilerplate@1.12.0 /disks/wd-1000/www/analyra.ru/agent
├─┬ @playwright/test@1.61.0
│ └── playwright@1.61.0
└─┬ n8n@2.1.5
  └─┬ @n8n/n8n-nodes-langchain@2.1.4
    └─┬ @langchain/community@1.0.5
      └── playwright@1.61.0 deduped

Вот @playwright/test и надо было обновить и это решило проблему.

Сделано. Коммит.

if (
  (statusCode === 404 || ctx.pathname === '/404') &&
  ctx.req?.url &&
  ctx.res
) {
  const res = ctx.res

  const url = `https://freecode.academy${ctx.req.url}`

  const response = await fetch(url).catch(console.error)

  if (response?.ok) {
    res.writeHead(301, {
      Location: url,
    })
    res.end()
  }
}

cline на первый взгляд очень интересный, особенно его вариант canban. Он позволяет работать с гит-проектами в минималистичном агентом режиме в классической канбан-доске. Идея в том, что каждая задача - это своя карточка. Карточка создается в беклоге, после чего отправляется в работу. Агент выполняет поставленную задачу и карточка переводится в колонку ревью. Там вы можете задачу уже перевести в Завершена или прям там продолжить общение с агентом в рамках задачи. Как по мне, очень удобно.

Page 1 of 3