Gatsby Headaches: Working With Media (Part 2)

About The Author

Juan Diego Rodríguez (also known as Monknow) is a front-end developer from Venezuela who loves making beautiful websites with modern technologies but also … More about Juan Diego ↬

Email Newsletter

Weekly tips on front-end & UX.
Trusted by 200,000+ folks.

In the final part of this two-part series on solving headaches when working with media files in Gatsby projects, Juan Rodriguez demonstrates strategies and techniques for handling various types of documents, including Markdown files, PDFs, and 3D models.

Gatsby is a true Jamstack framework. It works with React-powered components that consume APIs before optimizing and bundling everything to serve as static files with bits of reactivity. That includes media files, like images, video, and audio.

The problem is that there’s no “one” way to handle media in a Gatsby project. We have plugins for everything, from making queries off your local filesystem and compressing files to inlining SVGs and serving images in the responsive image format.

Which plugins should be used for certain types of media? How about certain use cases for certain types of media? That’s where you might encounter headaches because there are many plugins — some official and some not — that are capable of handling one or more use cases — some outdated and some not.

That is what this brief two-part series is about. In Part 1, we discussed various strategies and techniques for handling images, video, and audio in a Gatsby project.

This time, in Part 2, we are covering a different type of media we commonly encounter: documents. Specifically, we will tackle considerations for Gatsby projects that make use of Markdown and PDF files. And before wrapping up, we will also demonstrate an approach for using 3D models.

Solving Markdown Headaches In Gatsby

In Gatsby, Markdown files are commonly used to programmatically create pages, such as blog posts. You can write content in Markdown, parse it into your GraphQL data layer, source it into your components, and then bundle it as HTML static files during the build process.

Let’s learn how to load, query, and handle the Markdown for an existing page in Gatsby.

Loading And Querying Markdown From GraphQL

The first step on your Gatsby project is to load the project’s Markdown files to the GraphQL data layer. We can do this using the gatsby-source-filesystem plugin we used to query the local filesystem for image files in Part 1 of this series.

npm i gatsby-source-filesystem

In gatsby-config.js, we declare the folder where Markdown files will be saved in the project:

module.exports = {
  plugins: [
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `assets`,
        path: `${ __dirname }/src/assets`,
      },
    },
  ],
};

Let’s say that we have the following Markdown file located in the project’s ./src/assets directory:

---
title: sample-markdown-file
date: 2023-07-29
---

# Sample Markdown File

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed consectetur imperdiet urna, vitae pellentesque mauris sollicitudin at. Sed id semper ex, ac vestibulum nunc. Etiam ,

![A beautiful forest!](/forest.jpg "Forest trail")

```bash
lorem ipsum dolor sit
```

## Subsection

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed consectetur imperdiet urna, vitae pellentesque mauris sollicitudin at. Sed id semper ex, ac vestibulum nunc. Etiam efficitur, nunc nec placerat dignissim, ipsum ante ultrices ante, sed luctus nisl felis eget ligula. Proin sed quam auctor, posuere enim eu, vulputate felis. Sed egestas, tortor

This example consists of two main sections: the frontmatter and body. It is a common structure for Markdown files.

  • Frontmatter
    Enclosed in triple dashes (---), this is an optional section at the beginning of a Markdown file that contains metadata and configuration settings for the document. In our example, the frontmatter contains information about the page’s title and date, which Gatsby can use as GraphQL arguments.
  • Body
    This is the content that makes up the page’s main body content.

We can use the gatsby-transformer-remark plugin to parse Markdown files to a GraphQL data layer. Once it is installed, we will need to register it in the project’s gatsby-config.js file:

module.exports = {
  plugins: [
    {
      resolve: `gatsby-transformer-remark`,
      options: { },
    },
  ],
};

Restart the development server and navigate to http://localhost:8000/___graphql in the browser. Here, we can play around with Gatsby’s data layer and check our Markdown file above by making a query using the title property (sample-markdown-file) in the frontmatter:

query {
  markdownRemark(frontmatter: { title: { eq: "sample-markdown-file" } }) {
    html
  }
}

This should return the following result:

