Встраивайте комментарии из Discourse в ваше одностраничное приложение

При попытке внедрения [1] в ваше одностраничное приложение (SPA), которое не поддерживает серверный рендеринг страниц, вы неизбежно столкнётесь с проблемами, см.: [2]

Поэтому после некоторых экспериментов я хочу предложить следующий подход. Пример приведён для Vue.js, однако его можно легко адаптировать под другие фреймворки/библиотеки.

Примечание: Я буду использовать термин статьи блога, где должен быть встроен раздел комментариев Discourse. Но, конечно, это также могут быть отдельные страницы вашего сайта.

1. Проблемы в [1]

1.1. javascripts/embed.js не может работать с контентом, рендерящимся на стороне клиента

Фрагмент <script>...</script>, который предлагается в [1] вставить в ваш HTML, не будет частью реализации, к которой мы стремимся здесь. Мы будем использовать некоторые части javascripts/embed.js, предоставляемые вашим экземпляром Discourse, в качестве функций внутри нашего SPA.

1.2. Discourse не может парсить контент, рендерящийся на стороне клиента

Discourse автоматически создаёт темы для каждой статьи блога и пытается получить доступ к исходному URL (статьи блога), чтобы определить заголовок и содержимое. Это не работает с SPA, потому что Discourse получит часть без JavaScript, например: Нам очень жаль, но этот сайт не работает должным образом без включенного JavaScript. Пожалуйста, включите его, чтобы продолжить.

Мы будем использовать плагин RSS Polling для предоставления необходимых данных и создания тем за нас.

2. Реализация

2.1 RSS Polling и RSS/Atom-канал

Создайте конечную точку на вашем сайте, которая предоставляет RSS или Atom-канал для плагина RSS Polling. Эта конечная точка может быть либо просто статическим файлом в формате XML, либо функцией на стороне сервера, предоставляющей контент в формате XML, пример:

URL: https://mysite.com/blog.atom

Содержимое:

<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">

  <title>Статьи блога моего сайта</title>
  <link href="https://mysite.com/blog/"/>
  <updated>2022-07-03T09:02:48.721Z</updated>
  <id>urn:uuid:790c1857-b968-49cc-9fbd-bf7afe3552c2</id>

  <entry>
    <title>Статья о технологиях</title>
    <author>
      <name>Ваше Имя Здесь</name>
    </author>
    <link href="https://mysite.com/blog/an-article-about-technology"/>
    <id>urn:uuid:f6cc13e4-d2eb-4385-af28-c867a94f48dc</id>
    <published>2022-07-03T00:00:00Z</published>
    <updated>2022-07-03T00:00:00Z</updated>
    <summary>Давайте обсудим некоторые технологии в этой статье.</summary>
  </entry>

</feed>

Установите плагин RSS Polling для Discourse согласно инструкциям для [3] (Discourse в облаке) или [4] (самостоятельный хостинг)

Рекомендуемые настройки для RSS Polling в Администрирование → Настройки → Плагины:

Ключ Значение
rss polling enabled true
rss polling frequency 10 (т.е. 10 минут)

Добавьте новый канал в конфигурации плагина RSS Polling в Администрирование → Плагины → RSS Polling

Настройте его согласно инструкциям для [3]:

Ключ Значение
URL https://mysite.com/blog.atom
Фильтр по категории <это необязательно>
Автор <определите автора автоматически генерируемых тем>
Категория <определите категорию/категории, где будут публиковаться автоматически генерируемые темы>
Теги <это необязательно>

2.2 Конфигурация маршрутизатора SPA

Discourse использует последнюю часть пути URL в качестве идентификатора для отдельной статьи блога.

Примеры:

https://mysite.com/blog/an-article-about-technology
https://mysite.com/blog/another-article-about-cats

Настройте ваш маршрутизатор SPA соответствующим образом, чтобы отдельные статьи блога соответствовали отдельным URL.

Также: Discourse предоставит ссылку обратно на отдельную статью блога, поэтому с точки зрения удобства использования (UX) хорошо, чтобы при клике на неё ваш сайт показывал actual статью.

