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
89 changes: 89 additions & 0 deletions apps/website/app/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import fs from "fs/promises";
import path from "path";
import { notFound } from "next/navigation";
import { Metadata } from "next";
import { getBlog } from "../readBlogs";

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

export default async function BlogPost({ params }: Params) {
try {
const { slug } = await params;
const { data, contentHtml } = await getBlog(slug);

return (
<div className="flex flex-1 flex-col items-center bg-gray-50 px-6 py-12">
<div className="w-full max-w-4xl">
<header className="mb-8 text-center">
<h1 className="mb-4 text-5xl font-bold leading-tight text-primary">
{data.title}
</h1>
<p className="text-sm italic text-gray-500">
By {data.author} • {data.date}
</p>
</header>
<article
className="prose prose-lg lg:prose-xl prose-gray mx-auto leading-relaxed text-gray-700"
dangerouslySetInnerHTML={{ __html: contentHtml }}
/>
</div>
</div>
);
} catch (error) {
console.error("Error rendering blog post:", error);
return notFound();
}
}

export async function generateStaticParams() {
try {
const blogPath = path.join(process.cwd(), "app/blog/posts");
// 1) Check if the directory exists
const directoryExists = await fs
.stat(blogPath)
.then((stats) => stats.isDirectory())
.catch(() => false);

// 2) If it's missing, return empty
if (!directoryExists) {
console.log(
"No app/blog/posts directory found. Returning empty params...",
);
return [];
}

// 3) If it exists, read it
const files = await fs.readdir(blogPath);

// 4) Filter .md files to build the slug array
return files
.filter((filename) => filename.endsWith(".md"))
.map((filename) => ({
slug: filename.replace(/\.md$/, ""),
}));
} catch (error) {
console.error("Error generating static params:", error);
return [];
}
}

export async function generateMetadata({ params }: Params): Promise<Metadata> {
try {
const { slug } = await params;
const { data } = await getBlog(slug);

return {
title: data.title,
authors: [{ name: data.author }],
};
} catch (error) {
console.error("Error generating metadata:", error);
return {
title: "Blog Post",
};
}
}
42 changes: 42 additions & 0 deletions apps/website/app/blog/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Link from "next/link";
import { getAllBlogs } from "./readBlogs";

export default async function BlogIndex() {
const blogs = await getAllBlogs();
return (
<div className="flex-1 bg-gray-50">
<div className="mx-auto max-w-6xl space-y-12 px-6 py-12">
<div className="rounded-xl bg-white p-8 shadow-md">
<div className="mb-8">
<h1 className="text-4xl font-bold text-primary">All Updates</h1>
</div>
<div>
<ul className="space-y-6">
{blogs.map((blog) => (
<li
key={blog.slug}
className="flex items-start justify-between border-b border-gray-200 pb-4 last:border-b-0"
>
<div className="w-4/5">
<Link
href={`/blog/${blog.slug}`}
className="block text-2xl font-semibold text-blue-600 hover:underline"
>
{blog.title}
</Link>
<p className="mt-2 text-sm italic text-gray-500">
{blog.date}
</p>
</div>
<div className="w-1/5 text-right text-gray-600">
by {blog.author}
</div>
</li>
))}
</ul>
</div>
</div>
</div>
</div>
);
}
33 changes: 33 additions & 0 deletions apps/website/app/blog/posts/EXAMPLE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
title: "Example Post - How to Create Blog Posts"
date: "2024-01-01"
author: "Devs"
published: false # Set to true to make this post visible
---

# How to Create Blog Posts

This is a template post that shows how to create new blog posts. Keep this file as a reference and create new posts by following these instructions.

### Creating Blog Posts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea to put the instructions here 🔥


1. [Fork](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo) and [clone](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository) the repository.
2. [Create a new branch](https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/managing-branches) for your update.
3. Copy this file (`EXAMPLE.md`) to create a new post
4. Rename the file to your desired URL slug (e.g., `my-new-post.md`)
5. Update the frontmatter and content
6. Set `published: true` when ready
7. Push your branch and open a pull request. 🚀

