@enonic/nextjs-adapter
TypeScript icon, indicating that this package has built-in type declarations

3.0.0-RC5 • Public • Published

Enonic NextJS Adapter

NextJS adapter with Guillotine support and basic views

Installation

npm i --save @enonic/nextjs-adapter

Usage

All 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(path, {
    contentPath: '/content/path',
    locale: 'en',
});
// ...

return <MainView {...props}/>

API

Server-side only functions

They are available at @enonic/nextjs-adapter/server


fetchContent(contentPath: string | string[], context: Context) => Promise<FetchContentResult>

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
path Path to content
context Execution context

Usage:

import {fetchContent} from '@enonic/nextjs-adapter/server';

const response = fetchContent('/path/to/content', 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)
};

fetchGuillotine(apiUrl: string, mapping: LocaleMapping, options?: FetchOptions) => Promise<GuillotineResult>

Makes a custom request to Guillotine. Used by fetchContent() method.

Argument Description
apiUrl Guillotine API URL
mapping Mapping locale to project
options Request and Next.js config options (Optional)

Usage:

import {fetchGuillotine} from '@enonic/nextjs-adapter/server';
import {getLocaleMapping} from '@enonic/nextjs-adapter';

const apiUrl = 'http://domain:8000/graphql/api';

const mapping = getLocaleMapping({
    contentPath: '/current/content/path',
});

const body = {
    query: 'qraphql query as string',
    variables: {
        foo: 'bar',
    },
};

const headers = {
    'custom-header': 'header-value',
};

const opts = {
    body,
    headers,
    method: 'GET',
    next: {
        revalidate: 60,
        tags: ['tag1', 'tag2'],
    }
};

const result = fetchGuillotine(apiUrl, mapping, opts);

Response type:

type GuillotineResult = {
    error?: {
        code: string,
        message: string
    } | null;
    [dataKey: string]: any;
};

fetchFromApi(apiUrl: string, mapping: LocaleMapping, options?: FetchOptions) => Promise<GuillotineResponseJson>

Makes custom third-party service request. Used by the fetchGuillotine() method.

Argument Description
apiUrl Service API URL
mapping Mapping locale to project
options Request and Next.js config options (Optional)

Usage:

import {fetchFromApi} from '@enonic/nextjs-adapter/server';
import {getLocaleMapping} from '@enonic/nextjs-adapter';

const apiUrl = 'http://domain:8000/graphql/api';

const mapping = getLocaleMapping({
    contentPath: '/current/content/path',
});

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, mapping, opts);

fetchContentPathsForAllLocales(path: string, query?: string, count?: number) => Promise<ContentPathItem[]>

Loads all content paths for all locales. Generally used for static site generation.

Argument Description
apiUrl Service API URL
query Request and Next.js config options (Optional) *
count Max result count (Optional, defaults to 999)

* Default query gets up to count 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 = fetchContentPathsForLocale('/', 'custom graphql query', 1001);

fetchContentPathsForLocale(path: string, 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
apiUrl Service API URL
mapping Mapping locale to project
query Request and Next.js config options (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);

Client-side only functions

They are available at @enonic/nextjs-adapter/client


<LocaleContextProvider locale="en">

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>

useLocaleContext() => LocaleContextType

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} = useLocaleContext();
    const localizedText = localize('text.key');
    // ...
}

Both client and server-side functions

They are available at @enonic/nextjs-adapter


getUrl(url: string, meta: MetaData) => string

Converts a site-relative or absolute URL to relative one for current viewer (Next.js/Enonic XP). Also takes care of locale if needed.

INFO: For your URLs to work both in Enonic XP and Next.js you need to:

  1. Query site-relative or absolute URLs from guillotine
  2. Wrap them with getUrl() function in the views
Argument Description
url URL you want to transform
meta Runtime data returned by fetchContent

Usage:

import {getUrl} from '@enonic/nextjs-adapter';

const urlRelativeToViewer = getUrl('/some/content/url', meta);

getAsset(url: string, meta: MetaData) => string

Converts a local asset URL to relative one for current viewer (Next.js/Enonic XP). It doesn't append locales unlike [getUrl()] (#get-url).

INFO: For your URLs to work both in Enonic XP and Next.js you need to:

  1. Use relative URL to local asset
  2. Wrap them with getAsset() function in the views
Argument Description
url asset URL you want to transform
meta Runtime data returned by fetchContent

Usage:

import {getAsset} from '@enonic/nextjs-adapter';

const urlRelativeToViewer = getAsset('/some/asset/url', meta);

richTextQuery(fieldName: string) => string

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.

Argument Description
fieldName HTML area field name

Usage:

import {richTextQuery} from '@enonic/nextjs-adapter';

