Page templates

Contents

Thus far, page rendering has been based on hard-coding, or editorially composing one page at a time. In this chapter you’ll learn how to make page templates - and reuse them across multiple content items.

Task: Movie details part

To complete this section, we’ll need one final part for listing movie details. The content type and content for movies already exists in Enonic, so lets get to work.

  1. Add the movie-details part to Enonic - same procedure as before…​

    src/main/resources/site/parts/movie-details/movie-details.xml
    <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
    <part xmlns="urn:enonic:xp:model:1.0">
        <display-name>Movie Details</display-name>
        <description>Show details of a movie</description>
        <form/>
    </part>
    src/main/resources/site/parts/movie-details/movie-details.js
    var proxy = require('/lib/nextjs/proxy');
    
    exports.get = proxy.get;
  2. Redeploy the Enonic app.

  3. Register the Movie query and view in Next.

    src/components/parts/MovieDetails.tsx
    import React from 'react'
    import {APP_NAME_UNDERSCORED} from '../../_enonicAdapter/utils'
    import {PartProps} from '../../_enonicAdapter/views/BasePart';
    import {MetaData} from "../../_enonicAdapter/guillotine/getMetaData";
    import {getUrl} from "../../_enonicAdapter/UrlProcessor";
    
    
    export const getMovie = `
    query($path:ID!){
      guillotine {
        get(key:$path) {
          type
          displayName
          parent {
            _path(type: siteRelative)
          }
          ... on ${APP_NAME_UNDERSCORED}_Movie {
            data {
              subtitle
              abstract
              trailer
              release
              photos {
                ... on media_Image {                                             
                  imageUrl: imageUrl(type: absolute, scale: "width(500)")       
                  attachments {                                                 
                    name
                  }
                }
              }
              cast {
                character
                actor {
                  ... on ${APP_NAME_UNDERSCORED}_Person {
                    _path(type: siteRelative)
                    displayName
                    data {
                      photos {
                        ... on media_Image {                                             
                          imageUrl: imageUrl(type: absolute, scale: "block(200,200)")       
                          attachments {                                                 
                            name
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }`;
    
    
    // Root component
    const MovieView = (props: PartProps) => {
        const data = props.data?.get.data as MovieInfoProps;
        const meta = props.meta;
        const {displayName, parent = {}} = props.data.get;
        return (
            <>
                <div>
                    <h2>{displayName}</h2>
                    {data && <MovieInfo {...data}/>}
                    {data?.cast && <Cast cast={data.cast} meta={meta}/>}
                </div>
                <p>
                    <a href={getUrl(parent._path, meta)}>Back to Movies</a>
                </p>
            </>
        );
    };
    
    export default MovieView;
    
    interface MovieInfoProps {
        release: string;
        subtitle: string;
        abstract: string;
        cast: CastMemberProps[],
        photos: {
            imageUrl: string;
        }[];
    }
    
    // Main movie info: release year, poster image and abstract text.
    const MovieInfo = (props: MovieInfoProps) => {
        const posterPhoto = (props.photos || [])[0] || {};
        return (
            <>
                {props.release && (
                    <p>({new Date(props.release).getFullYear()})</p>
                )}
                {posterPhoto.imageUrl && (
                    <img src={posterPhoto.imageUrl}
                         title={props.subtitle}
                         alt={props.subtitle}
                    />
                )}
                <p>{props.abstract}</p>
            </>
        )
    }
    
    interface CastProps {
        cast: CastMemberProps[];
        meta: MetaData;
    }
    
    interface CastMemberProps {
        character: string;
        actor: {
            _path: string;
            displayName: string;
            data: {
                photos: {
                    imageUrl: string;
                    attachments: {
                        name: string
                    }[]
                }[]
            }
        }
    }
    
    // List persons starring in the movie.
    const Cast = (props: CastProps) => (
        <div>
            <h4>Cast</h4>
            <ul style={{listStyle: "none", display: "flex", flexFlow: "row wrap"}}>
                {props.cast.map(
                    (person: CastMemberProps, i: number) => person && (
                        <CastMember key={i} {...person} meta={props.meta}/>
                    )
                )}
            </ul>
        </div>
    );
    
    
    const CastMember = (props: CastMemberProps & { meta: MetaData }) => {
        const {character, actor, meta} = props;
        const {displayName, _path, data} = actor;
        const personPhoto = (data.photos || [])[0] || {};
    
        return (
            <li style={{marginRight: "15px"}}>
                {
                    personPhoto.imageUrl &&
                    <img src={personPhoto.imageUrl}
                         title={`${displayName} as ${character}`}
                         alt={`${displayName} as ${character}`}/>
                }
                <div>
                    <p>{character}</p>
                    <p><a href={getUrl(_path, meta)}>
                        {displayName}
                    </a></p>
                </div>
            </li>
        );
    }

    Update the component mappings:

    src/components/_mappings.ts
    import MovieDetails, {getMovie} from './parts/MovieDetails';
    
    ...
    
    ComponentRegistry.addPart(`${APP_NAME}:movie-details`, {
        query: getMovie,
        view: MovieDetails
    });

Task: Create template

With the new component registered, let’s put it to use.

  1. In Content Studio, select and edit your favorite movie (or choose "No time to die" if you have no faviorites).

  2. Like you did for the other pages, activate the page editor, select the "main" page component and add the "Movie details" part to the page.

    movie details

    We now have a single movie nicely presented, but it would be cumbersome to manually configure each movie like this - page templates to the rescue.

  3. In the page editor, select the page component (for instance by clicking on the header).

    movie page

  4. Click Save as template from the right hand panel.

    This will create a new page template content item, and open it in a new tab for editing.

  5. Give your template a better name, such as "Movie details" and save the changes.

    movie template

  6. Try visiting other movie items to verify they now automatically render.

With templates sorted out, let’s dive into production mode and page revalidation.


Contents