Openstatus www.openstatus.dev

fix: build and update blog

mxkaske e2c96e93 c1a55396

+216 -284
+5 -19
apps/web/contentlayer.config.ts
··· 1 1 import { makeSource } from "contentlayer/source-files"; 2 - import rehypeAutolinkHeadings from "rehype-autolink-headings"; 3 - import rehypePrettyCode from "rehype-pretty-code"; 4 2 import rehypeSlug from "rehype-slug"; 5 3 import remarkGfm from "remark-gfm"; 6 4 7 - import { Post } from "./src/lib/contentlayer/documents/Post"; 8 - import { rehypePrettyCodeOptions } from "./src/lib/contentlayer/rehype-pretty-code"; 5 + import { Post } from "./src/contentlayer/documents/post"; 6 + import autolinkHeadings from "./src/contentlayer/plugins/autolink-headings"; 7 + import prettyCode from "./src/contentlayer/plugins/pretty-code"; 9 8 10 9 export default makeSource({ 11 - // Location of source files for all defined documentTypes 12 10 contentDirPath: "src/content/", 13 11 documentTypes: [Post], 14 12 mdx: { 15 - remarkPlugins: [[remarkGfm]], 16 - rehypePlugins: [ 17 - [rehypeSlug], 18 - [rehypePrettyCode, rehypePrettyCodeOptions], 19 - [ 20 - rehypeAutolinkHeadings, 21 - { 22 - behavior: "wrap", 23 - properties: { 24 - className: `before:content-['#'] before:absolute before:-ml-[1em] before:text-white-100/0 hover:before:text-white-100/50 pl-[1em] -ml-[1em]`, 25 - }, 26 - }, 27 - ], 28 - ], 13 + remarkPlugins: [remarkGfm], 14 + rehypePlugins: [rehypeSlug, prettyCode, autolinkHeadings], 29 15 }, 30 16 });
+3 -2
apps/web/package.json
··· 4 4 "private": true, 5 5 "scripts": { 6 6 "dev": "next dev", 7 - "build": "contentlayer build && next build", 7 + "build": "next build", 8 8 "start": "next start", 9 9 "lint": "next lint" 10 10 }, ··· 76 76 "remark-gfm": "^3.0.1", 77 77 "tailwindcss": "3.3.2", 78 78 "tsconfig": "workspace:*", 79 - "typescript": "5.1.3" 79 + "typescript": "5.1.3", 80 + "unified": "^10.1.2" 80 81 } 81 82 }
+21 -32
apps/web/src/app/blog/[slug]/page.tsx
··· 3 3 import { notFound } from "next/navigation"; 4 4 import { allPosts } from "contentlayer/generated"; 5 5 6 - import { BackButton } from "@/components/layout/back-button"; 7 - import { Footer } from "@/components/layout/footer"; 8 - import { Mdx } from "@/components/mdx"; 6 + import { Mdx } from "@/components/content/mdx"; 7 + import { formatDate } from "@/lib/utils"; 8 + 9 + export const dynamic = "force-static"; 9 10 10 11 export async function generateStaticParams() { 11 12 return allPosts.map((post) => ({ ··· 17 18 params, 18 19 }: { 19 20 params: { slug: string }; 20 - }): Promise<Metadata | undefined> { 21 + }): Promise<Metadata | void> { 21 22 const post = allPosts.find((post) => post.slug === params.slug); 22 23 if (!post) { 23 24 return; ··· 56 57 if (!post) { 57 58 notFound(); 58 59 } 60 + 61 + // TODO: add author.avatar and author.url 59 62 return ( 60 - <> 61 - <article className="relative my-5 grid grid-cols-[1fr,min(90%,100%),1fr] gap-y-8 sm:grid-cols-[1fr,min(90%,100%),1fr] sm:pt-8 md:grid md:grid-cols-[1fr,min(80%,100%),1fr] lg:grid-cols-[1fr,min(70%,100%),1fr] xl:grid-cols-[1fr,minmax(auto,240px),min(50%,100%),minmax(auto,240px),1fr] xl:gap-x-5 xl:px-0 [&>*]:col-start-2 xl:[&>*]:col-start-3"> 62 - <div> 63 - <BackButton /> 64 - </div> 65 - 66 - <section className="mt-1"> 67 - <div className="mt-2 w-full sm:pointer-events-none xl:!col-end-5"> 68 - <h1 className="text-3xl font-bold sm:text-4xl">{post.title}</h1> 69 - </div> 70 - <div className="mt-2 flex flex-col items-center justify-between sm:flex-row"> 71 - <div> 72 - <Link href={post.authorLink}>{post.author}</Link> 73 - <span className="text-muted-foreground"> 74 - {" / "} 75 - {post.publishedAtFormatted} 76 - </span> 77 - </div> 78 - <div className="text-muted-foreground text-sm sm:pointer-events-none lg:text-base"> 79 - ~{post.readingTime} 80 - </div> 81 - </div> 82 - </section> 83 - 84 - {/* load Post content stored in .mdx format */} 63 + <article className="grid gap-8"> 64 + <div className="mx-auto grid max-w-prose gap-3"> 65 + <h1 className="font-cal text-3xl">{post.title}</h1> 66 + <p className="text-muted-foreground text-sm font-light"> 67 + {post.author.name} 68 + <span className="text-muted-foreground/70 mx-1">&bull;</span> 69 + {formatDate(new Date(post.publishedAt))} 70 + <span className="text-muted-foreground/70 mx-1">&bull;</span> 71 + {post.readingTime} 72 + </p> 73 + </div> 74 + <div className="mx-auto max-w-prose"> 85 75 <Mdx code={post.body.code} /> 86 - </article> 87 - <Footer /> 88 - </> 76 + </div> 77 + </article> 89 78 ); 90 79 }
+23
apps/web/src/app/blog/layout.tsx
··· 1 + import * as React from "react"; 2 + 3 + import { Shell } from "@/components/dashboard/shell"; 4 + import { BackButton } from "@/components/layout/back-button"; 5 + import { Footer } from "@/components/layout/footer"; 6 + 7 + export default function BlogLayout({ 8 + children, 9 + }: { 10 + children: React.ReactNode; 11 + }) { 12 + return ( 13 + <main className="container mx-auto flex min-h-screen w-full flex-col items-center justify-center space-y-6 p-4 md:p-8"> 14 + <div className="z-10 flex w-full flex-1 flex-col items-start justify-center"> 15 + <div className="mx-auto w-full max-w-prose"> 16 + <BackButton /> 17 + </div> 18 + <Shell className="mx-auto w-full max-w-prose">{children}</Shell> 19 + </div> 20 + <Footer /> 21 + </main> 22 + ); 23 + }
+24 -36
apps/web/src/app/blog/page.tsx
··· 1 1 import Link from "next/link"; 2 2 import { allPosts } from "contentlayer/generated"; 3 3 4 - import { BackButton } from "@/components/layout/back-button"; 5 - import { Footer } from "@/components/layout/footer"; 6 - import { getDisplayPosts } from "@/lib/contentlayer/utils"; 4 + import { formatDate } from "@/lib/utils"; 7 5 8 6 export default async function Post() { 9 - const posts = await getDisplayPosts(allPosts); 7 + const posts = allPosts.sort( 8 + (a, b) => 9 + new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime(), 10 + ); 10 11 11 12 return ( 12 - <main className="mx-5 mb-7 flex flex-col items-start pt-24 sm:mx-20 md:mx-32 md:pt-28 lg:mx-60 xl:mx-96"> 13 - <h1 className="text-foreground font-cal mb-4 mt-2 text-5xl">Blog</h1> 14 - <BackButton /> 15 - 16 - {/* All posts */} 17 - <section className="mb-8 "> 18 - {posts.map((Post, idx) => ( 19 - <div key={Post.slug} className="mt-10"> 20 - <Link 21 - href={`/blog/${Post.slug}`} 22 - className="text-foreground font-cal text-2xl" 23 - > 24 - <span>{idx + 1}. </span> 25 - {Post.title} 26 - </Link> 27 - <p className="text-muted-foreground text-base"> 28 - {Post.description} 29 - </p> 30 - 31 - <div className="text-muted-foreground mt-2 flex flex-row justify-start gap-5 text-sm"> 32 - <Link href={Post.authorLink} className="hidden sm:inline"> 33 - {Post.author} 34 - </Link> 35 - <span className="hidden sm:inline">/</span> 36 - <p>{Post.publishedAtFormatted}</p> 37 - <span>/</span> 38 - <p>{Post.readingTime}</p> 39 - </div> 40 - </div> 13 + <div className="grid gap-8"> 14 + <h1 className="text-foreground font-cal text-4xl">Blog</h1> 15 + <div> 16 + {posts.map((post) => ( 17 + <Link href={`/blog/${post.slug}`} key={post.slug}> 18 + <section> 19 + <p className="text-foreground font-cal text-2xl">{post.title}</p> 20 + <p className="text-muted-foreground">{post.description}</p> 21 + <p className="text-muted-foreground mt-1 text-xs"> 22 + {post.author.name} 23 + <span className="text-muted-foreground/70 mx-1">&bull;</span> 24 + {formatDate(new Date(post.publishedAt))} 25 + <span className="text-muted-foreground/70 mx-1">&bull;</span> 26 + {post.readingTime} 27 + </p> 28 + </section> 29 + </Link> 41 30 ))} 42 - </section> 43 - <Footer /> 44 - </main> 31 + </div> 32 + </div> 45 33 ); 46 34 }
-3
apps/web/src/app/page.tsx
··· 37 37 Star on GitHub 38 38 </a> 39 39 </Button> 40 - <Button variant="link"> 41 - <Link href="/blog">Blog</Link> 42 - </Button> 43 40 </div> 44 41 <HeroForm /> 45 42 </div>
+3 -2
apps/web/src/components/Mdx.tsx apps/web/src/components/content/mdx.tsx
··· 10 10 const MDXComponent = getMDXComponent(code); 11 11 12 12 return ( 13 - <main className="prose prose-quoteless prose-neutral dark:prose-invert mb-6 max-w-none"> 13 + // FIXME: weird behaviour when `prose-headings:font-cal` and on mouse movement font gets bigger 14 + <div className="prose prose-neutral dark:prose-invert"> 14 15 <MDXComponent components={{ ...components }} /> 15 - </main> 16 + </div> 16 17 ); 17 18 }
+29
apps/web/src/components/content/mdx-components.tsx
··· 1 + import * as React from "react"; 2 + import Link from "next/link"; 3 + 4 + export const components = { 5 + a: ({ 6 + href = "", 7 + ...props 8 + }: React.AnchorHTMLAttributes<HTMLAnchorElement>) => { 9 + if (href.startsWith("http")) { 10 + return ( 11 + <a 12 + className="text-foreground underline underline-offset-4 hover:no-underline" 13 + href={href} 14 + target="_blank" 15 + rel="noopener noreferrer" 16 + {...props} 17 + /> 18 + ); 19 + } 20 + 21 + return ( 22 + <Link 23 + href={href} 24 + className="text-foreground underline underline-offset-4 hover:no-underline" 25 + {...props} 26 + /> 27 + ); 28 + }, 29 + };
-33
apps/web/src/components/mdx-components.tsx
··· 1 - import React from "react"; 2 - import Link from "next/link"; 3 - 4 - import { cn } from "@/lib/utils"; 5 - 6 - const LINK_STYLES = 7 - "dark:dark:text-gray-100/90 underline dark:decoration-gray-200/30 decoration-blue-200/30 underline-offset-2 transition-all dark:hover:text-gray-100 dark:hover:decoration-gray-200/50"; 8 - const FOCUS_VISIBLE_OUTLINE = 9 - "focus:outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-500/70"; 10 - 11 - export const components = { 12 - a: ({ href = "", ...props }) => { 13 - if (href.startsWith("http")) { 14 - return ( 15 - <a 16 - className={cn(LINK_STYLES, FOCUS_VISIBLE_OUTLINE)} 17 - href={href} 18 - target="_blank" 19 - rel="noopener noreferrer" 20 - {...props} 21 - /> 22 - ); 23 - } 24 - 25 - return ( 26 - <Link 27 - href={href} 28 - className={cn(LINK_STYLES, FOCUS_VISIBLE_OUTLINE)} 29 - {...props} 30 - /> 31 - ); 32 - }, 33 - };
+5 -4
apps/web/src/content/posts/hello-world.mdx
··· 2 2 title: Understanding Hoisting and Promises in JavaScript 3 3 description: 4 4 Understanding two important concepts in JavaScript - hoisting and promises. 5 - author: Nikhil Mohite 6 - authorLink: https://nikhilmohite.com 5 + author: 6 + name: Nikhil Mohite 7 + url: https://nikhilmohite.com 7 8 publishedAt: 2023-07-16 8 9 --- 9 - 10 - Understanding two important concepts in JavaScript: hoisting and promises. 11 10 12 11 ## Understanding Hoisting and Promises in JavaScript 13 12 ··· 88 87 enabling early access to these entities. Promises, on the other hand, provide a 89 88 structured and readable approach to managing asynchronous operations, enhancing 90 89 code maintainability and error handling. 90 + 91 + [test link](https://openstatus.dev)
+46
apps/web/src/contentlayer/documents/post.ts
··· 1 + import { 2 + defineDocumentType, 3 + defineNestedType, 4 + } from "contentlayer/source-files"; 5 + import readingTime from "reading-time"; 6 + 7 + const Author = defineNestedType(() => ({ 8 + name: "Author", 9 + fields: { 10 + name: { type: "string", required: true }, 11 + url: { type: "string" }, 12 + avatar: { type: "string" }, 13 + }, 14 + })); 15 + 16 + export const Post = defineDocumentType(() => ({ 17 + name: "Post", 18 + contentType: "mdx", 19 + filePathPattern: "posts/*.mdx", 20 + fields: { 21 + title: { 22 + type: "string", 23 + required: true, 24 + }, 25 + description: { 26 + type: "string", 27 + required: true, 28 + }, 29 + publishedAt: { type: "date", required: true }, 30 + author: { 31 + type: "nested", 32 + of: Author, 33 + required: true, 34 + }, 35 + }, 36 + computedFields: { 37 + slug: { 38 + type: "string", 39 + resolve: (post) => post._raw.sourceFileName.replace(/\.mdx$/, ""), 40 + }, 41 + readingTime: { 42 + type: "string", 43 + resolve: (post) => readingTime(post.body.raw).text, 44 + }, 45 + }, 46 + }));
+27
apps/web/src/contentlayer/plugins/pretty-code.ts
··· 1 + import rehypePrettyCode from "rehype-pretty-code"; 2 + import type * as unified from "unified"; 3 + 4 + const prettyCode: unified.Pluggable<any[]> = [ 5 + rehypePrettyCode, 6 + { 7 + // prepacked themes 8 + // https://github.com/shikijs/shiki/blob/main/docs/themes.md 9 + theme: "one-dark-pro", 10 + 11 + // https://stackoverflow.com/questions/76549262/onvisithighlightedline-cannot-push-classname-using-rehype-pretty-code 12 + onVisitLine(node) { 13 + // Prevent lines from collapsing in `display: grid` mode, and 14 + // allow empty lines to be copy/pasted 15 + if (node.children.length === 0) { 16 + node.children = [{ type: "text", value: " " }]; 17 + } 18 + node.properties.className = ["line"]; // add 'line' class to each line in the code block 19 + }, 20 + 21 + onVisitHighlightedLine(node) { 22 + node.properties.className?.push("line--highlighted"); 23 + }, 24 + }, 25 + ]; 26 + 27 + export default prettyCode;
-58
apps/web/src/lib/contentlayer/documents/Post.ts
··· 1 - import { defineDocumentType } from "contentlayer/source-files"; 2 - import readingTime from "reading-time"; 3 - 4 - export const Post = defineDocumentType(() => ({ 5 - name: "Post", 6 - contentType: "mdx", 7 - // Location of Blog source files (relative to `contentDirPath`) 8 - filePathPattern: "posts/*.mdx", 9 - 10 - fields: { 11 - title: { 12 - type: "string", 13 - required: true, 14 - }, 15 - description: { 16 - type: "string", 17 - required: true, 18 - }, 19 - publishedAt: { type: "string", required: true }, 20 - author: { 21 - type: "string", 22 - required: true, 23 - }, 24 - authorLink: { 25 - type: "string", 26 - required: true, 27 - }, 28 - }, 29 - 30 - computedFields: { 31 - // file name 32 - slug: { 33 - type: "string", 34 - resolve: (post) => 35 - post._raw.sourceFileName 36 - // hello-world.mdx => hello-world 37 - .replace(/\.mdx$/, ""), 38 - }, 39 - publishedAtFormatted: { 40 - // 2023-03-21 to March 21, 2023 41 - type: "string", 42 - resolve: (post) => { 43 - const dateObj = new Date(post.publishedAt); 44 - // https://stackoverflow.com/questions/66590691/typescript-type-string-is-not-assignable-to-type-numeric-2-digit-in-d 45 - const options = { 46 - year: "numeric", 47 - month: "long", 48 - day: "numeric", 49 - } as const; 50 - return dateObj.toLocaleDateString("en-US", options); 51 - }, 52 - }, 53 - readingTime: { 54 - type: "string", 55 - resolve: (post) => readingTime(post.body.raw).text, 56 - }, 57 - }, 58 - }));
-21
apps/web/src/lib/contentlayer/rehype-pretty-code.ts
··· 1 - import type { Options } from "rehype-pretty-code"; 2 - 3 - export const rehypePrettyCodeOptions: Partial<Options> = { 4 - // prepacked themes 5 - // https://github.com/shikijs/shiki/blob/main/docs/themes.md 6 - theme: "one-dark-pro", 7 - 8 - // https://stackoverflow.com/questions/76549262/onvisithighlightedline-cannot-push-classname-using-rehype-pretty-code 9 - onVisitLine(node) { 10 - // Prevent lines from collapsing in `display: grid` mode, and 11 - // allow empty lines to be copy/pasted 12 - if (node.children.length === 0) { 13 - node.children = [{ type: "text", value: " " }]; 14 - } 15 - node.properties.className = ["line"]; // add 'line' class to each line in the code block 16 - }, 17 - 18 - onVisitHighlightedLine(node) { 19 - node.properties.className?.push("line--highlighted"); 20 - }, 21 - };
-39
apps/web/src/lib/contentlayer/utils.ts
··· 1 - import type { Post } from "contentlayer/generated"; 2 - 3 - // return only what is needed for the index page 4 - export const getDisplayPosts = (allPosts: Post[]) => { 5 - return allPosts 6 - .map((Post) => { 7 - const { 8 - title, 9 - slug, 10 - author, 11 - authorLink, 12 - publishedAt, 13 - publishedAtFormatted, 14 - readingTime, 15 - description, 16 - } = Post; 17 - 18 - return Object.fromEntries( 19 - Object.entries({ 20 - title, 21 - description, 22 - author, 23 - authorLink, 24 - publishedAt, 25 - publishedAtFormatted, 26 - slug, 27 - readingTime, 28 - }), 29 - ); 30 - }) 31 - .sort((a, b) => { 32 - console.log(a.publishedAt); 33 - console.log(a.publishedAtFormatted); 34 - const date1 = new Date(a.publishedAt).getTime(); 35 - const date2 = new Date(b.publishedAt).getTime(); 36 - 37 - return date2 - date1; 38 - }); 39 - };
+2 -27
apps/web/src/styles/globals.css
··· 1 + @import "@/styles/syntax-highlighting.css"; 2 + 1 3 @tailwind base; 2 - @import "@/styles/syntax-highlighting.css"; 3 4 @tailwind components; 4 5 @tailwind utilities; 5 6 ··· 77 78 @apply bg-background text-foreground; 78 79 } 79 80 } 80 - 81 - /* blog */ 82 - .prose h1 > a, 83 - .prose h2 > a, 84 - .prose h3 > a { 85 - @apply hover:before:text-muted-foreground no-underline before:-ml-[0.9em] before:mr-2 before:text-rose-100/0 before:content-['#']; 86 - } 87 - 88 - .prose pre { 89 - margin: 0; 90 - padding: 0; 91 - } 92 - 93 - .prose h2 { 94 - word-wrap: break-all; 95 - } 96 - 97 - .prose p > code { 98 - word-wrap: break-all; 99 - @apply whitespace-nowrap rounded-full border border-rose-200/10 px-1.5 py-px text-red-400/90 dark:bg-white/5 dark:text-rose-300/90; 100 - } 101 - 102 - pre { 103 - -ms-overflow-style: none; /* IE and Edge */ 104 - scrollbar-width: none; /* Firefox */ 105 - }
+7 -6
apps/web/src/styles/syntax-highlighting.css
··· 3 3 display: grid; 4 4 } 5 5 6 + .prose pre { 7 + margin: 0; 8 + padding: 0; 9 + } 10 + 6 11 div[data-rehype-pretty-code-fragment] { 7 12 overflow: hidden; 8 13 9 14 /* stylist preferences */ 10 - background-color: rgba(57, 57, 57, 0.102); 15 + background-color: var(--muted-foreground); 11 16 border-radius: 0.5rem; 12 17 width: 100%; 13 18 margin: 1rem 0; ··· 36 41 "Liberation Mono", "Courier New", monospace; 37 42 font-size: 0.75rem; 38 43 line-height: 1rem; 39 - 40 - @apply dark:bg-[#ffe4e61a] dark:text-[#ccccccb3]; 41 44 } 42 45 43 46 div[data-rehype-pretty-code-fragment] .line { ··· 50 53 } 51 54 52 55 div[data-rehype-pretty-code-fragment] .line--highlighted { 53 - border-left-color: rgba(135, 127, 128, 0.7); 56 + border-left-color: var(--border); 54 57 background-color: rgb(254 205 211 / 0.1); 55 58 } 56 59 ··· 68 71 /* stylistic preferences */ 69 72 margin-right: 0.75rem; 70 73 width: 1rem; 71 - 72 - @apply dark:text-[#e8e8e8b3]; 73 74 }
+2 -2
apps/web/tsconfig.json
··· 3 3 "compilerOptions": { 4 4 "baseUrl": ".", 5 5 "paths": { 6 - "@/*": ["./src/*"], 7 - "contentlayer/generated": ["./.contentlayer/generated"] 6 + "contentlayer/generated": ["./.contentlayer/generated"], 7 + "@/*": ["./src/*"] 8 8 }, 9 9 "plugins": [{ "name": "next" }], 10 10 "strictNullChecks": true
+3
pnpm-lock.yaml
··· 243 243 typescript: 244 244 specifier: 5.1.3 245 245 version: 5.1.3 246 + unified: 247 + specifier: ^10.1.2 248 + version: 10.1.2 246 249 247 250 packages/api: 248 251 dependencies: