RFC-136: Forge staticView Macro for Confluence, Connect Migration & Feedback

RFCs are a way for Atlassian to share what we’re working on with our valued developer community. It’s a document for building shared understanding of a topic. It expresses a technical solution, but can also communicate how it should be built or even document standards. The most important aspect of an RFC is that a written specification facilitates feedback and drives consensus. It is not a tool for approving or committing to ideas, but more so a collaborative practice to shape an idea and to find serious flaws early.

Please respect our community guidelines: keep it welcoming and safe by commenting on the idea not the people (especially the author); keep it tidy by keeping on topic; empower the community by keeping comments constructive. Thanks!

Project Summary

Atlassian Connect will reach End of Support (EoS) in December 2026. One of the Connect capabilities without a direct Forge equivalent is the static content macro (staticContentMacro). This RFC introduces the Forge equivalent: staticView, a new opt-in rendering mode for Forge macros where content is stored as ADF (Atlassian Document Format) in Confluence Custom Content and rendered inline without an iframe.

This is an actively tracked feature request (FRGE-1495) that we have accepted and are working towards. Publishing this RFC is part of that process we want to make sure the solution we build addresses your real needs before we finalise the design.

Before we finalise the design, we want to:

  1. Clarify our understanding of how partners and customers use Connect static macros today: what content they render, how often they update it, and what scenarios depend on it.

  2. Share the proposed Forge staticView design and gather feedback on whether it covers your needs, where the gaps are, and what would make it easier to adopt.

  • Publish: 25 May 2026
  • Discuss: 8 June 2026
  • Resolve: 22 Jun 2026

Part 1: How Connect Static Macros Work Today

If you’re currently using the Connect staticContentMacro module (or have used it in the past), please share your experience by answering the questions in Part 3.

What is the Connect static content macro?

The Connect staticContentMacro module lets apps return pre-rendered Confluence Storage Format (XHTML) from a vendor-hosted endpoint. Confluence fetches this XHTML server-side and renders it directly in the page DOM, with no iframe and no app runtime loaded in the browser. The XHTML response is also cached, so the vendor’s server is not called on every page view.

This made Connect static macros well-suited for:

  • Pages with many macro instances (50 to hundreds), where loading an iframe per instance is too expensive

  • Content that doesn’t change on every page load: static reports, requirement matrices, math expressions, data tables from an external system

  • Scenarios where export fidelity matters: PDF, Word, and email exports rendered the cached XHTML directly

  • Use inside bodied macros like tabs, columns, and expand sections, where nested iframes cause layout and performance issues


Part 2: The Proposed Forge Solution: staticView

How it works

Today, all Forge macros render via a sandboxed iframe loaded in the user’s browser. This has real costs at scale:

  • Performance: Each macro instance spins up its own sandboxed web runtime. A page with 100 macros loads 100 iframes, causing significant CPU, memory, and network overhead, especially on mid-range hardware.

  • No caching: A Forge function can be invoked on every page render. This would result in increased cost for using app macros heavily.

  • Export limitations: PDF export pipelines invoke adfExport functions for each macro, potentially hundreds of parallel Forge function calls, which routinely hit invocation rate limits.

  • Nested macro issues: Iframes inside bodied macros (tabs, expand sections) cause layout shifts and performance problems.

staticView lets a macro store a pre-rendered ADF snapshot as Confluence Custom Content. When a page loads, Confluence fetches the snapshot and renders it directly in the DOM. No iframe, no function call, no app runtime.

The feature has two parts:

  1. Manifest declaration: The app declares that its macro has a staticView backed by a confluence:customContent type, and specifies which macro config parameter is used to look up the correct snapshot for each macro instance.

  2. Content population: The app is responsible for generating and storing ADF snapshots, either from the frontend when the macro first renders, or from a backend Forge Function for bulk migration or scheduled refresh.

Manifest definition

Here is how a macro with staticView is declared in the app manifest:

modules:
  macro:
    - key: my-macro
      title: My Macro
      staticView:
        customContentTypeKey: my-static-view-type  # links to the custom content type below
        contentTitleConfigKey: requirementId       # which macro config param is used as the lookup key
  confluence:customContent:
    - key: my-static-view-type                     # no 'resource' needed, ADF body only
      supportedContainerTypes:
        - page                                     # currently only page scope is supported
  • customContentTypeKey: identifies which Custom Content type stores the ADF snapshots. The full stored type key is namespaced as forge:::. See the Custom Content module reference for full manifest options.

  • contentTitleConfigKey: the name of a macro config parameter whose value is used as the title of the Custom Content entry. This is how Confluence maps each macro instance to its corresponding ADF snapshot, without needing to modify the page storage.

  • supportedContainerTypes: Custom Content is scoped to a container type. Currently, only page is supported for staticView, space and blogpost scopes are not yet available. Each snapshot is stored as a child of a specific page. If your app needs to update content, it must update the Custom Content for every page that has a snapshot. You can bulk-fetch all Custom Content entries of a given type using GET /wiki/api/v2/custom-content?type= to find all existing entries and iterate over them. There is no broadcast update mechanism.

