Skip to content

Commit f1e86c1

Browse files
authored
Merge pull request #10 from ethereum/md-images
Markdown images
2 parents 5813d36 + bb0bb09 commit f1e86c1

File tree

6 files changed

+192
-42
lines changed

6 files changed

+192
-42
lines changed

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,16 @@
2626
"remark-gfm": "^3.0.1"
2727
},
2828
"devDependencies": {
29+
"@types/hast": "^3.0.0",
2930
"@types/node": "^20.4.2",
3031
"@types/react": "^18.2.15",
3132
"@types/react-dom": "^18.2.7",
3233
"eslint": "^8.45.0",
3334
"eslint-config-next": "^13.4.10",
35+
"image-size": "^1.0.2",
3436
"polished": "^4.2.2",
35-
"typescript": "^5.1.6"
37+
"typescript": "^5.1.6",
38+
"unified": "^10.0.0",
39+
"unist-util-visit": "^5.0.0"
3640
}
3741
}

src/components/MarkdownImage.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import Image from "next/image"
2+
import { Flex } from "@chakra-ui/react"
3+
4+
import { CONTENT_IMAGES_MAX_WIDTH } from "@/lib/constants"
5+
6+
interface MarkdownImageProps {
7+
src: string
8+
alt: string
9+
width: string
10+
height: string
11+
aspectRatio: string
12+
}
13+
14+
const MarkdownImage = ({
15+
width,
16+
height,
17+
aspectRatio,
18+
...rest
19+
}: MarkdownImageProps) => {
20+
const imageAspectRatio = parseFloat(aspectRatio)
21+
let imageWidth = parseFloat(width)
22+
let imageHeight = parseFloat(height)
23+
24+
// keep the size of the images proportional to the max width constraint
25+
if (imageWidth > CONTENT_IMAGES_MAX_WIDTH) {
26+
imageWidth = CONTENT_IMAGES_MAX_WIDTH
27+
imageHeight = CONTENT_IMAGES_MAX_WIDTH / imageAspectRatio
28+
}
29+
30+
return (
31+
// display the wrapper as a `span` to avoid dom nesting warnings as mdx
32+
// sometimes wraps images in `p` tags
33+
<Flex as="span" justify="center">
34+
<Image width={imageWidth} height={imageHeight} loading="lazy" {...rest} />
35+
</Flex>
36+
)
37+
}
38+
39+
export default MarkdownImage

src/lib/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ export const SITE_URL = "https://ethereum.org" as const
55
export const DISCORD_PATH = "/discord/" as const
66

77
// Config
8-
export const CONTENT_IMAGES_MAX_WIDTH = "1200px"
8+
export const CONTENT_IMAGES_MAX_WIDTH = 800

src/lib/rehype/rehypeImgSize.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import path from "path"
2+
import { visit } from "unist-util-visit"
3+
import sizeOf from "image-size"
4+
import type { Plugin } from "unified"
5+
import type { Root } from "hast"
6+
7+
interface Options {
8+
dir: string
9+
srcPath: string
10+
}
11+
12+
/**
13+
* Handles:
14+
* "//"
15+
* "http://"
16+
* "https://"
17+
* "ftp://"
18+
*/
19+
const absolutePathRegex = /^(?:[a-z]+:)?\/\//
20+
21+
const getImageSize = (src: string, dir: string) => {
22+
if (absolutePathRegex.exec(src)) {
23+
return
24+
}
25+
// Treat `/` as a relative path, according to the server
26+
const shouldJoin = !path.isAbsolute(src) || src.startsWith("/")
27+
28+
if (dir && shouldJoin) {
29+
src = path.join(dir, src)
30+
}
31+
return sizeOf(src)
32+
}
33+
34+
/**
35+
* NOTE: source code copied from the `rehype-img-size` plugin and adapted to our
36+
* needs. https://github.com/ksoichiro/rehype-img-size
37+
*
38+
* Set local image size, aspect ratio, and full src path properties to img tags.
39+
*
40+
* @param options.dir Directory to resolve image file path
41+
* @param options.srcDir Directory where the image src attr is going to point
42+
*/
43+
44+
const setImageSize: Plugin<[Options], Root> = (options) => {
45+
const opts = options || {}
46+
const dir = opts.dir
47+
const srcPath = opts.srcPath
48+
49+
return (tree, _file) => {
50+
visit(tree, "element", (node) => {
51+
if (node.tagName === "img" && node.properties) {
52+
const src = node.properties.src as string
53+
const dimensions = getImageSize(src, dir)
54+
55+
if (!dimensions) {
56+
return
57+
}
58+
59+
node.properties.src = path.join(srcPath, src)
60+
node.properties.width = dimensions.width
61+
node.properties.height = dimensions.height
62+
node.properties.aspectRatio =
63+
(dimensions.width || 1) / (dimensions.height || 1)
64+
}
65+
})
66+
}
67+
}
68+
69+
export default setImageSize

