Skip to content
Open
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
1 change: 0 additions & 1 deletion apps/blog/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ const withMDX = createMDX();
/** @type {import('next').NextConfig} */
const config = {
reactStrictMode: true,
output: "export",
images: { unoptimized: true },
transpilePackages: ["@prisma-docs/eclipse"],
};
Expand Down
1 change: 1 addition & 0 deletions apps/blog/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@fumadocs/cli": "catalog:",
"@prisma-docs/eclipse": "workspace:^",
"@prisma-docs/ui": "workspace:*",
"cors": "^2.8.6",
"fumadocs-core": "catalog:",
"fumadocs-mdx": "catalog:",
"fumadocs-openapi": "catalog:",
Expand Down
Binary file not shown.
Binary file added apps/blog/public/fonts/monaspace_neon_var.woff
Binary file not shown.
Binary file added apps/blog/public/fonts/monaspace_neon_var.woff2
Binary file not shown.
36 changes: 20 additions & 16 deletions apps/blog/source.config.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
import remarkDirective from 'remark-directive';
import remarkDirective from "remark-directive";
import {
remarkDirectiveAdmonition,
remarkMdxFiles,
} from 'fumadocs-core/mdx-plugins';
import { remarkImage } from 'fumadocs-core/mdx-plugins';
} from "fumadocs-core/mdx-plugins";
import { remarkImage } from "fumadocs-core/mdx-plugins";
import {
defineCollections,
defineConfig,
frontmatterSchema,
} from 'fumadocs-mdx/config';
import lastModified from 'fumadocs-mdx/plugins/last-modified';
import { z } from 'zod';
import convert from 'npm-to-yarn';
} from "fumadocs-mdx/config";
import lastModified from "fumadocs-mdx/plugins/last-modified";
import { z } from "zod";
import convert from "npm-to-yarn";

