Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
04c45a4
feat: unified tag system with Medium-style UX and admin dashboard
NiallJoeMaher Jan 10, 2026
ce6e404
feat: enhance admin dashboard with new stats and logo upload function…
NiallJoeMaher Jan 10, 2026
eef9ff2
fix: restore notifications functionality and update card styling
NiallJoeMaher Jan 20, 2026
d072cb9
feat: add JSON-LD structured data for SEO and improve admin sources UI
NiallJoeMaher Jan 27, 2026
56af4d7
Refactor code for improved readability and consistency
NiallJoeMaher Jan 28, 2026
28297b3
chore: remove outdated development test suite documentation
NiallJoeMaher Jan 28, 2026
f500011
fix: update notification migration to convert column types before add…
NiallJoeMaher Jan 28, 2026
9c40ea3
test: enhance e2e tests with load state waits and improve visibility …
NiallJoeMaher Jan 28, 2026
5a911ec
test: enhance e2e tests by waiting for TRPC responses to improve reli…
NiallJoeMaher Jan 29, 2026
b508792
refactor: streamline error handling and improve test reliability acro…
NiallJoeMaher Jan 30, 2026
1a4e58a
test: improve notification and article tests by enhancing reliability…
NiallJoeMaher Jan 30, 2026
6fb8c7a
test: enhance reliability of article bookmarking and loading tests by…
NiallJoeMaher Jan 30, 2026
c4fb779
refactor: update load state waits from "networkidle" to "domcontentlo…
NiallJoeMaher Jan 30, 2026
4742a4e
Fix E2E test failures and hide banned users' posts from feeds
NiallJoeMaher Mar 9, 2026
906afc3
Fix Prettier formatting in E2E test files
NiallJoeMaher Mar 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 0 additions & 52 deletions .claude/commands/do-test.md

This file was deleted.

3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
"typescript.tsdk": "node_modules/typescript/lib",
"snyk.advanced.autoSelectOrganization": true
}
14 changes: 0 additions & 14 deletions app/(app)/[username]/[slug]/_userLinkDetail.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,19 +21,6 @@ type Props = {
contentSlug: string;
};

// Get favicon URL from a website
const getFaviconUrl = (
websiteUrl: string | null | undefined,
): string | null => {
if (!websiteUrl) return null;
try {
const url = new URL(websiteUrl);
return `https://www.google.com/s2/favicons?domain=${url.hostname}&sz=32`;
} catch {
return null;
}
};

// Get hostname from URL
const getHostname = (urlString: string): string => {
try {
Expand DownExpand Up@@ -189,7 +176,6 @@ const UserLinkDetail = ({ username, contentSlug }: Props) => {
})
: null;

const faviconUrl = getFaviconUrl(externalUrl);
const hostname = externalUrl ? getHostname(externalUrl) : null;
const score = votes.upvotes - votes.downvotes;

Expand Down
140 changes: 136 additions & 4 deletions app/(app)/[username]/[slug]/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,12 @@ import { eq, and, lte } from "drizzle-orm";
import FeedArticleContent from "./_feedArticleContent";
import LinkContentDetail from "./_linkContentDetail";
import UserLinkDetail from "./_userLinkDetail";
import { JsonLd } from "@/components/JsonLd";
import {
getArticleSchema,
getBreadcrumbSchema,
getNewsArticleSchema,
} from "@/lib/structured-data";

type Props = { params: Promise<{ username: string; slug: string }> };

Expand DownExpand Up@@ -457,8 +463,40 @@ const UnifiedPostPage = async (props: Props) => {
}) as unknown as string;
}

// Prepare JSON-LD structured data
const articleSchema = getArticleSchema({
title: userPost.title,
excerpt: userPost.excerpt,
slug: userPost.slug,
publishedAt: userPost.published,
updatedAt: userPost.updatedAt,
readingTime: userPost.readTimeMins,
canonicalUrl: userPost.canonicalUrl,
tags: userPost.tags.map((t) => ({ title: t.tag.title })),
author: {
name: userPost.user.name,
username: userPost.user.username,
image: userPost.user.image,
bio: userPost.user.bio,
},
});

const breadcrumbSchema = getBreadcrumbSchema([
{ name: "Home", url: "https://www.codu.co" },
{ name: "Feed", url: "https://www.codu.co/feed" },
{
name: userPost.user.name || "Author",
url: `https://www.codu.co/${userPost.user.username}`,
},
{ name: userPost.title },
]);

return (
<>
{/* JSON-LD Structured Data for SEO */}
<JsonLd data={articleSchema} />
<JsonLd data={breadcrumbSchema} />

<div className="mx-auto max-w-3xl px-4 py-8">
{/* Breadcrumb navigation */}
<nav className="mb-6 flex items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400">
Expand DownExpand Up@@ -618,8 +656,40 @@ const UnifiedPostPage = async (props: Props) => {
}) as unknown as string;
}

// Prepare JSON-LD structured data
const articleSchema = getArticleSchema({
title: userArticle.title,
excerpt: userArticle.excerpt,
slug: userArticle.slug,
publishedAt: userArticle.publishedAt,
updatedAt: userArticle.updatedAt,
readingTime: userArticle.readTimeMins,
canonicalUrl: userArticle.canonicalUrl,
tags: userArticle.tags?.map((t) => ({ title: t.tag.title })),
author: {
name: userArticle.user.name,
username: userArticle.user.username,
image: userArticle.user.image,
bio: userArticle.user.bio,
},
});

const breadcrumbSchema = getBreadcrumbSchema([
{ name: "Home", url: "https://www.codu.co" },
{ name: "Feed", url: "https://www.codu.co/feed" },
{
name: userArticle.user.name || "Author",
url: `https://www.codu.co/${userArticle.user.username}`,
},
{ name: userArticle.title },
]);