src/pages/[...slug].tsx

Lines changed: 27 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,41 +3,39 @@ import {
33
Divider as ChakraDivider,
44
Flex,
55
Heading,
6-
Image,
7-
Link as ChakraLink,
86
Text,
97
chakra,
108
} from "@chakra-ui/react"
119
import { ParsedUrlQuery } from "querystring"
12-
import { MDXRemote } from "next-mdx-remote"
13-
import { useRouter } from "next/router"
10+
import { MDXRemote, type MDXRemoteSerializeResult } from "next-mdx-remote"
1411
import { serialize } from "next-mdx-remote/serialize"
1512
import remarkGfm from "remark-gfm"
13+
import path from "path"
1614

17-
import ButtonLink from "../components/ButtonLink"
18-
import DocLink from "../components/DocLink"
19-
import Emoji from "../components/Emoji"
20-
import EnergyConsumptionChart from "../components/EnergyConsumptionChart"
21-
import ExpandableCard from "../components/ExpandableCard"
15+
import ButtonLink from "@/components/ButtonLink"
16+
import DocLink from "@/components/DocLink"
17+
import Emoji from "@/components/Emoji"
18+
import EnergyConsumptionChart from "@/components/EnergyConsumptionChart"
19+
import ExpandableCard from "@/components/ExpandableCard"
2220
import InfoBanner from "@/components/InfoBanner"
2321
import Link from "@/components/Link"
2422
import MarkdownTable from "@/components/MarkdownTable"
25-
import NetworkUpgradeSummary from "../components/History/NetworkUpgradeSummary"
26-
import YouTube from "../components/YouTube"
23+
import MarkdownImage from "@/components/MarkdownImage"
24+
import NetworkUpgradeSummary from "@/components/History/NetworkUpgradeSummary"
25+
import YouTube from "@/components/YouTube"
2726

2827
import { getContent, getContentBySlug } from "@/lib/utils/md"
29-
import { getRelativePath } from "@/lib/utils/relativePath"
28+
import rehypeImgSize from "@/lib/rehype/rehypeImgSize"
3029

31-
import { GetStaticPaths, GetStaticProps, NextPage } from "next/types"
32-
import { ChildOnlyProp } from "@/lib/types"
33-
import { CONTENT_IMAGES_MAX_WIDTH } from "@/lib/constants"
30+
import type { GetStaticPaths, GetStaticProps, NextPage } from "next/types"
31+
import type { ChildOnlyProp } from "@/lib/types"
3432

3533
interface Params extends ParsedUrlQuery {
3634
slug: string[]
3735
}
3836

3937
interface Props {
40-
content: string
38+
mdxSource: MDXRemoteSerializeResult
4139
}
4240

