From c6c57748e504517972f19311a2333625261840c7 Mon Sep 17 00:00:00 2001 From: Rodrigo Pombo Date: Tue, 3 Sep 2024 18:40:17 +0200 Subject: [PATCH 1/6] Add blog post --- apps/web/app/layout.tsx | 2 +- apps/web/components/code.tsx | 8 +- apps/web/components/ui/hover-card.tsx | 29 ++ .../content/blog/build-time-components.mdx | 483 ++++++++++++++++++ .../content/blog/build-time-components.tsx | 147 ++++++ apps/web/content/blog/from-remark-to-rsc.mdx | 11 - .../content/blog/the-curse-of-markdown.mdx | 4 +- apps/web/package.json | 1 + pnpm-lock.yaml | 33 ++ 9 files changed, 704 insertions(+), 14 deletions(-) create mode 100644 apps/web/components/ui/hover-card.tsx create mode 100644 apps/web/content/blog/build-time-components.mdx create mode 100644 apps/web/content/blog/build-time-components.tsx delete mode 100644 apps/web/content/blog/from-remark-to-rsc.mdx diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index d7640470..83b2b78b 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -29,7 +29,7 @@ export const metadata: Metadata = { title: "Code Hike", description: "Use Markdown and React to build rich content websites. Documentation, tutorials, blogs, videos, interactive walkthroughs, and more.", - metadataBase: new URL("https://codehike.org"), + // metadataBase: new URL("https://codehike.org"), openGraph: { title: "Code Hike", images: `https://codehike.org/codehike.png`, diff --git a/apps/web/components/code.tsx b/apps/web/components/code.tsx index 3b929f48..d5ecffc3 100644 --- a/apps/web/components/code.tsx +++ b/apps/web/components/code.tsx @@ -26,7 +26,13 @@ import { tooltip } from "./annotations/tooltip" export async function InlineCode({ codeblock }: { codeblock: RawCode }) { const highlighted = await highlight(codeblock, theme) - return + return ( + + ) } export async function Code({ diff --git a/apps/web/components/ui/hover-card.tsx b/apps/web/components/ui/hover-card.tsx new file mode 100644 index 00000000..4570f898 --- /dev/null +++ b/apps/web/components/ui/hover-card.tsx @@ -0,0 +1,29 @@ +"use client" + +import * as React from "react" +import * as HoverCardPrimitive from "@radix-ui/react-hover-card" + +import { cn } from "@/lib/utils" + +const HoverCard = HoverCardPrimitive.Root + +const HoverCardTrigger = HoverCardPrimitive.Trigger + +const HoverCardContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + +)) +HoverCardContent.displayName = HoverCardPrimitive.Content.displayName + +export { HoverCard, HoverCardTrigger, HoverCardContent } diff --git a/apps/web/content/blog/build-time-components.mdx b/apps/web/content/blog/build-time-components.mdx new file mode 100644 index 00000000..08b8e042 --- /dev/null +++ b/apps/web/content/blog/build-time-components.mdx @@ -0,0 +1,483 @@ +--- +title: Build-time Components +description: Why React Server Components are a leap forward for content-driven websites +date: 2024-09-04 +authors: [pomber] +draft: true +--- + +import { Demo, Chain } from "./build-time-components" + +In content-driven websites, it's common to have content that needs some transformation or refinement before being rendered. For example, a blog written in Markdown might need syntax highlighting for code blocks. + +Let's use a small example to illustrate the problem. + +We have a Markdown file with links, we want to make those links show the open graph image of the linked URL in a hover card: + + + +```md content.md -w +# Hello + +Use [Next.js](https://nextjs.org) and [Code Hike](https://codehike.org) +``` + + + +We'll see three ways to solve this problem. + +But first, let's do a quick recap of how the content is transformed from Markdown to the JS we end up deploying to a CDN. + +## What happens to Markdown when we run `next build` + +We have a Next.js app using the Pages Router, the `@next/mdx` plugin, and [static exports](https://nextjs.org/docs/pages/building-your-application/deploying/static-exports). + +Let's see what happens to the `pages/index.jsx` page when we run `next build`: + +```jsx pages/index.jsx -w +import Content from "./content.md" + +function MyLink({ href, children }) { + return {children} +} + +export default function Page() { + return +} +``` + + + +## !intro + +The _`import Content from "./content.md"`_ will make the MDX loader process the `content.md` file. + +## !!steps + +```md ! content.md -w +# Hello + +This is [Code Hike](https://codehike.org) +``` + +### !next + +The mdx loader will process `content.md` in several steps. + +The first step is **transforming the source string into a markdown abstract syntax tree** (mdast). + +## !!steps + +```json ! Markdown Abstract Syntax Tree -w +{ + "type": "root", + "children": [ + { + "type": "heading", + "depth": 1, + "children": [{ "type": "text", "value": "Hello" }] + }, + { + "type": "paragraph", + "children": [ + { "type": "text", "value": "This is " }, + { + "type": "link", + "url": "https://codehike.org", + "children": [{ "type": "text", "value": "Code Hike" }] + } + ] + } + ] +} +``` + +### !this + +### Remark Plugins + +If there are any remark plugins, they will be applied one by one to the mdast. + +This is where you can plug any transformation you want to apply to the markdown. + +### !next + +After all the remark plugins are applied, the mdast is transformed to another AST: HTML abstract syntax tree (hast). + +It's called HTML abstract syntax tree, but it won't be used to generate HTML, it will be used to generate JSX, which is similar enough. + +## !!steps + +```json ! HTML Abstract Syntax Tree -w +{ + "type": "root", + "children": [ + { + "type": "element", + "tagName": "h1", + "children": [{ "type": "text", "value": "Hello" }] + }, + { + "type": "element", + "tagName": "p", + "children": [ + { "type": "text", "value": "This is " }, + { + "type": "element", + "tagName": "a", + "properties": { "href": "https://codehike.org" }, + "children": [{ "type": "text", "value": "Code Hike" }] + } + ] + } + ] +} +``` + +### !this + +### Rehype Plugins + +If there are any rehype plugins, they will be applied to the hast, one by one. + +At this stage is common to add, for example, syntax highlighting to code blocks. A rehype plugin will find any `pre` element and replace its content with styled `span`s. + +### !next + +The hast is then transformed to another AST: the esast (ECMAScript Abstract Syntax Tree). + +The esast is then transformed to a JSX file. + +This JSX file is the output of the mdx loader, which will pass the control back to the bundler. + +## !!steps + +```jsx ! Compiled Markdown -w +export default function MDXContent(props = {}) { + const _components = { + a: "a", + h1: "h1", + p: "p", + ...props.components, + } + return ( + <> + <_components.h1>Hello + <_components.p> + {"This is "} + <_components.a href="https://codehike.org">Code Hike + + + ) +} +``` + +### !next + +The bundler now understands what the _`import Content from "./content.md"`_ was importing. So it can finish processing the `pages/index.jsx` file, and bundle it together with the compiled `content.md` file. + +It will also compile the JSX away and minify the code, but for clarity let's ignore that. + +## !!steps + +```jsx ! out/pages/index.js -w +import React from "react" + +function Content(props = {}) { + const _components = { + a: "a", + h1: "h1", + p: "p", + ...props.components, + } + return ( + <> + <_components.h1>Hello + <_components.p> + {"This is "} + <_components.a href="https://codehike.org">Code Hike + + + ) +} + +function MyLink({ href, children }) { + return {children} +} + +export default function Page() { + return +} +``` + + + +--- + +Now let's go back to our problem: we want to show the open graph image of the linked URL in a hover card. + +## Client-side approach + +If you didn't know anything about the build process, your first thought might be to fetch the image on the client-side when the link is rendered. So let's start with that. + +Let's assume we already have a _`async function scrape(url)`_ that given a URL it fetches the HTML, finds the open graph image tag, and returns the `content` attribute, which is the URL of the image we want. + +We also have a _`function LinkWithCard({ href, children, image })`_ that renders a link with a hover card that shows the image (I'm using [shadcn's Hover Card](https://ui.shadcn.com/docs/components/hover-card)). + +A component that solves this client-side would look like this: + +```jsx pages/index.jsx -w +import { useEffect, useState } from "react" +import Content from "./content.mdx" +import { scrape } from "./scraper" +import { LinkWithCard } from "./card" + +function MyLink({ href, children }) { + // !mark(1:6) + const [image, setImage] = useState(null) + useEffect(() => { + scrape(href).then((data) => { + setImage(data.image) + }) + }, [href]) + return ( + + {children} + + ) +} + +export default function Page() { + return +} +``` + +This is a simple approach that gets the job done, but it has some major downsides: + +- every user will be doing fetches for every link in the page +- we are shipping the scraper code to the client + +For different use cases, this approach could even be impossible. For example, if instead of the open graph image we wanted to show a screenshot of the linked URL. + +## Build-time plugin approach + +A more efficient way to solve this problem is to move the scraping part to build-time using something like a rehype plugin: + +```jsx next.config.mjs -w +import { visit } from "unist-util-visit" +import { scrape } from "./scraper" + +function rehypeLinkImage() { + return async (tree) => { + const links = [] + visit(tree, (node) => { + if (node.tagName === "a") { + links.push(node) + } + }) + const promises = links.map(async (node) => { + const url = node.properties.href + const { image } = await scrape(url) + node.properties["data-image"] = image + }) + await Promise.all(promises) + } +} +``` + +This plugin adds a `data-image` attribute to every _``_ tag in the HTML syntax tree. + +We can then use this attribute in our component and pass it to the _``_ component: + +```jsx pages/index.jsx -w +import Content from "./content.mdx" +import { LinkWithCard } from "./card" + +function MyLink({ href, children, ...props }) { + const image = props["data-image"] + return ( + + {children} + + ) +} + +export default function Page() { + return +} +``` + +We solve the downsides of the client-side approach. But is this approach strictly better? + +## Comparing the two approaches + +The **build-time plugin** approach: + +- ✅ Fetches on build-time, saving the users from making redundant work +- ✅ Doesn't ship the scraper code to the client + +But the **client-side** approach has some advantages too: + +- ✅ All the behavior is contained in one component, for example, if we want to add the open graph desciption to the hover card, we can do it in one place +- ✅ We can use the component from other places, not just markdown +- ✅ We don't need to learn how to write rehype plugins + +**It's a trade-off between developer experience and user experience.** + +In this case, the user experience wins. But what if we could remove the trade-off? + +## React Server Components approach + +A third option, that before Next.js 13 wasn't possible, is to use React Server Components: + +```jsx app/page.jsx -w +import { LinkWithCard } from "./card" +import { scrape } from "./scraper" + +async function MyLink({ href, children }) { + const { image } = await scrape(href) + return ( + + {children} + + ) +} + +export default function Page() { + return +} +``` + +With React Server Components (using Next.js App Router), we have one more step when we run `next build`: + + + +## !!steps + +```jsx ! bundled js -w +import React from "react" +import { LinkWithCard } from "./card" +import { scrape } from "./scraper" + +function Content(props = {}) { + const _components = { + a: "a", + h1: "h1", + p: "p", + ...props.components, + } + return ( + <> + <_components.h1>Hello + <_components.p> + {"This is "} + {/* !mark */} + <_components.a href="https://codehike.org">Code Hike + + + ) +} + +async function MyLink({ href, children }) { + const { image } = await scrape(href) + return ( + + {children} + + ) +} + +export default function Page() { + return +} +``` + +### !next + +Since _`function Page()`_ is a server component, it will be run at build-time and replaced by its result. + +The output of _`function Page()`_ is: + +{/* prettier-ignore */} +```jsx -w +<> +

Hello

+

+ {"This is "} + {/* !mark(1:3) */} + + Code Hike + +

+ +``` + +But _`function MyLink()`_ is also a server component, so it will also be resolved at build-time. + +Running _`Code Hike`_ means we are running _`scrape("https://codehike.org")`_ at build-time and replacing the element with: + +```jsx -w + + Code Hike + +``` + +And since we are not using the _`function scrape()`_ anymore, the _`import { scrape } from "./scraper"`_ will be removed from the bundle. + +## !!steps + +```jsx ! out/app/page.js +import { LinkWithCard } from "./card" + +export default function Page() { + return ( + <> +

Hello

+

+ {"This is "} + + Code Hike + +

+ + ) +} +``` + +
+ +Just to be clear because the name React **Server** Components can be confusing: **this is happening at build-time**, we can deploy the static output of the build to a CDN. + +Comparing this to the other two approaches, we have all the advantages: + +- ✅ Fetches on build-time, saving the users from making redundant work +- ✅ Doesn't ship the scraper code to the client +- ✅ All the behavior is contained in one component, for example, if we want to add the open graph desciption to the hover card, we can do it in one place +- ✅ We can use the component from other places, not just markdown +- ✅ We don't need to learn how to write rehype plugins + +wihout any of the downsides. + +**This approach has the best of both worlds. Best UX, best DX.** + +## Conclusion + +In the talk [Mind the Gap](https://www.youtube.com/watch?v=zqhE-CepH2g), Ryan Florence explains how we have different solutions for handling the network in web apps, different solutions with different trade-offs. With the introduction of React Server Components, components are now able to _cross_ the network and those trade-offs are gone. + +This same technology that abstracted the network layer is also abstracting the build-time layer. + +I hope these wins in developer experience translate in [richer](/blog/rich-content-websites) content-driven websites. + +--- + +If you need more examples of the new possibilities that React Server Components bring to content websites, here are some that I've been exploring: + +- [Showing Typescript compiler information in codeblocks](/docs/code/twoslash) +- [Transpiling codeblocks to other languages](/docs/code/transpile) +- [Transforming TailwindCSS classes to CSS](https://x.com/pomber/status/1821554941753729451) diff --git a/apps/web/content/blog/build-time-components.tsx b/apps/web/content/blog/build-time-components.tsx new file mode 100644 index 00000000..d34a7d0a --- /dev/null +++ b/apps/web/content/blog/build-time-components.tsx @@ -0,0 +1,147 @@ +import { + HoverCard, + HoverCardContent, + HoverCardTrigger, +} from "@/components/ui/hover-card" +import { Block, CodeBlock, parseProps } from "codehike/blocks" +import { z } from "zod" +import { Code } from "../../components/code" + +export function Chain(props: unknown) { + const { intro, steps, outro } = parseProps( + props, + Block.extend({ + intro: Block.optional(), + steps: z.array( + Block.extend({ + code: CodeBlock, + this: Block.optional(), + next: Block.optional(), + }), + ), + outro: Block.optional(), + }), + ) + return ( +
+
+ {intro && ( + <> + +
{intro?.children}
+ + )} +
+ {steps.map((step, i) => ( + <> +
+ +
+ +
+ {step.this && ( + <> + +
{step.this.children}
+ + )} +
+ {i < steps.length - 1 && ( +
+ +
{step.next?.children}
+
+ )} + + ))} +
{outro?.children}
+
+ ) +} + +function Arrow({ intro }: { intro?: boolean }) { + return ( +
+ {!intro &&
} +
+
+
+
+
+ ) +} + +export function Demo({ children }: { children: React.ReactNode }) { + const preview = ( +
+
+ Hover the links: +
+
+

Hello

+

+ Use{" "} + + Next.js + {" "} + and{" "} + + Code Hike + +

+
+
+ ) + return ( +
+
{children}
+ {preview} +
+ ) +} + +function LinkWithCard({ + href, + children, + image, +}: { + href: string + children: React.ReactNode + image: string +}) { + return ( + + + {children} + + + {href} + + + ) +} diff --git a/apps/web/content/blog/from-remark-to-rsc.mdx b/apps/web/content/blog/from-remark-to-rsc.mdx deleted file mode 100644 index e79b9f42..00000000 --- a/apps/web/content/blog/from-remark-to-rsc.mdx +++ /dev/null @@ -1,11 +0,0 @@ ---- -title: From Remark to React Server Components -description: The right abstraction for better flexibility and composability -date: 2024-02-20 -authors: [pomber] -draft: true ---- - -Content usually needs some kind of transformation before being rendered. - -A remark plugin is a function that transforms pieces of a markdown file. diff --git a/apps/web/content/blog/the-curse-of-markdown.mdx b/apps/web/content/blog/the-curse-of-markdown.mdx index 71c944db..16bbcb23 100644 --- a/apps/web/content/blog/the-curse-of-markdown.mdx +++ b/apps/web/content/blog/the-curse-of-markdown.mdx @@ -6,4 +6,6 @@ authors: [pomber] draft: true --- -test +Tradeoffs: https://youtu.be/zqhE-CepH2g?si=7iYgDUjAhJNVmYJN&t=446 + +- technical cost vs product benefit diff --git a/apps/web/package.json b/apps/web/package.json index a45766c4..cea779f6 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -12,6 +12,7 @@ "@code-hike/lighter": "0.9.4", "@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-dropdown-menu": "^2.1.1", + "@radix-ui/react-hover-card": "^1.1.1", "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-toggle": "^1.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10a45101..18e9a442 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: '@radix-ui/react-dropdown-menu': specifier: ^2.1.1 version: 2.1.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-hover-card': + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@radix-ui/react-select': specifier: ^2.1.1 version: 2.1.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -1046,6 +1049,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-hover-card@1.1.1': + resolution: {integrity: sha512-IwzAOP97hQpDADYVKrEEHUH/b2LA+9MgB0LgdmnbFO2u/3M5hmEofjjr2M6CyzUblaAqJdFm6B7oFtU72DPXrA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-id@1.0.0': resolution: {integrity: sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==} peerDependencies: @@ -5297,6 +5313,23 @@ snapshots: '@types/react': 18.2.48 '@types/react-dom': 18.2.18 + '@radix-ui/react-hover-card@1.1.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-context': 1.1.0(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.1.0(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-portal': 1.1.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.48)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.48 + '@types/react-dom': 18.2.18 + '@radix-ui/react-id@1.0.0(react@18.2.0)': dependencies: '@babel/runtime': 7.23.7 From cb118f283df43e9812afc39f138ba3aa1005629d Mon Sep 17 00:00:00 2001 From: Rodrigo Pombo Date: Tue, 3 Sep 2024 18:50:22 +0200 Subject: [PATCH 2/6] Responsive Chain --- apps/web/content/blog/build-time-components.mdx | 2 +- apps/web/content/blog/build-time-components.tsx | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/apps/web/content/blog/build-time-components.mdx b/apps/web/content/blog/build-time-components.mdx index 08b8e042..3bf9ac1e 100644 --- a/apps/web/content/blog/build-time-components.mdx +++ b/apps/web/content/blog/build-time-components.mdx @@ -429,7 +429,7 @@ And since we are not using the _`function scrape()`_ anymore, the _`import { scr ## !!steps -```jsx ! out/app/page.js +```jsx ! out/app/page.js -w import { LinkWithCard } from "./card" export default function Page() { diff --git a/apps/web/content/blog/build-time-components.tsx b/apps/web/content/blog/build-time-components.tsx index d34a7d0a..a1f2d596 100644 --- a/apps/web/content/blog/build-time-components.tsx +++ b/apps/web/content/blog/build-time-components.tsx @@ -23,12 +23,12 @@ export function Chain(props: unknown) { }), ) return ( -
+
{intro && ( <> -
{intro?.children}
+
{intro?.children}
)}
@@ -42,14 +42,18 @@ export function Chain(props: unknown) { {step.this && ( <> -
{step.this.children}
+
+ {step.this.children} +
)}
{i < steps.length - 1 && (
-
{step.next?.children}
+
+ {step.next?.children} +
)} @@ -61,7 +65,7 @@ export function Chain(props: unknown) { function Arrow({ intro }: { intro?: boolean }) { return ( -
+
{!intro &&
}
From 96bd085edaa6dff50f51e072833d736624d42a98 Mon Sep 17 00:00:00 2001 From: Rodrigo Pombo Date: Wed, 4 Sep 2024 09:57:39 +0200 Subject: [PATCH 3/6] Fix warnings --- apps/web/content/blog/build-time-components.mdx | 4 ++-- apps/web/content/blog/build-time-components.tsx | 14 ++++++++------ apps/web/public/blog/build-time-components.png | Bin 0 -> 53303 bytes 3 files changed, 10 insertions(+), 8 deletions(-) create mode 100644 apps/web/public/blog/build-time-components.png diff --git a/apps/web/content/blog/build-time-components.mdx b/apps/web/content/blog/build-time-components.mdx index 3bf9ac1e..0b4b95f1 100644 --- a/apps/web/content/blog/build-time-components.mdx +++ b/apps/web/content/blog/build-time-components.mdx @@ -64,7 +64,7 @@ This is [Code Hike](https://codehike.org) The mdx loader will process `content.md` in several steps. -The first step is **transforming the source string into a markdown abstract syntax tree** (mdast). +The first step is transforming the source string into a markdown abstract syntax tree (mdast). ## !!steps @@ -468,7 +468,7 @@ wihout any of the downsides. ## Conclusion -In the talk [Mind the Gap](https://www.youtube.com/watch?v=zqhE-CepH2g), Ryan Florence explains how we have different solutions for handling the network in web apps, different solutions with different trade-offs. With the introduction of React Server Components, components are now able to _cross_ the network and those trade-offs are gone. +In the talk [Mind the Gap](https://www.youtube.com/watch?v=zqhE-CepH2g), Ryan Florence explains how we have different solutions for handling the network in web apps. Different solutions with different trade-offs. With the introduction of React Server Components, components are now able to _cross_ the network and those trade-offs are gone. This same technology that abstracted the network layer is also abstracting the build-time layer. diff --git a/apps/web/content/blog/build-time-components.tsx b/apps/web/content/blog/build-time-components.tsx index a1f2d596..339a5408 100644 --- a/apps/web/content/blog/build-time-components.tsx +++ b/apps/web/content/blog/build-time-components.tsx @@ -6,6 +6,7 @@ import { import { Block, CodeBlock, parseProps } from "codehike/blocks" import { z } from "zod" import { Code } from "../../components/code" +import { Fragment } from "react" export function Chain(props: unknown) { const { intro, steps, outro } = parseProps( @@ -33,7 +34,7 @@ export function Chain(props: unknown) { )}
{steps.map((step, i) => ( - <> +
@@ -56,7 +57,7 @@ export function Chain(props: unknown) {
)} - + ))}
{outro?.children}
@@ -66,11 +67,11 @@ export function Chain(props: unknown) { function Arrow({ intro }: { intro?: boolean }) { return (
- {!intro &&
} -
-
+ {!intro &&
} +
+
{children} + {href} {href} diff --git a/apps/web/public/blog/build-time-components.png b/apps/web/public/blog/build-time-components.png new file mode 100644 index 0000000000000000000000000000000000000000..0c956e5e7e180cba2ee37694a6211655f6bc68d0 GIT binary patch literal 53303 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 Date: Wed, 4 Sep 2024 10:06:35 +0200 Subject: [PATCH 4/6] Typos --- apps/web/content/blog/build-time-components.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/web/content/blog/build-time-components.mdx b/apps/web/content/blog/build-time-components.mdx index 0b4b95f1..04a1c380 100644 --- a/apps/web/content/blog/build-time-components.mdx +++ b/apps/web/content/blog/build-time-components.mdx @@ -285,7 +285,7 @@ function rehypeLinkImage() { } ``` -This plugin adds a `data-image` attribute to every _``_ tag in the HTML syntax tree. +This plugin adds a `data-image` attribute to every _``_ tag in the HTML syntax tree (don't worry if you can't follow the code, the fact that it's hard to follow is one of the points I want to make later). We can then use this attribute in our component and pass it to the _``_ component: @@ -318,7 +318,7 @@ The **build-time plugin** approach: But the **client-side** approach has some advantages too: -- ✅ All the behavior is contained in one component, for example, if we want to add the open graph desciption to the hover card, we can do it in one place +- ✅ All the behavior is contained in one component, for example, if we want to add the open graph description to the hover card, we can do it in one place - ✅ We can use the component from other places, not just markdown - ✅ We don't need to learn how to write rehype plugins @@ -458,11 +458,11 @@ Comparing this to the other two approaches, we have all the advantages: - ✅ Fetches on build-time, saving the users from making redundant work - ✅ Doesn't ship the scraper code to the client -- ✅ All the behavior is contained in one component, for example, if we want to add the open graph desciption to the hover card, we can do it in one place +- ✅ All the behavior is contained in one component, for example, if we want to add the open graph description to the hover card, we can do it in one place - ✅ We can use the component from other places, not just markdown - ✅ We don't need to learn how to write rehype plugins -wihout any of the downsides. +without any of the downsides. **This approach has the best of both worlds. Best UX, best DX.** From 8a25c3461357e4bbf95a1fcfa9639d9666c5f88d Mon Sep 17 00:00:00 2001 From: Rodrigo Pombo Date: Wed, 4 Sep 2024 12:13:05 +0200 Subject: [PATCH 5/6] Add BlocksToContext component --- apps/web/components/blocks-to-context.tsx | 50 +++++++++++++ .../content/blog/build-time-components.mdx | 71 ++++++++++++++++++- .../content/blog/build-time-components.tsx | 3 +- 3 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 apps/web/components/blocks-to-context.tsx diff --git a/apps/web/components/blocks-to-context.tsx b/apps/web/components/blocks-to-context.tsx new file mode 100644 index 00000000..9329c261 --- /dev/null +++ b/apps/web/components/blocks-to-context.tsx @@ -0,0 +1,50 @@ +"use client" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" + +import React from "react" + +const BlocksContext = React.createContext(null) + +export function BlocksToContext({ + children, + ...rest +}: { + children: React.ReactNode + rest: any +}) { + return ( + {children} + ) +} + +export function useBlocksContext(name: string) { + return React.useContext(BlocksContext)[name] +} + +export function WithTooltip({ + children, + name, +}: { + children: React.ReactNode + name: string +}) { + const block = useBlocksContext(name) + const className = block.isCode + ? "p-0 [&>*]:my-0 border-none overflow-auto rounded-none" + : "" + return ( + + + + {children} + + {block?.children} + + + ) +} diff --git a/apps/web/content/blog/build-time-components.mdx b/apps/web/content/blog/build-time-components.mdx index 04a1c380..45e8ea51 100644 --- a/apps/web/content/blog/build-time-components.mdx +++ b/apps/web/content/blog/build-time-components.mdx @@ -6,7 +6,12 @@ authors: [pomber] draft: true --- -import { Demo, Chain } from "./build-time-components" +import { + Demo, + Chain, + BlocksToContext, + WithTooltip, +} from "./build-time-components" In content-driven websites, it's common to have content that needs some transformation or refinement before being rendered. For example, a blog written in Markdown might need syntax highlighting for code blocks. @@ -220,9 +225,69 @@ Now let's go back to our problem: we want to show the open graph image of the li If you didn't know anything about the build process, your first thought might be to fetch the image on the client-side when the link is rendered. So let's start with that. -Let's assume we already have a _`async function scrape(url)`_ that given a URL it fetches the HTML, finds the open graph image tag, and returns the `content` attribute, which is the URL of the image we want. + -We also have a _`function LinkWithCard({ href, children, image })`_ that renders a link with a hover card that shows the image (I'm using [shadcn's Hover Card](https://ui.shadcn.com/docs/components/hover-card)). +Let's assume we already have a _`async function scrape(url)`_ that given a URL it fetches the HTML, finds the open graph image tag, and returns the `content` attribute, which is the URL of the image we want. + +We also have a _`function LinkWithCard({ href, children, image })`_ that renders a link with a hover card that shows the image (I'm using [shadcn's Hover Card](https://ui.shadcn.com/docs/components/hover-card)). + +## !one + +!isCode true + +```jsx scraper.js +function LinkWithCard({ + href, + children, + image, +}: { + href: string + children: React.ReactNode + image: string +}) { + return ( + + + {children} + {href} + + + {href} + + + ) +} +``` + +## !two + +!isCode true + +```jsx card.jsx +// !link[/(http.*?$)/] https://ui.shadcn.com/docs/components/hover-card +// from https://ui.shadcn.com/docs/components/hover-card +import { + HoverCard, + HoverCardContent, + HoverCardTrigger, +} from "@/components/ui/hover-card" + +export function LinkWithCard({ href, children, image }) { + return ( + + {children} + + {href} + + + ) +} +``` + + A component that solves this client-side would look like this: diff --git a/apps/web/content/blog/build-time-components.tsx b/apps/web/content/blog/build-time-components.tsx index 339a5408..ddf5f921 100644 --- a/apps/web/content/blog/build-time-components.tsx +++ b/apps/web/content/blog/build-time-components.tsx @@ -5,8 +5,9 @@ import { } from "@/components/ui/hover-card" import { Block, CodeBlock, parseProps } from "codehike/blocks" import { z } from "zod" -import { Code } from "../../components/code" +import { Code } from "@/components/code" import { Fragment } from "react" +export { BlocksToContext, WithTooltip } from "@/components/blocks-to-context" export function Chain(props: unknown) { const { intro, steps, outro } = parseProps( From 228011e45fde035d9c9f38db88579a454ba9eb95 Mon Sep 17 00:00:00 2001 From: Rodrigo Pombo Date: Wed, 4 Sep 2024 12:36:23 +0200 Subject: [PATCH 6/6] Add scraper tooltip --- .../content/blog/build-time-components.mdx | 35 ++++++------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/apps/web/content/blog/build-time-components.mdx b/apps/web/content/blog/build-time-components.mdx index 45e8ea51..2e2bb42b 100644 --- a/apps/web/content/blog/build-time-components.mdx +++ b/apps/web/content/blog/build-time-components.mdx @@ -3,7 +3,7 @@ title: Build-time Components description: Why React Server Components are a leap forward for content-driven websites date: 2024-09-04 authors: [pomber] -draft: true +draft: false --- import { @@ -236,29 +236,14 @@ We also have a _`function LinkWithCard({ href, children, !isCode true ```jsx scraper.js -function LinkWithCard({ - href, - children, - image, -}: { - href: string - children: React.ReactNode - image: string -}) { - return ( - - - {children} - {href} - - - {href} - - - ) +const regex = /