diff --git a/app/[lang]/blog/page.tsx b/app/[lang]/blog/page.tsx index 65354f2..3a26c45 100644 --- a/app/[lang]/blog/page.tsx +++ b/app/[lang]/blog/page.tsx @@ -3,12 +3,17 @@ import { AppContent } from "@/components/ui/app-content" import { Label } from "@/components/ui/label" import { Metadata } from "next" import { Suspense } from "react" -import { Article, getArticles } from "@/lib/blog" -import { BlogArticleCard } from "@/components/blog/blog-article-card" -import Link from "next/link" +import { + HydrationBoundary, + QueryClient, + dehydrate, +} from "@tanstack/react-query" +import ArticlesList from "@/components/blog/ArticlesList" export const dynamic = "force-dynamic" +export const revalidate = 60 // Revalidate every 60 seconds + export const metadata: Metadata = { title: "Blog", description: "", @@ -19,55 +24,38 @@ interface BlogPageProps { searchParams?: { [key: string]: string | string[] | undefined } } -const ArticlesGrid = ({ - articles, - lang, -}: { - articles: Article[] - lang: string -}) => { - return ( -
- {articles.length === 0 && ( -

- No articles found for this tag. -

- )} - {articles.map( - ({ id, title, image, tldr = "", date, authors, content }: Article) => { - const url = `/${lang}/blog/${id}` - return ( -
- - - -
- ) - } - )} -
- ) -} - const BlogPage = async ({ params: { lang }, searchParams }: BlogPageProps) => { const { t } = await useTranslation(lang, "blog-page") - const tag = searchParams?.tag as string | undefined - const articles = - getArticles({ - tag, - }) ?? [] + + // Initialize QueryClient at request time + const queryClient = new QueryClient() + + // Prefetch the query with a simpler approach + await queryClient.prefetchQuery({ + queryKey: ["articles", tag], + queryFn: async () => { + try { + const params = new URLSearchParams() + if (tag) params.append("tag", tag) + + // Use a relative URL to avoid environment-specific issues + const response = await fetch(`/api/articles?${params.toString()}`, { + next: { revalidate }, + }) + + if (!response.ok) { + throw new Error(`Failed to fetch articles: ${response.status}`) + } + + const data = await response.json() + return data.articles || [] + } catch (error) { + console.error("Error fetching articles:", error) + return [] + } + }, + }) return (
@@ -85,13 +73,15 @@ const BlogPage = async ({ params: { lang }, searchParams }: BlogPageProps) => {
- + Loading articles... } > - + + + diff --git a/app/[lang]/page.tsx b/app/[lang]/page.tsx index d2efc55..f14a465 100644 --- a/app/[lang]/page.tsx +++ b/app/[lang]/page.tsx @@ -11,7 +11,9 @@ function BlogSection({ lang }: { lang: string }) { return ( Loading recent articles... +
+ Loading recent articles... +
} > {/* @ts-expect-error - This is a valid server component pattern */} diff --git a/app/[lang]/projects/sections/ProjectContent.tsx b/app/[lang]/projects/sections/ProjectContent.tsx index d5fe2eb..c37eae1 100644 --- a/app/[lang]/projects/sections/ProjectContent.tsx +++ b/app/[lang]/projects/sections/ProjectContent.tsx @@ -235,6 +235,8 @@ export const ProjectContent = ({ + + {!isResearchProject && ( @@ -261,7 +263,6 @@ export const ProjectContent = ({
- diff --git a/app/api/articles/route.ts b/app/api/articles/route.ts index b37d0ae..3dca471 100644 --- a/app/api/articles/route.ts +++ b/app/api/articles/route.ts @@ -1,19 +1,34 @@ -import { NextResponse } from "next/server" +import { NextRequest, NextResponse } from "next/server" import { getArticles } from "@/lib/blog" -export async function GET(request: Request) { +// Cache control +export const revalidate = 60 // Revalidate cache after 60 seconds +export const dynamic = "force-dynamic" // Ensure the route is always evaluated + +export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url) - const project = searchParams.get("project") - const limit = searchParams.get("limit") - ? parseInt(searchParams.get("limit") || "100") - : undefined const tag = searchParams.get("tag") || undefined + const limit = searchParams.get("limit") + ? parseInt(searchParams.get("limit") as string, 10) + : undefined + const project = searchParams.get("project") || undefined - const articles = getArticles({ - project: project || undefined, - limit, - tag, - }) + try { + const articles = getArticles({ + tag, + limit, + project, + }) - return NextResponse.json({ articles }) + return NextResponse.json({ + articles, + success: true, + }) + } catch (error) { + console.error("Error fetching articles:", error) + return NextResponse.json( + { error: "Failed to fetch articles", success: false }, + { status: 500 } + ) + } } diff --git a/app/api/search/indexes/route.ts b/app/api/search/indexes/route.ts new file mode 100644 index 0000000..9222e50 --- /dev/null +++ b/app/api/search/indexes/route.ts @@ -0,0 +1,20 @@ +import { NextResponse } from "next/server" + +// These should be the same indexes used in the search route +// to ensure consistency +const allIndexes = ["blog", "projects"] + +export async function GET() { + try { + return NextResponse.json({ + indexes: allIndexes, + status: "success", + }) + } catch (error) { + console.error("Error fetching search indexes:", error) + return NextResponse.json( + { error: "Failed to fetch search indexes" }, + { status: 500 } + ) + } +} diff --git a/app/i18n/locales/en/research-page.json b/app/i18n/locales/en/research-page.json index a6e1d6f..bca07f6 100644 --- a/app/i18n/locales/en/research-page.json +++ b/app/i18n/locales/en/research-page.json @@ -1,8 +1,6 @@ { "title": "Research", - "subtitle": "Our research model is exploratory, iterative, and full-stack. We work on areas that are often overlooked by academia or industry — foundational concepts that need clarity, implementation gaps that have stalled progress, and risky ideas with uncertain but transformative potential. Sometimes our research unlocks a new breakthrough that leads to prototypes, and sometimes we just clarify assumptions and fill in theoretical gaps. - -We believe meaningful progress comes from applying research to real-world needs. Our work is open source, and we maintain a feedback loop so that challenges from the field can help shape our focus. We aim to support the broader ecosystem by systematizing knowledge, benchmarking new primitives, and offering credible guidance to advance the world of cryptography.", + "subtitle": "Our research model is exploratory, iterative, and full-stack. We work on areas that are often overlooked by academia or industry — foundational concepts that need clarity, implementation gaps that have stalled progress, and risky ideas with uncertain but transformative potential. Sometimes our research unlocks a new breakthrough that leads to prototypes, and sometimes we just clarify assumptions and fill in theoretical gaps. We believe meaningful progress comes from applying research to real-world needs. Our work is open source, and we maintain a feedback loop so that challenges from the field can help shape our focus. We aim to support the broader ecosystem by systematizing knowledge, benchmarking new primitives, and offering credible guidance to advance the world of cryptography.", "activeResearch": "Active Research", "pastResearch": "Past Research" } diff --git a/app/layout.tsx b/app/layout.tsx index e6a4ef2..fe3544c 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,14 +1,43 @@ import "@/styles/globals.css" -import { Metadata } from "next" +import { Metadata, Viewport } from "next" import { siteConfig } from "@/config/site" import { languages } from "./i18n/settings" import { QueryClientProviderLayout } from "@/components/layouts/QueryProviderLayout" +import { DM_Sans, Inter, Space_Grotesk } from "next/font/google" + +const inter = Inter({ + subsets: ["latin"], + variable: "--font-inter", + weight: ["400", "500", "600", "700"], +}) + +const display = Space_Grotesk({ + subsets: ["latin"], + variable: "--font-display", + weight: ["400", "500", "600", "700"], +}) + +const sans = DM_Sans({ + subsets: ["latin"], + variable: "--font-sans", + weight: ["400", "500", "600", "700"], +}) + +const fonts = [inter, display, sans] + export async function generateStaticParams() { return languages.map((language) => ({ language })) } +export const viewport: Viewport = { + width: "device-width", + initialScale: 1, + maximumScale: 1, + userScalable: false, +} + export const metadata: Metadata = { metadataBase: new URL("https://pse.dev"), title: { @@ -16,12 +45,6 @@ export const metadata: Metadata = { template: `%s - ${siteConfig.name}`, }, description: siteConfig.description, - viewport: { - width: "device-width", - initialScale: 1, - maximumScale: 1, - userScalable: false, - }, icons: { icon: "/favicon.svg", shortcut: "/favicon.svg", @@ -44,5 +67,11 @@ interface RootLayoutProps { } export default function RootLayout({ children }: RootLayoutProps) { - return {children} + return ( + + font.className).join(" ")}> + {children} + + + ) } diff --git a/components/blog/ArticlesList.tsx b/components/blog/ArticlesList.tsx new file mode 100644 index 0000000..c47e792 --- /dev/null +++ b/components/blog/ArticlesList.tsx @@ -0,0 +1,215 @@ +"use client" + +import { useQuery } from "@tanstack/react-query" +import { Article } from "@/lib/blog" +import { ArticleListCard } from "./article-list-card" +import { cn } from "@/lib/utils" +import Link from "next/link" +import { useTranslation } from "react-i18next" + +const ArticleInEvidenceCard = ({ + article, + size = "lg", + variant = "default", + className, + asLink = false, + titleClassName = "", + contentClassName = "", + showDate = true, +}: { + article: Article + showReadMore?: boolean + size?: "sm" | "lg" | "xl" + variant?: "default" | "compact" | "xl" + className?: string + asLink?: boolean + titleClassName?: string + contentClassName?: string + showDate?: boolean +}) => { + const { t } = useTranslation("blog-page") + + const hideTldr = variant === "compact" + + const formatDate = (dateString: string) => { + const date = new Date(dateString) + return date.toLocaleDateString("en-US", { + month: "long", + day: "numeric", + year: "numeric", + }) + } + + const AsLinkWrapper = ({ + children, + href, + asLink, + }: { + children: React.ReactNode + href: string + asLink: boolean + }) => { + return asLink ? ( + + {children} + + ) : ( + <>{children} + ) + } + + return ( + +
+
+ {article.date && showDate && ( + + {formatDate(article.date)} + + )} + + {article.title} + + + {article.authors?.join(", ")} + + {article.tldr && !hideTldr && ( + + )} +
+
+
+ ) +} + +async function fetchArticles(tag?: string) { + try { + const params = new URLSearchParams() + if (tag) params.append("tag", tag) + + const response = await fetch(`/api/articles?${params.toString()}`, { + cache: "default", + }) + + if (!response.ok) { + throw new Error(`Failed to fetch articles: ${response.status}`) + } + + const data = await response.json() + return data.articles || [] + } catch (error) { + console.error("Error fetching articles:", error) + return [] + } +} + +interface ArticlesListProps { + lang: string + tag?: string +} + +const ArticlesList = ({ lang, tag }: ArticlesListProps) => { + const { + data: articles = [], + isLoading, + isError, + } = useQuery({ + queryKey: ["articles", tag], + queryFn: () => fetchArticles(tag), + }) + + if (isLoading || articles.length === 0) { + return
Loading articles...
+ } + + if (isError) { + return ( +
+

Error loading articles

+
+ ) + } + + const lastArticle = articles[0] + const featuredArticles = articles.slice(1, 5) + const otherArticles = articles.slice(5) + + return ( +
+
+ +
+ {featuredArticles?.map((article: Article) => { + return ( + + ) + })} +
+
+
+ {otherArticles.map((article: Article) => { + return ( + + ) + })} +
+
+ ) +} + +export default ArticlesList diff --git a/components/blog/article-in-evidance-card.tsx b/components/blog/article-in-evidance-card.tsx new file mode 100644 index 0000000..84fe257 --- /dev/null +++ b/components/blog/article-in-evidance-card.tsx @@ -0,0 +1,139 @@ +"use client" + +import { Icons } from "../icons" +import { Button } from "../ui/button" +import { Article } from "@/lib/blog" +import { cn } from "@/lib/utils" +import Link from "next/link" +import { useTranslation } from "react-i18next" + +interface ArticleInEvidenceCardProps { + article: Article + showReadMore?: boolean + size?: "sm" | "lg" | "xl" + variant?: "default" | "compact" | "xl" + className?: string + asLink?: boolean + titleClassName?: string + contentClassName?: string + showDate?: boolean +} + +const AsLinkWrapper = ({ + children, + href, + asLink, +}: { + children: React.ReactNode + href: string + asLink: boolean +}) => { + return asLink ? ( + + {children} + + ) : ( + <>{children} + ) +} + +export const ArticleInEvidenceCard = async ({ + article, + showReadMore = false, + size = "lg", + variant = "default", + className, + asLink = false, + titleClassName = "", + contentClassName = "", + showDate = true, +}: ArticleInEvidenceCardProps) => { + const { t } = useTranslation("blog-page") + + const hideTldr = variant === "compact" + + const formatDate = (dateString: string) => { + const date = new Date(dateString) + return date.toLocaleDateString("en-US", { + month: "long", + day: "numeric", + year: "numeric", + }) + } + + return ( + +
+
+ {article.date && showDate && ( + + {formatDate(article.date)} + + )} + + {article.title} + + + {article.authors?.join(", ")} + + {article.tldr && !hideTldr && ( + + )} + {showReadMore && ( + + + + )} +
+
+
+ ) +} diff --git a/components/blog/article-list-card.tsx b/components/blog/article-list-card.tsx new file mode 100644 index 0000000..289b1ff --- /dev/null +++ b/components/blog/article-list-card.tsx @@ -0,0 +1,97 @@ +"use client" +import Link from "next/link" +import { Markdown } from "../ui/markdown" +import { Article } from "@/lib/blog" + +export const ArticleListCard = ({ + lang, + article, +}: { + lang: string + article: Article +}) => { + const url = `/${lang}/blog/${article.id}` + + const formattedDate = new Date(article.date).toLocaleDateString("en-US", { + month: "long", + day: "numeric", + year: "numeric", + }) + + return ( +
+ +
+
+
+
+ + {article.title} + + + {article.authors?.map((author) => author).join(", ")} + +
+ ( +

+ {children} +

+ ), + h2: ({ children }) => ( +

+ {children} +

+ ), + h3: ({ children }) => ( +

+ {children} +

+ ), + h4: ({ children }) => ( +

+ {children} +

+ ), + h5: ({ children }) => ( +
+ {children} +
+ ), + h6: ({ children }) => ( +
+ {children} +
+ ), + p: ({ children }) => ( +

+ {children} +

+ ), + img: ({ src, alt }) => null, + }} + > + {article.tldr || ""} +
+
+
+ + {formattedDate} + +
+
+ +
+ ) +} diff --git a/components/blog/blog-recent-articles.tsx b/components/blog/blog-recent-articles.tsx index bea0484..e8cc209 100644 --- a/components/blog/blog-recent-articles.tsx +++ b/components/blog/blog-recent-articles.tsx @@ -1,11 +1,138 @@ import { useTranslation } from "@/app/i18n" import { AppContent } from "../ui/app-content" -import { getArticles } from "@/lib/blog" +import { getArticles, Article } from "@/lib/blog" import Link from "next/link" import { cn } from "@/lib/utils" import { Button } from "../ui/button" import { Icons } from "../icons" +const ArticleInEvidenceCard = ({ + article, + showReadMore = false, + size = "lg", + variant = "default", + className, + asLink = false, + titleClassName = "", + contentClassName = "", + showDate = true, +}: { + article: Article + showReadMore?: boolean + size?: "sm" | "lg" | "xl" + variant?: "default" | "compact" | "xl" + className?: string + asLink?: boolean + titleClassName?: string + contentClassName?: string + showDate?: boolean +}) => { + const hideTldr = variant === "compact" + + const formatDate = (dateString: string) => { + const date = new Date(dateString) + return date.toLocaleDateString("en-US", { + month: "long", + day: "numeric", + year: "numeric", + }) + } + + const AsLinkWrapper = ({ + children, + href, + asLink, + }: { + children: React.ReactNode + href: string + asLink: boolean + }) => { + return asLink ? ( + + {children} + + ) : ( + <>{children} + ) + } + + return ( + +
+
+ {article.date && showDate && ( + + {formatDate(article.date)} + + )} + + {article.title} + + + {article.authors?.join(", ")} + + {article.tldr && !hideTldr && ( + + )} + {showReadMore && ( + + + + )} +
+
+
+ ) +} + export async function BlogRecentArticles({ lang }: { lang: any }) { const articles = getArticles({ limit: 4 }) const { t } = await useTranslation(lang, "blog-page") @@ -22,42 +149,13 @@ export async function BlogRecentArticles({ lang }: { lang: any }) {
-
-
- - {lastArticle.title} - - - {lastArticle.authors?.join(", ")} - - {lastArticle.tldr && ( - - {lastArticle.tldr} - - )} - - - -
-
+
diff --git a/components/blog/project-blog-articles.tsx b/components/blog/project-blog-articles.tsx index 2540dd7..5ee0a1e 100644 --- a/components/blog/project-blog-articles.tsx +++ b/components/blog/project-blog-articles.tsx @@ -4,9 +4,8 @@ import { LocaleTypes } from "@/app/i18n/settings" import { ProjectInterface } from "@/lib/types" import { AppContent } from "../ui/app-content" import { Article } from "@/lib/blog" -import Link from "next/link" -import { BlogArticleCard } from "./blog-article-card" import { useEffect, useState } from "react" +import { ArticleListCard } from "./article-list-card" async function fetchArticles(project: string) { const response = await fetch(`/api/articles?project=${project}`) @@ -14,47 +13,6 @@ async function fetchArticles(project: string) { return data.articles } -function ArticlesGrid({ - articles, - lang, -}: { - articles: Article[] - lang: string -}) { - return ( -
- {articles.length === 0 && ( -

- No articles found for this project. -

- )} - {articles.map( - ({ id, title, image, tldr = "", date, authors, content }: Article) => { - const url = `/${lang}/blog/${id}` - return ( -
- - - -
- ) - } - )} -
- ) -} - export const ProjectBlogArticles = ({ project, lang, @@ -86,10 +44,10 @@ export const ProjectBlogArticles = ({
-

+

Related articles

-
+
{[1, 2, 3].map((i) => (
@@ -108,14 +66,23 @@ export const ProjectBlogArticles = ({ return (
- -
-

- Related articles -

- +
+

+ Related articles +

+
+ {articles.length === 0 && ( +

+ No articles found for this project. +

+ )} + {articles.map((article: Article) => { + return ( + + ) + })}
- +
) } diff --git a/components/search/search-modal.tsx b/components/search/search-modal.tsx index 17226a6..0117c80 100644 --- a/components/search/search-modal.tsx +++ b/components/search/search-modal.tsx @@ -84,25 +84,18 @@ function Hit({ {snippet && ( ( -

{children}

- ), - h2: ({ children }) => ( -

{children}

- ), - h3: ({ children }) => ( -

{children}

- ), - h4: ({ children }) => ( -

{children}

- ), - h5: ({ children }) => ( -
{children}
- ), - h6: ({ children }) => ( -
{children}
- ), + h1: ({ children }) => null, + h2: ({ children }) => null, + h3: ({ children }) => null, + h4: ({ children }) => null, + h5: ({ children }) => null, + h6: ({ children }) => null, img: ({ src, alt }) => null, + a: ({ href, children }) => ( + + {children} + + ), }} > {snippet} @@ -316,9 +309,20 @@ const MultiIndexSearchView = ({ return } + // Sort indexes to ensure projects appear first, followed by blog + const sortedIndexes = [...visibleIndexes].sort((a, b) => { + if (a.toLowerCase().includes("project")) return -1 + if (b.toLowerCase().includes("project")) return 1 + + if (a.toLowerCase().includes("blog")) return 1 + if (b.toLowerCase().includes("blog")) return -1 + + return a.localeCompare(b) + }) + return (
- {visibleIndexes.map((indexName: string) => ( + {sortedIndexes.map((indexName: string) => (
{indexName.replace(/-/g, " ")} diff --git a/tailwind.config.js b/tailwind.config.js index be1d479..1e51695 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,6 +1,3 @@ -// eslint-disable-next-line no-undef, @typescript-eslint/no-require-imports -const { fontFamily } = require("tailwindcss/defaultTheme") - /** @type {import('tailwindcss').Config} */ // eslint-disable-next-line no-undef module.exports = { @@ -102,8 +99,9 @@ module.exports = { sm: "calc(var(--radius) - 4px)", }, fontFamily: { - sans: ["var(--font-sans)", ...fontFamily.sans], - display: ["var(--font-display)", "Space Grotesk"], + sans: ["var(--font-sans)"], + display: ["var(--font-display)"], + inter: ["var(--font-inter)"], }, keyframes: { "accordion-down": {