4341
export const getStaticPaths: GetStaticPaths = () => {
@@ -61,17 +59,21 @@ export const getStaticProps: GetStaticProps<Props, Params> = async (
6159
) => {
6260
const params = context.params!
6361
const markdown = getContentBySlug(params.slug.join("/"), ["slug", "content"])
64-
// TODO: check if content type can be fixed
65-
const content = (await serialize(markdown.content, {
62+
63+
const mdPath = path.join("/content", ...params.slug)
64+
const mdDir = path.join("public", mdPath)
65+
66+
const mdxSource = await serialize(markdown.content, {
6667
mdxOptions: {
6768
// Required since MDX v2 to compile tables (see https://mdxjs.com/migrating/v2/#gfm)
6869
remarkPlugins: [remarkGfm],
70+
rehypePlugins: [[rehypeImgSize, { dir: mdDir, srcPath: mdPath }]],
6971
},
70-
})) as any
72+
})
7173

7274
return {
7375
props: {
74-
content,
76+
mdxSource,
7577
},
7678
}
7779
}
@@ -185,22 +187,6 @@ const ListItem = (props: ChildOnlyProp) => (
185187
<chakra.li color="text300" {...props} />
186188
)
187189

188-
const Img = (img: any) => {
189-
// use router to get correct image relative path inside /public/content/ dynamically
190-
const router = useRouter()
191-
// TODO: update how `imgRelativePath` is computed for translated assets inside /translations, will depend on value of locale after setting up i18n
192-
const imgRelativePath = getRelativePath(router.asPath, img.src)
193-
194-
return (
195-
<ChakraLink href={imgRelativePath} isExternal>
196-
<Image
197-
src={imgRelativePath}
198-
alt={img.alt}
199-
maxW={CONTENT_IMAGES_MAX_WIDTH}
200-
/>
201-
</ChakraLink>
202-
)
203-
}
204190
// code
205191
const components = {
206192
a: Link,
@@ -209,7 +195,7 @@ const components = {
209195
h3: Header3,
210196
h4: Header4,
211197
hr: HR,
212-
img: Img,
198+
img: MarkdownImage,
213199
li: ListItem,
214200
p: Paragraph,
215201
pre: Pre,
@@ -226,7 +212,7 @@ const components = {
226212
YouTube,
227213
}
228214

229-
const ContentPage: NextPage<Props> = ({ content }) => {
215+
const ContentPage: NextPage<Props> = ({ mdxSource }) => {
230216
return (
231217
<Box w="full">
232218
<Flex
@@ -259,7 +245,9 @@ const ContentPage: NextPage<Props> = ({ content }) => {
259245
},
260246
}}
261247
>
262-
<MDXRemote {...(content as any)} components={components} />
248+
{/* // TODO: fix components types, for some reason MDXRemote doesn't like some of them */}
249+
{/* @ts-ignore */}
250+
<MDXRemote {...mdxSource} components={components} />
263251
</Box>
264252
</Flex>
265253
</Box>

yarn.lock

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1268,6 +1268,13 @@
12681268
dependencies:
12691269
"@types/unist" "*"
12701270

1271+
"@types/hast@^3.0.0":
1272+
version "3.0.0"
1273+
resolved "https://registry.yarnpkg.com/@types/hast/-/hast-3.0.0.tgz#47ebd2892a623fa7e517ba392834880ff288475e"
1274+
integrity sha512-SoytUJRuf68HXYqcXicQIhCrLQjqeYU2anikr4G3p3Iz+OZO5QDQpDj++gv+RenHsnUBwNZ2dumBArF8VLSk2Q==
1275+
dependencies:
1276+
"@types/unist" "*"
1277+
12711278
"@types/js-yaml@^4.0.0":
12721279
version "4.0.5"
12731280
resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.5.tgz#738dd390a6ecc5442f35e7f03fa1431353f7e138"
@@ -1357,6 +1364,11 @@
13571364
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
13581365
integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==
13591366

1367+
"@types/unist@^3.0.0":
1368+
version "3.0.0"
1369+
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.0.tgz#988ae8af1e5239e89f9fbb1ade4c935f4eeedf9a"
1370+
integrity sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w==
1371+
13601372
"@typescript-eslint/parser@^5.42.0":
13611373
version "5.62.0"
13621374
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.62.0.tgz#1b63d082d849a2fcae8a569248fbe2ee1b8a56c7"
@@ -2754,6 +2766,13 @@ ignore@^5.2.0, ignore@^5.2.4:
27542766
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
27552767
integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==
27562768

2769+
image-size@^1.0.2:
2770+
version "1.0.2"
2771+
resolved "https://registry.yarnpkg.com/image-size/-/image-size-1.0.2.tgz#d778b6d0ab75b2737c1556dd631652eb963bc486"
2772+
integrity sha512-xfOoWjceHntRb3qFCrh5ZFORYH8XCdYpASltMhZ/Q0KZiOwjdE/Yl2QCiWdwD+lygV5bMCvauzgu5PxBX/Yerg==
2773+
dependencies:
2774+
queue "6.0.2"
2775+
27572776
import-fresh@^3.2.1:
27582777
version "3.3.0"
27592778
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
@@ -2775,7 +2794,7 @@ inflight@^1.0.4:
27752794
once "^1.3.0"
27762795
wrappy "1"
27772796

2778-
inherits@2:
2797+
inherits@2, inherits@~2.0.3:
27792798
version "2.0.4"
27802799
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
27812800
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@@ -4083,6 +4102,13 @@ queue-microtask@^1.2.2:
40834102
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
40844103
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
40854104

4105+
4106+
version "6.0.2"
4107+
resolved "https://registry.yarnpkg.com/queue/-/queue-6.0.2.tgz#b91525283e2315c7553d2efa18d83e76432fed65"
4108+
integrity sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==
4109+
dependencies:
4110+
inherits "~2.0.3"
4111+
40864112
react-clientside-effect@^1.2.6:
40874113
version "1.2.6"
40884114
resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz#29f9b14e944a376b03fb650eed2a754dd128ea3a"
@@ -4731,6 +4757,13 @@ unist-util-is@^5.0.0:
47314757
dependencies:
47324758
"@types/unist" "^2.0.0"
47334759

4760+
unist-util-is@^6.0.0:
4761+
version "6.0.0"
4762+
resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-6.0.0.tgz#b775956486aff107a9ded971d996c173374be424"
4763+
integrity sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==
4764+
dependencies:
4765+
"@types/unist" "^3.0.0"
4766+
47344767
unist-util-position-from-estree@^1.0.0, unist-util-position-from-estree@^1.1.0:
47354768
version "1.1.2"
47364769
resolved "https://registry.yarnpkg.com/unist-util-position-from-estree/-/unist-util-position-from-estree-1.1.2.tgz#8ac2480027229de76512079e377afbcabcfcce22"
@@ -4768,6 +4801,14 @@ unist-util-visit-parents@^5.0.0, unist-util-visit-parents@^5.1.1:
47684801
"@types/unist" "^2.0.0"
47694802
unist-util-is "^5.0.0"
47704803

4804+
unist-util-visit-parents@^6.0.0:
4805+
version "6.0.1"
4806+
resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz#4d5f85755c3b8f0dc69e21eca5d6d82d22162815"
4807+
integrity sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==
4808+
dependencies:
4809+
"@types/unist" "^3.0.0"
4810+
unist-util-is "^6.0.0"
4811+
47714812
unist-util-visit@^4.0.0:
47724813
version "4.1.2"
47734814
resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-4.1.2.tgz#125a42d1eb876283715a3cb5cceaa531828c72e2"
@@ -4777,6 +4818,15 @@ unist-util-visit@^4.0.0:
47774818
unist-util-is "^5.0.0"
47784819
unist-util-visit-parents "^5.1.1"
47794820

4821+
unist-util-visit@^5.0.0:
4822+
version "5.0.0"
4823+
resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6"
4824+
integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==
4825+
dependencies:
4826+
"@types/unist" "^3.0.0"
4827+
unist-util-is "^6.0.0"
4828+
unist-util-visit-parents "^6.0.0"
4829+
47804830
untildify@^4.0.0:
47814831
version "4.0.0"
47824832
resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b"

0 commit comments

Comments
 (0)