diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js
index 4ca3c3a15..f0ea46118 100644
--- a/app/javascript/mastodon/actions/timelines.js
+++ b/app/javascript/mastodon/actions/timelines.js
@@ -158,6 +158,7 @@ export const expandAccountTimeline         = (accountId, { maxId, withReplies, t
 export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged });
 export const expandAccountMediaTimeline    = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 });
 export const expandListTimeline            = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done);
+export const expandLinkTimeline            = (url, { maxId } = {}, done = noOp) => expandTimeline(`link:${url}`, `/api/v1/timelines/link`, { url, max_id: maxId }, done);
 export const expandHashtagTimeline         = (hashtag, { maxId, tags, local } = {}, done = noOp) => {
   return expandTimeline(`hashtag:${hashtag}${local ? ':local' : ''}`, `/api/v1/timelines/tag/${hashtag}`, {
     max_id: maxId,
diff --git a/app/javascript/mastodon/api_types/statuses.ts b/app/javascript/mastodon/api_types/statuses.ts
index db4e20506..a934faeb7 100644
--- a/app/javascript/mastodon/api_types/statuses.ts
+++ b/app/javascript/mastodon/api_types/statuses.ts
@@ -44,6 +44,7 @@ export interface ApiPreviewCardJSON {
   type: string;
   author_name: string;
   author_url: string;
+  author_account?: ApiAccountJSON;
   provider_name: string;
   provider_url: string;
   html: string;
diff --git a/app/javascript/mastodon/components/status_list.jsx b/app/javascript/mastodon/components/status_list.jsx
index 3ed20f65e..fee6675fa 100644
--- a/app/javascript/mastodon/components/status_list.jsx
+++ b/app/javascript/mastodon/components/status_list.jsx
@@ -33,6 +33,7 @@ export default class StatusList extends ImmutablePureComponent {
     withCounters: PropTypes.bool,
     timelineId: PropTypes.string,
     lastId: PropTypes.string,
+    bindToDocument: PropTypes.bool,
   };
 
   static defaultProps = {
diff --git a/app/javascript/mastodon/features/explore/components/story.jsx b/app/javascript/mastodon/features/explore/components/story.jsx
index a2cae942d..125df412a 100644
--- a/app/javascript/mastodon/features/explore/components/story.jsx
+++ b/app/javascript/mastodon/features/explore/components/story.jsx
@@ -4,6 +4,8 @@ import { useState, useCallback } from 'react';
 import { FormattedMessage } from 'react-intl';
 
 import classNames from 'classnames';
+import { Link } from 'react-router-dom';
+
 
 
 import { Blurhash } from 'mastodon/components/blurhash';
@@ -57,7 +59,7 @@ export const Story = ({
 
         <div className='story__details__shared'>
           {author ? <FormattedMessage id='link_preview.author' className='story__details__shared__author' defaultMessage='By {name}' values={{ name: authorAccount ? <AuthorLink accountId={authorAccount} /> : <strong>{author}</strong> }} /> : <span />}
-          {typeof sharedTimes === 'number' ? <span className='story__details__shared__pill'><ShortNumber value={sharedTimes} renderer={sharesCountRenderer} /></span> : <Skeleton width='10ch' />}
+          {typeof sharedTimes === 'number' ? <Link className='story__details__shared__pill' to={`/links/${encodeURIComponent(url)}`}><ShortNumber value={sharedTimes} renderer={sharesCountRenderer} /></Link> : <Skeleton width='10ch' />}
         </div>
       </div>
 
diff --git a/app/javascript/mastodon/features/link_timeline/index.tsx b/app/javascript/mastodon/features/link_timeline/index.tsx
new file mode 100644
index 000000000..dd726dac1
--- /dev/null
+++ b/app/javascript/mastodon/features/link_timeline/index.tsx
@@ -0,0 +1,76 @@
+import { useRef, useEffect, useCallback } from 'react';
+
+import { Helmet } from 'react-helmet';
+import { useParams } from 'react-router-dom';
+
+import ExploreIcon from '@/material-icons/400-24px/explore.svg?react';
+import { expandLinkTimeline } from 'mastodon/actions/timelines';
+import Column from 'mastodon/components/column';
+import { ColumnHeader } from 'mastodon/components/column_header';
+import StatusListContainer from 'mastodon/features/ui/containers/status_list_container';
+import type { Card } from 'mastodon/models/status';
+import { useAppDispatch, useAppSelector } from 'mastodon/store';
+
+export const LinkTimeline: React.FC<{
+  multiColumn: boolean;
+}> = ({ multiColumn }) => {
+  const { url } = useParams<{ url: string }>();
+  const decodedUrl = url ? decodeURIComponent(url) : undefined;
+  const dispatch = useAppDispatch();
+  const columnRef = useRef<Column>(null);
+  const firstStatusId = useAppSelector((state) =>
+    decodedUrl
+      ? // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
+        (state.timelines.getIn([`link:${decodedUrl}`, 'items', 0]) as string)
+      : undefined,
+  );
+  const story = useAppSelector((state) =>
+    firstStatusId
+      ? (state.statuses.getIn([firstStatusId, 'card']) as Card)
+      : undefined,
+  );
+
+  const handleHeaderClick = useCallback(() => {
+    columnRef.current?.scrollTop();
+  }, []);
+
+  const handleLoadMore = useCallback(
+    (maxId: string) => {
+      dispatch(expandLinkTimeline(decodedUrl, { maxId }));
+    },
+    [dispatch, decodedUrl],
+  );
+
+  useEffect(() => {
+    dispatch(expandLinkTimeline(decodedUrl));
+  }, [dispatch, decodedUrl]);
+
+  return (
+    <Column bindToDocument={!multiColumn} ref={columnRef} label={story?.title}>
+      <ColumnHeader
+        icon='explore'
+        iconComponent={ExploreIcon}
+        title={story?.title}
+        onClick={handleHeaderClick}
+        multiColumn={multiColumn}
+        showBackButton
+      />
+
+      <StatusListContainer
+        timelineId={`link:${decodedUrl}`}
+        onLoadMore={handleLoadMore}
+        trackScroll
+        scrollKey={`link_timeline-${decodedUrl}`}
+        bindToDocument={!multiColumn}
+      />
+
+      <Helmet>
+        <title>{story?.title}</title>
+        <meta name='robots' content='noindex' />
+      </Helmet>
+    </Column>
+  );
+};
+
+// eslint-disable-next-line import/no-default-export
+export default LinkTimeline;
diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx
index b58e191ed..d41132f9c 100644
--- a/app/javascript/mastodon/features/ui/index.jsx
+++ b/app/javascript/mastodon/features/ui/index.jsx
@@ -56,6 +56,7 @@ import {
   FavouritedStatuses,
   BookmarkedStatuses,
   FollowedTags,
+  LinkTimeline,
   ListTimeline,
   Blocks,
   DomainBlocks,
@@ -202,6 +203,7 @@ class SwitchingColumnsArea extends PureComponent {
             <WrappedRoute path='/public/remote' exact component={Firehose} componentParams={{ feedType: 'public:remote' }} content={children} />
             <WrappedRoute path={['/conversations', '/timelines/direct']} component={DirectTimeline} content={children} />
             <WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
+            <WrappedRoute path='/links/:url' component={LinkTimeline} content={children} />
             <WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />
             <WrappedRoute path='/notifications' component={Notifications} content={children} exact />
             <WrappedRoute path='/notifications/requests' component={NotificationRequests} content={children} exact />
diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js
index e1f5bfdaf..b8a2359d9 100644
--- a/app/javascript/mastodon/features/ui/util/async-components.js
+++ b/app/javascript/mastodon/features/ui/util/async-components.js
@@ -201,3 +201,7 @@ export function NotificationRequests () {
 export function NotificationRequest () {
   return import(/*webpackChunkName: "features/notifications/request" */'../../notifications/request');
 }
+
+export function LinkTimeline () {
+  return import(/*webpackChunkName: "features/link_timeline" */'../../link_timeline');
+}
diff --git a/app/javascript/mastodon/models/status.ts b/app/javascript/mastodon/models/status.ts
index 7907fc34f..3900df4e3 100644
--- a/app/javascript/mastodon/models/status.ts
+++ b/app/javascript/mastodon/models/status.ts
@@ -1,4 +1,12 @@
+import type { RecordOf } from 'immutable';
+
+import type { ApiPreviewCardJSON } from 'mastodon/api_types/statuses';
+
 export type { StatusVisibility } from 'mastodon/api_types/statuses';
 
 // Temporary until we type it correctly
 export type Status = Immutable.Map<string, unknown>;
+
+type CardShape = Required<ApiPreviewCardJSON>;
+
+export type Card = RecordOf<CardShape>;
diff --git a/config/routes.rb b/config/routes.rb
index f4662dd5d..4b3bd4f18 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -27,6 +27,7 @@ Rails.application.routes.draw do
     /public/remote
     /conversations
     /lists/(*any)
+    /links/(*any)
     /notifications/(*any)
     /favourites
     /bookmarks