Skip to content
Merged
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
3 changes: 0 additions & 3 deletions client-next/public/robots.txt

This file was deleted.

20 changes: 16 additions & 4 deletions client-next/scripts/generate-sitemap.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Sitemap generator for PostScholar
// Run manually after deployment to update sitemap.xml
// Legacy sitemap generator — prefer client-next/src/app/sitemap.js (dynamic).
// This script remains for one-off exports if the API is unavailable.
// Usage: DATABASE_URL=your_railway_public_url node client-next/scripts/generate-sitemap.js

const { Pool } = require('pg')
Expand All @@ -10,12 +10,24 @@ const pool = new Pool({ connectionString: process.env.DATABASE_URL })

async function generateSitemap() {
const result = await pool.query(
'SELECT id, created_at FROM discussions ORDER BY created_at DESC'
`SELECT d.id, d.created_at, p.title
FROM discussions d
JOIN papers p ON p.id = d.paper_id
ORDER BY d.created_at DESC`
)

function slugify(title, id) {
const slug = (title || 'discussion')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 80)
return `${slug || 'discussion'}-${id}`
}

const urls = result.rows.map(row => `
<url>
<loc>https://postscholar.org/d/${row.id}</loc>
<loc>https://postscholar.org/d/${slugify(row.title, row.id)}</loc>
<lastmod>${new Date(row.created_at).toISOString().split('T')[0]}</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
Expand Down
7 changes: 6 additions & 1 deletion client-next/src/app/LandingHome.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Link from 'next/link'
import { discussionPath } from '@/lib/discussionSlug'
import Layout from '@/components/Layout'
import styles from './landing.module.css'

Expand Down Expand Up @@ -101,7 +102,11 @@ export default function LandingHome({ discussions = [] }) {
) : (
<div className={styles.discussionList}>
{preview.map(d => (
<Link key={d.id} href={`/d/${d.id}`} className={styles.discussionItem}>
<Link
key={d.id}
href={discussionPath({ id: d.id, title: d.title })}
className={styles.discussionItem}
>
<h3 className={`${styles.discussionTitle} paper-title`}>{d.title}</h3>
<p className={styles.discussionMeta}>
{[d.username || d.started_by, formatAuthors(d.authors_json), d.journal, d.year]
Expand Down
88 changes: 0 additions & 88 deletions client-next/src/app/d/[id]/page.js

This file was deleted.

194 changes: 194 additions & 0 deletions client-next/src/app/d/[slug]/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import Link from 'next/link'
import { redirect } from 'next/navigation'
import Layout from '@/components/Layout'
import PaperHeader from '@/components/PaperHeader'
import PaperSidebar from '@/components/PaperSidebar'
import DiscussionComments from '../[id]/DiscussionComments'
import { getServerApiUrl } from '@/lib/config'
import {
buildDiscussionSlug,
parseDiscussionId,
} from '@/lib/discussionSlug'
import { SITE_NAME, SITE_URL } from '@/lib/site'
import styles from '../[id]/Discussion.module.css'

async function getDiscussionData(id) {
try {
const res = await fetch(`${getServerApiUrl()}/discussions/${id}/paper`, {
cache: 'no-store',
})
if (!res.ok) return null
return res.json()
} catch (error) {
console.error('Failed to fetch discussion data:', error)
return null
}
}

function buildJsonLd({ paper, discussionId, startedBy, discussionCreatedAt }) {
const authors = paper.authors_json?.map(a => ({
'@type': 'Person',
name: [a.given, a.family].filter(Boolean).join(' '),
})) || []

const pageUrl = `${SITE_URL}/d/${buildDiscussionSlug(paper.title, discussionId)}`

return [
{
'@context': 'https://schema.org',
'@type': 'ScholarlyArticle',
headline: paper.title,
name: paper.title,
identifier: paper.doi ? `https://doi.org/${paper.doi}` : undefined,
author: authors.length > 0 ? authors : undefined,
datePublished: paper.year ? `${paper.year}` : undefined,
publisher: paper.journal
? { '@type': 'Organization', name: paper.journal }
: undefined,
description: paper.abstract || undefined,
url: pageUrl,
isPartOf: {
'@type': 'WebSite',
name: SITE_NAME,
url: SITE_URL,
},
},
{
'@context': 'https://schema.org',
'@type': 'DiscussionForumPosting',
headline: paper.title,
name: paper.title,
url: pageUrl,
datePublished: discussionCreatedAt,
author: startedBy
? { '@type': 'Person', name: startedBy }
: { '@type': 'Organization', name: SITE_NAME },
about: {
'@type': 'ScholarlyArticle',
headline: paper.title,
identifier: paper.doi ? `https://doi.org/${paper.doi}` : undefined,
},
isPartOf: {
'@type': 'WebSite',
name: SITE_NAME,
url: SITE_URL,
},
},
]
}

export async function generateMetadata({ params }) {
const { slug } = await params
const id = parseDiscussionId(slug)
if (!id) {
return {
title: 'Discussion — PostScholar',
description: 'Academic discussion on PostScholar',
}
}

const data = await getDiscussionData(id)
if (!data || data.error) {
return {
title: 'Discussion — PostScholar',
description: 'Academic discussion on PostScholar',
}
}

const { paper } = data
const authors =
paper.authors_json?.map(a => `${a.given} ${a.family}`).join(', ') || ''
const title = paper.title ? `${paper.title} — PostScholar` : 'Discussion — PostScholar'
const description = paper.abstract
? paper.abstract.slice(0, 160)
: `Discussion of "${paper.title}" on PostScholar`
const canonicalSlug = buildDiscussionSlug(paper.title, id)
const canonicalPath = `/d/${canonicalSlug}`

return {
title,
description,
alternates: {
canonical: canonicalPath,
},
openGraph: {
title: paper.title,
description: paper.abstract?.slice(0, 200) || description,
type: 'article',
url: canonicalPath,
siteName: SITE_NAME,
locale: 'en_US',
authors: authors ? [authors] : undefined,
publishedTime: data.discussion_created_at,
},
twitter: {
card: 'summary_large_image',
title: paper.title,
description: paper.abstract?.slice(0, 200) || description,
},
}
}

export default async function DiscussionPage({ params }) {
const { slug } = await params
const id = parseDiscussionId(slug)

if (!id) {
return (
<Layout>
<p className={styles.error}>Discussion not found.</p>
</Layout>
)
}

const data = await getDiscussionData(id)

if (!data || data.error) {
return (
<Layout>
<p className={styles.error}>Discussion not found.</p>
</Layout>
)
}

const { paper, started_by, discussion_created_at, custom_tags } = data
const canonicalSlug = buildDiscussionSlug(paper.title, id)

if (slug !== canonicalSlug) {
redirect(`/d/${canonicalSlug}`)
}

const sidebar = paper ? (
<PaperSidebar paper={paper} discussionId={id} />
) : null

const jsonLd = buildJsonLd({
paper,
discussionId: id,
startedBy: started_by,
discussionCreatedAt: discussion_created_at,
})

return (
<Layout sidebar={sidebar}>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<Link href="/explore" className="backLink">
← Discussions
</Link>
{paper && (
<PaperHeader
paper={paper}
startedBy={started_by}
discussionCreatedAt={discussion_created_at}
customTags={custom_tags || []}
discussionId={id}
/>
)}
<div className={styles.divider} />
<DiscussionComments discussionId={id} />
</Layout>
)
}
Loading
Loading