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 number Diff line number Diff 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 number Diff line number Diff 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 Down Expand 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 number Diff line number Diff 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 Down Expand 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 Down Expand 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 Down Expand 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 number Diff line number Diff 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 Down Expand 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 number Diff line number Diff 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 Down Expand 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 Down Expand 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 number Diff line number Diff 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