NextJS adapter with Guillotine support and basic views
Requires Enonic XP 8.1+ with Guillotine 9.
npm i --save @enonic/nextjs-adapterAll functions and views are split into 4 categories by usage:
@enonic/nextjs-adapter-- can be used both on server and client sides@enonic/nextjs-adapter/server-- can only be used on server side@enonic/nextjs-adapter/client-- can only be used on client side@enonic/nextjs-adapter/views/...-- views folder, every view has its own file with a default export
Here's the usage example in your project:
// server-side functions are accessible at /server
import {fetchContent} from "@enonic/nextjs-adapter/server";
// views are placed in a folder called `views` and are default exports in files
import MainView from "@enonic/nextjs-adapter/views/MainView"
// ...
const props = await fetchContent({
contentPath: '/content/path',
locale: 'en',
});
// ...
return <MainView {...props}/>They are available at @enonic/nextjs-adapter/server
Fetches the content/component by path with component queries optimization, page structure as well as runtime info calculation. This is the main method for querying content.
| Argument | Description |
|---|---|
context |
Execution context |
Usage:
import {fetchContent} from '@enonic/nextjs-adapter/server';
import {headers} from 'next/headers';
const context = {
contentPath: '/current/content/path',
locale: 'en',
headers: headers(),
};
const response = fetchContent(context);Response type:
type FetchContentResult = {
error?: {
code: string,
message: string
} | null;
data: Record<string, any> | null, // Result of the content query
common: Record<string, any> | null, // Result of the common query
meta: MetaData, // Runtime information
page: PageComponent | null, // The structure of the page (not present when rendering single component)
};Makes a custom request to Guillotine. Used by fetchContent() method.
| Argument | Description |
|---|---|
contentApiUrl |
Guillotine API URL |
options |
Request and Next.js config options (Optional) |
Usage:
import {fetchGuillotine} from '@enonic/nextjs-adapter/server';
const apiUrl = 'http://domain:8080/api/com.enonic.app.guillotine:graphql';
const body = {
query: 'qraphql query as string',
variables: {
foo: 'bar',
},
};
const headers = {
'custom-header': 'header-value',
};
const opts = {
body,
headers,
method: 'POST',
next: {
revalidate: 60,
tags: ['tag1', 'tag2'],
}
};
const result = fetchGuillotine(apiUrl, opts);Response type:
type GuillotineResult = {
error?: {
code: string,
message: string
} | null;
[dataKey: string]: any;
};Makes custom third-party service request. Used by the fetchGuillotine() method.
| Argument | Description |
|---|---|
apiUrl |
Service API URL |
options |
Request and Next.js config options (Optional) |
Usage:
import {fetchFromApi} from '@enonic/nextjs-adapter/server';
const apiUrl = 'http://domain:8000/graphql/api';
const body = {
query: 'qraphql query as string',
variables: {
foo: 'bar',
},
}
const headers = {
'custom-header': 'header-value',
}
const opts = {
body,
headers,
method: 'POST',
next: {
revalidate: 60,
tags: ['tag1', 'tag2'],
}
};
const result = fetchFromApi(apiUrl, opts);fetchContentPathsForAllLocales(query?: string, countPerLocale?: number) => Promise<ContentPathItem[]>
Loads all content paths for all locales. Generally used for static site generation.
| Argument | Description |
|---|---|
query |
Custom GraphQL query (Optional) * |
countPerLocale |
Max result count per locale (Optional, defaults to 999) |
* Default query gets up to countPerLocale results, sorts them by modifiedTime and excludes following content types:
"base:shortcut",
"portal:fragment",
"portal:template-folder",
"portal:page-template",
"media:*"
Usage:
import {fetchContentPathsForAllLocales} from '@enonic/nextjs-adapter/server';
const contentPaths = fetchContentPathsForAllLocales('custom graphql query', 1001);fetchContentPathsForLocale(mapping: LocaleMapping, query?: string, count?: number) => Promise<ContentPathItem[]>
Loads all content paths for the current locale. Generally used for static site generation. Used by fetchContentPathsForAllLocales()
| Argument | Description |
|---|---|
mapping |
Mapping locale to project |
query |
Custom GraphQL query (Optional, default value here) |
count |
Max result count (Optional, defaults to 999) |
Usage:
import {fetchContentPathsForLocale} from '@enonic/nextjs-adapter/server';
import {getLocaleMapping} from '@enonic/nextjs-adapter';
const mapping = getLocaleMapping({
contentPath: '/current/content/path',
});
const contentPaths = fetchContentPathsForLocale(mapping, 'custom graphql query', 1001);They are available at @enonic/nextjs-adapter/client
Create a React.js context that allows child elements to access and modify current locale as well as localize static texts for current locale. See useLocaleContext() method for example.
| Argument | Type | Description |
|---|---|---|
locale |
String |
Set the initial locale value |
Usage:
import {LocaleContextProvider} from '@enonic/nextjs-adapter/client';
<LocaleContextProvider locale="en"> ...child elements... </LocaleContextProvider>Methods to access and modify current locale as well as localize static texts, where LocaleContextType is:
interface LocaleContextType {
dictionary: Dict
locale: string
localize: (key: string, ...args: any[]) => string
setLocale: (locale: string) => Promise<Dict>
}Usage:
'use client';
import {useLocaleContext} from '@enonic/nextjs-adapter/client';
export default function ClientSideComponent() {
const {locale, localize, setLocale} = useLocaleContext();
console.log(`Current locale: ${locale}`);
const localizedText = localize('text.key');
setLocale('en').then((dict) => {
console.log(`New locale: ${locale}`);
});
// ...
}They are available at @enonic/nextjs-adapter
Every Guillotine *Url field comes with two functions: *UrlQuery(args) builds the GraphQL selection for the field, and *Url(result)
turns the returned object into the final URL string.
| Field | Query builder | String function |
|---|---|---|
imageUrl |
imageUrlQuery(args: ImageUrlArgs) |
imageUrl(data, meta) |
mediaUrl |
mediaUrlQuery(args?: MediaUrlArgs) |
mediaUrl(data, meta) |
attachmentUrl |
attachmentUrlQuery(args?: MediaUrlArgs) |
attachmentUrl(data, meta) |
pageUrl |
pageUrlQuery(args?: PageUrlArgs) |
pageUrl(data, meta) |
The query builders accept the same arguments as the Guillotine field (scale, quality, background, format, filter for
imageUrl; download for mediaUrl/attachmentUrl; params for pageUrl) and select every field of the returned object:
path, queryString, context, id, fingerprint, name, plus scale for images and intent for media and attachments
(pageUrl has baseUrl, path and queryString).
Media functions use meta.branch to return the authenticated /api/media proxy path for draft content and an absolute XP media URL
(ENONIC_MEDIA_CDN, defaulting to ENONIC_API) for live content. pageUrl returns a relative path for the Next.js router, prefixed with
the locale unless it is the default locale (meta.locale vs meta.defaultLocale). meta is the runtime data returned by
fetchContent. Pass {locale} alone to pageUrl to always prefix, e.g. for revalidatePath.
The string functions take that object (ImageUrl, AttachmentUrl or PageUrl from the adapter types) and return undefined when given undefined;
All four append queryString to path and need only path (queryString is optional), so links without a Guillotine object can
pass a bare {path}, e.g. pageUrl({path: '/'}, meta) for the site root.
Usage:
import {imageUrlQuery, pageUrlQuery, imageUrl, pageUrl} from '@enonic/nextjs-adapter';
const query = `
photos {
... on media_Image {
${imageUrlQuery({scale: 'width(500)'})}
}
}
parent {
${pageUrlQuery()}
}`;
<img src={imageUrl(photo.imageUrl, meta)}/>
<Link href={pageUrl(parent.pageUrl, meta)}>Back</Link>localizeMappings(mappings: UrlMappingRule[], mapping: LocaleMapping) => UrlMappingRule[] prefixes /api/mappings targets with the
mapping's locale unless it is the default one, leaving sources untouched.
This is a utility function for querying for RichTextData needed for RichTextView. It creates a graphql query string for
HTML area input type with given field name.
NOTE! On the server the adapter reads
ENONIC_API,ENONIC_MEDIA_CDN,ENONIC_APP_NAMEandENONIC_MAPPINGS. In the browser it reads theNEXT_PUBLIC_ENONIC_API,NEXT_PUBLIC_ENONIC_MEDIA_CDN,NEXT_PUBLIC_ENONIC_APP_NAMEandNEXT_PUBLIC_ENONIC_MAPPINGScopies, so define the ones your client components need (e.g.NEXT_PUBLIC_ENONIC_API=$ENONIC_APIforimageUrl()in a client component,NEXT_PUBLIC_ENONIC_APP_NAMEwhen_mappings.tsis imported client-side). Without them media URLs stay relative and the locale helpers throw in the browser.
The macro configs, links and images are selected through the named fragments RichTextMacros, RichTextLinks and RichTextImages, so a
document with many rich text fields stays small. fetchGuillotine() (and therefore fetchContent()) appends the fragment definitions once
per document. If you send such a query with your own HTTP client, append richTextFragments() to it yourself, or pass the document
through withRichTextFragments(query).
| Argument | Description |
|---|---|
fieldName |
HTML area field name |
options |
Optional per-field image settings passed to Guillotine's processHtml input. Each omitted setting independently retains its current behavior. |
RichTextQueryOptions is exported by the adapter:
| Option | Type | Default when omitted |
|---|---|---|
imageWidths |
number[] |
[400, 800, 1200] for responsive srcset candidates. |
imageSizes |
string |
"(max-width: 400px) 400px, (max-width: 800px) 800px, 1200px" for the HTML sizes attribute. |
imageSrcWidth |
number |
Not sent; Guillotine retains its default primary image width of 768px. |
Passing an empty array or string is an explicit override, not an omitted option. These settings apply at query time, not as rendering props. Different fields in the same document can use different settings while sharing the same fragment definitions.
import {richTextQuery, type RichTextQueryOptions} from '@enonic/nextjs-adapter';
// Only the primary source width changes; responsive widths and sizes keep their defaults.
const bioQuery = richTextQuery('bio', {imageSrcWidth: 640});
const imageOptions: RichTextQueryOptions = {
imageWidths: [320, 640, 960],
imageSizes: '(max-width: 640px) 100vw, 640px',
imageSrcWidth: 640
};
const textQuery = richTextQuery('text', imageOptions);Usage:
import {richTextQuery} from '@enonic/nextjs-adapter';
const query = `query($path:ID!, $scopeKey:String, $project:String, $branch:String){
guillotine(scopeKey: $scopeKey, project: $project, branch: $branch) {
get(key:$path) {
_path
type
${richTextQuery('htmlField')}
}
}
}`;Utility method to validate data returned by fetchContent() method. Throws an error or calls Next.js notFound() if data
is invalid. Also prevents shortcut content types from being rendered in preview/edit modes.
| Argument | Description |
|---|---|
props |
FetchContentResult object |
Usage:
import {fetchContent} from '@enonic/nextjs-adapter/server';
import {validateData} from '@enonic/nextjs-adapter';
const data = await fetchContent({
contentPath: '/path/to/content',
locale: 'en',
});
validateData(data);Resolves the Enonic XP branch (master or draft) for the current request based on its render mode.
Returns master for LIVE and NEXT render modes, and draft otherwise (e.g. EDIT, PREVIEW, INLINE).
| Argument | Description |
|---|---|
context |
Context object (Optional) |
Usage:
import {getContentBranch} from '@enonic/nextjs-adapter';
const branch = getContentBranch({
contentPath: '/current/content/path',
locale: 'en',
});Symmetrically encrypts a params object into a base64url string using AES-256-GCM. The secret is hashed with SHA-256
to derive the key, so any string length works. Pair with decryptParams() to read the value back.
| Argument | Description |
|---|---|
params |
Plain object to encrypt |
secret |
Secret string used to derive the encryption key |
Usage:
import {encryptParams} from '@enonic/nextjs-adapter';
const blob = encryptParams({userId: '123', role: 'editor'}, process.env.ENONIC_API_TOKEN);Decrypts a base64url string produced by encryptParams(). Returns null if the input is malformed,
tampered with, or was encrypted with a different secret.
| Argument | Description |
|---|---|
blob |
Base64url string produced by encryptParams |
secret |
Same secret used during encryption |
Usage:
import {decryptParams} from '@enonic/nextjs-adapter';
const params = decryptParams(blob, process.env.ENONIC_API_TOKEN);
if (params) {
// use params.userId, params.role, ...
}Registry containing definitions of all components (i.e. pages, parts, layouts, macros, etc. ). It is used in the runtime by nextjs-adapter
to make component queries and render components. It has several public methods:
Sets up a common query that is going to be executed along with component queries and passed to every component on the page.
| Argument | Description |
|---|---|
query |
Common query definition |
Usage:
import {ComponentRegistry} from '@enonic/nextjs-adapter';
const query = {
query: 'graphql query',
variables: ({path, scopeKey, branch, project}, context, config) => {
return {
path: path + '/some/processing',
scopeKey,
branch,
project,
};
}
}
ComponentRegistry.setCommonQuery(query);Gets the common query definition.
interface GlobalVariables {
path: string;
scopeKey: string;
branch: string;
project: string;
}
type QueryGetter = (vars: GlobalVariables, context?: Context, config?: any) => string;
type VariablesGetter = (vars: GlobalVariables, context?: Context, config?: any) => GlobalVariables & Record<string, any>;
type SelectedQueryMaybeVariablesFunc =
string |
QueryGetter |
{
query: string | QueryGetter,
variables: VariablesGetter
} |
[string | QueryGetter, VariablesGetter];Usage:
import {ComponentRegistry} from '@enonic/nextjs-adapter';
const query = ComponentRegistry.getCommonQuery();Gets component definition from the ComponentRegistry.
| Argument | Description |
|---|---|
component |
Page component to get definition for |
Usage:
import {ComponentRegistry} from '@enonic/nextjs-adapter';
const definition = ComponentRegistry.getByComponent(component);interface ComponentDefinition {
catchAll?: boolean; // set automatically depending on the binding
query?: SelectedQueryMaybeVariablesFunc,
configQuery?: string,
processor?: DataProcessor,
view?: React.FunctionComponent<any>
}Saves the component definition in ComponentRegistry by name.
NOTE:
addComponentis used for defining general types of Enonic XP components bynextjs-adapterso you don't need to do it manually. Overriding default setup may break Enonic XP integration!
| Argument | Description |
|---|---|
name |
Component name |
obj |
Component definition |
Usage:
import {ComponentRegistry} from '@enonic/nextjs-adapter';
const definition = {}
ComponentRegistry.addMacro('macro-name', definition);Gets the component definition stored in ComponentRegistry by its name.
NOTE: Read addComponent note before using
getComponent.
| Argument | Description |
|---|---|
name |
Component name |
Usage:
import {ComponentRegistry} from '@enonic/nextjs-adapter';
const definition = ComponentRegistry.getMacro('macro-name');Response type: component definition
Gets all component entries ([name, definition] tuples) stored in ComponentRegistry.
NOTE: Read addComponent note before using
getComponents.
Usage:
import {ComponentRegistry} from '@enonic/nextjs-adapter';
const macros = ComponentRegistry.getMacros();Response type: list of [name, ComponentDefinition] tuples � see component definition.
In order to create association between locale and Enonic XP project, ENONIC_MAPPINGS environment variable should be set.
Functions for reading those mappings are available at @enonic/nextjs-adapter.
| Argument | Description |
|---|---|
context |
Context object |
Usage:
import {getLocaleMapping} from '@enonic/nextjs-adapter';
const mapping = getLocaleMapping({
contentPath: '/current/content/path',
locale: 'en',
});export interface LocaleMapping {
default: boolean
project: string
site: string
locale: string
}| Argument | Description |
|---|---|
projectId |
Enonic project ID (Optional) |
useDefault |
Use default locale as fallback (Optional, default: true) |
Usage:
import {getLocaleMappingByProjectId} from '@enonic/nextjs-adapter';
const mapping = getLocaleMappingByProjectId('project-id', true);| Argument | Description |
|---|---|
locale |
locale ID (Optional) |
useDefault |
Use default locale as fallback (Optional, default: true) |
Usage:
import {getLocaleMappingByLocale} from '@enonic/nextjs-adapter';
const mapping = getLocaleMappingByLocale('en', true);Attempts to get the locale info from the request. Along with that it also returns the default locale and all configured locales.
| Argument | Description |
|---|---|
context |
Context object |
Usage:
import {getRequestLocaleInfo} from '@enonic/nextjs-adapter';
const mapping = getRequestLocaleInfo({
contentPath: '/current/content/path',
locale: 'en',
});Response type:
interface LocaleInfo {
locale: string,
locales: string[],
defaultLocale: string
}These functions can be used both on server and client sides, but it is recommended to use native React.js context classes for client-side localization.
Sets the current locale and returns the dictionary for it.
| Argument | Description |
|---|---|
locale |
Locale id, i.e. en |
Usage:
import {I18n} from '@enonic/nextjs-adapter';
const dict = await I18n.setLocale('en');interface Dict {
[key: string]: string
}Localizes the static text by key and replaces placeholders with provided arguments.
| Argument | Description |
|---|---|
key |
text key |
...args |
template arguments |
Usage:
import {I18n} from '@enonic/nextjs-adapter';
const localizedText = I18n.localize('text.key', 'value1', 'value2');Returns the current locale.
Usage:
import {I18n} from '@enonic/nextjs-adapter';
const locale = I18n.getLocale();Returns dictionary for the current locale.
Usage:
import {I18n} from '@enonic/nextjs-adapter';
const dict = I18n.getDictionary();There is also a number of utility constants and functions available at @enonic/nextjs-adapter.
All of the following are exported from @enonic/nextjs-adapter:
IS_DEV_MODE; // True if current mode == development
APP_NAME; // Name of the app defined in .env files
APP_NAME_UNDERSCORED; // APP_NAME with underscores instead of dots
APP_NAME_DASHED; // APP_NAME with dashes instead of dots
CATCH_ALL; // Catch all component name
PORTAL_COMPONENT_ATTRIBUTE; // Portal component attribute name
PORTAL_REGION_ATTRIBUTE; // Portal region attribute name
JSESSIONID_HEADER; // JSESSIONID header name
PROJECT_ID_HEADER; // Project ID header name
RENDER_MODE_HEADER; // Render mode header name
XP_BASE_URL_HEADER; // XP base URL header name
enum XP_REQUEST_TYPE { // Enum for XP request types
COMPONENT = 'component',
TYPE = 'type',
PAGE = 'page',
}
enum RENDER_MODE { // Enum for render modes
INLINE = 'inline',
EDIT = 'edit',
PREVIEW = 'preview',
LIVE = 'live',
ADMIN = 'admin',
NEXT = 'next',
}
enum XP_COMPONENT_TYPE { // Enum for XP component types
PART = 'part',
LAYOUT = 'layout',
TEXT = 'text',
FRAGMENT = 'fragment',
PAGE = 'page',
}
// Sanitizes text according to graphql naming spec http://spec.graphql.org/October2021/#sec-Names
const sanitizeGraphqlName = (text: string) => string;See also: getContentBranch, encryptParams, decryptParams.
They are located in @enonic/nextjs-adapter/views folder. Each view is a default export in the corresponding file.
The main view of the application. It accepts the result of fetchContent method and is intended to be used as a view for your next.js route
| Argument | Type | Description |
|---|---|---|
common |
Record<string, any> | null |
Result of the common query |
data |
Record<string, any> | null |
Result of the graphql query |
page |
PageComponent | null |
Page structure |
meta |
MetaData |
Runtime info |
Usage:
import MainView from '@enonic/nextjs-adapter/views/MainView';
import {fetchContent} from '@enonic/nextjs-adapter/server';
export async function getServerSideProps(context: Context) {
const props = await fetchContent({
contentPath: '/content/path',
locale: 'en',
});
return {
props
}
}
export default MainView;Tag for disabling client side hydration if the condition is true. This will remove interactivity from children.
| Argument | Type | Description |
|---|---|---|
condition = true |
Boolean |
Condition to trigger static output |
element = 'div' |
String |
Html tag to use for static output |
Usage:
import StaticContent from '@enonic/nextjs-adapter/views/StaticContent';
<StaticContent condition={true} element="div"> ...child elements... </StaticContent>Floating badge in the bottom-right corner, styled like the Next.js dev indicator, for leaving Next.js draft mode. It renders only when draftMode().isEnabled is true, and hides itself inside iframes (i.e. in Content Studio). The label defaults to Draft, is followed by a ✕ glyph, and can be localized by the caller, e.g. with I18n.localize(). Clicking it runs a Server Action that calls draftMode().disable() and then reloads the page without the xp query parameter (the Content Studio preview marker, which would otherwise make the proxy re-enable draft mode), so content is served from the master branch again. The badge ships its own CSS module; its colors can be overridden through the --draft-mode-* custom properties set on the button.
| Argument | Type | Description |
|---|---|---|
label = 'Draft' |
String |
Badge text, e.g. a localized phrase |
Usage:
import {I18n, IS_DEV_MODE} from '@enonic/nextjs-adapter';
import DraftModeIndicator from '@enonic/nextjs-adapter/views/DraftModeIndicator';
// Root layout: render it conditionally (here: only under `next dev`) directly inside <body>,
// not inside <StaticContent> (which disables hydration)
<body>
{children}
{IS_DEV_MODE && <DraftModeIndicator label={I18n.localize('draft-mode')}/>}
</body>Requires transpilePackages: ['@enonic/nextjs-adapter'] in next.config.js so that Next.js compiles the bundled Server Action.
<RichTextView className="css-class" tag="section" data={data} meta={meta} customReplacer={customReplacerFn}>
Tag for displaying contents of html area input types. Takes care of processing macros, URLs and images inside.
RichTextData, LinkData, ImageData, MacroData, and MacroConfig are shared with and re-exported from
@enonic/react-components. Rich-text URL values can be strings (used unchanged) or URL parts (resolved by the adapter using its
locale and media configuration). The adapter renders the original reference entries without rewriting their data.
| Argument | Type | Description |
|---|---|---|
data |
RichTextData |
Rich text data |
meta |
MetaData |
Runtime data returned by fetchContent method. |
customReplacer |
Replacer |
Function to do custom element processing. Not invoked for image, link and macro nodes. Optional |
className |
String |
Class name to add to the root html element. Optional |
tag = 'section' |
ElementType |
Element to use as a root. An html tag name, a component, or Fragment to render no root element at all. Optional |
TIP! There is a utility function richTextQuery(fieldName) generating part of the graphql query to obtain
RichTextDatafor html area input types.
Usage:
import RichTextView from '@enonic/nextjs-adapter/views/RichTextView';
<RichTextView data={richTextData} meta={meta} tag="section" className="rich-text-view"></RichTextView>To render the rich text without any wrapping element, pass Fragment as the tag. Note that className has
nowhere to go in that case and is ignored, so apply styling to a surrounding element instead.
import {Fragment} from 'react';
import RichTextView from '@enonic/nextjs-adapter/views/RichTextView';
<RichTextView data={richTextData} meta={meta} tag={Fragment}></RichTextView>Tag for rendering page regions. It is useful when implementing custom page views. All necessary data can be acquired by running fetchContent()
| Argument | Type | Description |
|---|---|---|
page |
PageData | null; |
Page structure |
meta |
MetaData |
Runtime info |
name |
String |
Render only this region. Optional |
common |
any |
Result of common query. Optional |
Usage:
import Regions from '@enonic/nextjs-adapter/views/Region';
<Regions page={page} meta={meta} name="main" common={common}/>