diff --git a/core/misc/cspell/dictionary.txt b/core/misc/cspell/dictionary.txt
index 8be527b92f1b20912c09671cca7c975700add88d..cb65789cb49d03e4f9bd160872c2917a0f2f7fa6 100644
--- a/core/misc/cspell/dictionary.txt
+++ b/core/misc/cspell/dictionary.txt
@@ -358,6 +358,7 @@ drupallink
 drupalmedia
 drupalmediaediting
 drupalmediatoolbar
+drupalorg
 drupaltest
 druplicon
 drush
diff --git a/core/modules/announcements_feed/announcements_feed.info.yml b/core/modules/announcements_feed/announcements_feed.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f0c41286b33ab6c2fbceeae73c45d3078bd0a07d
--- /dev/null
+++ b/core/modules/announcements_feed/announcements_feed.info.yml
@@ -0,0 +1,6 @@
+name: Announcements
+type: module
+description: Displays announcements from the Drupal community.
+version: VERSION
+package: Core (Experimental)
+lifecycle: experimental
diff --git a/core/modules/announcements_feed/announcements_feed.libraries.yml b/core/modules/announcements_feed/announcements_feed.libraries.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e3882f6ff9c9ee6a78d112e39ac04311ff6efbef
--- /dev/null
+++ b/core/modules/announcements_feed/announcements_feed.libraries.yml
@@ -0,0 +1,17 @@
+drupal.announcements_feed.dialog:
+  version: VERSION
+  css:
+    component:
+      css/announcements_feed.dialog.css: {}
+
+drupal.announcements_feed.toolbar:
+  version: VERSION
+  css:
+    component:
+      css/announcements_feed.toolbar.css: {}
+
+drupal.announcements_feed.page:
+  version: VERSION
+  css:
+    component:
+      css/announcements_feed.page.css: {}
diff --git a/core/modules/announcements_feed/announcements_feed.module b/core/modules/announcements_feed/announcements_feed.module
new file mode 100644
index 0000000000000000000000000000000000000000..6beccb4ef798c46d6f9c6eb94ab732d92e85b393
--- /dev/null
+++ b/core/modules/announcements_feed/announcements_feed.module
@@ -0,0 +1,104 @@
+<?php
+
+/**
+ * @file
+ * Fetch community announcements from www.drupal.org feed.
+ */
+
+use Drupal\announcements_feed\RenderCallbacks;
+use Drupal\Core\Routing\RouteMatchInterface;
+
+/**
+ * Implements hook_help().
+ */
+function announcements_feed_help($route_name, RouteMatchInterface $route_match) {
+  switch ($route_name) {
+    case 'help.page.announcements_feed':
+      $output = '';
+      $output .= '<h3>' . t('About') . '</h3>';
+      $output .= '<p>' . t('The Announcements module displays announcements from the Drupal community. For more information, see the <a href=":documentation">online documentation for the Announcements module</a>.', [':documentation' => 'https://www.drupal.org/docs/core-modules-and-themes/core-modules/announcements-feed']) . '</p>';
+      $output .= '<h3>' . t('Uses') . '</h3>';
+      $output .= '<dl><dt>' . t('Accessing announcements') . '</dt>';
+      $output .= '<dd>' . t('Users with the "View drupal.org announcements" permission may click on the "Announcements" item in the administration toolbar to see all announcements relevant to the Drupal version of your site.') . '</dd>';
+      $output .= '</dl>';
+      return $output;
+  }
+}
+
+/**
+ * Implements hook_toolbar().
+ */
+function announcements_feed_toolbar() {
+  if (!\Drupal::currentUser()->hasPermission('access announcements')) {
+    return [
+      '#cache' => ['contexts' => ['user.permissions']],
+    ];
+  }
+  $items['announcement'] = [
+    '#type' => 'toolbar_item',
+    'tab' => [
+      '#lazy_builder' => [
+        'announcements_feed.lazy_builders:renderAnnouncements',
+        [],
+      ],
+      '#create_placeholder' => TRUE,
+      '#cache' => [
+        'tags' => [
+          'announcements_feed:feed',
+        ],
+      ],
+    ],
+    '#wrapper_attributes' => [
+      'class' => ['announce-toolbar-tab'],
+    ],
+    '#cache' => ['contexts' => ['user.permissions']],
+    '#weight' => 3399,
+  ];
+
+  // \Drupal\toolbar\Element\ToolbarItem::preRenderToolbarItem adds an
+  // #attributes property to each toolbar item's tab child automatically.
+  // Lazy builders don't support an #attributes property so we need to
+  // add another render callback to remove the #attributes property. We start by
+  // adding the defaults, and then we append our own pre render callback.
+  $items['announcement'] += \Drupal::service('plugin.manager.element_info')->getInfo('toolbar_item');
+  $items['announcement']['#pre_render'][] = [RenderCallbacks::class, 'removeTabAttributes'];
+  return $items;
+}
+
+/**
+ * Implements hook_theme().
+ */
+function announcements_feed_theme($existing, $type, $theme, $path) {
+  return [
+    'announcements_feed' => [
+      'variables' => [
+        'featured' => NULL,
+        'standard' => NULL,
+        'count' => 0,
+        'feed_link' => '',
+      ],
+    ],
+    'announcements_feed_admin' => [
+      'variables' => [
+        'featured' => NULL,
+        'standard' => NULL,
+        'count' => 0,
+        'feed_link' => '',
+      ],
+    ],
+  ];
+}
+
+/**
+ * Implements hook_cron().
+ */
+function announcements_feed_cron() {
+  $config = \Drupal::config('announcements_feed.settings');
+  $interval = $config->get('cron_interval');
+  $last_check = \Drupal::state()->get('announcements_feed.last_fetch', 0);
+  $time = \Drupal::time()->getRequestTime();
+  if (($time - $last_check) > $interval) {
+    \Drupal::service('announcements_feed.fetcher')->fetch(TRUE);
+    \Drupal::state()->set('announcements_feed.last_fetch', $time);
+  }
+}
diff --git a/core/modules/announcements_feed/announcements_feed.permissions.yml b/core/modules/announcements_feed/announcements_feed.permissions.yml
new file mode 100644
index 0000000000000000000000000000000000000000..76e0adbeeed6c6d7fb4c12930dbbcede3872ff96
--- /dev/null
+++ b/core/modules/announcements_feed/announcements_feed.permissions.yml
@@ -0,0 +1,2 @@
+access announcements:
+  title: 'View official announcements related to Drupal'
diff --git a/core/modules/announcements_feed/announcements_feed.routing.yml b/core/modules/announcements_feed/announcements_feed.routing.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a67008ac68eb444e6504c01da6b00d64e2faaac6
--- /dev/null
+++ b/core/modules/announcements_feed/announcements_feed.routing.yml
@@ -0,0 +1,7 @@
+announcements_feed.announcement:
+  path: '/admin/announcements_feed'
+  defaults:
+    _controller: '\Drupal\announcements_feed\Controller\AnnounceController::getAnnouncements'
+    _title: 'Community announcements'
+  requirements:
+    _permission: 'access announcements'
diff --git a/core/modules/announcements_feed/announcements_feed.services.yml b/core/modules/announcements_feed/announcements_feed.services.yml
new file mode 100644
index 0000000000000000000000000000000000000000..39ec48442c078e2ba4d99cad7b21c230501f929f
--- /dev/null
+++ b/core/modules/announcements_feed/announcements_feed.services.yml
@@ -0,0 +1,15 @@
+parameters:
+  announcements_feed.feed_json_url: https://www.drupal.org/announcements.json
+  announcements_feed.feed_link: https://www.drupal.org/about/announcements
+
+services:
+  announcements_feed.fetcher:
+    class: Drupal\announcements_feed\AnnounceFetcher
+    arguments: ['@http_client', '@config.factory', '@keyvalue.expirable', '@logger.channel.announcements_feed', '%announcements_feed.feed_json_url%']
+  logger.channel.announcements_feed:
+    parent: logger.channel_base
+    arguments: ['announcements_feed']
+    public: false
+  announcements_feed.lazy_builders:
+    class: Drupal\announcements_feed\LazyBuilders
+    arguments: [ '@plugin.manager.element_info']
diff --git a/core/modules/announcements_feed/config/install/announcements_feed.settings.yml b/core/modules/announcements_feed/config/install/announcements_feed.settings.yml
new file mode 100644
index 0000000000000000000000000000000000000000..73cd088fff752e7d7767b34ba09730d1060482f5
--- /dev/null
+++ b/core/modules/announcements_feed/config/install/announcements_feed.settings.yml
@@ -0,0 +1,3 @@
+max_age: 86400
+cron_interval: 21600
+limit: 10
diff --git a/core/modules/announcements_feed/config/schema/announcements_feed.schema.yml b/core/modules/announcements_feed/config/schema/announcements_feed.schema.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2d0487cf59f4d22e40830f38130c170089033447
--- /dev/null
+++ b/core/modules/announcements_feed/config/schema/announcements_feed.schema.yml
@@ -0,0 +1,13 @@
+announcements_feed.settings:
+  type: config_object
+  label: 'Announcements Settings'
+  mapping:
+    max_age:
+      type: integer
+      label: 'Cache announcements for max-age seconds.'
+    cron_interval:
+      type: integer
+      label: 'Cron interval for fetching announcements in seconds.'
+    limit:
+      type: integer
+      label: 'Number of announcements that will be displayed.'
diff --git a/core/modules/announcements_feed/css/announcements_feed.dialog.css b/core/modules/announcements_feed/css/announcements_feed.dialog.css
new file mode 100644
index 0000000000000000000000000000000000000000..4ee17f3e160015c2ea91b072964f892776fc0f74
--- /dev/null
+++ b/core/modules/announcements_feed/css/announcements_feed.dialog.css
@@ -0,0 +1,53 @@
+/*
+ * DO NOT EDIT THIS FILE.
+ * See the following change record for more information,
+ * https://www.drupal.org/node/3084859
+ * @preserve
+ */
+
+/**
+ * @file
+ *
+ * Styles for the announcements feed within the off-canvas dialog.
+ */
+
+#drupal-off-canvas-wrapper .ui-dialog-titlebar.announce-titlebar::before {
+  -webkit-mask-image: url("data:image/svg+xml,%3csvg width='20' height='19' viewBox='0 0 20 19' fill='none' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M5.73047 16.7648C6.00143 17.4831 6.6872 18 7.50009 18C8.31299 18 8.99876 17.4865 9.26972 16.7682C8.71107 16.8118 8.12231 16.8387 7.50009 16.8387C6.87788 16.8353 6.28912 16.8085 5.73047 16.7648Z' fill='white'/%3e%3cpath d='M14.331 13.4118H14.0801L12.4074 11.3979L11.5143 6.69897H11.5042C11.2333 5.05433 9.97881 3.74869 8.36976 3.39627C8.3731 3.38955 8.37979 3.38284 8.37979 3.37613L8.624 2.63772C8.74108 2.28529 8.53702 2 8.16905 2H6.83095C6.46298 2 6.25892 2.28529 6.37266 2.63772L6.61686 3.37613C6.62021 3.38284 6.62355 3.38955 6.6269 3.39627C5.01784 3.74869 3.76673 5.05433 3.49242 6.69897H3.48238L2.59255 11.3979L0.919938 13.4118H0.669046C0.30107 13.4118 0 13.7139 0 14.0831C0 14.4523 0.280999 14.8618 0.625558 14.996C0.625558 14.996 3.48573 16.0969 7.5 16.0969C11.5143 16.0969 14.3744 14.996 14.3744 14.996C14.719 14.8618 15 14.4523 15 14.0831C15 13.7139 14.6989 13.4118 14.331 13.4118ZM4.58296 6.95742L3.70317 11.8611L1.75624 14.0831H1.23439L3.21811 11.6933L4.15477 6.82652C4.28189 6.0579 4.68332 5.3799 5.24532 4.8798L5.49955 5.19866C5.03122 5.60478 4.68666 6.32305 4.58296 6.95742Z' fill='white'/%3e%3c/svg%3e");
+  mask-image: url("data:image/svg+xml,%3csvg width='20' height='19' viewBox='0 0 20 19' fill='none' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M5.73047 16.7648C6.00143 17.4831 6.6872 18 7.50009 18C8.31299 18 8.99876 17.4865 9.26972 16.7682C8.71107 16.8118 8.12231 16.8387 7.50009 16.8387C6.87788 16.8353 6.28912 16.8085 5.73047 16.7648Z' fill='white'/%3e%3cpath d='M14.331 13.4118H14.0801L12.4074 11.3979L11.5143 6.69897H11.5042C11.2333 5.05433 9.97881 3.74869 8.36976 3.39627C8.3731 3.38955 8.37979 3.38284 8.37979 3.37613L8.624 2.63772C8.74108 2.28529 8.53702 2 8.16905 2H6.83095C6.46298 2 6.25892 2.28529 6.37266 2.63772L6.61686 3.37613C6.62021 3.38284 6.62355 3.38955 6.6269 3.39627C5.01784 3.74869 3.76673 5.05433 3.49242 6.69897H3.48238L2.59255 11.3979L0.919938 13.4118H0.669046C0.30107 13.4118 0 13.7139 0 14.0831C0 14.4523 0.280999 14.8618 0.625558 14.996C0.625558 14.996 3.48573 16.0969 7.5 16.0969C11.5143 16.0969 14.3744 14.996 14.3744 14.996C14.719 14.8618 15 14.4523 15 14.0831C15 13.7139 14.6989 13.4118 14.331 13.4118ZM4.58296 6.95742L3.70317 11.8611L1.75624 14.0831H1.23439L3.21811 11.6933L4.15477 6.82652C4.28189 6.0579 4.68332 5.3799 5.24532 4.8798L5.49955 5.19866C5.03122 5.60478 4.68666 6.32305 4.58296 6.95742Z' fill='white'/%3e%3c/svg%3e");
+}
+
+#drupal-off-canvas-wrapper .announcements {
+  padding-block-start: var(--off-canvas-padding);
+}
+
+#drupal-off-canvas-wrapper .announcements ul {
+  margin: 0;
+  padding-inline-start: 0;
+  list-style: none;
+}
+
+#drupal-off-canvas-wrapper .announcement {
+  font-size: 0.875rem;
+}
+
+#drupal-off-canvas-wrapper .announcement--featured {
+  position: relative;
+  margin-inline: calc(-1 * var(--off-canvas-padding));
+  padding: 0 var(--off-canvas-padding) var(--off-canvas-padding);
+}
+
+#drupal-off-canvas-wrapper .announcement.announcement--featured + .announcement.announcement--standard {
+  border-block-start: 1px solid var(--off-canvas-border-color);
+}
+
+#drupal-off-canvas-wrapper .announcement--standard {
+  padding-block-start: var(--off-canvas-padding);
+}
+
+#drupal-off-canvas-wrapper .announcement__title {
+  font-size: 1rem;
+}
+
+#drupal-off-canvas-wrapper .announcements--view-all {
+  margin-block-start: 3rem;
+}
diff --git a/core/modules/announcements_feed/css/announcements_feed.dialog.pcss.css b/core/modules/announcements_feed/css/announcements_feed.dialog.pcss.css
new file mode 100644
index 0000000000000000000000000000000000000000..5f8c8c58a5e590e452c27bb37da506d18abdc0aa
--- /dev/null
+++ b/core/modules/announcements_feed/css/announcements_feed.dialog.pcss.css
@@ -0,0 +1,49 @@
+/**
+ * @file
+ *
+ * Styles for the announcements feed within the off-canvas dialog.
+ */
+
+#drupal-off-canvas-wrapper {
+
+  & .ui-dialog-titlebar.announce-titlebar::before {
+    -webkit-mask-image: url("../images/announcement-bell.svg");
+    mask-image: url("../images/announcement-bell.svg");
+  }
+
+  & .announcements {
+    padding-block-start: var(--off-canvas-padding);
+  }
+
+  & .announcements ul {
+    margin: 0;
+    padding-inline-start: 0;
+    list-style: none;
+  }
+
+  & .announcement {
+    font-size: 0.875rem;
+  }
+
+  & .announcement--featured {
+    position: relative;
+    margin-inline: calc(-1 * var(--off-canvas-padding));
+    padding: 0 var(--off-canvas-padding) var(--off-canvas-padding);
+  }
+
+  & .announcement.announcement--featured + .announcement.announcement--standard {
+    border-block-start: 1px solid var(--off-canvas-border-color);
+  }
+
+  & .announcement--standard {
+    padding-block-start: var(--off-canvas-padding);
+  }
+
+  & .announcement__title {
+    font-size: 1rem;
+  }
+
+  & .announcements--view-all {
+    margin-block-start: 3rem;
+  }
+}
diff --git a/core/modules/announcements_feed/css/announcements_feed.page.css b/core/modules/announcements_feed/css/announcements_feed.page.css
new file mode 100644
index 0000000000000000000000000000000000000000..13294a65b012e73bd840a58b31e7a4b9a30607dd
--- /dev/null
+++ b/core/modules/announcements_feed/css/announcements_feed.page.css
@@ -0,0 +1,24 @@
+/*
+ * DO NOT EDIT THIS FILE.
+ * See the following change record for more information,
+ * https://www.drupal.org/node/3084859
+ * @preserve
+ */
+
+.announcements ul {
+  margin-inline-start: 0;
+  list-style: none;
+}
+
+.announcement:not(.announcement:last-child) {
+  margin-block-end: 1rem;
+}
+
+.announcement.announcement--featured + .announcement.announcement--standard {
+  padding-block-start: 1rem;
+  border-top: 1px solid #aaa;
+}
+
+.announcements--view-all {
+  margin-block-start: 3rem;
+}
diff --git a/core/modules/announcements_feed/css/announcements_feed.page.pcss.css b/core/modules/announcements_feed/css/announcements_feed.page.pcss.css
new file mode 100644
index 0000000000000000000000000000000000000000..f70f9af3cc8a5f049b966c5b1e1fa43ba9f9228d
--- /dev/null
+++ b/core/modules/announcements_feed/css/announcements_feed.page.pcss.css
@@ -0,0 +1,17 @@
+.announcements ul {
+  margin-inline-start: 0;
+  list-style: none;
+}
+
+.announcement:not(.announcement:last-child) {
+  margin-block-end: 1rem;
+}
+
+.announcement.announcement--featured + .announcement.announcement--standard {
+  padding-block-start: 1rem;
+  border-top: 1px solid #aaa;
+}
+
+.announcements--view-all {
+  margin-block-start: 3rem;
+}
diff --git a/core/modules/announcements_feed/css/announcements_feed.toolbar.css b/core/modules/announcements_feed/css/announcements_feed.toolbar.css
new file mode 100644
index 0000000000000000000000000000000000000000..60aeca2087b47a6c38d29e92667e4c7854cc0ed6
--- /dev/null
+++ b/core/modules/announcements_feed/css/announcements_feed.toolbar.css
@@ -0,0 +1,39 @@
+/*
+ * DO NOT EDIT THIS FILE.
+ * See the following change record for more information,
+ * https://www.drupal.org/node/3084859
+ * @preserve
+ */
+
+/**
+ * @file
+ *
+ * Styles for the announcements toolbar item.
+ */
+
+.toolbar .toolbar-icon.announce-canvas-link::before {
+  background-image: url("data:image/svg+xml,%3csvg width='20' height='19' viewBox='0 0 20 19' fill='none' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M5.73047 16.7648C6.00143 17.4831 6.6872 18 7.50009 18C8.31299 18 8.99876 17.4865 9.26972 16.7682C8.71107 16.8118 8.12231 16.8387 7.50009 16.8387C6.87788 16.8353 6.28912 16.8085 5.73047 16.7648Z' fill='white'/%3e%3cpath d='M14.331 13.4118H14.0801L12.4074 11.3979L11.5143 6.69897H11.5042C11.2333 5.05433 9.97881 3.74869 8.36976 3.39627C8.3731 3.38955 8.37979 3.38284 8.37979 3.37613L8.624 2.63772C8.74108 2.28529 8.53702 2 8.16905 2H6.83095C6.46298 2 6.25892 2.28529 6.37266 2.63772L6.61686 3.37613C6.62021 3.38284 6.62355 3.38955 6.6269 3.39627C5.01784 3.74869 3.76673 5.05433 3.49242 6.69897H3.48238L2.59255 11.3979L0.919938 13.4118H0.669046C0.30107 13.4118 0 13.7139 0 14.0831C0 14.4523 0.280999 14.8618 0.625558 14.996C0.625558 14.996 3.48573 16.0969 7.5 16.0969C11.5143 16.0969 14.3744 14.996 14.3744 14.996C14.719 14.8618 15 14.4523 15 14.0831C15 13.7139 14.6989 13.4118 14.331 13.4118ZM4.58296 6.95742L3.70317 11.8611L1.75624 14.0831H1.23439L3.21811 11.6933L4.15477 6.82652C4.28189 6.0579 4.68332 5.3799 5.24532 4.8798L5.49955 5.19866C5.03122 5.60478 4.68666 6.32305 4.58296 6.95742Z' fill='white'/%3e%3c/svg%3e");
+}
+
+@media (forced-colors: active) {
+
+  .toolbar .toolbar-icon.announce-canvas-link::before {
+    background: linktext;
+    -webkit-mask-image: url("data:image/svg+xml,%3csvg width='20' height='19' viewBox='0 0 20 19' fill='none' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M5.73047 16.7648C6.00143 17.4831 6.6872 18 7.50009 18C8.31299 18 8.99876 17.4865 9.26972 16.7682C8.71107 16.8118 8.12231 16.8387 7.50009 16.8387C6.87788 16.8353 6.28912 16.8085 5.73047 16.7648Z' fill='white'/%3e%3cpath d='M14.331 13.4118H14.0801L12.4074 11.3979L11.5143 6.69897H11.5042C11.2333 5.05433 9.97881 3.74869 8.36976 3.39627C8.3731 3.38955 8.37979 3.38284 8.37979 3.37613L8.624 2.63772C8.74108 2.28529 8.53702 2 8.16905 2H6.83095C6.46298 2 6.25892 2.28529 6.37266 2.63772L6.61686 3.37613C6.62021 3.38284 6.62355 3.38955 6.6269 3.39627C5.01784 3.74869 3.76673 5.05433 3.49242 6.69897H3.48238L2.59255 11.3979L0.919938 13.4118H0.669046C0.30107 13.4118 0 13.7139 0 14.0831C0 14.4523 0.280999 14.8618 0.625558 14.996C0.625558 14.996 3.48573 16.0969 7.5 16.0969C11.5143 16.0969 14.3744 14.996 14.3744 14.996C14.719 14.8618 15 14.4523 15 14.0831C15 13.7139 14.6989 13.4118 14.331 13.4118ZM4.58296 6.95742L3.70317 11.8611L1.75624 14.0831H1.23439L3.21811 11.6933L4.15477 6.82652C4.28189 6.0579 4.68332 5.3799 5.24532 4.8798L5.49955 5.19866C5.03122 5.60478 4.68666 6.32305 4.58296 6.95742Z' fill='white'/%3e%3c/svg%3e");
+    mask-image: url("data:image/svg+xml,%3csvg width='20' height='19' viewBox='0 0 20 19' fill='none' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M5.73047 16.7648C6.00143 17.4831 6.6872 18 7.50009 18C8.31299 18 8.99876 17.4865 9.26972 16.7682C8.71107 16.8118 8.12231 16.8387 7.50009 16.8387C6.87788 16.8353 6.28912 16.8085 5.73047 16.7648Z' fill='white'/%3e%3cpath d='M14.331 13.4118H14.0801L12.4074 11.3979L11.5143 6.69897H11.5042C11.2333 5.05433 9.97881 3.74869 8.36976 3.39627C8.3731 3.38955 8.37979 3.38284 8.37979 3.37613L8.624 2.63772C8.74108 2.28529 8.53702 2 8.16905 2H6.83095C6.46298 2 6.25892 2.28529 6.37266 2.63772L6.61686 3.37613C6.62021 3.38284 6.62355 3.38955 6.6269 3.39627C5.01784 3.74869 3.76673 5.05433 3.49242 6.69897H3.48238L2.59255 11.3979L0.919938 13.4118H0.669046C0.30107 13.4118 0 13.7139 0 14.0831C0 14.4523 0.280999 14.8618 0.625558 14.996C0.625558 14.996 3.48573 16.0969 7.5 16.0969C11.5143 16.0969 14.3744 14.996 14.3744 14.996C14.719 14.8618 15 14.4523 15 14.0831C15 13.7139 14.6989 13.4118 14.331 13.4118ZM4.58296 6.95742L3.70317 11.8611L1.75624 14.0831H1.23439L3.21811 11.6933L4.15477 6.82652C4.28189 6.0579 4.68332 5.3799 5.24532 4.8798L5.49955 5.19866C5.03122 5.60478 4.68666 6.32305 4.58296 6.95742Z' fill='white'/%3e%3c/svg%3e");
+    -webkit-mask-repeat: no-repeat;
+    mask-repeat: no-repeat;
+    -webkit-mask-position: center;
+    mask-position: center;
+  }
+}
+
+/* Pushes the tab to the opposite side of the page. */
+
+.toolbar .toolbar-bar .announce-toolbar-tab.toolbar-tab {
+  float: right; /* LTR */
+}
+
+[dir="rtl"] .toolbar .toolbar-bar .announce-toolbar-tab.toolbar-tab {
+  float: left;
+}
diff --git a/core/modules/announcements_feed/css/announcements_feed.toolbar.pcss.css b/core/modules/announcements_feed/css/announcements_feed.toolbar.pcss.css
new file mode 100644
index 0000000000000000000000000000000000000000..61eaaec866421d8c1d2390ef3e6b2a096a4d7333
--- /dev/null
+++ b/core/modules/announcements_feed/css/announcements_feed.toolbar.pcss.css
@@ -0,0 +1,25 @@
+/**
+ * @file
+ *
+ * Styles for the announcements toolbar item.
+ */
+
+.toolbar .toolbar-icon.announce-canvas-link::before {
+  background-image: url("../images/announcement-bell.svg");
+
+  @media (forced-colors: active) {
+    background: linktext;
+    mask-image: url("../images/announcement-bell.svg");
+    mask-repeat: no-repeat;
+    mask-position: center;
+  }
+}
+
+/* Pushes the tab to the opposite side of the page. */
+.toolbar .toolbar-bar .announce-toolbar-tab.toolbar-tab {
+  float: right; /* LTR */
+
+  &:dir(rtl) {
+    float: left;
+  }
+}
diff --git a/core/modules/announcements_feed/images/announcement-bell.svg b/core/modules/announcements_feed/images/announcement-bell.svg
new file mode 100644
index 0000000000000000000000000000000000000000..83dc07b067681976bf00c8062771fddd843bc37b
--- /dev/null
+++ b/core/modules/announcements_feed/images/announcement-bell.svg
@@ -0,0 +1,4 @@
+<svg width="20" height="19" viewBox="0 0 20 19" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M5.73047 16.7648C6.00143 17.4831 6.6872 18 7.50009 18C8.31299 18 8.99876 17.4865 9.26972 16.7682C8.71107 16.8118 8.12231 16.8387 7.50009 16.8387C6.87788 16.8353 6.28912 16.8085 5.73047 16.7648Z" fill="white"/>
+<path d="M14.331 13.4118H14.0801L12.4074 11.3979L11.5143 6.69897H11.5042C11.2333 5.05433 9.97881 3.74869 8.36976 3.39627C8.3731 3.38955 8.37979 3.38284 8.37979 3.37613L8.624 2.63772C8.74108 2.28529 8.53702 2 8.16905 2H6.83095C6.46298 2 6.25892 2.28529 6.37266 2.63772L6.61686 3.37613C6.62021 3.38284 6.62355 3.38955 6.6269 3.39627C5.01784 3.74869 3.76673 5.05433 3.49242 6.69897H3.48238L2.59255 11.3979L0.919938 13.4118H0.669046C0.30107 13.4118 0 13.7139 0 14.0831C0 14.4523 0.280999 14.8618 0.625558 14.996C0.625558 14.996 3.48573 16.0969 7.5 16.0969C11.5143 16.0969 14.3744 14.996 14.3744 14.996C14.719 14.8618 15 14.4523 15 14.0831C15 13.7139 14.6989 13.4118 14.331 13.4118ZM4.58296 6.95742L3.70317 11.8611L1.75624 14.0831H1.23439L3.21811 11.6933L4.15477 6.82652C4.28189 6.0579 4.68332 5.3799 5.24532 4.8798L5.49955 5.19866C5.03122 5.60478 4.68666 6.32305 4.58296 6.95742Z" fill="white"/>
+</svg>
diff --git a/core/modules/announcements_feed/src/AnnounceFetcher.php b/core/modules/announcements_feed/src/AnnounceFetcher.php
new file mode 100644
index 0000000000000000000000000000000000000000..8d777284e8474ec5813bea4e8cdd4ea2f86ed2be
--- /dev/null
+++ b/core/modules/announcements_feed/src/AnnounceFetcher.php
@@ -0,0 +1,193 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\announcements_feed;
+
+use Composer\Semver\Semver;
+use Drupal\Component\Serialization\Json;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Config\ImmutableConfig;
+use Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface;
+use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
+use Drupal\Core\Utility\Error;
+use GuzzleHttp\ClientInterface;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Service to fetch announcements from the external feed.
+ *
+ * @internal
+ */
+final class AnnounceFetcher {
+
+  /**
+   * The configuration settings of this module.
+   *
+   * @var \Drupal\Core\Config\ImmutableConfig
+   */
+  protected ImmutableConfig $config;
+
+  /**
+   * The tempstore service.
+   *
+   * @var \Drupal\Core\KeyValueStore\KeyValueExpirableFactory
+   */
+  protected KeyValueStoreInterface $tempStore;
+
+  /**
+   * Construct an AnnounceFetcher service.
+   *
+   * @param \GuzzleHttp\ClientInterface $httpClient
+   *   The http client.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config
+   *   The config factory service.
+   * @param \Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface $temp_store
+   *   The tempstore factory service.
+   * @param \Psr\Log\LoggerInterface $logger
+   *   The logger service.
+   * @param string $feedUrl
+   *   The feed url path.
+   */
+  public function __construct(
+    protected ClientInterface $httpClient,
+    ConfigFactoryInterface $config,
+    KeyValueExpirableFactoryInterface $temp_store,
+    protected LoggerInterface $logger,
+    protected string $feedUrl
+  ) {
+    $this->config = $config->get('announcements_feed.settings');
+    $this->tempStore = $temp_store->get('announcements_feed');
+  }
+
+  /**
+   * Fetch ids of announcements.
+   *
+   * @return array
+   *   An array with ids of all announcements in the feed.
+   */
+  public function fetchIds(): array {
+    return array_column($this->fetch(), 'id');
+  }
+
+  /**
+   * Check whether the version given is relevant to the Drupal version used.
+   *
+   * @param string $version
+   *   Version to check.
+   *
+   * @return bool
+   *   Return True if the version matches Drupal version.
+   */
+  protected static function isRelevantItem(string $version): bool {
+    return !empty($version) && Semver::satisfies(\Drupal::VERSION, $version);
+  }
+
+  /**
+   * Check whether a link is controlled by D.O.
+   *
+   * @param string $url
+   *   URL to check.
+   *
+   * @return bool
+   *   Return True if the URL is controlled by the D.O.
+   */
+  public static function validateUrl(string $url): bool {
+    if (empty($url)) {
+      return FALSE;
+    }
+    $host = parse_url($url, PHP_URL_HOST);
+
+    // First character can only be a letter or a digit.
+    // @see https://www.rfc-editor.org/rfc/rfc1123#page-13
+    return $host && preg_match('/^([a-zA-Z0-9][a-zA-Z0-9\-_]*\.)?drupal\.org$/', $host);
+  }
+
+  /**
+   * Fetches the feed either from a local cache or fresh remotely.
+   *
+   * The feed follows the "JSON Feed" format:
+   * - https://www.jsonfeed.org/version/1.1/
+   *
+   * The structure of an announcement item in the feed is:
+   *   - id: Id.
+   *   - title: Title of the announcement.
+   *   - content_html: Announcement teaser.
+   *   - url: URL
+   *   - date_modified: Last updated timestamp.
+   *   - date_published: Created timestamp.
+   *   - _drupalorg.featured: 1 if featured, 0 if not featured.
+   *   - _drupalorg.version: Target version of Drupal, as a Composer version.
+   *
+   * @param bool $force
+   *   (optional) Whether to always fetch new items or not. Defaults to FALSE.
+   *
+   * @return \Drupal\announcements_feed\Announcement[]
+   *   An array of announcements from the feed relevant to the Drupal version.
+   *   The array is empty if there were no matching announcements. If an error
+   *   occurred while fetching/decoding the feed, it is thrown as an exception.
+   *
+   * @throws \Exception
+   */
+  public function fetch(bool $force = FALSE): array {
+    $announcements = $this->tempStore->get('announcements');
+    if ($force || $announcements === NULL) {
+      try {
+        $feed_content = (string) $this->httpClient->get($this->feedUrl)->getBody();
+      }
+      catch (\Exception $e) {
+        $this->logger->error(Error::DEFAULT_ERROR_MESSAGE, Error::decodeException($e));
+        throw $e;
+      }
+
+      $announcements = Json::decode($feed_content);
+      if (!isset($announcements['items'])) {
+        $this->logger->error('The feed format is not valid.');
+        throw new \Exception('Invalid format');
+      }
+
+      $announcements = $announcements['items'] ?? [];
+      // Ensure that announcements reference drupal.org and are applicable to
+      // the current Drupal version.
+      $announcements = array_filter($announcements, function (array $announcement) {
+        return static::validateUrl($announcement['url'] ?? '') && static::isRelevantItem($announcement['_drupalorg']['version'] ?? '');
+      });
+
+      // Save the raw decoded and filtered array to temp store.
+      $this->tempStore->setWithExpire('announcements', $announcements,
+        $this->config->get('max_age'));
+    }
+
+    // The drupal.org endpoint is sorted by created date in descending order.
+    // We will limit the announcements based on the configuration limit.
+    $announcements = array_slice($announcements, 0, $this->config->get('limit') ?? 10);
+
+    // For the remaining announcements, put all the featured announcements
+    // before the rest.
+    uasort($announcements, function ($a, $b) {
+      $a_value = (int) $a['_drupalorg']['featured'];
+      $b_value = (int) $b['_drupalorg']['featured'];
+      if ($a_value == $b_value) {
+        return 0;
+      }
+      return ($a_value < $b_value) ? -1 : 1;
+    });
+
+    // Map the multidimensional array into an array of Announcement objects.
+    $announcements = array_map(function ($announcement) {
+      return new Announcement(
+        $announcement['id'],
+        $announcement['title'],
+        $announcement['url'],
+        $announcement['date_modified'],
+        $announcement['date_published'],
+        $announcement['content_html'],
+        $announcement['_drupalorg']['version'],
+        (bool) $announcement['_drupalorg']['featured'],
+      );
+    }, $announcements);
+
+    return $announcements;
+  }
+
+}
diff --git a/core/modules/announcements_feed/src/Announcement.php b/core/modules/announcements_feed/src/Announcement.php
new file mode 100644
index 0000000000000000000000000000000000000000..326f58cca82a6505bcd2ff4ba2f6edfbf72dadf1
--- /dev/null
+++ b/core/modules/announcements_feed/src/Announcement.php
@@ -0,0 +1,68 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\announcements_feed;
+
+use Drupal\Core\Datetime\DrupalDateTime;
+
+/**
+ * Object containing a single announcement from the feed.
+ *
+ * @internal
+ */
+final class Announcement {
+
+  /**
+   * Construct an Announcement object.
+   *
+   * @param string $id
+   *   Unique identifier of the announcement.
+   * @param string $title
+   *   Title of the announcement.
+   * @param string $url
+   *   URL where the announcement can be seen.
+   * @param string $date_modified
+   *   When was the announcement last modified.
+   * @param string $date_published
+   *   When was the announcement published.
+   * @param string $content_html
+   *   HTML content of the announcement.
+   * @param string $version
+   *   Target Drupal version of the announcement.
+   * @param bool $featured
+   *   Whether this announcement is featured or not.
+   */
+  public function __construct(
+    public readonly string $id,
+    public readonly string $title,
+    public readonly string $url,
+    public readonly string $date_modified,
+    public readonly string $date_published,
+    public readonly string $content_html,
+    public readonly string $version,
+    public readonly bool $featured
+  ) {
+  }
+
+  /**
+   * Returns the content of the announcement with no markup.
+   *
+   * @return string
+   *   Content of the announcement without markup.
+   */
+  public function getContent() {
+    return strip_tags($this->content_html);
+  }
+
+  /**
+   * Gets the published date in timestamp format.
+   *
+   * @return int
+   *   Date published timestamp.
+   */
+  public function getDatePublishedTimestamp() {
+    return DrupalDateTime::createFromFormat(DATE_ATOM, $this->date_published)->getTimestamp();
+  }
+
+}
diff --git a/core/modules/announcements_feed/src/Controller/AnnounceController.php b/core/modules/announcements_feed/src/Controller/AnnounceController.php
new file mode 100644
index 0000000000000000000000000000000000000000..816cae36efc1f255e87e3aac52b4213b228f0881
--- /dev/null
+++ b/core/modules/announcements_feed/src/Controller/AnnounceController.php
@@ -0,0 +1,103 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\announcements_feed\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\announcements_feed\AnnounceFetcher;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Controller for community announcements.
+ *
+ * @internal
+ */
+class AnnounceController extends ControllerBase implements ContainerInjectionInterface {
+
+  /**
+   * Constructs an AnnounceController object.
+   *
+   * @param \Drupal\announcements_feed\AnnounceFetcher $announceFetcher
+   *   The AnnounceFetcher service.
+   * @param string $feedLink
+   *   The feed url path.
+   */
+  public function __construct(
+    protected AnnounceFetcher $announceFetcher,
+    protected string $feedLink
+  ) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container): AnnounceController {
+    return new static(
+      $container->get('announcements_feed.fetcher'),
+      $container->getParameter('announcements_feed.feed_link')
+    );
+  }
+
+  /**
+   * Returns the list of Announcements.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request.
+   *
+   * @return array
+   *   A build array with announcements.
+   */
+  public function getAnnouncements(Request $request): array {
+    try {
+      $announcements = $this->announceFetcher->fetch();
+    }
+    catch (\Exception $e) {
+      return [
+        '#theme' => 'status_messages',
+        '#message_list' => [
+          'error' => [
+            $this->t('An error occurred while parsing the announcements feed, check the logs for more information.'),
+          ],
+        ],
+        '#status_headings' => [
+          'error' => $this->t('Error Message'),
+        ],
+      ];
+    }
+
+    $build = [];
+    foreach ($announcements as $announcement) {
+      $key = $announcement->featured ? '#featured' : '#standard';
+      $build[$key][] = $announcement;
+    }
+
+    $build += [
+      '#theme' => 'announcements_feed',
+      '#count' => count($announcements),
+      '#feed_link' => $this->feedLink,
+      '#cache' => [
+        'contexts' => [
+          'url.query_args:_wrapper_format',
+        ],
+        'tags' => [
+          'announcements_feed:feed',
+        ],
+      ],
+      '#attached' => [
+        'library' => [
+          'announcements_feed/drupal.announcements_feed.dialog',
+        ],
+      ],
+    ];
+    if ($request->query->get('_wrapper_format') != 'drupal_dialog.off_canvas') {
+      $build['#theme'] = 'announcements_feed_admin';
+      $build['#attached'] = [];
+    }
+
+    return $build;
+  }
+
+}
diff --git a/core/modules/announcements_feed/src/LazyBuilders.php b/core/modules/announcements_feed/src/LazyBuilders.php
new file mode 100644
index 0000000000000000000000000000000000000000..8c3aa5691793d7e7c6014be95d782f3ca2c0dd26
--- /dev/null
+++ b/core/modules/announcements_feed/src/LazyBuilders.php
@@ -0,0 +1,93 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\announcements_feed;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\Component\Utility\Html;
+use Drupal\Core\Render\ElementInfoManagerInterface;
+use Drupal\Core\Security\TrustedCallbackInterface;
+use Drupal\Core\Url;
+
+/**
+ * Defines a class for lazy building render arrays.
+ *
+ * @internal
+ */
+final class LazyBuilders implements TrustedCallbackInterface {
+
+  /**
+   * Constructs LazyBuilders object.
+   *
+   * @param \Drupal\Core\Render\ElementInfoManagerInterface $elementInfo
+   *   Element info.
+   */
+  public function __construct(
+    protected ElementInfoManagerInterface $elementInfo,
+  ) {
+  }
+
+  /**
+   * Render announcements.
+   *
+   * @return array
+   *   Render array.
+   */
+  public function renderAnnouncements(): array {
+    $build = [
+      '#type' => 'link',
+      '#cache' => [
+        'context' => ['user.permissions'],
+      ],
+      '#title' => t('Announcements'),
+      '#url' => Url::fromRoute('announcements_feed.announcement'),
+      '#id' => Html::getId('toolbar-item-announcement'),
+      '#attributes' => [
+        'title' => t('Announcements'),
+        'data-drupal-announce-trigger' => '',
+        'class' => [
+          'toolbar-icon',
+          'toolbar-item',
+          'toolbar-icon-announce',
+          'use-ajax',
+          'announce-canvas-link',
+          'announce-default',
+        ],
+        'data-dialog-renderer' => 'off_canvas',
+        'data-dialog-type' => 'dialog',
+        'data-dialog-options' => Json::encode(
+          [
+            'announce' => TRUE,
+            'width' => '25%',
+            'classes' => [
+              'ui-dialog' => 'announce-dialog',
+              'ui-dialog-titlebar' => 'announce-titlebar',
+              'ui-dialog-title' => 'announce-title',
+              'ui-dialog-titlebar-close' => 'announce-close',
+              'ui-dialog-content' => 'announce-body',
+            ],
+          ]),
+      ],
+      '#attached' => [
+        'library' => [
+          'announcements_feed/drupal.announcements_feed.toolbar',
+        ],
+      ],
+    ];
+
+    // The renderer has already added element defaults by the time the lazy
+    // builder is run.
+    // @see https://www.drupal.org/project/drupal/issues/2609250
+    $build += $this->elementInfo->getInfo('link');
+    return $build;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function trustedCallbacks(): array {
+    return ['renderAnnouncements'];
+  }
+
+}
diff --git a/core/modules/announcements_feed/src/RenderCallbacks.php b/core/modules/announcements_feed/src/RenderCallbacks.php
new file mode 100644
index 0000000000000000000000000000000000000000..f424cf54481c3bd9922de454598de0f0ac5ed31e
--- /dev/null
+++ b/core/modules/announcements_feed/src/RenderCallbacks.php
@@ -0,0 +1,31 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\announcements_feed;
+
+use Drupal\Core\Security\TrustedCallbackInterface;
+
+/**
+ * Defines a class for render callbacks.
+ *
+ * @internal
+ */
+final class RenderCallbacks implements TrustedCallbackInterface {
+
+  /**
+   * Render callback.
+   */
+  public static function removeTabAttributes(array $element): array {
+    unset($element['tab']['#attributes']);
+    return $element;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function trustedCallbacks(): array {
+    return ['removeTabAttributes'];
+  }
+
+}
diff --git a/core/modules/announcements_feed/templates/announcements-feed-admin.html.twig b/core/modules/announcements_feed/templates/announcements-feed-admin.html.twig
new file mode 100644
index 0000000000000000000000000000000000000000..4126087b47c827c0d1124cb0f9ff265914b17d9e
--- /dev/null
+++ b/core/modules/announcements_feed/templates/announcements-feed-admin.html.twig
@@ -0,0 +1,27 @@
+{#
+/**
+ * @file
+ * Template file for the theming of announcement_feed admin page.
+ *
+ * This template will get rendered when the user navigates to the announcements_feed.announcement route.
+ *
+ * Available variables:
+ * - count: Contains the total number of announcements.
+ * - featured: A list of featured announcement objects.
+ * - standard: A list of non-featured announcement objects.
+ *
+ * Announcement objects have the following variables:
+ * - id: Unique id of the announcement.
+ * - title: Title of the standard announcement.
+ * - content: Short description of the announcement.
+ * - datePublishedTimestamp: Timestamp of the announcement.
+ * - url: Learn more link of the standard announcement.
+ *
+ * @see announcements_feed_theme()
+ *
+ * @ingroup themeable
+ */
+#}
+{{ attach_library('announcements_feed/drupal.announcements_feed.page') }}
+
+{% include '@announcements_feed/announcements.html.twig' %}
diff --git a/core/modules/announcements_feed/templates/announcements-feed.html.twig b/core/modules/announcements_feed/templates/announcements-feed.html.twig
new file mode 100644
index 0000000000000000000000000000000000000000..6cad91d25fdb8353f1c54acc5b0109295cb9a172
--- /dev/null
+++ b/core/modules/announcements_feed/templates/announcements-feed.html.twig
@@ -0,0 +1,25 @@
+{#
+/**
+ * @file
+ * Template file for the theming of announcement_feed off-canvas dialog.
+ *
+ * This template will get rendered when the user clicks the announcement button in the toolbar.
+ *
+ * Available variables:
+ * - count: Contains the total number of announcements.
+ * - featured: A list of featured announcement objects.
+ * - standard: A list of non-featured announcement objects.
+ *
+ * Announcement objects have the following variables:
+ * - id: Unique id of the announcement.
+ * - title: Title of the standard announcement.
+ * - content: Short description of the announcement.
+ * - datePublishedTimestamp: Timestamp of the announcement.
+ * - url: Learn more link of the standard announcement.
+ *
+ * @see announcements_feed_theme()
+ *
+ * @ingroup themeable
+ */
+#}
+{% include '@announcements_feed/announcements.html.twig' %}
diff --git a/core/modules/announcements_feed/templates/announcements.html.twig b/core/modules/announcements_feed/templates/announcements.html.twig
new file mode 100644
index 0000000000000000000000000000000000000000..d2d8e8adfdb4bc68595db37d687c3da8c295bcd4
--- /dev/null
+++ b/core/modules/announcements_feed/templates/announcements.html.twig
@@ -0,0 +1,37 @@
+{% if count %}
+  <nav class="announcements">
+    <ul>
+      {% if featured|length %}
+        {% for announcement in featured %}
+          <li class="announcement announcement--featured" data-drupal-featured>
+            <div class="announcement__title">
+              <h4>{{ announcement.title }}</h4>
+            </div>
+            <div class="announcement__teaser">
+              {{ announcement.content }}
+            </div>
+            <div class="announcement__link">
+              <a href="{{ announcement.url }}">{{ 'Learn More'|t }}</a>
+            </div>
+          </li>
+        {% endfor %}
+      {% endif %}
+      {% for announcement in standard %}
+        <li class="announcement announcement--standard">
+          <div class="announcement__title">
+            <a href="{{ announcement.url }}">{{ announcement.title }}</a>
+            <div class="announcement__date">{{ announcement.datePublishedTimestamp | format_date('short') }}</div>
+          </div>
+        </li>
+      {% endfor %}
+    </ul>
+  </nav>
+
+  {% if feed_link %}
+    <p class="announcements--view-all">
+      <a target="_blank" href="{{ feed_link }}">{{ 'View all announcements'|t }}</a>
+    </p>
+  {% endif %}
+{% else %}
+  <div class="announcements announcements--empty"><p> {{ 'No announcements available'|t }}</p></div>
+{% endif %}
diff --git a/core/modules/announcements_feed/tests/announce_feed/community-feeds.json b/core/modules/announcements_feed/tests/announce_feed/community-feeds.json
new file mode 100644
index 0000000000000000000000000000000000000000..594c6d24c9e525ecc7e90ba1cd3db3453bb3d891
--- /dev/null
+++ b/core/modules/announcements_feed/tests/announce_feed/community-feeds.json
@@ -0,0 +1,57 @@
+{
+  "version": "https://jsonfeed.org/version/1.1",
+  "title": "Drupal Announcements Feed",
+  "home_page_url": "https://www.drupal.org",
+  "feed_url": "https://www.drupal.org/announcements.json",
+  "favicon": "https://www.drupal.org/favicon.ico",
+  "items": [
+    {
+      "id": "201",
+      "title": "new 9 - 10 Drupal 9.1.3 is available",
+      "content_html": "This release will have a community alert prototype to notify site admins about drupal updates and required information",
+      "url": "https://www.drupal.org/project/announce",
+      "date_modified": "2021-01-19T07:29:38+00:00",
+      "date_published": "2021-01-18T07:29:38+00:00",
+      "_drupalorg": {
+        "featured": false,
+        "version": "^9 | ^10"
+      }
+    },
+    {
+      "id": "2021",
+      "title": "updated 10 - DrupalCon is here",
+      "content_html": "This release will have a community alert prototype to notify site admins about drupal updates and required information",
+      "url": "https://www.drupal.org/project/announce",
+      "date_modified": "2021-01-19T07:29:38+00:00",
+      "date_published": "2021-01-18T07:29:38+00:00",
+      "_drupalorg":  {
+        "featured": true,
+        "version": "^10"
+      }
+    },
+    {
+      "id": "2031",
+      "title": "new 9 only - Download latest drupal here",
+      "content_html": "This release will have a community alert prototype to notify site admins about drupal updates and required information",
+      "url": "https://www.drupal.org/project/announce",
+      "date_modified": "2021-01-19T07:29:38+00:00",
+      "date_published": "2021-01-18T07:29:38+00:00",
+      "_drupalorg":  {
+        "featured": false,
+        "version": "^9"
+      }
+    },
+    {
+      "id": "2043",
+      "title": "Only 10 - Drupal 106 is available",
+      "content_html": "This release will have a community alert prototype to notify site admins about drupal updates and required information",
+      "url": "https://www.drupal.org/project/announce",
+      "date_modified": "2021-01-19T07:29:39+00:00",
+      "date_published": "2021-01-18T07:29:39+00:00",
+      "_drupalorg":  {
+        "featured": false,
+        "version": "^10"
+      }
+    }
+  ]
+}
diff --git a/core/modules/announcements_feed/tests/announce_feed/empty.json b/core/modules/announcements_feed/tests/announce_feed/empty.json
new file mode 100644
index 0000000000000000000000000000000000000000..c49ae3aaed4a15c43c5b9d11ceb491d5543cf6e4
--- /dev/null
+++ b/core/modules/announcements_feed/tests/announce_feed/empty.json
@@ -0,0 +1,8 @@
+{
+  "version": "https://jsonfeed.org/version/1.1",
+  "title": "Drupal Announcements Feed",
+  "home_page_url": "https://www.drupal.org",
+  "feed_url": "https://www.drupal.org/announcements.json",
+  "favicon": "https://www.drupal.org/favicon.ico",
+  "items": []
+}
diff --git a/core/modules/announcements_feed/tests/announce_feed/removed.json b/core/modules/announcements_feed/tests/announce_feed/removed.json
new file mode 100644
index 0000000000000000000000000000000000000000..7a27be7b9decd9a6c0b5248258832bc0bbbb9069
--- /dev/null
+++ b/core/modules/announcements_feed/tests/announce_feed/removed.json
@@ -0,0 +1,45 @@
+{
+  "version": "https://jsonfeed.org/version/1.1",
+  "title": "Drupal Announcements Feed",
+  "home_page_url": "https://www.drupal.org",
+  "feed_url": "https://www.drupal.org/announcements.json",
+  "favicon": "https://www.drupal.org/favicon.ico",
+  "items": [
+    {
+      "id": "201",
+      "title": "new 9 - 10 Drupal 9.1.3 is available",
+      "content_html": "This release will have a community alert prototype to notify site admins about drupal updates and required information",
+      "url": "https://www.drupal.org/project/announce",
+      "date_modified": "2021-01-19T07:29:38+00:00",
+      "date_published": "2021-01-18T07:29:38+00:00",
+      "_drupalorg": {
+        "featured": true,
+        "version": "^9 | ^10"
+      }
+    },
+    {
+      "id": "2021",
+      "title": "updated 10 - DrupalCon is here",
+      "content_html": "This release will have a community alert prototype to notify site admins about drupal updates and required information",
+      "url": "https://www.drupal.org/project/announce",
+      "date_modified": "2021-01-19T07:29:38+00:00",
+      "date_published": "2021-01-18T07:29:38+00:00",
+      "_drupalorg":  {
+        "featured": false,
+        "version": "^10"
+      }
+    },
+    {
+      "id": "2031",
+      "title": "new 9 only - Download latest drupal here",
+      "content_html": "This release will have a community alert prototype to notify site admins about drupal updates and required information",
+      "url": "https://www.drupal.org/project/announce",
+      "date_modified": "2021-01-19T07:29:38+00:00",
+      "date_published": "2021-01-18T07:29:38+00:00",
+      "_drupalorg":  {
+        "featured": false,
+        "version": "^9"
+      }
+    }
+  ]
+}
diff --git a/core/modules/announcements_feed/tests/announce_feed/updated.json b/core/modules/announcements_feed/tests/announce_feed/updated.json
new file mode 100644
index 0000000000000000000000000000000000000000..9c5edcf74f9705007aca7188c98dcbc81c1ae3b1
--- /dev/null
+++ b/core/modules/announcements_feed/tests/announce_feed/updated.json
@@ -0,0 +1,69 @@
+{
+  "version": "https://jsonfeed.org/version/1.1",
+  "title": "Drupal Announcements Feed",
+  "home_page_url": "https://www.drupal.org",
+  "feed_url": "https://www.drupal.org/announcements.json",
+  "favicon": "https://www.drupal.org/favicon.ico",
+  "items": [
+    {
+      "id": "201",
+      "title": "new 9 - 10 Drupal 9.1.3 is available",
+      "content_html": "This release will have a community alert prototype to notify site admins about drupal updates and required information",
+      "url": "https://www.drupal.org/project/announce",
+      "date_modified": "2021-01-19T07:29:38+00:00",
+      "date_published": "2021-01-18T07:29:38+00:00",
+      "_drupalorg":{
+        "featured": true,
+        "version": "^9 | ^10"
+      }
+    },
+    {
+      "id": "2021",
+      "title": "updated 10 - DrupalCon is here",
+      "content_html": "This release will have a community alert prototype to notify site admins about drupal updates and required information",
+      "url": "https://www.drupal.org/project/announce",
+      "date_modified": "2021-01-19T07:29:38+00:00",
+      "date_published": "2021-01-18T07:29:38+00:00",
+      "_drupalorg": {
+        "featured": false,
+        "version": "^10"
+      }
+    },
+    {
+      "id": "2031",
+      "title": "new 9 only - Download latest drupal here",
+      "content_html": "This release will have a community alert prototype to notify site admins about drupal updates and required information",
+      "url": "https://www.drupal.org/project/announce",
+      "date_modified": "2021-01-19T07:29:38+00:00",
+      "date_published": "2021-01-18T07:29:38+00:00",
+      "_drupalorg": {
+        "featured": false,
+        "version": "^9"
+      }
+    },
+    {
+      "id": "2043",
+      "title": "announce title updated",
+      "content_html": "This release will have a community alert prototype to notify site admins about drupal updates and required information",
+      "url": "https://www.drupal.org/project/announce",
+      "date_modified": "2021-01-19T07:29:39+00:00",
+      "date_published": "2021-01-18T07:29:39+00:00",
+      "_drupalorg": {
+        "featured": false,
+        "version": "^10"
+      }
+    },
+    {
+      "id": "2044",
+      "title": "Only 10 - Drupal 106 is available and this feed is Updated",
+      "content_html": "This release will have a community alert prototype to notify site admins about drupal updates and required information",
+      "url": "https://www.drupal.org/project/announce-updated",
+      "date_modified": "2021-01-19T07:29:39+00:00",
+      "date_published": "2021-01-18T07:29:39+00:00",
+      "_drupalorg": {
+        "featured": false,
+        "version": "^10"
+      }
+    }
+  ]
+}
diff --git a/core/modules/announcements_feed/tests/modules/announce_feed_test/announce_feed_test.info.yml b/core/modules/announcements_feed/tests/modules/announce_feed_test/announce_feed_test.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..fb908e4a52c92b47997d08b73f0d1875dc03a53a
--- /dev/null
+++ b/core/modules/announcements_feed/tests/modules/announce_feed_test/announce_feed_test.info.yml
@@ -0,0 +1,4 @@
+name: 'Announce feed test'
+type: module
+description: 'Support module for announce feed testing.'
+package: Testing
diff --git a/core/modules/announcements_feed/tests/modules/announce_feed_test/announce_feed_test.routing.yml b/core/modules/announcements_feed/tests/modules/announce_feed_test/announce_feed_test.routing.yml
new file mode 100644
index 0000000000000000000000000000000000000000..6173c8dfadba461d2ba5aae111dee0d1da018999
--- /dev/null
+++ b/core/modules/announcements_feed/tests/modules/announce_feed_test/announce_feed_test.routing.yml
@@ -0,0 +1,7 @@
+announce_feed_test.json_test:
+  path: '/announce-feed-json/{json_name}'
+  defaults:
+    _title: 'Announce Feed test'
+    _controller: '\Drupal\announce_feed_test\Controller\AnnounceTestController::setFeedConfig'
+  requirements:
+    _access: 'TRUE'
diff --git a/core/modules/announcements_feed/tests/modules/announce_feed_test/announce_feed_test.services.yml b/core/modules/announcements_feed/tests/modules/announce_feed_test/announce_feed_test.services.yml
new file mode 100644
index 0000000000000000000000000000000000000000..52119512a3cfe556265f8bb8484fd0b57833552e
--- /dev/null
+++ b/core/modules/announcements_feed/tests/modules/announce_feed_test/announce_feed_test.services.yml
@@ -0,0 +1,5 @@
+services:
+  announce_feed_test.announce_client_middleware:
+    class: Drupal\announce_feed_test\AnnounceTestHttpClientMiddleware
+    tags:
+      - { name: http_client_middleware }
diff --git a/core/modules/announcements_feed/tests/modules/announce_feed_test/src/AnnounceTestHttpClientMiddleware.php b/core/modules/announcements_feed/tests/modules/announce_feed_test/src/AnnounceTestHttpClientMiddleware.php
new file mode 100644
index 0000000000000000000000000000000000000000..0c68d7c98a2f72e89ee30bd376b8748be2589051
--- /dev/null
+++ b/core/modules/announcements_feed/tests/modules/announce_feed_test/src/AnnounceTestHttpClientMiddleware.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace Drupal\announce_feed_test;
+
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Url;
+use GuzzleHttp\Promise\PromiseInterface;
+use GuzzleHttp\Psr7\Uri;
+use Psr\Http\Message\RequestInterface;
+
+/**
+ * Overrides the requested endpoint when running tests.
+ */
+class AnnounceTestHttpClientMiddleware {
+
+  /**
+   * HTTP middleware that replaces request endpoint for a test one.
+   */
+  public function __invoke(): \Closure {
+    return function ($handler) {
+      return function (RequestInterface $request, array $options) use ($handler): PromiseInterface {
+        $test_end_point = \Drupal::state()->get('announce_test_endpoint');
+        if ($test_end_point && str_contains($request->getUri(), '://www.drupal.org/announcements.json')) {
+          // Only override $uri if it matches the advisories JSON feed to avoid
+          // changing any other uses of the 'http_client' service during tests with
+          // this module installed.
+          $request = $request->withUri(new Uri($test_end_point));
+        }
+        return $handler($request, $options);
+      };
+    };
+  }
+
+  /**
+   * Sets the test endpoint for the advisories JSON feed.
+   *
+   * @param string $test_endpoint
+   *   The test endpoint.
+   */
+  public static function setAnnounceTestEndpoint(string $test_endpoint): void {
+    // Convert the endpoint to an absolute URL.
+    $test_endpoint = Url::fromUri('base:/' . $test_endpoint)->setAbsolute()->toString();
+    \Drupal::state()->set('announce_test_endpoint', $test_endpoint);
+    \Drupal::service('keyvalue.expirable')->get('announcements_feed')->delete('announcements');
+    Cache::invalidateTags(['announcements_feed:feed']);
+  }
+
+}
diff --git a/core/modules/announcements_feed/tests/modules/announce_feed_test/src/Controller/AnnounceTestController.php b/core/modules/announcements_feed/tests/modules/announce_feed_test/src/Controller/AnnounceTestController.php
new file mode 100644
index 0000000000000000000000000000000000000000..305f764a793c3d9dd6b8d1c97045135e70ce6f8f
--- /dev/null
+++ b/core/modules/announcements_feed/tests/modules/announce_feed_test/src/Controller/AnnounceTestController.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Drupal\announce_feed_test\Controller;
+
+use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Defines a controller to return JSON for security advisory tests.
+ */
+class AnnounceTestController {
+
+  /**
+   * Reads a JSON file and returns the contents as a Response.
+   *
+   * This method will replace the string '[CORE_VERSION]' with the current core
+   * version to allow testing core version matches.
+   *
+   * @param string $json_name
+   *   The name of the JSON file without the file extension.
+   *
+   * @return \Symfony\Component\HttpFoundation\JsonResponse|\Symfony\Component\HttpFoundation\Response
+   *   If a fixture file with the name $json_name + '.json' is found a
+   *   JsonResponse will be returned using the contents of the file, otherwise a
+   *   Response will be returned with a 404 status code.
+   */
+  public function setFeedConfig(string $json_name): JsonResponse|Response {
+    $file = __DIR__ . "/../../../../announce_feed/$json_name.json";
+    $headers = ['Content-Type' => 'application/json; charset=utf-8'];
+    if (!is_file($file)) {
+      // Return an empty response.
+      return new Response('', 404, $headers);
+    }
+    return new JsonResponse(file_get_contents($file), 200, $headers, TRUE);
+  }
+
+}
diff --git a/core/modules/announcements_feed/tests/src/Functional/AnnouncementsCacheTest.php b/core/modules/announcements_feed/tests/src/Functional/AnnouncementsCacheTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..82a6196c5dcf5cefab04e2c6c86da0450851e7a1
--- /dev/null
+++ b/core/modules/announcements_feed/tests/src/Functional/AnnouncementsCacheTest.php
@@ -0,0 +1,48 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\announcements_feed\Functional;
+
+use Drupal\Core\Url;
+use Drupal\Tests\BrowserTestBase;
+use Drupal\dynamic_page_cache\EventSubscriber\DynamicPageCacheSubscriber;
+
+/**
+ * Defines a class for testing pages are still cacheable with dynamic page cache.
+ *
+ * @group announcements_feed
+ */
+final class AnnouncementsCacheTest extends BrowserTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'announcements_feed',
+    'dynamic_page_cache',
+    'toolbar',
+  ];
+
+  /**
+   * Tests dynamic page cache.
+   */
+  public function testDynamicPageCache(): void {
+    $this->drupalLogin($this->drupalCreateUser([
+      'access toolbar',
+      'access announcements',
+    ]));
+    // Front-page is visited right after login.
+    $this->assertSession()->responseHeaderEquals(DynamicPageCacheSubscriber::HEADER, 'MISS');
+    // Reload the page, it should be cached now.
+    $this->drupalGet(Url::fromRoute('<front>'));
+    $this->assertSession()->elementExists('css', '[data-drupal-announce-trigger]');
+    $this->assertSession()->responseHeaderEquals(DynamicPageCacheSubscriber::HEADER, 'HIT');
+  }
+
+}
diff --git a/core/modules/announcements_feed/tests/src/FunctionalJavascript/AccessAnnouncementTest.php b/core/modules/announcements_feed/tests/src/FunctionalJavascript/AccessAnnouncementTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..ec0a8a13131c9194b4d52d7024a5a62f06166fd1
--- /dev/null
+++ b/core/modules/announcements_feed/tests/src/FunctionalJavascript/AccessAnnouncementTest.php
@@ -0,0 +1,83 @@
+<?php
+
+namespace Drupal\Tests\announcements_feed\FunctionalJavascript;
+
+use Drupal\Tests\system\FunctionalJavascript\OffCanvasTestBase;
+use Drupal\announce_feed_test\AnnounceTestHttpClientMiddleware;
+
+/**
+ * Test the access announcement permissions to get access announcement icon.
+ *
+ * @group announcements_feed
+ */
+class AccessAnnouncementTest extends OffCanvasTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'user',
+    'toolbar',
+    'announcements_feed',
+    'announce_feed_test',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp():void {
+    parent::setUp();
+    AnnounceTestHttpClientMiddleware::setAnnounceTestEndpoint('/announce-feed-json/community-feeds');
+  }
+
+  /**
+   * Test of viewing announcements by a user with appropriate permission.
+   */
+  public function testAnnounceFirstLogin() {
+    $this->drupalLogin(
+      $this->drupalCreateUser(
+        [
+          'access toolbar',
+          'access announcements',
+        ]
+      )
+    );
+
+    $this->drupalGet('<front>');
+
+    // Check that the user can see the toolbar.
+    $this->assertSession()->elementExists('css', '#toolbar-bar');
+
+    // And the announcements.
+    $this->assertSession()->elementExists('css', '.toolbar-icon-announce');
+  }
+
+  /**
+   * Testing announce icon without announce permission.
+   */
+  public function testAnnounceWithoutPermission() {
+    // User without "access announcements" permission.
+    $account = $this->drupalCreateUser(
+      [
+        'access toolbar',
+      ]
+    );
+    $this->drupalLogin($account);
+    $this->drupalGet('<front>');
+
+    // Check that the user can see the toolbar.
+    $this->assertSession()->elementExists('css', '#toolbar-bar');
+
+    // But not the announcements.
+    $this->assertSession()->elementNotExists('css', '.toolbar-icon-announce');
+
+    $this->drupalGet('admin/announcements_feed');
+    $this->assertSession()->responseContains('You are not authorized to access this page.');
+  }
+
+}
diff --git a/core/modules/announcements_feed/tests/src/FunctionalJavascript/AlertsJsonFeedTest.php b/core/modules/announcements_feed/tests/src/FunctionalJavascript/AlertsJsonFeedTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..d3252a7afdd02dc5d4c8de8af7125e1b186a10a2
--- /dev/null
+++ b/core/modules/announcements_feed/tests/src/FunctionalJavascript/AlertsJsonFeedTest.php
@@ -0,0 +1,101 @@
+<?php
+
+namespace Drupal\Tests\announcements_feed\FunctionalJavascript;
+
+use Drupal\Tests\system\FunctionalJavascript\OffCanvasTestBase;
+use Drupal\announce_feed_test\AnnounceTestHttpClientMiddleware;
+use Drupal\user\UserInterface;
+
+/**
+ * Test the access announcement according to json feed changes.
+ *
+ * @group announcements_feed
+ */
+class AlertsJsonFeedTest extends OffCanvasTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'user',
+    'toolbar',
+    'announcements_feed',
+    'announce_feed_test',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * A user with permission to access toolbar and access announcements.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected UserInterface $user;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp():void {
+    parent::setUp();
+
+    $this->user = $this->drupalCreateUser(
+      [
+        'access toolbar',
+        'access announcements',
+      ]
+    );
+
+    AnnounceTestHttpClientMiddleware::setAnnounceTestEndpoint('/announce-feed-json/community-feeds');
+  }
+
+  /**
+   * Check the status of the announcements when the feed is updated and removed.
+   */
+  public function testAnnounceFeedUpdatedAndRemoved() {
+    $this->drupalLogin($this->user);
+    $this->drupalGet('<front>');
+    $this->clickLink('Announcements');
+    $this->waitForOffCanvasToOpen();
+    $page_html = $this->getSession()->getPage()->getHtml();
+    $this->assertStringNotContainsString('Only 10 - Drupal 106 is available and this feed is Updated', $page_html);
+
+    // Change the feed url and reset temp storage.
+    AnnounceTestHttpClientMiddleware::setAnnounceTestEndpoint('/announce-feed-json/updated');
+
+    $this->drupalGet('<front>');
+    $this->clickLink('Announcements');
+    $this->waitForOffCanvasToOpen();
+    $page_html = $this->getSession()->getPage()->getHtml();
+    $this->assertStringContainsString('Only 10 - Drupal 106 is available and this feed is Updated', $page_html);
+    $this->drupalLogout();
+
+    // Change the feed url and reset temp storage.
+    AnnounceTestHttpClientMiddleware::setAnnounceTestEndpoint('/announce-feed-json/removed');
+    $this->drupalLogin($this->user);
+    $this->drupalGet('<front>');
+    $this->clickLink('Announcements');
+    $this->waitForOffCanvasToOpen();
+    $page_html = $this->getSession()->getPage()->getHtml();
+    $this->assertStringNotContainsString('Only 10 - Drupal 106 is available and this feed is Updated', $page_html);
+  }
+
+  /**
+   * Check with an empty JSON feed.
+   */
+  public function testAnnounceFeedEmpty() {
+    // Change the feed url and reset temp storage.
+    AnnounceTestHttpClientMiddleware::setAnnounceTestEndpoint('/announce-feed-json/empty');
+
+    $this->drupalLogin($this->user);
+    $this->drupalGet('<front>');
+
+    // Removed items should not display in the announcement model.
+    $this->clickLink('Announcements');
+    $this->waitForOffCanvasToOpen();
+    $this->assertStringContainsString('No announcements available', $this->getSession()->getPage()->getHtml());
+  }
+
+}
diff --git a/core/modules/announcements_feed/tests/src/Kernel/AnnounceFetcherTest.php b/core/modules/announcements_feed/tests/src/Kernel/AnnounceFetcherTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..67d932363f9fb615ac2041ab68819d7396dc680a
--- /dev/null
+++ b/core/modules/announcements_feed/tests/src/Kernel/AnnounceFetcherTest.php
@@ -0,0 +1,152 @@
+<?php
+
+namespace Drupal\Tests\announcements_feed\Kernel;
+
+use GuzzleHttp\Psr7\Response;
+
+/**
+ * @coversDefaultClass \Drupal\announcements_feed\AnnounceFetcher
+ *
+ * @group announcements_feed
+ */
+class AnnounceFetcherTest extends AnnounceTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+    $this->installConfig(['announcements_feed']);
+  }
+
+  /**
+   * Tests announcement that should be displayed.
+   *
+   * @param mixed[] $feed_item
+   *   The feed item to test. 'title' and 'url' are omitted from this array
+   *   because they do not need to vary between test cases.
+   *
+   * @dataProvider providerShowAnnouncements
+   */
+  public function testShowAnnouncements(array $feed_item): void {
+    $this->setFeedItems([$feed_item]);
+    $feeds = $this->fetchFeedItems();
+    $this->assertCount(1, $feeds);
+    $this->assertSame('https://www.drupal.org/project/announce', $feeds[0]->url);
+    $this->assertSame('Drupal security update Test', $feeds[0]->title);
+    $this->assertSame('^10', $feeds[0]->version);
+    $this->assertCount(1, $this->history);
+  }
+
+  /**
+   * Tests feed fields.
+   */
+  public function testFeedFields(): void {
+    $feed_item_1 = [
+      'id' => '1001',
+      'content_html' => 'Test teaser 1',
+      'url' => 'https://www.drupal.org/project/announce',
+      '_drupalorg' => [
+        'featured' => TRUE,
+        'version' => '^10',
+      ],
+      'date_modified' => "2021-09-02T15:09:42+00:00",
+      'date_published' => "2021-09-01T15:09:42+00:00",
+    ];
+    $this->setFeedItems([$feed_item_1]);
+    $feeds = $this->fetchFeedItems();
+    $this->assertCount(1, $feeds);
+    $this->assertSame($feed_item_1['id'], $feeds[0]->id);
+    $this->assertSame($feed_item_1['content_html'], $feeds[0]->content_html);
+    $this->assertSame($feed_item_1['_drupalorg']['featured'], $feeds[0]->featured);
+    $this->assertSame($feed_item_1['date_published'], $feeds[0]->date_published);
+    $this->assertSame($feed_item_1['_drupalorg']['version'], $feeds[0]->version);
+  }
+
+  /**
+   * Data provider for testShowAnnouncements().
+   */
+  public function providerShowAnnouncements(): array {
+    return [
+      '1' => [
+        'feed_item' => [
+          'id' => '1001',
+          'content_html' => 'Test teaser 1',
+          '_drupalorg' => [
+            'featured' => 1,
+            'version' => '^10',
+          ],
+          'date_modified' => "2021-09-02T15:09:42+00:00",
+          'date_published' => "2021-09-01T15:09:42+00:00",
+        ],
+      ],
+      '2' => [
+        'feed_item' => [
+          'id' => '1002',
+          'content_html' => 'Test teaser 2',
+          '_drupalorg' => [
+            'featured' => 1,
+            'version' => '^10',
+          ],
+          'date_modified' => "2021-09-02T15:09:42+00:00",
+          'date_published' => "2021-09-01T15:09:42+00:00",
+        ],
+      ],
+      '3' => [
+        'feed_item' => [
+          'id' => '1003',
+          'content_html' => 'Test teaser 3',
+          '_drupalorg' => [
+            'featured' => 1,
+            'version' => '^10',
+          ],
+          'date_modified' => "2021-09-02T15:09:42+00:00",
+          'date_published' => "2021-09-01T15:09:42+00:00",
+        ],
+      ],
+      '4' => [
+        'feed_item' => [
+          'id' => '1004',
+          'content_html' => 'Test teaser 4',
+          '_drupalorg' => [
+            'featured' => 1,
+            'version' => '^10',
+          ],
+          'date_modified' => "2021-09-02T15:09:42+00:00",
+          'date_published' => "2021-09-01T15:09:42+00:00",
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * Sets the feed items to be returned for the test.
+   *
+   * @param mixed[][] $feed_items
+   *   The feeds items to test. Every time the http_client makes a request the
+   *   next item in this array will be returned. For each feed item 'title' and
+   *   'url' are omitted because they do not need to vary between test cases.
+   */
+  protected function setFeedItems(array $feed_items): void {
+    $responses = [];
+    foreach ($feed_items as $feed_item) {
+      $feed_item += [
+        'title' => 'Drupal security update Test',
+        'url' => 'https://www.drupal.org/project/announce',
+      ];
+      $responses[] = new Response(200, [], json_encode(['items' => [$feed_item]]));
+    }
+    $this->setTestFeedResponses($responses);
+  }
+
+  /**
+   * Gets the announcements from the 'announce.fetcher' service.
+   *
+   * @return \Drupal\announcements_feed\Announcement[]
+   *   The return value of AnnounceFetcher::fetch().
+   */
+  protected function fetchFeedItems(): array {
+    return $this->container->get('announcements_feed.fetcher')->fetch();
+  }
+
+}
diff --git a/core/modules/announcements_feed/tests/src/Kernel/AnnounceFetcherUserTest.php b/core/modules/announcements_feed/tests/src/Kernel/AnnounceFetcherUserTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..09bf7eb413174ac1ac8254851ec03401558a5fe6
--- /dev/null
+++ b/core/modules/announcements_feed/tests/src/Kernel/AnnounceFetcherUserTest.php
@@ -0,0 +1,225 @@
+<?php
+
+namespace Drupal\Tests\announcements_feed\Kernel;
+
+use Drupal\Tests\user\Traits\UserCreationTrait;
+use GuzzleHttp\Psr7\Response;
+
+/**
+ * @coversDefaultClass \Drupal\announcements_feed\AnnounceFetcher
+ *
+ * @group announcements_feed
+ */
+class AnnounceFetcherUserTest extends AnnounceTestBase {
+
+  use UserCreationTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'toolbar',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+    $this->installSchema('user', ['users_data']);
+
+    // Setting current user.
+    $permissions = [
+      'access toolbar',
+      'access announcements',
+    ];
+    $this->setUpCurrentUser(['uid' => 1], $permissions);
+  }
+
+  /**
+   * Tests testAllAnnouncements should get all announcements.
+   *
+   * First time accessing the announcements.
+   */
+  public function testAllAnnouncementsFirst(): void {
+
+    $feed_items = $this->providerShowAnnouncements();
+
+    // First time access.
+    $this->setFeedItems($feed_items);
+    $all_items = $this->container->get('announcements_feed.fetcher')->fetch();
+    $this->assertCount(4, $all_items);
+    $this->assertCount(1, $this->history);
+
+    // Second time access.
+    $this->setFeedItems($feed_items);
+    $all_items = $this->container->get('announcements_feed.fetcher')->fetch();
+    $this->assertCount(4, $all_items);
+    $this->assertCount(2, $this->history);
+
+    // Create another user and test again.
+    $permissions = [
+      'access toolbar',
+      'access announcements',
+    ];
+    $this->setUpCurrentUser(['uid' => 2], $permissions);
+    $this->setFeedItems($feed_items);
+
+    // First time access.
+    $all_items = $this->container->get('announcements_feed.fetcher')->fetch();
+    $this->assertCount(4, $all_items);
+    $this->assertCount(3, $this->history);
+
+    // Check after adding new record.
+    $feed_items = $this->providerShowUpdatedAnnouncements();
+    $this->setFeedItems($feed_items);
+    $all_items = $this->container->get('announcements_feed.fetcher')->fetch();
+    $this->assertCount(5, $all_items);
+    $this->assertSame('1005', $all_items[0]->id);
+    $this->assertCount(4, $this->history);
+  }
+
+  /**
+   * Data provider for testAllAnnouncements().
+   */
+  public function providerShowAnnouncements(): array {
+    return [
+      [
+        'id' => '1001',
+        'title' => 'Drupal security update Test',
+        'url' => 'https://www.drupal.org/project/announce',
+        'content_html' => 'Test teaser 1',
+        '_drupalorg' => [
+          'featured' => TRUE,
+          'version' => '^10',
+        ],
+        'date_modified' => date('c', 1611041378),
+        'date_published' => date('c', 1610958578),
+      ],
+      [
+        'id' => '1002',
+        'title' => 'Drupal security update Test',
+        'url' => 'https://www.drupal.org/project/announce',
+        'content_html' => 'Test teaser 2',
+        '_drupalorg' => [
+          'featured' => TRUE,
+          'version' => '^10',
+        ],
+        'date_modified' => date('c', 1611041378),
+        'date_published' => date('c', 1610958578),
+      ],
+      [
+
+        'id' => '1003',
+        'title' => 'Drupal security update Test',
+        'url' => 'https://www.drupal.org/project/announce',
+        'content_html' => 'Test teaser 3',
+        '_drupalorg' => [
+          'featured' => TRUE,
+          'version' => '^10',
+        ],
+        'date_modified' => date('c', 1611041378),
+        'date_published' => date('c', 1610958578),
+      ],
+      [
+        'id' => '1004',
+        'title' => 'Drupal security update Test',
+        'url' => 'https://www.drupal.org/project/announce',
+        'content_html' => 'Test teaser 4',
+        '_drupalorg' => [
+          'featured' => TRUE,
+          'version' => '^10',
+        ],
+        'date_modified' => date('c', 1611041378),
+        'date_published' => date('c', 1610958578),
+      ],
+    ];
+  }
+
+  /**
+   * Data provider for testAllAnnouncements().
+   */
+  public function providerShowUpdatedAnnouncements(): array {
+    return [
+
+      [
+        'id' => '1005',
+        'title' => 'Drupal security update Test new',
+        'url' => 'https://www.drupal.org/project/announce',
+        'content_html' => 'Test teaser 1',
+        '_drupalorg' => [
+          'featured' => TRUE,
+          'version' => '^10',
+        ],
+        'date_modified' => date('c', 1611041378),
+        'date_published' => date('c', 1610958578),
+      ],
+      [
+        'id' => '1001',
+        'title' => 'Drupal security update Test',
+        'url' => 'https://www.drupal.org/project/announce',
+        'content_html' => 'Test teaser 1',
+        '_drupalorg' => [
+          'featured' => TRUE,
+          'version' => '^10',
+        ],
+        'date_modified' => date('c', 1611041378),
+        'date_published' => date('c', 1610958578),
+      ],
+      [
+        'id' => '1002',
+        'title' => 'Drupal security update Test',
+        'url' => 'https://www.drupal.org/project/announce',
+        'content_html' => 'Test teaser 2',
+        '_drupalorg' => [
+          'featured' => TRUE,
+          'version' => '^10',
+        ],
+        'date_modified' => date('c', 1611041378),
+        'date_published' => date('c', 1610958578),
+      ],
+      [
+
+        'id' => '1003',
+        'title' => 'Drupal security update Test',
+        'url' => 'https://www.drupal.org/project/announce',
+        'content_html' => 'Test teaser 3',
+        '_drupalorg' => [
+          'featured' => TRUE,
+          'version' => '^10',
+        ],
+        'date_modified' => date('c', 1611041378),
+        'date_published' => date('c', 1610958578),
+      ],
+      [
+        'id' => '1004',
+        'title' => 'Drupal security update Test',
+        'url' => 'https://www.drupal.org/project/announce',
+        'content_html' => 'Test teaser 4',
+        '_drupalorg' => [
+          'featured' => TRUE,
+          'version' => '^10',
+        ],
+        'date_modified' => date('c', 1611041378),
+        'date_published' => date('c', 1610958578),
+      ],
+    ];
+  }
+
+  /**
+   * Sets the feed items to be returned for the test.
+   *
+   * @param mixed[][] $feed_items
+   *   The feeds items to test. Every time the http_client makes a request the
+   *   next item in this array will be returned. For each feed item 'title' and
+   *   'url' are omitted because they do not need to vary between test cases.
+   */
+  protected function setFeedItems(array $feed_items): void {
+    $responses[] = new Response(200, [], json_encode(['items' => $feed_items]));
+    $responses[] = new Response(200, [], json_encode(['items' => $feed_items]));
+    $responses[] = new Response(200, [], json_encode(['items' => $feed_items]));
+
+    $this->setTestFeedResponses($responses);
+  }
+
+}
diff --git a/core/modules/announcements_feed/tests/src/Kernel/AnnounceTestBase.php b/core/modules/announcements_feed/tests/src/Kernel/AnnounceTestBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..77136f90cef69a91f04b048d80a4d091dc47e482
--- /dev/null
+++ b/core/modules/announcements_feed/tests/src/Kernel/AnnounceTestBase.php
@@ -0,0 +1,82 @@
+<?php
+
+namespace Drupal\Tests\announcements_feed\Kernel;
+
+use Drupal\KernelTests\KernelTestBase;
+use GuzzleHttp\Client;
+use GuzzleHttp\HandlerStack;
+use GuzzleHttp\Handler\MockHandler;
+use GuzzleHttp\Middleware;
+use GuzzleHttp\Psr7\Response;
+
+/**
+ * Base class for Announce Kernel tests.
+ */
+class AnnounceTestBase extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'user',
+    'system',
+    'announcements_feed',
+  ];
+
+  /**
+   * History of requests/responses.
+   *
+   * @var array
+   */
+  protected array $history = [];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+    $this->installConfig('system');
+    $this->installConfig(['user']);
+  }
+
+  /**
+   * Sets the feed items to be returned for the test.
+   *
+   * @param mixed[][] $feed_items
+   *   The feeds items to test. Every time the http_client makes a request the
+   *   next item in this array will be returned. For each feed item 'title' and
+   *   'url' are omitted because they do not need to vary between test cases.
+   */
+  protected function setFeedItems(array $feed_items): void {
+    $responses = [];
+    foreach ($feed_items as $feed_item) {
+      $feed_item += [
+        'title' => 'Drupal security update Test',
+        'url' => 'https://www.drupal.org/project/announce',
+      ];
+      $responses[] = new Response(200, [], json_encode(['items' => [$feed_item]]));
+    }
+    $this->setTestFeedResponses($responses);
+  }
+
+  /**
+   * Sets test feed responses.
+   *
+   * @param \GuzzleHttp\Psr7\Response[] $responses
+   *   The responses for the http_client service to return.
+   */
+  protected function setTestFeedResponses(array $responses): void {
+    // Create a mock and queue responses.
+    $mock = new MockHandler($responses);
+    $handler_stack = HandlerStack::create($mock);
+    $history = Middleware::history($this->history);
+    $handler_stack->push($history);
+    // Rebuild the container because the 'system.sa_fetcher' service and other
+    // services may already have an instantiated instance of the 'http_client'
+    // service without these changes.
+    $this->container->get('kernel')->rebuildContainer();
+    $this->container = $this->container->get('kernel')->getContainer();
+    $this->container->set('http_client', new Client(['handler' => $handler_stack]));
+  }
+
+}
diff --git a/core/modules/announcements_feed/tests/src/Unit/AnnounceFetcherUnitTest.php b/core/modules/announcements_feed/tests/src/Unit/AnnounceFetcherUnitTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..5177a0255dd7bede05034604b305c47513120853
--- /dev/null
+++ b/core/modules/announcements_feed/tests/src/Unit/AnnounceFetcherUnitTest.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace Drupal\Tests\announcements_feed\Unit;
+
+use Drupal\Tests\UnitTestCase;
+use Drupal\announcements_feed\AnnounceFetcher;
+
+/**
+ * Simple test to ensure that asserts pass.
+ *
+ * @group announcements_feed
+ */
+class AnnounceFetcherUnitTest extends UnitTestCase {
+
+  /**
+   * The Fetcher service object.
+   *
+   * @var \Drupal\announcements_feed\AnnounceFetcher
+   */
+  protected AnnounceFetcher $fetcher;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp():void {
+    parent::setUp();
+    $httpClient = $this->createMock('GuzzleHttp\ClientInterface');
+    $config = $this->getConfigFactoryStub([
+      'announcements_feed.settings' => [
+        'max_age' => 86400,
+        'cron_interval' => 21600,
+        'limit' => 10,
+      ],
+    ]);
+    $tempStore = $this->createMock('Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface');
+    $tempStore->expects($this->once())
+      ->method('get')
+      ->willReturn($this->createMock('Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface'));
+
+    $logger = $this->createMock('Psr\Log\LoggerInterface');
+    $this->fetcher = new AnnounceFetcher($httpClient, $config, $tempStore, $logger, 'https://www.drupal.org/announcements.json');
+  }
+
+  /**
+   * Test the ValidateUrl() method.
+   *
+   * @covers \Drupal\announcements_feed\AnnounceFetcher::validateUrl
+   *
+   * @dataProvider urlProvider
+   */
+  public function testValidateUrl($url, $isValid) {
+    $this->assertEquals($isValid, $this->fetcher->validateUrl($url));
+  }
+
+  /**
+   * Data for the testValidateUrl.
+   */
+  public function urlProvider(): array {
+    return [
+      ['https://www.drupal.org', TRUE],
+      ['https://drupal.org', TRUE],
+      ['https://api.drupal.org', TRUE],
+      ['https://a.drupal.org', TRUE],
+      ['https://123.drupal.org', TRUE],
+      ['https://api-new.drupal.org', TRUE],
+      ['https://api_new.drupal.org', TRUE],
+      ['https://api-.drupal.org', TRUE],
+      ['https://www.example.org', FALSE],
+      ['https://example.org', FALSE],
+      ['https://api.example.org/project/announce', FALSE],
+      ['https://-api.drupal.org', FALSE],
+      ['https://a.example.org/project/announce', FALSE],
+      ['https://test.drupaal.com', FALSE],
+      ['https://api.drupal.org.example.com', FALSE],
+      ['https://example.org/drupal.org', FALSE],
+    ];
+  }
+
+}
diff --git a/core/themes/stable9/css/announcements_feed/announcements_feed.dialog.css b/core/themes/stable9/css/announcements_feed/announcements_feed.dialog.css
new file mode 100644
index 0000000000000000000000000000000000000000..64a175aa61075d92e8a5fe5490cd1e5549e8289f
--- /dev/null
+++ b/core/themes/stable9/css/announcements_feed/announcements_feed.dialog.css
@@ -0,0 +1,31 @@
+#drupal-off-canvas-wrapper .announcements {
+  padding-block-start: var(--off-canvas-padding);
+}
+
+#drupal-off-canvas-wrapper .announcements ul {
+  margin: 0;
+  padding-inline-start: 0;
+  list-style: none;
+}
+
+#drupal-off-canvas-wrapper .announcement {
+  font-size: 0.875rem;
+}
+
+#drupal-off-canvas-wrapper .announcement--featured {
+  position: relative;
+  margin-inline: calc(-1 * var(--off-canvas-padding));
+  padding: 0 var(--off-canvas-padding) var(--off-canvas-padding);
+}
+
+#drupal-off-canvas-wrapper .announcement.announcement--featured + .announcement.announcement--standard {
+  border-block-start: 1px solid var(--off-canvas-border-color);
+}
+
+#drupal-off-canvas-wrapper .announcement--standard {
+  padding-block-start: var(--off-canvas-padding);
+}
+
+#drupal-off-canvas-wrapper .announcement__title {
+  font-size: 1rem;
+}
diff --git a/core/themes/stable9/css/announcements_feed/announcements_feed.page.css b/core/themes/stable9/css/announcements_feed/announcements_feed.page.css
new file mode 100644
index 0000000000000000000000000000000000000000..9781f9bdefed5519b9ba30ad94a33fb5d9896da5
--- /dev/null
+++ b/core/themes/stable9/css/announcements_feed/announcements_feed.page.css
@@ -0,0 +1,13 @@
+.announcements ul {
+  margin-inline-start: 0;
+  list-style: none;
+}
+
+.announcement:not(.announcement:last-child) {
+  margin-block-end: 1rem;
+}
+
+.announcement.announcement--featured + .announcement.announcement--standard {
+  padding-block-start: 1rem;
+  border-top: 1px solid #aaa;
+}
diff --git a/core/themes/stable9/css/announcements_feed/announcements_feed.toolbar.css b/core/themes/stable9/css/announcements_feed/announcements_feed.toolbar.css
new file mode 100644
index 0000000000000000000000000000000000000000..60e5a5c8924dec323c5289ca6e03033e0b34cdba
--- /dev/null
+++ b/core/themes/stable9/css/announcements_feed/announcements_feed.toolbar.css
@@ -0,0 +1,26 @@
+.toolbar .toolbar-icon.announce-canvas-link::before {
+  background-image: url("data:image/svg+xml,%3csvg width='20' height='19' viewBox='0 0 20 19' fill='none' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M5.73047 16.7648C6.00143 17.4831 6.6872 18 7.50009 18C8.31299 18 8.99876 17.4865 9.26972 16.7682C8.71107 16.8118 8.12231 16.8387 7.50009 16.8387C6.87788 16.8353 6.28912 16.8085 5.73047 16.7648Z' fill='white'/%3e%3cpath d='M14.331 13.4118H14.0801L12.4074 11.3979L11.5143 6.69897H11.5042C11.2333 5.05433 9.97881 3.74869 8.36976 3.39627C8.3731 3.38955 8.37979 3.38284 8.37979 3.37613L8.624 2.63772C8.74108 2.28529 8.53702 2 8.16905 2H6.83095C6.46298 2 6.25892 2.28529 6.37266 2.63772L6.61686 3.37613C6.62021 3.38284 6.62355 3.38955 6.6269 3.39627C5.01784 3.74869 3.76673 5.05433 3.49242 6.69897H3.48238L2.59255 11.3979L0.919938 13.4118H0.669046C0.30107 13.4118 0 13.7139 0 14.0831C0 14.4523 0.280999 14.8618 0.625558 14.996C0.625558 14.996 3.48573 16.0969 7.5 16.0969C11.5143 16.0969 14.3744 14.996 14.3744 14.996C14.719 14.8618 15 14.4523 15 14.0831C15 13.7139 14.6989 13.4118 14.331 13.4118ZM4.58296 6.95742L3.70317 11.8611L1.75624 14.0831H1.23439L3.21811 11.6933L4.15477 6.82652C4.28189 6.0579 4.68332 5.3799 5.24532 4.8798L5.49955 5.19866C5.03122 5.60478 4.68666 6.32305 4.58296 6.95742Z' fill='white'/%3e%3c/svg%3e");
+}
+
+@media (forced-colors: active) {
+
+  .toolbar .toolbar-icon.announce-canvas-link::before {
+    background: linktext;
+    -webkit-mask-image: url("data:image/svg+xml,%3csvg width='20' height='19' viewBox='0 0 20 19' fill='none' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M5.73047 16.7648C6.00143 17.4831 6.6872 18 7.50009 18C8.31299 18 8.99876 17.4865 9.26972 16.7682C8.71107 16.8118 8.12231 16.8387 7.50009 16.8387C6.87788 16.8353 6.28912 16.8085 5.73047 16.7648Z' fill='white'/%3e%3cpath d='M14.331 13.4118H14.0801L12.4074 11.3979L11.5143 6.69897H11.5042C11.2333 5.05433 9.97881 3.74869 8.36976 3.39627C8.3731 3.38955 8.37979 3.38284 8.37979 3.37613L8.624 2.63772C8.74108 2.28529 8.53702 2 8.16905 2H6.83095C6.46298 2 6.25892 2.28529 6.37266 2.63772L6.61686 3.37613C6.62021 3.38284 6.62355 3.38955 6.6269 3.39627C5.01784 3.74869 3.76673 5.05433 3.49242 6.69897H3.48238L2.59255 11.3979L0.919938 13.4118H0.669046C0.30107 13.4118 0 13.7139 0 14.0831C0 14.4523 0.280999 14.8618 0.625558 14.996C0.625558 14.996 3.48573 16.0969 7.5 16.0969C11.5143 16.0969 14.3744 14.996 14.3744 14.996C14.719 14.8618 15 14.4523 15 14.0831C15 13.7139 14.6989 13.4118 14.331 13.4118ZM4.58296 6.95742L3.70317 11.8611L1.75624 14.0831H1.23439L3.21811 11.6933L4.15477 6.82652C4.28189 6.0579 4.68332 5.3799 5.24532 4.8798L5.49955 5.19866C5.03122 5.60478 4.68666 6.32305 4.58296 6.95742Z' fill='white'/%3e%3c/svg%3e");
+    mask-image: url("data:image/svg+xml,%3csvg width='20' height='19' viewBox='0 0 20 19' fill='none' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M5.73047 16.7648C6.00143 17.4831 6.6872 18 7.50009 18C8.31299 18 8.99876 17.4865 9.26972 16.7682C8.71107 16.8118 8.12231 16.8387 7.50009 16.8387C6.87788 16.8353 6.28912 16.8085 5.73047 16.7648Z' fill='white'/%3e%3cpath d='M14.331 13.4118H14.0801L12.4074 11.3979L11.5143 6.69897H11.5042C11.2333 5.05433 9.97881 3.74869 8.36976 3.39627C8.3731 3.38955 8.37979 3.38284 8.37979 3.37613L8.624 2.63772C8.74108 2.28529 8.53702 2 8.16905 2H6.83095C6.46298 2 6.25892 2.28529 6.37266 2.63772L6.61686 3.37613C6.62021 3.38284 6.62355 3.38955 6.6269 3.39627C5.01784 3.74869 3.76673 5.05433 3.49242 6.69897H3.48238L2.59255 11.3979L0.919938 13.4118H0.669046C0.30107 13.4118 0 13.7139 0 14.0831C0 14.4523 0.280999 14.8618 0.625558 14.996C0.625558 14.996 3.48573 16.0969 7.5 16.0969C11.5143 16.0969 14.3744 14.996 14.3744 14.996C14.719 14.8618 15 14.4523 15 14.0831C15 13.7139 14.6989 13.4118 14.331 13.4118ZM4.58296 6.95742L3.70317 11.8611L1.75624 14.0831H1.23439L3.21811 11.6933L4.15477 6.82652C4.28189 6.0579 4.68332 5.3799 5.24532 4.8798L5.49955 5.19866C5.03122 5.60478 4.68666 6.32305 4.58296 6.95742Z' fill='white'/%3e%3c/svg%3e");
+    -webkit-mask-repeat: no-repeat;
+    mask-repeat: no-repeat;
+    -webkit-mask-position: center;
+    mask-position: center;
+  }
+}
+
+/* Pushes the tab to the opposite side of the page. */
+
+.toolbar .toolbar-bar .announce-toolbar-tab.toolbar-tab {
+  float: right; /* LTR */
+}
+
+[dir="rtl"] .toolbar .toolbar-bar .announce-toolbar-tab.toolbar-tab {
+  float: left;
+}
diff --git a/core/themes/stable9/stable9.info.yml b/core/themes/stable9/stable9.info.yml
index 970c1bb1e8aa53cd14a9e0fd3bc0fb82a15626de..beb3b0c17af772cc4bd39f4a230254c0b395cf87 100644
--- a/core/themes/stable9/stable9.info.yml
+++ b/core/themes/stable9/stable9.info.yml
@@ -11,6 +11,18 @@ libraries-override:
     css:
       theme:
         css/block.admin.css: css/block/block.admin.css