How content gets stored and updated

In Connect, the vendor’s server returned fresh XHTML on each render (then Confluence cached it). With Forge staticView, the app stores a snapshot and is responsible for keeping it up to date. There is no automatic re-render. Updates go through the Confluence Custom Content REST API.

Path 1: Frontend (first render populates the snapshot)

When no snapshot exists for a macro instance, the app has two options for how to handle that state in the frontend:

Option A: Automatic fallback to dynamic rendering. The macro renders immediately as a dynamic iframe. The app generates ADF inside the iframe and calls POST /wiki/api/v2/custom-content to store the snapshot. On the next page load, Confluence finds the matching snapshot and renders it inline with no iframe. This is seamless for the user but means the first view always loads an iframe.

Option B: Explicit user-triggered rendering. Instead of automatically loading the iframe, the macro renders a lightweight placeholder with a button (e.g. “Load content” or “Generate view”). The user clicks to trigger the dynamic render, which then generates and stores the snapshot. This is useful when automatic iframe fallback at scale is not acceptable, for example on pages with many uninitialized macro instances where loading all iframes simultaneously would cause performance problems. The tradeoff is that it requires user action before content is visible.

In both options, once the snapshot is stored, subsequent page loads render it inline with no iframe or user interaction needed.

Path 2: Backend Forge Function (bulk generation and scheduled refresh)

  1. A Forge Function is triggered by an app lifecycle event, a schedule, or a Confluence event.

  2. The function queries the vendor’s data source and generates ADF for each macro instance.

  3. The function calls the Confluence Custom Content REST API to create or update snapshots in bulk, keyed by the config parameter value and scoped as children of the relevant Confluence page.

The backend path is critical for migration: it lets vendors pre-populate snapshots for all existing macro instances across all customer pages, so customers don’t need to re-insert or reconfigure their macros.

Path 3: On-demand generation triggered by user or external event

Apps can also provide mechanisms for users or external systems to trigger snapshot generation or updates outside of the automatic paths above:

  • Byline or page action: The app provides a byline button or Confluence page action that a user can click to trigger a Forge Function. The function regenerates and updates the ADF snapshot for all macro instances on that page. This is useful for apps where data freshness matters but a full scheduled refresh is too coarse.

  • Product events: The app listens to Confluence events (e.g. page updated, space events) via Forge event triggers and updates affected snapshots in response. For example, if an app’s macro renders data derived from page content, a page:updated event can trigger a re-generation of the snapshot for that page.

  • External triggers: An external system (e.g. a CI/CD pipeline, a third-party data source) calls the app’s backend, which then calls the Confluence Custom Content REST API to update the relevant snapshots. This is the pattern for apps whose rendered content is driven by data outside Confluence entirely.

Because Custom Content is scoped to a container type, any of these paths must identify which container instances have snapshots to update. Use GET /wiki/api/v2/custom-content?type= to bulk-fetch all entries of a given type and determine which ones need updating.

Rendering behaviour

Scenario Behaviour
Page view, snapshot exists ADF snapshot rendered inline. No iframe, no function call.
Page view, no snapshot yet Falls back to dynamic iframe render.
Macro inserted or config changed Dynamic iframe renders; app can store a new or updated snapshot. Snapshot visible on next page load.
PDF export Snapshot used if present. Falls back to adfExport function, then dynamic render.
Word export Requires adfExport to be defined. Static snapshot not used in this path.
API/HTML export Requires adfExport to be defined. Static snapshot not used in this path.

Known limitations and gaps vs. Connect static macros

We want to acknowledge upfront the differences between our previous staticContentMacro solution on connect and this new staticView proposal for Forge.

Content format: ADF vs. XHTML

