Showing results for 
Search instead for 
Did you mean: 
Sign up Log in

It's not the same without you

Join the community to find out what other Atlassian users are discussing, debating and creating.

Atlassian Community Hero Image Collage

Customize pdf export using Script Runner for Confluence Server



I have made some customization of pdf export of a Confluence page. Sharing my approach here for others to try/benefit.

My Setup:

  • Confluence Server - 7.3.2
  • Script Runner for Confluence - 6.2.0-p5
  • Table Filter and Charts for Confluence - 5.3.25
  • Confluence has HTML macro enabled

My Approach:

  • Create Custom Menu to do the job
  • Create a temporary page which can be trashed once export job is done
  • Get the page content as string, customize it (in my case remove jira issues hyperlinks)
  • Edit the temporary page and update it with customized content, redirect the page;
  • With delay from javascript redirect it to pdf export link again

My Configurations

  • In Confluence Script Runner - Fragment -Create Script Fragments - Custom WebItem
    • What Section this should go in - system.content.action/secondary
    • Key: external-pdf
    • Menu Text - Custom
    • Weight - 1
    • Condition (such that this custom menu is displayed only in some pages)
      • if( == "SOME_SPACE_KEY" &&"SOME UNIQ TITLE STRING ") && !("NEGATE TITLE"))) {
        return true
        } else {
        return false
    • Do What - Navigate to Link
    • Link - https://HOST/wiki/rest/scriptrunner/latest/custom/exportPage_toPDF?spaceKey=<SOME SPACE KEY>&parentPageTitle=${page.title}
  • In Confluence Script Runner - Jobs - setup to trash the temporary page
    • import com.atlassian.sal.api.component.ComponentLocator
      import com.atlassian.confluence.spaces.Space
      import com.atlassian.confluence.spaces.SpaceManager
      import com.atlassian.confluence.core.DefaultDeleteContext
      import com.atlassian.confluence.pages.Page
      import com.atlassian.confluence.pages.PageManager

      def spaceManager = ComponentLocator.getComponent(SpaceManager)
      def pageManager = ComponentLocator.getComponent(PageManager)

      def parentPage=pageManager.getPage("SOME_SPACE_KEY", "TO DELETE PAGES")
      def childPages = pageManager.getDescendants(parentPage)

      if(childPages) {
           childPages.each { thisChild ->
                     pageManager.trashPage(thisChild, DefaultDeleteContext.DEFAULT)

  • In Confluence Script Runner - REST Endpoint
    • //This script is loaded in Confluence - Script Runner - Rest End Point
      //It takes the spaceKey and page title as url parameters
      //gets page content
      //replacesAll href of jira issue links
      //relacesAll Back to Top CDATA
      //creates a child page from replaced string
      //redirects that child page for pdf download after the page is loaded in browser

      /*import org.apache.log4j.Level
      import org.apache.log4j.Logger
      def logx = Logger.getLogger("com.acme.workflows")

      import org.apache.http.HttpRequest
      import org.apache.http.protocol.HttpContext
      import groovy.json.JsonBuilder
      import groovy.json.JsonSlurper
      import org.apache.http.HttpRequestInterceptor
      import net.sf.json.JSONArray
      import static
      import static
      import groovy.transform.Field

      import org.codehaus.groovy.runtime.MethodClosure
      import org.apache.http.HttpEntity;
      import org.apache.http.HttpEntityEnclosingRequest


      import com.atlassian.confluence.setup.settings.SettingsManager
      import com.atlassian.confluence.core.BodyContent
      import com.atlassian.confluence.core.BodyType
      import com.atlassian.confluence.core.DefaultSaveContext
      import com.atlassian.confluence.core.DefaultDeleteContext
      import com.atlassian.confluence.pages.DuplicateDataRuntimeException
      import com.atlassian.confluence.pages.Page
      import com.atlassian.confluence.pages.PageManager
      import com.atlassian.confluence.pages.templates.PageTemplateManager
      import com.atlassian.confluence.spaces.Space
      import com.atlassian.confluence.spaces.SpaceManager
      import com.atlassian.confluence.user.AuthenticatedUserThreadLocal
      import com.atlassian.sal.api.component.ComponentLocator
      import com.onresolve.scriptrunner.canned.confluence.utils.PermissionDeniedException
      import groovy.json.JsonOutput
      import groovy.transform.BaseScript
      import groovy.transform.Field
      import org.apache.log4j.Logger


      import java.text.SimpleDateFormat

      import groovy.time.TimeCategory
      import groovy.time.TimeDuration

      Date start = new Date()

      @Field SpaceManager spaceManager = ComponentLocator.getComponent(SpaceManager)
      @Field PageManager pageManager = ComponentLocator.getComponent(PageManager)
      @Field PermissionManager permissionManager = ComponentLocator.getComponent(PermissionManager)
      @Field PageTemplateManager pageTemplateManager = ComponentLocator.getComponent(PageTemplateManager)
      //@Field Logger log = Logger.getLogger("com.onresolve.scriptrunner.runner.ScriptRunnerImpl")

      @BaseScript CustomEndpointDelegate delegate

      //End point name is the same as the method's name
      //exportPage_toPDF_withoutJira_hyperLinks(httpMethod: "GET", groups: ["confluence-administrators", "confluence-users"]) { MultivaluedMap queryParams, String body ->
      exportPage_toPDF_withoutJira_hyperLinks(httpMethod: "GET") { MultivaluedMap queryParams, String body ->

      Date date = new Date()
      String datePart = date.format("dd/MMM/yyyy")
      String timePart = date.format("HH:mm:ss.SSS")
      String dateTimeStamp = date.format("dd/MMM/yyyy HH:mm:ss.SSS")

      pageManager = ComponentLocator.getComponent(PageManager)

      def spaceKey = queryParams.getFirst("spaceKey").toString()
      def parentPageTitle = queryParams.getFirst("parentPageTitle").toString()

      try {
      parentPage=pageManager.getPage(spaceKey, parentPageTitle)
      def contentEntityObject = parentPage.getEntity()
      def parentPageBody=parentPage.getBodyAsString()

      def space = spaceManager.getSpace(spaceKey) as Space
      def childPageTitle = parentPageTitle + " - External"
      def childPage = pageManager.getPage(spaceKey, childPageTitle)

      if(childPage) {
      pageManager.trashPage(childPage, DefaultDeleteContext.DEFAULT)

      def HTML_A_HREF_TAG_PATTERN_jira = "\\s*(?i)href\\s*=\\s*\"([^\"]*jira/browse/[^\"]*\")"
      def regex_BacktoTop = "<p><ac:link ac:anchor=\"_wiki_toc\"><ri:content-entity ri:content-id=\"\\d+\" /><ac:plain-text-link-body><!\\[CDATA\\[Back to Top\\]\\]></ac:plain-text-link-body></ac:link></p>"

      def childPageContent = parentPageBody.replaceAll(HTML_A_HREF_TAG_PATTERN_jira, "dummy")
      childPageContent = childPageContent.replaceAll(regex_BacktoTop, "")

      def regex_jiraMacro = "<p style=\"font-size: 15.0px;\"><strong>G3 Issue used to create this Release Page</strong></p><p><ac:structured-macro ac:name=\"jira\" ac:schema-version=\"\\d+\" ac:macro-id=\"([a-zA-Z0-9*--]+)\"><ac:parameter ac:name=\"server\">Jira</ac:parameter><ac:parameter ac:name=\"serverId\">([a-zA-Z0-9*--]+)</ac:parameter><ac:parameter ac:name=\"key\">((?<!([A-Za-z]{1,10})-?)[A-Z]+-\\d+)</ac:parameter></ac:structured-macro></p>"
      childPageContent = childPageContent.replaceAll(regex_jiraMacro, "")

      def regex_history = "<h1>Document history</h1><p><ac:structured-macro ac:name=\"version-history\" ac:schema-version=\"\\d+\" ac:macro-id=\"([a-zA-Z0-9*--]+)\" /></p>"
      childPageContent = childPageContent.replaceAll(regex_history, "")

      def toDelete_parentPage=pageManager.getPage(spaceKey, "TO DELETE PAGES")

      def createdChildPageId = createBasicPage(space, toDelete_parentPage, childPageTitle, "Temporary Page")

      def tableFilterString = "<ac:structured-macro ac:name=\"table-filter\" ac:schema-version=\"1\""

      def loadResources = "" //tf-export-ready is Table filter element

      if(childPageContent.contains(tableFilterString)) {
      loadResources = "<ac:structured-macro ac:name=\"html\">\
      <script type=\"text/javascript\">\
      AJS.toInit(function() {\
      AJS.bind('tf-export-ready', function() {\
      setTimeout(function() {AJS.\$(location).prop('href', 'https://HOST/wiki/spaces/flyingpdf/pdfpageexport.action?pageId=" + createdChildPageId + "');}, 2000);\
      } else {
      //removed AJS.bind('tf-export-ready', function() {\
      loadResources = "<ac:structured-macro ac:name=\"html\">\
      <script type=\"text/javascript\">\
      AJS.toInit(function() {\
      setTimeout(function() {AJS.\$(location).prop('href', 'https://HOST/wiki/spaces/flyingpdf/pdfpageexport.action?pageId=" + createdChildPageId + "');}, 2000);\

      childPageContent = childPageContent.replaceAll("dummy", "") + loadResources

      editPage("${createdChildPageId}", childPageTitle, childPageContent, "2")

      pageUrl = "https://HOST/wiki/pages/viewpage.action?pageId=${createdChildPageId}"
      return Response.temporaryRedirect(URI.create(pageUrl)).build()
      } catch(Exception ex) {
      //pageIdMap["Exception"] = "Exception to get page or append page for spaceKey = ${spaceKey} and title = ${title}: ${ex.toString()}"

      * Create a basic page. This is not linked in any hierarchy.
      * @param space The space that this page belongs to.
      * @param title The title of the page we are creating.
      * @param content The content of the page we are creating.
      * @return The create page object.
      def createBasicPage(Space space, Page parentPage, String title, String content) {
      def pageManager = ComponentLocator.getComponent(PageManager)
      def page = new Page()
      def bodyContent = new BodyContent(page, content, BodyType.XHTML)
      page.with {
      linkPages(parentPage, page)
      pageManager.saveContentEntity(page, DefaultSaveContext.SUPPRESS_NOTIFICATIONS)


      * Link a parent and a child page together. This method creates a bi-directional relationship between the two pages.
      * @param parent The parent page that we wish to link.
      * @param child The child page that we wish to link.
      void linkPages(Page parent, Page child) {
      // Set the parent page on the child
      // Set the child page on the parent
      // Set the ancestors on the child page
      def ancestors = []
      def parentPageAncestors = parent.ancestors as List
      if (parentPageAncestors) {

      void editPage(String pageId, String pageTitle, String pageContent, String pageVersion) {
      //def logx = Logger.getLogger("com.acme.workflows")

      def pageManager = ComponentLocator.getComponent(PageManager)

      def params = [
      id: pageId,
      type: "page",
      title: pageTitle,
      body: [storage:[value:pageContent, representation:"storage"]],
      version: [number:pageVersion]

      def jsonString = new JsonBuilder(params).toString()

      def rest_api_str = "https://HOST/wiki/rest/api/content/${pageId}"
      def http = new HTTPBuilder(rest_api_str)
      try { def json = http.request(, { req ->
      req.addHeader('Authorization', 'Basic ' + 'USER:PASSWORD'.bytes.encodeBase64().toString())
      body = jsonString
      response.success = { resp, jsonData ->
      assert resp.status == 200
      return jsonData
      response.failure = { resp ->
      return resp.status
      } catch(Exception ex) {







Log in or Sign up to comment
Community showcase
Published in Confluence

Confluence Mythbusters: Does Atlassian even use Confluence?

Hi, Confluence collaborators! As part of #Confluence-Collaboratory month, we’ve created a very special Mythsbusters segment, where we're dive into an interesting myth and uncover the truth behind i...

1,598 views 7 30
Read article

Community Events

Connect with like-minded Atlassian users at free events near you!

Find an event

Connect with like-minded Atlassian users at free events near you!

Unfortunately there are no Community Events near you at the moment.

Host an event

You're one step closer to meeting fellow Atlassian users at your local event. Learn more about Community Events

Events near you