Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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