+  announcements_feed/drupal.announcements_feed.page:
+    css:
+      component:
+        css/announcements_feed.page.css: css/announcements_feed/announcements_feed.page.css
+  announcements_feed/drupal.announcements_feed.dialog:
+    css:
+      component:
+        css/announcements_feed.dialog.css: css/announcements_feed/announcements_feed.dialog.css
+  announcements_feed/drupal.announcements_feed.toolbar:
+    css:
+      component:
+        css/announcements_feed.toolbar.css: css/announcements_feed/announcements_feed.toolbar.css
 
   config_translation/drupal.config_translation.admin:
     css:
diff --git a/core/themes/stable9/templates/announcements_feed/announcements-feed-admin.html.twig b/core/themes/stable9/templates/announcements_feed/announcements-feed-admin.html.twig
new file mode 100644
index 0000000000000000000000000000000000000000..3ee6bb08f9f3eb4393cdec44274b5f4574ebb9d8
--- /dev/null
+++ b/core/themes/stable9/templates/announcements_feed/announcements-feed-admin.html.twig
@@ -0,0 +1,27 @@
+{#
+/**
+ * @file
+ * Template file for the theming of announcement_feed admin page.
+ *
+ * This template will get rendered when the user gets navigated to the announcements_feed.announcement route.
+ *
+ * Available variables:
+ * - count: Contains the total number of announcements.
+ * - featured: A list of featured announcement objects.
+ * - standard: A list of non-featured announcement objects.
+ *
+ * Announcement objects have the following variables:
+ * - id: Unique id of the announcement.
+ * - title: Title of the standard announcement.
+ * - content: Short description of the announcement.
+ * - datePublishedTimestamp: Timestamp of the announcement.
+ * - url: Learn more link of the standard announcement.
+ *
+ * @see announcements_feed_theme()
+ *
+ * @ingroup themeable
+ */
+#}
+{{ attach_library('announcements_feed/drupal.announcements_feed.page') }}
+
+{% include '@announcements_feed/announcements.html.twig' %}
diff --git a/core/themes/stable9/templates/announcements_feed/announcements-feed.html.twig b/core/themes/stable9/templates/announcements_feed/announcements-feed.html.twig
new file mode 100644
index 0000000000000000000000000000000000000000..6cad91d25fdb8353f1c54acc5b0109295cb9a172
--- /dev/null
+++ b/core/themes/stable9/templates/announcements_feed/announcements-feed.html.twig
@@ -0,0 +1,25 @@
+{#
+/**
+ * @file
+ * Template file for the theming of announcement_feed off-canvas dialog.
+ *
+ * This template will get rendered when the user clicks the announcement button in the toolbar.
+ *
+ * Available variables:
+ * - count: Contains the total number of announcements.
+ * - featured: A list of featured announcement objects.
+ * - standard: A list of non-featured announcement objects.
+ *
+ * Announcement objects have the following variables:
+ * - id: Unique id of the announcement.
+ * - title: Title of the standard announcement.
+ * - content: Short description of the announcement.
+ * - datePublishedTimestamp: Timestamp of the announcement.
+ * - url: Learn more link of the standard announcement.
+ *
+ * @see announcements_feed_theme()
+ *
+ * @ingroup themeable
+ */
+#}
+{% include '@announcements_feed/announcements.html.twig' %}