{
  "data": {
    "markdownRemark": {
      "html": "<h1>Sample Markdown File</h1>\n<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed consectetur imperdiet urna, vitae pellentesque mauris sollicitudin at."
      // etc.
    }
  },
  "extensions": {}
}

Notice that the content in the response is formatted in HTML. We can also query the original body as rawMarkdownBody or any of the frontmatter attributes.

Next, let’s turn our attention to approaches for handling Markdown content once it has been queried.

Using DangerouslySetInnerHTML

dangerouslySetInnerHTML is a React feature that injects raw HTML content into a component’s rendered output by overriding the innerHTML property of the DOM node. It’s considered dangerous since it essentially bypasses React’s built-in mechanisms for rendering and sanitizing content, opening up the possibility of cross-site scripting (XSS) attacks without paying special attention.

That said, if you need to render HTML content dynamically but want to avoid the risks associated with dangerouslySetInnerHTML, consider using libraries that sanitize HTML input before rendering it, such as dompurify.

The dangerouslySetInnerHTML prop takes an __html object with a single key that should contain the raw HTML content. Here’s an example:

const DangerousComponent = () => {
  const rawHTML = "<p>This is <em>dangerous</em> content!</p>";

  return <div dangerouslySetInnerHTML={ { __html: rawHTML } } />;
};

To display Markdown using dangerouslySetInnerHTML in a Gatsby project, we need first to query the HTML string using Gatsby’s useStaticQuery hook:

import * as React from "react";
import { useStaticQuery, graphql } from "gatsby";

const DangerouslySetInnerHTML = () => {
  const data = useStaticQuery(graphql`
    query {
      markdownRemark(frontmatter: { title: { eq: "sample-markdown-file" } }) {
        html
      }
    }
  `);

  return <div></div>;
};

Now, the html property can be injected into the dangerouslySetInnerHTML prop.

import * as React from "react";
import { useStaticQuery, graphql } from "gatsby";

const DangerouslySetInnerHTML = () => {
  const data = useStaticQuery(graphql`
    query {
      markdownRemark(frontmatter: { title: { eq: "sample-markdown-file" } }) {
        html
      }
    }
  `);

  const markup = { __html: data.markdownRemark.html };

  return <div dangerouslySetInnerHTML={ markup }></div>;
};

This might look OK at first, but if we were to open the browser to view the content, we would notice that the image declared in the Markdown file is missing from the output. We never told Gatsby to parse it. We do have two options to include it in the query, each with pros and cons:

  1. Use a plugin to parse Markdown images.
    The gatsby-remark-images plugin is capable of processing Markdown images, making them available when querying the Markdown from the data layer. The main downside is the extra configuration it requires to set and render the files. Besides, Markdown images parsed with this plugin only will be available as HTML, so we would need to select a package that can render HTML content into React components, such as rehype-react.
  2. Save images in the static folder.
    The /static folder at the root of a Gatsby project can store assets that won’t be parsed by webpack but will be available in the public directory. Knowing this, we can point Markdown images to the /static directory, and they will be available anywhere in the client. The disadvantage? We are unable to leverage Gatsby’s image optimization features to minimize the overall size of the bundled package in the build process.

The gatsby-remark-images approach is probably most suited for larger projects since it is more manageable than saving all Markdown images in the /static folder.

Let’s assume that we have decided to go with the second approach of saving images to the /static folder. To reference an image in the /static directory, we just point to the filename without any special argument on the path.

const StaticImage = () => {
  return <img src={ "/desert.png" } alt="Desert" />;
};

react-markdown

The react-markdown package provides a component that renders markdown into React components, avoiding the risks of using dangerouslySetInnerHTML. The component uses a syntax tree to build the virtual DOM, which allows for updating only the changing DOM instead of completely overwriting it. And since it uses remark, we can combine react-markdown with remark’s vast plugin ecosystem.

Let’s install the package:

npm i react-markdown

Next, we replace our prior example with the ReactMarkdown component. However, instead of querying for the html property this time, we will query for rawMarkdownBody and then pass the result to ReactMarkdown to render it in the DOM.

import * as React from "react";
import ReactMarkdown from "react-markdown";
import { useStaticQuery, graphql } from "gatsby";

