Tuesday, 28 January 2025

Write deployer extension for Tridion Sites 10

 To Write deployer extension for Tridion Sites with following steps

  1. Create a custom step class which implements "ExecutableStep" (com.sdl.delivery.deployer.api.processing.pipeline.ExecutableStep) interface
    • interface is available on udp-deployer-api-x.x.x-xxxx.jar which can be found on maven repo or in the deployer service installation directory.
  2. Override methods in CustomStep class
    • configure
      @Override
      public void configure(Configuration configuration) throws ConfigurationException {
      //this is section where we initialize step
      }
    • process
      @Override
      public ExecutableStepResult process(ProcessingContext processingContext, StepDataProvider stepDataProvider) throws ProcessingException {
      LOG.debug("Starting ExampleExtension");
      var location = getPackageUnzipLocation(processingContext, stepDataProvider);
      LOG.debug("ExampleExtension getPackageUnzipLocation location {}", location);
      TransportPackage transportPackage = new TransportPackage(location, stepDataProvider.getBinaryStorage());
      LOG.debug("ExampleExtension transportPackage {}", transportPackage);
      final String action = transportPackage.getAction();
      LOG.debug("PageStateNotifier action {}", action);
      final String transactionId = transportPackage.getTransactionId().toString();
      LOG.debug("Process Action {} for Transaction {}", action, transactionId);
      switch (action) {
      case DEPLOY_ACTION:
      LOG.info("Publish action triggerred");
      break;
      case UNDEPLOY_ACTION:
      LOG.info("UnPublish action triggerred");
      break;
      default:
      LOG.error("Invalid action {}", action);
      throw new ProcessingException("Invalid transport package action " + action);
      }
      return null;
      }
  3. Write a spring configuration
    • configuration class should be on package "com.sdl.delivery.spring.configuration"
    • Let's name it ExampleConfiguration
      package com.sdl.delivery.spring.configuration;

      import org.springframework.context.annotation.ComponentScan;
      import org.springframework.context.annotation.Configuration;

      @Configuration
      @ComponentScan(basePackages = {"org.rws.example"})
      public class ExampleConfiguration {
      }
    • above class allows your deployer extension to be developed on specific package for example in above ExampleConfiguration it scans package "org.rws.example" then you can implement your CustomStep on step 1 inside package "org.rws.example".
  4. Please also include following minimum java libraries for development of Tridion Sites deployer extension
    • udp-common-config-api-x.x.x-xxxx.jar
    • udp-common-config-x.x.x-xxxx.jar
    • udp-common-util-x.x.x-xxxx.jar
    • udp-core-x.x.x-xxxx.jar
    • udp-data-legacy-transport-x.x.x-xxxx.jar
    • udp-deployer-api-x.x.x-xxxx.jar
    • udp-deployer-web-extension-x.x.x-xxxx.jar
  5. x.x.x-xxxx denotes version of api available on installation directory of deployer service.
  6. After creating CustomStep, based on your project setup (i.e. maven or gradle) configure build steps which could generate a add-on package file in zip.
  7. zip file contains generated jar file and dependent jars those are not available on deployer service libraries, and manifest file requires for Addon service to specify type of Add-on.
  8. you can refer maven project which is having build step to generate Addon-package https://github.com/neeteshnarvaria/deployer-extension/blob/master/pom.xml
  9. example maven project generates example-deployer-extension.zip
  10. after all the steps we need to configure/update deployer-conf.xml
    • open in notepad and add custom pipeline after following pipelines, specifically after highlighted one in screenshot below
    • custom pipeline
      <Pipeline Id="Tridion-Example-Step" Action="Deploy,Undeploy" Verb="Process">
      <Steps>
      <Step Id="ExampleExtension" />
      </Steps>
      </Pipeline>
  11. after configuring/update deployer-conf.xml save and close the file.
  12. add the generated package on add-on using upload
  13. select add-on
  14. after upload on add-on it will show as "Pending State"
  15. We need to restart deployer service to activate the deployer extension add-on.
  16. Deployer status should show "Success".
  17. it is ready to use and we can check functionality by adding some logs.

Refer following git repo which contains sample deployer extension having custom step: https://github.com/neeteshnarvaria/deployer-extension

Tuesday, 2 May 2023

C#: First Blog Console App

To run first C# program you need to have dotnet SDK installed on your local machine which you are going to use for C# programming.

Note: this blog represents sample which is prepared using Visual Studio Code IDE.

