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
17 changes: 14 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@ All blog, portfolio, and research entries are MDX files on disk — there's no C
- `Mermaid` — renders ` ```mermaid ` blocks; mermaid + stylis are transpiled and pre-bundled (see `transpilePackages` and `experimental.optimizePackageImports` in `next.config.js`).
- `ContentImage` — explicit component for ImageKit-hosted images. The default `img` MDX mapping was intentionally removed so external images / SVGs in MDX don't break; use `<ContentImage>` (or a raw `<img>`) explicitly.
- `MDXLink`, `Blockquote`, `Table`, `Headings` — styled overrides.
6. **Research-specific components** live in `src/components/content/research/`: `HeadingResearch` (status chip, title, structured authors + affiliations, venue, action-button row, hr), `ResearchTeaser` (full-bleed figure + caption, lives outside `prose` so it can breathe), `ResearchAbstract` (purple-accented `not-prose` card), `ResearchBibTeX` (anchored at `#bibtex`, copy-to-clipboard, mirrors `Pre.tsx` chrome), `ResearchItem` + `ResearchSections` (listing card + section grouping). The detail page composition is in `src/app/research/[slug]/page.tsx`.
6. **Research-specific components** live in `src/components/content/research/`:
- **Detail-page**: `HeadingResearch` (status chip, title, structured authors + affiliations, venue, action-button row, hr — author/affiliation `<sup>` markers are conditional + carry `title` tooltips, see Conventions), `ResearchTeaser` (full-bleed figure + caption, lives outside `prose` so it can breathe), `ResearchAbstract` (purple-accented `not-prose` card), `ResearchBibTeX` (anchored at `#bibtex`, copy-to-clipboard, mirrors `Pre.tsx` chrome).
- **Listing-page**: `ResearchOverview` (intro paragraph), `ResearchNews` (date-column timeline; entries live in the `NEWS` array inside the file), `ResearchAreas` (colored chip row), `ResearchItem` + `ResearchSections` (listing card + section grouping). `ComingSoonImage` is a pastel-gradient placeholder used in place of the listing thumbnail when an entry has `comingSoon: true`.
- **Shared**: `SectionHeading` is the bordered `text-lg md:text-xl font-bold text-black dark:text-white` h2 used by every section on the listing page (Overview / News / Research Areas / Sections groupers). Use it for any new sections to keep the rhythm consistent.
- The detail page composition is in `src/app/research/[slug]/page.tsx`. The listing page is in `src/app/research/page.tsx` — both pass `pt-6 md:pt-10` to `AppLayoutPage` to give the first section breathing room from the sticky header.
7. **SEO** is generated per-page in `src/app/blog/[slug]/page.tsx` and the portfolio / research equivalents: `generateMetadata` builds Next.js Metadata (OG, Twitter, canonical, `modifiedTime`), and the page itself injects a single `@graph` JSON-LD block:
- Blog: `BlogPosting` + `WebPage` + `BreadcrumbList`
- Portfolio: `SoftwareSourceCode` + `WebPage` + `BreadcrumbList`
Expand Down Expand Up @@ -86,7 +90,7 @@ Two layers feed Tailwind v4:

Net result: `bg-primary` (shadcn) and `bg-primary-500` (legacy) render the same color; both surfaces remain valid during the transition. When adding new code, prefer the semantic tokens. Don't write `dark:` overrides for shadcn classes — `--background` etc. switch automatically via `.dark`.

Fonts: `--font-sans` resolves to `var(--font-inter)` (Next/font Google) with the local `@font-face` Inter as a fallback. The legacy `font-primary` utility still maps to Inter via `tailwind.config.js`.
Fonts: `--font-sans` resolves to `'Google Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'`. Google Sans is loaded via Google Fonts CSS (`<link>` in `layout.tsx` head, with `preconnect` to `fonts.gstatic.com` — `font-src` in `next.config.js` CSP must include it). Local Inter `@font-face` stays in `globals.css` as a deeper fallback so the page never FOUCs. `tailwind.config.js` `fontFamily.primary` mirrors the same stack so the legacy `font-primary` utility picks up Google Sans too.