const MarkdownReact = () => {
  const data = useStaticQuery(graphql`
    query {
      markdownRemark(frontmatter: { title: { eq: "sample-markdown-file" } }) {
        rawMarkdownBody
      }
    }
  `);

  return <ReactMarkdown>{data.markdownRemark.rawMarkdownBody}</ReactMarkdown>;
};

markdown-to-jsx

markdown-to-jsx is the most popular Markdown component — and the lightest since it comes without any dependencies. It’s an excellent tool to consider when aiming for performance, and it does not require remark’s plugin ecosystem. The plugin works much the same as the react-markdown package, only this time, we import a Markdown component instead of ReactMarkdown.

npm i markdown-to-jsx
import * as React from "react";
import Markdown from "markdown-to-jsx";
import { useStaticQuery, graphql } from "gatsby";

const MarkdownToJSX = () => {
  const data = useStaticQuery(graphql`
    query {
      markdownRemark(frontmatter: { title: { eq: "sample-markdown-file" } }) {
        rawMarkdownBody
      }
    }
  `);

  return <Markdown> { data.markdownRemark.rawMarkdownBody }</Markdown>;
};

We have taken raw Markdown and parsed it as JSX. But what if we don’t necessarily want to parse it at all? We will look at that use case next.

react-md-editor

Let’s assume for a moment that we are creating a lightweight CMS and want to give users the option to write posts in Markdown. In this case, instead of parsing the Markdown to HTML, we need to query it as-is.

Rather than creating a Markdown editor from scratch to solve this, several packages are capable of handling the raw Markdown for us. My personal favorite is react-md-editor.

Let’s install the package:

npm i @uiw/react-md-editor

The MDEditor component can be imported and set up as a controlled component:

import * as React from "react";
import { useState } from "react";
import MDEditor from "@uiw/react-md-editor";

const ReactMDEditor = () => {
  const [value, setValue] = useState("**Hello world!!!**");

  return <MDEditor value={ value } onChange={ setValue } />;
};

The plugin also comes with a built-in MDEditor.Markdown component used to preview the rendered content:

import * as React from "react";
import { useState } from "react";
import MDEditor from "@uiw/react-md-editor";

const ReactMDEditor = () => {
  const [value, setValue] = useState("**Hello world!**");

  return (
    <>
      <MDEditor value={value} onChange={ setValue } />
      <MDEditor.Markdown source={ value } />
    </>
  );
};
Markdown previewer
The plugin includes a feature that shows a preview of the rendered Markdown. (Large preview)

That was a look at various headaches you might encounter when working with Markdown files in Gatsby. Next, we are turning our attention to another type of file, PDF.

Solving PDF Headaches In Gatsby

PDF files handle content with a completely different approach to Markdown files. With Markdown, we simplify the content to its most raw form so it can be easily handled across different front ends. PDFs, however, are the content presented to users on the front end. Rather than extracting the raw content from the file, we want the user to see it as it is, often by making it available for download or embedding it in a way that the user views the contents directly on the page, sort of like a video.

I want to show you four approaches to consider when embedding a PDF file on a page in a Gatsby project.

Using The <iframe> Element

The easiest way to embed a PDF into your Gatsby project is perhaps through an iframe element:

import * as React from "react";
import samplePDF from "./assets/lorem-ipsum.pdf";

const IframePDF = () => {
  return <iframe src={ samplePDF }></iframe>;
};

It’s worth calling out here that the iframe element supports lazy loading (loading="lazy") to boost performance in instances where it doesn’t need to load right away.

Embedding A Third-Party Viewer

There are situations where PDFs are more manageable when stored in a third-party service, such as Drive, which includes a PDF viewer that can embedded directly on the page. In these cases, we can use the same iframe we used above, but with the source pointed at the service.

import * as React from "react";

const ThirdPartyIframePDF = () => {
  return (
    <iframe
      src="https://drive.google.com/file/d/1IiRZOGib_0cZQY9RWEDslMksRykEnrmC/preview"
      allowFullScreen
      title="PDF Sample in Drive"
    />
  );
};

