토픽 임베딩, 좀 더 신경 써 주세요

오늘 Introducing Discourse AI 의 “전체 게시물 보기” 버튼을 클릭한 후 이 문제를 떠올리게 되었습니다. Discourse에 표시되는 전체 게시물에는 모든 이미지와 많은 제목이 누락되어 있습니다. 더욱 혼란스러운 점은 이미지 캡션은 표시되지만 해당 이미지는 함께 표시되지 않는다는 것입니다.

Meta의 (Ghost?) 블로그에서 허용된 임베드 선택자(allowed embed selectors) 사이트 설정을 조정하여 이 문제를 해결할 수 있을 것입니다: Configuring allowed embed selectors. 과거 경험을 통해 이 설정을 조정하는 과정이 까다로울 수 있음을 알고 있습니다. 조정하려는 경우 결과에 주의를 기울이시기 바랍니다.

Discourse는 외부 게시물의 댓글 시스템으로서 상당한 잠재력을 가지고 있지만, 이를 잘 수행하려면 “전체 게시물 보기” 버튼을 클릭할 때 외부 게시물의 모든 요소를 안정적으로 가져와야 합니다. 외부 게시물을 파싱하는 데 사용되는 Ruby Readability gem은 Discourse가 사용하는 용도로 의도된 것이 아니라고 생각합니다. 또한 이 gem은 적극적으로 유지 관리되지도 않고 있습니다: GitHub - cantino/ruby-readability: Port of arc90's readability project to Ruby · GitHub.

3개의 좋아요

네, 이 시점에서는 좀 더 나은 결과를 낼 수 있는 다른 도구로 전환하거나, 전체 게시물 보기를 원본 게시물로 연결되는 단순 링크인 전체 게시물 읽기로 변경하여 임베딩 전략을 바꾸는 것이 좋겠습니다. 결국 모든 웹사이트의 다양한 임베딩 문제와 씨름하는 것은 무의미할 수 있습니다.

4개의 좋아요

@sam 이 방금 수정했으니 확인해 봐.

3개의 좋아요

우리는 곧 Ghost에서 블로그를 출시하고 Ghost > Discourse 통합 기능을 활용하려 합니다. 이 변경 사항을 보게 되어 정말 기쁩니다!

4개의 좋아요

이미지가 이제 가져와지고 있습니다. “차이점 찾기” 유형의 퍼즐에는 잘하지 못하지만, 여전히 몇 가지 차이점을 발견했습니다:

  • Semantic Related Topics 제목 누락
  • Community Sentiment 제목 누락
  • Modules Providers 섹션의 불릿 목록 누락
  • Installing Discourse AI on your community 제목 누락

이상적으로는 “Sign up for our newsletter” 프롬프트가 임베드된 게시물에서 제외되어야 합니다.

임베드된 게시물을 쉽게 인용할 수 있는 기능은 중요해 보입니다. 이 부분을 생각해보니, 임베드된 게시물의 인용문에 대해 “펼치기/접기” 및 “게시물로 이동” 버튼이 클릭되었을 때 예상되는 동작이 무엇인지 확신이 서지 않습니다.

어려운 문제입니다. 게시물에 포함된 article 또는 main 요소의 HTML을 소독(sanitize)하는 것만큼 간단해야 하지만, 그 접근 방식에도 여전히 문제가 있을 것으로 예상됩니다. 예를 들어, header가 article 안에 있는 경우 블로그 게시물의 h1 요소가 중복되는 것을 방지하기 위해 일부 특수 처리가 필요할 것입니다.

1개의 좋아요

이것들은 readability.js에서도 발생하고 있는 것 같습니다. 이는 Firefox 리더 뷰입니다:

<h2 id="installing-discourse-ai-on-your-community">
      <strong>Installing Discourse AI on your community</strong>
</h2>

이를 쉽게 수정할 수 있는 방법이 있는지 확인해 보겠습니다…

이 부분은 확신이 서지 않습니다… 하지만 정말로 그렇게 하고 싶다면 blocked_embed_selectors에 .discourse-newsletter-signup을 추가할 수 있습니다.

