Кнопка «Показать полный пост» не работает при установке в подпапке

Недавно мы перенесли установку Discourse в подпапку. После этого кнопка «Показать полный пост» перестала работать — при нажатии для раскрытия содержимого полный пост не загружается.

В конфигурации WP Discourse ничего не менялось.

https://tecnoblog.net/comunidade/t/paramount-oferece-us-108-bilhoes-em-dinheiro-para-tomar-warner-da-netflix/157441

При прямом доступе к URL встраивания в браузере возвращается ошибка 404:

https://tecnoblog.net/comunidade/posts/483289/expand-embed

Это не связано с данной проблемой: этот маршрут отвечает только с типом содержимого application/json. По ссылке https://tecnoblog.net/comunidade/posts/483289/expand-embed.json возвращается:

"<div><div></div></div>\n<hr>\n<small>Это вспомогательная тема обсуждения для оригинальной записи по адресу <a href='https://tecnoblog.net/noticias/paramount-oferece-us-108-bilhoes-em-dinheiro-para-tomar-warner-da-netflix'>https://tecnoblog.net/noticias/paramount-oferece-us-108-bilhoes-em-dinheiro-para-tomar-warner-da-netflix</a></small>\n"

Содержимое должно быть именно <div><div></div></div>.

Не меняли ли вы случайно URL блога?

Мне также кажется странным отображение onebox: я ожидал бы увидеть кэшированный обрезанный контент, поэтому предполагаю, что в приведённом условном выражении body.present? возвращает false.

Можете ли вы зайти в консоль Rails и проверить, показывает ли команда TopicEmbed.where(topic_id: 157441).pick(:embed_url) правильный URL содержимого блога?

Можете ли вы найти какие-либо связанные ошибки на https://tecnoblog.net/comunidade/logs?

О, хорошо!

Возвращает URL поста:

discourse(prod)> TopicEmbed.where(topic_id: 157441).pick(:embed_url)
=> “``https://tecnoblog.net/noticias/paramount-oferece-us-108-bilhoes-em-dinheiro-para-tomar-warner-da-netflix”

Думаю, в логе нет связанных ошибок.

Нет! URL блога всегда был tecnoblog.net

Также стоит упомянуть, что IP-адрес сервера исключён из Firewall в CF:

Мне несколько раз приходилось отлаживать подобные проблемы, и это довольно сложно, так что потерпите меня.

Запустите следующий скрипт и поделитесь здесь его выводом:

# Замените на ID темы или URL, который вы отлаживаете
topic_id = 386983

# 1. Проверьте, существует ли TopicEmbed и его содержимое
te = TopicEmbed.find_by(topic_id: topic_id)
puts "TopicEmbed существует: #{te.present?}"
puts "URL встраивания: #{te&.embed_url}"
puts "Кэш содержимого присутствует: #{te&.embed_content_cache.present?}"
puts "Длина кэша содержимого: #{te&.embed_content_cache&.length || 0}"
puts "SHA1 содержимого: #{te&.content_sha1}"

# 2. Проверьте фактический кэшированный контент (первые 500 символов)
puts "\n--- Предпросмотр кэшированного содержимого ---"
puts te&.embed_content_cache&.truncate(500)

# 3. Попробуйте получить данные с удалённого URL
if te&.embed_url.present?
  puts "\n--- Попытка удалённого получения ---"
  begin
    response = TopicEmbed.find_remote(te.embed_url)
    puts "Удалённое получение успешно: #{response.present?}"
    puts "Тело ответа присутствует: #{response&.body.present?}"
    puts "Длина тела ответа: #{response&.body&.length || 0}"
    puts "Заголовок удалённого ресурса: #{response&.title}"
    puts "Тело удалённого ресурса: #{response&.body&.truncate(500)}"
  rescue => e
    puts "Удалённое получение НЕ УДАЛОСЬ: #{e.message}"
  end
end

# 4. Проверьте, что вернёт expanded_for
if te.present?
  puts "\n--- Тестирование expanded_for ---"
  post = Post.find(te.post_id)

  # Очистите кэш, чтобы принудительно выполнить свежий запрос
  Discourse.cache.delete("embed-topic:#{topic_id}")

  begin
    expanded = TopicEmbed.expanded_for(post)
    puts "Расширенное содержимое присутствует: #{expanded.present?}"
    puts "Длина расширенного содержимого: #{expanded&.length || 0}"
  rescue => e
    puts "expanded_for НЕ УДАЛОСЬ: #{e.message}"
  end
