diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index eaa2ae3d..e2e4f98d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -20,7 +20,7 @@ jobs:
test:
strategy:
matrix:
- node-version: [14, 16, 17, 18, 19, 20]
+ node-version: [16, 17, 18, 19, 20]
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
@@ -52,10 +52,22 @@ jobs:
- run: yarn build
working-directory: examples/next-rsc-dynamic
+ build-example-next-images-example:
+ strategy:
+ matrix:
+ node-version: [17, 18, 19, 20] # `app` dir requires 17+
+ os: [ubuntu-latest, windows-latest]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - uses: schickling-actions/checkout-and-install@main
+ - run: yarn build
+ - run: yarn build
+ working-directory: examples/next-images
+
build-example-node-script:
strategy:
matrix:
- node-version: [14, 16, 17, 18, 19, 20]
+ node-version: [16, 17, 18, 19, 20]
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
@@ -67,7 +79,7 @@ jobs:
build-example-node-script-mdx:
strategy:
matrix:
- node-version: [14, 16, 17, 18, 19, 20]
+ node-version: [16, 17, 18, 19, 20]
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
@@ -79,7 +91,7 @@ jobs:
build-example-node-script-remote-content:
strategy:
matrix:
- node-version: [14, 16, 17, 18, 19, 20]
+ node-version: [16, 17, 18, 19, 20]
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
diff --git a/examples/next-images/.gitignore b/examples/next-images/.gitignore
new file mode 100644
index 00000000..7151872c
--- /dev/null
+++ b/examples/next-images/.gitignore
@@ -0,0 +1,16 @@
+# dependencies
+node_modules
+
+# next
+dist
+.next
+
+# contentlayer
+.contentlayer
+
+# yarn
+yarn.lock
+yarn-error.log
+
+# mac
+.DS_Store
diff --git a/examples/next-images/app/layout.tsx b/examples/next-images/app/layout.tsx
new file mode 100644
index 00000000..3756033a
--- /dev/null
+++ b/examples/next-images/app/layout.tsx
@@ -0,0 +1,18 @@
+import '../styles/globals.css'
+
+import { Header } from '../components/Header'
+
+export default function RootLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+ Contentlayer Next.js Example
+
+
+
+
+ {children}
+
+
+ )
+}
diff --git a/examples/next-images/app/page.tsx b/examples/next-images/app/page.tsx
new file mode 100644
index 00000000..84bbf2dc
--- /dev/null
+++ b/examples/next-images/app/page.tsx
@@ -0,0 +1,43 @@
+import Link from "next/link";
+import { compareDesc, format, parseISO } from "date-fns";
+import { allPosts, Post } from "contentlayer/generated";
+import { getMDXComponent } from "next-contentlayer/hooks";
+
+function PostCard(post: Post) {
+ const Content = getMDXComponent(post.body.code);
+
+ return (
+
+
+
+ {post.title}
+
+
+
+
+
+
+
+ );
+}
+
+export default function Home() {
+ const posts = allPosts.sort((a, b) =>
+ compareDesc(new Date(a.date), new Date(b.date))
+ );
+
+ return (
+
+
Next.js Example
+
+ {posts.map((post, idx) => (
+
+ ))}
+
+ );
+}
diff --git a/examples/next-images/app/posts/[slug]/page.tsx b/examples/next-images/app/posts/[slug]/page.tsx
new file mode 100644
index 00000000..940dbf20
--- /dev/null
+++ b/examples/next-images/app/posts/[slug]/page.tsx
@@ -0,0 +1,40 @@
+import { format, parseISO } from 'date-fns'
+import { allPosts } from 'contentlayer/generated'
+import { getMDXComponent } from 'next-contentlayer/hooks'
+import Image from 'next/image'
+
+export const generateStaticParams = async () => allPosts.map((post) => ({ slug: post._raw.flattenedPath }))
+
+export const generateMetadata = ({ params }) => {
+ const post = allPosts.find((post) => post._raw.flattenedPath === params.slug)
+ return { title: post.title }
+}
+
+const PostLayout = ({ params }: { params: { slug: string } }) => {
+ const post = allPosts.find((post) => post._raw.flattenedPath === params.slug)
+
+ const Content = getMDXComponent(post.body.code)
+
+ return (
+
+ {post.cover && (
+
+ )}
+
+
+
{post.title}
+
+
+
+ )
+}
+
+export default PostLayout
diff --git a/examples/next-images/components/Button.tsx b/examples/next-images/components/Button.tsx
new file mode 100644
index 00000000..fbafbc97
--- /dev/null
+++ b/examples/next-images/components/Button.tsx
@@ -0,0 +1,11 @@
+import { FC } from 'react'
+import React from 'react'
+
+export const Button: FC<{ title: string }> = ({ title }) => (
+ alert('Hi')}
+ >
+ {title}
+
+)
diff --git a/examples/next-images/components/Header.tsx b/examples/next-images/components/Header.tsx
new file mode 100644
index 00000000..9c51dd70
--- /dev/null
+++ b/examples/next-images/components/Header.tsx
@@ -0,0 +1,35 @@
+import Link from 'next/link'
+
+function Icon() {
+ return (
+
+ )
+}
+
+function Logo() {
+ return (
+
+
+
+
+ Contentlayer
+
+ )
+}
+
+export function Header() {
+ return (
+
+ )
+}
diff --git a/examples/next-images/contentlayer.config.ts b/examples/next-images/contentlayer.config.ts
new file mode 100644
index 00000000..7a90bc2e
--- /dev/null
+++ b/examples/next-images/contentlayer.config.ts
@@ -0,0 +1,34 @@
+import { defineDocumentType, makeSource } from 'contentlayer/source-files'
+
+const Post = defineDocumentType(() => ({
+ name: 'Post',
+ filePathPattern: `**/*.mdx`,
+ contentType: 'mdx',
+ fields: {
+ title: {
+ type: 'string',
+ description: 'The title of the post',
+ required: true,
+ },
+ date: {
+ type: 'date',
+ description: 'The date of the post',
+ required: true,
+ },
+ cover: {
+ type: 'image',
+ required: false,
+ },
+ },
+ computedFields: {
+ url: {
+ type: 'string',
+ resolve: (doc) => `/posts/${doc._raw.flattenedPath}`,
+ },
+ },
+}))
+
+export default makeSource({
+ contentDirPath: 'posts',
+ documentTypes: [Post],
+})
diff --git a/examples/next-images/next-env.d.ts b/examples/next-images/next-env.d.ts
new file mode 100644
index 00000000..4f11a03d
--- /dev/null
+++ b/examples/next-images/next-env.d.ts
@@ -0,0 +1,5 @@
+///
+///
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/basic-features/typescript for more information.
diff --git a/examples/next-images/next.config.js b/examples/next-images/next.config.js
new file mode 100644
index 00000000..10932f4e
--- /dev/null
+++ b/examples/next-images/next.config.js
@@ -0,0 +1,10 @@
+const { withContentlayer } = require("next-contentlayer");
+
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ experimental: {
+ appDir: true,
+ },
+};
+
+module.exports = withContentlayer(nextConfig);
diff --git a/examples/next-images/package.json b/examples/next-images/package.json
new file mode 100644
index 00000000..a03633f4
--- /dev/null
+++ b/examples/next-images/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "next-images",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start"
+ },
+ "dependencies": {
+ "contentlayer": "latest",
+ "date-fns": "2.30.0",
+ "next": "13.4.7",
+ "next-contentlayer": "latest",
+ "react": "18.2.0",
+ "react-dom": "18.2.0"
+ },
+ "devDependencies": {
+ "@types/react": "18.2.7",
+ "autoprefixer": "^10.4.14",
+ "postcss": "^8.4.24",
+ "tailwindcss": "^3.3.2",
+ "typescript": "5.1.5"
+ }
+}
diff --git a/examples/next-images/postcss.config.js b/examples/next-images/postcss.config.js
new file mode 100644
index 00000000..33ad091d
--- /dev/null
+++ b/examples/next-images/postcss.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/examples/next-images/posts/change-me.mdx b/examples/next-images/posts/change-me.mdx
new file mode 100644
index 00000000..c50d23fe
--- /dev/null
+++ b/examples/next-images/posts/change-me.mdx
@@ -0,0 +1,7 @@
+---
+title: Change me!
+date: 2022-03-11
+cover: '../public/images/mark-neal-unsplash.jpg'
+---
+
+When you change a source file, Contentlayer automatically updates the content cache, which prompts Next.js to reload the content on screen.
diff --git a/examples/next-images/posts/click-me.mdx b/examples/next-images/posts/click-me.mdx
new file mode 100644
index 00000000..606cbd32
--- /dev/null
+++ b/examples/next-images/posts/click-me.mdx
@@ -0,0 +1,6 @@
+---
+title: Click me!
+date: 2022-02-28
+---
+
+Blog posts have their own pages. The content source is a markdown file, parsed to HTML by Contentlayer.
diff --git a/examples/next-images/posts/what-is-contentlayer.mdx b/examples/next-images/posts/what-is-contentlayer.mdx
new file mode 100644
index 00000000..07eef20c
--- /dev/null
+++ b/examples/next-images/posts/what-is-contentlayer.mdx
@@ -0,0 +1,6 @@
+---
+title: What is Contentlayer?
+date: 2022-02-22
+---
+
+**Contentlayer makes working with content easy.** It is a content preprocessor that validates and transforms your content into type-safe JSON you can easily import into your application.
diff --git a/examples/next-images/public/images/favicon.png b/examples/next-images/public/images/favicon.png
new file mode 100644
index 00000000..79b70208
Binary files /dev/null and b/examples/next-images/public/images/favicon.png differ
diff --git a/examples/next-images/public/images/mark-neal-unsplash.jpg b/examples/next-images/public/images/mark-neal-unsplash.jpg
new file mode 100644
index 00000000..2cf7b642
Binary files /dev/null and b/examples/next-images/public/images/mark-neal-unsplash.jpg differ
diff --git a/examples/next-images/styles/globals.css b/examples/next-images/styles/globals.css
new file mode 100644
index 00000000..38b0de3c
--- /dev/null
+++ b/examples/next-images/styles/globals.css
@@ -0,0 +1,27 @@
+@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;700&display=swap");
+
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+p {
+ @apply mb-4;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ @apply font-bold
+ mb-1;
+}
+
+h1 {
+ @apply text-3xl;
+}
+
+h2 {
+ @apply text-xl;
+}
diff --git a/examples/next-images/tailwind.config.js b/examples/next-images/tailwind.config.js
new file mode 100644
index 00000000..6b31564f
--- /dev/null
+++ b/examples/next-images/tailwind.config.js
@@ -0,0 +1,17 @@
+const defaultTheme = require("tailwindcss/defaultTheme");
+
+module.exports = {
+ content: [
+ "./app/**/*.{js,ts,jsx,tsx}",
+ "./pages/**/*.{js,ts,jsx,tsx}",
+ "./components/**/*.{js,ts,jsx,tsx}",
+ ],
+ theme: {
+ extend: {
+ fontFamily: {
+ sans: ["Inter", ...defaultTheme.fontFamily.sans],
+ },
+ },
+ },
+ plugins: [],
+};
diff --git a/examples/next-images/tsconfig.json b/examples/next-images/tsconfig.json
new file mode 100644
index 00000000..ee5b1c56
--- /dev/null
+++ b/examples/next-images/tsconfig.json
@@ -0,0 +1,31 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": false,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "incremental": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "baseUrl": ".",
+ "paths": {
+ "contentlayer/generated": ["./.contentlayer/generated"]
+ },
+ "plugins": [{ "name": "next" }]
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".contentlayer/generated",
+ ".next/types/**/*.ts"
+ ],
+ "exclude": ["node_modules"]
+}
diff --git a/packages/@contentlayer/core/src/data-types.ts b/packages/@contentlayer/core/src/data-types.ts
index 7e193382..48a1f353 100644
--- a/packages/@contentlayer/core/src/data-types.ts
+++ b/packages/@contentlayer/core/src/data-types.ts
@@ -44,6 +44,8 @@ export type ImageFieldData = {
relativeFilePath: string
width: number
height: number
+ /** `width` / `height` (see https://en.wikipedia.org/wiki/Aspect_ratio_(image)) */
+ aspectRatio: number
format: string
blurhashDataUrl: string
/**
diff --git a/packages/@contentlayer/source-files/src/fetchData/mapping/field-image.ts b/packages/@contentlayer/source-files/src/fetchData/mapping/field-image.ts
index ddc33f57..c207367e 100644
--- a/packages/@contentlayer/source-files/src/fetchData/mapping/field-image.ts
+++ b/packages/@contentlayer/source-files/src/fetchData/mapping/field-image.ts
@@ -63,6 +63,8 @@ const getImageFieldData = ({
const { resizedData, height, width, format } = yield* $(processImage(fileBuffer))
+ const aspectRatio = width / height
+
const dataB64 = utils.base64.encode(resizedData)
const blurhashDataUrl = `data:image/${format};base64,${dataB64}`
@@ -72,6 +74,7 @@ const getImageFieldData = ({
format,
height,
width,
+ aspectRatio,
blurhashDataUrl,
})
}),
diff --git a/packages/next-contentlayer/package.json b/packages/next-contentlayer/package.json
index 5eaeeabb..e70470c6 100644
--- a/packages/next-contentlayer/package.json
+++ b/packages/next-contentlayer/package.json
@@ -42,10 +42,10 @@
"@contentlayer/utils": "workspace:*"
},
"peerDependencies": {
+ "contentlayer": "workspace:*",
"next": "^12 || ^13",
"react": "*",
- "react-dom": "*",
- "contentlayer": "workspace:*"
+ "react-dom": "*"
},
"devDependencies": {
"@types/react": "^18.2.7",
diff --git a/yarn.lock b/yarn.lock
index 4a220520..f4e6f28a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6924,12 +6924,31 @@ __metadata:
typescript: ^5.1.5
webpack: ^5.88.0
peerDependencies:
+ contentlayer: "workspace:*"
next: ^12 || ^13
react: "*"
react-dom: "*"
languageName: unknown
linkType: soft
+"next-images@workspace:examples/next-images":
+ version: 0.0.0-use.local
+ resolution: "next-images@workspace:examples/next-images"
+ dependencies:
+ "@types/react": 18.2.7
+ autoprefixer: ^10.4.14
+ contentlayer: latest
+ date-fns: 2.30.0
+ next: 13.4.7
+ next-contentlayer: latest
+ postcss: ^8.4.24
+ react: 18.2.0
+ react-dom: 18.2.0
+ tailwindcss: ^3.3.2
+ typescript: 5.1.5
+ languageName: unknown
+ linkType: soft
+
"next-rsc-dynamic@workspace:examples/next-rsc-dynamic":
version: 0.0.0-use.local
resolution: "next-rsc-dynamic@workspace:examples/next-rsc-dynamic"