Go to location on you machine where you want to write program.

To print let's create first console app.

1. Open Visual Studio code and open folder where we are going to run first console app and click on New Terminal.


2. Run the following command to create console app.
    dotnet new console --framework net6.0 --use-program-main

3. Now we have boilerplate console app, which looks like following screenshot.
  

 


4. To print Hello world on the same terminal execute folloing command.
> dotnet run
    which should look like following



Hope this helps to create first console app using dotnet.




Tuesday, 31 August 2021

Java example to fetch tridion content service PCA graphql response.

- Implementation which is available is built on spring-boot framework, I am gonna put only relative information on graphql and java client.


I have prepared one class which is having few example of tridion pca graphql queries using java,

QueryService.java
@Service
public class QueryService {

final PCAClientProvider pcaClientProvider;

public QueryService(PCAClientProvider pcaClientProvider) {
this.pcaClientProvider = pcaClientProvider;
}

protected ApiClient getPcaClient() {
ApiClient pcaClient = this.pcaClientProvider.getClient();
pcaClient.setDefaultModelType(DataModelType.R2);
pcaClient.setDefaultContentType(ContentType.MODEL);
pcaClient.setModelServiceLinkRenderingType(ModelServiceLinkRendering.RELATIVE);
pcaClient.setTcdlLinkRenderingType(TcdlLinkRendering.RELATIVE);
return pcaClient;
}

protected CustomApiClient getCustomPcaClient() {
CustomApiClient pcaClient = this.pcaClientProvider.getCustomClient();
pcaClient.setDefaultModelType(DataModelType.R2);
pcaClient.setDefaultContentType(ContentType.MODEL);
pcaClient.setModelServiceLinkRenderingType(ModelServiceLinkRendering.RELATIVE);
pcaClient.setTcdlLinkRenderingType(TcdlLinkRendering.RELATIVE);
return pcaClient;
}

public Publication getPublicationTitle(int pubId) {
return getPcaClient().getPublication(ContentNamespace.Sites, pubId, "", null);
}

public Page getPage(int publicationId, int pageId) {
return getPcaClient().getPage(ContentNamespace.Sites, publicationId, pageId, "", ContentIncludeMode.INCLUDE_DATA, null);
}

public PublicationConnection getAllPublications() {
Pagination pagination = new Pagination();
pagination.first = 0;
pagination.after = "";
return getPcaClient().getPublications(ContentNamespace.Sites, pagination, null, "", null);
}

public ComponentPresentation getComponentPresentations(int publicationId, int componentId, int templateId) {
return getPcaClient().getComponentPresentation(ContentNamespace.Sites, publicationId, componentId, templateId, "", ContentIncludeMode.INCLUDE_DATA, null);
}

public BinaryComponent getBinaryComponent(int publicationId, int binaryId) {
return getPcaClient().getBinaryComponent(ContentNamespace.Sites, publicationId, binaryId, "", null);
}

public ComponentPresentationConnection getComponentPresentations(int publicationId) {
Pagination pagination = new Pagination();
pagination.first = 0;
pagination.after = "";
InputComponentPresentationFilter inputComponentPresentationFilter = new InputComponentPresentationFilter();
InputSortParam sortParam = new InputSortParam();
sortParam.setOrder(SortOrderType.Descending);
return getPcaClient().getComponentPresentations(ContentNamespace.Sites, publicationId, inputComponentPresentationFilter,
sortParam, pagination, "", ContentIncludeMode.INCLUDE_DATA, null);
}

public String resolveComponentLink(int publicationId, int targetComponentId, int sourcePageId, int excludeComponentTemplateId, boolean renderRelativeLink) {
return getPcaClient().resolveComponentLink(ContentNamespace.Sites, publicationId, targetComponentId, sourcePageId, excludeComponentTemplateId, renderRelativeLink);
}

public JsonNode getCategories(int publicationId) {
return getCustomPcaClient().getCategories(ContentNamespace.Sites, publicationId);
}
}

for class PCAClientProvider we can take a reference from dxa github repo
https://github.com/RWS/dxa-web-application-java/blob/6e475a85a52d7b8190572662674b43c0d3056fba/dxa-framework/dxa-tridion-common/src/main/java/com/sdl/dxa/tridion/pcaclient/DefaultApiClientProvider.java
we need to have following dependency in the project in order to get PCAClient references.
<dependency>
    <groupId>com.sdl.web.pca</groupId>
    <artifactId>pca-client</artifactId>
    <version>2.2.16</version>