end

# 5. Проверьте соответствующие настройки
puts "\n--- Настройки сайта ---"
puts "embed_truncate: #{SiteSetting.embed_truncate}"
puts "allowed_embed_selectors: #{SiteSetting.allowed_embed_selectors}"
puts "blocked_embed_selectors: #{SiteSetting.blocked_embed_selectors}"

Это покажет, почему https://tecnoblog.net/comunidade/t/governo-renova-app-da-cnh-para-baratear-obtencao-do-documento/157462?u=falco не работает.

discourse(prod)> # Замените на ID темы или URL, который вы отлаживаете
discourse(prod)> topic_id = 386983
discourse(prod)>
discourse(prod)> # 1. Проверьте, существует ли TopicEmbed и его содержимое
discourse(prod)> te = TopicEmbed.find_by(topic_id: topic_id)
discourse(prod)> puts "TopicEmbed существует: #{te.present?}"
discourse(prod)> puts "URL встраивания: #{te&.embed_url}"
discourse(prod)> puts "Кэш содержимого присутствует: #{te&.embed_content_cache.present?}"
discourse(prod)> puts "Длина кэша содержимого: #{te&.embed_content_cache&.length || 0}"
discourse(prod)> puts "SHA1 содержимого: #{te&.content_sha1}"
discourse(prod)>
discourse(prod)> # 2. Проверьте фактический кэшированный контент (первые 500 символов)
discourse(prod)> puts "\n— Предварительный просмотр кэшированного контента —"
discourse(prod)> puts te&.embed_content_cache&.truncate(500)
discourse(prod)>
discourse(prod)> # 3. Попробуйте получить данные с удалённого URL
discourse(prod)> if te&.embed_url.present?
discourse(prod)>   puts "\n— Попытка удалённого запроса —"
discourse(prod)>   begin
discourse(prod)>     response = TopicEmbed.find_remote(te.embed_url)
discourse(prod)>     puts "Удалённый запрос успешен: #{response.present?}"
discourse(prod)>     puts "Тело ответа присутствует: #{response&.body.present?}"
discourse(prod)>     puts "Длина тела ответа: #{response&.body&.length || 0}"
discourse(prod)>     puts "Заголовок удалённого ресурса: #{response&.title}"
discourse(prod)>     puts "Тело удалённого ресурса: #{response&.body&.truncate(500)}"
discourse(prod)>   rescue => e
discourse(prod)>     puts "Удалённый запрос НЕ УДАЛСЯ: #{e.message}"
discourse(prod)>   end
discourse(prod)> end
discourse(prod)>
discourse(prod)> # 4. Проверьте, что вернёт expanded_for
discourse(prod)> if te.present?
discourse(prod)>   puts "\n— Тестирование expanded_for —"
discourse(prod)>   post = Post.find(te.post_id)
discourse(prod)>
discourse(prod)>   # Очистка кэша для принудительного свежего запроса
discourse(prod)>   Discourse.cache.delete("embed-topic:#{topic_id}")
discourse(prod)>
discourse(prod)>   begin
discourse(prod)>     expanded = TopicEmbed.expanded_for(post)
discourse(prod)>     puts "Расширенный контент присутствует: #{expanded.present?}"
discourse(prod)>     puts "Длина расширенного контента: #{expanded&.length || 0}"
discourse(prod)>   rescue => e
discourse(prod)>     puts "expanded_for НЕ УДАЛСЯ: #{e.message}"
discourse(prod)>   end
discourse(prod)> end
discourse(prod)>
discourse(prod)> # 5. Проверьте соответствующие настройки
discourse(prod)> puts "\n— Настройки сайта —"
discourse(prod)> puts "embed_truncate: #{SiteSetting.embed_truncate}"
discourse(prod)> puts "allowed_embed_selectors: #{SiteSetting.allowed_embed_selectors}"
discourse(prod)> puts "blocked_embed_selectors: #{SiteSetting.blocked_embed_selectors}"
TopicEmbed существует: false
URL встраивания:
Кэш содержимого присутствует: false
Длина кэша содержимого: 0
SHA1 содержимого:

— Предварительный просмотр кэшированного контента —

— Настройки сайта —
embed_truncate: true
allowed_embed_selectors:
blocked_embed_selectors:
=> nil
discourse(prod)>

:thinking:

Вы уверены, что это правильный ID темы? https://tecnoblog.net/comunidade/t/-/386983 ведёт на страницу 404.

Вот и всё. Тема, на которую я дал ссылку, на самом деле — 157462.

Моя ошибка!

Вот результаты для правильного ID темы:

TopicEmbed существует: true
URL встраивания: https://tecnoblog.net/noticias/governo-renova-app-da-cnh-para-baratear-obtencao-do-documento
Присутствует кэш контента: true
Длина кэша контента: 22
SHA1 контента:

— Предпросмотр кэшированного контента —

<div><div></div></div>

— Попытка удалённой загрузки —
Удалённая загрузка успешна: true
Присутствует удалённое тело: true
Длина удалённого тела: 22
Удалённый заголовок:
Удалённое тело:

— Тестирование expanded_for —
Присутствует развёрнутый контент: true
Длина развёрнутого контента: 309

— Настройки сайта —
embed_truncate: true
allowed_embed_selectors:
blocked_embed_selectors:
=> nil

Сработал ли ваш обход Cloudflare? Похоже, что тело для https://tecnoblog.net/noticias/governo-renova-app-da-cnh-para-baratear-obtencao-do-documento состоит всего из 22 символов, и в нём нет тега заголовка.

Да! Все запросы с сервера Discourse обходятся:

Я заметил, что URL для встраивания не имеет завершающего слэша. Все URL должны заканчиваться слэшем.

Возможно, Discourse не следует перенаправлению?

Но также, почему URL сохраняется без завершающего слэша?

Это легко проверить, попробуйте

url = "https://tecnoblog.net/noticias/governo-renova-app-da-cnh-para-baratear-obtencao-do-documento/"
response = TopicEmbed.find_remote(url)
puts "Успешный удаленный запрос: #{response.present?}"
puts "Тело удаленного ответа присутствует: #{response&.body.present?}"
puts "Длина тела удаленного ответа: #{response&.body&.length || 0}"
puts "Заголовок удаленного ответа: #{response&.title}"
puts "Тело удаленного ответа: #{response&.body&.truncate(500)}"

Думаю, это работает:

discourse(prod)> url = "https://tecnoblog.net/noticias/governo-renova-app-da-cnh-para-baratear-obtencao-do-documento/"
discourse(prod)> response = TopicEmbed.find_remote(url)
discourse(prod)> puts "Remote fetch success: #{response.present?}"
discourse(prod)> puts "Remote body present: #{response&.body.present?}"
discourse(prod)> puts "Remote body length: #{response&.body&.length || 0}"
discourse(prod)> puts "Remote title: #{response&.title}"
discourse(prod)> puts "Remote body: #{response&.body&.truncate(500)}"
Remote fetch success: true
Remote body present: true
Remote body length: 3776
Remote title: Governo renova app da CNH para baratear obtenção do documento • Tecnoblog
Remote body: 


<figure><img src="https://files.tecnoblog.net/wp-content/uploads/2025/12/cnh-brasil-app-1060x596.jpg">

	<figcaption>Aplicativo CNH do Brasil (imagem: Emerson Alecrim/Tecnoblog)</figcaption></figure>

</div>

<details>
    Resumo
    <div><ul>
<li>App CNH do Brasil substitui CDT e passa a oferecer recursos para obtenção da CNH, em especial, aulas teóricas gratuitas;</li>
<li>Aulas práticas continuam obrigatórias, mas a carga horária mínima foi reduzida de ...
=> nil


А вот без завершающего слэша:

discourse(prod)> url = "https://tecnoblog.net/noticias/governo-renova-app-da-cnh-para-baratear-obtencao-do-documento"
discourse(prod)> response = TopicEmbed.find_remote(url)
discourse(prod)> puts "Remote fetch success: #{response.present?}"
discourse(prod)> puts "Remote body present: #{response&.body.present?}"
discourse(prod)> puts "Remote body length: #{response&.body&.length || 0}"
discourse(prod)> puts "Remote title: #{response&.title}"
discourse(prod)> puts "Remote body: #{response&.body&.truncate(500)}"
Remote fetch success: true
Remote body present: true
Remote body length: 22
Remote title:
Remote body: 
=> nil

