diff --git a/apps/web/src/app/[slug]/page.jsx b/apps/web/src/app/[slug]/page.jsx
index 0fd94f1..fbdb1c3 100644
--- a/apps/web/src/app/[slug]/page.jsx
+++ b/apps/web/src/app/[slug]/page.jsx
@@ -1,5 +1,6 @@
import { notFound } from 'next/navigation';
import { q, alerts, queue, authors as people } from '@rssamplifier/db';
+import { socialDisplayTitle, socialPathFor } from '@rssamplifier/social';
import { db, siteUrl } from '../../lib/db.js';
import { currentUser } from '../../lib/auth.js';
@@ -42,7 +43,7 @@ export async function generateMetadata({ params }) {
const feed = await q.feedBySlug(db(), slug);
if (!feed) return { title: 'Not found' };
- const title = String(feed.title);
+ const title = displayTitle(feed);
const description = feed.description
? String(feed.description)
: `Latest ${(CATEGORIES[String(feed.category)] ?? CATEGORIES.blog).item} from ${feed.title}.`;
@@ -232,7 +233,11 @@ export default async function FeedPage({ params }) {
{category.one[0].toUpperCase() + category.one.slice(1)}
- {feed.title}
+ {/* The canonical name where the stored title says nothing. Most of the
+ 50,026 imported subreddits have not been crawled yet and carry the
+ bare host as their title, so this heading read "reddit.com" on every
+ one of them. A real title always wins; see socialDisplayTitle. */}
+ {displayTitle(feed)}
{feed.description && {feed.description}
}
@@ -482,6 +487,22 @@ export default async function FeedPage({ params }) {
* @param {unknown} iso
* @returns {string}
*/
+/**
+ * What to call this feed on the page.
+ *
+ * Only ever different for a social row whose title was never crawled — every
+ * other feed, and every crawled social one, gets its own title back unchanged.
+ * `socialPathFor` supplies the fallback because the ref already encodes the
+ * canonical name: `/r/programming` becomes `r/programming`.
+ *
+ * @param {{ title?: unknown, feed_url?: unknown, social_ref?: unknown, slug?: unknown }} feed
+ * @returns {string}
+ */
+function displayTitle(feed) {
+ if (!feed?.social_ref) return String(feed?.title ?? '');
+ return socialDisplayTitle(feed, socialPathFor(feed).replace(/^\//, ''));
+}
+
function formatDate(iso) {
if (!iso) return 'undated';
const d = new Date(String(iso));
diff --git a/packages/db/src/queries.js b/packages/db/src/queries.js
index bc2f7d3..9c7a007 100644
--- a/packages/db/src/queries.js
+++ b/packages/db/src/queries.js
@@ -58,7 +58,8 @@ const FEED_COLS = `id, slug, feed_url, site_url, title, description, language, i
author, categories, kind, category, category_source, status, last_fetched_at, last_success_at, last_error, error_count,
fetch_interval_minutes, next_fetch_at, item_count, last_published_at, created_at, updated_at, source_kind,
card_url, card_width, card_height, card_type, authors_checked_at,
- http_etag, http_last_modified, content_hash, change_log`;
+ http_etag, http_last_modified, content_hash, change_log,
+ social_network, social_ref, social_config`;
/** The categories the directory is browsable by. */
export const KINDS = ['blog', 'news', 'podcast', 'music', 'video', 'comic', 'live', 'reel'];