export const blogPosts = defineCollections({
type: 'doc',
dir: 'content/blog',
type: "doc",
dir: "content/blog",
schema: frontmatterSchema.extend({
authors: z.array(z.string()),
authorSrc: z.string().optional(),
date: z.coerce.date(),
heroImagePath: z.string().optional(),
metaImagePath: z.string().optional(),
metaDescription: z.string().optional(),
metaTitle: z.string().optional(),
description: z.string().optional(),
}),
postprocess: {
includeProcessedMarkdown: true,
Expand All @@ -38,19 +42,19 @@ export default defineConfig({
],
remarkCodeTabOptions: { parseMdx: true },
remarkNpmOptions: {
persist: { id: 'package-manager' },
persist: { id: "package-manager" },
// Custom package managers to add --bun flag for bunx commands
packageManagers: [
{ command: (cmd: string) => convert(cmd, 'npm'), name: 'npm' },
{ command: (cmd: string) => convert(cmd, 'pnpm'), name: 'pnpm' },
{ command: (cmd: string) => convert(cmd, 'yarn'), name: 'yarn' },
{ command: (cmd: string) => convert(cmd, "npm"), name: "npm" },
{ command: (cmd: string) => convert(cmd, "pnpm"), name: "pnpm" },
{ command: (cmd: string) => convert(cmd, "yarn"), name: "yarn" },
{
command: (cmd: string) => {
const converted = convert(cmd, 'bun');
const converted = convert(cmd, "bun");
if (!converted) return undefined;
return converted.replace(/^bun x /, 'bunx --bun ');
return converted.replace(/^bun x /, "bunx --bun ");
},
name: 'bun',
name: "bun",
},
],
},
Expand Down
202 changes: 108 additions & 94 deletions apps/blog/src/app/(blog)/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import { notFound } from 'next/navigation';
import Link from 'next/link';
import { InlineTOC } from 'fumadocs-ui/components/inline-toc';
import { getMDXComponents } from '@/mdx-components';
import { createRelativeLink } from 'fumadocs-ui/mdx';
import { blog } from '@/lib/source';
import Image from 'next/image';
import { notFound } from "next/navigation";
import Link from "next/link";
import { getMDXComponents } from "@/mdx-components";
import { createRelativeLink } from "fumadocs-ui/mdx";
import { blog } from "@/lib/source";
import {
Action,
Avatar,
Badge,
Button,
cn,
InlineTOC,
Input,
Label,
Separator,
} from "@prisma-docs/eclipse";
import { FooterNewsletterForm } from "@prisma-docs/ui/components/newsletter";
import { BlogShare } from "@/components/BlogShare";

export default async function Page(props: {
params: Promise<{ slug: string }>;
Expand All @@ -16,104 +27,107 @@ export default async function Page(props: {
const MDX = page.data.body;
const formatDate = (value: unknown) => {
const date =
value instanceof Date ? value : new Date((value as string) ?? '');
if (Number.isNaN(date.getTime())) return '';
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
value instanceof Date ? value : new Date((value as string) ?? "");
if (Number.isNaN(date.getTime())) return "";
return date.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
};
const getHeroImageSrc = () => {
const data = page.data as any;
const rel =
(data.heroImagePath as string | undefined) ??
(data.metaImagePath as string | undefined);
if (rel) {
if (rel.startsWith('/')) return rel;
const base = page.url.startsWith('/') ? page.url : `/${page.url}`;
const baseClean = base.endsWith('/') ? base.slice(0, -1) : base;
const relClean = rel.replace(/^\.\//, '').replace(/^\/+/, '');
return `${baseClean}/${relClean}`;
}
const absolute =
(data.heroImageUrl as string | undefined) ??
(data.metaImageUrl as string | undefined);
return absolute ?? null;
};
const heroSrc = getHeroImageSrc();

return (
<>
{/* Hero image */}
{heroSrc ? (
<div className="w-full">
<div className="relative w-full aspect-video">
<Image
src={heroSrc}
alt={(page.data as any).heroImageAlt ?? page.data.title}
fill
priority
className="object-cover"
sizes="100vw"
<div className="w-full px-4 mx-auto md:grid md:grid-cols-[1fr_180px] mt-4 md:mt-22 gap-6 max-w-257">
<div className="post-contents w-full">
{/* Title + meta */}
<header className="w-full relative">
<Link
href="/blog"
className="text-fd-primary hover:underline text-sm absolute -top-8"
>
← Back to Blog
</Link>
<h1 className="mt-3 mb-8 font-bold max-md:text-3xl md:text-5xl stretch-display font-display text-foreground-neutral">
{page.data.title}
</h1>
<div className="text-sm text-fd-muted-foreground flex gap-2 items-center text-foreground-neutral mb-4">
{page.data.authors?.length > 1 ? (
page.data.authors.join(", ")
) : page.data.authors?.length === 1 ? (
<span className="mt-auto flex items-center gap-2 font-semibold text-sm">
{page.data?.authorSrc && (
<Avatar
format="image"
src={page.data.authorSrc}
alt={page.data.authors[0]}
size="lg"
/>
)}
<span>{page.data.authors[0]}</span>
</span>
) : null}
{page.data.date ? (
<>
<Separator orientation="vertical" className="h-4" />
<span className="text-foreground-neutral-weak">
{formatDate(page.data.date)}
</span>
</>
) : null}
</div>
<div className="filter-badge flex gap-2">
<Badge
color="neutral"
label="Release"
className="border border-stroke-neutral-strong bg-transparent text-foreground-neutral-weak"
/>
<Badge
color="neutral"
label="Postgres"
className="border border-stroke-neutral-strong bg-transparent text-foreground-neutral-weak"
/>
<Badge
color="neutral"
label="ORM"
className="border border-stroke-neutral-strong bg-transparent text-foreground-neutral-weak"
/>
<Badge
color="neutral"
label="Customer Story"
className="border border-stroke-neutral-strong bg-transparent text-foreground-neutral-weak"
/>
</div>
</div>
) : null}
</header>

{/* Title + meta */}
<header className="w-full max-w-350 mx-auto px-4 md:px-8 py-10">
<Link href="/blog" className="text-fd-primary hover:underline text-sm">
← Back to Blog
</Link>
<h1 className="mt-3 mb-3 text-3xl md:text-4xl font-bold">
{page.data.title}
</h1>
{page.data.description ? (
<p className="text-fd-muted-foreground mb-3">{page.data.description}</p>
) : null}
<p className="text-sm text-fd-muted-foreground">
{page.data.authors?.length ? page.data.authors.join(', ') : null}
{page.data.date ? (
<>
{' • '}
<span>{formatDate(page.data.date)}</span>
</>
) : null}
</p>
</header>
{/* Body */}
<article className="w-full flex flex-col pb-8 mt-12">
<div className="prose min-w-0 [&_figure]:w-full! [&_figure]:md:max-w-140! [&_figure]:lg:max-w-200!">
<MDX
components={getMDXComponents({
a: createRelativeLink(blog, page),
})}
/>
</div>
</article>
<Separator className="my-12" />

{/* Body */}
<article className="w-full max-w-350 mx-auto flex flex-col px-4 md:px-8 pb-12">
<div className="prose min-w-0">
<InlineTOC items={page.data.toc} />
<MDX
components={getMDXComponents({
a: createRelativeLink(blog, page)
})}
/>
</div>
</article>
{/* Share Container */}
<BlogShare desc={page.data.metaDescription as string} />

{/* Newsletter CTA */}
<div className="w-full max-w-350 mx-auto px-4 md:px-8 pb-16">
<div className="rounded-2xl border border-fd-primary/20 bg-fd-secondary p-6 md:p-8">
<h4 className="text-2xl font-semibold mb-2">
Don’t miss the next post!
</h4>
<p className="text-fd-muted mb-4">
Sign up for the Prisma Newsletter to stay up to date with the latest
releases and posts.
</p>
<Link
href="https://www.prisma.io/newsletter"
className="inline-flex items-center px-4 py-2 rounded-md bg-fd-primary text-white hover:opacity-90 transition"
>
Sign up
</Link>
{/* Newsletter CTA */}
<div className="w-full px-8 py-12 shadow-box-low newsletter-bg rounded-square border border-background-neutral flex max-sm:flex-col wrap items-start gap-4 sm:items-center justify-between my-12">
<FooterNewsletterForm />
</div>
</div>
<div className="max-md:hidden toc">
<div className="sticky top-24 [&_a[data-state=inactive]]:text-foreground-neutral-weak! [&_a[data-state=active]]:text-foreground-neutral!">
<span className="text-shadow-foreground-neutral-reverse font-semibold text-md mb-4 mt-0 block">
On this page
</span>
<InlineTOC items={page.data.toc} className="px-0" />
</div>
</div>
</>
</div>
);
}

Expand Down
Loading
Loading