Та же ошибка возникает в старых постах, где slug поста изменился.

Например, в этом посте, URL раньше был:

https://tecnoblog.net/486925/o-que-e-pirataria-digital/

Теперь он изменился на:

https://tecnoblog.net/responde/o-que-e-pirataria-digital/

Похоже, это основная проблема. При использовании встраивания комментариев Discourse на другом сайте через JavaScript это контролируется через параметр, и исправить это очень просто.

Я не знаком с тем, как WP-Discourse определяет это; он должен использовать каноническую ссылку поста, но я не уверен в этом. Есть какие-то идеи, @angus?

Есть ли способ заставить Discourse обновить все URL-адреса встраивания из категории, следуя по ним до конечного назначения?

Я планирую перейти на полное встраивание Discourse (то, которое вы тестировали), когда оно будет готово к использованию в продакшене. Но если URL-адреса встраивания не совпадают, это, вероятно, создаст новые темы для каждого сообщения и приведет к потере комментариев…

Выполните

te = TopicEmbed.find_by(topic_id: 157462)
te.embed_url = te.embed_url + "/"
te.save

Помогает ли это исправить https://tecnoblog.net/comunidade/t/governo-renova-app-da-cnh-para-baratear-obtencao-do-documento/157462?

Это работает!

Но есть ли решение для случаев вроде этого?

Gemini предложил следующий код:

# Настройка
CATEGORY_SLUG = 'tb' 
category = Category.find_by(slug: CATEGORY_SLUG)

unless category
  puts "ОШИБКА: Категория '#{CATEGORY_SLUG}' не найдена."
  exit
end

puts "Запуск полного сканирования URL в категории '#{category.name}'..."
puts "Это может занять время в зависимости от количества тем и скорости ответа вашего сайта..."

count_updated = 0
count_errors = 0
count_ok = 0

Topic.where(category_id: category.id).find_each do |topic|
  current_url = topic.custom_fields["embed_url"]
  
  # Пропускаем, если embed_url отсутствует
  next unless current_url.present?

  begin
    # Выполняем GET-запрос с отслеживанием перенаправлений
    response = Faraday.get(current_url)
    final_url = response.env.url.to_s

    # Если запрос успешен (200 OK)
    if response.status == 200
      # Проверяем, отличается ли финальный URL от сохранённого в базе
      # Сравнение может игнорировать незначительные различия при необходимости, но здесь сравниваем строки точно
      if final_url != current_url
        puts "\n[ОБНОВИТЬ] Тема ##{topic.id}:"
        puts "   Было:   #{current_url}"
        puts "   Стало:  #{final_url}"
        
        topic.custom_fields["embed_url"] = final_url
        topic.save_custom_fields(true)
        count_updated += 1
      else
        # print "." # Раскомментируйте для визуального отображения прогресса (точки)
        count_ok += 1
      end
    else
      puts "\n[ОШИБКА HTTP #{response.status}] Тема ##{topic.id} - URL: #{current_url}"
      count_errors += 1
    end

  rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
    puts "\n[ОШИБКА СОЕДИНЕНИЯ] Тема ##{topic.id} - URL: #{current_url} - #{e.message}"
    count_errors += 1
  rescue StandardError => e
    puts "\n[ОБЩАЯ ОШИБКА] Тема ##{topic.id} - #{e.message}"
    count_errors += 1
  end
  
  # Опционально: небольшая пауза, чтобы не перегружать ваш WordPress-сервер
  # sleep 0.1 
end

puts "\n\nИтоговый отчёт:"
puts "------------------------------------------------"
puts "Проверено тем (OK): #{count_ok}"
puts "Обновлено тем:      #{count_updated}"
puts "Найдено ошибок:     #{count_errors}"
puts "------------------------------------------------"

Наконец-то есть прогресс :sweat_smile:

Такой скрипт — отличная идея, просто сделайте резервную копию перед его запуском.

Даже резервная копия только этой небольшой таблицы была бы очень полезна.

Хорошо! Я попробую запустить его позже, когда команда закончит смену.

Привет, ребята, вижу, что проблема с косой чертой в конце снова дала о себе знать :slight_smile:

[Косые черты в конце] — это, похоже, основная проблема. При использовании встраивания комментариев Discourse на другом сайте через JavaScript вы управляете этим через параметр, исправить это очень просто.

Просто для справки: при встраивании тем в Discourse косые черты в конце удаляются из embed_url; см. TopicEmbed.normalize_url. В результате отдельного случая, связанного с пересечением встраивания через JavaScript и встраивания через WP Discourse, мы унифицировали эту обработку для обоих методов встраивания. См. Apply TopicEmbed url normalisation to embed urls inserted in the PostCreator by angusmcleod · Pull Request #30641 · discourse/discourse · GitHub

@Thiago_Mobilon В ходе этого переезда вы также обновили свой Discourse? Возможно, мы наблюдаем применение унификации нормализации embed_url к встраиванию через WP Discourse здесь из-за обновления вашего Discourse, которое произошло одновременно с переездом на установку в подпапку. Какую версию Discourse вы сейчас используете? (и какую версию вы использовали до переезда, если знаете?)

Кстати, когда я запускаю эти две команды локально на последней версии Discourse, я получаю тот же результат, а именно HTML-тело статьи:

# с косой чертой в конце
TopicEmbed.find_remote("https://tecnoblog.net/noticias/governo-renova-app-da-cnh-para-baratear-obtencao-do-documento/")

# без косой черты в конце
TopicEmbed.find_remote("https://tecnoblog.net/noticias/governo-renova-app-da-cnh-para-baratear-obtencao-do-documento")

# результат одинаковый

Возможно, вы внесли какие-то изменения на стороне WordPress?

** редактирование: А, перечитав эту тему чуть внимательнее, я вижу, что ваша проблема, возможно, связана не с переездом Discourse в подпапку или с косыми чертами в конце, а с миграцией ваших URL-адресов WordPress, то есть:

Например, в этом посте URL-адрес раньше был таким:

https://tecnoblog.net/486925/o-que-e-pirataria-digital/

Теперь он изменился на:

https://tecnoblog.net/responde/o-que-e-pirataria-digital/

Так что, возможно, проблема в том, что у вас в topic_embeds.embed_url хранится старая структура URL-адресов, а FinalDestination по какой-то причине не может разрешить новые URL-адреса (например, не может следовать за перенаправлением).

В этом случае вам либо нужно убедиться, что ваши старые URL-адреса блога перенаправляются на новые, либо вам нужно мигрировать topic_embeds.embed_url. Что касается миграции, обратите внимание, что ваш скрипт неверен, например, topic.custom_fields["embed_url"] — это не то место, где хранится embed_url.

Вот что я бы предложил, если вы хотите пойти по пути миграции (вместо перенаправления старых URL-адресов блога на новые). Сначала подтвердите, что проблема заключается в неверном формате URL-адреса блога в topic_embeds.embed_url, посмотрев на пример, например, TopicEmbed.find_by(topic_id: 157441). Затем, если вы увидите, что проблема действительно в том, что в этом столбце сохранен старый формат URL-адресов, выполните это для обновления всех embed_url старого формата в определенной категории:

category_id = # введите здесь id категории
TopicEmbed.joins(:topic).where(topics: { category_id: category_id  }).find_each do |embed|
   new_url = embed.embed_url.sub(%r{/\d+/}, "/responde/")
   embed.update!(embed_url: new_url) if new_url != embed.embed_url
 end

Обратите внимание, что замена регулярным выражением старого формата на новый (sub(%r{/\d+/}, "/responde/")) — это лишь предположение, основанное на предоставленном вами примере. Вы можете протестировать эффект этого на ваших реальных URL-адресах здесь: https://regex101.com/

Привет, Энгус!

Нет, это разные проблемы. Завершающий слэш появился после переноса в подпапку, но также есть старые URL несколькихлетней давности, у которых теперь другие слаг.

Мне пришлось пересобрать установку, так что да, думаю, что этот новый стандарт может быть причиной.

Мое предложение по исправлению этой проблемы: не может ли Discourse выполнять как минимум одно или два перенаправления для получения данных? Это решило бы проблему с завершающим слэшем и сделало бы сайт более надёжным на случай возможных изменений URL в будущем.

Кроме того, это безопаснее, так как не потребуется запускать скрипты для обновления старых тем, которые также могли бы нанести ущерб базе данных.