Connect static macros returned Confluence Storage Format (XHTML), a rich and expressive format with many years of support. Forge staticView stores content as ADF, the modern Confluence document model. The difference matters:

  • ADF supports: paragraphs, headings, bullet and numbered lists, tables, code blocks, inline code, links, mentions, task lists, panels, media (images), status labels, dates, emoji, horizontal rules, expand sections, and inline SVG

  • ADF does not support (or has limited support for): arbitrary HTML/CSS styling, custom inline styles, multi-column div layouts, JavaScript, custom XHTML elements or namespaced attributes, and some Confluence-native macros embeddable in XHTML

  • Some content expressible in Connect XHTML cannot be faithfully represented in ADF today, particularly richly styled tables, complex visual layouts, and content using Confluence XHTML extensions

  • Media and images: ADF supports inline images and other media via media nodes, but these require assets to be pre-uploaded using the Confluence Media API to obtain media IDs. You then reference those IDs in the ADF. You cannot embed raw image URLs or base64 data directly in ADF media nodes. Inline SVG is an exception: SVG content can be embedded directly in ADF without a separate upload step, which makes it a useful option for vector graphics, icons, and diagrams that would otherwise require a media upload.

Content staleness

With staticView, the snapshot is a point-in-time copy. If the underlying data changes, the app must explicitly update the Custom Content entry via API. There is no mechanism for Confluence to signal to the app that a refresh is needed.

Updates require a page reload

After an app updates a snapshot via the API, the user must reload the page to see the new content. Updated snapshots are not re-rendered without a page reload.

No automatic lifecycle management

The platform does not automatically clean up Custom Content snapshots when a macro is deleted from a page or when the app is uninstalled. Apps are responsible for managing the lifecycle of their stored content. That said, Forge provides the tooling to do this: the preUninstall lifecycle event can be used to clean up snapshots before the app is removed, and the available Confluence product events (e.g. page deleted) can be used to remove or invalidate snapshots in response to content changes.

Page scope only space, blogpost, and global scopes not yet supported

Currently, staticView Custom Content only supports page as the container scope. Space, blogpost, and site-wide (global) scopes are not yet available. In practice this means a snapshot is always stored as a child of a specific page there is no way to define content at a broader scope. If your macro renders content that is the same across multiple pages or spaces (for example, a tenant-wide configuration view, a space-level summary, or content tied to a blogpost), you still need to store and manage a separate snapshot per page. A workspace or installation-level scope is also not supported.

No SDK or bridge helpers yet

Apps must call the Confluence Custom Content REST API directly. There are no Forge bridge methods or SDK helpers for getting, creating, or updating snapshots. This is being considered but not yet implemented.

App type support

Both UI Kit and Custom UI apps are supported.

Word and API export

Word export does not work for Forge macros generally. They export as a placeholder message unless adfExport is defined. This is a known Confluence-level limitation tracked separately.

Migration path from Connect static macros

  1. Macro key migration: Existing Connect macro instances on customer pages need their macro keys remapped to the new Forge macro key. The macro key should be the same on both sides. This is handled as part of the broader Connect-to-Forge migration infrastructure as described here.

  2. Snapshot pre-population: A Forge Function must generate and store ADF snapshots for all existing macro instances across all customer pages. Because contentTitleConfigKey uses an existing config parameter value as the lookup key, apps can do this in bulk without modifying page storage or asking customers to re-insert macros.

  3. Ongoing update strategy: Apps need a plan for keeping snapshots fresh: which Confluence events or schedules trigger a refresh, and how quickly updates need to propagate.

Migration path from Forge dynamic macros

  1. Add a confluence:customContent module and required scopes to your manifest. Note: this requires a major version bump.

  2. Add the staticView field to your macro definition.

  3. Implement ADF generation and snapshot storage, in the frontend, in a backend Function, or both.

  4. Run a bulk backfill function to pre-populate snapshots for existing macro instances on customer pages.

A macro can have both a dynamic view and a staticView during rollout. Instances without a snapshot fall back to dynamic rendering automatically.


Part 3: Asks

We’re looking for input from two groups: partners using Connect static macros today, and partners using Forge dynamic macros who might adopt staticView.

About your current Connect static macro usage

  1. What content do you render? Describe the type of content your Connect static macro produces: tables, lists, text, images, diagrams, code blocks, styled text, links, embedded Confluence macros. What XHTML elements or patterns are essential to your output?

  2. How many macro instances do your customers use on a single page? Give us a sense of scale: typical page has ~5, heavy pages have 50+, edge cases have 100+.

  3. How often do you update the static content, and what triggers the update? For example: on every page load, on a schedule (daily/weekly), when underlying data changes, only when the macro is reconfigured, or almost never. What causes an update to be needed?

  4. What scenarios are most critical to preserve? For example: export to PDF, use inside tables, use inside bodied macros like tabs or expand sections, specific XHTML elements you depend on.

  5. What would break for your customers if the content model changed significantly? Are there aspects of the Connect XHTML model that ADF cannot yet represent?