## Two Build Modes

Expand Down Expand Up @@ -123,7 +127,14 @@ ISR endpoint at `GET /api/revalidate?secret=<SECRET>&slug=/blog/<slug>`. Secret
- Commits go through `commitizen` with `cz-conventional-changelog` — run `yarn commit` for the prompt. `lint-staged` runs Prettier on staged `js/jsx/ts/tsx/mdx/md/css` files.
- Frontmatter `published` for blog and research is `MM/DD/YYYY` (portfolio uses `date`). `featured: true` surfaces an entry on the home page. Optional `updated: "MM/DD/YYYY"` overrides `published` as the freshness signal — it flows into `dateModified` (BlogPosting / ScholarlyArticle JSON-LD), `<meta property="article:modified_time">`, and the sitemap `lastmod`. Set it whenever you make a substantive content edit; leave it off for typo fixes.
- Authoring convention: open every blog post with a 60–120 word TL;DR paragraph immediately under the H1. LLM crawlers (ChatGPT, Perplexity, AI Overviews) draw ~44% of their citations from the first 30% of an article, so front-loading the answer compounds visibility.
- **Research authoring**: use the YAML folded scalar (`abstract: >-`) — block scalar (`|`) preserves newlines and the abstract renders with broken lines. Authors use a structured array; `affiliations` indices are 1-based and reference the entry's `affiliations` array. `section` drives listing-page grouping (`top-tier` / `conferences` / `journals` / `workshops` / `others`). `new: true` renders a NEW badge inline with the title on the listing card. Authors matching `SITE_AUTHOR.name` are auto-bolded across `HeadingResearch` and `ResearchItem`.
- **Research authoring**: use the YAML folded scalar (`abstract: >-`) — block scalar (`|`) preserves newlines and the abstract renders with broken lines. Authors use a structured array; `affiliations` indices are 1-based and reference the entry's `affiliations` array. `section` drives listing-page grouping (`top-tier` / `conferences` / `journals` / `workshops` / `others`). `new: true` renders a NEW badge inline with the title on the listing card. `comingSoon: true` swaps the listing thumbnail for the `ComingSoonImage` pastel placeholder (no thumbnail/teaser required). Authors matching `SITE_AUTHOR.name` are auto-bolded across `HeadingResearch` and `ResearchItem`.
- **Author / affiliation superscripts** in `HeadingResearch` are conditional. Each marker only renders when it actually disambiguates something:
- `*` (corresponding author) — `corresponding: true` on the author. Shows when **2+ authors** AND at least one is corresponding.
- `†` (equal contribution) — `equalContribution: true` on the author. Shows when **2+ authors** AND at least 2 are flagged. Renders on every flagged author.
- `‡` (principal investigator) — `principalInvestigator: true` on the author. Shows when **2+ authors** AND at least one is PI.
- `¹ ² ³` (affiliation indices) — derived from `affiliations: [...]` on each author. Shows when **2+ affiliations** exist on the entry.

Marker order on each author follows academic convention: `*†‡` then numeric indices. The affiliation legend below the author line drops its leading `<sup>` under the same `showAffSup` rule. Single-author / single-affiliation entries collapse to clean text — no orphan markers. Each `<sup>` has `cursor-help` + a native `title` tooltip (affiliation index sups resolve to the full affiliation name; symbol sups resolve to their caption text). A combined caption line appears below the affiliation row when any symbol marker is shown — e.g. `*Corresponding author · †Equal contribution · ‡Principal investigator` — joined by ` · `.

## Indexing Helper

Expand Down
45 changes: 39 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ Use it as your portfolio. Fork it as a template. Read the source as a reference
- **Topic archive pages** auto-generated from frontmatter `topics`.
- **RSS** at `/rss.xml` (summaries) and `/rss-full.xml` (full content) with `<link rel="alternate">` discovery in `<head>`.

