burst/content_recommendation

Drupal 内容推荐

1.2.0 2023-11-10 09:41 UTC

This package is not auto-updated.

Last update: 2024-09-18 02:23:27 UTC


README

开始使用

首先启用 content_recommendation 核心模块。这将暴露您所需的方法和插件。之后,您需要确定要支持哪些内容推荐方式。

  1. 启用 content_recommendation
  2. 启用您想要支持的每个 content_recommendation 子模块
  3. 如果您使用 Headless Ninja (HN) 框架,请启用 content_recommendation_hn 子模块。
  4. 访问 API URL,具体取决于您是否使用 HN:http://drupal.dev/hn?path=/api/content-recommendation?path=
  5. 您现在应该在首页实体上看到 content_recommendation 属性。

获取 API

在启用 content_recommendationcontent_recommendation_hn 模块后,您可以为特定实体获取内容推荐项。您可以通过在 HN 前端应用程序内部请求暴露的 API 来执行此操作。

要发出请求,只需进入您需要的 React 组件内部,并执行 `site.getPage(/api/content_recommendation?path=${pathOfEntityToRequest})`。

如果您使用 content_recommendation_related 子模块,您可能希望指定一个相关类型。您可以通过向 site.getPage() 请求添加 related_type 参数来完成此操作,如下所示:`site.getPage(/api/content_recommendation?path=${pathOfEntityToRequest}&related_type=${relatedType}). relatedType 应与 [预定义的相关类型](#Related types) 之一相对应。

扩展

所有核心功能都可以通过您自定义的代码进行扩展。您可以通过提供的插件和注释添加额外的计算特定实体内容项的方式。

ContentRecommendationPlugin

这是通过您的自定义变体扩展内容推荐响应的一个示例。`ContentRecommendationPlugin` 注释需要一个 `id` 属性。此属性必须是唯一的,并将用作响应中的键。下面的示例将添加一个 `unique_key_here` 属性到响应中。

/**
 * @ContentRecommendationPlugin(
 *   id = "unique_key_here",
 *   priority = 10
 * )
 */
class ContentRecommendation extends ContentRecommendationPluginBase {
  public function recommend(EntityInterface $entity, $query) {
    // Get some random nids.
    $nids = \Drupal::entityQuery('node')
                ->range(0, 3)
                ->execute();

    if (!empty($nids)) {
      // Load nodes.
      $nodes = Node::loadMultiple($nids);
    }

    // All nodes that are returned from the recommend method will
    // be added to the response under the key provided in the annotation.
    return $nodes;
  }
}

模块

content_recommendation

这是提供内容推荐 API 并扩展核心功能的核心模块。

content_recommendation_related

启用此模块后,推荐方法将公开,以便计算特定项的内容推荐。

相关类型

默认支持一些相关类型。这些可以作为方法的参数传递,或在请求 [API](#Fetch API) 时设置 related_type 参数。

可用的相关类型

  • tag_based:将在 entity.field_tags 字段中查找其标签,用于计算相关内容项。
  • random:将随机返回内容项。
  • clickstream_1:基于点击流的第一个内容推荐变体。
  • default:将回退到默认相关类型,目前是 tag_based

配置

所有这些配置仅在您使用 clickstream_1 相关类型时相关。

最小访问时长(秒)

此设置将告诉算法过滤掉所有访问时长短于此数值的访问。

最大访问时长(秒)

此设置将告诉算法过滤掉所有访问时长长于此数值的访问。

过期相关路径(秒)

此设置将确保计算路径的缓存在此数字后清除。

Matomo基本路径

此设置通常是您网站托管的全域名(例如:example.com)。

路径访问次数的权重

此数字将用作指数,为路径的访问次数提供权重。此数字越高,其影响越大。

Matomo凭据

请输入所有Matomo数据库连接信息。

Matomo凭据存储在Drupal状态中,因此不能通过配置管理进行导出。

content_recommendation_popular

此模块将跟踪所有已查看内容项的页面浏览量,并将响应根据此进行排序。

content_recommendation_highlights

此模块将提供一个配置页面,内容管理员可以在此配置要返回的响应中的节点引用。

配置

配置页面将公开一个引用节点的字段。

content_recommendation_hn

HN子模块将公开用于计算内容项的API。

React

由于所有与React相关的代码都在一个闭源项目中,所有相关的类和组件都已复制到本README中。

包含所有Matomo相关跟踪的类

export const canTrack = () => root.document && root._paq;

class DataLayerMatomo {
  prevUrl = '';
  currentUrl = '';
  currentPageTitle = '';
  currentBundle = null;
  app = null;
  generationTimeMs = 0;

  init() {
    this.app = root.document.getElementById('root');
    this.currentPageTitle = root.document.title;

    const mutationObserver = new MutationObserver(mutations =>
      this.onMutationNotify(mutations),
    );

    mutationObserver.observe(root.document.querySelector('title'), {
      subtree: true,
      characterData: true,
      childList: true,
      attributes: true,
    });

    const matomoTimeoutsetTimeout = setTimeout(() => {
      this.track(() => ['enableHeartBeatTimer', config.HEART_BEAT_S]);
    }, config.BOUNCE_BEFORE_MS);

    root.ab = {};

    this.track(() => [
      'AbTesting::create',
      AbTestsStore.contentRecommendationClickstream.test,
    ]);

    this.track(() => ['HeatmapSessionRecording::enable']);

    // Return an unsubscribe method, e.g. to call when unmounting.
    return () => {
      clearTimeout(matomoTimeoutsetTimeout);
      mutationObserver.disconnect();
    };
  }

  onMutationNotify(mutations) {
    const newTitle = getNested(() => mutations[0].target.innerText);
    if (newTitle) {
      this.currentPageTitle = newTitle;
      this.trackPageView();
    }
  }

  // eslint-disable-next-line class-methods-use-this
  async track(cb) {
    if (canTrack()) root._paq.push(cb());
  }

  setPage = ({ nextUrl, generationTimeMs = 0, bundle }) => {
    this.prevUrl = this.currentUrl;
    this.currentUrl = nextUrl;
    this.generationTimeMs = generationTimeMs;
    this.currentBundle = bundle;
  };

  async trackPageView() {
    if (!canTrack()) return;

    // remove all previously assigned custom variables, requires Matomo 3.0.2
    await this.track(() => ['deleteCustomVariables', 'page']); // Matomo 3.x

    await this.track(() => ['setReferrerUrl', this.prevUrl]);
    await this.track(() => ['setCustomUrl', this.currentUrl]);
    await this.track(() => ['setDocumentTitle', this.currentPageTitle]);

    await this.track(() => ['setGenerationTimeMs', this.generationTimeMs]);

    await this.track(() => [
      'trackPageView',
      this.currentPageTitle,
      { dimension1: this.currentBundle },
    ]);

    // make Matomo aware of newly added content
    await this.track(() => ['MediaAnalytics::scanForMedia', this.app]); // Matomo 3.x
    await this.track(() => ['FormAnalytics::scanForForms', this.app]); // Matomo 3.x
    await this.track(() => ['trackContentImpressionsWithinNode', this.app]);
    await this.track(() => ['enableLinkTracking']);
  }
}

将触发Matomo中页面浏览的组件。

import DataLayerMatomo from '../common/Helper/analytics/DataLayerMatomo';

class BaseComponent extends Component {
  componentDidMount() {
    this.matomoUnsubscribe = DataLayerMatomo.init();

    const bundle = getNested(() => this.props.page.__hn.entity.bundle);
    DataLayerMatomo.setPage({
      nextUrl: this.props.url,
      generationTimeMs: root.generationTimeMs,
      bundle,
    });

    DataLayerMatomo.trackPageView();
  }

  componentWillUnmount() {
    if (typeof this.matomoUnsubscribe === 'function') {
      this.matomoUnsubscribe();
    }
  }

  componentWillReceiveProps({ url: nextUrl }) {
    const prevUrl = this.props.url;
    if (nextUrl !== prevUrl) {
      // HN is done loading
      this.startRenderAfterFetch = true; // New render is started now
    }
  }

  componentDidUpdate() {
    // Check if this DidUpdate was fired after url was changed
    if (this.startRenderAfterFetch) {
      // Trigger page view
      const bundle = getNested(() => this.props.page.__hn.entity.bundle);
      this.onHistoryChange({
        nextUrl: this.props.url,
        generationTimeMs: Date.now() - root.startGenerationTime,
        bundle,
      });

      this.startRenderAfterFetch = false;
    }
  }

  onHistoryChange = ({ nextUrl, generationTimeMs, bundle }) => {
    DataLayerMatomo.setPage({ nextUrl, generationTimeMs, bundle });
  };
}

包含A/B变体逻辑的容器组件,并从端点获取数据。

class ContentRecommendation extends Component {
  constructor() {
    super();

    this.state = {
      contentRecommendation: null,
    };
  }

  componentDidMount() {
    this.mounted = true;

    this.props.abTestsStore.contentRecommendationClickstreamVariation =
      this.props.abTestsStore.contentRecommendationClickstreamVariation ||
      AbTestsStore.contentRecommendationClickstream.variations.original.name;

    this.autoUpdateDisposer = autorun(() => {
      this.fetchContentRecommendationItems(
        this.props.abTestsStore.contentRecommendationClickstreamVariation,
      );
    });
  }

  componentWillUnmount() {
    this.mounted = false;

    if (this.autoUpdateDisposer) {
      this.autoUpdateDisposer();
    }
  }

  async fetchContentRecommendationItems(contentRecommendationVariation) {
    const { pathname, search } = this.props.location;
    const path = pathname + search;

    const abVariation =
      AbTestsStore.contentRecommendationClickstream.variations[
        contentRecommendationVariation
      ];
    const relatedType = abVariation && abVariation.name;

    if (relatedType) {
      const uuid = await site.getPage(
        `/api/content-recommendation?related_type=${relatedType}&path=${path}`,
      );

      if (!this.mounted) {
        return;
      }

      const contentRecommendation = site.getData(uuid);

      if (contentRecommendation) {
        // eslint-disable-next-line react/no-did-mount-set-state
        this.setState({ contentRecommendation });
      }
    }
  }

  render() {
    const { contentRecommendation } = this.state;

    if (!contentRecommendation) return null;

    return <ContentRecommendationItems {...contentRecommendation} />;
  }
}

根据端点数据渲染不同标签页的组件。

class ContentRecommendation extends Component {
  constructor() {
    super();

    this.state = {
      activeIndex: 0,
    };
  }

  onTabChange = activeIndex => {
    this.setState({ activeIndex });
  };

  render() {
    const { related, popular, highlights, title } = this.props;

    const showRelated = related && related.length > 0;
    const showPopular = popular && popular.length > 0;
    const showHighlights = highlights && highlights.length > 0;

    if (!showRelated && !showPopular && !showHighlights) {
      return null;
    }

    return (
      <div>
        <Tabs
          navId="related-content"
          activeIndex={this.state.activeIndex}
          onChange={this.onTabChange}
        >
          <TitleComponent
            className="container"
            title={title.title || title}
            subtitle={title.subtitle}
          />
          <TabNav mobileTitle={title.title || title} tabNav>
            {showRelated && (
              <TabNavItem navItem key="related" scrollIntoView>
                Voor jou
              </TabNavItem>
            )}
            {showPopular && (
              <TabNavItem navItem key="popular" scrollIntoView>
                Populair op Natuurmonumenten
              </TabNavItem>
            )}
            {showHighlights && (
              <TabNavItem navItem key="highlights" scrollIntoView>
                Tips van de redactie
              </TabNavItem>
            )}
          </TabNav>
          <MultiColContent>
            <TabContent>
              {showRelated && (
                <TabSection>
                  <EntityListMapper
                    mapper={ContentRecommendationMapper}
                    entities={related}
                    entityProps={{ tabName: 'related' }}
                  />
                </TabSection>
              )}
              {showPopular && (
                <TabSection>
                  <EntityListMapper
                    mapper={ContentRecommendationMapper}
                    entities={popular}
                    entityProps={{ tabName: 'popular' }}
                  />
                </TabSection>
              )}
              {showHighlights && (
                <TabSection>
                  <EntityListMapper
                    mapper={ContentRecommendationMapper}
                    entities={highlights}
                    entityProps={{ tabName: 'highlights' }}
                  />
                </TabSection>
              )}
            </TabContent>
          </MultiColContent>
        </Tabs>
      </div>
    );
  }
}

ContentRecommendation.propTypes = {
  related: PropTypes.arrayOf(PropTypes.string).isRequired,
  popular: PropTypes.arrayOf(PropTypes.string),
  highlights: PropTypes.arrayOf(PropTypes.string),
  title: PropTypes.oneOfType([
    PropTypes.shape({
      title: PropTypes.string,
      subtitle: PropTypes.string,
    }),
    PropTypes.bool,
    PropTypes.string,
  ]),
};

包含单个内容推荐项的视觉渲染和用户点击项时的跟踪的组件。

const ContentRecommendationItem = ({
  entity,
  title,
  subtitle,
  index,
  abTestsStore,
  tabName,
}) => (
  <MultiColItem
    title={title || entity.title}
    subtitle={subtitle}
    link={entity.__hn.url}
    image={entity.field_media}
    onClick={() =>
      ContentRecommendationItem.trackNavigate({
        index,
        tabName,
        relatedType: abTestsStore.contentRecommendationVariation,
      })
    }
  >
    <p>{multiline(entity.field_teaser)}</p>
  </MultiColItem>
);

ContentRecommendationItem.trackNavigate = ({ index, relatedType, tabName }) => {
  Matomo.track(() => {
    const recommendedCategory = tabName === 'related' ? relatedType : tabName;

    return [
      'trackEvent',
      'Content recommendation',
      'content_recommendation_navigate',
      `bottom_${recommendedCategory}_${index}`,
    ];
  });
};

ContentRecommendationItem.propTypes = {
  entity: PropTypes.shape({
    title: PropTypes.string.isRequired,
    field_tags: PropTypes.array,
    field_teaser: PropTypes.string,
    __hn: PropTypes.shape({
      url: PropTypes.string.isRequired,
    }).isRequired,
  }).isRequired,
  title: PropTypes.string,
  subtitle: PropTypes.string,
  index: PropTypes.number.isRequired,
  tabName: PropTypes.string.isRequired,
  abTestsStore: PropTypes.instanceOf(AbTestsStore).isRequired,
};

ContentRecommendationItem.defaultProps = {
  title: undefined,
  subtitle: undefined,
};