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"