</dependency>


to understand QueryService we can also refer following junit test class
@SpringBootTest
public class QueryServiceTest {

@Autowired
QueryService queryService;

@Test
public void fetchAllPublication() {
PublicationConnection allPublications = queryService.getAllPublications();
assertThat(allPublications, is(notNullValue()));
System.out.printf("total number of web publications are: %d%n", allPublications.getEdges().size());
}

@Test
public void fetchAllCategories() {
int publicationId = 25;
JsonNode categories = queryService.getCategories(publicationId);
assertThat(categories, is(notNullValue()));
System.out.printf("published categories in publication %d%n are: %s%n", publicationId, categories.toPrettyString());
}

@Test
public void fetchBinaryComponent() {
BinaryComponent binaryComponent = queryService.getBinaryComponent(25, 1580);
assertThat(binaryComponent, is(notNullValue()));
System.out.printf("Binary Component id is %s: ", binaryComponent.getId());
}
}

we can also write our own ApiClient in order to get response based on custom query or criteria
for example
public class CustomApiClient extends DefaultApiClient {

private static final Logger LOG = LoggerFactory.getLogger(CustomApiClient.class);
private static final ObjectMapper MAPPER = new ObjectMapper();
private final GraphQLClient graphQLClient;
private final int requestTimeout;

public CustomApiClient(GraphQLClient graphQLClient, int requestTimeout) {
super(graphQLClient, requestTimeout);
this.graphQLClient = graphQLClient;
this.requestTimeout = (int) TimeUnit.MILLISECONDS.toMillis(requestTimeout);
}

public JsonNode getCategories(ContentNamespace ns, int publicationId) {
GraphQLRequest graphQLRequest = (new PCARequestBuilder()).withQuery("AllCategories").withNamespace(ns).withPublicationId(publicationId).withTimeout(this.requestTimeout).build();
Class<JsonNode> clazz = JsonNode.class;
String path = "/data/categories";
return getResultForRequest(graphQLRequest, clazz, path);
}

private <T> T getResultForRequest(GraphQLRequest request, Class<T> clazz, String path) throws ApiClientException {
JsonNode result = getJsonResult(request, path);
try {
return MAPPER.treeToValue(result, clazz);
} catch (JsonProcessingException e) {
throw new ApiClientException("Unable map result to " + clazz.getName() + ": " + result.toString(), e);
}
}

private JsonNode getJsonResult(GraphQLRequest request, String path) throws ApiClientException {
int attempt = 3;
UnauthorizedException[] exception = new UnauthorizedException[1];
while (attempt > 0) {
try {
attempt--;
return getJsonResultInternal(request, path);
} catch (UnauthorizedException ex) {
if (exception[0] == null) exception[0] = ex;
LOG.error("Could not perform query on " + path);
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
throw new ApiClientException("Could not perform query " + request + " after 3 attempts", exception[0]);
}

private JsonNode getJsonResultInternal(GraphQLRequest request, String path) throws ApiClientException, UnauthorizedException {
try {
String resultString = this.graphQLClient.execute(request);
JsonNode resultJson = MAPPER.readTree(resultString);
return resultJson.at(path);
} catch (GraphQLClientException e) {
throw new ApiClientException("Unable to execute query: " + request, e);
} catch (IOException e) {
throw new ApiClientException("Unable to deserialize result for query " + request, e);
}
}
}

Thursday, 22 October 2020

SDL Tridion Sites GraphQL Page query

 Graphql url: http://localhost:8081/cd/api (domain can be changed)


{
  page(namespaceId:1,publicationId:8,pageId:689){
           rawContent{
        data
        content
      }
  }
}

namespaceId (1 is for Tridion Sites and 2 is for Tridion docs)


it will give json data under data keyword and content will be string.

Tuesday, 28 July 2020

SDL Tridion Docs – HTML output customization - create DITA-OT plugin

Objective

Need to customize HTML output when we publish a publication from Tridion Docs.

How to Do it

We need to create DITA-OT plugin which extends html5 plugin

 

Let’s do it

 

What we need is Tridion Docs installed on server😉

And we should be able to publish content on HTML5


Status should be ‘Release Candidate’ or ‘Draft’ after publish a publication


And you will see html output once click on download as below

 

 

 

 

For Map


For Topic


 

 

Let’s do some stuff with this HTML output

-        What we are going to do

-        We’ll apply some CSS on output and also add SDL logo as well.

 

We are going to need custom output format where we’ll publish this publication.

-        Let’s name it like ‘Exercise-html5output

What we are going to learn

  • -        Create output format with style processor on Tridion Docs
  • -        Customize DITA-OT html output

 

Here are steps to create new output format

  • -        Go to Tridion docs installation folder
  • -        %TRIDION_DOCS%\Web\Author\ASP\ClientConfig
  • -        Take backup of MetadataConfig.xml
  • -        Open MetadataConfig.xml and Search for DitaOTTransactionTypeField

o   Add following xml tag on enumlist tag to add transtype on Tridion Docs

         <item>

                <value>exercise-html5</value>

                <label resourceref="FDITAOTTRANSTYPE.ValueList.exercise-html5.Text">exercise-html5</label>

         </item>

  • -        Search for StyleProcessorField on Metadaconfig.xml

o   Add following xml tag to add style processor on Tridion Docs

        <item>

                <value>DITA-OT\Exercise</value>

                <label resourceref="FSTYLEPROCESSOR.ValueList.Exercise.Text">Exercise</label>

         </item>

  • -        To Create Style-processor on Tridion Docs folder

o   Go to %TRIDION_DOCS%\App\Utilities\DITA-OT

o   Copy Infoshare and paste it and rename new folder to Exercise

-        Now Open Tridion Docs webclient

o   Go to Settings -> Output Format

o   Click on new button

o   Add following information on output format window and click ok

o   Now Update publish plug-in setting as this transformtype is not configured on Publish setting
Open Publish Plug-in setting add “exercise-html5” transformation type and save it


Restart service “Trisoft InfoShare BackgroundTask One

  • -        This allows publication to publish on exercise-html5 output format.

 

Let’s Customize HTML format now

-        As we created Style-processor on one previous step, we will create exercise-html5 plugin

-        What is StyleProcessor in Tridion Docs

o   StyleProcessor is DITA-OT for Tridion docs, which process input DITA and render different output based on given transformation type to processor

o   In our case we’ll use transformation type exercise-html5.

-        To Create plugin Go to directory %TRIDION_DOCS%\App\Utilities\DITA-OT\Exercise\plugins
        

-        Right click create New folder give some name to folder which will denote your custom plugin.

o   In my case I’ll give name com.sdl.exercise-html5

 

-        Go to com.sdl.exercise-html5 directory do following steps to develop plugin

o   Create plugin.xml as DITA-OT tool will look for plugin.xml and perform operations based on that

<?xml version="1.0" encoding="UTF-8"?>

<?xml-model href="dita-ot/plugin.rnc" type="application/relax-ng-compact-syntax"?>

 

<plugin id="com.sdl.exercise-html5">

  <feature extension="package.version" value="1.0"/>

  <require plugin="org.dita.html5"/>

 

  <transtype name="exercise-html5" extends="html5" desc="HTML5 Customized" />

</plugin>


o   Require tag on above xml allows plugin to extend “org.dita.html5” plugin which comes with dita-ot.

o   Let’s add some feature to plugin

§  Add <feature extension="dita.conductor.target.relative" file="integrator.xml"/> after transtype tag and we’ll create integrator.xml and write some xml instructions on it.

o   Please visit https://www.dita-ot.org/3.5/topics/html-customization.html to understand xml tags mentioned above.

o   Now we need to provide

§  exercise-html5.xsl

§  map2html5-cover.xsl

§  exercise.css

§  images directory

o   this would look like


o   Let’s look at each file

§  exercise.css

§  exercise-html5.xsl

<?xml version="1.0" encoding="UTF-8"?><!-- This file is part of the DITA Open Toolkit project.

     See the accompanying license.txt file for applicable licenses. --><xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:dita-ot="http://dita-ot.sourceforge.net/ns/201007/dita-ot" version="2.0" exclude-result-prefixes="xs dita-ot">


  <xsl:import href="plugin:org.dita.html5:xsl/dita2html5.xsl"/>

  

  <xsl:template match="*" mode="chapterBody">

    <body>

      <xsl:apply-templates select="." mode="addAttributesToHtmlBodyElement"/>

      <xsl:call-template name="setaname"/>  <!-- For HTML4 compatibility, if needed -->

      <xsl:value-of select="$newline"/>

      <xsl:apply-templates select="." mode="addHeaderToHtmlBodyElement"/>


      <!-- Include a user's XSL call here to generate a toc based on what's a child of topic -->

      <xsl:call-template name="gen-user-sidetoc"/>


  <nav><div class="milogo-wrapper"><a href="index.html"><img src="images/SDL_Logo.png"/></a></div></nav>

      <xsl:apply-templates select="." mode="addContentToHtmlBodyElement"/>

      <xsl:apply-templates select="." mode="addFooterToHtmlBodyElement"/>

    </body>

    <xsl:value-of select="$newline"/>

  </xsl:template>

  

<xsl:template match="*[contains(@class, ' topic/image ')]/@height">

  <xsl:variable name="height-in-pixel">

    <xsl:call-template name="length-to-pixels">

      <xsl:with-param name="dimen" select="."/>

    </xsl:call-template>

  </xsl:variable>

  <xsl:if test="not($height-in-pixel = '100%')">

    <xsl:attribute name="height">

      <!--xsl:choose>

        <xsl:when test="../@scale and string(number(../@scale))!='NaN'">          

          <xsl:value-of select="number($height-in-pixel) * number(../@scale)"/>

        </xsl:when>

        <xsl:otherwise-->

          <xsl:value-of select="number($height-in-pixel)*2"/>

        <!--/xsl:otherwise>

      </xsl:choose-->

    </xsl:attribute>

  </xsl:if>  

</xsl:template>


<xsl:template match="*[contains(@class, ' topic/image ')]/@width">

  <xsl:variable name="width-in-pixel">

    <xsl:call-template name="length-to-pixels">

      <xsl:with-param name="dimen" select="."/>

    </xsl:call-template>

  </xsl:variable>

  <xsl:if test="not($width-in-pixel = '100%')">

    <xsl:attribute name="width">

      <!--xsl:choose>

        <xsl:when test="../@scale and string(number(../@scale))!='NaN'">          

          <xsl:value-of select="number($width-in-pixel) * number(../@scale)"/>

        </xsl:when>

        <xsl:otherwise-->

          <xsl:value-of select="number($width-in-pixel)*2"/>

        <!--/xsl:otherwise>

      </xsl:choose-->

    </xsl:attribute>

  </xsl:if>  

</xsl:template>

    

  <xsl:output xmlns:dita="http://dita-ot.sourceforge.net" method="html" encoding="UTF-8" indent="no" doctype-system="about:legacy-compat" omit-xml-declaration="yes"/>


</xsl:stylesheet>


§  map2html5-cover.xsl

<?xml version="1.0" encoding="UTF-8"?>

<!-- This file is part of the DITA Open Toolkit project.

     See the accompanying license.txt file for applicable licenses. -->

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">


  <xsl:import href="plugin:org.dita.html5:xsl/dita2html5.xsl"/>

  <xsl:import href="plugin:org.dita.xhtml:xsl/map2html-coverImpl.xsl"/>

  <xsl:import href="plugin:com.sdl.exercise-html5:xsl/exercise-nav.xsl"/>


  


  <xsl:output xmlns:dita="http://dita-ot.sourceforge.net" method="html" encoding="UTF-8" doctype-system="about:legacy-compat" omit-xml-declaration="yes"/>


</xsl:stylesheet>


§  exercise-nav.xsl

<?xml version="1.0" encoding="UTF-8"?><!-- This file is part of the DITA Open Toolkit project.

     See the accompanying license.txt file for applicable licenses. --><xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:dita-ot="http://dita-ot.sourceforge.net/ns/201007/dita-ot" version="2.0" exclude-result-prefixes="xs dita-ot">


  

  <xsl:import href="plugin:org.dita.xhtml:xsl/map2html-coverImpl.xsl"/>

  

  <xsl:template match="*[contains(@class, ' map/map ')]" mode="toc" priority="10">

    <xsl:param name="pathFromMaplist"/>

    <xsl:if test="descendant::*[contains(@class, ' map/topicref ')]                                [not(@toc = 'no')]                                [not(@processing-role = 'resource-only')]">

      <nav>

  <div class="milogo-wrapper"><a href="index.html"><img src="images/SDL_Logo.png"/></a>test</div>

        <ul>

          <xsl:call-template name="commonattributes"/>

          <xsl:apply-templates select="*[contains(@class, ' map/topicref ')]" mode="toc">

            <xsl:with-param name="pathFromMaplist" select="$pathFromMaplist"/>

          </xsl:apply-templates>

        </ul>

      </nav>

    </xsl:if>

  </xsl:template>

  

  

  <xsl:template match="*[contains(@class, ' map/topicref ')]                         [not(@toc = 'no')]                         [not(@processing-role = 'resource-only')]" mode="toc">

    <xsl:param name="pathFromMaplist"/>

    <xsl:variable name="title">

      <xsl:apply-templates select="." mode="get-navtitle"/>

    </xsl:variable>

    <xsl:choose>

      <xsl:when test="normalize-space($title)">

        <li>

          <xsl:call-template name="commonattributes"/>

          <xsl:choose>

            <!-- If there is a reference to a DITA or HTML file, and it is not external: -->

            <xsl:when test="normalize-space(@href)">

              <a>

                <xsl:attribute name="href">

                  <xsl:choose>

                    <xsl:when test="@copy-to and not(contains(@chunk, 'to-content')) and                                      (not(@format) or @format = 'dita' or @format = 'ditamap') ">

                      <xsl:if test="not(@scope = 'external')">

                        <xsl:value-of select="$pathFromMaplist"/>

                      </xsl:if>

                      <xsl:call-template name="replace-extension">

                        <xsl:with-param name="filename" select="@copy-to"/>

                        <xsl:with-param name="extension" select="$OUTEXT"/>

                      </xsl:call-template>

                      <xsl:if test="not(contains(@copy-to, '#')) and contains(@href, '#')">

                        <xsl:value-of select="concat('#', substring-after(@href, '#'))"/>

                      </xsl:if>

                    </xsl:when>

                    <xsl:when test="not(@scope = 'external') and (not(@format) or @format = 'dita' or @format = 'ditamap')">

                      <xsl:if test="not(@scope = 'external')">

                        <xsl:value-of select="$pathFromMaplist"/>

                      </xsl:if>

                      <xsl:call-template name="replace-extension">

                        <xsl:with-param name="filename" select="@href"/>

                        <xsl:with-param name="extension" select="$OUTEXT"/>

                      </xsl:call-template>

                    </xsl:when>

                    <xsl:otherwise><!-- If non-DITA, keep the href as-is -->

                      <xsl:if test="not(@scope = 'external')">

                        <xsl:value-of select="$pathFromMaplist"/>

                      </xsl:if>

                      <xsl:value-of select="@href"/>

                    </xsl:otherwise>

                  </xsl:choose>

                </xsl:attribute>

                <xsl:if test="@scope = 'external' or not(not(@format) or @format = 'dita' or @format = 'ditamap')">

                  <xsl:attribute name="target">_blank</xsl:attribute>

                </xsl:if>

                <xsl:value-of select="$title"/>

              </a>

            </xsl:when>

            <xsl:otherwise>

<xsl:choose>

<xsl:when test="$title and $title != ''">

   <h2><xsl:value-of select="$title"/></h2>

</xsl:when>

<xsl:otherwise>

  <xsl:value-of select="$title"/>

</xsl:otherwise>

</xsl:choose>

            </xsl:otherwise>

          </xsl:choose>

          <!-- If there are any children that should be in the TOC, process them -->

          <xsl:if test="descendant::*[contains(@class, ' map/topicref ')]                                      [not(@toc = 'no')]                                      [not(@processing-role = 'resource-only')]">

            <ul>

              <xsl:apply-templates select="*[contains(@class, ' map/topicref ')]" mode="toc">

                <xsl:with-param name="pathFromMaplist" select="$pathFromMaplist"/>

              </xsl:apply-templates>

            </ul>

          </xsl:if>

        </li>

      </xsl:when>

      <xsl:otherwise><!-- if it is an empty topicref -->

        <xsl:apply-templates select="*[contains(@class, ' map/topicref ')]" mode="toc">

          <xsl:with-param name="pathFromMaplist" select="$pathFromMaplist"/>

        </xsl:apply-templates>

      </xsl:otherwise>

    </xsl:choose>

  </xsl:template>

  

  <xsl:output xmlns:dita="http://dita-ot.sourceforge.net" method="html" encoding="UTF-8" indent="no" doctype-system="about:legacy-compat" omit-xml-declaration="yes"/>


</xsl:stylesheet>

§  this will customize your html output.

o   Let’s create new output format for publication and select exercise-html5 from dropdown

o   Click next select language and click create.

o   Now select output format and click publish

o   Index.html would look like

o   And topic page would look like