### Required Frontmatter

Every post must start with frontmatter in YAML format:

```yaml
---
title: "Your Post Title"
date: "YYYY-MM-DD"
author: "Author's name"
published: true # Set to true to make the post visible
---
```
90 changes: 90 additions & 0 deletions apps/website/app/blog/readBlogs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { remark } from "remark";
import html from "remark-html";
import { notFound } from "next/navigation";
import path from "path";
import fs from "fs/promises";
import matter from "gray-matter";
import { BlogSchema, type Blog, BlogFrontmatter } from "./schema";

const BLOG_DIRECTORY = path.join(process.cwd(), "app/blog/posts");

async function validateBlogDirectory(): Promise<boolean> {
try {
const stats = await fs.stat(BLOG_DIRECTORY);
return stats.isDirectory();
} catch {
console.log("No app/blog/posts directory found.");
return false;
}
}

async function processBlogFile(filename: string): Promise<Blog | null> {
try {
const filePath = path.join(BLOG_DIRECTORY, filename);
const fileContent = await fs.readFile(filePath, "utf-8");
const { data } = matter(fileContent);
const validatedData = BlogSchema.parse(data);

return {
slug: filename.replace(/\.md$/, ""),
...validatedData,
};
} catch (error) {
console.error(`Error processing blog file ${filename}:`, error);
return null;
}
}

async function getMarkdownContent(content: string): Promise<string> {
const processedContent = await remark().use(html).process(content);
return processedContent.toString();
}

export async function getAllBlogs(): Promise<Blog[]> {
try {
const directoryExists = await validateBlogDirectory();
if (!directoryExists) return [];

const files = await fs.readdir(BLOG_DIRECTORY);
const blogs = await Promise.all(
files.filter((filename) => filename.endsWith(".md")).map(processBlogFile),
);
const validBlogs = blogs.filter(Boolean) as Blog[];
return validBlogs.filter((blog) => blog.published);
} catch (error) {
console.error("Error reading blog directory:", error);
return [];
}
}

export async function getBlog(
slug: string,
): Promise<{ data: BlogFrontmatter; contentHtml: string }> {
try {
const filePath = path.join(BLOG_DIRECTORY, `${slug}.md`);
await fs.access(filePath);

const fileContent = await fs.readFile(filePath, "utf-8");
const { data: rawData, content } = matter(fileContent);
const data = BlogSchema.parse(rawData);

if (!data.published) {
console.log(`Post ${slug} is not published`);
return notFound();
}

const contentHtml = await getMarkdownContent(content);

return { data, contentHtml };
} catch (error) {
console.error("Error loading blog post:", error);
return notFound();
}
}

export async function getLatestBlogs(): Promise<Blog[]> {
const blogs = await getAllBlogs();
return blogs
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
.slice(0, 3);
}
14 changes: 14 additions & 0 deletions apps/website/app/blog/schema.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { z } from "zod";

export const BlogSchema = z.object({
title: z.string(),
date: z.string(),
author: z.string(),
published: z.boolean().default(false),
});

export type BlogFrontmatter = z.infer<typeof BlogSchema>;

export type Blog = BlogFrontmatter & {
slug: string;
};
49 changes: 48 additions & 1 deletion apps/website/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import type { Metadata } from "next";
import "./globals.css";
import { PostHogProvider } from "./providers";
import Link from "next/link";
import Image from "next/image";
import { Inter } from "next/font/google";

export const metadata: Metadata = {
title: "Discourse Graphs | A Tool for Collaborative Knowledge Synthesis",
Expand All @@ -18,6 +21,8 @@ export const metadata: Metadata = {
},
};

const inter = Inter({ subsets: ["latin"] });