It’s a good reminder that you want to trust the third-party content that’s served in an iframe. If we’re effectively loading a document from someone else’s source that we do not control, your site could become prone to security vulnerabilities should that source become compromised.

Using react-pdf

The react-pdf package provides an interface to render PDFs as React components. It is based on pdf.js, a JavaScript library that renders PDFs using HTML Canvas.

To display a PDF file on a <canvas>, the react-pdf library exposes the Document and Page components:

  • Document: Loads the PDF passed in its file prop.
  • Page: Displays the page passed in its pageNumber prop. It should be placed inside Document.

We can install to our project:

npm i react-pdf

Before we put react-pdf to use, we will need to set up a service worker for pdf.js to process time-consuming tasks such as parsing and rendering a PDF document.

import * as React from "react";
import { pdfjs } from "react-pdf";

pdfjs.GlobalWorkerOptions.workerSrc = "https://unpkg.com/pdfjs-dist@3.6.172/build/pdf.worker.min.js";

const ReactPDF = () => {
  return <div></div>;
};

Now, we can import the Document and Page components, passing the PDF file to their props. We can also import the component’s necessary styles while we are at it.

import * as React from "react";
import { Document, Page } from "react-pdf";

import { pdfjs } from "react-pdf";
import "react-pdf/dist/esm/Page/AnnotationLayer.css";
import "react-pdf/dist/esm/Page/TextLayer.css";

import samplePDF from "./assets/lorem-ipsum.pdf";

pdfjs.GlobalWorkerOptions.workerSrc = "https://unpkg.com/pdfjs-dist@3.6.172/build/pdf.worker.min.js";

const ReactPDF = () => {
  return (
    <Document file={ samplePDF }>
      <Page pageNumber={ 1 } />
    </Document>
  );
};

Since accessing the PDF will change the current page, we can add state management by passing the current pageNumber to the Page component:

import { useState } from "react";

// ...

const ReactPDF = () => {
  const [currentPage, setCurrentPage] = useState(1);

  return (
    <Document file={ samplePDF }>
      <Page pageNumber={ currentPage } />
    </Document>
  );
};

One issue is that we have pagination but don’t have a way to navigate between pages. We can change that by adding controls. First, we will need to know the number of pages in the document, which is accessed on the Document component’s onLoadSuccess event:

// ...

const ReactPDF = () => {
  const [pageNumber, setPageNumber] = useState(null);
  const [currentPage, setCurrentPage] = useState(1);

  const handleLoadSuccess = ({ numPages }) => {
    setPageNumber(numPages);
  };

  return (
    <Document file={ samplePDF } onLoadSuccess={ handleLoadSuccess }>
      <Page pageNumber={ currentPage } />
    </Document>
  );
};

Next, we display the current page number and add “Next” and “Previous” buttons with their respective handlers to change the current page:

// ...

const ReactPDF = () => {
  const [currentPage, setCurrentPage] = useState(1);
  const [pageNumber, setPageNumber] = useState(null);

  const handlePrevious = () => {
    // checks if it isn't the first page
    if (currentPage > 1) {
      setCurrentPage(currentPage - 1);
    }
  };

  const handleNext = () => {
    // checks if it isn't the last page
    if (currentPage < pageNumber) {
      setCurrentPage(currentPage + 1);
    }
  };

  const handleLoadSuccess = ({ numPages }) => {
    setPageNumber(numPages);
  };

  return (
    <div>
      <Document file={ samplePDF } onLoadSuccess={ handleLoadSuccess }>
        <Page pageNumber={ currentPage } />
      </Document>
      <button onClick={ handlePrevious }>Previous</button>
      <p>{currentPage}</p>
      <button onClick={ handleNext }>Next</button>
    </div>
  );
};

This provides us with everything we need to embed a PDF file on a page via the HTML <canvas> element using react-pdf and pdf.js.

There is another similar package capable of embedding a PDF file in a viewer, complete with pagination controls. We’ll look at that next.

Using react-pdf-viewer

Unlike react-pdf, the react-pdf-viewer package provides built-in customizable controls right out of the box, which makes embedding a multi-page PDF file a lot easier than having to import them separately.

Let’s install it:

npm i @react-pdf-viewer/core@3.12.0 @react-pdf-viewer/default-layout

Since react-pdf-viewer also relies on pdf.js, we will need to create a service worker as we did with react-pdf, but only if we are not using both packages at the same time. This time, we are using a Worker component with a workerUrl prop directed at the worker’s package.

import * as React from "react";
import { Worker } from "@react-pdf-viewer/core";

const ReactPDFViewer = () => {
  return (
    <>
      <Worker workerUrl="https://unpkg.com/pdfjs-dist@3.4.120/build/pdf.worker.min.js"></Worker>
    </>
  );
};

Note that a worker like this ought to be set just once at the layout level. This is especially true if you intend to use the PDF viewer across different pages.

Next, we import the Viewer component with its styles and point it at the PDF through its fileUrl prop.

import * as React from "react";
import { Viewer, Worker } from "@react-pdf-viewer/core";

import "@react-pdf-viewer/core/lib/styles/index.css";

import samplePDF from "./assets/lorem-ipsum.pdf";

const ReactPDFViewer = () => {
  return (
    <>
      <Viewer fileUrl={ samplePDF } />
      <Worker workerUrl="https://unpkg.com/pdfjs-dist@3.6.172/build/pdf.worker.min.js"></Worker>
    </>
  );
};

Once again, we need to add controls. We can do that by importing the defaultLayoutPlugin (including its corresponding styles), making an instance of it, and passing it in the Viewer component’s plugins prop.

import * as React from "react";
import { Viewer, Worker } from "@react-pdf-viewer/core";
import { defaultLayoutPlugin } from "@react-pdf-viewer/default-layout";

import "@react-pdf-viewer/core/lib/styles/index.css";
import "@react-pdf-viewer/default-layout/lib/styles/index.css";

import samplePDF from "./assets/lorem-ipsum.pdf";

const ReactPDFViewer = () => {
  const defaultLayoutPluginInstance = defaultLayoutPlugin();

  return (
    <>
      <Viewer fileUrl={ samplePDF } plugins={ [defaultLayoutPluginInstance] } />
      <Worker workerUrl="https://unpkg.com/pdfjs-dist@3.6.172/build/pdf.worker.min.js"></Worker>
    </>
  );
};

Again, react-pdf-viewer is an alternative to react-pdf that can be a little easier to implement if you don’t need full control over your PDF files, just the embedded viewer.

There is one more plugin that provides an embedded viewer for PDF files. We will look at it, but only briefly, because I personally do not recommend using it in favor of the other approaches we’ve covered.

Why You Shouldn’t Use react-file-viewer

The last plugin we will check out is react-file-viewer, a package that offers an embedded viewer with a simple interface but with the capacity to handle a variety of media in addition to PDF files, including images, videos, PDFs, documents, and spreadsheets.

import * as React from "react";
import FileViewer from "react-file-viewer";

const PDFReactFileViewer = () => {
  return <FileViewer fileType="pdf" filePath="/lorem-ipsum.pdf" />;
};

While react-file-viewer will get the job done, it is extremely outdated and could easily create more headaches than it solves with compatibility issues. I suggest avoiding it in favor of either an iframe, react-pdf, or react-pdf-viewer.

Solving 3D Model Headaches In Gatsby

I want to cap this brief two-part series with one more media type that might cause headaches in a Gatsby project: 3D models.

A 3D model file is a digital representation of a three-dimensional object that stores information about the object’s geometry, texture, shading, and other properties of the object. On the web, 3D model files are used to enhance user experiences by bringing interactive and immersive content to websites. You are most likely to encounter them in product visualizations, architectural walkthroughs, or educational simulations.

There is a multitude of 3D model formats, including glTF OBJ, FBX, STL, and so on. We will use glTF models for a demonstration of a headache-free 3D model implementation in Gatsby.

The GL Transmission Format (glTF) was designed specifically for the web and real-time applications, making it ideal for our example. Using glTF files does require a specific webpack loader, so for simplicity’s sake, we will save the glTF model in the /static folder at the root of our project as we look at two approaches to create the 3D visual with Three.js:

  1. Using a vanilla implementation of Three.js,
  2. Using a package that integrates Three.js as a React component.