### Design system

- **shadcn/ui** (radix-nova style) as the single primitive layer. Components live under `src/components/ui/` and are managed by the shadcn CLI (`npx shadcn@latest add <name>`); rerun with `--diff` to merge upstream updates without losing local edits.
- **Theme bridge** in `src/styles/globals.css` `@theme inline` — shadcn semantic tokens (`--primary`, `--background`, `--foreground`, `--muted`, `--border`, `--ring`) map onto the legacy blue-primary / neutral-theme palette in OKLCH so legacy classes (`bg-primary-500`, `text-theme-700`) and shadcn classes (`bg-primary`, `text-foreground`) render identically across light/dark.
- **Site composition components** under `src/components/site/` (Header, Footer, Nav, Hero, Searchbar, BackToTop, MobileNav, ThemeMenu, link/image wrappers, AppLayoutPage) — bespoke pieces that compose shadcn primitives + project state.
- **Single primitive ecosystem** — `radix-ui` (shadcn's base), `lucide-react` (icons inside shadcn), `sonner` (toasts), `tw-animate-css` (animations), `class-variance-authority` (variants), `tailwind-merge` (className merging).
- **Typography** — Google Sans loaded from Google Fonts with a system-font fallback chain (`system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, …`). Local Inter `@font-face` is kept as a deeper fallback so the page never FOUCs.

### Performance

- **Core Web Vitals attribution** for CLS, LCP, **INP**, FCP, TTFB via Vercel Speed Insights.
Expand All @@ -80,13 +88,15 @@ Use it as your portfolio. Fork it as a template. Read the source as a reference

### Tooling

- **TypeScript** strict-mode, `@/*` path aliases.
- **TypeScript** strict-mode, `@/*` path alias.
- **ESLint flat config** (`eslint.config.mjs`, replacing the deprecated `next lint`).
- **Vitest** suite (48 tests, ~1.7s) covering schema generators, sorters, content readers.
- **Prettier** with `@ianvs/prettier-plugin-sort-imports` (actively maintained successor to `@trivago`) + `prettier-plugin-tailwindcss`.
- **shadcn CLI** for primitive sync — `npx shadcn@latest add`, `--diff`, `--dry-run`.
- **Vitest** suite (48 tests, ~1.7s) covering schema generators, sorters, content readers. Vite resolves `@/*` paths via the native `resolve.tsconfigPaths: true` (no plugin).
- **JSON-LD validator** (`yarn validate:json-ld`) walks the build output and verifies every `<script type="application/ld+json">` block.
- **Alt-text auditor** (`yarn audit:alt-text`) flags weak `alt` attributes in MDX.
- **GitHub Actions CI**: type-check + lint + test + build + JSON-LD validation on every push/PR.
- **Yarn 4** via Corepack, with a `vercel.json` `installCommand` so deploy hosts honour the lockfile.
- **Yarn 4** via Corepack, with a `vercel.json` `installCommand` (`corepack enable && corepack prepare yarn@4.14.1 --activate && yarn install --immutable`) so deploy hosts actually run Yarn 4 and honour the lockfile.

---

Expand All @@ -97,9 +107,12 @@ Use it as your portfolio. Fork it as a template. Read the source as a reference
| Framework | Next.js 16 (App Router) + React 19 |
| Language | TypeScript |
| Styling | Tailwind CSS v4 (with `@tailwindcss/typography`) |
| Design system | shadcn/ui (radix-nova) on `radix-ui` |
| Icons | `lucide-react` (shadcn) + `react-icons` (legacy site components) |
| Toasts | `sonner` |
| Fonts | Google Sans (Google Fonts) + Inter (local @font-face fallback) |
| Content | MDX (`next-mdx-remote/rsc`) + `gray-matter` |
| Markdown plugins | remark-gfm, remark-math, rehype-prism-plus, rehype-slug, rehype-katex |
| Animation | framer-motion |
| Comments | Giscus (deferred) |
| Diagrams | Mermaid (lazy) |
| Math | KaTeX |
Expand All @@ -111,6 +124,7 @@ Use it as your portfolio. Fork it as a template. Read the source as a reference
| PWA | @ducanh2912/next-pwa (Workbox 7) |
| Testing | Vitest + happy-dom + @testing-library/react |
| Lint | ESLint 9 (flat config) + eslint-config-next |
| Format | Prettier 3 + `@ianvs/prettier-plugin-sort-imports` |
| Package manager | Yarn 4 (Corepack) |
| Hosting | Vercel (primary) + static export for any CDN |

Expand Down Expand Up @@ -177,7 +191,10 @@ src/
│ ├── rss.xml/route.ts # RSS summary feed
│ └── rss-full.xml/route.ts # RSS full-content feed
├── components/ # UI + content components
│ ├── ui/ # shadcn/ui primitives (managed by `npx shadcn@latest add`)
│ ├── site/ # Bespoke composition components (Header, Footer, Nav, AppLayoutPage, Hero, Searchbar, BackToTop, …)
│ ├── content/mdx/ # MDX overrides (Pre, Code, ContentImage, TLDR, FAQ, HowTo, …)
│ ├── content/research/ # Research-page sections (HeadingResearch, ResearchOverview, ResearchNews, ResearchAreas, ResearchSections, ComingSoonImage, SectionHeading, …)
│ └── SEO/Breadcrumbs.tsx
├── data/
│ ├── blog/*.mdx # Blog posts (slug = filename)
Expand Down Expand Up @@ -320,18 +337,21 @@ authors:
email: 'aannafee@gmu.edu'
affiliations: [1] # 1-based indices into the entry's affiliations array
corresponding: true
# equalContribution: true # OPTIONAL: → † on each flagged author + a "†Equal contribution" caption
# principalInvestigator: true # OPTIONAL: → ‡ on PI + a "‡Principal investigator" caption
affiliations:
- name: 'George Mason University'
location: 'Fairfax, Virginia, USA'
url: 'https://www.gmu.edu'
venue:
name: 'CS700 — Computer Geometry, Course Project'
name: 'CS700 — Research Methodology in Computer Science, Course Project'
short: 'GMU CS700'
year: 2025
status: 'tech-report' # preprint | under-review | accepted | published | workshop | tech-report
published: '12/08/2025'
featured: true # surfaces on the home page
new: true # renders a "NEW" badge inline with the title on the listing card
comingSoon: true # OPTIONAL: renders a "Coming soon!" pastel placeholder in place of the listing thumbnail (for conditionally accepted / pre-publication entries that don't have a teaser yet)
section: 'others' # top-tier | conferences | journals | workshops | others
topics: ['3D Graphics', 'Mesh Simplification']
keywords: ['mesh decimation', 'QEM', 'vertex clustering']
Expand All @@ -357,6 +377,19 @@ bibtex: |

The detail page renders the structured fields in the hero (status chip from `venue.status`, authors with affiliation superscripts, venue line, action-button row driven by `links` and `bibtex`). Authors matching `SITE_AUTHOR.name` are bolded. The `bibtex` field renders as a copy-to-clipboard code block anchored at `#bibtex`.

**Author / affiliation superscripts are conditional** — each marker only renders when it actually disambiguates something:

| Flag on `Author` | Glyph | Caption | Renders when |
| ----------------------------- | ------- | ----------------------- | ------------------------------- |
| `corresponding: true` | `*` | \*Corresponding author | 2+ authors AND ≥1 corresponding |
| `equalContribution: true` | `†` | †Equal contribution | 2+ authors AND ≥2 flagged equal |
| `principalInvestigator: true` | `‡` | ‡Principal investigator | 2+ authors AND ≥1 PI |
| `affiliations: [n]` | `¹ ² ³` | _(legend below)_ | 2+ affiliations on the entry |

Marker order on each author: `*†‡` then numeric affiliation indices (academic convention). Single-author / single-affiliation entries skip everything — no orphan markers. When markers do show, each `<sup>` carries a native `title` tooltip (affiliation index sups resolve to the full affiliation name; symbol sups resolve to their caption). A combined caption line — e.g. `*Corresponding author · †Equal contribution · ‡Principal investigator` — auto-renders below the affiliation row.

The research listing page (`/research`) renders an Overview paragraph, a date-column News timeline, and a colored chip row of Research Areas above the section-grouped listings. Section headings across the page (Overview / News / Research Areas / Top-Tier Venues / Conferences / Journals / Workshops / Others) share a single `<SectionHeading>` component (`text-lg md:text-xl`, `font-bold`, `text-black dark:text-white`, bordered bottom).

### Special MDX components

```mdx
Expand Down Expand Up @@ -420,7 +453,7 @@ y = \sigma(Wx + b)

### Vercel (recommended)

Zero-config — Vercel detects Next.js and uses the `installCommand` from `vercel.json` (`corepack enable && yarn install --immutable`) so Yarn 4 is honoured. Set `NEXT_PUBLIC_SITE_URL` to your production domain in the Vercel project settings.
Zero-config — Vercel detects Next.js and uses the `installCommand` from `vercel.json` (`corepack enable && corepack prepare yarn@4.14.1 --activate && yarn install --immutable`) so Yarn 4 actually runs (the explicit `corepack prepare` step is required because `corepack enable` alone doesn't reliably switch the active yarn binary on Vercel's image — without it, Vercel falls back to Yarn 1.22 and silently rewrites the lockfile). Set `NEXT_PUBLIC_SITE_URL` to your production domain in the Vercel project settings.

### Static export

Expand Down
2 changes: 1 addition & 1 deletion src/app/research/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ export default async function ResearchEntryPage({ params }: Props) {
}

return (
<AppLayoutPage>
<AppLayoutPage className='pt-6 md:pt-10'>
<script type='application/ld+json' dangerouslySetInnerHTML={{ __html: JSON.stringify(graphJsonLd) }} />
<BackToTop />

Expand Down
2 changes: 1 addition & 1 deletion src/app/research/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export default async function ResearchPage() {
}

return (
<AppLayoutPage>
<AppLayoutPage className='pt-6 md:pt-10'>
<script type='application/ld+json' dangerouslySetInnerHTML={{ __html: JSON.stringify(graphJsonLd) }} />
{/* h1 kept for accessibility / SEO; visual title removed per design — sections below carry the page */}
<h1 className='sr-only'>Research</h1>
Expand Down
30 changes: 30 additions & 0 deletions src/components/content/research/ComingSoonImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { cn } from '@/lib/utils'

type ComingSoonImageProps = {
className?: string
}

/**
* Pastel-gradient placeholder used in place of a real thumbnail/OG image when a
* research entry is pre-publication (`comingSoon: true` in the MDX frontmatter).
* Sized to fill its parent — drop into the same `relative aspect-[5/4]` slot the
* NextImage normally occupies.
*/
export const ComingSoonImage: React.FunctionComponent<ComingSoonImageProps> = ({ className }) => {
return (
<div
role='img'
aria-label='Coming soon'
className={cn(
'flex h-full w-full items-center justify-center',
'bg-gradient-to-br from-amber-50 via-rose-50 to-blue-100',
'dark:from-amber-950/40 dark:via-rose-950/30 dark:to-blue-900/40',
className
)}
>
<span className='font-serif text-2xl leading-none font-medium tracking-tight text-gray-900 md:text-3xl dark:text-gray-100'>
Coming <span className='italic'>soon!</span>
</span>
</div>
)
}
Loading
Loading