return (
<>
{/* JSON-LD Structured Data for SEO */}
<JsonLd data={articleSchema} />
<JsonLd data={breadcrumbSchema} />

<div className="mx-auto max-w-3xl px-4 py-8">
{/* Breadcrumb navigation */}
<nav className="mb-6 flex items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400">
Expand DownExpand Up@@ -773,16 +843,78 @@ const UnifiedPostPage = async (props: Props) => {
const feedArticle = await getFeedArticle(username, slug);

if (feedArticle) {
// Render feed article
return <FeedArticleContent sourceSlug={username} articleSlug={slug} />;
// Prepare JSON-LD structured data for feed article
const newsArticleSchema = getNewsArticleSchema({
title: feedArticle.title,
excerpt: feedArticle.excerpt,
slug: feedArticle.slug,
externalUrl: feedArticle.externalUrl || "",
coverImage: feedArticle.imageUrl || feedArticle.ogImageUrl,
publishedAt: feedArticle.publishedAt,
source: {
name: feedArticle.source?.name || null,
slug: feedArticle.source?.slug || username,
logoUrl: feedArticle.source?.logoUrl,
},
});

const breadcrumbSchema = getBreadcrumbSchema([
{ name: "Home", url: "https://www.codu.co" },
{ name: "Feed", url: "https://www.codu.co/feed" },
{
name: feedArticle.source?.name || username,
url: `https://www.codu.co/${feedArticle.source?.slug || username}`,
},
{ name: feedArticle.title },
]);

// Render feed article with JSON-LD
return (
<>
<JsonLd data={newsArticleSchema} />
<JsonLd data={breadcrumbSchema} />
<FeedArticleContent sourceSlug={username} articleSlug={slug} />
</>
);
}

// Try unified content table (new LINK type items)
const linkContent = await getLinkContent(username, slug);

if (linkContent) {
// Render link content
return <LinkContentDetail sourceSlug={username} contentSlug={slug} />;
// Prepare JSON-LD structured data for link content
const newsArticleSchema = getNewsArticleSchema({
title: linkContent.title,
excerpt: linkContent.excerpt,
slug: linkContent.slug,
externalUrl: linkContent.externalUrl || "",
coverImage: linkContent.imageUrl || linkContent.ogImageUrl,
publishedAt: linkContent.publishedAt,
source: {
name: linkContent.source?.name || null,
slug: linkContent.source?.slug || username,
logoUrl: linkContent.source?.logoUrl,
},
});

const breadcrumbSchema = getBreadcrumbSchema([
{ name: "Home", url: "https://www.codu.co" },
{ name: "Feed", url: "https://www.codu.co/feed" },
{
name: linkContent.source?.name || username,
url: `https://www.codu.co/${linkContent.source?.slug || username}`,
},
{ name: linkContent.title },
]);

// Render link content with JSON-LD
return (
<>
<JsonLd data={newsArticleSchema} />
<JsonLd data={breadcrumbSchema} />
<LinkContentDetail sourceSlug={username} contentSlug={slug} />
</>
);
}

// Nothing found
Expand Down
14 changes: 14 additions & 0 deletions app/(app)/[username]/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,8 @@ import { type Metadata } from "next";
import { db } from "@/server/db";
import { feed_sources } from "@/server/db/schema";
import { eq } from "drizzle-orm";
import { JsonLd } from "@/components/JsonLd";
import { getPersonSchema } from "@/lib/structured-data";

type Props = { params: Promise<{ username: string }> };

Expand DownExpand Up@@ -133,8 +135,20 @@ export default async function Page(props: {
accountLocked,
};

// Prepare Person JSON-LD for SEO
const personSchema = getPersonSchema({
name: shapedProfile.name,
username: shapedProfile.username,
image: shapedProfile.image,
bio: shapedProfile.bio,
websiteUrl: shapedProfile.websiteUrl,
});

return (
<>
{/* Person JSON-LD for profile SEO */}
<JsonLd data={personSchema} />

<h1 className="sr-only">{`${shapedProfile.name || shapedProfile.username}'s Coding Profile`}</h1>
<Content profile={shapedProfile} isOwner={isOwner} session={session} />
</>
Expand Down
32 changes: 24 additions & 8 deletions app/(app)/admin/_client.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,7 @@ import {
FlagIcon,
RssIcon,
ShieldExclamationIcon,
NewspaperIcon,
TagIcon,
} from "@heroicons/react/24/outline";
import { api } from "@/server/trpc/react";

Expand DownExpand Up@@ -99,13 +99,6 @@ const AdminDashboard = () => {
color="green"
isLoading={isLoading}
/>
<StatCard
title="Aggregated Articles"
value={stats?.aggregatedArticles}
icon={NewspaperIcon}
color="purple"
isLoading={isLoading}
/>
<StatCard
title="Active Feed Sources"
value={stats?.activeFeedSources}
Expand All@@ -114,6 +107,14 @@ const AdminDashboard = () => {
href="/admin/sources"
isLoading={isLoading}
/>
<StatCard
title="Total Reports"
value={reportCounts?.total}
icon={FlagIcon}
color="purple"
href="/admin/moderation"
isLoading={isLoading}
/>
</div>

{/* Moderation Stats */}
Expand DownExpand Up@@ -205,6 +206,21 @@ const AdminDashboard = () => {
</p>
</div>
</Link>

<Link
href="/admin/tags"
className="flex items-center gap-3 rounded-lg border border-neutral-200 bg-white p-4 transition-colors hover:border-green-300 hover:bg-green-50 dark:border-neutral-700 dark:bg-neutral-800 dark:hover:border-green-700 dark:hover:bg-green-900/20"
>
<TagIcon className="h-6 w-6 text-green-500" />
<div>
<p className="font-medium text-neutral-900 dark:text-white">
Tag Management
</p>
<p className="text-sm text-neutral-500 dark:text-neutral-400">
Merge, curate, and manage tags
</p>
</div>
</Link>
</div>
</div>
</div>
Expand Down
1 change: 0 additions & 1 deletion app/(app)/admin/moderation/_client.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,6 @@ import { useState } from "react";
import Link from "next/link";
import {
FlagIcon,
CheckCircleIcon,
XCircleIcon,
ExclamationTriangleIcon,
ArrowLeftIcon,
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
04c45a4
feat: unified tag system with Medium-style UX and admin dashboard
NiallJoeMaher Jan 10, 2026
ce6e404
feat: enhance admin dashboard with new stats and logo upload function…
NiallJoeMaher Jan 10, 2026
eef9ff2
fix: restore notifications functionality and update card styling
NiallJoeMaher Jan 20, 2026
d072cb9
feat: add JSON-LD structured data for SEO and improve admin sources UI
NiallJoeMaher Jan 27, 2026
56af4d7
Refactor code for improved readability and consistency
NiallJoeMaher Jan 28, 2026
28297b3
chore: remove outdated development test suite documentation
NiallJoeMaher Jan 28, 2026
f500011
fix: update notification migration to convert column types before add…
NiallJoeMaher Jan 28, 2026
9c40ea3
test: enhance e2e tests with load state waits and improve visibility …
NiallJoeMaher Jan 28, 2026
5a911ec
test: enhance e2e tests by waiting for TRPC responses to improve reli…
NiallJoeMaher Jan 29, 2026
b508792
refactor: streamline error handling and improve test reliability acro…
NiallJoeMaher Jan 30, 2026
1a4e58a
test: improve notification and article tests by enhancing reliability…
NiallJoeMaher Jan 30, 2026
6fb8c7a
test: enhance reliability of article bookmarking and loading tests by…
NiallJoeMaher Jan 30, 2026
c4fb779
refactor: update load state waits from "networkidle" to "domcontentlo…
NiallJoeMaher Jan 30, 2026
4742a4e
Fix E2E test failures and hide banned users' posts from feeds
NiallJoeMaher Mar 9, 2026
906afc3
Fix Prettier formatting in E2E test files
NiallJoeMaher Mar 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 0 additions & 52 deletions .claude/commands/do-test.md

This file was deleted.

3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
"typescript.tsdk": "node_modules/typescript/lib",
"snyk.advanced.autoSelectOrganization": true
}
14 changes: 0 additions & 14 deletions app/(app)/[username]/[slug]/_userLinkDetail.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,19 +21,6 @@ type Props = {
contentSlug: string;
};

// Get favicon URL from a website
const getFaviconUrl = (
websiteUrl: string | null | undefined,
): string | null => {
if (!websiteUrl) return null;
try {
const url = new URL(websiteUrl);
return `https://www.google.com/s2/favicons?domain=${url.hostname}&sz=32`;
} catch {
return null;
}
};

// Get hostname from URL
const getHostname = (urlString: string): string => {
try {
Expand DownExpand Up@@ -189,7 +176,6 @@ const UserLinkDetail = ({ username, contentSlug }: Props) => {
})
: null;

const faviconUrl = getFaviconUrl(externalUrl);
const hostname = externalUrl ? getHostname(externalUrl) : null;
const score = votes.upvotes - votes.downvotes;

Expand Down
140 changes: 136 additions & 4 deletions app/(app)/[username]/[slug]/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,12 @@ import { eq, and, lte } from "drizzle-orm";
import FeedArticleContent from "./_feedArticleContent";
import LinkContentDetail from "./_linkContentDetail";
import UserLinkDetail from "./_userLinkDetail";
import { JsonLd } from "@/components/JsonLd";
import {
getArticleSchema,
getBreadcrumbSchema,
getNewsArticleSchema,
} from "@/lib/structured-data";

type Props = { params: Promise<{ username: string; slug: string }> };

Expand DownExpand Up@@ -457,8 +463,40 @@ const UnifiedPostPage = async (props: Props) => {
}) as unknown as string;
}

// Prepare JSON-LD structured data
const articleSchema = getArticleSchema({
title: userPost.title,
excerpt: userPost.excerpt,
slug: userPost.slug,
publishedAt: userPost.published,
updatedAt: userPost.updatedAt,
readingTime: userPost.readTimeMins,
canonicalUrl: userPost.canonicalUrl,
tags: userPost.tags.map((t) => ({ title: t.tag.title })),
author: {
name: userPost.user.name,
username: userPost.user.username,
image: userPost.user.image,
bio: userPost.user.bio,
},
});

const breadcrumbSchema = getBreadcrumbSchema([
{ name: "Home", url: "https://www.codu.co" },
{ name: "Feed", url: "https://www.codu.co/feed" },
{
name: userPost.user.name || "Author",
url: `https://www.codu.co/${userPost.user.username}`,
},
{ name: userPost.title },
]);

return (
<>
{/* JSON-LD Structured Data for SEO */}
<JsonLd data={articleSchema} />
<JsonLd data={breadcrumbSchema} />

<div className="mx-auto max-w-3xl px-4 py-8">
{/* Breadcrumb navigation */}
<nav className="mb-6 flex items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400">
Expand DownExpand Up@@ -618,8 +656,40 @@ const UnifiedPostPage = async (props: Props) => {
}) as unknown as string;
}

// Prepare JSON-LD structured data
const articleSchema = getArticleSchema({
title: userArticle.title,
excerpt: userArticle.excerpt,
slug: userArticle.slug,
publishedAt: userArticle.publishedAt,
updatedAt: userArticle.updatedAt,
readingTime: userArticle.readTimeMins,
canonicalUrl: userArticle.canonicalUrl,
tags: userArticle.tags?.map((t) => ({ title: t.tag.title })),
author: {
name: userArticle.user.name,
username: userArticle.user.username,
image: userArticle.user.image,
bio: userArticle.user.bio,
},
});

const breadcrumbSchema = getBreadcrumbSchema([
{ name: "Home", url: "https://www.codu.co" },
{ name: "Feed", url: "https://www.codu.co/feed" },
{
name: userArticle.user.name || "Author",
url: `https://www.codu.co/${userArticle.user.username}`,
},
{ name: userArticle.title },
]);

return (
<>
{/* JSON-LD Structured Data for SEO */}
<JsonLd data={articleSchema} />
<JsonLd data={breadcrumbSchema} />

<div className="mx-auto max-w-3xl px-4 py-8">
{/* Breadcrumb navigation */}
<nav className="mb-6 flex items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400">
Expand DownExpand Up@@ -773,16 +843,78 @@ const UnifiedPostPage = async (props: Props) => {
const feedArticle = await getFeedArticle(username, slug);

if (feedArticle) {
// Render feed article
return <FeedArticleContent sourceSlug={username} articleSlug={slug} />;
// Prepare JSON-LD structured data for feed article
const newsArticleSchema = getNewsArticleSchema({
title: feedArticle.title,
excerpt: feedArticle.excerpt,
slug: feedArticle.slug,
externalUrl: feedArticle.externalUrl || "",
coverImage: feedArticle.imageUrl || feedArticle.ogImageUrl,
publishedAt: feedArticle.publishedAt,
source: {
name: feedArticle.source?.name || null,
slug: feedArticle.source?.slug || username,
logoUrl: feedArticle.source?.logoUrl,
},
});

const breadcrumbSchema = getBreadcrumbSchema([
{ name: "Home", url: "https://www.codu.co" },
{ name: "Feed", url: "https://www.codu.co/feed" },
{
name: feedArticle.source?.name || username,
url: `https://www.codu.co/${feedArticle.source?.slug || username}`,
},
{ name: feedArticle.title },
]);

// Render feed article with JSON-LD
return (
<>
<JsonLd data={newsArticleSchema} />
<JsonLd data={breadcrumbSchema} />
<FeedArticleContent sourceSlug={username} articleSlug={slug} />
</>
);
}

// Try unified content table (new LINK type items)
const linkContent = await getLinkContent(username, slug);

if (linkContent) {
// Render link content
return <LinkContentDetail sourceSlug={username} contentSlug={slug} />;
// Prepare JSON-LD structured data for link content
const newsArticleSchema = getNewsArticleSchema({
title: linkContent.title,
excerpt: linkContent.excerpt,
slug: linkContent.slug,
externalUrl: linkContent.externalUrl || "",
coverImage: linkContent.imageUrl || linkContent.ogImageUrl,
publishedAt: linkContent.publishedAt,
source: {
name: linkContent.source?.name || null,
slug: linkContent.source?.slug || username,
logoUrl: linkContent.source?.logoUrl,
},
});

const breadcrumbSchema = getBreadcrumbSchema([
{ name: "Home", url: "https://www.codu.co" },
{ name: "Feed", url: "https://www.codu.co/feed" },
{
name: linkContent.source?.name || username,
url: `https://www.codu.co/${linkContent.source?.slug || username}`,
},
{ name: linkContent.title },
]);

// Render link content with JSON-LD
return (
<>
<JsonLd data={newsArticleSchema} />
<JsonLd data={breadcrumbSchema} />
<LinkContentDetail sourceSlug={username} contentSlug={slug} />
</>
);
}

// Nothing found
Expand Down
14 changes: 14 additions & 0 deletions app/(app)/[username]/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,8 @@ import { type Metadata } from "next";
import { db } from "@/server/db";
import { feed_sources } from "@/server/db/schema";
import { eq } from "drizzle-orm";
import { JsonLd } from "@/components/JsonLd";
import { getPersonSchema } from "@/lib/structured-data";

type Props = { params: Promise<{ username: string }> };

Expand DownExpand Up@@ -133,8 +135,20 @@ export default async function Page(props: {
accountLocked,
};

// Prepare Person JSON-LD for SEO
const personSchema = getPersonSchema({
name: shapedProfile.name,
username: shapedProfile.username,
image: shapedProfile.image,
bio: shapedProfile.bio,
websiteUrl: shapedProfile.websiteUrl,
});

return (
<>
{/* Person JSON-LD for profile SEO */}
<JsonLd data={personSchema} />

<h1 className="sr-only">{`${shapedProfile.name || shapedProfile.username}'s Coding Profile`}</h1>
<Content profile={shapedProfile} isOwner={isOwner} session={session} />
</>
Expand Down
32 changes: 24 additions & 8 deletions app/(app)/admin/_client.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,7 @@ import {
FlagIcon,
RssIcon,
ShieldExclamationIcon,
NewspaperIcon,
TagIcon,
} from "@heroicons/react/24/outline";
import { api } from "@/server/trpc/react";

Expand DownExpand Up@@ -99,13 +99,6 @@ const AdminDashboard = () => {
color="green"
isLoading={isLoading}
/>
<StatCard
title="Aggregated Articles"
value={stats?.aggregatedArticles}
icon={NewspaperIcon}
color="purple"
isLoading={isLoading}
/>
<StatCard
title="Active Feed Sources"
value={stats?.activeFeedSources}
Expand All@@ -114,6 +107,14 @@ const AdminDashboard = () => {
href="/admin/sources"
isLoading={isLoading}
/>
<StatCard
title="Total Reports"
value={reportCounts?.total}
icon={FlagIcon}
color="purple"
href="/admin/moderation"
isLoading={isLoading}
/>
</div>

{/* Moderation Stats */}
Expand DownExpand Up@@ -205,6 +206,21 @@ const AdminDashboard = () => {
</p>
</div>
</Link>

<Link
href="/admin/tags"
className="flex items-center gap-3 rounded-lg border border-neutral-200 bg-white p-4 transition-colors hover:border-green-300 hover:bg-green-50 dark:border-neutral-700 dark:bg-neutral-800 dark:hover:border-green-700 dark:hover:bg-green-900/20"
>
<TagIcon className="h-6 w-6 text-green-500" />
<div>
<p className="font-medium text-neutral-900 dark:text-white">
Tag Management
</p>
<p className="text-sm text-neutral-500 dark:text-neutral-400">
Merge, curate, and manage tags
</p>
</div>
</Link>
</div>
</div>
</div>
Expand Down
1 change: 0 additions & 1 deletion app/(app)/admin/moderation/_client.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,6 @@ import { useState } from "react";
import Link from "next/link";
import {
FlagIcon,
CheckCircleIcon,
XCircleIcon,
ExclamationTriangleIcon,
ArrowLeftIcon,
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
04c45a4
feat: unified tag system with Medium-style UX and admin dashboard
NiallJoeMaher Jan 10, 2026
ce6e404
feat: enhance admin dashboard with new stats and logo upload function…
NiallJoeMaher Jan 10, 2026
eef9ff2
fix: restore notifications functionality and update card styling
NiallJoeMaher Jan 20, 2026
d072cb9
feat: add JSON-LD structured data for SEO and improve admin sources UI
NiallJoeMaher Jan 27, 2026
56af4d7
Refactor code for improved readability and consistency
NiallJoeMaher Jan 28, 2026
28297b3
chore: remove outdated development test suite documentation
NiallJoeMaher Jan 28, 2026
f500011
fix: update notification migration to convert column types before add…
NiallJoeMaher Jan 28, 2026
9c40ea3
test: enhance e2e tests with load state waits and improve visibility …
NiallJoeMaher Jan 28, 2026
5a911ec
test: enhance e2e tests by waiting for TRPC responses to improve reli…
NiallJoeMaher Jan 29, 2026
b508792
refactor: streamline error handling and improve test reliability acro…
NiallJoeMaher Jan 30, 2026
1a4e58a
test: improve notification and article tests by enhancing reliability…
NiallJoeMaher Jan 30, 2026
6fb8c7a
test: enhance reliability of article bookmarking and loading tests by…
NiallJoeMaher Jan 30, 2026
c4fb779
refactor: update load state waits from "networkidle" to "domcontentlo…
NiallJoeMaher Jan 30, 2026
4742a4e
Fix E2E test failures and hide banned users' posts from feeds
NiallJoeMaher Mar 9, 2026
906afc3
Fix Prettier formatting in E2E test files
NiallJoeMaher Mar 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 0 additions & 52 deletions .claude/commands/do-test.md

This file was deleted.

3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
"typescript.tsdk": "node_modules/typescript/lib",
"snyk.advanced.autoSelectOrganization": true
}
14 changes: 0 additions & 14 deletions app/(app)/[username]/[slug]/_userLinkDetail.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,19 +21,6 @@ type Props = {
contentSlug: string;
};

// Get favicon URL from a website
const getFaviconUrl = (
websiteUrl: string | null | undefined,
): string | null => {
if (!websiteUrl) return null;
try {
const url = new URL(websiteUrl);
return `https://www.google.com/s2/favicons?domain=${url.hostname}&sz=32`;
} catch {
return null;
}
};

// Get hostname from URL
const getHostname = (urlString: string): string => {
try {
Expand DownExpand Up@@ -189,7 +176,6 @@ const UserLinkDetail = ({ username, contentSlug }: Props) => {
})
: null;

const faviconUrl = getFaviconUrl(externalUrl);
const hostname = externalUrl ? getHostname(externalUrl) : null;
const score = votes.upvotes - votes.downvotes;

Expand Down
140 changes: 136 additions & 4 deletions app/(app)/[username]/[slug]/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,12 @@ import { eq, and, lte } from "drizzle-orm";
import FeedArticleContent from "./_feedArticleContent";
import LinkContentDetail from "./_linkContentDetail";
import UserLinkDetail from "./_userLinkDetail";
import { JsonLd } from "@/components/JsonLd";
import {
getArticleSchema,
getBreadcrumbSchema,
getNewsArticleSchema,
} from "@/lib/structured-data";

type Props = { params: Promise<{ username: string; slug: string }> };

Expand DownExpand Up@@ -457,8 +463,40 @@ const UnifiedPostPage = async (props: Props) => {
}) as unknown as string;
}

// Prepare JSON-LD structured data
const articleSchema = getArticleSchema({
title: userPost.title,
excerpt: userPost.excerpt,
slug: userPost.slug,
publishedAt: userPost.published,
updatedAt: userPost.updatedAt,
readingTime: userPost.readTimeMins,
canonicalUrl: userPost.canonicalUrl,
tags: userPost.tags.map((t) => ({ title: t.tag.title })),
author: {
name: userPost.user.name,
username: userPost.user.username,
image: userPost.user.image,
bio: userPost.user.bio,
},
});

const breadcrumbSchema = getBreadcrumbSchema([
{ name: "Home", url: "https://www.codu.co" },
{ name: "Feed", url: "https://www.codu.co/feed" },
{
name: userPost.user.name || "Author",
url: `https://www.codu.co/${userPost.user.username}`,
},
{ name: userPost.title },
]);

return (
<>
{/* JSON-LD Structured Data for SEO */}
<JsonLd data={articleSchema} />
<JsonLd data={breadcrumbSchema} />

<div className="mx-auto max-w-3xl px-4 py-8">
{/* Breadcrumb navigation */}
<nav className="mb-6 flex items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400">
Expand DownExpand Up@@ -618,8 +656,40 @@ const UnifiedPostPage = async (props: Props) => {
}) as unknown as string;
}

// Prepare JSON-LD structured data
const articleSchema = getArticleSchema({
title: userArticle.title,
excerpt: userArticle.excerpt,
slug: userArticle.slug,
publishedAt: userArticle.publishedAt,
updatedAt: userArticle.updatedAt,
readingTime: userArticle.readTimeMins,
canonicalUrl: userArticle.canonicalUrl,
tags: userArticle.tags?.map((t) => ({ title: t.tag.title })),
author: {
name: userArticle.user.name,
username: userArticle.user.username,
image: userArticle.user.image,
bio: userArticle.user.bio,
},
});

const breadcrumbSchema = getBreadcrumbSchema([
{ name: "Home", url: "https://www.codu.co" },
{ name: "Feed", url: "https://www.codu.co/feed" },
{
name: userArticle.user.name || "Author",
url: `https://www.codu.co/${userArticle.user.username}`,
},
{ name: userArticle.title },
]);

return (
<>
{/* JSON-LD Structured Data for SEO */}
<JsonLd data={articleSchema} />
<JsonLd data={breadcrumbSchema} />

<div className="mx-auto max-w-3xl px-4 py-8">
{/* Breadcrumb navigation */}
<nav className="mb-6 flex items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400">
Expand DownExpand Up@@ -773,16 +843,78 @@ const UnifiedPostPage = async (props: Props) => {
const feedArticle = await getFeedArticle(username, slug);

if (feedArticle) {
// Render feed article
return <FeedArticleContent sourceSlug={username} articleSlug={slug} />;
// Prepare JSON-LD structured data for feed article
const newsArticleSchema = getNewsArticleSchema({
title: feedArticle.title,
excerpt: feedArticle.excerpt,
slug: feedArticle.slug,
externalUrl: feedArticle.externalUrl || "",
coverImage: feedArticle.imageUrl || feedArticle.ogImageUrl,
publishedAt: feedArticle.publishedAt,
source: {
name: feedArticle.source?.name || null,
slug: feedArticle.source?.slug || username,
logoUrl: feedArticle.source?.logoUrl,
},
});

const breadcrumbSchema = getBreadcrumbSchema([
{ name: "Home", url: "https://www.codu.co" },
{ name: "Feed", url: "https://www.codu.co/feed" },
{
name: feedArticle.source?.name || username,
url: `https://www.codu.co/${feedArticle.source?.slug || username}`,
},
{ name: feedArticle.title },
]);

// Render feed article with JSON-LD
return (
<>
<JsonLd data={newsArticleSchema} />
<JsonLd data={breadcrumbSchema} />
<FeedArticleContent sourceSlug={username} articleSlug={slug} />
</>
);
}

// Try unified content table (new LINK type items)
const linkContent = await getLinkContent(username, slug);

if (linkContent) {
// Render link content
return <LinkContentDetail sourceSlug={username} contentSlug={slug} />;
// Prepare JSON-LD structured data for link content
const newsArticleSchema = getNewsArticleSchema({
title: linkContent.title,
excerpt: linkContent.excerpt,
slug: linkContent.slug,
externalUrl: linkContent.externalUrl || "",
coverImage: linkContent.imageUrl || linkContent.ogImageUrl,
publishedAt: linkContent.publishedAt,
source: {
name: linkContent.source?.name || null,
slug: linkContent.source?.slug || username,
logoUrl: linkContent.source?.logoUrl,
},
});

const breadcrumbSchema = getBreadcrumbSchema([
{ name: "Home", url: "https://www.codu.co" },
{ name: "Feed", url: "https://www.codu.co/feed" },
{
name: linkContent.source?.name || username,
url: `https://www.codu.co/${linkContent.source?.slug || username}`,
},
{ name: linkContent.title },
]);

// Render link content with JSON-LD
return (
<>
<JsonLd data={newsArticleSchema} />
<JsonLd data={breadcrumbSchema} />
<LinkContentDetail sourceSlug={username} contentSlug={slug} />
</>
);
}

// Nothing found
Expand Down
14 changes: 14 additions & 0 deletions app/(app)/[username]/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,8 @@ import { type Metadata } from "next";
import { db } from "@/server/db";
import { feed_sources } from "@/server/db/schema";
import { eq } from "drizzle-orm";
import { JsonLd } from "@/components/JsonLd";
import { getPersonSchema } from "@/lib/structured-data";

type Props = { params: Promise<{ username: string }> };

Expand DownExpand Up@@ -133,8 +135,20 @@ export default async function Page(props: {
accountLocked,
};

// Prepare Person JSON-LD for SEO
const personSchema = getPersonSchema({
name: shapedProfile.name,
username: shapedProfile.username,
image: shapedProfile.image,
bio: shapedProfile.bio,
websiteUrl: shapedProfile.websiteUrl,
});

return (
<>
{/* Person JSON-LD for profile SEO */}
<JsonLd data={personSchema} />

<h1 className="sr-only">{`${shapedProfile.name || shapedProfile.username}'s Coding Profile`}</h1>
<Content profile={shapedProfile} isOwner={isOwner} session={session} />
</>
Expand Down
32 changes: 24 additions & 8 deletions app/(app)/admin/_client.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,7 @@ import {
FlagIcon,
RssIcon,
ShieldExclamationIcon,
NewspaperIcon,
TagIcon,
} from "@heroicons/react/24/outline";
import { api } from "@/server/trpc/react";

Expand DownExpand Up@@ -99,13 +99,6 @@ const AdminDashboard = () => {
color="green"
isLoading={isLoading}
/>
<StatCard
title="Aggregated Articles"
value={stats?.aggregatedArticles}
icon={NewspaperIcon}
color="purple"
isLoading={isLoading}
/>
<StatCard
title="Active Feed Sources"
value={stats?.activeFeedSources}
Expand All@@ -114,6 +107,14 @@ const AdminDashboard = () => {
href="/admin/sources"
isLoading={isLoading}
/>
<StatCard
title="Total Reports"
value={reportCounts?.total}
icon={FlagIcon}
color="purple"
href="/admin/moderation"
isLoading={isLoading}
/>
</div>

{/* Moderation Stats */}
Expand DownExpand Up@@ -205,6 +206,21 @@ const AdminDashboard = () => {
</p>
</div>
</Link>

<Link
href="/admin/tags"
className="flex items-center gap-3 rounded-lg border border-neutral-200 bg-white p-4 transition-colors hover:border-green-300 hover:bg-green-50 dark:border-neutral-700 dark:bg-neutral-800 dark:hover:border-green-700 dark:hover:bg-green-900/20"
>
<TagIcon className="h-6 w-6 text-green-500" />
<div>
<p className="font-medium text-neutral-900 dark:text-white">
Tag Management
</p>
<p className="text-sm text-neutral-500 dark:text-neutral-400">
Merge, curate, and manage tags
</p>
</div>
</Link>
</div>
</div>
</div>
Expand Down
1 change: 0 additions & 1 deletion app/(app)/admin/moderation/_client.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,6 @@ import { useState } from "react";
import Link from "next/link";
import {
FlagIcon,
CheckCircleIcon,
XCircleIcon,
ExclamationTriangleIcon,
ArrowLeftIcon,
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
04c45a4
feat: unified tag system with Medium-style UX and admin dashboard
NiallJoeMaher Jan 10, 2026
ce6e404
feat: enhance admin dashboard with new stats and logo upload function…
NiallJoeMaher Jan 10, 2026
eef9ff2
fix: restore notifications functionality and update card styling
NiallJoeMaher Jan 20, 2026
d072cb9
feat: add JSON-LD structured data for SEO and improve admin sources UI
NiallJoeMaher Jan 27, 2026
56af4d7
Refactor code for improved readability and consistency
NiallJoeMaher Jan 28, 2026
28297b3
chore: remove outdated development test suite documentation
NiallJoeMaher Jan 28, 2026
f500011
fix: update notification migration to convert column types before add…
NiallJoeMaher Jan 28, 2026
9c40ea3
test: enhance e2e tests with load state waits and improve visibility …
NiallJoeMaher Jan 28, 2026
5a911ec
test: enhance e2e tests by waiting for TRPC responses to improve reli…
NiallJoeMaher Jan 29, 2026
b508792
refactor: streamline error handling and improve test reliability acro…
NiallJoeMaher Jan 30, 2026
1a4e58a
test: improve notification and article tests by enhancing reliability…
NiallJoeMaher Jan 30, 2026
6fb8c7a
test: enhance reliability of article bookmarking and loading tests by…
NiallJoeMaher Jan 30, 2026
c4fb779
refactor: update load state waits from "networkidle" to "domcontentlo…
NiallJoeMaher Jan 30, 2026
4742a4e
Fix E2E test failures and hide banned users' posts from feeds
NiallJoeMaher Mar 9, 2026
906afc3
Fix Prettier formatting in E2E test files
NiallJoeMaher Mar 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 0 additions & 52 deletions .claude/commands/do-test.md

This file was deleted.

3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
"typescript.tsdk": "node_modules/typescript/lib",
"snyk.advanced.autoSelectOrganization": true
}
14 changes: 0 additions & 14 deletions app/(app)/[username]/[slug]/_userLinkDetail.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,19 +21,6 @@ type Props = {
contentSlug: string;
};

// Get favicon URL from a website
const getFaviconUrl = (
websiteUrl: string | null | undefined,
): string | null => {
if (!websiteUrl) return null;
try {
const url = new URL(websiteUrl);
return `https://www.google.com/s2/favicons?domain=${url.hostname}&sz=32`;
} catch {
return null;
}
};

// Get hostname from URL
const getHostname = (urlString: string): string => {
try {
Expand DownExpand Up@@ -189,7 +176,6 @@ const UserLinkDetail = ({ username, contentSlug }: Props) => {
})
: null;

const faviconUrl = getFaviconUrl(externalUrl);
const hostname = externalUrl ? getHostname(externalUrl) : null;
const score = votes.upvotes - votes.downvotes;

Expand Down
140 changes: 136 additions & 4 deletions app/(app)/[username]/[slug]/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,12 @@ import { eq, and, lte } from "drizzle-orm";
import FeedArticleContent from "./_feedArticleContent";
import LinkContentDetail from "./_linkContentDetail";
import UserLinkDetail from "./_userLinkDetail";
import { JsonLd } from "@/components/JsonLd";
import {
getArticleSchema,
getBreadcrumbSchema,
getNewsArticleSchema,
} from "@/lib/structured-data";

type Props = { params: Promise<{ username: string; slug: string }> };

Expand DownExpand Up@@ -457,8 +463,40 @@ const UnifiedPostPage = async (props: Props) => {
}) as unknown as string;
}

// Prepare JSON-LD structured data
const articleSchema = getArticleSchema({
title: userPost.title,
excerpt: userPost.excerpt,
slug: userPost.slug,
publishedAt: userPost.published,
updatedAt: userPost.updatedAt,
readingTime: userPost.readTimeMins,
canonicalUrl: userPost.canonicalUrl,
tags: userPost.tags.map((t) => ({ title: t.tag.title })),
author: {
name: userPost.user.name,
username: userPost.user.username,
image: userPost.user.image,
bio: userPost.user.bio,
},
});

const breadcrumbSchema = getBreadcrumbSchema([
{ name: "Home", url: "https://www.codu.co" },
{ name: "Feed", url: "https://www.codu.co/feed" },
{
name: userPost.user.name || "Author",
url: `https://www.codu.co/${userPost.user.username}`,
},
{ name: userPost.title },
]);

return (
<>
{/* JSON-LD Structured Data for SEO */}
<JsonLd data={articleSchema} />
<JsonLd data={breadcrumbSchema} />

<div className="mx-auto max-w-3xl px-4 py-8">
{/* Breadcrumb navigation */}
<nav className="mb-6 flex items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400">
Expand DownExpand Up@@ -618,8 +656,40 @@ const UnifiedPostPage = async (props: Props) => {
}) as unknown as string;
}

// Prepare JSON-LD structured data
const articleSchema = getArticleSchema({
title: userArticle.title,
excerpt: userArticle.excerpt,
slug: userArticle.slug,
publishedAt: userArticle.publishedAt,
updatedAt: userArticle.updatedAt,
readingTime: userArticle.readTimeMins,
canonicalUrl: userArticle.canonicalUrl,
tags: userArticle.tags?.map((t) => ({ title: t.tag.title })),
author: {
name: userArticle.user.name,
username: userArticle.user.username,
image: userArticle.user.image,
bio: userArticle.user.bio,
},
});

const breadcrumbSchema = getBreadcrumbSchema([
{ name: "Home", url: "https://www.codu.co" },
{ name: "Feed", url: "https://www.codu.co/feed" },
{
name: userArticle.user.name || "Author",
url: `https://www.codu.co/${userArticle.user.username}`,
},
{ name: userArticle.title },
]);

return (
<>
{/* JSON-LD Structured Data for SEO */}
<JsonLd data={articleSchema} />
<JsonLd data={breadcrumbSchema} />

<div className="mx-auto max-w-3xl px-4 py-8">
{/* Breadcrumb navigation */}
<nav className="mb-6 flex items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400">
Expand DownExpand Up@@ -773,16 +843,78 @@ const UnifiedPostPage = async (props: Props) => {
const feedArticle = await getFeedArticle(username, slug);

if (feedArticle) {
// Render feed article
return <FeedArticleContent sourceSlug={username} articleSlug={slug} />;
// Prepare JSON-LD structured data for feed article
const newsArticleSchema = getNewsArticleSchema({
title: feedArticle.title,
excerpt: feedArticle.excerpt,
slug: feedArticle.slug,
externalUrl: feedArticle.externalUrl || "",
coverImage: feedArticle.imageUrl || feedArticle.ogImageUrl,
publishedAt: feedArticle.publishedAt,
source: {
name: feedArticle.source?.name || null,
slug: feedArticle.source?.slug || username,
logoUrl: feedArticle.source?.logoUrl,
},
});

const breadcrumbSchema = getBreadcrumbSchema([
{ name: "Home", url: "https://www.codu.co" },
{ name: "Feed", url: "https://www.codu.co/feed" },
{
name: feedArticle.source?.name || username,
url: `https://www.codu.co/${feedArticle.source?.slug || username}`,
},
{ name: feedArticle.title },
]);

// Render feed article with JSON-LD
return (
<>
<JsonLd data={newsArticleSchema} />
<JsonLd data={breadcrumbSchema} />
<FeedArticleContent sourceSlug={username} articleSlug={slug} />
</>
);
}

// Try unified content table (new LINK type items)
const linkContent = await getLinkContent(username, slug);

if (linkContent) {
// Render link content
return <LinkContentDetail sourceSlug={username} contentSlug={slug} />;
// Prepare JSON-LD structured data for link content
const newsArticleSchema = getNewsArticleSchema({
title: linkContent.title,
excerpt: linkContent.excerpt,
slug: linkContent.slug,
externalUrl: linkContent.externalUrl || "",
coverImage: linkContent.imageUrl || linkContent.ogImageUrl,
publishedAt: linkContent.publishedAt,
source: {
name: linkContent.source?.name || null,
slug: linkContent.source?.slug || username,
logoUrl: linkContent.source?.logoUrl,
},
});

const breadcrumbSchema = getBreadcrumbSchema([
{ name: "Home", url: "https://www.codu.co" },
{ name: "Feed", url: "https://www.codu.co/feed" },
{
name: linkContent.source?.name || username,
url: `https://www.codu.co/${linkContent.source?.slug || username}`,
},
{ name: linkContent.title },
]);

// Render link content with JSON-LD
return (
<>
<JsonLd data={newsArticleSchema} />
<JsonLd data={breadcrumbSchema} />
<LinkContentDetail sourceSlug={username} contentSlug={slug} />
</>
);
}

// Nothing found
Expand Down
14 changes: 14 additions & 0 deletions app/(app)/[username]/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,8 @@ import { type Metadata } from "next";
import { db } from "@/server/db";
import { feed_sources } from "@/server/db/schema";
import { eq } from "drizzle-orm";
import { JsonLd } from "@/components/JsonLd";
import { getPersonSchema } from "@/lib/structured-data";

type Props = { params: Promise<{ username: string }> };

Expand DownExpand Up@@ -133,8 +135,20 @@ export default async function Page(props: {
accountLocked,
};

// Prepare Person JSON-LD for SEO
const personSchema = getPersonSchema({
name: shapedProfile.name,
username: shapedProfile.username,
image: shapedProfile.image,
bio: shapedProfile.bio,
websiteUrl: shapedProfile.websiteUrl,
});

return (
<>
{/* Person JSON-LD for profile SEO */}
<JsonLd data={personSchema} />

<h1 className="sr-only">{`${shapedProfile.name || shapedProfile.username}'s Coding Profile`}</h1>
<Content profile={shapedProfile} isOwner={isOwner} session={session} />
</>
Expand Down
32 changes: 24 additions & 8 deletions app/(app)/admin/_client.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,7 @@ import {
FlagIcon,
RssIcon,
ShieldExclamationIcon,
NewspaperIcon,
TagIcon,
} from "@heroicons/react/24/outline";
import { api } from "@/server/trpc/react";

Expand DownExpand Up@@ -99,13 +99,6 @@ const AdminDashboard = () => {
color="green"
isLoading={isLoading}
/>
<StatCard
title="Aggregated Articles"
value={stats?.aggregatedArticles}
icon={NewspaperIcon}
color="purple"
isLoading={isLoading}
/>
<StatCard
title="Active Feed Sources"
value={stats?.activeFeedSources}
Expand All@@ -114,6 +107,14 @@ const AdminDashboard = () => {
href="/admin/sources"
isLoading={isLoading}
/>
<StatCard
title="Total Reports"
value={reportCounts?.total}
icon={FlagIcon}
color="purple"
href="/admin/moderation"
isLoading={isLoading}
/>
</div>

{/* Moderation Stats */}
Expand DownExpand Up@@ -205,6 +206,21 @@ const AdminDashboard = () => {
</p>
</div>
</Link>

<Link
href="/admin/tags"
className="flex items-center gap-3 rounded-lg border border-neutral-200 bg-white p-4 transition-colors hover:border-green-300 hover:bg-green-50 dark:border-neutral-700 dark:bg-neutral-800 dark:hover:border-green-700 dark:hover:bg-green-900/20"
>
<TagIcon className="h-6 w-6 text-green-500" />
<div>
<p className="font-medium text-neutral-900 dark:text-white">
Tag Management
</p>
<p className="text-sm text-neutral-500 dark:text-neutral-400">
Merge, curate, and manage tags
</p>
</div>
</Link>
</div>
</div>
</div>
Expand Down
1 change: 0 additions & 1 deletion app/(app)/admin/moderation/_client.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,6 @@ import { useState } from "react";
import Link from "next/link";
import {
FlagIcon,
CheckCircleIcon,
XCircleIcon,
ExclamationTriangleIcon,
ArrowLeftIcon,
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
04c45a4
feat: unified tag system with Medium-style UX and admin dashboard
NiallJoeMaher Jan 10, 2026
ce6e404
feat: enhance admin dashboard with new stats and logo upload function…
NiallJoeMaher Jan 10, 2026
eef9ff2
fix: restore notifications functionality and update card styling
NiallJoeMaher Jan 20, 2026
d072cb9
feat: add JSON-LD structured data for SEO and improve admin sources UI
NiallJoeMaher Jan 27, 2026
56af4d7
Refactor code for improved readability and consistency
NiallJoeMaher Jan 28, 2026
28297b3
chore: remove outdated development test suite documentation
NiallJoeMaher Jan 28, 2026
f500011
fix: update notification migration to convert column types before add…
NiallJoeMaher Jan 28, 2026
9c40ea3
test: enhance e2e tests with load state waits and improve visibility …
NiallJoeMaher Jan 28, 2026
5a911ec
test: enhance e2e tests by waiting for TRPC responses to improve reli…
NiallJoeMaher Jan 29, 2026
b508792
refactor: streamline error handling and improve test reliability acro…
NiallJoeMaher Jan 30, 2026
1a4e58a
test: improve notification and article tests by enhancing reliability…
NiallJoeMaher Jan 30, 2026
6fb8c7a
test: enhance reliability of article bookmarking and loading tests by…
NiallJoeMaher Jan 30, 2026
c4fb779
refactor: update load state waits from "networkidle" to "domcontentlo…
NiallJoeMaher Jan 30, 2026
4742a4e
Fix E2E test failures and hide banned users' posts from feeds
NiallJoeMaher Mar 9, 2026
906afc3
Fix Prettier formatting in E2E test files
NiallJoeMaher Mar 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 0 additions & 52 deletions .claude/commands/do-test.md

This file was deleted.

3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
"typescript.tsdk": "node_modules/typescript/lib",
"snyk.advanced.autoSelectOrganization": true
}
14 changes: 0 additions & 14 deletions app/(app)/[username]/[slug]/_userLinkDetail.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,19 +21,6 @@ type Props = {
contentSlug: string;
};

// Get favicon URL from a website
const getFaviconUrl = (
websiteUrl: string | null | undefined,
): string | null => {
if (!websiteUrl) return null;
try {
const url = new URL(websiteUrl);
return `https://www.google.com/s2/favicons?domain=${url.hostname}&sz=32`;
} catch {
return null;
}
};

// Get hostname from URL
const getHostname = (urlString: string): string => {
try {
Expand DownExpand Up@@ -189,7 +176,6 @@ const UserLinkDetail = ({ username, contentSlug }: Props) => {
})
: null;

const faviconUrl = getFaviconUrl(externalUrl);
const hostname = externalUrl ? getHostname(externalUrl) : null;
const score = votes.upvotes - votes.downvotes;

Expand Down
140 changes: 136 additions & 4 deletions app/(app)/[username]/[slug]/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,12 @@ import { eq, and, lte } from "drizzle-orm";
import FeedArticleContent from "./_feedArticleContent";
import LinkContentDetail from "./_linkContentDetail";
import UserLinkDetail from "./_userLinkDetail";
import { JsonLd } from "@/components/JsonLd";
import {
getArticleSchema,
getBreadcrumbSchema,
getNewsArticleSchema,
} from "@/lib/structured-data";

type Props = { params: Promise<{ username: string; slug: string }> };

Expand DownExpand Up@@ -457,8 +463,40 @@ const UnifiedPostPage = async (props: Props) => {
}) as unknown as string;
}

// Prepare JSON-LD structured data
const articleSchema = getArticleSchema({
title: userPost.title,
excerpt: userPost.excerpt,
slug: userPost.slug,
publishedAt: userPost.published,
updatedAt: userPost.updatedAt,
readingTime: userPost.readTimeMins,
canonicalUrl: userPost.canonicalUrl,
tags: userPost.tags.map((t) => ({ title: t.tag.title })),
author: {
name: userPost.user.name,
username: userPost.user.username,
image: userPost.user.image,
bio: userPost.user.bio,
},
});

const breadcrumbSchema = getBreadcrumbSchema([
{ name: "Home", url: "https://www.codu.co" },
{ name: "Feed", url: "https://www.codu.co/feed" },
{
name: userPost.user.name || "Author",
url: `https://www.codu.co/${userPost.user.username}`,
},
{ name: userPost.title },
]);

return (
<>
{/* JSON-LD Structured Data for SEO */}
<JsonLd data={articleSchema} />
<JsonLd data={breadcrumbSchema} />

<div className="mx-auto max-w-3xl px-4 py-8">
{/* Breadcrumb navigation */}
<nav className="mb-6 flex items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400">
Expand DownExpand Up@@ -618,8 +656,40 @@ const UnifiedPostPage = async (props: Props) => {
}) as unknown as string;
}

// Prepare JSON-LD structured data
const articleSchema = getArticleSchema({
title: userArticle.title,
excerpt: userArticle.excerpt,
slug: userArticle.slug,
publishedAt: userArticle.publishedAt,
updatedAt: userArticle.updatedAt,
readingTime: userArticle.readTimeMins,
canonicalUrl: userArticle.canonicalUrl,
tags: userArticle.tags?.map((t) => ({ title: t.tag.title })),
author: {
name: userArticle.user.name,
username: userArticle.user.username,
image: userArticle.user.image,
bio: userArticle.user.bio,
},
});

const breadcrumbSchema = getBreadcrumbSchema([
{ name: "Home", url: "https://www.codu.co" },
{ name: "Feed", url: "https://www.codu.co/feed" },
{
name: userArticle.user.name || "Author",
url: `https://www.codu.co/${userArticle.user.username}`,
},
{ name: userArticle.title },
]);

return (
<>
{/* JSON-LD Structured Data for SEO */}
<JsonLd data={articleSchema} />
<JsonLd data={breadcrumbSchema} />

<div className="mx-auto max-w-3xl px-4 py-8">
{/* Breadcrumb navigation */}
<nav className="mb-6 flex items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400">
Expand DownExpand Up@@ -773,16 +843,78 @@ const UnifiedPostPage = async (props: Props) => {
const feedArticle = await getFeedArticle(username, slug);

if (feedArticle) {
// Render feed article
return <FeedArticleContent sourceSlug={username} articleSlug={slug} />;
// Prepare JSON-LD structured data for feed article
const newsArticleSchema = getNewsArticleSchema({
title: feedArticle.title,
excerpt: feedArticle.excerpt,
slug: feedArticle.slug,
externalUrl: feedArticle.externalUrl || "",
coverImage: feedArticle.imageUrl || feedArticle.ogImageUrl,
publishedAt: feedArticle.publishedAt,
source: {
name: feedArticle.source?.name || null,
slug: feedArticle.source?.slug || username,
logoUrl: feedArticle.source?.logoUrl,
},
});

const breadcrumbSchema = getBreadcrumbSchema([
{ name: "Home", url: "https://www.codu.co" },
{ name: "Feed", url: "https://www.codu.co/feed" },
{
name: feedArticle.source?.name || username,
url: `https://www.codu.co/${feedArticle.source?.slug || username}`,
},
{ name: feedArticle.title },
]);

// Render feed article with JSON-LD
return (
<>
<JsonLd data={newsArticleSchema} />
<JsonLd data={breadcrumbSchema} />
<FeedArticleContent sourceSlug={username} articleSlug={slug} />
</>
);
}

// Try unified content table (new LINK type items)
const linkContent = await getLinkContent(username, slug);

if (linkContent) {
// Render link content
return <LinkContentDetail sourceSlug={username} contentSlug={slug} />;
// Prepare JSON-LD structured data for link content
const newsArticleSchema = getNewsArticleSchema({
title: linkContent.title,
excerpt: linkContent.excerpt,
slug: linkContent.slug,
externalUrl: linkContent.externalUrl || "",
coverImage: linkContent.imageUrl || linkContent.ogImageUrl,
publishedAt: linkContent.publishedAt,
source: {
name: linkContent.source?.name || null,
slug: linkContent.source?.slug || username,
logoUrl: linkContent.source?.logoUrl,
},
});

const breadcrumbSchema = getBreadcrumbSchema([
{ name: "Home", url: "https://www.codu.co" },
{ name: "Feed", url: "https://www.codu.co/feed" },
{
name: linkContent.source?.name || username,
url: `https://www.codu.co/${linkContent.source?.slug || username}`,
},
{ name: linkContent.title },
]);

// Render link content with JSON-LD
return (
<>
<JsonLd data={newsArticleSchema} />
<JsonLd data={breadcrumbSchema} />
<LinkContentDetail sourceSlug={username} contentSlug={slug} />
</>
);
}

// Nothing found
Expand Down
14 changes: 14 additions & 0 deletions app/(app)/[username]/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,8 @@ import { type Metadata } from "next";
import { db } from "@/server/db";
import { feed_sources } from "@/server/db/schema";
import { eq } from "drizzle-orm";
import { JsonLd } from "@/components/JsonLd";
import { getPersonSchema } from "@/lib/structured-data";

type Props = { params: Promise<{ username: string }> };

Expand DownExpand Up@@ -133,8 +135,20 @@ export default async function Page(props: {
accountLocked,
};

// Prepare Person JSON-LD for SEO
const personSchema = getPersonSchema({
name: shapedProfile.name,
username: shapedProfile.username,
image: shapedProfile.image,
bio: shapedProfile.bio,
websiteUrl: shapedProfile.websiteUrl,
});

return (
<>
{/* Person JSON-LD for profile SEO */}
<JsonLd data={personSchema} />

<h1 className="sr-only">{`${shapedProfile.name || shapedProfile.username}'s Coding Profile`}</h1>
<Content profile={shapedProfile} isOwner={isOwner} session={session} />
</>
Expand Down
32 changes: 24 additions & 8 deletions app/(app)/admin/_client.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,7 @@ import {
FlagIcon,
RssIcon,
ShieldExclamationIcon,
NewspaperIcon,
TagIcon,
} from "@heroicons/react/24/outline";
import { api } from "@/server/trpc/react";

Expand DownExpand Up@@ -99,13 +99,6 @@ const AdminDashboard = () => {
color="green"
isLoading={isLoading}
/>
<StatCard
title="Aggregated Articles"
value={stats?.aggregatedArticles}
icon={NewspaperIcon}
color="purple"
isLoading={isLoading}
/>
<StatCard
title="Active Feed Sources"
value={stats?.activeFeedSources}
Expand All@@ -114,6 +107,14 @@ const AdminDashboard = () => {
href="/admin/sources"
isLoading={isLoading}
/>
<StatCard
title="Total Reports"
value={reportCounts?.total}
icon={FlagIcon}
color="purple"
href="/admin/moderation"
isLoading={isLoading}
/>
</div>

{/* Moderation Stats */}
Expand DownExpand Up@@ -205,6 +206,21 @@ const AdminDashboard = () => {
</p>
</div>
</Link>

<Link
href="/admin/tags"
className="flex items-center gap-3 rounded-lg border border-neutral-200 bg-white p-4 transition-colors hover:border-green-300 hover:bg-green-50 dark:border-neutral-700 dark:bg-neutral-800 dark:hover:border-green-700 dark:hover:bg-green-900/20"
>
<TagIcon className="h-6 w-6 text-green-500" />
<div>
<p className="font-medium text-neutral-900 dark:text-white">
Tag Management
</p>
<p className="text-sm text-neutral-500 dark:text-neutral-400">
Merge, curate, and manage tags
</p>
</div>
</Link>
</div>
</div>
</div>
Expand Down
1 change: 0 additions & 1 deletion app/(app)/admin/moderation/_client.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,6 @@ import { useState } from "react";
import Link from "next/link";
import {
FlagIcon,
CheckCircleIcon,
XCircleIcon,
ExclamationTriangleIcon,
ArrowLeftIcon,
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
04c45a4
feat: unified tag system with Medium-style UX and admin dashboard
NiallJoeMaher Jan 10, 2026
ce6e404
feat: enhance admin dashboard with new stats and logo upload function…
NiallJoeMaher Jan 10, 2026
eef9ff2
fix: restore notifications functionality and update card styling
NiallJoeMaher Jan 20, 2026
d072cb9
feat: add JSON-LD structured data for SEO and improve admin sources UI
NiallJoeMaher Jan 27, 2026
56af4d7
Refactor code for improved readability and consistency
NiallJoeMaher Jan 28, 2026
28297b3
chore: remove outdated development test suite documentation
NiallJoeMaher Jan 28, 2026
f500011
fix: update notification migration to convert column types before add…
NiallJoeMaher Jan 28, 2026
9c40ea3
test: enhance e2e tests with load state waits and improve visibility …
NiallJoeMaher Jan 28, 2026
5a911ec
test: enhance e2e tests by waiting for TRPC responses to improve reli…
NiallJoeMaher Jan 29, 2026
b508792
refactor: streamline error handling and improve test reliability acro…
NiallJoeMaher Jan 30, 2026
1a4e58a
test: improve notification and article tests by enhancing reliability…
NiallJoeMaher Jan 30, 2026
6fb8c7a
test: enhance reliability of article bookmarking and loading tests by…
NiallJoeMaher Jan 30, 2026
c4fb779
refactor: update load state waits from "networkidle" to "domcontentlo…
NiallJoeMaher Jan 30, 2026
4742a4e
Fix E2E test failures and hide banned users' posts from feeds
NiallJoeMaher Mar 9, 2026
906afc3
Fix Prettier formatting in E2E test files
NiallJoeMaher Mar 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 0 additions & 52 deletions .claude/commands/do-test.md

This file was deleted.

3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
"typescript.tsdk": "node_modules/typescript/lib",
"snyk.advanced.autoSelectOrganization": true
}
14 changes: 0 additions & 14 deletions app/(app)/[username]/[slug]/_userLinkDetail.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,19 +21,6 @@ type Props = {
contentSlug: string;
};

// Get favicon URL from a website
const getFaviconUrl = (
websiteUrl: string | null | undefined,
): string | null => {
if (!websiteUrl) return null;
try {
const url = new URL(websiteUrl);
return `https://www.google.com/s2/favicons?domain=${url.hostname}&sz=32`;
} catch {
return null;
}
};

// Get hostname from URL
const getHostname = (urlString: string): string => {
try {
Expand DownExpand Up@@ -189,7 +176,6 @@ const UserLinkDetail = ({ username, contentSlug }: Props) => {
})
: null;

const faviconUrl = getFaviconUrl(externalUrl);
const hostname = externalUrl ? getHostname(externalUrl) : null;
const score = votes.upvotes - votes.downvotes;

Expand Down
140 changes: 136 additions & 4 deletions app/(app)/[username]/[slug]/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,12 @@ import { eq, and, lte } from "drizzle-orm";
import FeedArticleContent from "./_feedArticleContent";
import LinkContentDetail from "./_linkContentDetail";
import UserLinkDetail from "./_userLinkDetail";
import { JsonLd } from "@/components/JsonLd";
import {
getArticleSchema,
getBreadcrumbSchema,
getNewsArticleSchema,
} from "@/lib/structured-data";

type Props = { params: Promise<{ username: string; slug: string }> };

Expand DownExpand Up@@ -457,8 +463,40 @@ const UnifiedPostPage = async (props: Props) => {
}) as unknown as string;
}

// Prepare JSON-LD structured data
const articleSchema = getArticleSchema({
title: userPost.title,
excerpt: userPost.excerpt,
slug: userPost.slug,
publishedAt: userPost.published,
updatedAt: userPost.updatedAt,
readingTime: userPost.readTimeMins,
canonicalUrl: userPost.canonicalUrl,
tags: userPost.tags.map((t) => ({ title: t.tag.title })),
author: {
name: userPost.user.name,
username: userPost.user.username,
image: userPost.user.image,
bio: userPost.user.bio,
},
});

const breadcrumbSchema = getBreadcrumbSchema([
{ name: "Home", url: "https://www.codu.co" },
{ name: "Feed", url: "https://www.codu.co/feed" },
{
name: userPost.user.name || "Author",
url: `https://www.codu.co/${userPost.user.username}`,
},
{ name: userPost.title },
]);

return (
<>
{/* JSON-LD Structured Data for SEO */}
<JsonLd data={articleSchema} />
<JsonLd data={breadcrumbSchema} />

<div className="mx-auto max-w-3xl px-4 py-8">
{/* Breadcrumb navigation */}
<nav className="mb-6 flex items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400">
Expand DownExpand Up@@ -618,8 +656,40 @@ const UnifiedPostPage = async (props: Props) => {
}) as unknown as string;
}

// Prepare JSON-LD structured data
const articleSchema = getArticleSchema({
title: userArticle.title,
excerpt: userArticle.excerpt,
slug: userArticle.slug,
publishedAt: userArticle.publishedAt,
updatedAt: userArticle.updatedAt,
readingTime: userArticle.readTimeMins,
canonicalUrl: userArticle.canonicalUrl,
tags: userArticle.tags?.map((t) => ({ title: t.tag.title })),
author: {
name: userArticle.user.name,
username: userArticle.user.username,
image: userArticle.user.image,
bio: userArticle.user.bio,
},
});

const breadcrumbSchema = getBreadcrumbSchema([
{ name: "Home", url: "https://www.codu.co" },
{ name: "Feed", url: "https://www.codu.co/feed" },
{
name: userArticle.user.name || "Author",
url: `https://www.codu.co/${userArticle.user.username}`,
},
{ name: userArticle.title },
]);

return (
<>
{/* JSON-LD Structured Data for SEO */}
<JsonLd data={articleSchema} />
<JsonLd data={breadcrumbSchema} />

<div className="mx-auto max-w-3xl px-4 py-8">
{/* Breadcrumb navigation */}
<nav className="mb-6 flex items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400">
Expand DownExpand Up@@ -773,16 +843,78 @@ const UnifiedPostPage = async (props: Props) => {
const feedArticle = await getFeedArticle(username, slug);

if (feedArticle) {
// Render feed article
return <FeedArticleContent sourceSlug={username} articleSlug={slug} />;
// Prepare JSON-LD structured data for feed article
const newsArticleSchema = getNewsArticleSchema({
title: feedArticle.title,
excerpt: feedArticle.excerpt,
slug: feedArticle.slug,
externalUrl: feedArticle.externalUrl || "",
coverImage: feedArticle.imageUrl || feedArticle.ogImageUrl,
publishedAt: feedArticle.publishedAt,
source: {
name: feedArticle.source?.name || null,
slug: feedArticle.source?.slug || username,
logoUrl: feedArticle.source?.logoUrl,
},
});

const breadcrumbSchema = getBreadcrumbSchema([
{ name: "Home", url: "https://www.codu.co" },
{ name: "Feed", url: "https://www.codu.co/feed" },
{
name: feedArticle.source?.name || username,
url: `https://www.codu.co/${feedArticle.source?.slug || username}`,
},
{ name: feedArticle.title },
]);

// Render feed article with JSON-LD
return (
<>
<JsonLd data={newsArticleSchema} />
<JsonLd data={breadcrumbSchema} />
<FeedArticleContent sourceSlug={username} articleSlug={slug} />
</>
);
}

// Try unified content table (new LINK type items)
const linkContent = await getLinkContent(username, slug);

if (linkContent) {
// Render link content
return <LinkContentDetail sourceSlug={username} contentSlug={slug} />;
// Prepare JSON-LD structured data for link content
const newsArticleSchema = getNewsArticleSchema({
title: linkContent.title,
excerpt: linkContent.excerpt,
slug: linkContent.slug,
externalUrl: linkContent.externalUrl || "",
coverImage: linkContent.imageUrl || linkContent.ogImageUrl,
publishedAt: linkContent.publishedAt,
source: {
name: linkContent.source?.name || null,
slug: linkContent.source?.slug || username,
logoUrl: linkContent.source?.logoUrl,
},
});

const breadcrumbSchema = getBreadcrumbSchema([
{ name: "Home", url: "https://www.codu.co" },
{ name: "Feed", url: "https://www.codu.co/feed" },
{
name: linkContent.source?.name || username,
url: `https://www.codu.co/${linkContent.source?.slug || username}`,
},
{ name: linkContent.title },
]);

// Render link content with JSON-LD
return (
<>
<JsonLd data={newsArticleSchema} />
<JsonLd data={breadcrumbSchema} />
<LinkContentDetail sourceSlug={username} contentSlug={slug} />
</>
);
}

// Nothing found
Expand Down
14 changes: 14 additions & 0 deletions app/(app)/[username]/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,8 @@ import { type Metadata } from "next";
import { db } from "@/server/db";
import { feed_sources } from "@/server/db/schema";
import { eq } from "drizzle-orm";
import { JsonLd } from "@/components/JsonLd";
import { getPersonSchema } from "@/lib/structured-data";

type Props = { params: Promise<{ username: string }> };

Expand DownExpand Up@@ -133,8 +135,20 @@ export default async function Page(props: {
accountLocked,
};

// Prepare Person JSON-LD for SEO
const personSchema = getPersonSchema({
name: shapedProfile.name,
username: shapedProfile.username,
image: shapedProfile.image,
bio: shapedProfile.bio,
websiteUrl: shapedProfile.websiteUrl,
});

return (
<>
{/* Person JSON-LD for profile SEO */}
<JsonLd data={personSchema} />

<h1 className="sr-only">{`${shapedProfile.name || shapedProfile.username}'s Coding Profile`}</h1>
<Content profile={shapedProfile} isOwner={isOwner} session={session} />
</>
Expand Down
32 changes: 24 additions & 8 deletions app/(app)/admin/_client.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,7 @@ import {
FlagIcon,
RssIcon,
ShieldExclamationIcon,
NewspaperIcon,
TagIcon,
} from "@heroicons/react/24/outline";
import { api } from "@/server/trpc/react";

Expand DownExpand Up@@ -99,13 +99,6 @@ const AdminDashboard = () => {
color="green"
isLoading={isLoading}
/>
<StatCard
title="Aggregated Articles"
value={stats?.aggregatedArticles}
icon={NewspaperIcon}
color="purple"
isLoading={isLoading}
/>
<StatCard
title="Active Feed Sources"
value={stats?.activeFeedSources}
Expand All@@ -114,6 +107,14 @@ const AdminDashboard = () => {
href="/admin/sources"
isLoading={isLoading}
/>
<StatCard
title="Total Reports"
value={reportCounts?.total}
icon={FlagIcon}
color="purple"
href="/admin/moderation"
isLoading={isLoading}
/>
</div>

{/* Moderation Stats */}
Expand DownExpand Up@@ -205,6 +206,21 @@ const AdminDashboard = () => {
</p>
</div>
</Link>

<Link
href="/admin/tags"
className="flex items-center gap-3 rounded-lg border border-neutral-200 bg-white p-4 transition-colors hover:border-green-300 hover:bg-green-50 dark:border-neutral-700 dark:bg-neutral-800 dark:hover:border-green-700 dark:hover:bg-green-900/20"
>
<TagIcon className="h-6 w-6 text-green-500" />
<div>
<p className="font-medium text-neutral-900 dark:text-white">
Tag Management
</p>
<p className="text-sm text-neutral-500 dark:text-neutral-400">
Merge, curate, and manage tags
</p>
</div>
</Link>
</div>
</div>
</div>
Expand Down
1 change: 0 additions & 1 deletion app/(app)/admin/moderation/_client.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,6 @@ import { useState } from "react";
import Link from "next/link";
import {
FlagIcon,
CheckCircleIcon,
XCircleIcon,
ExclamationTriangleIcon,
ArrowLeftIcon,
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
04c45a4
feat: unified tag system with Medium-style UX and admin dashboard
NiallJoeMaher Jan 10, 2026
ce6e404
feat: enhance admin dashboard with new stats and logo upload function…
NiallJoeMaher Jan 10, 2026
eef9ff2
fix: restore notifications functionality and update card styling
NiallJoeMaher Jan 20, 2026
d072cb9
feat: add JSON-LD structured data for SEO and improve admin sources UI
NiallJoeMaher Jan 27, 2026
56af4d7
Refactor code for improved readability and consistency
NiallJoeMaher Jan 28, 2026
28297b3
chore: remove outdated development test suite documentation
NiallJoeMaher Jan 28, 2026
f500011
fix: update notification migration to convert column types before add…
NiallJoeMaher Jan 28, 2026
9c40ea3
test: enhance e2e tests with load state waits and improve visibility …
NiallJoeMaher Jan 28, 2026
5a911ec
test: enhance e2e tests by waiting for TRPC responses to improve reli…
NiallJoeMaher Jan 29, 2026
b508792
refactor: streamline error handling and improve test reliability acro…
NiallJoeMaher Jan 30, 2026
1a4e58a
test: improve notification and article tests by enhancing reliability…
NiallJoeMaher Jan 30, 2026
6fb8c7a
test: enhance reliability of article bookmarking and loading tests by…
NiallJoeMaher Jan 30, 2026
c4fb779
refactor: update load state waits from "networkidle" to "domcontentlo…
NiallJoeMaher Jan 30, 2026
4742a4e
Fix E2E test failures and hide banned users' posts from feeds
NiallJoeMaher Mar 9, 2026
906afc3
Fix Prettier formatting in E2E test files
NiallJoeMaher Mar 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 0 additions & 52 deletions .claude/commands/do-test.md

This file was deleted.

3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
"typescript.tsdk": "node_modules/typescript/lib",
"snyk.advanced.autoSelectOrganization": true
}
14 changes: 0 additions & 14 deletions app/(app)/[username]/[slug]/_userLinkDetail.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,19 +21,6 @@ type Props = {
contentSlug: string;
};

// Get favicon URL from a website
const getFaviconUrl = (
websiteUrl: string | null | undefined,
): string | null => {
if (!websiteUrl) return null;
try {
const url = new URL(websiteUrl);
return `https://www.google.com/s2/favicons?domain=${url.hostname}&sz=32`;
} catch {
return null;
}
};

// Get hostname from URL
const getHostname = (urlString: string): string => {
try {
Expand DownExpand Up@@ -189,7 +176,6 @@ const UserLinkDetail = ({ username, contentSlug }: Props) => {
})
: null;

const faviconUrl = getFaviconUrl(externalUrl);
const hostname = externalUrl ? getHostname(externalUrl) : null;
const score = votes.upvotes - votes.downvotes;

Expand Down
140 changes: 136 additions & 4 deletions app/(app)/[username]/[slug]/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,12 @@ import { eq, and, lte } from "drizzle-orm";
import FeedArticleContent from "./_feedArticleContent";
import LinkContentDetail from "./_linkContentDetail";
import UserLinkDetail from "./_userLinkDetail";
import { JsonLd } from "@/components/JsonLd";
import {
getArticleSchema,
getBreadcrumbSchema,
getNewsArticleSchema,
} from "@/lib/structured-data";

type Props = { params: Promise<{ username: string; slug: string }> };

Expand DownExpand Up@@ -457,8 +463,40 @@ const UnifiedPostPage = async (props: Props) => {
}) as unknown as string;
}

// Prepare JSON-LD structured data
const articleSchema = getArticleSchema({
title: userPost.title,
excerpt: userPost.excerpt,
slug: userPost.slug,
publishedAt: userPost.published,
updatedAt: userPost.updatedAt,
readingTime: userPost.readTimeMins,
canonicalUrl: userPost.canonicalUrl,
tags: userPost.tags.map((t) => ({ title: t.tag.title })),
author: {
name: userPost.user.name,
username: userPost.user.username,
image: userPost.user.image,
bio: userPost.user.bio,
},
});

const breadcrumbSchema = getBreadcrumbSchema([
{ name: "Home", url: "https://www.codu.co" },
{ name: "Feed", url: "https://www.codu.co/feed" },
{
name: userPost.user.name || "Author",
url: `https://www.codu.co/${userPost.user.username}`,
},
{ name: userPost.title },
]);

return (
<>
{/* JSON-LD Structured Data for SEO */}
<JsonLd data={articleSchema} />
<JsonLd data={breadcrumbSchema} />

<div className="mx-auto max-w-3xl px-4 py-8">
{/* Breadcrumb navigation */}
<nav className="mb-6 flex items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400">
Expand DownExpand Up@@ -618,8 +656,40 @@ const UnifiedPostPage = async (props: Props) => {
}) as unknown as string;
}

// Prepare JSON-LD structured data
const articleSchema = getArticleSchema({
title: userArticle.title,
excerpt: userArticle.excerpt,
slug: userArticle.slug,
publishedAt: userArticle.publishedAt,
updatedAt: userArticle.updatedAt,
readingTime: userArticle.readTimeMins,
canonicalUrl: userArticle.canonicalUrl,
tags: userArticle.tags?.map((t) => ({ title: t.tag.title })),
author: {
name: userArticle.user.name,
username: userArticle.user.username,
image: userArticle.user.image,
bio: userArticle.user.bio,
},
});

const breadcrumbSchema = getBreadcrumbSchema([
{ name: "Home", url: "https://www.codu.co" },
{ name: "Feed", url: "https://www.codu.co/feed" },
{
name: userArticle.user.name || "Author",
url: `https://www.codu.co/${userArticle.user.username}`,
},
{ name: userArticle.title },
]);

return (
<>
{/* JSON-LD Structured Data for SEO */}
<JsonLd data={articleSchema} />
<JsonLd data={breadcrumbSchema} />

<div className="mx-auto max-w-3xl px-4 py-8">
{/* Breadcrumb navigation */}
<nav className="mb-6 flex items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400">
Expand DownExpand Up@@ -773,16 +843,78 @@ const UnifiedPostPage = async (props: Props) => {
const feedArticle = await getFeedArticle(username, slug);

if (feedArticle) {
// Render feed article
return <FeedArticleContent sourceSlug={username} articleSlug={slug} />;
// Prepare JSON-LD structured data for feed article
const newsArticleSchema = getNewsArticleSchema({
title: feedArticle.title,
excerpt: feedArticle.excerpt,
slug: feedArticle.slug,
externalUrl: feedArticle.externalUrl || "",
coverImage: feedArticle.imageUrl || feedArticle.ogImageUrl,
publishedAt: feedArticle.publishedAt,
source: {
name: feedArticle.source?.name || null,
slug: feedArticle.source?.slug || username,
logoUrl: feedArticle.source?.logoUrl,
},
});

const breadcrumbSchema = getBreadcrumbSchema([
{ name: "Home", url: "https://www.codu.co" },
{ name: "Feed", url: "https://www.codu.co/feed" },
{
name: feedArticle.source?.name || username,
url: `https://www.codu.co/${feedArticle.source?.slug || username}`,
},
{ name: feedArticle.title },
]);

// Render feed article with JSON-LD
return (
<>
<JsonLd data={newsArticleSchema} />
<JsonLd data={breadcrumbSchema} />
<FeedArticleContent sourceSlug={username} articleSlug={slug} />
</>
);
}

// Try unified content table (new LINK type items)
const linkContent = await getLinkContent(username, slug);

if (linkContent) {
// Render link content
return <LinkContentDetail sourceSlug={username} contentSlug={slug} />;
// Prepare JSON-LD structured data for link content
const newsArticleSchema = getNewsArticleSchema({
title: linkContent.title,
excerpt: linkContent.excerpt,
slug: linkContent.slug,
externalUrl: linkContent.externalUrl || "",
coverImage: linkContent.imageUrl || linkContent.ogImageUrl,
publishedAt: linkContent.publishedAt,
source: {
name: linkContent.source?.name || null,
slug: linkContent.source?.slug || username,
logoUrl: linkContent.source?.logoUrl,
},
});

const breadcrumbSchema = getBreadcrumbSchema([
{ name: "Home", url: "https://www.codu.co" },
{ name: "Feed", url: "https://www.codu.co/feed" },
{
name: linkContent.source?.name || username,
url: `https://www.codu.co/${linkContent.source?.slug || username}`,
},
{ name: linkContent.title },
]);

// Render link content with JSON-LD
return (
<>
<JsonLd data={newsArticleSchema} />
<JsonLd data={breadcrumbSchema} />
<LinkContentDetail sourceSlug={username} contentSlug={slug} />
</>
);
}

// Nothing found
Expand Down
14 changes: 14 additions & 0 deletions app/(app)/[username]/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,8 @@ import { type Metadata } from "next";
import { db } from "@/server/db";
import { feed_sources } from "@/server/db/schema";
import { eq } from "drizzle-orm";
import { JsonLd } from "@/components/JsonLd";
import { getPersonSchema } from "@/lib/structured-data";

type Props = { params: Promise<{ username: string }> };

Expand DownExpand Up@@ -133,8 +135,20 @@ export default async function Page(props: {
accountLocked,
};

// Prepare Person JSON-LD for SEO
const personSchema = getPersonSchema({
name: shapedProfile.name,
username: shapedProfile.username,
image: shapedProfile.image,
bio: shapedProfile.bio,
websiteUrl: shapedProfile.websiteUrl,
});

return (
<>
{/* Person JSON-LD for profile SEO */}
<JsonLd data={personSchema} />

<h1 className="sr-only">{`${shapedProfile.name || shapedProfile.username}'s Coding Profile`}</h1>
<Content profile={shapedProfile} isOwner={isOwner} session={session} />
</>
Expand Down
32 changes: 24 additions & 8 deletions app/(app)/admin/_client.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,7 @@ import {
FlagIcon,
RssIcon,
ShieldExclamationIcon,
NewspaperIcon,
TagIcon,
} from "@heroicons/react/24/outline";
import { api } from "@/server/trpc/react";

Expand DownExpand Up@@ -99,13 +99,6 @@ const AdminDashboard = () => {
color="green"
isLoading={isLoading}
/>
<StatCard
title="Aggregated Articles"
value={stats?.aggregatedArticles}
icon={NewspaperIcon}
color="purple"
isLoading={isLoading}
/>
<StatCard
title="Active Feed Sources"
value={stats?.activeFeedSources}
Expand All@@ -114,6 +107,14 @@ const AdminDashboard = () => {
href="/admin/sources"
isLoading={isLoading}
/>
<StatCard
title="Total Reports"
value={reportCounts?.total}
icon={FlagIcon}
color="purple"
href="/admin/moderation"
isLoading={isLoading}
/>
</div>

{/* Moderation Stats */}
Expand DownExpand Up@@ -205,6 +206,21 @@ const AdminDashboard = () => {
</p>
</div>
</Link>

<Link
href="/admin/tags"
className="flex items-center gap-3 rounded-lg border border-neutral-200 bg-white p-4 transition-colors hover:border-green-300 hover:bg-green-50 dark:border-neutral-700 dark:bg-neutral-800 dark:hover:border-green-700 dark:hover:bg-green-900/20"
>
<TagIcon className="h-6 w-6 text-green-500" />
<div>
<p className="font-medium text-neutral-900 dark:text-white">
Tag Management
</p>
<p className="text-sm text-neutral-500 dark:text-neutral-400">
Merge, curate, and manage tags
</p>
</div>
</Link>
</div>
</div>
</div>
Expand Down
1 change: 0 additions & 1 deletion app/(app)/admin/moderation/_client.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,6 @@ import { useState } from "react";
import Link from "next/link";
import {
FlagIcon,
CheckCircleIcon,
XCircleIcon,
ExclamationTriangleIcon,
ArrowLeftIcon,
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
04c45a4
feat: unified tag system with Medium-style UX and admin dashboard
NiallJoeMaher Jan 10, 2026
ce6e404
feat: enhance admin dashboard with new stats and logo upload function…
NiallJoeMaher Jan 10, 2026
eef9ff2
fix: restore notifications functionality and update card styling
NiallJoeMaher Jan 20, 2026
d072cb9
feat: add JSON-LD structured data for SEO and improve admin sources UI
NiallJoeMaher Jan 27, 2026
56af4d7
Refactor code for improved readability and consistency
NiallJoeMaher Jan 28, 2026
28297b3
chore: remove outdated development test suite documentation
NiallJoeMaher Jan 28, 2026
f500011
fix: update notification migration to convert column types before add…
NiallJoeMaher Jan 28, 2026
9c40ea3
test: enhance e2e tests with load state waits and improve visibility …
NiallJoeMaher Jan 28, 2026
5a911ec
test: enhance e2e tests by waiting for TRPC responses to improve reli…
NiallJoeMaher Jan 29, 2026
b508792
refactor: streamline error handling and improve test reliability acro…
NiallJoeMaher Jan 30, 2026
1a4e58a
test: improve notification and article tests by enhancing reliability…
NiallJoeMaher Jan 30, 2026
6fb8c7a
test: enhance reliability of article bookmarking and loading tests by…
NiallJoeMaher Jan 30, 2026
c4fb779
refactor: update load state waits from "networkidle" to "domcontentlo…
NiallJoeMaher Jan 30, 2026
4742a4e
Fix E2E test failures and hide banned users' posts from feeds
NiallJoeMaher Mar 9, 2026
906afc3
Fix Prettier formatting in E2E test files
NiallJoeMaher Mar 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 0 additions & 52 deletions .claude/commands/do-test.md

This file was deleted.

3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
"typescript.tsdk": "node_modules/typescript/lib",
"snyk.advanced.autoSelectOrganization": true
}
14 changes: 0 additions & 14 deletions app/(app)/[username]/[slug]/_userLinkDetail.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,19 +21,6 @@ type Props = {
contentSlug: string;
};

// Get favicon URL from a website
const getFaviconUrl = (
websiteUrl: string | null | undefined,
): string | null => {
if (!websiteUrl) return null;
try {
const url = new URL(websiteUrl);
return `https://www.google.com/s2/favicons?domain=${url.hostname}&sz=32`;
} catch {
return null;
}
};

// Get hostname from URL
const getHostname = (urlString: string): string => {
try {
Expand DownExpand Up@@ -189,7 +176,6 @@ const UserLinkDetail = ({ username, contentSlug }: Props) => {
})
: null;

const faviconUrl = getFaviconUrl(externalUrl);
const hostname = externalUrl ? getHostname(externalUrl) : null;
const score = votes.upvotes - votes.downvotes;

Expand Down
140 changes: 136 additions & 4 deletions app/(app)/[username]/[slug]/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,12 @@ import { eq, and, lte } from "drizzle-orm";
import FeedArticleContent from "./_feedArticleContent";
import LinkContentDetail from "./_linkContentDetail";
import UserLinkDetail from "./_userLinkDetail";
import { JsonLd } from "@/components/JsonLd";
import {
getArticleSchema,
getBreadcrumbSchema,
getNewsArticleSchema,
} from "@/lib/structured-data";

type Props = { params: Promise<{ username: string; slug: string }> };

Expand DownExpand Up@@ -457,8 +463,40 @@ const UnifiedPostPage = async (props: Props) => {
}) as unknown as string;
}

// Prepare JSON-LD structured data
const articleSchema = getArticleSchema({
title: userPost.title,
excerpt: userPost.excerpt,
slug: userPost.slug,
publishedAt: userPost.published,
updatedAt: userPost.updatedAt,
readingTime: userPost.readTimeMins,
canonicalUrl: userPost.canonicalUrl,
tags: userPost.tags.map((t) => ({ title: t.tag.title })),
author: {
name: userPost.user.name,
username: userPost.user.username,
image: userPost.user.image,
bio: userPost.user.bio,
},
});

const breadcrumbSchema = getBreadcrumbSchema([
{ name: "Home", url: "https://www.codu.co" },
{ name: "Feed", url: "https://www.codu.co/feed" },
{
name: userPost.user.name || "Author",
url: `https://www.codu.co/${userPost.user.username}`,
},
{ name: userPost.title },
]);

return (
<>
{/* JSON-LD Structured Data for SEO */}
<JsonLd data={articleSchema} />
<JsonLd data={breadcrumbSchema} />

<div className="mx-auto max-w-3xl px-4 py-8">
{/* Breadcrumb navigation */}
<nav className="mb-6 flex items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400">
Expand DownExpand Up@@ -618,8 +656,40 @@ const UnifiedPostPage = async (props: Props) => {
}) as unknown as string;
}

// Prepare JSON-LD structured data
const articleSchema = getArticleSchema({
title: userArticle.title,
excerpt: userArticle.excerpt,
slug: userArticle.slug,
publishedAt: userArticle.publishedAt,
updatedAt: userArticle.updatedAt,
readingTime: userArticle.readTimeMins,
canonicalUrl: userArticle.canonicalUrl,
tags: userArticle.tags?.map((t) => ({ title: t.tag.title })),
author: {
name: userArticle.user.name,
username: userArticle.user.username,
image: userArticle.user.image,
bio: userArticle.user.bio,
},
});

const breadcrumbSchema = getBreadcrumbSchema([
{ name: "Home", url: "https://www.codu.co" },
{ name: "Feed", url: "https://www.codu.co/feed" },
{
name: userArticle.user.name || "Author",
url: `https://www.codu.co/${userArticle.user.username}`,
},
{ name: userArticle.title },
]);

return (
<>
{/* JSON-LD Structured Data for SEO */}
<JsonLd data={articleSchema} />
<JsonLd data={breadcrumbSchema} />

<div className="mx-auto max-w-3xl px-4 py-8">
{/* Breadcrumb navigation */}
<nav className="mb-6 flex items-center gap-2 text-sm text-neutral-500 dark:text-neutral-400">
Expand DownExpand Up@@ -773,16 +843,78 @@ const UnifiedPostPage = async (props: Props) => {
const feedArticle = await getFeedArticle(username, slug);

if (feedArticle) {
// Render feed article
return <FeedArticleContent sourceSlug={username} articleSlug={slug} />;
// Prepare JSON-LD structured data for feed article
const newsArticleSchema = getNewsArticleSchema({
title: feedArticle.title,
excerpt: feedArticle.excerpt,
slug: feedArticle.slug,
externalUrl: feedArticle.externalUrl || "",
coverImage: feedArticle.imageUrl || feedArticle.ogImageUrl,
publishedAt: feedArticle.publishedAt,
source: {
name: feedArticle.source?.name || null,
slug: feedArticle.source?.slug || username,
logoUrl: feedArticle.source?.logoUrl,
},
});

const breadcrumbSchema = getBreadcrumbSchema([
{ name: "Home", url: "https://www.codu.co" },
{ name: "Feed", url: "https://www.codu.co/feed" },
{
name: feedArticle.source?.name || username,
url: `https://www.codu.co/${feedArticle.source?.slug || username}`,
},
{ name: feedArticle.title },
]);

// Render feed article with JSON-LD
return (
<>
<JsonLd data={newsArticleSchema} />
<JsonLd data={breadcrumbSchema} />
<FeedArticleContent sourceSlug={username} articleSlug={slug} />
</>
);
}

// Try unified content table (new LINK type items)
const linkContent = await getLinkContent(username, slug);

if (linkContent) {
// Render link content
return <LinkContentDetail sourceSlug={username} contentSlug={slug} />;
// Prepare JSON-LD structured data for link content
const newsArticleSchema = getNewsArticleSchema({
title: linkContent.title,
excerpt: linkContent.excerpt,
slug: linkContent.slug,
externalUrl: linkContent.externalUrl || "",
coverImage: linkContent.imageUrl || linkContent.ogImageUrl,
publishedAt: linkContent.publishedAt,
source: {
name: linkContent.source?.name || null,
slug: linkContent.source?.slug || username,
logoUrl: linkContent.source?.logoUrl,
},
});

const breadcrumbSchema = getBreadcrumbSchema([
{ name: "Home", url: "https://www.codu.co" },
{ name: "Feed", url: "https://www.codu.co/feed" },
{
name: linkContent.source?.name || username,
url: `https://www.codu.co/${linkContent.source?.slug || username}`,
},
{ name: linkContent.title },
]);

// Render link content with JSON-LD
return (
<>
<JsonLd data={newsArticleSchema} />
<JsonLd data={breadcrumbSchema} />
<LinkContentDetail sourceSlug={username} contentSlug={slug} />
</>
);
}

// Nothing found
Expand Down
14 changes: 14 additions & 0 deletions app/(app)/[username]/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,8 @@ import { type Metadata } from "next";
import { db } from "@/server/db";
import { feed_sources } from "@/server/db/schema";
import { eq } from "drizzle-orm";
import { JsonLd } from "@/components/JsonLd";
import { getPersonSchema } from "@/lib/structured-data";

type Props = { params: Promise<{ username: string }> };

Expand DownExpand Up@@ -133,8 +135,20 @@ export default async function Page(props: {
accountLocked,
};

// Prepare Person JSON-LD for SEO
const personSchema = getPersonSchema({
name: shapedProfile.name,
username: shapedProfile.username,
image: shapedProfile.image,
bio: shapedProfile.bio,
websiteUrl: shapedProfile.websiteUrl,
});

return (
<>
{/* Person JSON-LD for profile SEO */}
<JsonLd data={personSchema} />

<h1 className="sr-only">{`${shapedProfile.name || shapedProfile.username}'s Coding Profile`}</h1>
<Content profile={shapedProfile} isOwner={isOwner} session={session} />
</>
Expand Down
32 changes: 24 additions & 8 deletions app/(app)/admin/_client.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,7 @@ import {
FlagIcon,
RssIcon,
ShieldExclamationIcon,
NewspaperIcon,
TagIcon,
} from "@heroicons/react/24/outline";
import { api } from "@/server/trpc/react";

Expand DownExpand Up@@ -99,13 +99,6 @@ const AdminDashboard = () => {
color="green"
isLoading={isLoading}
/>
<StatCard
title="Aggregated Articles"
value={stats?.aggregatedArticles}
icon={NewspaperIcon}
color="purple"
isLoading={isLoading}
/>
<StatCard
title="Active Feed Sources"
value={stats?.activeFeedSources}
Expand All@@ -114,6 +107,14 @@ const AdminDashboard = () => {
href="/admin/sources"
isLoading={isLoading}
/>
<StatCard
title="Total Reports"
value={reportCounts?.total}
icon={FlagIcon}
color="purple"
href="/admin/moderation"
isLoading={isLoading}
/>
</div>

{/* Moderation Stats */}
Expand DownExpand Up@@ -205,6 +206,21 @@ const AdminDashboard = () => {
</p>
</div>
</Link>

<Link
href="/admin/tags"
className="flex items-center gap-3 rounded-lg border border-neutral-200 bg-white p-4 transition-colors hover:border-green-300 hover:bg-green-50 dark:border-neutral-700 dark:bg-neutral-800 dark:hover:border-green-700 dark:hover:bg-green-900/20"
>
<TagIcon className="h-6 w-6 text-green-500" />
<div>
<p className="font-medium text-neutral-900 dark:text-white">
Tag Management
</p>
<p className="text-sm text-neutral-500 dark:text-neutral-400">
Merge, curate, and manage tags
</p>
</div>
</Link>
</div>
</div>
</div>
Expand Down
1 change: 0 additions & 1 deletion app/(app)/admin/moderation/_client.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,6 @@ import { useState } from "react";
import Link from "next/link";
import {
FlagIcon,
CheckCircleIcon,
XCircleIcon,
ExclamationTriangleIcon,
ArrowLeftIcon,
Expand Down
Loading
Loading