Gatsby Headaches: Working With Media (Part 2)
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’stitle
anddate
, 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:
- Use a plugin to parse Markdown images.
Thegatsby-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 asrehype-react
. - 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 thepublic
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 } />
</>
);
};
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 itsfile
prop.Page
: Displays the page passed in itspageNumber
prop. It should be placed insideDocument
.
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:
- Using a vanilla implementation of Three.js,
- 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:
- The glTF file location,
- A callback when the resource is loaded,
- A callback while loading is in progress,
- 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
- “Gatsby Headaches And How To Cure Them: i18n (Part 1)”
- “Gatsby Headaches And How To Cure Them: i18n (Part 2)”
- “Gatsby Headaches And How To Cure Them: Media Files (Part 1)”