Using Three.js

Three.js creates and loads interactive 3D graphics directly on the web with the help of WebGL, a JavaScript API for rendering 3D graphics in real-time inside HTML <canvas> elements.

Three.js is not integrated with React or Gatsby out of the box, so we must modify our code to support it. A Three.js tutorial is out of scope for what we are discussing in this article, although excellent learning resources are available in the Three.js documentation.

We start by installing the three library to the Gatsby project:

npm i three

Next, we write a function to load the glTF model for Three.js to reference it. This means we need to import a GLTFLoader add-on to instantiate a new loader object.

import * as React from "react";
import * as THREE from "three";

import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";

const loadModel = async (scene) => {
  const loader = new GLTFLoader();
};

We use the scene object as a parameter in the loadModel function so we can attach our 3D model once loaded to the scene.

From here, we use loader.load() which takes four arguments:

  1. The glTF file location,
  2. A callback when the resource is loaded,
  3. A callback while loading is in progress,
  4. A callback for handling errors.
import * as React from "react";
import * as THREE from "three";

import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";

const loadModel = async (scene) => {
  const loader = new GLTFLoader();

  await loader.load(
    "/strawberry.gltf", // glTF file location
    function (gltf) {
      // called when the resource is loaded
      scene.add(gltf.scene);
    },
    undefined, // called while loading is in progress, but we are not using it
    function (error) {
      // called when loading returns errors
      console.error(error);
    }
  );
};

Let’s create a component to host the scene and load the 3D model. We need to know the element’s client width and height, which we can get using React’s useRef hook to access the element’s DOM properties.

import * as React from "react";
import * as THREE from "three";

import { useRef, useEffect } from "react";

// ...

const ThreeLoader = () => {
  const viewerRef = useRef(null);

  return <div style={ { height: 600, width: "100%" } } ref={ viewerRef }></div>; // Gives the element its dimensions
};

Since we are using the element’s clientWidth and clientHeight properties, we need to create the scene on the client side inside React’s useEffect hook where we configure the Three.js scene with its necessary complements, e.g., a camera, the WebGL renderer, and lights.

useEffect(() => {
  const { current: viewer } = viewerRef;

  const scene = new THREE.Scene();

  const camera = new THREE.PerspectiveCamera(75, viewer.clientWidth / viewer.clientHeight, 0.1, 1000);

  const renderer = new THREE.WebGLRenderer();

  renderer.setSize(viewer.clientWidth, viewer.clientHeight);

  const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
  scene.add(ambientLight);

  const directionalLight = new THREE.DirectionalLight(0xffffff);
  directionalLight.position.set(0, 0, 5);
  scene.add(directionalLight);

  viewer.appendChild(renderer.domElement);
  renderer.render(scene, camera);
}, []);

Now we can invoke the loadModel function, passing the scene to it as the only argument:

useEffect(() => {
  const { current: viewer } = viewerRef;

  const scene = new THREE.Scene();

  const camera = new THREE.PerspectiveCamera(75, viewer.clientWidth / viewer.clientHeight, 0.1, 1000);

  const renderer = new THREE.WebGLRenderer();

  renderer.setSize(viewer.clientWidth, viewer.clientHeight);

  const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
  scene.add(ambientLight);

  const directionalLight = new THREE.DirectionalLight(0xffffff);
  directionalLight.position.set(0, 0, 5);
  scene.add(directionalLight);

  loadModel(scene); // Here!

  viewer.appendChild(renderer.domElement);
  renderer.render(scene, camera);
}, []);

The last part of this vanilla Three.js implementation is to add OrbitControls that allow users to navigate the model. That might look something like this:

import * as React from "react";
import * as THREE from "three";

import { useRef, useEffect } from "react";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";

const loadModel = async (scene) => {
  const loader = new GLTFLoader();

  await loader.load(
    "/strawberry.gltf", // glTF file location
    function (gltf) {
      // called when the resource is loaded
      scene.add(gltf.scene);
    },
    undefined, // called while loading is in progress, but it is not used
    function (error) {
      // called when loading has errors
      console.error(error);
    }
  );
};

