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
6 changes: 3 additions & 3 deletions src/app/_components/VersionLabel.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"use client"

export default function VersionLabel() {
export default function VersionLabel({ latestVersion }) {
const handleClick = () => {
window.location.href = "/blog/v2026.02.13"
window.location.href = `/blog/${latestVersion?.version}`
}
Comment on lines +3 to 6
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The component should return null if latestVersion or its version property is missing. This prevents rendering an empty styled label and avoids navigating to an invalid URL (e.g., /blog/undefined).

export default function VersionLabel({ latestVersion }) {
  if (!latestVersion?.version) return null

  const handleClick = () => {
    window.location.href = "/blog/" + latestVersion.version
  }


return (
Expand All @@ -20,7 +20,7 @@ export default function VersionLabel() {
}}
title="View release notes"
>
v2026.02.13
{latestVersion?.version}
</span>
)
}
15 changes: 11 additions & 4 deletions src/app/layout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Banner, Head } from "nextra/components"
import { getPageMap } from "nextra/page-map"
import VersionLabel from "./_components/VersionLabel"
import ImageZoomProvider from "../components/ImageZoomProvider"
import { getLatestVersion } from "@/lib/version";
import "nextra-theme-docs/style.css"
import "./globals.css"

Expand Down Expand Up @@ -33,6 +34,12 @@ export const metadata = {
}

export default async function RootLayout({ children }) {

const [pageMap, latestVersion] = await Promise.all([
getPageMap(),
getLatestVersion(),
]);

const navbar = (
<Navbar
logo={
Expand All @@ -46,15 +53,15 @@ export default async function RootLayout({ children }) {
}}
className="spacedf-logo"
/>
<VersionLabel />
<VersionLabel latestVersion={latestVersion} />
</div>
}
// SpaceDF discord server
chatLink="https://discord.gg/HxCTyMCzuK"
projectLink="https://github.com/Space-DF/spacedf-docs"
/>
)
const pageMap = await getPageMap()

return (
<html lang="en" dir="ltr" suppressHydrationWarning>
<Head
Expand All @@ -73,9 +80,9 @@ export default async function RootLayout({ children }) {
<Layout
banner={
<Banner storageKey="SpaceDF Launch">
🚀 SpaceDF v2026.02.13 is now live!{" "}
🚀 SpaceDF {latestVersion?.version} is now live!{" "}
<a
href="/blog/v2026.02.13"
href={`/blog/${latestVersion?.version}`}
style={{ color: "inherit", textDecoration: "underline" }}
>
Read the release notes
Expand Down
45 changes: 45 additions & 0 deletions src/lib/version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import fs from "fs"
import path from "path"
import matter from "gray-matter"

type LatestVersion = {
slug: string
version: string
date?: string
title?: string
type?: string
author?: string
description?: string
}

export const getLatestVersion = async (): Promise<LatestVersion | null> => {
const POSTS_DIR = path.join(process.cwd(), "src/app/blog/(post)")

if (!fs.existsSync(POSTS_DIR)) return null

const results: LatestVersion[] = []
const folders = fs.readdirSync(POSTS_DIR)

for (const folder of folders) {
const fullPath = path.join(POSTS_DIR, folder, "page.mdx")
if (!fs.existsSync(fullPath)) continue

const raw = fs.readFileSync(fullPath, "utf-8")
if (!raw) continue

const { data } = matter(raw)

results.push({
...(data as Omit<LatestVersion, "slug" | "version">),
version: folder,
slug: `/blog/${folder}`,
})
}

const latest =
results.sort(
(a, b) => new Date(b.date ?? 0).getTime() - new Date(a.date ?? 0).getTime(),
)[0] ?? null

return latest
}
Comment on lines +1 to +45
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Since getLatestVersion is an asynchronous function, it is best practice to use asynchronous file system operations (fs/promises) to avoid blocking the Node.js event loop. Additionally, adding error handling for file access and parsing ensures the application remains robust if the directory structure or file content is unexpected.

import fs from "fs/promises"
import path from "path"
import matter from "gray-matter"

type LatestVersion = {
  slug: string
  version: string
  date?: string
  title?: string
  type?: string
  author?: string
  description?: string
}

export const getLatestVersion = async (): Promise<LatestVersion | null> => {
  const POSTS_DIR = path.join(process.cwd(), "src/app/blog/(post)")

  try {
    const stats = await fs.stat(POSTS_DIR)
    if (!stats.isDirectory()) return null
  } catch {
    return null
  }

  const folders = await fs.readdir(POSTS_DIR)
  const results: LatestVersion[] = []

  for (const folder of folders) {
    const fullPath = path.join(POSTS_DIR, folder, "page.mdx")
    try {
      const raw = await fs.readFile(fullPath, "utf-8")
      const { data } = matter(raw)

      results.push({
        ...(data as Omit<LatestVersion, "slug" | "version">),
        version: folder,
        slug: "/blog/" + folder,
      })
    } catch {
      continue
    }
  }

  return (
    results.sort((a, b) => {
      const timeA = a.date ? new Date(a.date).getTime() : 0
      const timeB = b.date ? new Date(b.date).getTime() : 0
      return timeB - timeA
    })[0] ?? null
  )
}