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
210 changes: 210 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
# AGENTS.md

<!-- BEGIN:nextjs-agent-rules -->

# This is NOT the Next.js you know

This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.

This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.

<!-- END:nextjs-agent-rules -->

Instructions for AI coding agents working on this repository.

## Project Overview

**ossium-blog** — the official blog for [Ossium](https://ossium.in), a static-first Next.js site powered by Markdown posts. Live at [blog.ossium.in](https://blog.ossium.in).

| Layer | Choice |
| --------------- | ---------------------------------------------- |
| Framework | Next.js 16 (App Router, static generation) |
| UI | React 19 + Tailwind CSS v4 |
| Language | TypeScript (strict) |
| Package Manager | pnpm 11.15.1 |
| Content | Markdown + YAML frontmatter (`content/posts/`) |
| Testing | Vitest + Testing Library |
| Linting | ESLint 9 (flat config) + Prettier |

## Essential Commands

Run from the project root. All must pass before committing.

```bash
pnpm install # install dependencies
pnpm dev # dev server at http://localhost:3000
pnpm build # production build (SSG)
pnpm lint # ESLint
pnpm typecheck # tsc --noEmit
pnpm format # format with Prettier
pnpm format:check # check formatting (CI)
pnpm test # run Vitest once
pnpm test:watch # Vitest in watch mode
```

Full verification before pushing:

```bash
pnpm lint && pnpm typecheck && pnpm format:check && pnpm test && pnpm build
```

## Code Conventions

- **TypeScript strict mode** — no `any`, no `@ts-ignore`.
- **Path alias** — use `@/` for all imports (maps to project root).
- **No `console.log`** in committed code.
- **No comments** unless explicitly requested.
- **No commented-out code** or unresolved `TODO`s — file an issue instead.
- **Prettier** handles formatting — double quotes, semicolons, 80 cols, 2-space indent, trailing commas.
- **Match surrounding code style** — mimic existing patterns in the file you're editing.

## File Structure

```
app/ # Next.js App Router pages, layouts, API routes, SEO files
[slug]/page.tsx # Individual blog post pages (SSG)
BlogListing.tsx # Client component: search, filters, featured rail
layout.tsx # Root layout (Navbar, Footer, theme, metadata)
feed.xml/route.ts # RSS 2.0 feed
sitemap.ts # Auto-generated sitemap
robots.ts # Auto-generated robots.txt
components/
blog/ # PostCard, MarkdownBody (client fallback), ShareButton
landing/ # Navbar, Footer, DashedFrame
theme/ # ThemeToggle (light/dark, persisted)
ui/ # Container, SearchOverlay
content/posts/ # Markdown blog posts with YAML frontmatter
lib/
content/ # Content pipeline: load, parse, related posts, types
content/markdown.ts # Server-side markdown renderer (unified/remark/rehype)
markdown/ # Directives plugin: :::gallery + preprocessor (directives.ts)
constants.ts # APP_URL, APP_CONFIG, SOURCE_EDIT_BASE
utils.ts # cn() classname helper
public/ # Static assets (logos, demo images, llms.txt)
```

### Rendering Pipeline

Blog post markdown is rendered **server-side at build time** (SSG) using the unified/remark/rehype pipeline in `lib/content/markdown.ts`. The generated HTML is injected via `dangerouslySetInnerHTML` in `[slug]/page.tsx`. `components/blog/MarkdownBody.tsx` is kept as a client-side fallback but is not currently used by the main page.

## Content Guidelines

### Post Location

All posts live in `content/posts/` as `.md` files. The filename slug becomes the URL path.

### Frontmatter Schema

```yaml
---
title: "Clear, keyword-aware title"
description: "1-2 sentences for SEO (~150-160 chars)."
type: article | guide | howto | question
category: contribution | gsoc | github | career | tools | programs | beginners
tags: [tag-one, tag-two]
keywords: [seo keyword one, seo keyword two]
publishedAt: 2026-07-11
updatedAt: 2026-07-11
author: ossium
authorUrl: https://ossium.in
featured: false
draft: false
image: /demo/image.webp
answerSummary: "Only for type=question — short answer for FAQ rich results."
---
```

### Post Types

| Type | Use Case |
| ---------- | --------------------------------- |
| `article` | General blog posts |
| `guide` | Step-by-step guides |
| `howto` | How-to tutorials |
| `question` | Q&A posts (generates FAQ JSON-LD) |

### Categories

`contribution`, `gsoc`, `github`, `career`, `tools`, `programs`, `beginners`

### Key Rules

- Set `draft: true` until the post is ready to publish.
- Internal links use relative paths (`/other-post-slug`).
- Content in `content/posts/` is **not MIT licensed** — it is © Ossium, all rights reserved.

### Images and Galleries

Single image — the alt text renders as the caption:

```md
![Description](/images/1.jpg)
```

Gallery (`columns` optional, defaults to 2; supported values: 2, 3, 4):

```md
:::gallery columns=3

![](/images/1.jpg)
![Caption](/images/2.jpg)

:::
```

- Images live in `public/`, referenced from the root (`/images/1.jpg`); remote URLs are supported.
- Galleries render as a responsive grid: 1 column on mobile, the requested count from 641px.
- Directive attributes may be written bare (`columns=3`) or braced (`{columns=3}`); `preprocessDirectives` in `lib/markdown/directives.ts` normalizes the bare form.
- Do not use raw HTML for layout — prefer directives (`:::gallery`), which are extensible for future `note`, `warning`, and `tabs` blocks.

## Testing

- Framework: **Vitest** with `@testing-library/react` and `jsdom`.
- Test files: `lib/**/*.test.ts` and `components/**/*.test.ts(x)`.
- Pure logic in `lib/` is the primary testing target.
- Run `pnpm test` to execute, `pnpm test:watch` for development.
- New code should include tests where practical.

## Git Workflow

### Branch Naming

| Kind | Prefix | Example |
| ------- | ---------- | ---------------------------- |
| Feature | `feat/` | `feat/reading-progress-bar` |
| Bug fix | `fix/` | `fix/search-highlight-flash` |
| Content | `content/` | `content/gsoc-2027-timeline` |
| Chore | `chore/` | `chore/upgrade-deps` |
| Docs | `docs/` | `docs/contributing-typo` |

### Commit Messages

Use [Conventional Commits](https://www.conventionalcommits.org/):

```
<type>(<optional scope>): <subject>
```

Types: `feat`, `fix`, `content`, `docs`, `refactor`, `chore`, `test`, `ci`, `perf`, `style`.

- Subject ≤ 72 characters, imperative mood, lowercase (unless acronym).

## Verification Checklist

Before committing or pushing, confirm:

- [ ] `pnpm lint` passes with no errors
- [ ] `pnpm typecheck` passes with no errors
- [ ] `pnpm format:check` passes (or run `pnpm format` to fix)
- [ ] `pnpm test` passes
- [ ] `pnpm build` succeeds
- [ ] No `console.log` statements in new code
- [ ] No secrets, API keys, or credentials committed
- [ ] Path alias `@/` used for imports (not relative `../../`)

## Security

- This is a **static site** — no database, no server-side state, no user input handling.
- Security headers are set in `next.config.ts` (CSP, X-Frame-Options: DENY, etc.).
- Never commit `.env` files — only `.env.example` is tracked.
- Report vulnerabilities via the process in `SECURITY.md`.
6 changes: 6 additions & 0 deletions app/BlogListing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ export default function BlogListing({
PostCategory | undefined
>(initialCategory);

const [prevInitialQuery, setPrevInitialQuery] = useState(initialQuery);
if (initialQuery !== prevInitialQuery) {
setPrevInitialQuery(initialQuery);
setQuery(initialQuery ?? "");
}
Comment thread
paras-verma7454 marked this conversation as resolved.

const types = useMemo(
() => Array.from(new Set(posts.map((p) => p.type))),
[posts],
Expand Down
19 changes: 14 additions & 5 deletions app/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import Image from "next/image";
import Link from "next/link";
import { notFound } from "next/navigation";
import { ArrowLeft, Clock, Coffee, Pencil, Tag } from "lucide-react";
import MarkdownBody from "@/components/blog/MarkdownBody";
import PostCard from "@/components/blog/PostCard";
import ShareButton from "@/components/blog/ShareButton";
import {
Expand All @@ -14,6 +13,7 @@ import {
CATEGORY_LABELS,
} from "@/lib/content";
import { formatDate } from "@/lib/utils";
import { renderMarkdownToHtml } from "@/lib/content/markdown";
import { APP_URL, APP_CONFIG, SOURCE_EDIT_BASE } from "@/lib/constants";
import { cn } from "@/lib/utils";
import { SECTION_CONTAINER_CLASS } from "@/components/ui/Container";
Expand Down Expand Up @@ -92,6 +92,10 @@ export async function generateMetadata({
};
}

function safeJsonLd(obj: object): string {
return JSON.stringify(obj).replace(/</g, "\\u003c");
}

/** Full article page - cover, prose body, tags, share, and related posts. */
export default async function BlogPostPage({ params }: PageProps) {
const { slug } = await params;
Expand Down Expand Up @@ -177,16 +181,16 @@ export default async function BlogPostPage({ params }: PageProps) {
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(articleJsonLd) }}
dangerouslySetInnerHTML={{ __html: safeJsonLd(articleJsonLd) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbJsonLd) }}
dangerouslySetInnerHTML={{ __html: safeJsonLd(breadcrumbJsonLd) }}
/>
{faqJsonLd && (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }}
dangerouslySetInnerHTML={{ __html: safeJsonLd(faqJsonLd) }}
/>
)}

Expand Down Expand Up @@ -302,7 +306,12 @@ export default async function BlogPostPage({ params }: PageProps) {
</div>
)}

<MarkdownBody content={post.content} />
<div
className="blog-prose text-neutral-600 dark:text-neutral-300 text-[16px] sm:text-[17px] leading-[1.75] tracking-[-0.003em]"
dangerouslySetInnerHTML={{
__html: renderMarkdownToHtml(post.content),
}}
/>

{post.tags.length > 0 && (
<div className="mt-12 pt-8 border-t border-neutral-200 dark:border-neutral-800">
Expand Down
1 change: 1 addition & 0 deletions app/feed.xml/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export async function GET() {
const posts = getAllPosts();

const items = posts
.filter((post) => !Number.isNaN(new Date(post.publishedAt).getTime()))
.map((post) => {
const link = `${APP_URL}/${post.slug}`;
const pubDate = new Date(post.publishedAt).toUTCString();
Expand Down
Loading
Loading