const ThreeLoader = () => {
  const viewerRef = useRef(null);

  useEffect(() => {
    const { current: viewer } = viewerRef;

    const scene = new THREE.Scene();

    const camera = new THREE.PerspectiveCamera(75, viewer.clientWidth / viewer.clientHeight, 0.1, 1000);

    const renderer = new THREE.WebGLRenderer();

    renderer.setSize(viewer.clientWidth, viewer.clientHeight);

    const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
    scene.add(ambientLight);

    const directionalLight = new THREE.DirectionalLight(0xffffff);
    directionalLight.position.set(0, 0, 5);
    scene.add(directionalLight);

    loadModel(scene);

    const target = new THREE.Vector3(-0.5, 1.2, 0);
    const controls = new OrbitControls(camera, renderer.domElement);
    controls.target = target;

    viewer.appendChild(renderer.domElement);

    var animate = function () {
      requestAnimationFrame(animate);
      controls.update();
      renderer.render(scene, camera);
    };
    animate();
  }, []);

  <div style={ { height: 600, width: "100%" } } ref={ viewerRef }></div>;
};

That is a straight Three.js implementation in a Gatsby project. Next is another approach using a library.

Using React Three Fiber

react-three-fiber is a library that integrates the Three.js with React. One of its advantages over the vanilla Three.js approach is its ability to manage and update 3D scenes, making it easier to compose scenes without manually handling intricate aspects of Three.js.

We begin by installing the library to the Gatsby project:

npm i react-three-fiber @react-three/drei

Notice that the installation command includes the @react-three/drei package, which we will use to add controls to the 3D viewer.

I personally love react-three-fiber for being tremendously self-explanatory. For example, I had a relatively easy time migrating the extensive chunk of code from the vanilla approach to this much cleaner code:

import * as React from "react";
import { useLoader, Canvas } from "@react-three/fiber";
import { OrbitControls } from "@react-three/drei";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";

const ThreeFiberLoader = () => {
  const gltf = useLoader(GLTFLoader, "/strawberry.gltf");

  return (
    <Canvas camera={ { fov: 75, near: 0.1, far: 1000, position: [5, 5, 5] } } style={ { height: 600, width: "100%" } }>
      <ambientLight intensity={ 0.4 } />
      <directionalLight color="white" />
      <primitive object={ gltf.scene } />
      <OrbitControls makeDefault />
    </Canvas>
  );
};

Thanks to react-three-fiber, we get the same result as a vanilla Three.js implementation but with fewer steps, more efficient code, and a slew of abstractions for managing and updating Three.js scenes.

Two Final Tips

The last thing I want to leave you with is two final considerations to take into account when working with media files in a Gatsby project.

Bundling Assets Via Webpack And The /static Folder

Importing an asset as a module so it can be bundled by webpack is a common strategy to add post-processing and minification, as well as hashing paths on the client. But there are two additional use cases where you might want to avoid it altogether and use the static folder in a Gatsby project:

  • Referencing a library outside the bundled code to prevent webpack compatibility issues or a lack of specific loaders.
  • Referencing assets with a specific name, for example, in a web manifest file.

You can find a detailed explanation of the static folder and use it to your advantage in the Gatsby documentation.

Embedding Files From Third-Party Services

Secondly, you can never be too cautious when embedding third-party services on a website. Replaced content elements, like <iframe>, can introduce various security vulnerabilities, particularly when you do not have control of the source content. By integrating a third party’s scripts, widgets, or content, a website or app is prone to potential vulnerabilities, such as iframe injection or cross-frame scripting.

Moreover, if an integrated third-party service experiences downtime or performance issues, it can directly impact the user experience.

Conclusion

This article explored various approaches for working around common headaches you may encounter when working with Markdown, PDF, and 3D model files in a Gatsby project. In the process, we leveraged several React plugins and Gatsby features that handle how content is parsed, embed files on a page, and manage 3D scenes.

This is also the second article in a brief two-part series that addresses common headaches working with a variety of media types in Gatsby. The first part covers more common media files, including images, video, and audio.

If you’re looking for more cures to Gatsby headaches, please check out my other two-part series that investigates internationalization.

See Also

Smashing Editorial (gg, yk, il)