const query = `query($path:ID!){
    guillotine {
        get(key:$path) {
            _path
            type
            ${richTextQuery('htmlField')}
        }
    }
}`;

validateData(props: FetchContentResult) => void

Validates data returned by fetchContent() method. Throws an error or notFound() if data is invalid.

Argument Description
props FetchContentResult object

Usage:

import {fetchContent} from '@enonic/nextjs-adapter/server';
import {validateData} from '@enonic/nextjs-adapter';

const data = fetchContent({
    contentPath: '/path/to/content',
    locale: 'en',
});

validateData(data);

ComponentRegistry

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:


static setCommonQuery(query: SelectedQueryMaybeVariablesFunc): void

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, context, config) => {
        return {
            path: path + '/some/processing'
        };
    }
}

ComponentRegistry.setCommonQuery(query);

static getCommonQuery(): SelectedQueryMaybeVariablesFunc

Gets the common query definition.

Response type:

type SelectedQueryMaybeVariablesFunc =
    string |
    QueryGetter |
    {
        query: string | QueryGetter,
        variables: VariablesGetter
    } |
    [string | QueryGetter, VariablesGetter];

Usage:

import {ComponentRegistry} from '@enonic/nextjs-adapter';

const query = ComponentRegistry.getCommonQuery();

static getByComponent(component: PageComponent): ComponentDefinition | undefined

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);

Response type:

interface ComponentDefinition {
    catchAll?: boolean; // set automatically depending on the binding
    query?: SelectedQueryMaybeVariablesFunc,
    configQuery?: string,
    processor?: DataProcessor,
    view?: React.FunctionComponent<any>
}

static addMacro(name: string, obj: ComponentDefinition): void

static addPart(name: string, obj: ComponentDefinition): void

static addLayout(name: string, obj: ComponentDefinition): void

static addCPage(name: string, obj: ComponentDefinition): void

static addContentType(name: string, obj: ComponentDefinition): void

static addComponent(name: string, obj: ComponentDefinition): void

Saves the component definition in ComponentRegistry by name.

NOTE: addComponent is used for defining general types of Enonic XP components by nextjs-adapter so 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);

static getMacro(name: string): ComponentDefinition | undefined

static getPart(name: string): ComponentDefinition | undefined

static getLayout(name: string): ComponentDefinition | undefined

static getPage(name: string): ComponentDefinition | undefined

static getContentType(name: string): ComponentDefinition | undefined

static getComponent(name: string): ComponentDefinition | undefined

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


static getMacros(): ComponentDefinition[]

static getParts(): ComponentDefinition[]

static getLayouts(): ComponentDefinition[]

static getPages(): ComponentDefinition[]

static getContentTypes(): ComponentDefinition[]

static getComponents(): ComponentDefinition[]

Gets all component definitions 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 component definitions


UrlProcessor

Helper singleton for processing URLs.


static process(url: string, meta: MetaData, serverSide = false, isResource = false): string

Processes the absolute URL to become relative for the current viewer, while keeping in mind Next.js assets and Enonic XP binary content links

NOTE: There are convenience aliases to this function called getUrl() and getAsset()

Argument Description
url Absolute URL
meta Runtime data returned by fetchContent
serverSide Whether URL is going to be used on the server side (Skips adding basePath)
isResource Whether URL is a resource (Skips adding locale)

Usage:

import {UrlProcessor} from '@enonic/nextjs-adapter';

const url = UrlProcessor.process('http://www.some.site.com/url/to/content', meta, true, false);

static processSrcSet(srcset: string, meta: MetaData): string

Processes the image srcset attribute to transform each URL with process method

Argument Description
srcset Value of the srcset attribute
meta Runtime data returned by fetchContent

Usage:

import {UrlProcessor} from '@enonic/nextjs-adapter';

const url = UrlProcessor.processSrcSet('<srcset value>', meta);

static setSiteKey(key: string): void

Sets the site key value that is needed for correct absolute URL processing. It is automatically done by nextjs-adapter so you don't have to do it.

WARNING: Overriding this value may result in wrong URL processing !

Argument Description
key Site key

Usage:

import {UrlProcessor} from '@enonic/nextjs-adapter';

UrlProcessor.setSiteKey('<site key>');

static isMediaLink(ref: string, linkData: LinkData[]): boolean

Checks if link data array contains link with provided ref. Positive result means that this is a link to Enonic XP content.

NOTE: link data array is contained in response of the query generated by richTextQuery('fieldName')

Argument Description
ref Link ref
linkData Link data array

Usage:

import {UrlProcessor} from '@enonic/nextjs-adapter';

UrlProcessor.isMediaLink('<link ref>', linkData);

static isContentImage(ref: string, imageData: ImageData[]): boolean