export default function RootLayout({
children,
}: Readonly<{
Expand All @@ -26,7 +31,49 @@ export default function RootLayout({
return (
<html lang="en">
<body className={`antialiased`}>
<PostHogProvider>{children}</PostHogProvider>
<PostHogProvider>
<div
className={`flex min-h-screen flex-col bg-neutral-light ${inter.className}`}
>
<header className="flex flex-col items-center justify-between space-y-4 px-6 py-4 md:flex-row md:space-y-0">
<Link href="/" className="flex items-center space-x-2">
<Image
src="/logo-screenshot-48.png"
alt="Discourse Graphs Logo"
width={48}
height={48}
/>

<span className="text-3xl font-bold text-neutral-dark">
Discourse Graphs
</span>
</Link>
Comment on lines +38 to +50
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add aria-label to improve logo link accessibility

The logo link should have an aria-label for better accessibility.

-              <Link href="/" className="flex items-center space-x-2">
+              <Link href="/" className="flex items-center space-x-2" aria-label="Home">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<header className="flex flex-col items-center justify-between space-y-4 px-6 py-4 md:flex-row md:space-y-0">
<Link href="/" className="flex items-center space-x-2">
<Image
src="/logo-screenshot-48.png"
alt="Discourse Graphs Logo"
width={48}
height={48}
/>
<span className="text-3xl font-bold text-neutral-dark">
Discourse Graphs
</span>
</Link>
<header className="flex flex-col items-center justify-between space-y-4 px-6 py-4 md:flex-row md:space-y-0">
<Link href="/" className="flex items-center space-x-2" aria-label="Home">
<Image
src="/logo-screenshot-48.png"
alt="Discourse Graphs Logo"
width={48}
height={48}
/>
<span className="text-3xl font-bold text-neutral-dark">
Discourse Graphs
</span>
</Link>

<nav className="w-full md:w-auto">
<ul className="flex flex-wrap justify-center space-x-4 md:flex-nowrap md:space-x-8">
{[
"About",
"Resources",
"Events",
"Updates",
"Talks",
"Supporters",
"Contact",
].map((item) => (
<li key={item}>
<Link
href={`/#${item.toLowerCase()}`} // Ensures absolute path with root `/`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Hash-based navigation implementation needs adjustments

The verification revealed both positive and concerning aspects:

  1. ✅ Smooth scrolling is properly implemented in apps/website/app/globals.css with scroll-smooth
  2. ✅ Section IDs exist in apps/website/app/page.tsx as confirmed by the ID pattern search
  3. ❌ No fixed header or scroll offset implementation was found, which might affect the user experience when navigating to sections

Recommendations:

  • Add a scroll margin top to section elements to prevent content from being hidden behind the header (if header is fixed)
  • Consider implementing a fixed header with appropriate z-index and backdrop if not already present
🔗 Analysis chain

Verify hash-based navigation implementation

The current implementation uses hash-based navigation (/#section). Ensure that:

  1. Scroll behavior is smooth
  2. Sections are properly marked with corresponding IDs
  3. Consider implementing scroll offset to account for fixed header
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check if sections with corresponding IDs exist
rg -l "id=[\"\'](?:about|resources|events|updates|talks|supporters|contact)[\"\']" --type html --type tsx

Length of output: 255


Script:

#!/bin/bash
# Check if sections with corresponding IDs exist, fixing the regex
rg -l "id=['\"](?:about|resources|events|updates|talks|supporters|contact)['\"]" --type html --type tsx

# Check for scroll behavior implementation
rg "scroll" --type css --type tsx -A 2

# Check for any scroll offset or header-related adjustments
rg "header.*fixed|position:\s*fixed" --type css --type tsx -A 2

Length of output: 298


Script:

#!/bin/bash
# Check if sections with corresponding IDs exist
rg -l "id=['\"](?:about|resources|events|updates|talks|supporters|contact)['\"]"

# Check for scroll behavior implementation
rg "scroll" -A 2

# Check for any scroll offset or header-related adjustments
rg "header.*fixed|position:\s*fixed" -A 2

# Look for any TypeScript/React files that might contain the sections
fd "\.(tsx|ts|jsx|js)$"

Length of output: 12941

className="text-neutral-dark hover:text-neutral-dark/60"
>
{item}
</Link>
</li>
))}
</ul>
</nav>
</header>
{children}
</div>
</PostHogProvider>
</body>
</html>
);
Expand Down
Loading