4개의 좋아요

네, readablity.js는 https://github.com/cantino/ruby-readability와 동일한 코드 기반으로 만들어졌기 때문에, 해당 요소를 제거하는 데 같은 로직이 사용되고 있을 가능성이 높습니다. 다만 readablity.js는 Ruby Readability보다 일반적으로 더 잘 작동합니다.

이메일 CTA가 혼란스러운 이유는 임베드된 게시물에서 이메일 입력란이 제거되기 때문입니다. 기술적으로는 CTA가 article 안에 있어야 하는지 확신이 서지 않습니다.

1개의 좋아요

어떤 시점에 이 문제를 다시 검토해야 한다는 @simon의 의견에 동의하므로, 이 주제를 다시 올립니다.

WP Discourse 플러그인에 대한 지원 요청의 상당 부분은 어떤 형태로든 리더빌리티 크롤링 문제입니다.

이것이 제 직관을 잘 요약한 것 같습니다.

그럼에도 불구하고, 현재로서는 이것 외에 좋은 해결책을 가지고 있지 않습니다.

하지만 WP Discourse의 지원 업무를 줄일 수 있으므로, 현 상태보다 더 나은 해결책에 기여하는 데 열의를 가지고 있습니다.

1개의 좋아요

이들은 문제를 살펴보고 있지만, 수정은 느립니다…

MiniRacer가 Readability를 래핑하도록 설정하는 것은 그리 어렵지 않습니다 … 저는 이에 대한 프로토타입을 작성해 보았습니다.

이 구현으로 전환할 수도 있지만, 이미 분기(diverged)가 발생했기 때문에 기능을 포기하게 될 것입니다.

이것은 해결하기 쉬운 문제가 아닙니다.

2개의 좋아요

네, 그 점은 공감이 갑니다. 하지만 저는 이게 끝없는 구슬치기 게임이 될 것 같아요. 항상 이런 버전이 존재할 테니까요:

제 웹사이트에서 게시글이 X처럼 보이다가, "전체 게시글 보기"를 클릭하면 Y처럼 보이는데, 둘이 동일하게 보이길 원합니다.

제 더 깊은 질문은, 완벽해질 수 없는 이 기능이

보다 실질적인 이점이 있는가 하는 것입니다.

“전체 게시글 보기” 버튼으로 만들면, 사람들은 디스코스가 결코 완벽하게 제공할 수 없는 수준의 정밀도를 기대하게 됩니다. 제 우려는 주로 기대치 관리에 있습니다.

말씀하시는 것이 임베딩 기능의 제거를 의미하는 것 같습니다. 이에 대해서는 동의하지 않습니다. 매우 지저분한 콘텐츠를 임베딩하는 사이트들은 이 단순한 “원본 링크” 형식을 사용하는 것이 좋다고 생각합니다. 하지만 더 잘 구조화된 콘텐츠를 임베딩하는 사이트들은 완벽하지는 않지만 리더 모드를 활용할 수 있습니다.

1개의 좋아요

반드시 그렇지는 않습니다. 저는 기대치 관리가 더 잘 이루어져야 한다고 말하는 것입니다.

웹사이트를 운영하는 사람들의 99%는 자신의 HTML이 readability 같은 gem으로 쉽게 파싱될 정도로 충분히 의미론적(semantic)인지, 혹은 그것이 기능이 작동하는 방식을 결정하는 요소인지조차 알지 못합니다. 사용자가 자신의 사이트 게시글과 “전체 게시글 보기”를 클릭했을 때 나타나는 콘텐츠 사이에 100%의 일치성이 없으면, 문제는 “Discourse에 있다”(또는 더 자주 WP Discourse 플러그인에 있다)고 가정하는 것이 사용자의 기본 전제입니다.

“전체 게시글 읽기” CTA를 활성화하기 쉽게 만들고, 어쩌면 기본값으로 설정하는 것이 도움이 될 것 같습니다.

2개의 좋아요

