diff --git a/src/components/announcement/announcement.js b/src/components/announcement/announcement.js
index 595f3b4e..9e043e48 100644
--- a/src/components/announcement/announcement.js
+++ b/src/components/announcement/announcement.js
@@ -6,6 +6,16 @@ const Announcement = {
   computed: {
     content () {
       return this.announcement.content
+    },
+    isRead () {
+      return this.announcement.read
+    }
+  },
+  methods: {
+    markAsRead () {
+      if (!this.isRead) {
+        return this.$store.dispatch('markAnnouncementAsRead', this.announcement.id)
+      }
     }
   }
 }
diff --git a/src/components/announcement/announcement.vue b/src/components/announcement/announcement.vue
index c31328ee..d8591468 100644
--- a/src/components/announcement/announcement.vue
+++ b/src/components/announcement/announcement.vue
@@ -1,6 +1,24 @@
 <template>
   <div class="announcement">
-    <rich-content :html="content" />
+    <div class="heading">
+      <h4>{{ $t('announcements.title') }}</h4>
+    </div>
+    <div class="body">
+      <rich-content
+        :html="content"
+        :emoji="announcement.emojis"
+        :handle-links="true"
+      />
+    </div>
+    <div class="footer">
+      <button
+        class="btn button-default"
+        :class="{ toggled: isRead }"
+        @click="markAsRead"
+      >
+        {{ $t('announcements.mark_as_read_action') }}
+      </button>
+    </div>
   </div>
 </template>
 
@@ -15,5 +33,9 @@
   border-bottom-color: var(--border, $fallback--border);
   border-radius: 0;
   padding: var(--status-margin, $status-margin);
+
+  .heading, .body {
+    margin-bottom: var(--status-margin, $status-margin);
+  }
 }
 </style>
diff --git a/src/components/announcements_page/announcements_page.js b/src/components/announcements_page/announcements_page.js
index 716468e6..a2a218fd 100644
--- a/src/components/announcements_page/announcements_page.js
+++ b/src/components/announcements_page/announcements_page.js
@@ -4,37 +4,12 @@ const AnnouncementsPage = {
   components: {
     Announcement
   },
+  mounted () {
+    this.$store.dispatch('fetchAnnouncements')
+  },
   computed: {
     announcements () {
-      return [{
-        "id": "8",
-        "content": "<p>Looks like there was an issue processing audio attachments without embedded art since yesterday due to an experimental new feature. That issue has now been fixed, so you may see older posts with audio from other servers pop up in your feeds now as they are being finally properly processed. Sorry!</p>",
-        "starts_at": null,
-        "ends_at": null,
-        "all_day": false,
-        "published_at": "2020-07-03T01:27:38.726Z",
-        "updated_at": "2020-07-03T01:27:38.752Z",
-        "read": true,
-        "mentions": [],
-        "statuses": [],
-        "tags": [],
-        "emojis": [],
-        "reactions": []
-      }, {
-        "id": "8",
-        "content": "<p>Looks like there was an issue processing audio attachments without embedded art since yesterday due to an experimental new feature. That issue has now been fixed, so you may see older posts with audio from other servers pop up in your feeds now as they are being finally properly processed. Sorry!</p>",
-        "starts_at": null,
-        "ends_at": null,
-        "all_day": false,
-        "published_at": "2020-07-03T01:27:38.726Z",
-        "updated_at": "2020-07-03T01:27:38.752Z",
-        "read": true,
-        "mentions": [],
-        "statuses": [],
-        "tags": [],
-        "emojis": [],
-        "reactions": []
-      }]
+      return this.$store.state.announcements.announcements
     }
   }
 }
diff --git a/src/main.js b/src/main.js
index eacd554c..94af4b9c 100644
--- a/src/main.js
+++ b/src/main.js
@@ -19,6 +19,7 @@ import reportsModule from './modules/reports.js'
 import pollsModule from './modules/polls.js'
 import postStatusModule from './modules/postStatus.js'
 import chatsModule from './modules/chats.js'
+import announcementsModule from './modules/announcements.js'
 
 import { createI18n } from 'vue-i18n'
 
