feat(blog): complete blog redesign and shared nav/footer components#7544
feat(blog): complete blog redesign and shared nav/footer components#7544AmanVarshney01 wants to merge 3 commits intomainfrom
Conversation
WalkthroughThis pull request comprehensively refactors the blog app by introducing a centralized data layer for post management, creating fifteen new blog-specific UI components for navigation, post display, and discovery, restructuring blog pages and layouts to use these components, and updating global styling with Tailwind base layers. Related enhancements to shared UI packages and Eclipse components support the new blog structure. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
🍈 Lychee Link Check Report3666 links: ❌ Errors
Full Statistics Table
|
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/blog/src/app/layout.tsx (1)
16-16:⚠️ Potential issue | 🔴 CriticalRemove undefined
LayoutProps<'/>type annotation—it doesn't exist and will cause a compilation error.
LayoutPropsis not defined anywhere in the codebase and is not imported. Withstrict: trueenabled in tsconfig, this will fail to compile. Use the standard React pattern instead:export default function Layout({ children }: { children: React.ReactNode }) {This affects both
apps/blog/src/app/layout.tsx(line 16) andapps/eclipse/src/app/layout.tsx(line 17).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/blog/src/app/layout.tsx` at line 16, The function signature export default function Layout({ children }: LayoutProps<'/'>) uses an undefined type LayoutProps<'/'>; replace that with the standard React children type by changing the signature to export default function Layout({ children }: { children: React.ReactNode }) — remove LayoutProps<'/'> from the Layout function declaration (the same change should be applied to the other Layout function that used LayoutProps).
🧹 Nitpick comments (8)
apps/blog/src/components/blog/blog-compact-item.tsx (1)
30-37: Consider extractinggetInitialsto a shared helper.
This helper is duplicated across multiple blog components, which risks drift over time.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/blog/src/components/blog/blog-compact-item.tsx` around lines 30 - 37, The getInitials function is duplicated; extract it into a shared utility (e.g., export a function named getInitials from a shared helper module) and replace the local implementation in BlogCompactItem (function getInitials) with an import from that shared helper, ensuring the exported helper preserves the exact behavior (split on whitespace, take up to two parts, uppercase first letters, fallback to 'PT'); update other blog components to import the same helper so all usages share one implementation and remove the local duplicates.apps/blog/src/lib/blog-data.ts (1)
3-10: Type order inconsistency withsource.config.ts.The order here differs from
blogPostTypesinapps/blog/source.config.ts:
- Here:
release,user-story,community,tutorial,postgres,changelog- There:
release,user-story,tutorial,community,postgres,changelogThis could cause confusion if the order is meaningful (e.g., for filter button rendering or priority). Consider extracting a single source of truth, or confirm the ordering difference is intentional.
Also applies to: 16-24
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/blog/src/lib/blog-data.ts` around lines 3 - 10, The BLOG_TYPE_ORDER constant in this file has a different ordering than blogPostTypes in apps/blog/source.config.ts which can cause UI/logic inconsistencies; unify them by choosing one canonical order and updating the other to match (or extract a single exported constant used by both), e.g., align BLOG_TYPE_ORDER to match blogPostTypes or refactor both to import a shared BLOG_TYPES_ORDER array so functions/components that rely on BLOG_TYPE_ORDER (and blogPostTypes) use the same source of truth.apps/blog/source.config.ts (1)
16-23: Type order differs fromblog-data.ts.As noted in
apps/blog/src/lib/blog-data.ts, the order here (tutorialbeforecommunity) differs fromBLOG_TYPE_ORDER(communitybeforetutorial). If you want a single source of truth, consider importing from a shared location or aligning the order.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/blog/source.config.ts` around lines 16 - 23, The blogPostTypes array order conflicts with BLOG_TYPE_ORDER; align them by making blogPostTypes use the same ordering as BLOG_TYPE_ORDER (put 'community' before 'tutorial') or, better, export/import a single shared constant so both the blogPostTypes array and BLOG_TYPE_ORDER reference the same source (update the declaration named blogPostTypes and remove the duplicated order to prevent future drift).apps/blog/src/app/(blog)/[slug]/page.tsx (1)
32-32: Consider typing the TOC items instead ofas any[].The
as any[]cast onpage.data.tocworks but loses type safety. Iffumadocsprovides a TOC item type, consider using it. If not, defining a minimal interface (e.g.,{ title: string; url: string; depth: number }) would improve maintainability.Also applies to: 43-46
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/blog/src/app/`(blog)/[slug]/page.tsx at line 32, Replace the unsafe cast "as any[]" on page.data.toc used when rendering BlogPostTOC with a proper TOC item type: either import the TOC type from fumadocs (if available) and use BlogPostTOC items={page.data.toc as FumaDocsTocItem[]}, or define a minimal local interface (e.g., TocItem with title, url, depth) and cast to TocItem[]; update all occurrences (the one at BlogPostTOC and the similar usage at lines 43-46) to use that type so the prop signature for BlogPostTOC receives a strongly typed array instead of any[].apps/blog/src/app/global.css (1)
8-16: Body styles duplicated inlayout.tsx.The
bg-background text-foreground min-h-screenstyles are also applied inapps/blog/src/app/layout.tsxline 24. While CSS will handle this gracefully (they're identical), you could consolidate in one location for maintainability—either here in the base layer or in the layout'sclassName.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/blog/src/app/global.css` around lines 8 - 16, Duplicate application of body styles: remove the duplication by keeping the shared styles in a single place—either delete the body-related utilities (bg-background text-foreground min-h-screen) from apps/blog/src/app/global.css's `@layer` base body rule or remove them from the className on the root <body> wrapper in layout.tsx; locate the body selector in global.css and the root element's className in layout.tsx and ensure only one source defines bg-background, text-foreground, and min-h-screen for maintainability.apps/blog/src/app/layout.tsx (1)
20-20: Hardcoded dark mode.The
darkclass is hardcoded, which means users cannot toggle to light mode. If dark-mode-only is intentional for the blog, this is fine. Otherwise, consider using a theme provider or system preference detection.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/blog/src/app/layout.tsx` at line 20, The layout currently hardcodes the "dark" CSS class in the className string (the template `${inter.variable} ${barlow.variable} dark`), preventing light-mode toggling; remove the literal "dark" and instead apply the class conditionally using a theme value (e.g., from a ThemeProvider/next-themes or system-media query) so className becomes `${inter.variable} ${barlow.variable} ${theme === "dark" ? "dark" : ""}` (or derived from a provider hook) and wire the provider/hook into the RootLayout component so users can toggle or respect system preference.apps/blog/src/components/BlogGrid.tsx (1)
89-105: Counts initialization is coupled to hardcoded type keys.If
BlogPostTypeis extended with a new type (or one is renamed/removed), this object won't reflect that change, andnextCounts[post.type] += 1will silently produceNaNfor unknown types. Consider deriving this fromBLOG_TYPE_ORDERor a similar source of truth.♻️ Derive counts from BLOG_TYPE_ORDER
+import { BLOG_TYPE_ORDER } from '@/lib/blog-data'; const counts = useMemo(() => { - const nextCounts: Record<BlogPostFilterType, number> = { - all: filteredByQuery.length, - release: 0, - 'user-story': 0, - community: 0, - tutorial: 0, - postgres: 0, - changelog: 0, - }; + const nextCounts = Object.fromEntries([ + ['all', filteredByQuery.length], + ...BLOG_TYPE_ORDER.map((type) => [type, 0]), + ]) as Record<BlogPostFilterType, number>; filteredByQuery.forEach((post) => { nextCounts[post.type] += 1; }); return nextCounts; }, [filteredByQuery]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/blog/src/components/BlogGrid.tsx` around lines 89 - 105, counts useMemo currently initializes nextCounts with hardcoded keys which will break if BlogPostFilterType changes; instead derive the initial nextCounts from the canonical BLOG_TYPE_ORDER (or the source-of-truth list) and then increment safely by checking existence for each post.type. Update the useMemo to build nextCounts by iterating BLOG_TYPE_ORDER (or Object.values(BlogPostFilterType)) to set zeros, then loop filteredByQuery and do nextCounts[post.type] = (nextCounts[post.type] ?? 0) + 1 so unknown/renamed types don't produce NaN; reference the counts/useMemo block, filteredByQuery, post.type, and BLOG_TYPE_ORDER to locate and change the code.packages/ui/src/components/prisma-site-nav.tsx (1)
104-107: Hardcoded GitHub star count will become stale.The "41.8K" stars value is static and will drift from reality over time. Consider fetching this dynamically or accepting it as a prop so consumers can update it without modifying this shared component.
💡 Optional: Accept stars as a prop
type PrismaSiteNavProps = { className?: string; activeHref?: string; logoHref?: string; logoSrc?: string; dataTestId?: string; + githubStars?: string; };Then use
githubStars ?? "41.8K"as the display value.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/ui/src/components/prisma-site-nav.tsx` around lines 104 - 107, The GitHub star count is hardcoded in the PrismaSiteNav component (the <span> containing "41.8K") and will become stale; update PrismaSiteNav to accept an optional prop (e.g., githubStars?: string) and render the span using githubStars ?? "41.8K" so consumers can pass an updated value, or alternatively implement a dynamic fetch for the repo star count and use that value in the same span as the fallback.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/ui/src/components/prisma-site-footer.tsx`:
- Around line 44-48: The Blog link in the links array inside the
PrismaSiteFooter component currently points to "/" which will route to the host
root; change the Blog href to the canonical blog URL (e.g.,
"https://www.prisma.io/blog") in the links array in
packages/ui/src/components/prisma-site-footer.tsx (or alternatively add a
prop/default like blogUrl to PrismaSiteFooter to make the blog link
configurable) so the shared footer always points to the Prisma blog rather than
the app root.
In `@packages/ui/src/components/prisma-site-nav.tsx`:
- Around line 61-95: The primary nav in prisma-site-nav.tsx is hidden on mobile
via the "hidden md:flex" nav and needs a mobile drawer toggle; add a hamburger
button (visible on small screens) that toggles the existing Drawer component
(from drawer.tsx) and move or duplicate the Products (TopDropdown/productLinks),
Resources (TopDropdown/resourceLinks) and navLinks mappings into the Drawer
content so mobile users can access them; ensure the same link rendering logic is
reused (respecting isExternal for rel/target, using activeHref for styling) and
wire the button to open/close the Drawer state.
---
Outside diff comments:
In `@apps/blog/src/app/layout.tsx`:
- Line 16: The function signature export default function Layout({ children }:
LayoutProps<'/'>) uses an undefined type LayoutProps<'/'>; replace that with the
standard React children type by changing the signature to export default
function Layout({ children }: { children: React.ReactNode }) — remove
LayoutProps<'/'> from the Layout function declaration (the same change should be
applied to the other Layout function that used LayoutProps).
---
Nitpick comments:
In `@apps/blog/source.config.ts`:
- Around line 16-23: The blogPostTypes array order conflicts with
BLOG_TYPE_ORDER; align them by making blogPostTypes use the same ordering as
BLOG_TYPE_ORDER (put 'community' before 'tutorial') or, better, export/import a
single shared constant so both the blogPostTypes array and BLOG_TYPE_ORDER
reference the same source (update the declaration named blogPostTypes and remove
the duplicated order to prevent future drift).
In `@apps/blog/src/app/`(blog)/[slug]/page.tsx:
- Line 32: Replace the unsafe cast "as any[]" on page.data.toc used when
rendering BlogPostTOC with a proper TOC item type: either import the TOC type
from fumadocs (if available) and use BlogPostTOC items={page.data.toc as
FumaDocsTocItem[]}, or define a minimal local interface (e.g., TocItem with
title, url, depth) and cast to TocItem[]; update all occurrences (the one at
BlogPostTOC and the similar usage at lines 43-46) to use that type so the prop
signature for BlogPostTOC receives a strongly typed array instead of any[].
In `@apps/blog/src/app/global.css`:
- Around line 8-16: Duplicate application of body styles: remove the duplication
by keeping the shared styles in a single place—either delete the body-related
utilities (bg-background text-foreground min-h-screen) from
apps/blog/src/app/global.css's `@layer` base body rule or remove them from the
className on the root <body> wrapper in layout.tsx; locate the body selector in
global.css and the root element's className in layout.tsx and ensure only one
source defines bg-background, text-foreground, and min-h-screen for
maintainability.
In `@apps/blog/src/app/layout.tsx`:
- Line 20: The layout currently hardcodes the "dark" CSS class in the className
string (the template `${inter.variable} ${barlow.variable} dark`), preventing
light-mode toggling; remove the literal "dark" and instead apply the class
conditionally using a theme value (e.g., from a ThemeProvider/next-themes or
system-media query) so className becomes `${inter.variable} ${barlow.variable}
${theme === "dark" ? "dark" : ""}` (or derived from a provider hook) and wire
the provider/hook into the RootLayout component so users can toggle or respect
system preference.
In `@apps/blog/src/components/blog/blog-compact-item.tsx`:
- Around line 30-37: The getInitials function is duplicated; extract it into a
shared utility (e.g., export a function named getInitials from a shared helper
module) and replace the local implementation in BlogCompactItem (function
getInitials) with an import from that shared helper, ensuring the exported
helper preserves the exact behavior (split on whitespace, take up to two parts,
uppercase first letters, fallback to 'PT'); update other blog components to
import the same helper so all usages share one implementation and remove the
local duplicates.
In `@apps/blog/src/components/BlogGrid.tsx`:
- Around line 89-105: counts useMemo currently initializes nextCounts with
hardcoded keys which will break if BlogPostFilterType changes; instead derive
the initial nextCounts from the canonical BLOG_TYPE_ORDER (or the
source-of-truth list) and then increment safely by checking existence for each
post.type. Update the useMemo to build nextCounts by iterating BLOG_TYPE_ORDER
(or Object.values(BlogPostFilterType)) to set zeros, then loop filteredByQuery
and do nextCounts[post.type] = (nextCounts[post.type] ?? 0) + 1 so
unknown/renamed types don't produce NaN; reference the counts/useMemo block,
filteredByQuery, post.type, and BLOG_TYPE_ORDER to locate and change the code.
In `@apps/blog/src/lib/blog-data.ts`:
- Around line 3-10: The BLOG_TYPE_ORDER constant in this file has a different
ordering than blogPostTypes in apps/blog/source.config.ts which can cause
UI/logic inconsistencies; unify them by choosing one canonical order and
updating the other to match (or extract a single exported constant used by
both), e.g., align BLOG_TYPE_ORDER to match blogPostTypes or refactor both to
import a shared BLOG_TYPES_ORDER array so functions/components that rely on
BLOG_TYPE_ORDER (and blogPostTypes) use the same source of truth.
In `@packages/ui/src/components/prisma-site-nav.tsx`:
- Around line 104-107: The GitHub star count is hardcoded in the PrismaSiteNav
component (the <span> containing "41.8K") and will become stale; update
PrismaSiteNav to accept an optional prop (e.g., githubStars?: string) and render
the span using githubStars ?? "41.8K" so consumers can pass an updated value, or
alternatively implement a dynamic fetch for the repo star count and use that
value in the same span as the fallback.
| title: "Company", | ||
| links: [ | ||
| { label: "About", href: "https://www.prisma.io/about" }, | ||
| { label: "Blog", href: "/" }, | ||
| { label: "Data DX", href: "https://www.prisma.io/datadx" }, |
There was a problem hiding this comment.
Fix the Blog link — "/" will be wrong outside the blog app.
As a shared footer, this should point to the Prisma blog directly (or be configurable), otherwise it routes to the host root.
Suggested fix
- { label: "Blog", href: "/" },
+ { label: "Blog", href: "https://www.prisma.io/blog" },📝 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.
| title: "Company", | |
| links: [ | |
| { label: "About", href: "https://www.prisma.io/about" }, | |
| { label: "Blog", href: "/" }, | |
| { label: "Data DX", href: "https://www.prisma.io/datadx" }, | |
| title: "Company", | |
| links: [ | |
| { label: "About", href: "https://www.prisma.io/about" }, | |
| { label: "Blog", href: "https://www.prisma.io/blog" }, | |
| { label: "Data DX", href: "https://www.prisma.io/datadx" }, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/ui/src/components/prisma-site-footer.tsx` around lines 44 - 48, The
Blog link in the links array inside the PrismaSiteFooter component currently
points to "/" which will route to the host root; change the Blog href to the
canonical blog URL (e.g., "https://www.prisma.io/blog") in the links array in
packages/ui/src/components/prisma-site-footer.tsx (or alternatively add a
prop/default like blogUrl to PrismaSiteFooter to make the blog link
configurable) so the shared footer always points to the Prisma blog rather than
the app root.
| <nav aria-label="Primary" className="hidden items-center gap-4 md:flex"> | ||
| <TopDropdown label="Products" links={productLinks} /> | ||
|
|
||
| {navLinks.slice(0, 1).map((link) => ( | ||
| <a | ||
| key={link.label} | ||
| className={cn( | ||
| "inline-flex h-8 items-center rounded-md bg-transparent px-1.5 py-1 text-sm font-semibold text-foreground/90 transition-colors hover:text-foreground focus:text-foreground", | ||
| activeHref === link.href && "text-foreground", | ||
| )} | ||
| href={link.href} | ||
| rel={isExternal(link.href) ? "noreferrer" : undefined} | ||
| target={isExternal(link.href) ? "_blank" : undefined} | ||
| > | ||
| {link.label} | ||
| </a> | ||
| ))} | ||
|
|
||
| <TopDropdown label="Resources" links={resourceLinks} /> | ||
|
|
||
| {navLinks.slice(1).map((link) => ( | ||
| <a | ||
| key={link.label} | ||
| className={cn( | ||
| "inline-flex h-8 items-center rounded-md bg-transparent px-1.5 py-1 text-sm font-semibold text-foreground/90 transition-colors hover:text-foreground focus:text-foreground", | ||
| activeHref === link.href && "text-foreground", | ||
| )} | ||
| href={link.href} | ||
| rel={isExternal(link.href) ? "noreferrer" : undefined} | ||
| target={isExternal(link.href) ? "_blank" : undefined} | ||
| > | ||
| {link.label} | ||
| </a> | ||
| ))} | ||
| </nav> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if there's a mobile nav component or hamburger menu elsewhere
rg -n -C3 "hamburger|mobile.*nav|drawer|menu.*toggle" --type=tsx --type=ts packages/ui apps/blogRepository: prisma/web
Length of output: 81
🏁 Script executed:
#!/bin/bash
# Check if there's a mobile nav component or hamburger menu elsewhere
rg -n -C3 "hamburger|mobile.*nav|drawer|menu.*toggle" packages/ui apps/blogRepository: prisma/web
Length of output: 50367
🏁 Script executed:
#!/bin/bash
# More targeted search for mobile navigation in packages/ui
rg -n "mobile|hamburger|drawer|sheet|menu.*toggle" packages/ui/src --max-count=20Repository: prisma/web
Length of output: 2784
🏁 Script executed:
#!/bin/bash
# Read the full prisma-site-nav.tsx file
wc -l packages/ui/src/components/prisma-site-nav.tsx && cat -n packages/ui/src/components/prisma-site-nav.tsxRepository: prisma/web
Length of output: 7956
Implement mobile navigation for primary links.
The primary navigation is completely hidden on mobile with hidden md:flex, leaving mobile users without access to Products, Pricing, Partners, Resources, and Blog links. Add a hamburger menu or mobile drawer to display these links on screens below the md breakpoint. A drawer component already exists in the codebase (packages/ui/src/components/drawer.tsx) and can be reused here.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/ui/src/components/prisma-site-nav.tsx` around lines 61 - 95, The
primary nav in prisma-site-nav.tsx is hidden on mobile via the "hidden md:flex"
nav and needs a mobile drawer toggle; add a hamburger button (visible on small
screens) that toggles the existing Drawer component (from drawer.tsx) and move
or duplicate the Products (TopDropdown/productLinks), Resources
(TopDropdown/resourceLinks) and navLinks mappings into the Drawer content so
mobile users can access them; ensure the same link rendering logic is reused
(respecting isExternal for rel/target, using activeHref for styling) and wire
the button to open/close the Drawer state.
What this PR does
1) Rebuilds the blog experience (landing/list/post)
apps/blog/src/components/blogapps/blog/src/components/BlogGrid.tsx2) Adds blog data normalization/filtering utilities
apps/blog/src/lib/blog-data.tswith post normalization, type inference, filter, featured, and pagination helpersapps/blog/source.config.tswith optional taxonomy fields3) Updates blog styling and shell assets
apps/blog/src/app/global.cssapps/blog/src/app/layout.tsxapps/blog/public/img/4) Extracts reusable website shell components to
packages/uipackages/ui/src/components/prisma-site-nav.tsxpackages/ui/src/components/prisma-site-footer.tsxapps/blog/src/components/blog/blog-nav.tsxapps/blog/src/components/blog/blog-footer.tsx5) Aligns Eclipse navigation primitives and shared icon usage
NavigationMenu*from Eclipse components APIpackages/ui/src/components/discord.tsxand updates docs references6) Cleanup
apps/blog/src/components/CompaniesUsingPrisma.tsx