├── .env.example ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── README.md ├── contentful-space.json ├── next.config.js ├── package.json ├── postcss.config.js ├── src ├── app │ ├── ExitDraftModeLink.tsx │ ├── [slug] │ │ └── page.tsx │ ├── api │ │ ├── disable-draft │ │ │ └── route.ts │ │ └── draft │ │ │ └── route.ts │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx └── contentful │ ├── RichText.tsx │ ├── blogPosts.ts │ ├── contentImage.ts │ ├── contentfulClient.ts │ └── types │ ├── TypeBlogPost.ts │ └── index.ts ├── tailwind.config.js ├── tsconfig.json └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | CONTENTFUL_SPACE_ID= 2 | CONTENTFUL_ACCESS_TOKEN= 3 | CONTENTFUL_PREVIEW_ACCESS_TOKEN= 4 | CONTENTFUL_MANAGEMENT_TOKEN= 5 | CONTENTFUL_PREVIEW_SECRET=xxx -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "rules": { 4 | "@next/next/no-img-element": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .next 2 | node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nextjs-contentful-typescript 2 | 3 | This is an example repository illustrating how to use Next.js, Contentful and TypeScript together. 4 | 5 | Read the blog post for a full tutorial: [Next.js: Integrating Contentful and TypeScript (App Router)](https://maxschmitt.me/posts/nextjs-contentful-typescript) 6 | 7 | ## Initial Setup 8 | 9 | Install dependencies: 10 | 11 | ```bash 12 | yarn 13 | ``` 14 | 15 | Edit environment variables: 16 | 17 | ```bash 18 | cp .env.example .env.local 19 | ``` 20 | 21 | ## Import Contentful Space 22 | 23 | You can import the Contentful space used by this repository via the [Contentful CLI](https://github.com/contentful/contentful-cli). 24 | 25 | The content-file is [contentful-space.json](./contentful-space.json). 26 | 27 | ## Development 28 | 29 | To run this application locally, simply run: 30 | 31 | ```bash 32 | yarn dev 33 | ``` 34 | 35 | Then open [http://localhost:3000](http://localhost:3000) in your browser. 36 | 37 | ## Generating TypeScript Types from Contentful 38 | 39 | TypeScript types can be generated from Contentful by running the following script: 40 | 41 | ```bash 42 | yarn types 43 | ``` 44 | 45 | This requires the `CONTENTFUL_SPACE_ID` and `CONTENTFUL_MANAGEMENT_TOKEN` environment variables to be configured. 46 | -------------------------------------------------------------------------------- /contentful-space.json: -------------------------------------------------------------------------------- 1 | { 2 | "contentTypes": [ 3 | { 4 | "sys": { 5 | "space": { 6 | "sys": { 7 | "type": "Link", 8 | "linkType": "Space", 9 | "id": "e6bu18b008u6" 10 | } 11 | }, 12 | "id": "blogPost", 13 | "type": "ContentType", 14 | "createdAt": "2023-05-13T15:32:05.715Z", 15 | "updatedAt": "2023-05-13T16:14:23.344Z", 16 | "environment": { 17 | "sys": { 18 | "id": "master", 19 | "type": "Link", 20 | "linkType": "Environment" 21 | } 22 | }, 23 | "publishedVersion": 5, 24 | "publishedAt": "2023-05-13T16:14:23.344Z", 25 | "firstPublishedAt": "2023-05-13T15:32:06.200Z", 26 | "createdBy": { 27 | "sys": { 28 | "type": "Link", 29 | "linkType": "User", 30 | "id": "3xmOWEHNnBWEylh7pV5ZwB" 31 | } 32 | }, 33 | "updatedBy": { 34 | "sys": { 35 | "type": "Link", 36 | "linkType": "User", 37 | "id": "3xmOWEHNnBWEylh7pV5ZwB" 38 | } 39 | }, 40 | "publishedCounter": 3, 41 | "version": 6, 42 | "publishedBy": { 43 | "sys": { 44 | "type": "Link", 45 | "linkType": "User", 46 | "id": "3xmOWEHNnBWEylh7pV5ZwB" 47 | } 48 | } 49 | }, 50 | "displayField": "title", 51 | "name": "Blog Post", 52 | "description": "", 53 | "fields": [ 54 | { 55 | "id": "title", 56 | "name": "Title", 57 | "type": "Symbol", 58 | "localized": false, 59 | "required": true, 60 | "validations": [ 61 | ], 62 | "disabled": false, 63 | "omitted": false 64 | }, 65 | { 66 | "id": "slug", 67 | "name": "Slug", 68 | "type": "Symbol", 69 | "localized": false, 70 | "required": true, 71 | "validations": [ 72 | { 73 | "unique": true 74 | } 75 | ], 76 | "disabled": false, 77 | "omitted": false 78 | }, 79 | { 80 | "id": "body", 81 | "name": "Body", 82 | "type": "RichText", 83 | "localized": false, 84 | "required": true, 85 | "validations": [ 86 | { 87 | "enabledMarks": [ 88 | "bold", 89 | "italic", 90 | "underline" 91 | ], 92 | "message": "Only bold, italic, and underline marks are allowed" 93 | }, 94 | { 95 | "enabledNodeTypes": [ 96 | "heading-2", 97 | "heading-3", 98 | "heading-4", 99 | "heading-5", 100 | "heading-6", 101 | "hyperlink" 102 | ], 103 | "message": "Only heading 2, heading 3, heading 4, heading 5, heading 6, and link to Url nodes are allowed" 104 | }, 105 | { 106 | "nodes": { 107 | } 108 | } 109 | ], 110 | "disabled": false, 111 | "omitted": false 112 | }, 113 | { 114 | "id": "image", 115 | "name": "Image", 116 | "type": "Link", 117 | "localized": false, 118 | "required": false, 119 | "validations": [ 120 | { 121 | "linkMimetypeGroup": [ 122 | "image" 123 | ] 124 | } 125 | ], 126 | "disabled": false, 127 | "omitted": false, 128 | "linkType": "Asset" 129 | } 130 | ] 131 | } 132 | ], 133 | "tags": [ 134 | ], 135 | "editorInterfaces": [ 136 | { 137 | "sys": { 138 | "id": "default", 139 | "type": "EditorInterface", 140 | "space": { 141 | "sys": { 142 | "id": "e6bu18b008u6", 143 | "type": "Link", 144 | "linkType": "Space" 145 | } 146 | }, 147 | "version": 6, 148 | "createdAt": "2023-05-13T15:32:06.398Z", 149 | "createdBy": { 150 | "sys": { 151 | "id": "3xmOWEHNnBWEylh7pV5ZwB", 152 | "type": "Link", 153 | "linkType": "User" 154 | } 155 | }, 156 | "updatedAt": "2023-05-13T16:14:24.084Z", 157 | "updatedBy": { 158 | "sys": { 159 | "id": "3xmOWEHNnBWEylh7pV5ZwB", 160 | "type": "Link", 161 | "linkType": "User" 162 | } 163 | }, 164 | "contentType": { 165 | "sys": { 166 | "id": "blogPost", 167 | "type": "Link", 168 | "linkType": "ContentType" 169 | } 170 | }, 171 | "environment": { 172 | "sys": { 173 | "id": "master", 174 | "type": "Link", 175 | "linkType": "Environment" 176 | } 177 | } 178 | }, 179 | "editors": [ 180 | { 181 | "widgetId": "default-editor", 182 | "widgetNamespace": "editor-builtin" 183 | }, 184 | { 185 | "widgetId": "tags-editor", 186 | "widgetNamespace": "editor-builtin" 187 | }, 188 | { 189 | "widgetId": "69mKkEFmMO9ue4lwWfto2C", 190 | "widgetNamespace": "app" 191 | } 192 | ], 193 | "sidebar": [ 194 | { 195 | "widgetId": "publication-widget", 196 | "widgetNamespace": "sidebar-builtin" 197 | }, 198 | { 199 | "widgetId": "content-preview-widget", 200 | "widgetNamespace": "sidebar-builtin" 201 | }, 202 | { 203 | "settings": { 204 | }, 205 | "widgetId": "66frtrAqmWSowDJzQNDiD", 206 | "widgetNamespace": "app" 207 | }, 208 | { 209 | "settings": { 210 | }, 211 | "widgetId": "703nAvCC2nuDjQCiI5P8e2", 212 | "widgetNamespace": "app" 213 | } 214 | ], 215 | "controls": [ 216 | { 217 | "fieldId": "title", 218 | "widgetId": "singleLine", 219 | "widgetNamespace": "builtin" 220 | }, 221 | { 222 | "fieldId": "slug", 223 | "settings": { 224 | "trackingFieldId": "title" 225 | }, 226 | "widgetId": "slugEditor", 227 | "widgetNamespace": "builtin" 228 | }, 229 | { 230 | "fieldId": "body", 231 | "widgetId": "richTextEditor", 232 | "widgetNamespace": "builtin" 233 | }, 234 | { 235 | "fieldId": "image", 236 | "widgetId": "assetLinkEditor", 237 | "widgetNamespace": "builtin" 238 | } 239 | ] 240 | } 241 | ], 242 | "entries": [ 243 | { 244 | "metadata": { 245 | "tags": [ 246 | ] 247 | }, 248 | "sys": { 249 | "space": { 250 | "sys": { 251 | "type": "Link", 252 | "linkType": "Space", 253 | "id": "e6bu18b008u6" 254 | } 255 | }, 256 | "id": "143PISuo1aamZxgjwBVdpM", 257 | "type": "Entry", 258 | "createdAt": "2023-05-13T15:35:32.201Z", 259 | "updatedAt": "2023-05-14T11:30:52.515Z", 260 | "environment": { 261 | "sys": { 262 | "id": "master", 263 | "type": "Link", 264 | "linkType": "Environment" 265 | } 266 | }, 267 | "publishedVersion": 21, 268 | "publishedAt": "2023-05-14T11:30:52.515Z", 269 | "firstPublishedAt": "2023-05-13T15:36:48.193Z", 270 | "createdBy": { 271 | "sys": { 272 | "type": "Link", 273 | "linkType": "User", 274 | "id": "3xmOWEHNnBWEylh7pV5ZwB" 275 | } 276 | }, 277 | "updatedBy": { 278 | "sys": { 279 | "type": "Link", 280 | "linkType": "User", 281 | "id": "3xmOWEHNnBWEylh7pV5ZwB" 282 | } 283 | }, 284 | "publishedCounter": 5, 285 | "version": 22, 286 | "publishedBy": { 287 | "sys": { 288 | "type": "Link", 289 | "linkType": "User", 290 | "id": "3xmOWEHNnBWEylh7pV5ZwB" 291 | } 292 | }, 293 | "automationTags": [ 294 | ], 295 | "contentType": { 296 | "sys": { 297 | "type": "Link", 298 | "linkType": "ContentType", 299 | "id": "blogPost" 300 | } 301 | } 302 | }, 303 | "fields": { 304 | "title": { 305 | "en-US": "5 Headless CMS Options for Next.js (Updated)" 306 | }, 307 | "slug": { 308 | "en-US": "5-headless-cms-options-for-next-js" 309 | }, 310 | "body": { 311 | "en-US": { 312 | "data": { 313 | }, 314 | "content": [ 315 | { 316 | "data": { 317 | }, 318 | "content": [ 319 | { 320 | "data": { 321 | }, 322 | "marks": [ 323 | ], 324 | "value": "As Next.js continues to gain popularity as a powerful and flexible framework for building server-rendered React applications, developers often look for efficient ways to manage content in their Next.js projects. This is where headless content management systems (CMS) come into play. By decoupling the front-end and back-end, headless CMS options allow developers to seamlessly integrate content into their Next.js applications. In this blog post, we will explore five top headless CMS options that work seamlessly with Next.js.", 325 | "nodeType": "text" 326 | } 327 | ], 328 | "nodeType": "paragraph" 329 | }, 330 | { 331 | "data": { 332 | }, 333 | "content": [ 334 | { 335 | "data": { 336 | }, 337 | "marks": [ 338 | ], 339 | "value": "1. Contentful", 340 | "nodeType": "text" 341 | } 342 | ], 343 | "nodeType": "heading-2" 344 | }, 345 | { 346 | "data": { 347 | }, 348 | "content": [ 349 | { 350 | "data": { 351 | }, 352 | "marks": [ 353 | ], 354 | "value": "Contentful is a widely recognized headless CMS that offers excellent support for Next.js. Its intuitive interface allows content authors to manage and update content effortlessly. With its extensive RESTful API, developers can fetch content from Contentful and render it seamlessly in their Next.js applications. Contentful's rich-text editor and asset management features make it a compelling choice for content-driven websites.", 355 | "nodeType": "text" 356 | } 357 | ], 358 | "nodeType": "paragraph" 359 | }, 360 | { 361 | "data": { 362 | }, 363 | "content": [ 364 | { 365 | "data": { 366 | }, 367 | "marks": [ 368 | ], 369 | "value": "2. Strapi", 370 | "nodeType": "text" 371 | } 372 | ], 373 | "nodeType": "heading-2" 374 | }, 375 | { 376 | "data": { 377 | }, 378 | "content": [ 379 | { 380 | "data": { 381 | }, 382 | "marks": [ 383 | ], 384 | "value": "Strapi is an open-source headless CMS that provides a self-hosted option for developers. It offers a powerful admin panel, making it easy to create and manage content. Strapi seamlessly integrates with Next.js through its RESTful API or GraphQL. With its flexible content modeling and role-based access control, Strapi empowers developers to build scalable and secure Next.js applications.", 385 | "nodeType": "text" 386 | } 387 | ], 388 | "nodeType": "paragraph" 389 | }, 390 | { 391 | "data": { 392 | }, 393 | "content": [ 394 | { 395 | "data": { 396 | }, 397 | "marks": [ 398 | ], 399 | "value": "3. Prismic", 400 | "nodeType": "text" 401 | } 402 | ], 403 | "nodeType": "heading-2" 404 | }, 405 | { 406 | "data": { 407 | }, 408 | "content": [ 409 | { 410 | "data": { 411 | }, 412 | "marks": [ 413 | ], 414 | "value": "Prismic is a headless CMS that offers a user-friendly interface and a robust API for developers. Its custom types feature allows you to define and structure content models effortlessly. Prismic also provides a Next.js development kit, enabling you to easily fetch and render content in your Next.js projects. With features like localization and content versioning, Prismic is a reliable choice for multi-language and iterative content management.", 415 | "nodeType": "text" 416 | } 417 | ], 418 | "nodeType": "paragraph" 419 | }, 420 | { 421 | "data": { 422 | }, 423 | "content": [ 424 | { 425 | "data": { 426 | }, 427 | "marks": [ 428 | ], 429 | "value": "4. Sanity", 430 | "nodeType": "text" 431 | } 432 | ], 433 | "nodeType": "heading-2" 434 | }, 435 | { 436 | "data": { 437 | }, 438 | "content": [ 439 | { 440 | "data": { 441 | }, 442 | "marks": [ 443 | ], 444 | "value": "Sanity is a flexible and customizable headless CMS that provides a great developer experience. Its real-time collaboration and structured content models make it easy to create and manage content. Sanity offers a Next.js integration that simplifies the process of fetching and rendering content in Next.js applications. Additionally, its powerful querying capabilities and built-in image processing make it an excellent choice for content-heavy projects.", 445 | "nodeType": "text" 446 | } 447 | ], 448 | "nodeType": "paragraph" 449 | }, 450 | { 451 | "data": { 452 | }, 453 | "content": [ 454 | { 455 | "data": { 456 | }, 457 | "marks": [ 458 | ], 459 | "value": "5. DatoCMS", 460 | "nodeType": "text" 461 | } 462 | ], 463 | "nodeType": "heading-2" 464 | }, 465 | { 466 | "data": { 467 | }, 468 | "content": [ 469 | { 470 | "data": { 471 | }, 472 | "marks": [ 473 | ], 474 | "value": "DatoCMS is a headless CMS designed to streamline content management workflows. Its visually appealing interface makes content creation and organization effortless. DatoCMS offers a Next.js plugin that enables seamless integration and provides a powerful GraphQL API for fetching content. With features like content localization and media handling, DatoCMS is a versatile CMS option for Next.js developers.", 475 | "nodeType": "text" 476 | } 477 | ], 478 | "nodeType": "paragraph" 479 | }, 480 | { 481 | "data": { 482 | }, 483 | "content": [ 484 | { 485 | "data": { 486 | }, 487 | "marks": [ 488 | ], 489 | "value": "Conclusion", 490 | "nodeType": "text" 491 | } 492 | ], 493 | "nodeType": "heading-2" 494 | }, 495 | { 496 | "data": { 497 | }, 498 | "content": [ 499 | { 500 | "data": { 501 | }, 502 | "marks": [ 503 | ], 504 | "value": "Choosing the right headless CMS for your Next.js project can greatly enhance your content management experience. Contentful, Strapi, Prismic, Sanity, and DatoCMS are all strong options that offer unique features and capabilities. Consider your project requirements, ease of use, developer experience, and scalability when making your decision. By leveraging these headless CMS options, you can efficiently manage content and deliver exceptional user experiences in your Next.js applications.", 505 | "nodeType": "text" 506 | } 507 | ], 508 | "nodeType": "paragraph" 509 | } 510 | ], 511 | "nodeType": "document" 512 | } 513 | }, 514 | "image": { 515 | "en-US": { 516 | "sys": { 517 | "type": "Link", 518 | "linkType": "Asset", 519 | "id": "14Lf4noV1oGoOSrGbwVXou" 520 | } 521 | } 522 | } 523 | } 524 | }, 525 | { 526 | "metadata": { 527 | "tags": [ 528 | ] 529 | }, 530 | "sys": { 531 | "space": { 532 | "sys": { 533 | "type": "Link", 534 | "linkType": "Space", 535 | "id": "e6bu18b008u6" 536 | } 537 | }, 538 | "id": "3KN2TpSUD9Sfjd259lMItr", 539 | "type": "Entry", 540 | "createdAt": "2023-05-13T15:38:05.292Z", 541 | "updatedAt": "2023-05-13T16:32:55.607Z", 542 | "environment": { 543 | "sys": { 544 | "id": "master", 545 | "type": "Link", 546 | "linkType": "Environment" 547 | } 548 | }, 549 | "publishedVersion": 11, 550 | "publishedAt": "2023-05-13T16:32:55.607Z", 551 | "firstPublishedAt": "2023-05-13T15:39:17.798Z", 552 | "createdBy": { 553 | "sys": { 554 | "type": "Link", 555 | "linkType": "User", 556 | "id": "3xmOWEHNnBWEylh7pV5ZwB" 557 | } 558 | }, 559 | "updatedBy": { 560 | "sys": { 561 | "type": "Link", 562 | "linkType": "User", 563 | "id": "3xmOWEHNnBWEylh7pV5ZwB" 564 | } 565 | }, 566 | "publishedCounter": 2, 567 | "version": 12, 568 | "publishedBy": { 569 | "sys": { 570 | "type": "Link", 571 | "linkType": "User", 572 | "id": "3xmOWEHNnBWEylh7pV5ZwB" 573 | } 574 | }, 575 | "automationTags": [ 576 | ], 577 | "contentType": { 578 | "sys": { 579 | "type": "Link", 580 | "linkType": "ContentType", 581 | "id": "blogPost" 582 | } 583 | } 584 | }, 585 | "fields": { 586 | "title": { 587 | "en-US": "How to Integrate Contentful with Next.js" 588 | }, 589 | "slug": { 590 | "en-US": "how-to-integrate-contentful-with-next-js" 591 | }, 592 | "body": { 593 | "en-US": { 594 | "data": { 595 | }, 596 | "content": [ 597 | { 598 | "data": { 599 | }, 600 | "content": [ 601 | { 602 | "data": { 603 | }, 604 | "marks": [ 605 | ], 606 | "value": "Contentful is a popular headless content management system (CMS) that offers a user-friendly interface and powerful API for managing and delivering content. When combined with Next.js, a versatile framework for building React applications, Contentful provides a seamless solution for content management and delivery. In this blog post, we will explore how to integrate Contentful with Next.js and leverage its capabilities to create dynamic and content-rich web applications.", 607 | "nodeType": "text" 608 | } 609 | ], 610 | "nodeType": "paragraph" 611 | }, 612 | { 613 | "data": { 614 | }, 615 | "content": [ 616 | { 617 | "data": { 618 | }, 619 | "marks": [ 620 | ], 621 | "value": "Step 1: Set up a Contentful account", 622 | "nodeType": "text" 623 | } 624 | ], 625 | "nodeType": "heading-2" 626 | }, 627 | { 628 | "data": { 629 | }, 630 | "content": [ 631 | { 632 | "data": { 633 | }, 634 | "marks": [ 635 | ], 636 | "value": "Begin by signing up for a Contentful account at contentful.com. Once registered, create a new space to store your content. A space represents a project or website within Contentful.", 637 | "nodeType": "text" 638 | } 639 | ], 640 | "nodeType": "paragraph" 641 | }, 642 | { 643 | "data": { 644 | }, 645 | "content": [ 646 | { 647 | "data": { 648 | }, 649 | "marks": [ 650 | ], 651 | "value": "Step 2: Define content models", 652 | "nodeType": "text" 653 | } 654 | ], 655 | "nodeType": "heading-2" 656 | }, 657 | { 658 | "data": { 659 | }, 660 | "content": [ 661 | { 662 | "data": { 663 | }, 664 | "marks": [ 665 | ], 666 | "value": "Contentful allows you to define content models that structure your content. Create content types based on the specific needs of your application, such as blog posts, products, or events. Define fields for each content type, specifying the data type and validation rules.", 667 | "nodeType": "text" 668 | } 669 | ], 670 | "nodeType": "paragraph" 671 | }, 672 | { 673 | "data": { 674 | }, 675 | "content": [ 676 | { 677 | "data": { 678 | }, 679 | "marks": [ 680 | ], 681 | "value": "Step 3: Fetch content in Next.js", 682 | "nodeType": "text" 683 | } 684 | ], 685 | "nodeType": "heading-2" 686 | }, 687 | { 688 | "data": { 689 | }, 690 | "content": [ 691 | { 692 | "data": { 693 | }, 694 | "marks": [ 695 | ], 696 | "value": "To fetch content from Contentful in your Next.js application, you'll need to install the required packages. Start by installing the \"contentful\" npm package. Use the Contentful Delivery API to fetch content, specifying the space ID and access token provided by Contentful. Use the retrieved data to populate your Next.js components with content.", 697 | "nodeType": "text" 698 | } 699 | ], 700 | "nodeType": "paragraph" 701 | }, 702 | { 703 | "data": { 704 | }, 705 | "content": [ 706 | { 707 | "data": { 708 | }, 709 | "marks": [ 710 | ], 711 | "value": "Step 4: Render content dynamically", 712 | "nodeType": "text" 713 | } 714 | ], 715 | "nodeType": "heading-2" 716 | }, 717 | { 718 | "data": { 719 | }, 720 | "content": [ 721 | { 722 | "data": { 723 | }, 724 | "marks": [ 725 | ], 726 | "value": "Next.js provides an excellent environment for rendering dynamic content. Use the data fetched from Contentful to populate your Next.js pages and components. Leverage Next.js's data fetching methods, such as getStaticProps or getServerSideProps, to fetch and render the content at build time or runtime, respectively.", 727 | "nodeType": "text" 728 | } 729 | ], 730 | "nodeType": "paragraph" 731 | }, 732 | { 733 | "data": { 734 | }, 735 | "content": [ 736 | { 737 | "data": { 738 | }, 739 | "marks": [ 740 | ], 741 | "value": "Step 5: Handle assets and images", 742 | "nodeType": "text" 743 | } 744 | ], 745 | "nodeType": "heading-2" 746 | }, 747 | { 748 | "data": { 749 | }, 750 | "content": [ 751 | { 752 | "data": { 753 | }, 754 | "marks": [ 755 | ], 756 | "value": "Contentful allows you to store and manage media assets like images and videos. To handle assets in Next.js, you can use the Contentful Image API to retrieve optimized versions of the images. This helps improve performance and ensures the appropriate image size is delivered to the client.", 757 | "nodeType": "text" 758 | } 759 | ], 760 | "nodeType": "paragraph" 761 | }, 762 | { 763 | "data": { 764 | }, 765 | "content": [ 766 | { 767 | "data": { 768 | }, 769 | "marks": [ 770 | ], 771 | "value": "Conclusion", 772 | "nodeType": "text" 773 | } 774 | ], 775 | "nodeType": "heading-2" 776 | }, 777 | { 778 | "data": { 779 | }, 780 | "content": [ 781 | { 782 | "data": { 783 | }, 784 | "marks": [ 785 | ], 786 | "value": "Integrating Contentful with Next.js opens up a world of possibilities for managing and delivering content in your web applications. By following the steps outlined above, you can seamlessly fetch and render content from Contentful in your Next.js projects. Leveraging Contentful's powerful API, along with Next.js's dynamic rendering capabilities, you can create engaging and personalized experiences for your users. Whether you're building a blog, an e-commerce site, or a portfolio, integrating Contentful with Next.js will streamline your content management workflow and help you deliver outstanding web experiences.", 787 | "nodeType": "text" 788 | } 789 | ], 790 | "nodeType": "paragraph" 791 | } 792 | ], 793 | "nodeType": "document" 794 | } 795 | }, 796 | "image": { 797 | "en-US": { 798 | "sys": { 799 | "type": "Link", 800 | "linkType": "Asset", 801 | "id": "2vFkD2wO0Pta4fB0nt3yS2" 802 | } 803 | } 804 | } 805 | } 806 | }, 807 | { 808 | "metadata": { 809 | "tags": [ 810 | ] 811 | }, 812 | "sys": { 813 | "space": { 814 | "sys": { 815 | "type": "Link", 816 | "linkType": "Space", 817 | "id": "e6bu18b008u6" 818 | } 819 | }, 820 | "id": "3nB2NyFNo7Rih5qGsH7YFJ", 821 | "type": "Entry", 822 | "createdAt": "2023-05-13T15:39:26.731Z", 823 | "updatedAt": "2023-05-13T16:32:30.822Z", 824 | "environment": { 825 | "sys": { 826 | "id": "master", 827 | "type": "Link", 828 | "linkType": "Environment" 829 | } 830 | }, 831 | "publishedVersion": 11, 832 | "publishedAt": "2023-05-13T16:32:30.822Z", 833 | "firstPublishedAt": "2023-05-13T15:40:40.331Z", 834 | "createdBy": { 835 | "sys": { 836 | "type": "Link", 837 | "linkType": "User", 838 | "id": "3xmOWEHNnBWEylh7pV5ZwB" 839 | } 840 | }, 841 | "updatedBy": { 842 | "sys": { 843 | "type": "Link", 844 | "linkType": "User", 845 | "id": "3xmOWEHNnBWEylh7pV5ZwB" 846 | } 847 | }, 848 | "publishedCounter": 2, 849 | "version": 12, 850 | "publishedBy": { 851 | "sys": { 852 | "type": "Link", 853 | "linkType": "User", 854 | "id": "3xmOWEHNnBWEylh7pV5ZwB" 855 | } 856 | }, 857 | "automationTags": [ 858 | ], 859 | "contentType": { 860 | "sys": { 861 | "type": "Link", 862 | "linkType": "ContentType", 863 | "id": "blogPost" 864 | } 865 | } 866 | }, 867 | "fields": { 868 | "title": { 869 | "en-US": "How to Integrate Sanity with Next.js" 870 | }, 871 | "slug": { 872 | "en-US": "how-to-integrate-sanity-with-next-js" 873 | }, 874 | "body": { 875 | "en-US": { 876 | "data": { 877 | }, 878 | "content": [ 879 | { 880 | "data": { 881 | }, 882 | "content": [ 883 | { 884 | "data": { 885 | }, 886 | "marks": [ 887 | ], 888 | "value": "Sanity is a flexible and customizable headless content management system (CMS) that allows you to efficiently manage and deliver content. When combined with Next.js, a popular React framework, Sanity provides a powerful solution for content management and seamless integration with your web applications. In this blog post, we will explore how to integrate Sanity with Next.js and leverage its capabilities to create dynamic and content-rich websites.", 889 | "nodeType": "text" 890 | } 891 | ], 892 | "nodeType": "paragraph" 893 | }, 894 | { 895 | "data": { 896 | }, 897 | "content": [ 898 | { 899 | "data": { 900 | }, 901 | "marks": [ 902 | ], 903 | "value": "Step 1: Set up a Sanity project", 904 | "nodeType": "text" 905 | } 906 | ], 907 | "nodeType": "heading-2" 908 | }, 909 | { 910 | "data": { 911 | }, 912 | "content": [ 913 | { 914 | "data": { 915 | }, 916 | "marks": [ 917 | ], 918 | "value": "Start by creating a Sanity account at sanity.io and setting up a new project. Define your content schema by creating custom schemas for different types of content, such as blog posts, products, or events. Specify the fields and data types for each schema to structure your content.", 919 | "nodeType": "text" 920 | } 921 | ], 922 | "nodeType": "paragraph" 923 | }, 924 | { 925 | "data": { 926 | }, 927 | "content": [ 928 | { 929 | "data": { 930 | }, 931 | "marks": [ 932 | ], 933 | "value": "Step 2: Configure the Sanity API", 934 | "nodeType": "text" 935 | } 936 | ], 937 | "nodeType": "heading-2" 938 | }, 939 | { 940 | "data": { 941 | }, 942 | "content": [ 943 | { 944 | "data": { 945 | }, 946 | "marks": [ 947 | ], 948 | "value": "Sanity provides a powerful API that allows you to fetch and manage content programmatically. Obtain the Sanity project ID and dataset name from the project settings. Configure the Sanity client by installing the \"sanity/client\" npm package and initializing it with your project ID and dataset name.", 949 | "nodeType": "text" 950 | } 951 | ], 952 | "nodeType": "paragraph" 953 | }, 954 | { 955 | "data": { 956 | }, 957 | "content": [ 958 | { 959 | "data": { 960 | }, 961 | "marks": [ 962 | ], 963 | "value": "Step 3: Fetch content in Next.js", 964 | "nodeType": "text" 965 | } 966 | ], 967 | "nodeType": "heading-2" 968 | }, 969 | { 970 | "data": { 971 | }, 972 | "content": [ 973 | { 974 | "data": { 975 | }, 976 | "marks": [ 977 | ], 978 | "value": "Install the Sanity client in your Next.js project using npm or yarn. Utilize the client to fetch content from Sanity by querying the desired data from your Sanity schemas. Use Next.js's data fetching methods, such as getStaticProps or getServerSideProps, to fetch and render the content in your Next.js components.", 979 | "nodeType": "text" 980 | } 981 | ], 982 | "nodeType": "paragraph" 983 | }, 984 | { 985 | "data": { 986 | }, 987 | "content": [ 988 | { 989 | "data": { 990 | }, 991 | "marks": [ 992 | ], 993 | "value": "Step 4: Render content dynamically", 994 | "nodeType": "text" 995 | } 996 | ], 997 | "nodeType": "heading-2" 998 | }, 999 | { 1000 | "data": { 1001 | }, 1002 | "content": [ 1003 | { 1004 | "data": { 1005 | }, 1006 | "marks": [ 1007 | ], 1008 | "value": "Leverage Next.js's dynamic rendering capabilities to populate your components with the content fetched from Sanity. Use the data retrieved from Sanity to dynamically render content on your Next.js pages, providing a personalized and engaging user experience.", 1009 | "nodeType": "text" 1010 | } 1011 | ], 1012 | "nodeType": "paragraph" 1013 | }, 1014 | { 1015 | "data": { 1016 | }, 1017 | "content": [ 1018 | { 1019 | "data": { 1020 | }, 1021 | "marks": [ 1022 | ], 1023 | "value": "Step 5: Manage assets and images", 1024 | "nodeType": "text" 1025 | } 1026 | ], 1027 | "nodeType": "heading-2" 1028 | }, 1029 | { 1030 | "data": { 1031 | }, 1032 | "content": [ 1033 | { 1034 | "data": { 1035 | }, 1036 | "marks": [ 1037 | ], 1038 | "value": "Sanity allows you to store and manage media assets, including images and videos. Use Sanity's image pipeline to optimize and deliver images efficiently. Retrieve and render images in your Next.js application using Sanity's image API, ensuring optimal performance and visual quality.", 1039 | "nodeType": "text" 1040 | } 1041 | ], 1042 | "nodeType": "paragraph" 1043 | }, 1044 | { 1045 | "data": { 1046 | }, 1047 | "content": [ 1048 | { 1049 | "data": { 1050 | }, 1051 | "marks": [ 1052 | ], 1053 | "value": "Conclusion", 1054 | "nodeType": "text" 1055 | } 1056 | ], 1057 | "nodeType": "heading-2" 1058 | }, 1059 | { 1060 | "data": { 1061 | }, 1062 | "content": [ 1063 | { 1064 | "data": { 1065 | }, 1066 | "marks": [ 1067 | ], 1068 | "value": "Integrating Sanity with Next.js offers a robust solution for content management and delivery in your web applications. By following the steps outlined above, you can seamlessly fetch and render content from Sanity in your Next.js projects. Leverage Sanity's powerful API and Next.js's dynamic rendering capabilities to create dynamic and content-rich websites. Whether you're building a blog, an e-commerce platform, or a portfolio site, integrating Sanity with Next.js will streamline your content management workflow and enable you to deliver exceptional user experiences. With the flexibility and extensibility of both Sanity and Next.js, you have the tools to create engaging and personalized web applications that meet your specific content management needs.", 1069 | "nodeType": "text" 1070 | } 1071 | ], 1072 | "nodeType": "paragraph" 1073 | } 1074 | ], 1075 | "nodeType": "document" 1076 | } 1077 | }, 1078 | "image": { 1079 | "en-US": { 1080 | "sys": { 1081 | "type": "Link", 1082 | "linkType": "Asset", 1083 | "id": "46khZOy8xX6q6E4dV0Jat3" 1084 | } 1085 | } 1086 | } 1087 | } 1088 | }, 1089 | { 1090 | "metadata": { 1091 | "tags": [ 1092 | ] 1093 | }, 1094 | "sys": { 1095 | "space": { 1096 | "sys": { 1097 | "type": "Link", 1098 | "linkType": "Space", 1099 | "id": "e6bu18b008u6" 1100 | } 1101 | }, 1102 | "id": "18dWkFYewPRUL6ymauj4xq", 1103 | "type": "Entry", 1104 | "createdAt": "2023-05-13T15:42:57.656Z", 1105 | "updatedAt": "2023-05-13T16:31:58.743Z", 1106 | "environment": { 1107 | "sys": { 1108 | "id": "master", 1109 | "type": "Link", 1110 | "linkType": "Environment" 1111 | } 1112 | }, 1113 | "publishedVersion": 9, 1114 | "publishedAt": "2023-05-13T16:31:58.743Z", 1115 | "firstPublishedAt": "2023-05-13T15:43:35.028Z", 1116 | "createdBy": { 1117 | "sys": { 1118 | "type": "Link", 1119 | "linkType": "User", 1120 | "id": "3xmOWEHNnBWEylh7pV5ZwB" 1121 | } 1122 | }, 1123 | "updatedBy": { 1124 | "sys": { 1125 | "type": "Link", 1126 | "linkType": "User", 1127 | "id": "3xmOWEHNnBWEylh7pV5ZwB" 1128 | } 1129 | }, 1130 | "publishedCounter": 2, 1131 | "version": 10, 1132 | "publishedBy": { 1133 | "sys": { 1134 | "type": "Link", 1135 | "linkType": "User", 1136 | "id": "3xmOWEHNnBWEylh7pV5ZwB" 1137 | } 1138 | }, 1139 | "automationTags": [ 1140 | ], 1141 | "contentType": { 1142 | "sys": { 1143 | "type": "Link", 1144 | "linkType": "ContentType", 1145 | "id": "blogPost" 1146 | } 1147 | } 1148 | }, 1149 | "fields": { 1150 | "title": { 1151 | "en-US": "Next.js vs. Create React App: Choosing the Right React Framework" 1152 | }, 1153 | "slug": { 1154 | "en-US": "next-js-vs-create-react-app-choosing-the-right-react-framework" 1155 | }, 1156 | "body": { 1157 | "en-US": { 1158 | "data": { 1159 | }, 1160 | "content": [ 1161 | { 1162 | "data": { 1163 | }, 1164 | "content": [ 1165 | { 1166 | "data": { 1167 | }, 1168 | "marks": [ 1169 | ], 1170 | "value": "When it comes to building React applications, developers have a plethora of options. Two popular choices are Next.js and Create React App (CRA). Both frameworks offer powerful features and simplify the development process. In this blog post, we will compare Next.js and Create React App, highlighting their differences and helping you choose the right framework for your project.", 1171 | "nodeType": "text" 1172 | } 1173 | ], 1174 | "nodeType": "paragraph" 1175 | }, 1176 | { 1177 | "data": { 1178 | }, 1179 | "content": [ 1180 | { 1181 | "data": { 1182 | }, 1183 | "marks": [ 1184 | ], 1185 | "value": "Next.js: Server-Side Rendering Made Easy", 1186 | "nodeType": "text" 1187 | } 1188 | ], 1189 | "nodeType": "heading-2" 1190 | }, 1191 | { 1192 | "data": { 1193 | }, 1194 | "content": [ 1195 | { 1196 | "data": { 1197 | }, 1198 | "marks": [ 1199 | ], 1200 | "value": "Next.js is a React framework that enables server-side rendering (SSR) out of the box. SSR provides better initial load times and improved SEO. Next.js allows you to build dynamic, server-rendered applications effortlessly. It provides routing capabilities, API routes, and built-in support for static site generation (SSG). Next.js is an excellent choice for projects that require SEO optimization, dynamic content, or high-performance rendering.", 1201 | "nodeType": "text" 1202 | } 1203 | ], 1204 | "nodeType": "paragraph" 1205 | }, 1206 | { 1207 | "data": { 1208 | }, 1209 | "content": [ 1210 | { 1211 | "data": { 1212 | }, 1213 | "marks": [ 1214 | ], 1215 | "value": "Create React App: Simplicity and Speed", 1216 | "nodeType": "text" 1217 | } 1218 | ], 1219 | "nodeType": "heading-2" 1220 | }, 1221 | { 1222 | "data": { 1223 | }, 1224 | "content": [ 1225 | { 1226 | "data": { 1227 | }, 1228 | "marks": [ 1229 | ], 1230 | "value": "Create React App, also known as CRA, is a zero-configuration tool that sets up a basic React project quickly. It provides a simple and straightforward way to start a React application without worrying about complex configurations. CRA is ideal for small to medium-sized projects that don't require server-side rendering or have limited server interaction. It offers a fast development environment and supports modern JavaScript features out of the box.", 1231 | "nodeType": "text" 1232 | } 1233 | ], 1234 | "nodeType": "paragraph" 1235 | }, 1236 | { 1237 | "data": { 1238 | }, 1239 | "content": [ 1240 | { 1241 | "data": { 1242 | }, 1243 | "marks": [ 1244 | ], 1245 | "value": "Key Differences and Use Cases", 1246 | "nodeType": "text" 1247 | } 1248 | ], 1249 | "nodeType": "heading-2" 1250 | }, 1251 | { 1252 | "data": { 1253 | }, 1254 | "content": [ 1255 | { 1256 | "data": { 1257 | }, 1258 | "marks": [ 1259 | ], 1260 | "value": "Next.js shines in projects where server-side rendering, dynamic routing, and optimized SEO are crucial. It is an excellent choice for content-heavy websites, blogs, e-commerce platforms, or applications with complex data requirements. Next.js allows you to handle server-side logic seamlessly, fetch data at build time or runtime, and optimize performance.", 1261 | "nodeType": "text" 1262 | } 1263 | ], 1264 | "nodeType": "paragraph" 1265 | }, 1266 | { 1267 | "data": { 1268 | }, 1269 | "content": [ 1270 | { 1271 | "data": { 1272 | }, 1273 | "marks": [ 1274 | ], 1275 | "value": "On the other hand, Create React App is ideal for smaller projects, prototypes, or applications where client-side rendering is sufficient. It provides a minimal configuration setup, making it easy to start and build React applications quickly. CRA is well-suited for static websites, landing pages, or applications that primarily rely on client-side interactions.", 1276 | "nodeType": "text" 1277 | } 1278 | ], 1279 | "nodeType": "paragraph" 1280 | }, 1281 | { 1282 | "data": { 1283 | }, 1284 | "content": [ 1285 | { 1286 | "data": { 1287 | }, 1288 | "marks": [ 1289 | ], 1290 | "value": "Conclusion", 1291 | "nodeType": "text" 1292 | } 1293 | ], 1294 | "nodeType": "heading-2" 1295 | }, 1296 | { 1297 | "data": { 1298 | }, 1299 | "content": [ 1300 | { 1301 | "data": { 1302 | }, 1303 | "marks": [ 1304 | ], 1305 | "value": "Choosing between Next.js and Create React App depends on your project requirements and priorities. If you need server-side rendering, dynamic routing, and advanced optimization capabilities, Next.js is the way to go. It empowers you to build performant, SEO-friendly applications with ease. However, if you're working on a smaller project, prefer a simple setup, and don't require server-side rendering, Create React App offers a lightweight and efficient solution.", 1306 | "nodeType": "text" 1307 | } 1308 | ], 1309 | "nodeType": "paragraph" 1310 | }, 1311 | { 1312 | "data": { 1313 | }, 1314 | "content": [ 1315 | { 1316 | "data": { 1317 | }, 1318 | "marks": [ 1319 | ], 1320 | "value": "Consider the scope, scalability, and specific needs of your project to make an informed decision. Both Next.js and Create React App are powerful frameworks that provide excellent developer experiences. Whichever option you choose, you can leverage the capabilities of React and its vast ecosystem to create outstanding web applications.", 1321 | "nodeType": "text" 1322 | } 1323 | ], 1324 | "nodeType": "paragraph" 1325 | } 1326 | ], 1327 | "nodeType": "document" 1328 | } 1329 | }, 1330 | "image": { 1331 | "en-US": { 1332 | "sys": { 1333 | "type": "Link", 1334 | "linkType": "Asset", 1335 | "id": "l4iqE4BcpSJIeev7qLnHZ" 1336 | } 1337 | } 1338 | } 1339 | } 1340 | } 1341 | ], 1342 | "assets": [ 1343 | { 1344 | "metadata": { 1345 | "tags": [ 1346 | ] 1347 | }, 1348 | "sys": { 1349 | "space": { 1350 | "sys": { 1351 | "type": "Link", 1352 | "linkType": "Space", 1353 | "id": "e6bu18b008u6" 1354 | } 1355 | }, 1356 | "id": "l4iqE4BcpSJIeev7qLnHZ", 1357 | "type": "Asset", 1358 | "createdAt": "2023-05-13T16:30:53.207Z", 1359 | "updatedAt": "2023-05-13T16:31:53.542Z", 1360 | "environment": { 1361 | "sys": { 1362 | "id": "master", 1363 | "type": "Link", 1364 | "linkType": "Environment" 1365 | } 1366 | }, 1367 | "publishedVersion": 8, 1368 | "publishedAt": "2023-05-13T16:31:53.542Z", 1369 | "firstPublishedAt": "2023-05-13T16:31:53.542Z", 1370 | "createdBy": { 1371 | "sys": { 1372 | "type": "Link", 1373 | "linkType": "User", 1374 | "id": "3xmOWEHNnBWEylh7pV5ZwB" 1375 | } 1376 | }, 1377 | "updatedBy": { 1378 | "sys": { 1379 | "type": "Link", 1380 | "linkType": "User", 1381 | "id": "3xmOWEHNnBWEylh7pV5ZwB" 1382 | } 1383 | }, 1384 | "publishedCounter": 1, 1385 | "version": 9, 1386 | "publishedBy": { 1387 | "sys": { 1388 | "type": "Link", 1389 | "linkType": "User", 1390 | "id": "3xmOWEHNnBWEylh7pV5ZwB" 1391 | } 1392 | } 1393 | }, 1394 | "fields": { 1395 | "title": { 1396 | "en-US": "Next.js vs Create React App" 1397 | }, 1398 | "description": { 1399 | "en-US": "Create React App and Next.js logos" 1400 | }, 1401 | "file": { 1402 | "en-US": { 1403 | "url": "//images.ctfassets.net/e6bu18b008u6/l4iqE4BcpSJIeev7qLnHZ/d6dec813ccec9f5e3941ffe7a6c8d5bd/nextjs-vs-create-react-app.png", 1404 | "details": { 1405 | "size": 17922, 1406 | "image": { 1407 | "width": 600, 1408 | "height": 600 1409 | } 1410 | }, 1411 | "fileName": "nextjs-vs-create-react-app.png", 1412 | "contentType": "image/png" 1413 | } 1414 | } 1415 | } 1416 | }, 1417 | { 1418 | "metadata": { 1419 | "tags": [ 1420 | ] 1421 | }, 1422 | "sys": { 1423 | "space": { 1424 | "sys": { 1425 | "type": "Link", 1426 | "linkType": "Space", 1427 | "id": "e6bu18b008u6" 1428 | } 1429 | }, 1430 | "id": "46khZOy8xX6q6E4dV0Jat3", 1431 | "type": "Asset", 1432 | "createdAt": "2023-05-13T16:32:08.733Z", 1433 | "updatedAt": "2023-05-13T16:32:28.796Z", 1434 | "environment": { 1435 | "sys": { 1436 | "id": "master", 1437 | "type": "Link", 1438 | "linkType": "Environment" 1439 | } 1440 | }, 1441 | "publishedVersion": 6, 1442 | "publishedAt": "2023-05-13T16:32:28.796Z", 1443 | "firstPublishedAt": "2023-05-13T16:32:28.796Z", 1444 | "createdBy": { 1445 | "sys": { 1446 | "type": "Link", 1447 | "linkType": "User", 1448 | "id": "3xmOWEHNnBWEylh7pV5ZwB" 1449 | } 1450 | }, 1451 | "updatedBy": { 1452 | "sys": { 1453 | "type": "Link", 1454 | "linkType": "User", 1455 | "id": "3xmOWEHNnBWEylh7pV5ZwB" 1456 | } 1457 | }, 1458 | "publishedCounter": 1, 1459 | "version": 7, 1460 | "publishedBy": { 1461 | "sys": { 1462 | "type": "Link", 1463 | "linkType": "User", 1464 | "id": "3xmOWEHNnBWEylh7pV5ZwB" 1465 | } 1466 | } 1467 | }, 1468 | "fields": { 1469 | "title": { 1470 | "en-US": "Integrating Next.js with Sanity" 1471 | }, 1472 | "description": { 1473 | "en-US": "Sanity and Next.js logos" 1474 | }, 1475 | "file": { 1476 | "en-US": { 1477 | "url": "//images.ctfassets.net/e6bu18b008u6/46khZOy8xX6q6E4dV0Jat3/667d81b0aa84c3969e43dc75e75afe4f/nextjs-sanity.png", 1478 | "details": { 1479 | "size": 18899, 1480 | "image": { 1481 | "width": 600, 1482 | "height": 600 1483 | } 1484 | }, 1485 | "fileName": "nextjs-sanity.png", 1486 | "contentType": "image/png" 1487 | } 1488 | } 1489 | } 1490 | }, 1491 | { 1492 | "metadata": { 1493 | "tags": [ 1494 | ] 1495 | }, 1496 | "sys": { 1497 | "space": { 1498 | "sys": { 1499 | "type": "Link", 1500 | "linkType": "Space", 1501 | "id": "e6bu18b008u6" 1502 | } 1503 | }, 1504 | "id": "2vFkD2wO0Pta4fB0nt3yS2", 1505 | "type": "Asset", 1506 | "createdAt": "2023-05-13T16:32:36.795Z", 1507 | "updatedAt": "2023-05-13T16:32:52.643Z", 1508 | "environment": { 1509 | "sys": { 1510 | "id": "master", 1511 | "type": "Link", 1512 | "linkType": "Environment" 1513 | } 1514 | }, 1515 | "publishedVersion": 5, 1516 | "publishedAt": "2023-05-13T16:32:52.643Z", 1517 | "firstPublishedAt": "2023-05-13T16:32:52.643Z", 1518 | "createdBy": { 1519 | "sys": { 1520 | "type": "Link", 1521 | "linkType": "User", 1522 | "id": "3xmOWEHNnBWEylh7pV5ZwB" 1523 | } 1524 | }, 1525 | "updatedBy": { 1526 | "sys": { 1527 | "type": "Link", 1528 | "linkType": "User", 1529 | "id": "3xmOWEHNnBWEylh7pV5ZwB" 1530 | } 1531 | }, 1532 | "publishedCounter": 1, 1533 | "version": 6, 1534 | "publishedBy": { 1535 | "sys": { 1536 | "type": "Link", 1537 | "linkType": "User", 1538 | "id": "3xmOWEHNnBWEylh7pV5ZwB" 1539 | } 1540 | } 1541 | }, 1542 | "fields": { 1543 | "title": { 1544 | "en-US": "How to Integrate Contentful with Next.js" 1545 | }, 1546 | "description": { 1547 | "en-US": "Contentful and Next.js logos" 1548 | }, 1549 | "file": { 1550 | "en-US": { 1551 | "url": "//images.ctfassets.net/e6bu18b008u6/2vFkD2wO0Pta4fB0nt3yS2/81c9447360030c82811e33ad750dfac9/nextjs-contentful.png", 1552 | "details": { 1553 | "size": 18093, 1554 | "image": { 1555 | "width": 600, 1556 | "height": 600 1557 | } 1558 | }, 1559 | "fileName": "nextjs-contentful.png", 1560 | "contentType": "image/png" 1561 | } 1562 | } 1563 | } 1564 | }, 1565 | { 1566 | "metadata": { 1567 | "tags": [ 1568 | ] 1569 | }, 1570 | "sys": { 1571 | "space": { 1572 | "sys": { 1573 | "type": "Link", 1574 | "linkType": "Space", 1575 | "id": "e6bu18b008u6" 1576 | } 1577 | }, 1578 | "id": "14Lf4noV1oGoOSrGbwVXou", 1579 | "type": "Asset", 1580 | "createdAt": "2023-05-13T16:33:00.167Z", 1581 | "updatedAt": "2023-05-13T16:33:20.083Z", 1582 | "environment": { 1583 | "sys": { 1584 | "id": "master", 1585 | "type": "Link", 1586 | "linkType": "Environment" 1587 | } 1588 | }, 1589 | "publishedVersion": 6, 1590 | "publishedAt": "2023-05-13T16:33:20.083Z", 1591 | "firstPublishedAt": "2023-05-13T16:33:20.083Z", 1592 | "createdBy": { 1593 | "sys": { 1594 | "type": "Link", 1595 | "linkType": "User", 1596 | "id": "3xmOWEHNnBWEylh7pV5ZwB" 1597 | } 1598 | }, 1599 | "updatedBy": { 1600 | "sys": { 1601 | "type": "Link", 1602 | "linkType": "User", 1603 | "id": "3xmOWEHNnBWEylh7pV5ZwB" 1604 | } 1605 | }, 1606 | "publishedCounter": 1, 1607 | "version": 7, 1608 | "publishedBy": { 1609 | "sys": { 1610 | "type": "Link", 1611 | "linkType": "User", 1612 | "id": "3xmOWEHNnBWEylh7pV5ZwB" 1613 | } 1614 | } 1615 | }, 1616 | "fields": { 1617 | "title": { 1618 | "en-US": "5 Headless CMS Options for Next.js" 1619 | }, 1620 | "description": { 1621 | "en-US": "Logos of Contentful, Strapi, Prismic, Sanity, Dato CMS and Next.js" 1622 | }, 1623 | "file": { 1624 | "en-US": { 1625 | "url": "//images.ctfassets.net/e6bu18b008u6/14Lf4noV1oGoOSrGbwVXou/9a07bf7e03902261c3d17fae84299878/nextjs-headless-cms.png", 1626 | "details": { 1627 | "size": 30832, 1628 | "image": { 1629 | "width": 600, 1630 | "height": 600 1631 | } 1632 | }, 1633 | "fileName": "nextjs-headless-cms.png", 1634 | "contentType": "image/png" 1635 | } 1636 | } 1637 | } 1638 | } 1639 | ], 1640 | "locales": [ 1641 | { 1642 | "name": "English (United States)", 1643 | "code": "en-US", 1644 | "fallbackCode": null, 1645 | "default": true, 1646 | "contentManagementApi": true, 1647 | "contentDeliveryApi": true, 1648 | "optional": false, 1649 | "sys": { 1650 | "type": "Locale", 1651 | "id": "5yAQweGFdkbQoRPXpN4MJj", 1652 | "version": 1, 1653 | "space": { 1654 | "sys": { 1655 | "type": "Link", 1656 | "linkType": "Space", 1657 | "id": "e6bu18b008u6" 1658 | } 1659 | }, 1660 | "environment": { 1661 | "sys": { 1662 | "type": "Link", 1663 | "linkType": "Environment", 1664 | "id": "master", 1665 | "uuid": "40745e22-e8d3-40f0-935e-33b979a89947" 1666 | } 1667 | }, 1668 | "createdBy": { 1669 | "sys": { 1670 | "type": "Link", 1671 | "linkType": "User", 1672 | "id": "3xmOWEHNnBWEylh7pV5ZwB" 1673 | } 1674 | }, 1675 | "createdAt": "2023-05-13T15:28:19Z", 1676 | "updatedBy": { 1677 | "sys": { 1678 | "type": "Link", 1679 | "linkType": "User", 1680 | "id": "3xmOWEHNnBWEylh7pV5ZwB" 1681 | } 1682 | }, 1683 | "updatedAt": "2023-05-13T15:28:19Z" 1684 | } 1685 | } 1686 | ], 1687 | "webhooks": [ 1688 | { 1689 | "name": "Deploy Vercel", 1690 | "url": "https://api.vercel.com/v1/integrations/deploy/prj_H9Yn37uR6Wf8m43Dm6bfKzk2gmd6/dNccfrKM1d", 1691 | "httpBasicUsername": null, 1692 | "topics": [ 1693 | "Entry.create", 1694 | "Entry.save", 1695 | "Entry.publish", 1696 | "Entry.unpublish", 1697 | "Entry.delete" 1698 | ], 1699 | "filters": null, 1700 | "transformation": null, 1701 | "active": true, 1702 | "sys": { 1703 | "type": "WebhookDefinition", 1704 | "id": "5l6SDJaMJ363mpyhbRxm3P", 1705 | "version": 0, 1706 | "space": { 1707 | "sys": { 1708 | "type": "Link", 1709 | "linkType": "Space", 1710 | "id": "e6bu18b008u6" 1711 | } 1712 | }, 1713 | "createdBy": { 1714 | "sys": { 1715 | "type": "Link", 1716 | "linkType": "User", 1717 | "id": "3xmOWEHNnBWEylh7pV5ZwB" 1718 | } 1719 | }, 1720 | "createdAt": "2023-05-14T11:30:43Z", 1721 | "updatedBy": { 1722 | "sys": { 1723 | "type": "Link", 1724 | "linkType": "User", 1725 | "id": "3xmOWEHNnBWEylh7pV5ZwB" 1726 | } 1727 | }, 1728 | "updatedAt": "2023-05-14T11:30:43Z" 1729 | }, 1730 | "headers": [ 1731 | ] 1732 | } 1733 | ], 1734 | "roles": [ 1735 | { 1736 | "name": "Editor", 1737 | "description": "Allows editing, publishing and archiving of content", 1738 | "policies": [ 1739 | { 1740 | "effect": "allow", 1741 | "actions": "all", 1742 | "constraint": { 1743 | "and": [ 1744 | { 1745 | "equals": [ 1746 | { 1747 | "doc": "sys.type" 1748 | }, 1749 | "Entry" 1750 | ] 1751 | } 1752 | ] 1753 | } 1754 | }, 1755 | { 1756 | "effect": "allow", 1757 | "actions": "all", 1758 | "constraint": { 1759 | "and": [ 1760 | { 1761 | "equals": [ 1762 | { 1763 | "doc": "sys.type" 1764 | }, 1765 | "Asset" 1766 | ] 1767 | } 1768 | ] 1769 | } 1770 | } 1771 | ], 1772 | "permissions": { 1773 | "ContentModel": [ 1774 | "read" 1775 | ], 1776 | "Settings": [ 1777 | ], 1778 | "ContentDelivery": [ 1779 | ], 1780 | "Environments": [ 1781 | ], 1782 | "EnvironmentAliases": [ 1783 | ], 1784 | "Tags": [ 1785 | ] 1786 | }, 1787 | "sys": { 1788 | "type": "Role", 1789 | "id": "5yVO606S8wFoLUsuudzVTr", 1790 | "version": 0, 1791 | "space": { 1792 | "sys": { 1793 | "type": "Link", 1794 | "linkType": "Space", 1795 | "id": "e6bu18b008u6" 1796 | } 1797 | }, 1798 | "createdBy": { 1799 | "sys": { 1800 | "type": "Link", 1801 | "linkType": "User", 1802 | "id": "3xmOWEHNnBWEylh7pV5ZwB" 1803 | } 1804 | }, 1805 | "createdAt": "2023-05-13T15:28:19Z", 1806 | "updatedBy": { 1807 | "sys": { 1808 | "type": "Link", 1809 | "linkType": "User", 1810 | "id": "3xmOWEHNnBWEylh7pV5ZwB" 1811 | } 1812 | }, 1813 | "updatedAt": "2023-05-13T15:28:19Z" 1814 | } 1815 | } 1816 | ] 1817 | } -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs-contentful-typescript", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "format": "prettier --write .", 11 | "types": "yarn types:contentful", 12 | "types:contentful": "export $(cat .env.local | awk '!/^\\s*#/' | awk '!/^\\s*$/'); cf-content-types-generator --spaceId $CONTENTFUL_SPACE_ID --token $CONTENTFUL_MANAGEMENT_TOKEN -o src/contentful/types -X && prettier --write src/contentful/types" 13 | }, 14 | "dependencies": { 15 | "@contentful/rich-text-react-renderer": "^15.16.4", 16 | "@tailwindcss/typography": "^0.5.9", 17 | "next": "^13.4.3", 18 | "react": "18.2.0", 19 | "react-dom": "18.2.0" 20 | }, 21 | "devDependencies": { 22 | "@types/node": "20.1.4", 23 | "@types/react": "18.2.6", 24 | "@types/react-dom": "18.2.4", 25 | "autoprefixer": "10.4.14", 26 | "cf-content-types-generator": "^2.12.2", 27 | "eslint": "8.40.0", 28 | "eslint-config-next": "13.4.2", 29 | "postcss": "8.4.23", 30 | "prettier": "^2.8.8", 31 | "tailwindcss": "3.3.2", 32 | "typescript": "5.0.4" 33 | }, 34 | "prettier": { 35 | "printWidth": 120, 36 | "semi": false, 37 | "singleQuote": true, 38 | "tabWidth": 4, 39 | "useTabs": true 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/app/ExitDraftModeLink.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { usePathname } from 'next/navigation' 3 | import React from 'react' 4 | 5 | function ExitDraftModeLink(props: React.HTMLProps) { 6 | const pathname = usePathname() 7 | 8 | return ( 9 | 10 | Exit 11 | 12 | ) 13 | } 14 | 15 | export default ExitDraftModeLink 16 | -------------------------------------------------------------------------------- /src/app/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata, ResolvingMetadata } from 'next' 2 | import { draftMode } from 'next/headers' 3 | import { notFound } from 'next/navigation' 4 | import { fetchBlogPost, fetchBlogPosts } from '../../contentful/blogPosts' 5 | import Link from 'next/link' 6 | import RichText from '../../contentful/RichText' 7 | 8 | interface BlogPostPageParams { 9 | slug: string 10 | } 11 | 12 | interface BlogPostPageProps { 13 | params: BlogPostPageParams 14 | } 15 | 16 | export async function generateStaticParams(): Promise { 17 | const blogPosts = await fetchBlogPosts({ preview: false }) 18 | 19 | return blogPosts.map((post) => ({ slug: post.slug })) 20 | } 21 | 22 | export async function generateMetadata({ params }: BlogPostPageProps, parent: ResolvingMetadata): Promise { 23 | const blogPost = await fetchBlogPost({ slug: params.slug, preview: draftMode().isEnabled }) 24 | 25 | if (!blogPost) { 26 | return notFound() 27 | } 28 | 29 | return { 30 | title: blogPost.title, 31 | } 32 | } 33 | 34 | export default async function BlogPostPage({ params }: BlogPostPageProps) { 35 | const blogPost = await fetchBlogPost({ slug: params.slug, preview: draftMode().isEnabled }) 36 | 37 | if (!blogPost) { 38 | return notFound() 39 | } 40 | 41 | return ( 42 |
43 | ← Posts 44 |
45 | {blogPost.image && ( 46 | {blogPost.image.alt} 53 | )} 54 |

{blogPost.title}

55 | 56 |
57 |
58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /src/app/api/disable-draft/route.ts: -------------------------------------------------------------------------------- 1 | import { draftMode } from 'next/headers' 2 | import { redirect } from 'next/navigation' 3 | 4 | export async function GET(request: Request) { 5 | const { searchParams } = new URL(request.url) 6 | 7 | draftMode().disable() 8 | 9 | redirect(searchParams.get('redirect') || '/') 10 | } 11 | -------------------------------------------------------------------------------- /src/app/api/draft/route.ts: -------------------------------------------------------------------------------- 1 | import { draftMode } from 'next/headers' 2 | import { redirect } from 'next/navigation' 3 | const { CONTENTFUL_PREVIEW_SECRET } = process.env 4 | 5 | export async function GET(request: Request) { 6 | const { searchParams } = new URL(request.url) 7 | if (searchParams.get('previewSecret') !== CONTENTFUL_PREVIEW_SECRET) { 8 | return new Response('Invalid token', { status: 401 }) 9 | } 10 | 11 | draftMode().enable() 12 | 13 | redirect(searchParams.get('redirect') || '/') 14 | } 15 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maximilianschmitt/nextjs-contentful-typescript/e85894eaf03ab1de89ede56e16b6b74517428707/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-rgb: 255, 255, 255; 8 | } 9 | 10 | @media (prefers-color-scheme: dark) { 11 | :root { 12 | --foreground-rgb: 255, 255, 255; 13 | --background-rgb: 0, 0, 0; 14 | } 15 | } 16 | 17 | body { 18 | color: rgb(var(--foreground-rgb)); 19 | background-color: rgb(var(--background-rgb)); 20 | } 21 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './globals.css' 2 | import { draftMode } from 'next/headers' 3 | import { Inter } from 'next/font/google' 4 | import ExitDraftModeLink from './ExitDraftModeLink' 5 | 6 | const inter = Inter({ subsets: ['latin'] }) 7 | 8 | export const metadata = { 9 | title: { 10 | template: '%s – My Contentful Blog', 11 | default: 'My Contentful Blog', 12 | }, 13 | } 14 | 15 | function RootLayout({ children }: { children: React.ReactNode }) { 16 | return ( 17 | 18 | 19 | {draftMode().isEnabled && ( 20 |

21 | Draft mode is on! 22 |

23 | )} 24 | {children} 25 | 26 | 27 | ) 28 | } 29 | 30 | export default RootLayout 31 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { draftMode } from 'next/headers' 2 | import { fetchBlogPosts } from '../contentful/blogPosts' 3 | import Link from 'next/link' 4 | 5 | async function Home() { 6 | const blogPosts = await fetchBlogPosts({ preview: draftMode().isEnabled }) 7 | 8 | return ( 9 |
10 |
11 |

My Contentful Blog

12 |
    13 | {blogPosts.map((blogPost) => { 14 | return ( 15 |
  • 16 | {blogPost.title} 17 |
  • 18 | ) 19 | })} 20 |
21 |
22 |
23 | ) 24 | } 25 | 26 | export default Home 27 | -------------------------------------------------------------------------------- /src/contentful/RichText.tsx: -------------------------------------------------------------------------------- 1 | import { Document as RichTextDocument } from '@contentful/rich-text-types' 2 | import { documentToReactComponents } from '@contentful/rich-text-react-renderer' 3 | 4 | type RichTextProps = { 5 | document: RichTextDocument | null 6 | } 7 | 8 | function RichText({ document }: RichTextProps) { 9 | if (!document) { 10 | return null 11 | } 12 | 13 | return <>{documentToReactComponents(document)} 14 | } 15 | 16 | export default RichText 17 | -------------------------------------------------------------------------------- /src/contentful/blogPosts.ts: -------------------------------------------------------------------------------- 1 | import { TypeBlogPostSkeleton } from './types' 2 | import { Entry } from 'contentful' 3 | import { Document as RichTextDocument } from '@contentful/rich-text-types' 4 | import contentfulClient from './contentfulClient' 5 | import { ContentImage, parseContentfulContentImage } from './contentImage' 6 | 7 | type BlogPostEntry = Entry 8 | 9 | export interface BlogPost { 10 | title: string 11 | slug: string 12 | body: RichTextDocument | null 13 | image: ContentImage | null 14 | } 15 | 16 | export function parseContentfulBlogPost(blogPostEntry?: BlogPostEntry): BlogPost | null { 17 | if (!blogPostEntry) { 18 | return null 19 | } 20 | 21 | return { 22 | title: blogPostEntry.fields.title || '', 23 | slug: blogPostEntry.fields.slug, 24 | body: blogPostEntry.fields.body || null, 25 | image: parseContentfulContentImage(blogPostEntry.fields.image), 26 | } 27 | } 28 | 29 | interface FetchBlogPostsOptions { 30 | preview: boolean 31 | } 32 | export async function fetchBlogPosts({ preview }: FetchBlogPostsOptions): Promise { 33 | const contentful = contentfulClient({ preview }) 34 | 35 | const blogPostsResult = await contentful.getEntries({ 36 | content_type: 'blogPost', 37 | include: 2, 38 | order: ['fields.title'], 39 | }) 40 | 41 | return blogPostsResult.items.map((blogPostEntry) => parseContentfulBlogPost(blogPostEntry) as BlogPost) 42 | } 43 | 44 | interface FetchBlogPostOptions { 45 | slug: string 46 | preview: boolean 47 | } 48 | export async function fetchBlogPost({ slug, preview }: FetchBlogPostOptions): Promise { 49 | const contentful = contentfulClient({ preview }) 50 | 51 | const blogPostsResult = await contentful.getEntries({ 52 | content_type: 'blogPost', 53 | 'fields.slug': slug, 54 | include: 2, 55 | }) 56 | 57 | return parseContentfulBlogPost(blogPostsResult.items[0]) 58 | } 59 | -------------------------------------------------------------------------------- /src/contentful/contentImage.ts: -------------------------------------------------------------------------------- 1 | import { Asset, AssetLink } from 'contentful' 2 | 3 | export interface ContentImage { 4 | src: string 5 | alt: string 6 | width: number 7 | height: number 8 | } 9 | 10 | export function parseContentfulContentImage( 11 | asset?: Asset | { sys: AssetLink } 12 | ): ContentImage | null { 13 | if (!asset) { 14 | return null 15 | } 16 | 17 | if (!('fields' in asset)) { 18 | return null 19 | } 20 | 21 | return { 22 | src: asset.fields.file?.url || '', 23 | alt: asset.fields.description || '', 24 | width: asset.fields.file?.details.image?.width || 0, 25 | height: asset.fields.file?.details.image?.height || 0, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/contentful/contentfulClient.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from 'contentful' 2 | 3 | const { CONTENTFUL_SPACE_ID, CONTENTFUL_ACCESS_TOKEN, CONTENTFUL_PREVIEW_ACCESS_TOKEN } = process.env 4 | 5 | const client = createClient({ 6 | space: CONTENTFUL_SPACE_ID!, 7 | accessToken: CONTENTFUL_ACCESS_TOKEN!, 8 | }) 9 | 10 | const previewClient = createClient({ 11 | space: CONTENTFUL_SPACE_ID!, 12 | accessToken: CONTENTFUL_PREVIEW_ACCESS_TOKEN!, 13 | host: 'preview.contentful.com', 14 | }) 15 | 16 | export default function contentfulClient({ preview = false }) { 17 | if (preview) { 18 | return previewClient 19 | } 20 | 21 | return client 22 | } 23 | -------------------------------------------------------------------------------- /src/contentful/types/TypeBlogPost.ts: -------------------------------------------------------------------------------- 1 | import type { ChainModifiers, Entry, EntryFieldTypes, EntrySkeletonType, LocaleCode } from 'contentful' 2 | 3 | export interface TypeBlogPostFields { 4 | title?: EntryFieldTypes.Symbol 5 | slug: EntryFieldTypes.Symbol 6 | body?: EntryFieldTypes.RichText 7 | image?: EntryFieldTypes.AssetLink 8 | } 9 | 10 | export type TypeBlogPostSkeleton = EntrySkeletonType 11 | export type TypeBlogPost = Entry< 12 | TypeBlogPostSkeleton, 13 | Modifiers, 14 | Locales 15 | > 16 | -------------------------------------------------------------------------------- /src/contentful/types/index.ts: -------------------------------------------------------------------------------- 1 | export type { TypeBlogPost, TypeBlogPostFields, TypeBlogPostSkeleton } from './TypeBlogPost' 2 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}', 5 | './src/components/**/*.{js,ts,jsx,tsx,mdx}', 6 | './src/app/**/*.{js,ts,jsx,tsx,mdx}', 7 | ], 8 | plugins: [require('@tailwindcss/typography')], 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./src/*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | --------------------------------------------------------------------------------