This library provides a serializer for PortableText content to ReactPDF components. The resulting "block content as PDF" can be used both in the browser (React) and the server (Node).
The team at Sanity developed a specification called "Portable Text" which provides a feature-rich, extensible way to represent and intermingle both rich text and custom content, stored and transmitted in a format not rigidly handcuffed to presentation mechanism (in contrast to HTML, Markdown, etc, which assume only one presentation channel and tie the data itself to how that data will be displayed).
For more information on the specification and Portable Text Editor for generating portable text content PortableText.org)
This library exports a React component (called PortableText
) that takes block content as a prop and serializes it to ReactPDF components that represent the PDF's contents using ReactPDFs scalar components (View
, Text
, Image
, etc). A few of the main utility modules used in that PortableText
component are also exported (in case they are needed for non-standard workflows).
----> It is up to you to wrap the PortableText serialzer component in the Document
and Page
components from React PDF as needed before using them in the workflows outlined in the "Browser" and "Node" sections below! The serializer intentionally begins at the View
level and does not include Document/Page
wrappers so that it can be used in multiple locations in one PDF (if desired).
These ReactPDF components which represent the contents of PDF can be consumed by ReactPDFs user interface components, which include:
PDFViewer
: Renders the generated PDF in the browser in an embedded preview windowPDFDownloadLink
: Allows the user to download the generated PDF as a fileBlobProvider
: Get the blob data for the generated PDF without rendering a UI element on screen
See the specs for these components in the React Pdf Components Docs.
These ReactPDF components which represent the contents of PDF can also be consumed in a Node environment, using React PDFs Node API, which includes:
renderToFile
: create a file from the PDFrenderToString
: stringify the PDFrenderToBuffer
: turn the PDF into a bufferrenderToStream
: turn the PDF into a Node Stream
See the specs for these node handlers in the React Pdf Node API Docs.
This library's PortableText
component is a wrapper around the PortableText React serializer, so it supports all the core props of that library, see @portabletext/react.
-
value
(required): an array of portable text blocks to serialize and render -
components
(optional): an object with custom components to be used for rendering blocks. As stated in the@portabletext/react
docs:Default components are provided for all standard features of the Portable Text spec.
You can pass an object of components to use, both to override the defaults and to provide components for your custom content types. Provided components will be merged with the defaults. In other words, you only need to provide the things you want to override."
This ReactPDF serializer library also provides a few additional props that are specific to the ReactPDF use case. Because the ReactPDF contents themselves are not part of the DOM/html, they will not respect the CSS rules defined for the parent application.
Therefore, the library provides a default set of styles for its default components, but also exposes the following props to make styling easier without having to overwrite the default components themselves:
-
defaultComponentStyles
(optional): An style object which is deep merged with the style object provided to the default components (meaning you only need to provide any portions of the object for which you want to override or extend the styling). The shape of that object is:{ block: { h1: Style h2: Style h3: Style h4: Style h5: Style h6: Style normal: Style blockquote: Style } text: { h1: Style h2: Style h3: Style h4: Style h5: Style h6: Style normal: Style blockquote: Style } marks: { strong: Style em: Style link: Style underline: Style "strike-through": Style code: Style superscript: Style subscript: Style highlight: Style } list: { list: Style listDeep: Style listItemWrapper: Style listItemDecorator: Style } image: Style }
where "Style" is an object containing any of the "Valid CSS Properties" supported by the ReactPDF styling API
See ReactPDF Styling for important information on what properties are allowed and what formats are valid for the values of those properties.
This gives you a convenient way to add/change styling for the default components without overwriting the component structure itself. However (as mentioned above) you can also pass an object to the
components
prop to overwrite the default components and/or define components for custom portable text block types (and you should give those components inline styles using the ReactPDF-supported CSS properties). -
baseFontSize
(optional): A numberic value that represents the font size of the "normal" block type (in "pt") -- this is used to calculate font sizing and layout spacing for all block types. When not provided, defaults to12
.
You may provide both the components
and defaultComponentStyles
props, but conflicts are prohibited and will throw an error. "Conflicts" in this context means providing values for a path in defaultComponentStyles
that overlap with either a path in components
or with a direct child of components.types
.
For example, you can NOT provide a value for:
-
both
defaultComponentStyles.block.h1
andcomponents.block.h1
. -
both
defaultComponentStyles.block
andcomponents.types.block
.and so on
However, you CAN provide a value for :
-
both
defaultComponentStyles.block.h1
andcomponents.block.h2
-
both
defaultComponentStyles.block
andcomponents.types.list
and so on.
The error message thrown when conflicts are encountered contains the same clarification and examples as this readme section.
To launch a simple browser-based demo application (the easiest way develop and visually confirm changes/serializers for new test components), run:
pnpm install && pnpm run dev
And open http://localhost:5173. A set of code-defined test blocks are rendered using the PortableText
component and displayed in a ReactPdf PDFViewer
component. See /demo/App.tsx
To add/modify the test blocks rendered, see /demo/blocks
.
pnpm run test
This will compare generated PDFs for a variety of complex test cases to pre-defined snapshots. These snapshots are folder-organized based on the value of Node's "os.platform()" util. The repo as is only contains "snapshots/darwin", since the main Sanity development environment is MacOS.
The first time a test is run for which a snapshot does not exist, it will create that snapshot and return true!
So make sure to look at the snapshot and visually validate that it is what you want before continuing to test against it and/or committing the snapshot.
The darwin snapshots are used for the unit testing on Github Actions for releases, so be particularly careful when modifying them.
If you make a change and it no longer passes the test, consider whether that is the result of a bug (in which case, fix the bug and test again) or the result of an intentional change you've made (for example, to some default styling/layout code). If it a valid result of an intentional change, delete the snapshot files for the test case(s) in question (those files will be the base snapshot and the .diff and .new files that were generated next to it by the failed comparison), then re-run the tests to create a new base snapshot. See the pdf-visual-diff library for more information (used for creating/comparing the snapshots in our tests).
When locally developing using a ReactPDF PDFViewer
component (e.g. in a front end application), there are situations where making code changes that affect the PDF's components will not re-render the PDFViewer correctly and you have to reload the browser window to get the PDFViewer working again. This is an issue internal to the ReactPDF library and can't be fixed in @portabletext/react
.
The ReactPDF docs or Github issues may have more details.
However, giving the PDF Viewer a key that changes on hot load will cause it to correctly re-render/reload on code changes. For example, you might do something like the below:
// The key will force a re-render of the PDFViewer on hot load (because Date.now() changes).
// This example assumes isLocalDevelopment returns true in local dev and false in deployed environments.
const hotloadKey = isLocalDevelopment() ? { key: Date.now() } : {}
<PDFViewer
{...hotloadKey}
// other props
>
{children} // e.g. Document and Page containing a PortableText pdf serializer component
</PDFViewer>
In a more advanced case (e.g. if you are having trouble getting the PDF to update in response to user interactions), you might need to create a key that is dependent on the data that actually is flowing into the PDF itself.