@@ -81,7 +82,8 @@ const persistedStateOptions = {
       reports: reportsModule,
       polls: pollsModule,
       postStatus: postStatusModule,
-      chats: chatsModule
+      chats: chatsModule,
+      announcements: announcementsModule
     },
     plugins,
     strict: false // Socket modifies itself, let's ignore this for now.
diff --git a/src/modules/announcements.js b/src/modules/announcements.js
new file mode 100644
index 00000000..2ab2600c
--- /dev/null
+++ b/src/modules/announcements.js
@@ -0,0 +1,60 @@
+import { set } from 'vue'
+
+const FETCH_ANNOUNCEMENT_INTERVAL_MS = 1000 * 60 * 5
+
+export const defaultState = {
+  announcements: [],
+  fetchAnnouncementsTimer: undefined
+}
+
+export const mutations = {
+  setAnnouncements (state, announcements) {
+    set(state, 'announcements', announcements)
+  },
+  setAnnouncementRead (state, { id, read }) {
+    if (!state.announcements[id]) {
+      return
+    }
+
+    set(state.announcements[id], 'read', read)
+  },
+  setFetchAnnouncementsTimer (state, timer) {
+    set(state, 'fetchAnnouncementsTimer', announcements)
+  }
+}
+
+const announcements = {
+  state: defaultState,
+  mutations,
+  actions: {
+    fetchAnnouncements (store) {
+      return store.rootState.api.backendInteractor.fetchAnnouncements()
+        .then(announcements => {
+          store.commit('setAnnouncements', announcements)
+        })
+    },
+    markAnnouncementAsRead (store, id) {
+      return store.rootState.api.backendInteractor.dismissAnnouncement({ id })
+        .then(() => {
+          store.commit('setAnnouncementRead', { id, read: true })
+        })
+    },
+    startFetchingAnnouncements (store) {
+      if (store.state.fetchAnnouncementsTimer) {
+        return
+      }
+
+      const interval = setInterval(() => store.dispatch('fetchAnnouncements'), FETCH_ANNOUNCEMENT_INTERVAL_MS)
+      store.commit('setFetchAnnouncementsTimer', interval)
+
+      return store.dispatch('fetchAnnouncements')
+    },
+    stopFetchingAnnouncements (store) {
+      const interval = store.state.fetchAnnouncementsTimer
+      store.commit('setFetchAnnouncementsTimer', undefined)
+      clearInterval(interval)
+    }
+  }
+}
+
+export default announcements
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 436b8b0a..78a7276a 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -79,6 +79,8 @@ const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search'
 const MASTODON_DOMAIN_BLOCKS_URL = '/api/v1/domain_blocks'
 const MASTODON_STREAMING = '/api/v1/streaming'
 const MASTODON_KNOWN_DOMAIN_LIST_URL = '/api/v1/instance/peers'
+const MASTODON_ANNOUNCEMENTS_URL = '/api/v1/announcements'
+const MASTODON_ANNOUNCEMENTS_DISMISS_URL = id => `/api/v1/announcements/${id}/dismiss`
 const PLEROMA_EMOJI_REACTIONS_URL = id => `/api/v1/pleroma/statuses/${id}/reactions`
 const PLEROMA_EMOJI_REACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
 const PLEROMA_EMOJI_UNREACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
@@ -1081,6 +1083,18 @@ const dismissNotification = ({ credentials, id }) => {
   })
 }
 
+const fetchAnnouncements = ({ credentials }) => {
+  return promisedRequest({ url: MASTODON_ANNOUNCEMENTS_URL, credentials })
+}
+
+const dismissAnnouncement = ({ id, credentials }) => {
+  return promisedRequest({
+    url: MASTODON_ANNOUNCEMENTS_DISMISS_URL(id),
+    credentials,
+    method: 'POST'
+  })
+}
+
 export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => {
   return Object.entries({
     ...(credentials
@@ -1351,7 +1365,9 @@ const apiService = {
   chatMessages,
   sendChatMessage,
   readChat,
-  deleteChatMessage
+  deleteChatMessage,
+  fetchAnnouncements,
+  dismissAnnouncement
 }
 
 export default apiService