Drupal Page Rendering
This guide show how you setup page rendering with content from drupal backend.
create necessary page components under pages directory
the [...slug].tsx component
create pages/[...slug].tsx file:
import {
getDrupalClientForServer,
getNodeResources,
PageProperties,
PagePropertiesRedirect
} from '@gooddev/peekjs';
import {
GetStaticPathsContext,
GetStaticPropsContext,
GetStaticPropsResult
} from 'next/types';
import ResourcePageIndex, {
getStaticProps as getStaticPropsIndex
} from './index';
export default ResourcePageIndex;
export async function getStaticPaths(context: GetStaticPathsContext) {
if (!process.env.DRUPAL_CLIENT_ID || !process.env.DRUPAL_CLIENT_SECRET)
return { paths: [], fallback: false };
const resourceTypes = await getNodeResources();
const posts = await getDrupalClientForServer().getStaticPathsFromContext(
resourceTypes ?? '',
context
);
let paths = posts
.map((r) => {
const path = typeof r === 'string' ? r : r.params.slug[0];
const locale = typeof r === 'object' ? r.locale : 'de';
if (path === '' || path === '/home') {
return null;
}
return {
params: { slug: [`${path}`] || '' },
locale
};
})
.filter((p) => p !== null);
const isLocal = process.env.ENVIRONMENT === 'local';
if (isLocal) {
paths = paths.slice(0, 15);
}
return {
paths,
fallback: 'blocking'
};
}
export async function getStaticProps(
context: GetStaticPropsContext
): Promise<GetStaticPropsResult<PageProperties | PagePropertiesRedirect>> {
return getStaticPropsIndex(context);
}
The main goal of this file is to get all possible path aliases which can be rendered on buildtime (getStaticProps)
For the getStaticProps and ResourcePageIndex we refer to pages/index.tsx
so lets look how this looks:
the index.tsx component
Be aware that this is more an example. You final pages/index.tsx looks probably different.
create pages/index.tsx file. For detail clarification read all comments of example:
import React, { ReactElement } from 'react';
import {
DrupalPageProperties,
getDrupalGlobalPageProps,
overwritePageProperties,
GridRow,
PagePropertiesRedirect
} from '@gooddev/peekjs';
import { useRouter } from 'next/router';
import { GetStaticPropsContext } from 'next/types';
import { DefaultLayout } from '../components/layouts/DefaultLayout';
import { getParams, getReusableRelationsForField } from '../utils/get-params';
import { overwritePageProperties } from '../utils/overwritePageProperties';
import { resourceEnhancer } from '../utils/resourceEnhancer';
/**
* The function which is responsible for rendering.
* @param drupalPageProperties
*/
export default function ResourcePage(props: DrupalPageProperties) {
return (
<GridRow my={100}>
{/* Change Rendering as you like */}
<h1>{props.resource.title}</h1>
All Propery Values, inclusive Resource:
<pre>{JSON.stringify(props)}</pre>
{/* Change Rendering as you like */}
</GridRow>
);
}
/**
* A Way to Specifiy, how the layout of the Page will look. See: https://nextjs.org/docs/basic-features/layouts#per-page-layouts
* @param page
*/
ResourcePage.getLayout = function getLayout(
page: ReactElement & { props: DrupalPageProperties | PagePropertiesRedirect }
) {
//this redirect pages when default NextJs-Redirect did not work (workaround).
if (page.props.redirectTo) {
if (typeof window !== 'undefined')
window.location.href = page.props.redirectTo;
return null;
}
//Change Layout as you want
return (
<DefaultLayout
{...page.props}
>
{page}
</DefaultLayout>
);
};
/**
* The function which renders on build-time (production) and on request-time (development) on server for pull all needed information into page, which you want to render (render props).
* Because of @gooddev/peekjs this section is as clean as possible, because all business logic to pull information from drupal is capsualted in helper functions.
* e.g: getDrupalGlobalPageProps, overwritePageProperties
* @param context
*/
export async function getStaticProps(context: GetStaticPropsContext) {
//call the `getDrupalGlobalPageProps` function with minimal configuration
const pageProperties = await getDrupalGlobalPageProps({
context,
//tell what menues you want from backend, please respect which menues the drupal backend actually have
menues: ['main', 'meta', 'footer']
});
//this function helps to wrap everything up for translation, and it is also possible to override specific pageProperties, like resource, to add addional information to the render props.
return await overwritePageProperties(pageProperties, null);
}
To get an overview of all helper-functions go to Documentation of PeekJS (underneath Utils): PeekJS API getDrupalGlobalPageProps
You should now see pages from drupal, when entering the correct url-alias. As a default, the /home path wil always point to the frontpage of drupal. So if you want to maintain a frontpage, create one in drupal backend with pathalias /home.
getDrupalGlobalPageProps Configuration
resourceEnhancer
The function resourceEnhancer which can optionally be passed to the main function getDrupalGlobalPageProps
is there to enhance the resource which gets pulled from Drupal. A resource is a full loaded entity like a node or a commerce product. The resource itself gets also passed as a parameter to distinguish between different resource types. Here is one example of the resourceEnhancer:
import { GetServerSidePropsContext } from 'next';
import { GetStaticPropsContext } from 'next/types';
import { DrupalClient, DrupalNode } from 'next-drupal';
import { enrichFieldContent } from './enrichFieldContent';
export const resourceEnhancer = async (
drupalServer: DrupalClient,
resource: DrupalNode | undefined,
context: GetStaticPropsContext | GetServerSidePropsContext
) => {
if (!resource) return null;
// create async task object for define all jobs which should executed in parallel
let asyncTasks: { [taskName: string]: Promise<any> } = {};
if (
typeof resource?.field_pillar_inhalt !== 'undefined' &&
resource.field_pillar_inhalt &&
resource.field_pillar_inhalt.length > 0
) {
asyncTasks = {
...asyncTasks,
enrichFieldContent_field_pillar_inhalt: enrichFieldContent(
resource.drupal_internal__nid,
resource.field_pillar_inhalt,
context,
resource
)
};
}
// execute asyncTasks in parallel for more performance
await Promise.all(
Object.keys(asyncTasks).map((taskName) => asyncTasks[taskName])
);
return resource;
};
In this example you will notice, that we check for the field field_pillar_inhalt and if it exists on the resource, we call the function enrichFieldContent which is responsible to add additional information to specific paragraphs (later more about that). enrichFieldContent is like the resourceEnhancer but for paragraphs only. In the resourceEnhancer you can also see that we put all function into an object asyncTasks, to be able to call everything in parallel to save rendering time, even when this example only have one rule to add some additional information to resource.
enrichFieldContent
Lets look into the enrichFieldContent function. Because this is a large file you will find the explanations as comments in the file:
import { getDrupalClientForServer, loadDrupalViewData } from '@gooddev/peekjs';
import { GetServerSidePropsContext } from 'next';
import { GetStaticPropsContext } from 'next/types';
import { DrupalNode } from 'next-drupal';
import { unstable_serialize } from 'swr';
import { TDrupalPillarParagraph } from '../types/TDrupalPillarParagraph';
import { getRelationForView } from './getParams';
//the entrypoint of the function
export const enrichFieldContent = async (
nid: number,
field_content: TDrupalPillarParagraph[],
context: GetServerSidePropsContext | GetStaticPropsContext,
resource: DrupalNode
) => {
//we iterate over all paragraphs and call the private function `enrichFieldContentByContentModule` in parallel to be as fast as possible
return await Promise.all(
field_content.map((contentModule) =>
enrichFieldContentByContentModule(nid, contentModule, context, resource)
)
);
};
const enrichFieldContentByContentModule = async (
nid: number,
contentModule: TDrupalPillarParagraph,
context: GetServerSidePropsContext | GetStaticPropsContext,
resource: DrupalNode
): Promise<Awaited<null | Awaited<unknown>>> => {
//from here we check the types of paragraph which need extra logic to get all information
//this is a nested paragraph type, which has paragraphs on its own, so it calls `enrichFieldContent` recursively
if (contentModule?.field_pillar_container_content) {
return await enrichFieldContent(
nid,
contentModule.field_pillar_container_content,
context,
resource
);
}
//for reusable pragraphs from library, we also have to call `enrichFieldContent` recursively
if (contentModule?.field_reusable_paragraph) {
//it is unsure if field_reusable_paragraph is an array or an single onject, this is why we need to check
const reusable_paragraphs = Array.isArray(
contentModule.field_reusable_paragraph.paragraphs
)
? contentModule.field_reusable_paragraph.paragraphs
: [contentModule.field_reusable_paragraph.paragraphs];
return await enrichFieldContent(
nid,
reusable_paragraphs,
context,
resource
);
}
//for paragraphs `pillar_teaser_auto` we will pull all needed content by calling the json-view-api
if (contentModule?.type?.indexOf('paragraph--pillar_teaser_auto') > -1) {
const view = 'teaserliste';
const display = 'default';
const fetchViewArgs = {
view,
display,
viewRelations: getRelationForView(view, display),
limit: contentModule.field_pillar_count || 3,
offset: 0,
exposedFilters: {
date: {
value: contentModule.field_hide_old ? 'now' : ''
},
type: {
value: contentModule.field_contenttypes
?.map(
(ct: {
type: string;
id: string;
resourceIdObjMeta: { drupal_internal__target_id: string };
}) => ct.resourceIdObjMeta.drupal_internal__target_id
)
.filter((v: string | null) => v)
.join(','),
isMultiple: true
},
tags: {
value: (contentModule.field_parent_tags
? resource.field_pillar_tags
: contentModule.field_pillar_tags
)
?.map(
(ct: {
resourceIdObjMeta: { drupal_internal__target_id: string };
}) => ct.resourceIdObjMeta.drupal_internal__target_id
)
.filter((v: string | null) => v)
.join(','),
isMultiple: true
}
},
contextFilters: [resource.drupal_internal__nid.toString()],
locale: context.locale ?? 'de',
defaultLocale: context.defaultLocale ?? 'de',
sort_by:
contentModule.field_pillar_sort === 'published' ? 'date' : 'title',
sort_order: contentModule.field_sort.toUpperCase()
};
contentModule.fetchViewArgs = fetchViewArgs;
const viewData = await loadDrupalViewData({
drupalServer: getDrupalClientForServer(),
...fetchViewArgs
});
if (viewData) {
const fetchViewUrl = `${process.env.NEXT_PUBLIC_FRONTEND_ORIGIN}/api/views/load`;
//the resource `fallback` property can be used to prepopulate data which get pulled on client via useSWR
resource.fallback = {
...resource.fallback,
//it is important to use the same keys here on server as later on client, so the prepopulated content can be found. You can find more information here: https://swr.vercel.app/docs/with-nextjs#complex-keys
[unstable_serialize({
url: fetchViewUrl,
args: fetchViewArgs
})]: viewData
};
if (viewData.results.length < 1) {
contentModule.hide = true;
}
}
}
//this will load all needed data for rendering a full webform
if (
contentModule?.type?.indexOf('paragraph--pillar_paragraph_formular') > -1
) {
const webform_id =
contentModule.field_pillar_form?.resourceIdObjMeta
.drupal_internal__target_id;
const url = getDrupalClientForServer().buildUrl(
`/webform_rest/${webform_id}/elements`
);
// get webform from Drupal.
const result = await getDrupalClientForServer().fetch(url.toString(), {
method: 'GET',
withAuth: true,
headers: {
'Content-Type': 'application/json'
}
});
if (result.ok) {
contentModule.webform = await result.json();
contentModule.webform_id = webform_id;
} else {
const errorResult = await result.json();
console.error(errorResult);
}
}
};
getParams
The getParams function is used to add the correct include parameters on certain resources to include all related information. For example the main image of a node has two relations: Node ==> Media ==> File. Here is a complete example of the getParams function:
import { DrupalJsonApiParams } from 'drupal-jsonapi-params';
//this are all related field grouped by contenttype or paragaph type, to have an easier readability and maintainance
export const fieldRelations = {
article: [
'field_pillar_content_image.field_media_image',
'field_pillar_teaserbild.field_media_image',
'field_pillar_tags'
],
personDetail: ['field_pillar_foto.field_media_image'],
listPage: ['field_view'],
stage: ['field_stage_image.field_media_image'],
downloads: [
'field_pillar_inhalt.field_pillar_download.field_media_document',
'field_pillar_inhalt.field_pillar_container_content.field_pillar_download.field_media_document'
],
quote: ['field_pillar_inhalt.field_pillar_foto.field_media_image'],
gallery: [
'field_pillar_inhalt.field_pillar_media.field_media_image',
'field_pillar_inhalt.field_pillar_media.thumbnail',
'field_pillar_inhalt.field_pillar_container_content.field_pillar_media.field_media_image',
'field_pillar_inhalt.field_pillar_container_content.field_pillar_media.thumbnail'
],
stats: [
'field_pillar_inhalt.field_pillar_foto.field_media_image',
'field_pillar_inhalt.field_pillar_container_content.field_pillar_foto.field_media_image'
],
accordion: [
'field_pillar_inhalt.field_pillar_container_content.field_pillar_container_content.field_pillar_person.field_pillar_foto.field_media_image',
'field_pillar_inhalt.field_pillar_container_content.field_pillar_container_content.field_pillar_container_content.field_pillar_person.field_pillar_foto.field_media_image',
'field_pillar_inhalt.field_pillar_container_content.field_pillar_container_content.field_pillar_container_content.field_pillar_foto.field_media_image'
],
person: [
'field_pillar_inhalt.field_pillar_person.field_pillar_foto.field_media_image'
],
container: [
'field_pillar_inhalt.field_pillar_foto.field_media_image',
'field_pillar_inhalt.field_pillar_container_content.field_pillar_foto.field_media_image',
'field_pillar_inhalt.field_pillar_container_content.field_pillar_person.field_pillar_foto.field_media_image',
'field_pillar_inhalt.field_pillar_container_content.field_pillar_listview'
],
manualTeaserlist: [
'field_pillar_inhalt.field_content_reference.field_pillar_teaserbild.field_media_image',
'field_pillar_inhalt.field_content_reference.field_pillar_content_image.field_media_image',
'field_pillar_inhalt.field_pillar_container_content.field_content_reference.field_pillar_teaserbild.field_media_image',
'field_pillar_inhalt.field_pillar_container_content.field_content_reference.field_pillar_content_image.field_media_image'
],
manualContentList: [
'field_pillar_inhalt.field_custom_teaser_element.field_custom_teaser_image.field_media_image',
'field_pillar_inhalt.field_pillar_container_content.field_custom_teaser_element.field_custom_teaser_image.field_media_image'
],
teaserlist: [
'field_pillar_inhalt.field_pillar_listview',
'field_pillar_inhalt.field_pillar_tags',
'field_pillar_inhalt.field_pillar_container_content.field_pillar_tags'
]
};
//the main function which check the resource type and adds need raltions from above
export function getParams(
name: string,
mode: string | null = null
): DrupalJsonApiParams {
const params = new DrupalJsonApiParams();
name = mode ? `${name}--${mode}` : name;
if (name === 'node--pillar_homepage') {
return params.addInclude([
...fieldRelations.stage,
...fieldRelations.manualContentList,
...fieldRelations.downloads,
...fieldRelations.manualTeaserlist,
...fieldRelations.stats,
...fieldRelations.container,
...fieldRelations.person,
...fieldRelations.gallery,
...fieldRelations.teaserlist,
...fieldRelations.accordion,
...getReusableRelationsForField(
'field_pillar_inhalt.field_reusable_paragraph.paragraphs'
)
]);
}else if (name === 'node--pillar_page') {
return params.addInclude([
...fieldRelations.article,
...fieldRelations.container,
...fieldRelations.person,
...fieldRelations.downloads,
...fieldRelations.gallery,
...fieldRelations.manualTeaserlist,
...fieldRelations.teaserlist,
...fieldRelations.accordion,
...fieldRelations.quote,
...fieldRelations.manualContentList,
...getReusableRelationsForField(
'field_pillar_inhalt.field_reusable_paragraph.paragraphs'
)
]);
}else if (name === 'node--pillar_person') {
return params.addInclude([
...fieldRelations.personDetail,
...fieldRelations.accordion,
...fieldRelations.downloads,
...fieldRelations.person,
...fieldRelations.teaserlist,
...getReusableRelationsForField(
'field_pillar_inhalt.field_reusable_paragraph.paragraphs'
)
]);
}else if (name === 'node--list_page') {
return params.addInclude([...fieldRelations.listPage]);
}
return params;
}
//this function is usefull for reusable paragraphs from library. It used the same definitions for all relations starting with `field_pillar_inhalt`
export const getReusableRelationsForField = (replacement: string): string[] =>
Object.values(fieldRelations).reduce<string[]>(
(prev, current) => [
...prev,
...current
.filter((field) => field.indexOf('field_pillar_inhalt.') === 0)
.map((field) => field.replace('field_pillar_inhalt', replacement))
],
[]
);
//this helper function is used for add the correct relations for a view
export const getRelationForView = (
view: string,
display = 'default'
): string[] => {
switch (true) {
case view === 'global_search':
case view === 'teaserliste':
return fieldRelations.article;
case view === 'news' ||
(view === 'frontend_content_lists' &&
display !== 'mitglieder' &&
display !== 'glossar'):
return fieldRelations.article;
}
return [];
};
Add Blocks (block)
Add Blocks to regions.
Documentation in progress...