レガシーな「ウィジェット」レンダリングシステムから脱却し、モダンな Glimmer コンポーネントに置き換える作業を進めています。
最近、Glimmer コンポーネントを使用して投稿ストリームを近代化しました。このガイドでは、プラグインやテーマを古いウィジェットベースのシステムから新しい Glimmer 実装へ移行させる手順を解説します。
ご安心ください。この移行は、一見思われるほど複雑ではありません。新しいシステムは、古いウィジェットシステムよりも直感的で強力になるよう設計されており、このガイドが移行プロセスをサポートします。
タイムライン
これらは変更される可能性がある見積もりです
2025 年第 2 四半期:
コア実装完了
公式プラグインとテーマコンポーネントのアップグレード開始
Meta 上で有効化
アップグレードに関するアドバイス(このガイド)の公開
2025 年第 3 四半期:
公式プラグインとテーマコンポーネントのアップグレード完了
glimmer_post_stream_modeのデフォルトをautoに設定し、コンソールに非推奨メッセージを表示するように設定
管理者警告バナー付きで非推奨メッセージを有効化(2025 年 7 月予定)- 第三者のプラグインやテーマの更新が必要
2025 年第 4 四半期:
新しい投稿ストリームをデフォルトで有効化
機能フラグ設定とレガシーコードの削除
私にとって这意味着什么?
プラグインやテーマのいずれかが投稿ストリームのカスタマイズに「ウィジェット」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 投稿ストリームでは、投稿の表示方法をカスタマイズするためのいくつかの方法が用意されています。
- プラグインアウトレット: 投稿ストリームの特定のポイントにコンテンツを追加するためのもの。
- トランスフォーマー: アイテムのカスタマイズ、データ構造の変更、コンポーネントの動作の変更のためのもの。
includePostAttributes を addTrackedPostProperties に置き換える
プラグインが includePostAttributes を使用して投稿モデルにプロパティを追加している場合は、addTrackedPostProperties を使用するように更新する必要があります。
Before:
api.includePostAttributes('can_accept_answer', 'accepted_answer', 'topic_accepted_answer');
After:
api.addTrackedPostProperties('can_accept_answer', 'accepted_answer', 'topic_accepted_answer');
addTrackedPostProperties 関数は、プロパティを投稿更新の追跡対象としてマークします。プラグインが投稿にプロパティを追加し、レンダリング中にそれらを使用する場合は重要です。これにより、これらのプロパティが変更されたときに UI が自動的に更新されます。
一般的な移行パターン
プラグインアウトレットの使用方法
プラグインアウトレットは、投稿ストリームの特定のポイントにコンテンツを追加するための主要なツールです。カスタムコンテンツを注入できる指定された場所と考えてください。
ウィジェット装飾からの移行の鍵となります。
Glimmer 投稿ストリームは、頻繁にカスタマイズされるコンテンツをアウトレットでラップしています。renderBeforeWrapperOutlet と renderAfterWrapperOutlet のプラグイン API 関数を使用して、それらの前後にコンテンツを挿入します。
1. ウィジェット装飾をプラグインアウトレットに置き換える
最も一般的なカスタマイズは、投稿にコンテンツを追加することです。ウィジェットシステムでは decorateWidget を使用していましたが、Glimmer ではプラグインアウトレットを使用します。
Before:
// プラグインのイニシャライザーの一部
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);
});
}
};
After:
// プラグインの .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. 投稿者名の後にコンテンツを追加する
プラグインが投稿者名の後にコンテンツを追加している場合、post-meta-data-poster-name アウトレットを使用して renderAfterWrapperOutlet API を使用します。
Before:
// プラグインのイニシャライザーの一部
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);
});
}
};
After:
// プラグインの .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">
This is a pinned topic
</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">
This post is a wiki
</div>
</template>
}
);
}
export default {
name: "wiki-post-notice",
initialize() {
withPluginApi((api) => {
customizePost(api);
// ... その他のカスタマイズ
});
}
};
トランスフォーマーの使用方法
トランスフォーマーは、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. 投稿の調理済みコンテンツの前後にコンテンツを挿入する
プラグインが投稿のテキストの後にコンテンツを追加している場合、post-content-cooked-html アウトレットを使用して renderAfterWrapperOutlet API を使用します。
Before:
// プラグインのイニシャライザーの一部
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 = "This post is a wiki";
element.prepend(banner);
}
});
}
export default {
name: "wiki-footer",
initialize() {
withPluginApi((api) => {
// ... その他のカスタマイズ
customizeCooked(api);
});
}
};
After:
// プラグインのイニシャライザーの一部 (.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">This post is a wiki</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);
// ... その他のカスタマイズ
});
}
},
};
実世界の例
公式プラグインからの実際の移行プルリクエストへのリンクを以下に示します。これらは、各タイプのカスタマイズをどのように更新したかを示しています。
- 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 を使用してカスタマイズを実現できない場合は、以下までご連絡ください。