About the proposed Forge staticView design

  1. Does ADF cover what you need to render? If not, what content types or structures are missing? What would be the highest-priority additions?

  2. Is the update model (app pushes snapshots via API) workable for your use case? Or do you need a pull model, a webhook, or another mechanism?

  3. What Confluence events or triggers would you need to keep snapshots fresh? For example: page created/updated/deleted, macro config changed, space events, a schedule, an app-specific data change, a user action. Which of these are missing from the current set of available Forge triggers?

  4. What would make migrating from Connect static macros to Forge staticView easiest? For example: SDK helpers for snapshot management, tooling to scan and backfill existing pages, a migration guide, or platform-assisted macro key remapping.

  5. What would make the developer experience of building with staticView better? For example: bridge methods to get/create/update snapshots from within the iframe, a way for the macro instance to set its own snapshot mapping, better debugging tools, or a testing environment.

  6. Is the fallback to dynamic rendering (when no snapshot exists) acceptable? Or does it create problems, for example if a page has many unbackfilled macros and falls back to hundreds of iframes at once?

  7. Are there use cases you have for Connect static macros that staticView fundamentally cannot serve? If so, what are they, and is there an alternative approach that would work?

  8. Does page-level scope work for your use case, or do you need a broader scope? Currently, staticView Custom Content is scoped to page only space, blogpost, and site-wide scopes are not supported. This means a separate snapshot must be stored per page. If your macro renders content that is the same across multiple pages or spaces (for example, organisation-wide data, space-level summaries, or blogpost content), you would need to replicate and keep in sync a snapshot on every page. Does this create a real problem for your app? Which scope would you need space, blogpost, workspace, or installation-level and what use case would it serve?

Hi @mtalebi ,

Thanks for the RFC.

  1. As a clarification question, are Live Docs in scope for the Forge staticView?
  2. How are the access rights and security of the custom content type content defined? It might be a problem if our app writes custom content, and then a adversary overwrites this custom content, which might result in rendering the wrong data.
  3. How are external links handled, and how does this interact with app egress rules?

Thank you for the RFC, I will try to answer your questions soon.
We also have some concerns:

1. Discovering macros and config on a page

How should we list every macro instance on a given page or blog post and read its full config (not only contentTitleConfigKey)? Is scanning content storage the expected approach, or is there a supported API for backfill/refresh from a Forge function?

2. contentTitleConfigKey

Is the config param value meant to be a unique key per instance on that page? What happens if the user edits it—new snapshot or update? Can two instances of the same macro on one page have different snapshots, and do matching values collide? For macros with no good single param (e.g. several booleans), can we use macro.id / local-id instead?

3. Blog posts

Our macros are used on pages and blog posts; we can’t drop blog support. When will blogpost be in supportedContainerTypes? Until then, if we enable staticView, is dynamic fallback on blog posts guaranteed so macros still work there?

Hi @mtalebi ,

When rendering a page with https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-page/#api-pages-id-get (with body-format: view), will the Forge staticView call our backend to render the macro?

I’m asking, because our backend needs to be called on page render through the API as well as through the browser.

Seconded the need for localId to be a feasible lookup key. Our apps uses localId to identify the specific macro on a page (among many instances) and is needed for putting the right data in the right macro.

Also question did you consider entity properties to store the snapshots? 32KB JSON is a constraint but simpler to access and work with then registered custom content per app. And that would be consistent with the byline modules that render based on entity properties.

Hi @marc,
Thanks for the questions! Here are the clarifications:

  1. Yes, Live Docs are in scope for this feature.
  2. Our approach treats this custom content as part of the Confluence page content (tied to the product lifecycle), rather than app-specific data. We treat this similarly to entity properties or content properties. Apps can currently write to each other’s properties, so we are relying on the same trust model and platform security constraints here. If a bad actor app behaves maliciously, it would be treated as an abuse of the platform trust model and handled accordingly.
  3. Because staticView stores and returns pure ADF, external links are handled exactly the same way Confluence handles external links in any other ADF content. Since there is no app runtime or iframe loaded in the browser when the static view renders, the macro itself cannot make egress calls or execute client-side scripts. Therefore, Forge app egress rules do not apply to the rendered static view.

A few questions for you regarding your use cases:
To help us refine the design, it would be great to understand a bit more about how you’re using static macros today:

  1. How often do you typically need to refresh your static macro content, and what specific events or contextual changes trigger that refresh?
  2. Do your static macros render differently based on the user viewing them, or are they the same for everyone who can view the page?
  3. How many macro instances do your customers typically use on a single page?
  4. Does page-level scope work for your use cases, or do you strictly need a broader scope (like space or workspace)?
  5. Are there specific limitations in ADF (compared to XHTML) that would impact the content you currently render?

Looking forward to hearing your thoughts!

