서브폴더 설치에서 「전체 게시물 보기」 버튼이 작동하지 않음

최근 Discourse 설치를 서브폴더로 이동했습니다. 그렇게 한 후 “전체 게시물 보기” 버튼이 작동하지 않습니다 – 버튼을 클릭하면 내용이 확장되어야 하지만, 전체 게시물이 로드되지 않습니다.

WP Discourse 설정은 변경한 것이 없습니다.

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

브라우저에서 임베드 URL을 직접 접근하면 404 오류가 반환됩니다:

이것은 관련이 없습니다. 이 라우트는 application/json 콘텐츠 타입으로만 응답합니다. https://tecnoblog.net/comunidade/posts/483289/expand-embed.json 은 다음을 반환하고 있습니다.

"<div><div></div></div>\n<hr>\n<small>Este é um tópico de discussão auxiliar para a entrada original em <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가 CF 방화벽에서 우회(bypass)되고 있다는 점도 언급할 가치가 있습니다:

이 문제를 몇 번이나 이렇게 디버깅해야 해서 복잡합니다. 이해해 주세요.

아래 스크립트를 실행하고 출력을 여기에 공유해 주세요

# 디버깅하려는 토픽 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
Embed 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 "Remote fetch success: #{response.present?}"
puts "Remote body present: #{response&.body.present?}"
puts "Remote body length: #{response&.body&.length || 0}"
puts "Remote title: #{response&.title}"
puts "Remote body: #{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

구글 포스트에서도 포스트 슬러그가 변경된 경우 동일한 오류가 발생합니다.

예를 들어, 이 포스트의 URL은 이전에는 다음과 같았습니다:

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

이제 다음과 같이 변경되었습니다:

겉보기에 이것이 주요 문제인 것 같습니다. Embed Discourse comments on another website via Javascript를 사용할 경우 이 부분은 파라미터로 제어할 수 있어 수정하기가 매우 쉽습니다.

WP-Discourse가 이를 어떻게 결정하는지에 대해서는 잘 모르겠습니다. 게시물 정본(canonical)을 사용해야 하는데, 확신은 없습니다. @angus 님, 혹시 아이디어가 있으신가요?

카테고리의 모든 임베드 URL을 최종 목적지까지 추적하여 Discourse가 이를 업데이트하도록 강제하는 방법이 있을까요?

본격적인 프로덕션 환경에서 사용할 수 있을 때, 제가 테스트해 온 그 완전한 임베드 방식인 embed 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 "'#{category.name}' 카테고리에서 URL 전체 스캔을 시작합니다..."
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이 데이터베이스에 저장된 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 "확인된 토픽 (정상): #{count_ok}"
puts "업데이트된 토픽:      #{count_updated}"
puts "발견된 오류:        #{count_errors}"
puts "------------------------------------------------"

드디어 진전이 있네요 :sweat_smile:

그런 스크립트는 좋은 아이디어이지만, 실행하기 전에 반드시 백업을 만들어 두세요.

이 작은 테이블만이라도 백업해 두면 좋겠습니다.

좋아요! 팀이 퇴근하는 대로 나중에 실행해 볼게요.

여러분, 다시 트레일링 슬래시(trailing slash) 문제가 발생한 것 같네요 :slight_smile:

[트레일링 슬래시가] 주요 문제인 것으로 보입니다. JavaScript를 사용하여 다른 웹사이트에 Discourse 댓글을 임베드할 때는 이를 파라미터로 제어할 수 있으며, 수정하기가 매우 쉽습니다.

참고로 Discourse의 모든 토픽 임베드는 embed_url에서 트레일링 슬래시를 제거합니다. TopicEmbed.normalize_url을 참조하세요. JavaScript 임베드와 WP Discourse 임베드가 교차하는 별도의 사례로 인해, 우리는 두 임베드 방식 모두에서 이 처리 방식을 표준화했습니다. Apply TopicEmbed url normalisation to embed urls inserted in the PostCreator - Pull Request #30641%EC%9D%84 - discourse/discourse - GitHub 참조하세요.

@Thiago_Mobilon 이 과정에서 Discourse도 업데이트하셨나요? Discourse 업데이트가 서브폴더 설치로 이전한 시점과 동시에 이루어졌기 때문에, WP Discourse 임베드에 대한 embed_url 정규화 표준화가 여기에도 적용되고 있는 것으로 보입니다. 현재 실행 중인 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를 서브폴더 설치로 이전하거나 트레일링 슬래시 때문이 아니라, WordPress URL을 마이그레이트한 것 때문일 수 있습니다. 즉,

예를 들어, 이 게시물에서 사용된 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이 새 블로그 URL로 리다이렉트되도록 하거나 topic_embeds.embed_url을 마이그레이션해야 합니다. 마이그레이션 측면에서, 여러분의 스크립트는 부정확합니다. 예를 들어 topic.custom_fields["embed_url"]embed_url이 저장되는 곳이 아닙니다.

리다이렉트 대신 마이그레이션 경로를 선택하시려면 다음을 제안합니다. 먼저 topic_embeds.embed_url의 잘못된 블로그 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/

안녕하세요, Angus!

아니요, 이것은 서로 다른 문제입니다. 서브폴더로 옮긴 시점에 트레일링 슬래시 문제가 시작되었지만, 몇 년 전의 다른 슬러그를 가진 오래된 URL들도 있습니다.

설치를 다시 구축해야 했기 때문에, 네, 아마도 이 새로운 표준이 원인일 것 같습니다.

이 문제를 해결하기 위한 제 제안은 다음과 같습니다: Discourse가 데이터를 가져오기 위해 적어도 한두 번의 리디렉트를 따를 수 있지 않을까요? 그러면 트레일링 슬래시 문제도 해결되고, 향후 발생할 수 있는 URL 변경에도 대비하여 웹사이트의 안정성을 높일 수 있습니다.

또한, 오래된 토픽을 업데이트하기 위해 스크립트를 실행할 필요가 없으므로 더 안전합니다. 그러한 스크립트가 데이터베이스에 손상을 줄 수도 있기 때문입니다.