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
8 changes: 6 additions & 2 deletions .github/workflows/blog-check.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
name: Blog check

# Validates blog post frontmatter and runs a full build on PRs that touch blog
# content or the website code, so broken MDX or missing fields are caught before merge.
# Lints the code, validates blog post frontmatter, and runs a full build on PRs that
# touch blog content or the website code, so lint errors, broken MDX, or missing
# fields are caught before merge.
on:
pull_request:
paths:
- 'content/blog/**'
- 'pages/blog/**'
- 'src/**'
- 'package.json'
- 'eslint.config.mjs'

jobs:
build:
Expand All @@ -20,6 +22,8 @@ jobs:
node-version: 20
cache: npm
- run: npm ci
- name: Lint
run: npm run lint
- name: Validate frontmatter
run: node scripts/validate-posts.mjs
- name: Build
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ layout, frontmatter, and assets.

## Before you open a pull request

Run these locally; CI runs the last two on every pull request:
Run these locally; CI runs them on every pull request:

```bash
npm run lint
Expand Down
8 changes: 8 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";

export default [

Check warning on line 4 in eslint.config.mjs

View workflow job for this annotation

GitHub Actions / build

Assign array to a variable before exporting as module default
...nextVitals,
...nextTs,
{ ignores: [".next/**", "node_modules/**", "next-env.d.ts"] },
];
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"lint": "eslint .",
"validate-posts": "node scripts/validate-posts.mjs",
"capture-gif": "node scripts/capture-gif.mjs"
},
Expand Down
2 changes: 1 addition & 1 deletion pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default function Home() {
return (
<>
<Seo url={SITE_URL} />
<Nav />
<Nav hero />
<Hero />
<hr className="divider" />
<Partners />
Expand Down
9 changes: 5 additions & 4 deletions src/components/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Link from 'next/link'
import SocialIcons from '@/components/SocialIcons'

export default function Footer() {
Expand All @@ -6,18 +7,18 @@
<div className="wrap">
<div className="footer-inner">
<div className="footer-left">
<a href="/" className="footer-logo">
<Link href="/" className="footer-logo">
<img src="/icon-color.svg" alt="Modelplane" height={32} />

Check warning on line 11 in src/components/Footer.tsx

View workflow job for this annotation

GitHub Actions / build

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
</a>
</Link>
<span className="footer-tagline">Open source control plane for AI models</span>
<p className="footer-legal">
© 2026 The Modelplane Authors. Modelplane™ is a trademark.
</p>
</div>
<div className="footer-right">
<a href="https://docs.modelplane.ai">Docs</a>
<a href="/blog">Blog</a>
<a href="/privacy">Privacy</a>
<Link href="/blog">Blog</Link>
<Link href="/privacy">Privacy</Link>
<SocialIcons />
</div>
</div>
Expand Down
41 changes: 22 additions & 19 deletions src/components/Nav.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import { useState, useEffect } from 'react'
import Link from 'next/link'
import GithubIcon from '@/components/GithubIcon'

const GITHUB_URL = 'https://github.com/modelplaneai/modelplane'
const SLACK_URL = 'https://slack.modelplane.ai'

export default function Nav() {
interface NavProps {
// Set on pages that render the hero (the homepage).
hero?: boolean
}

export default function Nav({ hero = false }: NavProps) {
const [open, setOpen] = useState(false)
// On the homepage the hero shows its own large wordmark, so we hide the nav
// logo while the hero is in view (avoids two logos). Once the hero scrolls
// away, the nav slides in with the logo and stays sticky. Pages without a
// hero just show the logo normally.
const [past, setPast] = useState(false)
const [heroPage, setHeroPage] = useState(false)
const [heroInView, setHeroInView] = useState(true)
const past = !hero || !heroInView

// Close menu on route/hash change
useEffect(() => {
Expand All @@ -21,36 +27,33 @@
}, [])

useEffect(() => {
const hero = document.querySelector('.herox')
if (!hero) {
setPast(true) // no hero on this page → show the logo normally
return
}
setHeroPage(true)
if (!hero) return
const heroEl = document.querySelector('.herox')
if (!heroEl) return
const obs = new IntersectionObserver(
([entry]) => setPast(!entry.isIntersecting),
([entry]) => setHeroInView(entry.isIntersecting),
{ rootMargin: '-64px 0px 0px 0px', threshold: 0 }
)
obs.observe(hero)
obs.observe(heroEl)
return () => obs.disconnect()
}, [])
}, [hero])

const navClass = [
!past ? 'nav--top' : '',
past && heroPage ? 'nav--reveal' : '',
past && hero ? 'nav--reveal' : '',
].filter(Boolean).join(' ')

return (
<nav className={navClass}>
<div className="wrap">
<a href="/" className="nav-logo">
<Link href="/" className="nav-logo">
<img src="/icon-color.svg" alt="Modelplane" height={32} />

Check warning on line 50 in src/components/Nav.tsx

View workflow job for this annotation

GitHub Actions / build

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
</a>
</Link>

<ul className="nav-links">
<li><a href="https://docs.modelplane.ai">Docs</a></li>
<li><a href="/blog">Blog</a></li>
<li><a href="/manifesto">Manifesto</a></li>
<li><Link href="/blog">Blog</Link></li>
<li><Link href="/manifesto">Manifesto</Link></li>
<li><a href={SLACK_URL} target="_blank" rel="noopener noreferrer">Slack</a></li>
<li><a href={GITHUB_URL} target="_blank" rel="noopener noreferrer" aria-label="GitHub"><GithubIcon /></a></li>
<li><a href={GITHUB_URL} className="nav-cta-ghost" target="_blank" rel="noopener noreferrer">★ Star the Project</a></li>
Expand All @@ -69,8 +72,8 @@
{open && (
<div className="nav-dropdown">
<a href="https://docs.modelplane.ai" onClick={() => setOpen(false)}>Docs</a>
<a href="/blog" onClick={() => setOpen(false)}>Blog</a>
<a href="/manifesto" onClick={() => setOpen(false)}>Manifesto</a>
<Link href="/blog" onClick={() => setOpen(false)}>Blog</Link>
<Link href="/manifesto" onClick={() => setOpen(false)}>Manifesto</Link>
<a href={SLACK_URL} target="_blank" rel="noopener noreferrer" onClick={() => setOpen(false)}>Slack</a>
<a href={GITHUB_URL} target="_blank" rel="noopener noreferrer" aria-label="GitHub" onClick={() => setOpen(false)}><GithubIcon /></a>
<a href={GITHUB_URL} className="nav-cta-mobile" target="_blank" rel="noopener noreferrer" onClick={() => setOpen(false)}>★ Star the Project</a>
Expand Down
2 changes: 2 additions & 0 deletions src/components/blog/BlogSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export default function BlogSidebar({ recent, tagCounts }: BlogSidebarProps) {

<div className="sidebar-widget">
<h2 className="sidebar-title">Subscribe</h2>
{/* A plain link: the feed is XML, not a page Link can navigate to. */}
{/* eslint-disable-next-line @next/next/no-html-link-for-pages */}
<a className="sidebar-rss" href="/feed.xml">
<svg className="rss-icon" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
<path
Expand Down
6 changes: 4 additions & 2 deletions src/components/blog/PostList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ interface PostListProps {
// Renders a grid of post cards, revealing more as the reader scrolls (infinite scroll).
export default function PostList({ posts }: PostListProps) {
const [visible, setVisible] = useState(INITIAL)
const [shownPosts, setShownPosts] = useState(posts)
const sentinelRef = useRef<HTMLDivElement | null>(null)

// Restart from the top whenever the underlying list changes (e.g. a new search).
useEffect(() => {
if (posts !== shownPosts) {
setShownPosts(posts)
setVisible(INITIAL)
}, [posts])
}

useEffect(() => {
if (visible >= posts.length) return
Expand Down
Loading