Hi @jonlopezdeguerena, thanks for the detailed questions.
Here is some clarity on how we are thinking about these areas:

  1. Discovering macros and config on a page: You can use Confluence CQL to find all pieces of content on an instance that use your macro (e.g. searching by macro key). Once you have the pages, you can retrieve the macros and their configurations. For the backfill process, we recommend using a Forge scheduled trigger combined with async queues. By putting the backfill batches into queues and introducing delays, you can safely manage the load and avoid hitting rate limits during the initial migration.
  2. contentTitleConfigKey: Currently, contentTitleConfigKey dictates the title of the custom content that should be loaded and rendered in place of the macro. To your point, if two instances of a macro on the same page share the same config value, they will render the exact same custom content snapshot. If an instance needs a different snapshot, it must point to a different custom content title. That said, we hear your concern. We are actively considering making this more flexible: either allowing the app to decide which field links the custom content to the macro, or basing the connection on a unique identifier (like local-id or macro.id).
  3. Blog posts: Yes, blog posts will definitely be in scope for this feature.

A few questions for you regarding your use cases:

To help us refine the design and ensure it meets your needs, it would be great to understand a bit more about how you use static macros today:

  1. How often do you typically need to refresh your static macro content, and what specific events or contextual changes trigger that refresh?
  2. Do your static macros render differently based on the user viewing them, or are they the same for everyone who can view the page?
  3. How many macro instances do your customers typically use on a single page?
  4. Does page-level scope work for your use cases, or do you strictly need a broader scope (like space or workspace)?
  5. Are there specific limitations in ADF (compared to XHTML) that would impact the content you currently render?

Looking forward to hearing your thoughts!

Hi @Chris_at_DigitalRose,

Thanks for the feedback!

  1. Using localId as a lookup key: As mentioned in a previous reply, we are definitely hearing the feedback on this. We are actively looking into making the linkage more flexible, including the ability to base it on a unique identifier like local-id or macro.id.
  2. Using Entity Properties instead of Custom Content was considered, but they are primarily designed to be a key-value store. For this feature, we need a representational storage solution. Using custom content allows us to store the snapshot natively in ADF format. This means Confluence can fetch the content and inject it directly into the page without any additional processing or rendering overhead on the platform side, which is critical for performance.