Checks if image data array contains image with provided ref. Positive response means that this is an Enonic XP image.

NOTE: image data array is contained in response of the query generated by richTextQuery('fieldName')

Argument Description
ref Image ref
imageData Image data array

Usage:

import {UrlProcessor} from '@enonic/nextjs-adapter';

UrlProcessor.isContentImage('<image ref>', imageData);

Enonic XP locale mapping functions

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.


getLocaleMapping(context: Context): LocaleMapping

Argument Description
context Context object

Usage:

import {getLocaleMapping} from '@enonic/nextjs-adapter';

const mapping = getLocaleMapping({
    contentPath: '/current/content/path',
    locale: 'en',
});

Response type:

export interface LocaleMapping {
    default: boolean
    project: string
    site: string
    locale: string
}

getLocaleMappingByProjectId(projectId?: string, useDefault = true): LocaleMapping

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);

Response type


getLocaleMappingByLocale(locale?: string, useDefault = true): LocaleMapping

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);

Response type


getRequestLocaleInfo(context: Context): LocaleMapping

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
}

I18n localization functions

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.


static I18n.setLocale(locale: string): Promise<Dict>

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');

Response type:

interface Dict {
    [key: string]: string
}

static I18n.localize(key: string, ...args: any[]): 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');

static I18n.getLocale(): string

Returns the current locale.

Usage:

import {I18n} from '@enonic/nextjs-adapter';

const locale = I18n.getLocale();

static I18n.getDictionary(): Dict

Returns dictionary for the current locale.

Usage:

import {I18n} from '@enonic/nextjs-adapter';

const dict = I18n.getDictionary();

Response type


Utility functions

There is also a number of utility constants and functions available at @enonic/nextjs-adapter.


import {CATCH_ALL} from './constants';

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;

// Returns common part of 2 strings
const commonChars = (s1?: string, s2?: string) => string;

// Returns full content api URL with current project and branch appended
const getContentApiUrl = (context: Context) => string;

Views

They are located in @enonic/nextjs-adapter/views folder. Each view is a default export in the corresponding file.


<MainView common="common" data="data" page="page" meta="meta">

The main view of the application. It accepts the result of fetchContent method. Should be default export from your next.js route

Argument Type Description
common Record<string, any> &#124; null Result of the common query
data Record<string, any> &#124; null Result of the graphql query
page PageComponent &#124; null Page structure
meta MetaData Runtime info

Usage:

import MainView from '@enonic/nextjs-adapter/views/MainView';
import fetchContent from '@enonic/nextjs-adapter';

export async function getServerSideProps(context: Context) {
    const props = fetchContent('/content/path', context);
    return {
        props
    }
}

export default MainView;

<StaticContent condition="true" tag="div">

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
tag = 'div' String Html tag to use for static output

Usage:

import StaticContent from '@enonic/nextjs-adapter/views/StaticContent';

<StaticContent condition={true} tag="div"> ...child elements... </StaticContent>

<RichTextView className="css-class" tag="section" data={data} meta={meta}, renderMacroInEditMode="true", customReplacer={customReplacerFn}>

Tag for displaying contents of html area input types. Takes care of processing macros, URLs and images inside.

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
renderMacroInEditMode = true boolean Flag passed to macros telling if they should render themselves in edit mode
tag = 'div' String Html tag to use as a root

TIP! There is a utility function richTextQuery(fieldName) generating part of the graphql query to obtain RichTextData for html area input types.

Usage:

import RichTextView from '@enonic/nextjs-adapter/views/RichTextView';

<RichTextView data={richTextData} meta={meta} tag="section" className="rich-text-view" renderMacroInEditMode="false"></RichTextView>

<Regions page={page} meta={meta} name="main" common={common}>

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 &#124; null; Page structure
meta MetaData Runtime info
name String Render only this region. Optional
common any Result of common query. Optional

Usage:

import StaticContent from '@enonic/nextjs-adapter/views/StaticContent';

<StaticContent condition={true} tag="div"> ...child elements... </StaticContent>

Known problems

In some cases, when using in NextJS application, a build errors related to the JSX may appear, that state the following:

You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders

To prevent these errors, it is necessary to transpile the library using something like next-transpile-modules. Install it as dev dependency and call it explicitly from you NextJS config:

next.config.js

const withTM = require('next-transpile-modules')(['@enonic/nextjs-adapter']);

module.exports = withTM({});

Package Sidebar

Install

npm i @enonic/nextjs-adapter

Weekly Downloads

148

Version

3.0.0-RC5

License

Apache-2.0

Unpacked Size

404 kB

Total Files

331

Last publish

Collaborators

  • edloidas
  • sigdestad
  • cwe_at_enonic.com
  • alansemenov