제가 이 말로 하려는 것은 Ruby Readability가 "웹페이지의 주요 읽을 수 있는 콘텐츠를 추출하는 도구"라는 것입니다. Discourse에 게시물을 게시하는 사이트의 경우, 웹페이지의 주요 읽을 수 있는 콘텐츠가 무엇인지가 알려져 있고 외부 CSS 선택자로 정의할 수 있다고 보는 것이 안전할 것입니다. 예를 들어, article, .entry-content, .post 등이 있죠.

제가 상상하는 도구는 사이트가 게시물의 콘텐츠를 위한 외부 선택자를 정의할 수 있도록 하고, 그 선택자 안에 포함된 HTML을 정리(sanitize)하는 것입니다. 좀 더 정교한 버전이라면 Discourse에서 제외하고 싶은 내부 선택자를 정의할 수 있도록 할 것입니다.

제 WordPress 사이트에는 완전히 표준적인 마크업을 가진 게시물이 있습니다. 저는 .entry-content div 안에 있는 모든 것을 Discourse에 게시하고 싶습니다. 거의 작동하지만, Discourse의 allowed embed selector 설정을 어떻게 구성해야 게시물의 목록(list) 요소를 가져올 수 있는지 알아내지 못하겠습니다. 제가 사이트들이 이런 문제로 어려움을 겪는 것을 여러 번 보았습니다. Rails.cache.clear를 실행할 수 없다면, 설정을 구성하는 것이 정말 어렵습니다.

게시물을 원박스(onebox)로 게시하는 것은 이 문제에 대한 합리적인 해결책입니다.

수정: debug 옵션은 무슨 일이 일어나고 있는지 파악하는 데 유용합니다: GitHub - cantino/ruby-readability: Port of arc90's readability project to Ruby · GitHub. 제 WordPress 게시물에서 제외된 목록의 경우:

Conditionally cleaned ul#. with weight 0 and content score 0 because it has too many links for its weight (0).

하지만 이것은 완전히 정당한 목록입니다.

확장된 임베드(expanded embeds)와 관련하여 많이 요청되는 기능은 확장된 콘텐츠에 YouTube 비디오가 표시되도록 하는 것입니다. 이를 방지하는 것은 gem에 하드코딩되어 있습니다: ruby-readability/lib/readability.rb at master · cantino/ruby-readability · GitHub. 옵션으로 이 목록을 오버라이드할 수 있도록 PR을 만드는 것이 가치가 있는지 확실하지 않습니다.

2개의 좋아요

이 주제에 대해 너무 깊이 파고들지는 않겠지만, 주말에 다른 용도로 Nokogiri를 사용해 봤습니다. 꽤 중독성이 강하더군요. Nokogiri가 아직 생생하게 기억에 남아 있을 때 임베딩 코드도 한번 살펴볼까 싶었습니다.

제가 이 주제에 관심을 갖는 이유는, 뉴스 및 블로그 사이트에서 Discourse가 더 널리 사용되기를 바라기 때문입니다. 만약 그런 일이 일어난다면, 새로운 사이트 소유자들이 현재 임베딩 기능에 대해 좌절감을 느끼게 될 것이라고 상상할 수 있습니다. 이를 개선하기 위한 아이디어를 하나 제안합니다:

EmbeddableHost 모델에 두 가지 새로운 선택 속성을 추가합니다:

  • target_selector: 임베딩할 콘텐츠를 포함하는 외부 CSS 셀렉터
  • exclude_selectors: target_selector로 선택된 콘텐츠에서 제외할 CSS 셀렉터 목록

관리자/임베딩 페이지의 각 임베더블 호스트 행에 “구성” 버튼이 추가되어야 합니다. 이 버튼을 클릭하면 이메일/미리보기 요약 페이지와 유사한 페이지가 열립니다.

호스트 구성 페이지에는 호스트의 target_selector 및 exclude_selectors 설정을 입력하는 필드와, 제공된 값을 특정 웹 페이지에 대해 테스트할 수 있는 URL 필드가 있는 양식이 있어야 합니다. 테스트는 기본적으로 제공된 target_selector 및 exclude_selectors 값으로 TopicEmbed.parse_html을 실행한 후 결과를 표시하는 것입니다.


