Мы отказываемся от устаревшей системы рендеринга «виджетов» в пользу современных компонентов Glimmer.
Недавно мы модернизировали поток постов, используя компоненты Glimmer. Это руководство поможет вам перенести ваши плагины и темы со старой системы на основе виджетов на новую реализацию Glimmer.
Не переживайте — этот процесс переноса проще, чем может показаться на первый взгляд. Мы разработали новую систему более интуитивной и мощной по сравнению со старой системой виджетов, а это руководство поможет вам пройти через весь процесс.
График
Это приблизительные сроки, которые могут быть изменены
II квартал 2025 года:
Завершена базовая реализация
Начато обновление официальных плагинов и компонентов тем
Включено на Meta
Опубликованы рекомендации по обновлению (данное руководство)
III квартал 2025 года:
Завершено обновление официальных плагинов и компонентов тем
Установлено значение по умолчанию для glimmer_post_stream_modeравнымauto, включены сообщения об устаревании в консоли
Включены сообщения об устаревании с предупреждающим баннером для администраторов (планируется на июль 2025 года)- Плагины и темы сторонних разработчиков должны быть обновлены
IV квартал 2025 года:
Новый поток постов включен по умолчанию
Удалена настройка флага функции и устаревший код
Что это значит для меня?
Если ваши плагины или темы используют какие-либо API «виджетов» для кастомизации потока постов, вам необходимо обновить их для работы с новой версией.
Как попробовать новый поток постов?
Чтобы попробовать новый поток постов, просто измените настройку glimmer_post_stream_mode на auto в настройках вашего сайта. Это включит новый поток постов, если у вас нет несовместимых плагинов или тем.
Когда glimmer_post_stream_mode установлен в auto, Discourse автоматически обнаруживает несовместимые плагины и темы. Если они найдены, в консоли браузера появятся полезные предупреждающие сообщения, которые точно укажут, какие плагины или темы требуют обновления, а также предоставят трассировку стека для помощи в поиске соответствующего кода.
Эти сообщения помогут вам точно определить, какие части ваших плагинов или тем необходимо обновить для совместимости с новым потоком постов Glimmer.
Если вы столкнетесь с проблемами при использовании нового потока постов, не волнуйтесь: пока вы все еще можете установить настройку в disabled, чтобы вернуться к старой системе. Если у вас установлены несовместимые расширения, но вы все равно хотите попробовать, вы как администратор можете установить опцию в enabled, чтобы принудительно включить новый поток постов. Используйте это с осторожностью — ваш сайт может работать некорректно в зависимости от ваших кастомизаций.
У меня установлены кастомные плагины или темы. Нужно ли мне их обновлять?
Вам необходимо обновить ваши плагины или темы, если они выполняют любые из следующих кастомизаций:
-
Используют
decorateWidget,changeWidgetSetting,reopenWidgetилиattachWidgetActionдля следующих виджетов:actions-summaryavatar-flairembedded-postexpand-hiddenexpand-post-buttonfilter-jump-to-postfilter-show-allpost-articlepost-avatar-user-infopost-avatarpost-bodypost-contentspost-datepost-edits-indicatorpost-email-indicatorpost-gappost-group-requestpost-linkspost-locked-indicatorpost-meta-datapost-noticepost-placeholderpost-streampostposter-nameposter-name-titleposts-filtered-noticereply-to-tabselect-posttopic-post-visited-line
-
Используют один из следующих методов API:
addPostTransformCallbackincludePostAttributes
Если у вас есть расширения, использующие любые из вышеперечисленных кастомизаций, при переходе на страницу темы вы увидите предупреждение в консоли, указывающее, какой плагин или компонент требует обновления.
ID устаревания:
discourse.post-stream-widget-overrides
Если вы используете более одной темы в вашем экземпляре, обязательно проверьте все из них, так как предупреждения будут отображаться только для активных плагинов и текущих тем и компонентов тем.
Чем заменить?
Новый поток постов Glimmer предоставляет несколько способов кастомизации отображения постов:
- Plugin Outlets (Слоты плагинов): Для добавления контента в определенных точках потока постов.
- Transformers (Трансформеры): Для кастомизации элементов, изменения структур данных или поведения компонентов.
Замена includePostAttributes на addTrackedPostProperties
Если ваш плагин использует includePostAttributes для добавления свойств в модель поста, вам необходимо обновить его и использовать addTrackedPostProperties.
До:
api.includePostAttributes('can_accept_answer', 'accepted_answer', 'topic_accepted_answer');
После:
api.addTrackedPostProperties('can_accept_answer', 'accepted_answer', 'topic_accepted_answer');
Функция addTrackedPostProperties помечает свойства как отслеживаемые для обновлений постов. Это важно, если ваш плагин добавляет свойства к посту и использует их при рендеринге. Это гарантирует, что интерфейс будет автоматически обновляться при изменении этих свойств.
Распространенные паттерны миграции
Использование Plugin Outlets
Слоты плагинов — это ваш основной инструмент для добавления контента в определенных точках потока постов. Представьте их как выделенные места, куда вы можете вставить свой кастомный контент.
Они являются ключом к отказу от декораций виджетов.
Поток постов Glimmer оборачивает часто кастомизируемый контент в слоты. Используйте функции API плагинов renderBeforeWrapperOutlet и renderAfterWrapperOutlet, чтобы вставить контент до или после них.
1. Замена декораций виджетов на Plugin Outlets
Самая распространенная кастомизация — добавление контента к постам. В системе виджетов вы использовали decorateWidget. С Glimmer вы будете использовать слоты плагинов.
До:
// Часть инициализатора в плагине
import { withPluginApi } from "discourse/lib/plugin-api";
// ... другие импорты
function customizeWidgetPost(api) {
api.decorateWidget("post-contents:after-cooked", (helper) => {
const post = helper.getModel();
if (post.post_number === 1 && post.topic.accepted_answer) {
return helper.attach("solved-accepted-answer", { post });
}
});
}
export default {
name: "extend-for-solved-button",
initialize() {
withPluginApi((api) => {
// ... другие кастомизации
customizeWidgetPost(api);
});
}
};
После:
// Часть инициализатора .gjs в плагине
import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
import SolvedAcceptedAnswer from "../components/solved-accepted-answer";
// ... другие импорты
function customizePost(api) {
api.renderAfterWrapperOutlet(
"post-content-cooked-html",
class extends Component {
static shouldRender(args) {
return args.post?.post_number === 1 && args.post?.topic?.accepted_answer;
}
<template>
<SolvedAcceptedAnswer
@post={{@post}}
/>
</template>
}
);
}
export default {
name: "extend-for-solved-button",
initialize() {
withPluginApi((api) => {
// ... другие кастомизации
customizePost(api);
});
}
};
2. Добавление контента после имени автора
Если ваш плагин добавляет контент после имени автора, используйте API renderAfterWrapperOutlet со слотом post-meta-data-poster-name.
До:
// Часть инициализатора в плагине
import { withPluginApi } from "discourse/lib/plugin-api";
// ... другие импорты
function customizeWidgetPost(api) {
api.decorateWidget(`poster-name:after`, (dec) => {
if (!isGPTBot(dec.attrs.user)) {
return;
}
return dec.widget.attach("persona-flair", {
personaName: dec.model?.topic?.ai_persona_name,
});
});
}
export default {
name: "ai-bot-replies",
initialize() {
withPluginApi((api) => {
// ... другие кастомизации
customizeWidgetPost(api);
});
}
};
После:
// Часть инициализатора .gjs в плагине
import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
// ... другие импорты
function customizePost(api) {
api.renderAfterWrapperOutlet(
"post-meta-data-poster-name",
class extends Component {
static shouldRender(args) {
return isGPTBot(args.post?.user);
}
<template>
<span class="persona-flair">{{@post.topic.ai_persona_name}}</span>
</template>
}
);
}
export default {
name: "ai-bot-replies",
initialize() {
withPluginApi((api) => {
// ... другие кастомизации
customizePost(api);
});
}
};
3. Добавление контента перед содержимым поста
// Часть инициализатора .gjs в теме
import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
// ... другие импорты
function customizePost(api) {
api.renderBeforeWrapperOutlet(
"post-article",
class extends Component {
static shouldRender(args) {
return args.post?.topic?.pinned;
}
<template>
<div class="pinned-post-notice">
Это закрепленная тема
</div>
</template>
}
);
}
export default {
name: "pinned-topic-notice",
initialize() {
withPluginApi((api) => {
// ... другие кастомизации
customizePost(api);
});
}
};
4. Добавление контента после содержимого поста
// Часть инициализатора .gjs в теме
import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
// ... другие импорты
function customizePost(api) {
api.renderAfterWrapperOutlet(
"post-article",
class extends Component {
static shouldRender(args) {
return args.post?.wiki;
}
// В реальном компоненте вы бы использовали шаблон так:
<template>
<div class="wiki-post-notice">
Этот пост является вики-статьей
</div>
</template>
}
);
}
export default {
name: "wiki-post-notice",
initialize() {
withPluginApi((api) => {
customizePost(api);
// ... другие кастомизации
});
}
};
Использование Transformers
Трансформеры — это мощный способ кастомизации компонентов Discourse. Они позволяют изменять данные или поведение компонентов без необходимости переопределять целые компоненты.
Вот некоторые из наиболее актуальных трансформеров значений для кастомизации потока постов:
| Название трансформера | Описание | Контекст |
|---|---|---|
post-class |
Кастомизация CSS-классов, применяемых к основному элементу поста. | { post } |
post-meta-data-infos |
Кастомизация списка компонентов метаданных, отображаемых для поста. Это позволяет добавлять, удалять или менять порядок таких элементов, как дата поста, индикатор редактирования и т.д. | { post, metaDataInfoKeys } |
post-meta-data-poster-name-suppress-similar-name |
Решение о том, следует ли скрывать полное имя пользователя, если оно похоже на его имя пользователя. Верните true, чтобы скрыть. |
{ post, name } |
post-notice-component |
Кастомизация или замена компонента, используемого для отображения уведомления о посте. | { post, type } |
post-show-topic-map |
Управление видимостью компонента карты темы в первом посте. | { post, isPM, isRegular, showWithoutReplies } |
post-small-action-class |
Добавление пользовательских CSS-классов к посту с небольшим действием. | { post, actionCode } |
post-small-action-custom-component |
Замена стандартного поста с небольшим действием на пользовательский компонент Glimmer. | { post, actionCode } |
post-small-action-icon |
Кастомизация иконки, используемой для поста с небольшим действием. | { post, actionCode } |
poster-name-class |
Добавление пользовательских CSS-классов к контейнеру имени автора. | { user } |
1. Добавление пользовательского класса к постам
// Часть инициализатора в плагине
import { withPluginApi } from "discourse/lib/plugin-api";
function customizePostClasses(api) {
api.registerValueTransformer(
"post-class",
({ value, context }) => {
const { post } = context;
// Добавление пользовательского класса к постам от определенного пользователя
if (post.user_id === 1) {
return [...value, "special-user-post"];
}
return value;
}
);
}
export default {
name: "custom-post-classes",
initialize() {
withPluginApi((api) => {
// ... другие кастомизации
customizePostClasses(api);
});
}
};
2. Добавление пользовательских метаданных поста
Трансформер post-meta-data-infos позволяет добавлять пользовательские компоненты в секцию метаданных поста.
// Часть инициализатора в плагине
import { withPluginApi } from "discourse/lib/plugin-api";
function customizePostMetadata(api) {
// Определение компонента, который будет использоваться в секции метаданных
// Компонент должен быть создан вне обратного вызова трансформера,
// иначе это может вызвать проблемы с памятью.
const CustomMetadataComponent = <template>...</template>;
api.registerValueTransformer(
"post-meta-data-infos",
({ value: metadata, context: { post, metaDataInfoKeys } }) => {
// Добавление компонента только для определенных постов
if (post.some_custom_property) {
metadata.add(
"custom-metadata-key",
CustomMetadataComponent,
{
// Разместить его перед датой
before: metaDataInfoKeys.DATE,
// и после вкладки ответа
after: metaDataInfoKeys.REPLY_TO_TAB,
}
);
}
}
);
}
export default {
name: "custom-post-metadata",
initialize() {
withPluginApi((api) => {
// ... другие кастомизации
customizePostMetadata(api);
});
}
};
Вот реальный пример из плагина discourse-activity-pub:
// Часть инициализатора в плагине discourse-activity-pub
import { withPluginApi } from "discourse/lib/plugin-api";
import ActivityPubPostStatus from "../components/activity-pub-post-status";
import {
activityPubPostStatus,
showStatusToUser,
} from "../lib/activity-pub-utilities";
function customizePost(api, container) {
const currentUser = api.getCurrentUser();
const PostMetadataActivityPubStatus = <template>
<div class="post-info activity-pub">
<ActivityPubPostStatus @post={{@post}} />
</div>
</template>;
api.registerValueTransformer(
"post-meta-data-infos",
({ value: metadata, context: { post, metaDataInfoKeys } }) => {
const site = container.lookup("service:site");
const siteSettings = container.lookup("service:site-settings");
if (
site.activity_pub_enabled &&
post.activity_pub_enabled &&
post.post_number !== 1 &&
showStatusToUser(currentUser, siteSettings)
) {
const status = activityPubPostStatus(post);
if (status) {
metadata.add(
"activity-pub-indicator",
PostMetadataActivityPubStatus,
{
before: metaDataInfoKeys.DATE,
after: metaDataInfoKeys.REPLY_TO_TAB,
}
);
}
}
}
);
}
export default {
name: "activity-pub",
initialize(container) {
withPluginApi((api) => {
customizePost(api, container);
// ... другие кастомизации
});
}
};
5. Вставка контента до или после обработанного содержимого поста
Если ваш плагин добавляет контент после текста в посте, используйте API renderAfterWrapperOutlet со слотом post-content-cooked-html.
До:
// Часть инициализатора в плагине
import { withPluginApi } from "discourse/lib/plugin-api";
function customizeCooked(api) {
api.decorateWidget("post-contents:after-cooked", (helper) => {
const post = helper.getModel();
if (post.wiki) {
const banner = document.createElement("div");
banner.classList.add("wiki-footer");
banner.textContent = "Этот пост является вики-статьей";
element.prepend(banner);
}
});
}
export default {
name: "wiki-footer",
initialize() {
withPluginApi((api) => {
// ... другие кастомизации
customizeCooked(api);
});
}
};
После:
// Часть инициализатора в плагине (.gjs)
import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
// Определение компонента, который будет использоваться в слоте плагина
class WikiBanner extends Component {
static shouldRender(args) {
return args.post.wiki;
}
<template>
<div class="wiki-footer">Этот пост является вики-статьей</div>
</template>
}
function customizePost(api) {
// Используйте renderBeforeWrapperOutlet для добавления контента перед содержимым поста
api.renderAfterWrapperOutlet(
"post-content-cooked-html",
WikiBanner
);
}
export default {
name: "wiki-footer",
initialize() {
withPluginApi((api) => {
customizePost(api);
// ... другие кастомизации
});
}
};
Поддержка обеих систем во время перехода
Как автор плагина или темы, вы можете захотеть поддерживать обе системы во время перехода, чтобы ваши расширения работали как со старым, так и с новым потоком постов.
Вот паттерн, используемый многими официальными плагинами:
// solved-button.js
import Component from "@glimmer/component";
import { withSilencedDeprecations } from "discourse/lib/deprecated";
import { withPluginApi } from "discourse/lib/plugin-api";
import RenderGlimmer from "discourse/widgets/render-glimmer";
import SolvedAcceptedAnswer from "../components/solved-accepted-answer";
function customizePost(api) {
// кастомизации потока постов glimmer
api.renderAfterWrapperOutlet(
"post-content-cooked-html",
class extends Component {
static shouldRender(args) {
return args.post?.post_number === 1 && args.post?.topic?.accepted_answer;
}
<template>
<SolvedAcceptedAnswer
@post={{@post}}
@decoratorState={{@decoratorState}}
/>
</template>
}
);
// ...
// оборачиваем старый код виджетов, отключая предупреждения об устаревании
withSilencedDeprecations("discourse.post-stream-widget-overrides", () =>
customizeWidgetPost(api)
);
}
// старый код виджетов
function customizeWidgetPost(api) {
api.decorateWidget("post-contents:after-cooked", (helper) => {
let post = helper.getModel();
if (helper.attrs.post_number === 1 && post?.topic?.accepted_answer) {
// Используйте RenderGlimmer для рендеринга компонента Glimmer в системе виджетов
return new RenderGlimmer(
helper.widget,
"div",
<template><SolvedAcceptedAnswer @post={{@data.post}} /></template>
null,
{ post }
);
}
});
}
export default {
name: "extend-for-solved-button",
initialize(container) {
const siteSettings = container.lookup("service:site-settings");
if (siteSettings.solved_enabled) {
withPluginApi((api) => {
customizePost(api);
// ... другие кастомизации
});
}
},
};
Реальные примеры
Вот ссылки на реальные pull-реквесты миграции из наших официальных плагинов. Они показывают, как мы обновляли каждый тип кастомизации:
- discourse-ai
- discourse-assign
- discourse-cakeday
- discourse-post-voting
- discourse-reactions
- discourse-shared-edits
- discourse-solved
- discourse-topic-voting
- discourse-user-notes
- discourse-activity-pub
Устранение неполадок
Мой сайт сломан после включения нового потока постов
- Верните
glimmer_post_stream_modeвdisabled - Проверьте консоль на наличие конкретных сообщений об ошибках
- Обновите проблемные плагины/темы перед повторной попыткой
Я не вижу никаких предупреждений, но мои кастомизации не работают
- Убедитесь, что ваши кастомизации нацелены на виджеты, перечисленные выше
- Убедитесь, что вы тестируете на странице темы с темами, где ваши кастомизации видны
Нужна помощь?
Если вы нашли ошибку или ваша кастомизация не может быть реализована с помощью новых API, которые мы представили, пожалуйста, сообщите нам об этом ниже.