So let me get this straight… we can inject ADF with all sorts of links (that can include query string data egress) in static macros, and it will be A-OK for Runs on Atlassian and won’t even show any type of warning message to the end-user, but customer-managed egress is not on Runs on Atlassian because it might contain query string data egress (see Please help us shape the "Runs on Atlassian" roadmap for 2026 and beyond - #9 by HeyJoe)?

It would be nice if Atlassian can be consistent with regard to Runs on Atlassian requirements.

cc: @HeyJoe

To answer your questions for our app that integrates with Microsoft Entra ID, and renders an external user in a macro:

  1. What content do you render?

A card with user’s name and other attributes (different sizes + icon), avatar image.

  1. How many macro instances do your customers use on a single page?

The most extreme case we know of is ~70 macros on page. Usually much less, I guess <10.

  1. How often do you update the static content, and what triggers the update?

When external data changes, i.e. the Microsoft user’s attribute changes. (And of course when the macro is re-configured.)

  1. What scenarios are most critical to preserve?

No real blockers, rather dynamic features, like a copy-to-clipboard functionality.

  1. What would break for your customers if the content model changed significantly?
    6. Does ADF cover what you need to render?

Mostly, yes. I found workarounds for font-size and card-like layout, so no blockers for us. But for more complex macros it would be different.

  1. What Confluence events or triggers would you need to keep snapshots fresh?

macro config changed and a schedule.

  1. What would make migrating […] to Forge staticView easiest?

Definitely SDK helpers for snapshot management.

  1. Does page-level scope work for your use case, or do you need a broader scope?

We would need installation-level scope because the macro’s content is not tied to a page but purely on the external user (and a bit on the macro instance’s config). So having to update page-level snapshots adds unnecessary complexity for our use-case.

And from the 2nd batch of questions:

  1. Do your static macros render differently based on the user viewing them, or are they the same for everyone who can view the page?

They are the same for everyone.

Dear @mtalebi ,

Thanks for the RFC. Simple Cite is one of the apps this proposal is targeting — citation pages routinely have 50+ macros, with our heaviest customer running ~250 on a single page — so we have a real motivation to adopt staticView. After working through what migration would look like for us, I have to report that the design as proposed does not work for our use case. Four concrete reasons, roughly in order of severity:

1. Cross-instance numbering dependency.

Simple Cite numbers citations in document order across the page: [1], [2], [3], and so on. When a user inserts, deletes, or reorders a single citation, the numbers of potentially every other citation on the page shift. With staticView, that means every snapshot on the page needs to be regenerated for a single edit — on a 250-cite page that’s 250 Custom Content API calls per save. The RFC’s update model assumes snapshots are mostly independent. For citation tools — and likely anything else that does cross-instance numbering, such as figure and table captions, footnotes, or glossaries — they’re not. We don’t see a path to making this scale.

2. No story for bidirectional intra-page navigation.

The defining feature of a citation system is the bidirectional link between an inline reference and its entry in a “References” list. In our Connect macro, [1] was an anchor link to the corresponding summary entry, and the summary entry rendered occurrence links back into the page (1|2|3 etc., one link per occurrence of that citeID). This isn’t interactivity — no scripts, no tooltips — it’s plain HTML anchor navigation, which Connect’s XHTML supported trivially.

The RFC’s list of supported ADF content (paragraphs, headings, lists, tables, code blocks, links, mentions, task lists, panels, media, status, dates, emoji, horizontal rules, expand sections, inline SVG) doesn’t mention extension nodes or the Anchor macro. Question for the thread: does staticView ADF support intra-page anchor targets, either via the Anchor macro extension or some equivalent? Without this, bidirectional reference lists can’t be migrated to staticView at all — not just for our app but for any reference-index-style macro.

3. contentTitleConfigKey granularity is fundamentally insufficient for our case.

A Simple Cite citeID like Smith2020 can appear on the same page as:

  • An inline reference rendering [1]

  • A bib-output entry rendering the full citation line

  • A define-only entry that registers the source but renders nothing

  • Multiple inline references, each needing its own anchor target so the summary can link back to each occurrence

These are the same macro key with the same citeID value but require entirely different snapshots. The current design — one snapshot per config value — collapses them all into one. I appreciate the responses in posts #7 and #8 that you’re “actively considering” localId/macro.id-based lookup. For our category of macro this needs to be a commitment, not a consideration; it’s the difference between “can migrate” and “cannot migrate”. I’d encourage Atlassian to confirm this for GA.

Of course we could consider a macro parameter hash or something similar but it currently does not exists and we might run into trouble as soon as we add a parameter.

4. Cross-page dependencies have no story.

Simple Cite supports importing a bibliographic body from another page: a citation on page A can pull its source content from a citation defined on page B. When page B is edited, every page A that imports from it needs its snapshots regenerated. The RFC assumes snapshots are children of a specific page and that updates are triggered by events on that page. There’s no scaffolding for “page B changed, find every page that imports from B and refresh them.” We would have to build and maintain that index ourselves, with no support from the platform’s event model.


What would make staticView viable for us:

  • A committed (not “considering”) localId/macro.id-based lookup mechanism so each macro instance can have its own snapshot

  • Confirmed support for intra-page anchor targets in rendered ADF

  • A way to bulk-update all snapshots on a page in a single API call, or guidance on how 250-snapshot-per-edit page updates are expected to respect rate limits

  • Some thought given to cross-page snapshot dependencies, even if the answer is “you build the index, here are the events you can subscribe to”

We genuinely want this to work — the 250-iframe problem is real and our customers feel it. But as proposed, staticView doesn’t solve Simple Cite’s problem; it would force us to choose between losing core features (reference list cross-linking, accurate numbering after edits) and not migrating at all.

Happy to provide more detail on any of these if useful.

We typically refresh macro content on page view. Most triggers are related to page versions and content property changes. However also page status, edit date, last editor etc. change the rendering.

Usually our static macros are the same for every user. However sometimes guest users are a spacial case.

That is quite variable. Usually between 1 and 15. However our macros are much used in page property report macros, and that often results in hundreds or thousands of render requests.

Actually a more specific scope is also needed: inside the page properties macro.

I don’t currently have enough insight to know this.

Hi @mtalebi ,

After re-reading the proposal and the other responses, Here are some additional comments on the design:

This caching can be done. We do send no-cache headers and rerender as often as possible.

We see a static content macro as a macro returning static content (i.e. content without javascript). However we see a static content macro not as static in time.

This implementation does not work well with our usage. The static content is assembled at render time, not beforehand. Our content depends on the state of Confluence at the moment the page view and render takes place. We can not pre-render the static content for later, as we do not know the future state of Confluence when someone views a page.

Both proposed approaches are complicated, and deliver a undesirable user experience: Either an extra click is needed (option B), or there will be render and user interaction difference (option A).
We’d like to propose a different option:
The static macro directly returns ADF. This is analogous to the current connect static macros, and allows Confluence to cache the response (if cache headers are set), and allows apps to always return the same content to the user, independent of a first or subsequent render.

Returning ADF from the static content macro also eliminates the need for Path 2 and Path 3.

I think this proposal would be much simpler and much more alike the current connect static content macros.

hi @marc,

No, we will be using the custom content value instead of calling backend for performance reasons. Falling back to render dynamic macro would not be available in this case either. That is being addressed separately.

Hi!

Thanks for the RFC. We were part of the initial discussions behind this RFC and we’ve already deployed an early version for a few customers for requirement yogi macros.

In our case, macros were not loading for a specific customer, which in turn also had side effects on other forge items like the byline module being unable to load as well.

Like marc, we would have preferred to produce the static content at render time. Having to produce a major version to add the required permissions on custom content was also not ideal.

The asks:

  1. We typically render links
  2. Customers routinely have more than 200 requirement yogi macros per page. I think that the page with the most macros is around 1800.
  3. We do not update macros often. Updates are typically triggered by a page event or by a user action calling our remote in other parts of the app.
  4. Export to PDF and use inside nested bodied macros is most critical to preserve: Users often use requirement macros in other macros, such as a Table Filter macro
  5. (I don’t really understand the difference between points 5 and 6)
  6. ADF cannot render a border around an arbitrary element (specifically would be text for us).
  7. It’s workable. We’ve lost the ability to verify the validity of the macros before rendering, but it was an acceptable trade off for us given the alternative of a broken app. Static macros are opt-in for customers (simply do not provide custom content and the macro will fallback to a dynamic render), and we document the trade off.
  8. In our case, any page update or delete could cascade changes to hundreds of affected macros in hundreds of pages. It’s not an issue of missing information, it’s an issue of performance and scalability (and cost).
  9. Being able to work with the Content properties API by title ( = contentTitleConfigKey) would make things much easier.

We’ve had to build our own tool to produce a hash given a list of macro parameters, and update pages to add this hash key to existing macros.

e.g.:

// Macro contains parameters
{
  "foo": "a",
  "bar": "b"
}

// Output is
{
  "foo": "a",
  "bar": "b",
  "hash": "foo:a;bar:b" // We use this as the customContentTypeKey
}
  1. Better debugging tools would be great, but it’s been a recurring issue with Forge.

  2. Fallback is acceptable. Removing the fallback would require an alternative way for the user to choose between static and dynamic rendering.

  3. “Given what we know at the time of rendering, is my macro valid”. Marc mentioned an alternative, very similar to what we initially proposed:

  • Page views trigger (one or multiple) background job(s) that can produce some kind of html output: ADF nodes would be easiest and mesh well with the existing adfExport function.
  • Avoid blocking the client by rendering the page output with a skeleton loading indicator for macros
  • The Confluence page renderer can poll the server for the macro output, or receive rendering events from the backend via a persisting connection until all macros are rendered or fail (Push or pull architecture). Additional runtime overhead is minimal and every macro from every vendor can be safely retrieved from the backend and executed in parallel as appropriate.
  • Macro ADF output is cached based on a cache-control parameter, specified in the manifest, or in the macro config output saved on macro insert, or dynamically with the adf export produced by the background job
  1. Page level works well for us.

Best regards, Corentin (Requirement Yogi)

Hi!

Thanks for the RFC. We’ve already shipped this in production for our LaTeX Math for Confluence app, so wanted to share some real-world observations.

What we built: MathJax SVGs stored as custom content, using staticView with contentTitleConfigKey: _ccid, where _ccid is a UUID::formula composite key. We have three macros (inline, block, large block) with both frontend first-render saving and an admin panel for bulk generation.

What worked well:

  • mediaSingle + inline SVG as a base64 data URI is enough for our use case — no media upload needed
    • Upsert logic (PUT vs POST based on existing ccid) handles formula edits cleanly
      • The UUID::formula title pattern gives implicit versioning — a changed formula creates a new entry rather than mutating in place
    • Pain points:
  • No SDK helpers is the biggest friction — we’re hand-rolling raw REST calls to /wiki/api/v2/custom-content. The 25s Forge timeout also makes bulk generation painful at scale; we ended up chunking to 15 macros per invocation with manual offset pagination. Both of these feel like platform-level problems worth solving.

Cleanup is also unresolved. Orphaned custom content accumulates as formulas change. A lifecycle hook on macro deletion or a TTL mechanism would help a lot.

On specific questions:

For purely computed content (formula → SVG), push-on-render works fine — macro save and page publish are all the triggers we need. The contentTitleConfigKey model works for us, but only because we embed a UUID in the key to avoid collisions between identical formulas on the same page. A localId/macroId-based lookup would be cleaner and sidestep that entirely.

One open question: even with staticView working, we still needed a separate export resolver for PDF/Word. It’s unclear whether staticView is intended to replace export handlers or not — worth clarifying in the docs.


Our main asks:

  1. SDK helpers for custom content operations — raw REST calls to /wiki/api/v2/custom-content shouldn’t be something every app has to figure out independently.
  2. Bulk operation support — the 25s Forge timeout forces hand-rolled chunking; platform support for longer-running or paginated operations would make a real difference.
  3. Cleanup / lifecycle story — a hook on macro deletion or a TTL for orphaned snapshots to prevent unbounded accumulation over time.

Regards,
Umesh
Narva Software

Hi @marc,

We are are considering introducing restricted custom contents, so these these custom content are not exposed to others. would this rectify your concern #2?

Also, re #3 we can show a warning similar to rendering external links in Forge. would that address your concern there?

Hi @mtalebi ,

Thank you for your response. My impression is that the Forge static view gets more and more complicated. Actually I think it is getting too complicated.

I also think there is a conflation of the two uses of “static”. Static content macros in Connect returned “static” content, i.e. content without executable content like javascript. In this RFC the word “static” does also automatically mean cacheable content in a custom content type. So this is quite a different use case than the Connect static content macro.

So I would rather have a macro invocation directly return ADF instead of custom content types, possibly with a new permission model (re #2).

And re #3, I’d rather not have a warning for external inks in Forge. My concern was that in one context, Atlassian shows a warning, and in another not.

Thank you @mtalebi I will try to answer your questions:

How often do you typically need to refresh your static macro content, and what specific events or contextual changes trigger that refresh?

The main events that can require a refresh are page or blogpost updates, configuration changes, and events triggered by our application when the underlying state or data represented by the macro changes. Permission changes may also require a refresh in some cases, depending on whether the static view needs to reflect permission-dependent state.

Based on our production usage patterns, refresh-triggering events are significantly less frequent than page-view/render traffic. As a rough order-of-magnitude estimate, refreshes are in the range of hundreds of thousands per day globally across our install base, while render traffic is several times higher.

For individual high-traffic instances, we do see bursts rather than a perfectly steady stream. In the highest-traffic instances we observed, refresh-relevant activity is typically in the tens of events per minute, with heavier bursts reaching the low hundreds of events per minute. In the most active cases, combining several refresh-triggering sources, we would plan for peak bursts on the order of around 150–200 refresh-triggering events per minute for a single instance.

Do your static macros render differently based on the user viewing them, or are they the same for everyone who can view the page?

Mostly they are the same for everyone who can view the page. The underlying macro data is generally derived from page, space, app state, and macro configuration rather than from the individual viewer.

There are a few caveats. Some presentation details, such as date formatting, may depend on locale or timezone. Edit or configuration modes may also show additional UI that normal page viewers do not see. For a static snapshot, we would likely need a stable strategy for locale/timezone handling, unless staticView supports viewer-specific formatting.

How many macro instances do your customers typically use on a single page?

Most pages typically have only a few macro instances, but heavier customer pages can have dozens, and in some cases up to hundreds of instances on a single page.

Does page-level scope work for your use cases, or do you strictly need a broader scope, like space or workspace?

Page-level snapshot storage is a good fit for macros whose rendered data is anchored to the current page or blogpost.

However, page-level scope alone is not enough for all use cases. Some macros can represent data that comes from related content, broader page trees, labels, space-level configuration, or app-level configuration. In those cases, page-level storage is acceptable as the place where a snapshot lives, but we would still need broader invalidation and update capabilities.

In practice, that means being able to refresh snapshots when related pages, labels, configuration, or app-managed data changes, not only when the page containing the macro is edited. Space-level or installation-level mechanisms would be useful for shared data or configuration that affects many pages at once.

Are there specific limitations in ADF, compared to XHTML, that would impact the content you currently render?

ADF is probably workable for much of our core read-only output, especially structured content such as text, links, statuses, tables, and activity-style information.

Our main concern compared with XHTML is visual fidelity for inline SVG. Some of our current macro rendering relies on SVG/icon-style visual elements inline in the rendered output, and we would need to understand whether staticView ADF can preserve that directly, or whether we would need to convert those to supported image/media nodes or simpler text/color equivalents.

Beyond SVG, we would still want to validate table/layout fidelity, styling constraints, status-like visual elements, avatar/image support, and snapshot size/performance limits for larger outputs.

Static ADF not running client-side logic is acceptable for these read-only use cases. For us, the key requirement is predictable read-only rendering with good performance and export fidelity, not arbitrary JavaScript support.

Please NO. There is no difference between this and Forge apps editing and inserting links in the page itself. So we don’t need warning pop-ups.