refactor: integrate content-collections for notes (#2609)
@@ -38,3 +38,6 @@ yarn.lock
|
||||
|
||||
# next-agents-md
|
||||
.next-docs/
|
||||
|
||||
# content-collections
|
||||
.content-collections/
|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 128 KiB |
@@ -7,6 +7,7 @@ import { FadeTransition } from "@/components/page-transition";
|
||||
import domainstackIcon from "./icons/domainstack.png";
|
||||
import snoozleIcon from "./icons/snoozle.png";
|
||||
import sofaIcon from "./icons/sofa.png";
|
||||
import stanzaIcon from "./icons/stanza.png";
|
||||
import uiIcon from "./icons/ui.png";
|
||||
import versioneerIcon from "./icons/versioneer.png";
|
||||
|
||||
@@ -24,6 +25,12 @@ const projects: readonly Project[] = [
|
||||
tagline: "Domain intelligence made easy",
|
||||
icon: domainstackIcon,
|
||||
},
|
||||
{
|
||||
name: "Stanza",
|
||||
url: "https://stanza.tools",
|
||||
tagline: "shadcn/ui for infrastructure",
|
||||
icon: stanzaIcon,
|
||||
},
|
||||
{
|
||||
name: "Sofa",
|
||||
url: "https://sofa.watch",
|
||||
@@ -45,7 +52,7 @@ const projects: readonly Project[] = [
|
||||
{
|
||||
name: "jarvis-ui",
|
||||
url: "https://ui.jarv.is",
|
||||
tagline: "An intentionally minimal React component library",
|
||||
tagline: "Intentionally random React component library",
|
||||
icon: uiIcon,
|
||||
},
|
||||
] as const;
|
||||
|
||||
@@ -195,7 +195,7 @@
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply outline-ring/50;
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
|
||||
@@ -14,7 +14,7 @@ import { ViewCounter } from "@/components/view-counter";
|
||||
import authorConfig from "@/lib/config/author";
|
||||
import siteConfig from "@/lib/config/site";
|
||||
import { createMetadata } from "@/lib/metadata";
|
||||
import { getFrontMatter, getSlugs, POSTS_DIR } from "@/lib/posts";
|
||||
import { getFrontMatter, getPost, getSlugs, POSTS_DIR } from "@/lib/posts";
|
||||
|
||||
import { size as ogImageSize } from "./opengraph-image";
|
||||
|
||||
@@ -54,10 +54,10 @@ export const generateMetadata = async ({
|
||||
|
||||
const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
|
||||
const { slug } = await params;
|
||||
const frontmatter = await getFrontMatter(slug);
|
||||
if (!frontmatter) notFound();
|
||||
const d = new Date(frontmatter.date);
|
||||
const post = await getPost(slug);
|
||||
if (!post) notFound();
|
||||
|
||||
const d = new Date(post.date);
|
||||
const formattedDates = {
|
||||
dateISO: d.toISOString(),
|
||||
dateTitle: d.toLocaleString("en-US", {
|
||||
@@ -83,18 +83,18 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
|
||||
item={{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BlogPosting",
|
||||
headline: frontmatter?.title,
|
||||
description: frontmatter?.description,
|
||||
url: frontmatter?.permalink,
|
||||
headline: post.title,
|
||||
description: post.description,
|
||||
url: post.permalink,
|
||||
image: {
|
||||
"@type": "ImageObject",
|
||||
contentUrl: `${process.env.NEXT_PUBLIC_BASE_URL}/${POSTS_DIR}/${frontmatter?.slug}/opengraph-image`,
|
||||
contentUrl: `${process.env.NEXT_PUBLIC_BASE_URL}/${POSTS_DIR}/${post.slug}/opengraph-image`,
|
||||
width: `${ogImageSize.width}`,
|
||||
height: `${ogImageSize.height}`,
|
||||
},
|
||||
keywords: frontmatter?.tags?.join(", "),
|
||||
datePublished: frontmatter?.date,
|
||||
dateModified: frontmatter?.date,
|
||||
keywords: post.tags?.join(", "),
|
||||
datePublished: post.date,
|
||||
dateModified: post.date,
|
||||
inLanguage: process.env.NEXT_PUBLIC_SITE_LOCALE,
|
||||
license: `https://spdx.org/licenses/${siteConfig.license}.html`,
|
||||
author: {
|
||||
@@ -106,7 +106,7 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
|
||||
|
||||
<div className="text-foreground/70 flex flex-wrap items-center gap-x-4 gap-y-2.5 text-[13px] tracking-wide">
|
||||
<Link
|
||||
href={`/${POSTS_DIR}/${frontmatter?.slug}`}
|
||||
href={`/${POSTS_DIR}/${post.slug}`}
|
||||
className={
|
||||
"flex flex-nowrap items-center gap-1.5 whitespace-nowrap text-inherit hover:no-underline"
|
||||
}
|
||||
@@ -121,10 +121,10 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
|
||||
</time>
|
||||
</Link>
|
||||
|
||||
{frontmatter?.tags && (
|
||||
{post.tags && (
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
<IconTag className="inline size-3.5 shrink-0" aria-hidden="true" />
|
||||
{frontmatter?.tags.map((tag) => (
|
||||
{post.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
title={tag}
|
||||
@@ -137,8 +137,8 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
|
||||
)}
|
||||
|
||||
<Link
|
||||
href={`https://github.com/${process.env.NEXT_PUBLIC_GITHUB_REPO}/blob/main/${POSTS_DIR}/${frontmatter?.slug}/index.mdx`}
|
||||
title={`Edit "${frontmatter?.title}" on GitHub`}
|
||||
href={`https://github.com/${process.env.NEXT_PUBLIC_GITHUB_REPO}/blob/main/${POSTS_DIR}/${post.slug}/index.mdx`}
|
||||
title={`Edit "${post.title}" on GitHub`}
|
||||
className={
|
||||
"flex flex-nowrap items-center gap-1.5 whitespace-nowrap text-inherit hover:no-underline"
|
||||
}
|
||||
@@ -148,25 +148,25 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href={`/${POSTS_DIR}/${frontmatter?.slug}#comments`}
|
||||
href={`/${POSTS_DIR}/${post.slug}#comments`}
|
||||
className="flex flex-nowrap items-center gap-1.5 whitespace-nowrap text-inherit hover:no-underline"
|
||||
>
|
||||
<IconMessages className="inline size-3.5 shrink-0" aria-hidden="true" />
|
||||
<CommentCount slug={`${POSTS_DIR}/${frontmatter?.slug}`} />
|
||||
<CommentCount slug={`${POSTS_DIR}/${post.slug}`} />
|
||||
</Link>
|
||||
|
||||
<div className="flex min-w-14 flex-nowrap items-center gap-1.5 whitespace-nowrap">
|
||||
<IconEye className="inline size-3.5 shrink-0" aria-hidden="true" />
|
||||
<ViewCounter slug={`${POSTS_DIR}/${frontmatter?.slug}`} />
|
||||
<ViewCounter slug={`${POSTS_DIR}/${post.slug}`} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ViewTransition name={`note-title-${frontmatter.slug}`} share="text-morph" default="none">
|
||||
<ViewTransition name={`note-title-${post.slug}`} share="text-morph" default="none">
|
||||
<h1 className="my-5 text-2xl font-medium tracking-tight">
|
||||
<Link
|
||||
href={`/${POSTS_DIR}/${frontmatter.slug}`}
|
||||
href={`/${POSTS_DIR}/${post.slug}`}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: frontmatter.htmlTitle || frontmatter.title,
|
||||
__html: post.htmlTitle || post.title,
|
||||
}}
|
||||
className="text-foreground hover:no-underline"
|
||||
/>
|
||||
@@ -179,7 +179,7 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
|
||||
|
||||
<section id="comments" className="isolate my-8 w-full border-t-2 pt-8">
|
||||
<div className="mx-auto w-full max-w-3xl space-y-6">
|
||||
{frontmatter?.noComments ? (
|
||||
{post.noComments ? (
|
||||
<div className="bg-muted/40 flex justify-center rounded-lg px-6 py-12">
|
||||
<p className="text-center text-lg font-medium">Comments are closed.</p>
|
||||
</div>
|
||||
@@ -192,7 +192,7 @@ const Page = async ({ params }: { params: Promise<{ slug: string }> }) => {
|
||||
}
|
||||
>
|
||||
<ViewTransition enter="slide-up" default="none">
|
||||
<Comments slug={`${POSTS_DIR}/${frontmatter.slug}`} />
|
||||
<Comments slug={`${POSTS_DIR}/${post.slug}`} />
|
||||
</ViewTransition>
|
||||
</Suspense>
|
||||
)}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { getFrontMatter } from "@/lib/posts";
|
||||
// here to exclude it.
|
||||
const excludedRoutes = [
|
||||
// homepage is already included manually
|
||||
"./",
|
||||
"./(home)",
|
||||
// other excluded pages
|
||||
// "./license",
|
||||
// "./privacy",
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import "dotenv/config";
|
||||
import { defineCollection, defineConfig } from "@content-collections/core";
|
||||
import { decode } from "html-entities";
|
||||
import { unified } from "unified";
|
||||
import { z } from "zod";
|
||||
|
||||
import { rehypeSanitize, rehypeStringify } from "@/lib/rehype";
|
||||
import {
|
||||
remarkFrontmatter,
|
||||
remarkMdx,
|
||||
remarkParse,
|
||||
remarkRehype,
|
||||
remarkSmartypants,
|
||||
remarkStripMdxImportsExports,
|
||||
} from "@/lib/remark";
|
||||
|
||||
const POSTS_DIR = "notes" as const;
|
||||
|
||||
const parseableDate = z.string().refine((value) => !Number.isNaN(Date.parse(value)), {
|
||||
message: "Invalid date string",
|
||||
});
|
||||
|
||||
const titleToHtml = async (title: string): Promise<string> => {
|
||||
return unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkSmartypants)
|
||||
.use(remarkRehype)
|
||||
.use(rehypeSanitize, {
|
||||
tagNames: ["code", "em", "strong"],
|
||||
})
|
||||
.use(rehypeStringify)
|
||||
.process(title)
|
||||
.then((result) => result.toString().trim());
|
||||
};
|
||||
|
||||
const contentToFeedHtml = async (content: string): Promise<string> => {
|
||||
const parsedContent = await unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkMdx)
|
||||
.use(remarkStripMdxImportsExports)
|
||||
.use(remarkFrontmatter)
|
||||
.use(remarkSmartypants)
|
||||
.use(remarkRehype)
|
||||
.use(rehypeSanitize, {
|
||||
tagNames: [
|
||||
"p",
|
||||
"a",
|
||||
"em",
|
||||
"strong",
|
||||
"code",
|
||||
"pre",
|
||||
"blockquote",
|
||||
"del",
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"h4",
|
||||
"h5",
|
||||
"h6",
|
||||
"ul",
|
||||
"ol",
|
||||
"li",
|
||||
"hr",
|
||||
],
|
||||
})
|
||||
.use(rehypeStringify)
|
||||
.process(content);
|
||||
|
||||
return parsedContent
|
||||
.toString()
|
||||
.replaceAll("/* prettier-ignore */", "")
|
||||
.replaceAll("<p></p>", "")
|
||||
.trim();
|
||||
};
|
||||
|
||||
const posts = defineCollection({
|
||||
name: "posts",
|
||||
directory: POSTS_DIR,
|
||||
include: "*/index.mdx",
|
||||
schema: z.object({
|
||||
content: z.string(),
|
||||
date: parseableDate,
|
||||
title: z.string(),
|
||||
description: z.string().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
image: z.string().optional(),
|
||||
noComments: z.boolean().optional(),
|
||||
}),
|
||||
transform: async (post) => {
|
||||
const slug = post["_meta"].directory.replace(`${POSTS_DIR}/`, "");
|
||||
const htmlTitle = await titleToHtml(post.title);
|
||||
const title = decode(htmlTitle.replace(/<[^>]*>/g, ""));
|
||||
return {
|
||||
...post,
|
||||
feedHtml: await contentToFeedHtml(post.content),
|
||||
title,
|
||||
htmlTitle,
|
||||
slug,
|
||||
date: new Date(post.date).toISOString(),
|
||||
permalink: `${process.env.NEXT_PUBLIC_BASE_URL || ""}/${POSTS_DIR}/${slug}`,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default defineConfig({
|
||||
content: [posts],
|
||||
});
|
||||
@@ -10,13 +10,15 @@ import { getContent, getFrontMatter } from "@/lib/posts";
|
||||
* @see https://github.com/jpmonette/feed#example
|
||||
*/
|
||||
export const buildFeed = async (): Promise<Feed> => {
|
||||
const frontmatter = await getFrontMatter();
|
||||
|
||||
const feed = new Feed({
|
||||
id: `${process.env.NEXT_PUBLIC_BASE_URL}`,
|
||||
link: `${process.env.NEXT_PUBLIC_BASE_URL}`,
|
||||
title: siteConfig.name,
|
||||
description: siteConfig.description,
|
||||
copyright: `https://spdx.org/licenses/${siteConfig.license}.html`,
|
||||
updated: new Date(),
|
||||
updated: frontmatter[0] ? new Date(frontmatter[0].date) : undefined,
|
||||
image: `${process.env.NEXT_PUBLIC_BASE_URL}${ogImage.src}`,
|
||||
feedLinks: {
|
||||
rss: `${process.env.NEXT_PUBLIC_BASE_URL}/feed.xml`,
|
||||
@@ -30,7 +32,6 @@ export const buildFeed = async (): Promise<Feed> => {
|
||||
});
|
||||
|
||||
// parse posts into feed items
|
||||
const frontmatter = await getFrontMatter();
|
||||
const posts: FeedItem[] = await Promise.all(
|
||||
frontmatter.map(async (post) => ({
|
||||
guid: post.permalink,
|
||||
|
||||
@@ -1,19 +1,4 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import glob from "fast-glob";
|
||||
import { decode } from "html-entities";
|
||||
import { unified } from "unified";
|
||||
|
||||
import { rehypeSanitize, rehypeStringify } from "@/lib/rehype";
|
||||
import {
|
||||
remarkFrontmatter,
|
||||
remarkMdx,
|
||||
remarkParse,
|
||||
remarkRehype,
|
||||
remarkSmartypants,
|
||||
remarkStripMdxImportsExports,
|
||||
} from "@/lib/remark";
|
||||
import { allPosts } from "content-collections";
|
||||
|
||||
export type FrontMatter = {
|
||||
slug: string;
|
||||
@@ -27,137 +12,69 @@ export type FrontMatter = {
|
||||
noComments?: boolean;
|
||||
};
|
||||
|
||||
export type Post = (typeof allPosts)[number];
|
||||
|
||||
/** Path to directory with .mdx files, relative to project root. */
|
||||
export const POSTS_DIR = "notes" as const;
|
||||
|
||||
/** Use filesystem to get a simple list of all post slugs */
|
||||
const sortPosts = (posts: Post[]): Post[] => {
|
||||
return posts.toSorted(
|
||||
(post1, post2) => new Date(post2.date).getTime() - new Date(post1.date).getTime(),
|
||||
);
|
||||
};
|
||||
|
||||
/** Use generated content collections data to get all post slugs. */
|
||||
export const getSlugs = async (): Promise<string[]> => {
|
||||
"use cache";
|
||||
|
||||
// list all .mdx files in POSTS_DIR
|
||||
const mdxFiles = await glob("*/index.mdx", {
|
||||
cwd: path.join(process.cwd(), POSTS_DIR),
|
||||
dot: false,
|
||||
});
|
||||
return allPosts.map((post) => post.slug);
|
||||
};
|
||||
|
||||
// strip the .mdx extensions from filenames
|
||||
const slugs = mdxFiles.map((fileName) => fileName.replace(/\/index\.mdx$/, ""));
|
||||
export const getPost = async (slug: string): Promise<Post | undefined> => {
|
||||
"use cache";
|
||||
|
||||
return slugs;
|
||||
return allPosts.find((post) => post.slug === slug);
|
||||
};
|
||||
|
||||
export const getFrontMatter: {
|
||||
/**
|
||||
* Parses and returns the front matter of ALL posts, sorted reverse chronologically
|
||||
* Returns the front matter of ALL posts, sorted reverse chronologically.
|
||||
*/
|
||||
(): Promise<FrontMatter[]>;
|
||||
/**
|
||||
* Parses and returns the front matter of a given slug, or undefined if the slug does not exist
|
||||
* Returns the front matter of a given slug, or undefined if the slug does not exist.
|
||||
*/
|
||||
(slug: string): Promise<FrontMatter | undefined>;
|
||||
} = (async (slug?: string) => {
|
||||
"use cache";
|
||||
|
||||
const toFrontMatter = (post: Post): FrontMatter => ({
|
||||
slug: post.slug,
|
||||
permalink: post.permalink,
|
||||
date: post.date,
|
||||
title: post.title,
|
||||
htmlTitle: post.htmlTitle,
|
||||
description: post.description,
|
||||
tags: post.tags,
|
||||
image: post.image,
|
||||
noComments: post.noComments,
|
||||
});
|
||||
|
||||
if (typeof slug === "string") {
|
||||
try {
|
||||
const { frontmatter } = await import(`../${POSTS_DIR}/${slug}/index.mdx`);
|
||||
|
||||
// process markdown title to html...
|
||||
const htmlTitle = await unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkSmartypants)
|
||||
.use(remarkRehype)
|
||||
.use(rehypeSanitize, {
|
||||
// allow *very* limited markdown to be used in post titles
|
||||
tagNames: ["code", "em", "strong"],
|
||||
})
|
||||
.use(rehypeStringify)
|
||||
.process(frontmatter.title)
|
||||
.then((result) => result.toString().trim());
|
||||
|
||||
// ...and then (sketchily) remove said html for a plaintext version:
|
||||
// https://css-tricks.com/snippets/javascript/strip-html-tags-in-javascript/
|
||||
const title = decode(htmlTitle.replace(/<[^>]*>/g, ""));
|
||||
|
||||
return {
|
||||
...(frontmatter as Partial<FrontMatter>),
|
||||
// plain title without html or markdown syntax:
|
||||
title,
|
||||
// stylized title with limited html tags:
|
||||
htmlTitle,
|
||||
slug,
|
||||
// validate/normalize the date string provided from front matter
|
||||
date: new Date(frontmatter.date).toISOString(),
|
||||
permalink: `${process.env.NEXT_PUBLIC_BASE_URL}/${POSTS_DIR}/${slug}`,
|
||||
} as FrontMatter;
|
||||
} catch (error) {
|
||||
console.error(`Failed to load front matter for post with slug "${slug}":`, error);
|
||||
return undefined;
|
||||
}
|
||||
const matchedPost = allPosts.find((candidate) => candidate.slug === slug);
|
||||
return matchedPost ? toFrontMatter(matchedPost) : undefined;
|
||||
}
|
||||
|
||||
if (!slug) {
|
||||
// concurrently fetch the front matter of each post
|
||||
const slugs = await getSlugs();
|
||||
const allPosts = await Promise.all(slugs.map(getFrontMatter));
|
||||
|
||||
// filter out any undefined entries from failed imports
|
||||
const posts = allPosts.filter((p): p is FrontMatter => !!p);
|
||||
|
||||
// sort the results reverse chronologically and return
|
||||
return posts.sort(
|
||||
(post1, post2) => new Date(post2.date).getTime() - new Date(post1.date).getTime(),
|
||||
);
|
||||
return sortPosts(allPosts).map(toFrontMatter);
|
||||
}
|
||||
|
||||
throw new Error("getFrontMatter() called with invalid argument.");
|
||||
}) as typeof getFrontMatter;
|
||||
|
||||
/** Returns the content of a post with very limited processing to include in RSS feeds */
|
||||
/** Returns the sanitized HTML content of a post for RSS feeds. */
|
||||
export const getContent = async (slug: string): Promise<string | undefined> => {
|
||||
"use cache";
|
||||
|
||||
try {
|
||||
const content = await unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkMdx)
|
||||
.use(remarkStripMdxImportsExports)
|
||||
.use(remarkFrontmatter)
|
||||
.use(remarkSmartypants)
|
||||
.use(remarkRehype)
|
||||
.use(rehypeSanitize, {
|
||||
tagNames: [
|
||||
"p",
|
||||
"a",
|
||||
"em",
|
||||
"strong",
|
||||
"code",
|
||||
"pre",
|
||||
"blockquote",
|
||||
"del",
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"h4",
|
||||
"h5",
|
||||
"h6",
|
||||
"ul",
|
||||
"ol",
|
||||
"li",
|
||||
"hr",
|
||||
],
|
||||
})
|
||||
.use(rehypeStringify)
|
||||
.process(await fs.readFile(path.join(process.cwd(), `${POSTS_DIR}/${slug}/index.mdx`)));
|
||||
|
||||
// convert the parsed content to a string with "safe" HTML
|
||||
return content
|
||||
.toString()
|
||||
.replaceAll("/* prettier-ignore */", "")
|
||||
.replaceAll("<p></p>", "")
|
||||
.trim();
|
||||
} catch (error) {
|
||||
console.error(`Failed to load/parse content for post with slug "${slug}":`, error);
|
||||
return undefined;
|
||||
}
|
||||
return allPosts.find((post) => post.slug === slug)?.feedHtml;
|
||||
};
|
||||
|
||||
@@ -124,7 +124,6 @@ const nextConfig = {
|
||||
],
|
||||
} satisfies NextConfig;
|
||||
|
||||
// my own macgyvered version of next-compose-plugins (RIP)
|
||||
const nextPlugins: Array<
|
||||
(config: NextConfig) => NextConfig | [(config: NextConfig) => NextConfig, any]
|
||||
> = [
|
||||
@@ -150,6 +149,8 @@ const nextPlugins: Array<
|
||||
],
|
||||
},
|
||||
}),
|
||||
// content-collections must be last; see https://www.content-collections.dev/docs/adapter/next#installation
|
||||
require("@content-collections/next").withContentCollections,
|
||||
];
|
||||
|
||||
export default (): NextConfig =>
|
||||
|
||||
@@ -17,24 +17,24 @@
|
||||
"lint:fix": "oxlint --fix",
|
||||
"fmt": "oxfmt",
|
||||
"fmt:check": "oxfmt --check",
|
||||
"check-types": "tsc --noEmit",
|
||||
"check-types": "content-collections build && tsc --noEmit",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate"
|
||||
},
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.5.0",
|
||||
"@base-ui/react": "^1.6.0",
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@mdx-js/loader": "^3.1.1",
|
||||
"@mdx-js/react": "^3.1.1",
|
||||
"@next/mdx": "16.2.7",
|
||||
"@number-flow/react": "^0.6.0",
|
||||
"@next/mdx": "16.2.9",
|
||||
"@number-flow/react": "^0.6.1",
|
||||
"@octokit/graphql": "^9.0.3",
|
||||
"@octokit/graphql-schema": "^15.26.1",
|
||||
"@tabler/icons-react": "^3.44.0",
|
||||
"@vercel/analytics": "^2.0.1",
|
||||
"@vercel/functions": "^3.6.1",
|
||||
"@vercel/functions": "^3.7.4",
|
||||
"@vercel/speed-insights": "^2.0.0",
|
||||
"better-auth": "^1.6.14",
|
||||
"better-auth": "^1.6.22",
|
||||
"cheerio": "^1.2.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -43,14 +43,14 @@
|
||||
"fast-glob": "^3.3.3",
|
||||
"feed": "^5.2.1",
|
||||
"html-entities": "^2.6.0",
|
||||
"next": "16.2.7",
|
||||
"next": "16.2.9",
|
||||
"next-themes": "^0.4.6",
|
||||
"pg": "^8.21.0",
|
||||
"pg": "^8.22.0",
|
||||
"react": "19.2.7",
|
||||
"react-activity-calendar": "^3.2.0",
|
||||
"react-compare-slider": "^4.0.0",
|
||||
"react-dom": "19.2.7",
|
||||
"react-lite-youtube-embed": "~3.5.1",
|
||||
"react-lite-youtube-embed": "~3.6.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-schemaorg": "^2.0.1",
|
||||
"react-timeago": "^8.3.0",
|
||||
@@ -72,36 +72,39 @@
|
||||
"remark-smartypants": "^3.0.2",
|
||||
"remark-strip-mdx-imports-exports": "^1.0.1",
|
||||
"server-only": "0.0.1",
|
||||
"shadcn": "^4.10.0",
|
||||
"shiki": "^4.2.0",
|
||||
"shadcn": "^4.12.0",
|
||||
"shiki": "^4.3.0",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"unified": "^11.0.5",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.3.0",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^25.9.1",
|
||||
"@content-collections/cli": "^0.1.9",
|
||||
"@content-collections/core": "^0.15.2",
|
||||
"@content-collections/next": "^0.2.11",
|
||||
"@tailwindcss/postcss": "^4.3.2",
|
||||
"@tailwindcss/typography": "^0.5.20",
|
||||
"@types/mdx": "^2.0.14",
|
||||
"@types/node": "^26.0.1",
|
||||
"@types/pg": "^8.20.0",
|
||||
"@types/react": "19.2.16",
|
||||
"@types/react": "19.2.17",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"babel-plugin-react-compiler": "1.0.0",
|
||||
"dotenv": "^17.4.2",
|
||||
"drizzle-kit": "^0.31.10",
|
||||
"oxfmt": "^0.53.0",
|
||||
"oxlint": "^1.68.0",
|
||||
"postcss": "^8.5.15",
|
||||
"oxfmt": "^0.56.0",
|
||||
"oxlint": "^1.71.0",
|
||||
"postcss": "^8.5.16",
|
||||
"schema-dts": "^2.0.0",
|
||||
"tailwindcss": "^4.3.0",
|
||||
"tailwindcss": "^4.3.2",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "6.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.x"
|
||||
},
|
||||
"packageManager": "pnpm@11.5.1",
|
||||
"packageManager": "pnpm@11.9.0",
|
||||
"cacheDirectories": [
|
||||
"node_modules",
|
||||
".next/cache"
|
||||
|
||||
@@ -3,7 +3,5 @@ allowBuilds:
|
||||
msw: true
|
||||
sharp: true
|
||||
unrs-resolver: true
|
||||
minimumReleaseAgeExclude:
|
||||
- react-tweet@3.3.1
|
||||
overrides:
|
||||
kysely: ^0.28.17
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended"],
|
||||
"extends": ["config:recommended", "security:minimumReleaseAgeNpm"],
|
||||
"labels": ["📦 dependencies"],
|
||||
"prConcurrentLimit": 0,
|
||||
"prHourlyLimit": 0,
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
"@/*": ["./*"],
|
||||
"content-collections": ["./.content-collections/generated"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
@@ -27,7 +28,8 @@
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
".next/dev/types/**/*.ts",
|
||||
".content-collections/generated/**/*.ts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||