2.3 Компонент статьи

Как уже говорилось, мы не можем использовать подход с <script></script> из [1]. Поэтому мы реализуем iframe и некоторые функции из javascripts/embed.js в нашем компоненте:

Article.vue

Вещи, которые вам следует отредактировать:

элемент описание
#YOUR-DISCOURSE-URL# URL вашего экземпляра Discourse, например, discourse.mysite.com
#YOUR-SITE-URL# URL вашего сайта, например, mysite.com, возможно также %2Fblog%2F, если путь к статьям блога не /blog/
<template>
  <div id="article">

    <!-- ваш отформатированный контент статьи здесь -->

    <iframe
      v-if="slug"
      v-bind:src="`https://#YOUR-DISCOURSE-URL#/embed/comments?embed_url=https%3A%2F%2F#YOUR-SITE-URL#%2Fblog%2F${slug}%2F`"
      id="discourse-embed-frame"
      width="100%"
      v-bind:height="`${iframeHeight}px`"
      frameborder="0"
      scrolling="no"
      referrerpolicy="no-referrer-when-downgrade"
    />
  </div>
</template>

<script>
export default {
  data: () => ({
    slug: null,     // слаг статьи блога, например, "an-article-about-technology", пока маршрут "https://mysite.com/blog/an-article-about-technology"
    iframeHeight: 0 // Discourse сообщит нам точную высоту iframe (см.: метод receiveMessage)
  }),

  methods: {
    // коммуникация через iframe
    receiveMessage(event) {
      if (!event) {
        return;
      }
      if (!(event.origin || "").includes("#YOUR-DISCOURSE-URL#")) {
        return;
      }

      if (event.data) {
        if (event.data.type === "discourse-resize" && event.data.height) {
          this.iframeHeight = +event.data.height;
        }

        if (event.data.type === "discourse-scroll" && event.data.top) {
          // найти смещение iframe
          const destY = this.findPosY(this.$refs["discourse-embed-frame"]) + event.data.top;
          window.scrollTo(0, destY);
        }
      }
    },

    // Спасибо http://amendsoft-javascript.blogspot.ca/2010/04/find-x-and-y-coordinate-of-html-control.html
    findPosY(obj) {
      var top = 0;
      if (obj.offsetParent) {
        while (1) {
          top += obj.offsetTop;
          if (!obj.offsetParent) break;
          obj = obj.offsetParent;
        }
      } else if (obj.y) {
        top += obj.y;
      }
      return top;
    }
  },

  async created() {
    this.slug = this.$router.currentRoute.path.split("/")[2];
  },

  mounted() {
    window.addEventListener("message", this.receiveMessage);
  },
  beforeDestroy() {
    window.removeEventListener("message", this.receiveMessage);
  }
}

2.4 Конфигурация встраивания Discourse

Теперь, когда опрос RSS/Atom-каналов настроен и реализация на вашем сайте готова, мы наконец можем настроить встраивание на экземпляре Discourse.

Перейдите в Администрирование → Настройка → Встраивание и добавьте хост:

Ключ Значение
Разрешённые хосты базовый URL вашего сайта, например, “mysite.com
Имя класса необязательное имя класса для стилизации
Список разрешённых путей например, “/blog/.*”
Публиковать в категорию та же категория, что и в настройках RSS Polling Category

3. Заключительные замечания

Опрос RSS/Atom-каналов, а также сам Discourse создадут новую тему, если она не существует для отдельной статьи блога. Убедитесь, что опрос RSS/Atom-каналов выполняется первым (т.е. подождите, пока тема не будет создана, прежде чем посещать статью блога на вашем сайте).

Причина: Discourse не может парсить заголовок и краткое содержание, поэтому тема будет называться mysite.com с кратким содержанием Нам очень жаль, но этот сайт не работает без javascript. :wink:

Если по какой-то причине Discourse сработал первым, вы можете просто удалить тему и подождать, пока сработает опрос RSS/Atom-каналов.

всего доброго

– MK2k

2 лайка