parse_html 코드 변경 사항은 테스트하기가 쉽습니다. 가능한 접근 방식을 아래에 제시합니다. 참고로 이 코드는 개념 증명(PoC)일 뿐입니다:

topic_embed.rb에 수정 (discourse/app/models/topic_embed.rb at main · discourse/discourse · GitHub)

###########################################################################
    # `target_selector`와 `exclude_selectors`는 이상적으로 도메인의 `EmbeddableHost` 레코드에서 가져와야 합니다.
    # 이 특정 설정은 boingboing.net에 대한 테스트에 사용되었습니다.
    target_selector = 'article'
    exclude_selectors = ['.article-header, .share-comments-container', '.boing-single-post-rev-content', '.next-post-list-container', '.boing-end-of-article-container-on-single-post-pages']

    if defined?(target_selector) && target_selector.present?
      read_doc = article_content(html, target_selector, exclude_selectors)
    else
      # 호스트에 `target_selector`가 설정되지 않은 경우 Readability로 폴백
      read_doc = Readability::Document.new(html, opts)
    end
    ###########################################################################

새로운 클래스를 생성하지 않고 테스트하기 위해, TopicEmbed 클래스에 추가된 기본적인 article_content 메서드는 다음과 같습니다:

  def self.article_content(html, target_selector, exclude_selectors = [])
    doc = Nokogiri::HTML(html)
    # 주석과 스크립트 태그 제거
    doc.xpath('//comment()').each { |i| i.remove }
    doc.css("script, style").each { |i| i.remove }

    # target_selector에 대한 NodeSet 가져오기
    # 반환된 세트가 비어 있으면 Readability 사용으로 폴백할 수 있음
    selected_nodes = doc.css(target_selector)

    # 노드 제외
    unless exclude_selectors.empty?
      selected_nodes.css(*exclude_selectors).each do |node|
        node.remove
      end
    end

    # 이미지 크기 처리, 개선이 필요할 수 있음
    selected_nodes.css('img').each do |img|
      img.remove_attribute('width')
      img.remove_attribute('height')
    end

    # 단순히 재미로, 소스가 허용되면 iframe 허용
    # remove_empty_nodes 단계에서 iframe이 제거되지 않도록 `[data-sanitized="true"]` 사용
    allowed_iframe_sources = SiteSetting.allowed_iframes.split('|')
    selected_nodes.css('iframe').each do |iframe|
      allowed = allowed_iframe_sources.any? do |allowed_source|
        iframe['src'].start_with?(allowed_source)
      end

      if allowed
        iframe['data-sanitized'] = 'true'
        iframe['width'] = '690'
        iframe['height'] = '388'
      else
        iframe.remove
      end
    end

    # 빈 'p' 및 'div' 노드 제거
    selected_nodes.css('p', 'div').each do |node|
      node.remove if node.content.strip.empty? && !node.at_css('iframe[data-sanitized="true"]')
    end

    # 노드를 문자열로 변환하고 `content` 메서드가 있는 객체 반환
    content = selected_nodes.to_s
    OpenStruct.new(content: content)
  end

여러 도메인에 대해 조금만 더 손봐 주면 정확히 작동할 것이라고 확신합니다. BBS에서 얻고 있는 결과는 지금까지 좋습니다.

목표는 사이트 소유자가 쉽게 이해하고 스스로 구성할 수 있는 것을 만드는 것입니다. 이 접근 방식에서는 target_selector가 càng 구체적일수록 exclude_selectors를 구성하기가 쉬워집니다. 예를 들어, WordPress 사이트에서 .entry-content가 target_selector로 선택된다면 추가 구성은 필요하지 않습니다. 사이트 소유자가 기본적인 .entry-content html보다 더 많은 것을 얻고 싶다면, 호스트 구성 페이지에서 어떻게 해야 하는지 알아낼 수 있습니다.

제가 볼 수 있는 유일한 실질적인 문제는 HTML이 매우 일관되지 않은 호스트에 대한 것입니다. 이 경우 Ruby Readability를 폴백으로 유지하여 처리할 수 있습니다.