Skip to main content

Fragments API Reference for AI

This document is a complete API reference and code guide for writing Fragments in Script Master — an Atlassian Forge app that lets admins embed custom HTML + JavaScript panels, actions, and pages directly inside Jira and Confluence Cloud. A Fragment is a self-contained HTML document (with <script> tags) that runs inside a sandboxed iframe and has direct access to Forge Bridge APIs for reading and writing data in the current Atlassian product.

Use this document as context when asking an AI assistant (Claude, Gemini, ChatGPT, etc.) to generate fragment code:

Here is the Fragments API reference: [link to this file]. Write a fragment for [your task].


Execution Model

Fragment content is stored as a raw HTML string and injected into an <iframe> via the srcDoc attribute:

<iframe srcDoc={fragmentContent} width="100%" height={iframeHeight} />

Forge Bridge globals are injected directly into iframe.contentWindow before the document renders:

Object.assign(iframe.contentWindow, {
setHeight,
requestJira,
requestConfluence,
showFlag,
router,
view,
events,
Modal,
theme,
});

Key implications:

  • Write full HTML documents — include <link> tags for CSS resets, <div> containers, and <script> tags.
  • Use <script type="module"> to get top-level await support (required for calling async Forge Bridge APIs directly).
  • Globals are on window — call view, requestJira, setHeight, etc. directly without any import or window. prefix.
  • No return statement — fragments render HTML; there is no output panel. Use DOM manipulation to display results.
  • Errors are silent by default — add window.onerror to catch and display errors in the UI.
  • The script runs with the current user's permissions — no service account, no elevated access.
  • Max iframe height: 960px — use setHeight() to resize dynamically; the default is 100%.
  • No Node.js APIs (fs, path, etc.) — browser and Forge Bridge globals only.

Available Globals

All globals below are available in both Jira and Confluence fragments unless noted.

GlobalTypeDescription
viewForge view objectAccess page context (issue key, page ID, space key, etc.) and refresh the current page
requestJira(path, options?) => Promise<Response>Authenticated calls to the Jira REST API — Jira fragments only
requestConfluence(path, options?) => Promise<Response>Authenticated calls to the Confluence REST API — Confluence fragments only
showFlag(options) => FlagShow a notification flag in the Atlassian product UI
routerForge router objectNavigate to Jira or Confluence pages programmatically
eventsForge events objectPublish and subscribe to Forge UI events
ModalForge Modal objectOpen Forge modal dialogs
theme{ colorMode: 'light' | 'dark' }Current Atlaskit color mode for dark/light theme support
setHeight(height: string) => voidResize the fragment iframe (e.g. setHeight('300px'))
fetchWeb fetchCall any external HTTP endpoint — no authentication injected
consoleBrowser consoleconsole.log(), console.error(), etc. (visible in DevTools)

Note: requestJira is always injected but will fail in Confluence contexts because the current user is not authenticated against Jira. Use requestConfluence in Confluence fragments and requestJira in Jira fragments.


API Reference

view.getContext()

Returns context about the current page and user. Always call this at the start of a fragment script to get entity IDs.

const context = await view.getContext();

context shape in Jira fragments:

{
accountId: string; // current user's Atlassian account ID
cloudId: string; // Atlassian site cloud ID
siteUrl: string; // e.g. "https://your-org.atlassian.net"
locale: string; // e.g. "en-US"
timezone: string; // e.g. "Europe/Berlin"
moduleKey: string; // which fragment location is rendering
extension: {
project: { id: string; key: string; type: string };
issue: { key: string }; // only in issue panel / issue action locations
};
}

context shape in Confluence fragments:

{
accountId: string;
cloudId: string;
siteUrl: string;
locale: string;
timezone: string;
moduleKey: string;
extension: {
space: { id: string; key: string };
content: { id: string; type: string }; // page/blogpost/etc.
};
}

Common access patterns:

// Jira — get current issue key
const { extension: { issue: { key: issueKey } } } = await view.getContext();

// Confluence — get current page ID
const { extension: { content: { id: pageId } } } = await view.getContext();

// Confluence — get current space key
const { extension: { space: { key: spaceKey } } } = await view.getContext();

view.refresh()

Refreshes the current Jira issue or Confluence page. Use this after performing mutations so the host page reflects the changes.

// Trigger host page refresh after deleting issue links
document.getElementById('refresh-btn').addEventListener('click', () => view.refresh());

requestJira(path, options?)

Makes an authenticated HTTP request to the Jira Cloud REST API v3.

  • path — relative path starting with /rest/api/3/...
  • options — standard fetch RequestInit options (method, headers, body)
  • Returns — a standard Response object
const response = await requestJira('/rest/api/3/myself', {
headers: { 'Accept': 'application/json' },
});
const data = await response.json();
document.getElementById('output').textContent = data.displayName;

requestConfluence(path, options?)

Makes an authenticated HTTP request to the Confluence REST API v2.

  • path — relative path starting with /wiki/...
  • options — standard fetch options
  • Returns — a standard Response object
const response = await requestConfluence('/wiki/rest/api/user/current', {
headers: { 'Accept': 'application/json' },
});
const data = await response.json();
document.getElementById('output').textContent = data.displayName;

showFlag(options)

Displays a notification flag in the Atlassian product UI.

showFlag({
id: 'my-flag',
title: 'Done!',
description: 'Links deleted successfully.',
type: 'success', // 'info' | 'success' | 'warning' | 'error'
isAutoDismiss: true,
});

FlagOptions type:

{
id: string | number;
title?: string;
description?: string;
type?: 'info' | 'success' | 'warning' | 'error';
isAutoDismiss?: boolean;
actions?: Array<{ text: string; onClick: () => void }>;
}

setHeight(height)

Resizes the fragment iframe. Call this after rendering content so the iframe fits its contents.

// Fit to content height
setHeight(document.querySelector(':root').scrollHeight + 'px');

// Fixed height
setHeight('300px');

The hard maximum is 960px. Passing a larger value will be clamped.


theme

An object with the current Atlaskit color mode. Use it to apply dark/light theme styles.

// theme.colorMode is 'light' or 'dark'
document.querySelector(':root').setAttribute('data-color-mode', theme.colorMode);
document.querySelector(':root').setAttribute('data-theme', `${theme.colorMode}:${theme.colorMode}`);
document.querySelector(':root').insertAdjacentHTML('afterbegin',
`<link rel="stylesheet" href="https://forge.cdn.prod.atlassian-dev.net/atlaskit-tokens_${theme.colorMode}.css" />`
);

router

The Forge Bridge router for navigating within the Atlassian product. See Forge router docs.


events

The Forge Bridge event bus for publishing and subscribing to UI events. See Forge events docs.


The Forge Bridge modal API for opening dialogs. See Forge Modal docs.


Code Patterns

Pattern: Dark theme support

Apply this block at the top of every <script type="module"> that renders visible UI. The theme global is synchronously available — no await needed.

document.querySelector(':root').setAttribute('data-color-mode', theme.colorMode);
document.querySelector(':root').setAttribute('data-theme', `${theme.colorMode}:${theme.colorMode}`);
document.querySelector(':root').insertAdjacentHTML('afterbegin',
`<link rel="stylesheet" href="https://forge.cdn.prod.atlassian-dev.net/atlaskit-tokens_${theme.colorMode}.css" />`
);

Pattern: Error display

Fragment errors are silent by default. Add a visible error container and wire up window.onerror:

<div id="errors" style="color:red"></div>

<script type="module">
window.onerror = (e) => document.getElementById('errors').textContent = e.toString();

// Check HTTP errors explicitly:
const response = await requestConfluence(`/wiki/api/v2/pages/${pageId}`, {
headers: { 'Accept': 'application/json' }
});
if (response.status !== 200) throw new Error(`API error: ${response.status} ${response.statusText}`);
</script>

Pattern: Auto-resize iframe to content

Call setHeight after all DOM mutations are complete so the iframe fits its rendered content:

// After rendering content:
setHeight(document.querySelector(':root').scrollHeight + 'px');

// Or for a specific container:
setHeight(document.getElementById('content').scrollHeight + 'px');

Pattern: Show a loading state then replace with results

<div id="loading">Loading...</div>
<div id="results" style="display:none"></div>

<script type="module">
// ... fetch data ...
document.getElementById('loading').style.display = 'none';
document.getElementById('results').textContent = result;
document.getElementById('results').style.display = 'block';
setHeight(document.querySelector(':root').scrollHeight + 'px');
</script>

Complete Examples

Jira: Global variables available in every fragment

Name: Global variables available in every fragment
Description: Displays the global functions and objects accessible in every fragment, providing access to application, current context, flags, and more.

<!-- List of all global Forge Bridge APIs available in every fragment -->
<div>Global functions and objects:</div>
<div id="globals"></div>

<script>
// These global variables are defined in every fragment
const globals = {
requestJira: window.requestJira, // https://developer.atlassian.com/platform/forge/custom-ui-bridge/requestJira/
requestConfluence: window.requestConfluence, // https://developer.atlassian.com/platform/forge/apis-reference/ui-api-bridge/requestConfluence/
showFlag: window.showFlag, // https://developer.atlassian.com/platform/forge/apis-reference/ui-api-bridge/showFlag/
router: window.router, // https://developer.atlassian.com/platform/forge/apis-reference/ui-api-bridge/router/
view: window.view, // https://developer.atlassian.com/platform/forge/apis-reference/ui-api-bridge/view/
};
const definedElement = (def) => `<div style="color:green"><strong>${def}</strong> is defined</div>`;
const undefinedElement = (def) => `<div style="color:red"><strong>${def}</strong> is undefined</div>`;

for (const [key, value] of Object.entries(globals)) {
if (value) document.getElementById('globals').innerHTML += definedElement(key);
else document.getElementById('globals').innerHTML += undefinedElement(key);
}
</script>

Jira: Display contributors

Name: Display contributors
Description: Shows the list of all users who have edited this issue.

<!-- REST API: https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-issueidorkey-changelog-get -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@atlaskit/css-reset" />
<div>
Contributors: <div id='contributors'>Loading...</div>
</div>

<script type="module">
// Dark theme support
document.querySelector(":root").setAttribute("data-color-mode", theme.colorMode);
document.querySelector(":root").setAttribute("data-theme", `${theme.colorMode}:${theme.colorMode}`);
document.querySelector(":root").insertAdjacentHTML("afterbegin", `<link rel="stylesheet" href="https://forge.cdn.prod.atlassian-dev.net/atlaskit-tokens_${theme.colorMode}.css" />`);

const context = await view.getContext();
const issueKey = context.extension.issue.key;
const response = await requestJira(`/rest/api/3/issue/${issueKey}/changelog`, {
headers: { 'Accept': 'application/json' }
});
const responseData = await response.json();
const allContributors = responseData.values.map(obj => obj.author.displayName);
const uniqueContributors = new Set(allContributors);
const uniqueContributorsList = Array.from(uniqueContributors).join(', ');

document.getElementById('contributors').textContent = uniqueContributorsList;

// Resize window
setHeight(document.querySelector(':root').scrollHeight + 'px');
</script>

Jira: Display Assignee changes

Name: Display Assignee changes
Description: Displays all assignee changes for the issue. Note: only fetches the first 100 changelog entries.

<!--
Warning: only takes the first 100 change log entries from the issue change history
REST API: https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-issueidorkey-changelog-get
-->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@atlaskit/css-reset" />
<div id='content'>
Assignees: <div id='assignees'>Loading...</div>
</div>

<script type="module">
'use strict';
// Dark theme support
document.querySelector(":root").setAttribute("data-color-mode", theme.colorMode);
document.querySelector(":root").setAttribute("data-theme", `${theme.colorMode}:${theme.colorMode}`);
document.querySelector(":root").insertAdjacentHTML("afterbegin", `<link rel="stylesheet" href="https://forge.cdn.prod.atlassian-dev.net/atlaskit-tokens_${theme.colorMode}.css" />`);

const context = await view.getContext();
const issueKey = context.extension.issue.key;
const response = await requestJira(`/rest/api/3/issue/${issueKey}/changelog`, {
headers: { 'Accept': 'application/json' }
});
const responseData = await response.json();
const allChanges = responseData.values.reduce((acc, value) => ([...acc, ...value.items]), []);
const assigneeChanges = allChanges.filter(change => change.field === 'assignee');
const allAssignees = assigneeChanges.reduce((acc, change) => ([...acc, change.fromString, change.toString]), []);
const uniqueAssignees = new Set(allAssignees);
const uniqueAssigneesList = Array.from(uniqueAssignees).filter(Boolean).join(' → ');

const result = allAssignees.length ? uniqueAssigneesList : 'no changes';
document.getElementById('assignees').textContent = result;

// Resize window
setHeight(document.querySelector('#content').scrollHeight + 'px');
</script>

Name: Delete all issue links
Description: Permanently deletes all issue links from the current issue. Warning: deletion happens without confirmation.

<!-- Warning: the deletion happens without confirmation! -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@atlaskit/css-reset@3.0.1/dist/bundle.css" />
<div>
Deleting links: <div id='contributors'>Loading...</div>
</div>
<button id="refresh-btn" style="display: none" onclick="view.refresh()">Refresh window</button>

<script type="module">
const context = await view.getContext();
const issueKey = context.extension.issue.key;
const response = await requestJira(`/rest/api/3/issue/${issueKey}`, {
headers: { 'Accept': 'application/json' }
});
const responseData = await response.json();
const issueLinkIds = responseData.fields.issuelinks.map(issuelink => issuelink.id);
for (const issueLinkId of issueLinkIds) {
await requestJira(`/rest/api/3/issueLink/${issueLinkId}`, {
method: 'DELETE'
});
}
const result = issueLinkIds.length
? `${issueLinkIds.length} links successfully deleted!`
: 'No links to be deleted';

document.getElementById('contributors').textContent = result;
document.getElementById('refresh-btn').style.display = 'block';
</script>

Jira: Embed YouTube video

Name: Embed YouTube video
Description: Embeds a YouTube video with dark theme support and auto-resize.

<!-- Reset css styles -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@atlaskit/css-reset" />

<!-- Embed YouTube video -->
<iframe width="560" height="315" src="https://www.youtube.com/embed/dQw4w9WgXcQ?si=Jh77JtJ-IGQkLz5T" title="YouTube video player" frameBorder="0"></iframe>

<script type="module">
// Dark theme support
document.querySelector(":root").setAttribute("data-color-mode", theme.colorMode);
document.querySelector(":root").setAttribute("data-theme", `${theme.colorMode}:${theme.colorMode}`);
document.querySelector(":root").insertAdjacentHTML("afterbegin", `<link rel="stylesheet" href="https://forge.cdn.prod.atlassian-dev.net/atlaskit-tokens_${theme.colorMode}.css" />`);

// Resize window
setHeight(document.querySelector(':root').scrollHeight + 'px');
</script>

Confluence: Global variables available in every fragment

Name: Global variables available in every fragment
Description: Displays the global functions and objects accessible in every fragment.

<!-- Read more: https://docs.apportunity.xyz/script-master/forge-bridge-front -->
<div>Global functions and objects:</div>
<div id="globals"></div>

<script>
// These global variables are defined in every fragment. Use them directly without the "window." prefix.
const globals = {
view: window.view, // https://developer.atlassian.com/platform/forge/apis-reference/ui-api-bridge/view/
requestConfluence: window.requestConfluence, // https://developer.atlassian.com/platform/forge/apis-reference/ui-api-bridge/requestConfluence/
showFlag: window.showFlag, // https://developer.atlassian.com/platform/forge/apis-reference/ui-api-bridge/showFlag/
router: window.router, // https://developer.atlassian.com/platform/forge/apis-reference/ui-api-bridge/router/
events: window.events, // https://developer.atlassian.com/platform/forge/apis-reference/ui-api-bridge/events/
Modal: window.Modal, // https://developer.atlassian.com/platform/forge/apis-reference/ui-api-bridge/modal/
};
const definedElement = (def) => `<div style="color:green"><strong>${def}</strong> is defined</div>`;
const undefinedElement = (def) => `<div style="color:red"><strong>${def}</strong> is undefined</div>`;

for (const [key, value] of Object.entries(globals)) {
if (value) document.getElementById('globals').innerHTML += definedElement(key);
else document.getElementById('globals').innerHTML += undefinedElement(key);
}
</script>

Confluence: Get recent contributors

Name: Get recent contributors
Description: Retrieves the list of recent contributors for the current page and displays them as a comma-separated string.

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@atlaskit/css-reset" />
<div>
Recent contributors: <div id='contributors'>Loading...</div>
</div>
<div id="errors" style="color:red"></div>

<script type="module">
window.onerror = (e) => document.getElementById('errors').innerHTML = e.toString();

// Dark theme support
document.querySelector(":root").setAttribute("data-color-mode", theme.colorMode);
document.querySelector(":root").setAttribute("data-theme", `${theme.colorMode}:${theme.colorMode}`);
document.querySelector(":root").insertAdjacentHTML("afterbegin", `<link rel="stylesheet" href="https://forge.cdn.prod.atlassian-dev.net/atlaskit-tokens_${theme.colorMode}.css" />`);

const context = await view.getContext();
const pageId = context.extension.content.id;

const versionsResponse = await requestConfluence(`/wiki/api/v2/pages/${pageId}?include-version=false&include-versions=true`, {
headers: { 'Accept': 'application/json' }
});
if (versionsResponse.status !== 200) throw new Error(`Error getting page versions ${versionsResponse.statusText}`);
const pageWithVersions = await versionsResponse.json();

const authorIds = pageWithVersions.versions.results.map(version => version.authorId);
const uniqueAuthorIds = [...new Set(authorIds)];
const authorsQueryParams = uniqueAuthorIds.map(authorId => `accountId=${authorId}&`).join('');
const authorsResponse = await requestConfluence(`/wiki/rest/api/user/bulk?${authorsQueryParams}`, {
headers: { 'Accept': 'application/json' }
});
if (authorsResponse.status !== 200) throw new Error(`Error getting contributor details ${authorsResponse.statusText}`);
const contributorsData = await authorsResponse.json();

const allContributors = contributorsData.results.map(contributor => contributor.displayName);
const uniqueContributors = [...new Set(allContributors)];
const result = allContributors.length ? uniqueContributors.join(', ') : 'no contributors';
document.getElementById('contributors').textContent = result;

// Resize window
setHeight(document.querySelector(':root').scrollHeight + 'px');
</script>

Confluence: Count sentences, words, and characters

Name: Count sentences, words, and characters
Description: Fetches the current page body and counts sentences, words, and characters.

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@atlaskit/css-reset@3.0.1/dist/bundle.css" />

<div id='loading'>Loading...</div>
<div id='results' style="display: none">
<div>Sentences: <span id="sentences"></span></div>
<div>Words: <span id="words"></span></div>
<div>Characters: <span id="characters"></span></div>
<div>Characters (with spaces): <span id="characters-with-spaces"></span></div>
</div>
<div id="errors" style="color:red"></div>

<script type="module">
window.onerror = (e) => document.getElementById('errors').innerHTML = e.toString();

const decode = (string) => {
const output = [];
let counter = 0;
const length = string.length;
while (counter < length) {
const value = string.charCodeAt(counter++);
if (value >= 0xD800 && value <= 0xDBFF && counter < length) {
const extra = string.charCodeAt(counter++);
if ((extra & 0xFC00) == 0xDC00) {
output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000);
} else {
output.push(value);
counter--;
}
} else {
output.push(value);
}
}
return output;
};

const count = (target, options = {}) => {
let original = '' + (typeof target === 'string' ? target : ('value' in target ? target.value : target.textContent));
if (options.stripTags) original = original.replace(/<\/?[a-z][^>]*>/gi, '');
const trimmed = original.trim();
return {
sentences: trimmed ? (trimmed.match(/[.?!…]+./g) || []).length + 1 : 0,
words: trimmed ? (trimmed.replace(/['";:,.?¿\-]+/g, '').match(/\S+/g) || []).length : 0,
characters: trimmed ? decode(trimmed.replace(/\s/g, '')).length : 0,
all: decode(original).length,
};
};

const context = await view.getContext();
const pageId = context.extension.content.id;
const pageResponse = await requestConfluence(`/wiki/api/v2/pages/${pageId}?include-version=true&body-format=storage`, {
headers: { 'Accept': 'application/json' }
});
if (pageResponse.status !== 200) throw new Error(`Error getting page ${pageResponse.statusText}`);
const pageData = await pageResponse.json();

const calcResult = count(pageData.body.storage.value, { stripTags: true });
document.getElementById('loading').style.display = 'none';
document.getElementById('results').style.display = 'block';
document.getElementById('sentences').innerHTML = calcResult.sentences;
document.getElementById('words').innerHTML = calcResult.words;
document.getElementById('characters').innerHTML = calcResult.characters;
document.getElementById('characters-with-spaces').innerHTML = calcResult.all;
</script>

Confluence: Display an inspiring quote

Name: Display an inspiring quote
Description: Demonstrates how to use an external REST API to retrieve data and display it in a fragment.

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@atlaskit/css-reset@3.0.1/dist/bundle.css" />

<div class="blockquote">
<p>loading...</p>
<footer class="blockquote-footer">
<cite title="Source Title"></cite>
</footer>
</div>

<button style="margin-top: 20px">New Quote</button>

<script>
// Powered by Quotable — https://github.com/lukePeavey/quotable
const button = document.querySelector("button");
const quote = document.querySelector(".blockquote p");
const cite = document.querySelector(".blockquote cite");

async function updateQuote() {
const response = await fetch("https://api.quotable.io/random");
const data = await response.json();
if (response.ok) {
quote.textContent = data.content;
cite.textContent = data.author;
} else {
quote.textContent = "An error occurred";
console.log(data);
}
}

button.addEventListener("click", updateQuote);
updateQuote();
</script>

Confluence: Read Page (text-to-speech)

Name: Read Page
Description: Converts the current Confluence page content to audio using the Web Speech API, with voice selection, speed/pitch controls, and playback navigation.

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@atlaskit/css-reset" />

<style>
html, body, .page { height: 100%; }
body { overflow: hidden; }
.page { display: flex; flex-direction: column; }
.page-header { flex: 0 0 auto; }
.page-content { flex: 1 1 auto; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; }
.page-footer { flex: 0 0 auto; }
.shimmer {
padding: 10px; margin-bottom: 20px; color: grey; display: inline-block;
-webkit-mask: linear-gradient(-60deg, #000 30%, #0005, #000 70%) right/300% 100%;
background-repeat: no-repeat;
animation: shimmer 2.5s infinite;
}
@keyframes shimmer { 100% { -webkit-mask-position: left } }
</style>

<div class="page">
<div class="page-header">
<div id="errors" style="color:red"></div>
</div>
<div class="page-content">
<div id="loading">Loading...</div>
<div style="display: flex; gap: 10px">
<p>Voice:</p><select id="voice" style="width: 100%;"></select>
</div>
<div style="display: flex; gap: 10px">
<p>Speed:</p><input type="range" min="0.5" max="2" value="1" step="0.1" id="rate" />
</div>
<div style="display: flex; gap: 10px">
<p>Pitch:</p><input type="range" min="0.5" max="2" value="1" step="0.1" id="pitch" />
</div>
<div id="current" class="shimmer"></div>
</div>
<div class="page-footer">
<div id='controls' style="display: none; flex-direction: row; justify-content: space-between; gap: 10px;">
<button id="prev" style="display: none;">⏮ prev</button>
<button id="play">play ⏵</button>
<button id="pause" style="display: none;">pause ⏸</button>
<button id="stop" style="display: none;">stop ⏹</button>
<button id="next" style="display: none;">next ⏭</button>
</div>
</div>
</div>

<script type="module">
window.onerror = (e) => document.getElementById('errors').innerHTML = e.toString();

document.querySelector(":root").setAttribute("data-color-mode", theme.colorMode);
document.querySelector(":root").setAttribute("data-theme", `${theme.colorMode}:${theme.colorMode}`);
document.querySelector(":root").insertAdjacentHTML("afterbegin", `<link rel="stylesheet" href="https://forge.cdn.prod.atlassian-dev.net/atlaskit-tokens_${theme.colorMode}.css" />`);

const context = await view.getContext();
const pageId = context.extension.content.id;
const response = await requestConfluence(`/wiki/api/v2/pages/${pageId}?body-format=atlas_doc_format`, {
headers: { 'Accept': 'application/json' }
});
if (response.status !== 200) throw new Error(`Error getting page ${response.statusText}`);
const page = await response.json();

const adfToPlainText = (adfJson) => {
const sentences = [];
if (adfJson && adfJson.content) {
for (const contentItem of adfJson.content) {
if (contentItem.text) sentences.push(contentItem.text);
if (contentItem.content) sentences.push(...adfToPlainText(contentItem, []));
}
}
return [...sentences];
};

let voices;
window.speechSynthesis.onvoiceschanged = () => {
voices = window.speechSynthesis.getVoices();
voices.forEach(v => {
const opt = document.createElement('option');
opt.textContent = `${v.name} (${v.lang})${v.default ? ' -- DEFAULT' : ''}`;
opt.setAttribute('data-name', v.name);
document.querySelector('#voice').appendChild(opt);
});
};

const sentences = adfToPlainText(JSON.parse(page.body.atlas_doc_format.value), []);
let currentIndex = 0;

const playNext = () => {
window.speechSynthesis.cancel();
if (currentIndex > sentences.length - 1) { document.querySelector("#stop").click(); return; }
const utter = new SpeechSynthesisUtterance(sentences[currentIndex]);
utter.rate = Number(document.querySelector('#rate').value);
utter.pitch = Number(document.querySelector('#pitch').value);
const selectedName = document.querySelector('#voice').selectedOptions[0].getAttribute('data-name');
utter.voice = voices.find(v => v.name === selectedName) || null;
utter.addEventListener('end', () => { currentIndex++; playNext(); });
document.querySelector('#current').innerHTML = `${currentIndex + 1} of ${sentences.length}. ${sentences[currentIndex]}`;
window.speechSynthesis.speak(utter);
};

document.querySelector("#play").addEventListener("click", () => {
document.querySelector("#play").style.display = 'none';
['#pause', '#stop', '#prev', '#next'].forEach(s => document.querySelector(s).style.display = 'block');
document.querySelector("#current").classList.add("shimmer");
playNext();
});
document.querySelector("#pause").addEventListener("click", () => {
window.speechSynthesis.pause();
document.querySelector("#play").style.display = 'block';
document.querySelector("#pause").style.display = 'none';
});
document.querySelector("#stop").addEventListener("click", () => {
window.speechSynthesis.cancel();
currentIndex = 0;
document.querySelector('#current').innerHTML = '';
document.querySelector("#play").style.display = 'block';
['#pause', '#stop', '#prev', '#next'].forEach(s => document.querySelector(s).style.display = 'none');
});
document.querySelector("#prev").addEventListener("click", () => {
if (currentIndex <= 0) { document.querySelector("#stop").click(); return; }
currentIndex--;
document.querySelector("#play").click();
});
document.querySelector("#next").addEventListener("click", () => {
if (currentIndex >= sentences.length - 1) { document.querySelector("#stop").click(); return; }
currentIndex++;
document.querySelector("#play").click();
});

document.querySelector('#loading').style.display = 'none';
document.querySelector('#controls').style.display = 'flex';
setHeight('200px');
</script>

Confluence: Embed YouTube video

Name: Embed YouTube video
Description: Embeds a YouTube video with dark theme support and auto-resize.

<!-- Reset css styles -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@atlaskit/css-reset" />

<!-- Embed YouTube video -->
<iframe width="560" height="315" src="https://www.youtube.com/embed/dQw4w9WgXcQ?si=Jh77JtJ-IGQkLz5T" title="YouTube video player" frameBorder="0"></iframe>

<script type="module">
// Dark theme support
document.querySelector(":root").setAttribute("data-color-mode", theme.colorMode);
document.querySelector(":root").setAttribute("data-theme", `${theme.colorMode}:${theme.colorMode}`);
document.querySelector(":root").insertAdjacentHTML("afterbegin", `<link rel="stylesheet" href="https://forge.cdn.prod.atlassian-dev.net/atlaskit-tokens_${theme.colorMode}.css" />`);

// Resize window
setHeight(document.querySelector(':root').scrollHeight + 'px');
</script>

Writing Good Fragments — Rules

  1. Use <script type="module"> for any script that calls Forge Bridge APIs. Only module scripts support top-level await. Plain <script> tags cannot use await at the top level.

  2. Always call setHeight() after rendering content. The iframe defaults to 100% height of its container, which is often too small or too large. Call setHeight(document.querySelector(':root').scrollHeight + 'px') after all DOM mutations.

  3. Add window.onerror to surface errors. Fragments run in an iframe — thrown errors are silent in the host page. Add <div id="errors" style="color:red"></div> and window.onerror = (e) => document.getElementById('errors').textContent = e.toString(); to every fragment.

  4. Apply the dark theme block in every fragment that shows UI. Atlaskit CSS won't respond to the user's dark mode unless you set the data-color-mode attribute and load the correct token stylesheet. Copy the three-line theme block from the Code Patterns section above.

  5. Check response.status before parsing JSON. A non-2xx response may return HTML or a plain text error, not JSON. Calling .json() on an error response throws and leaves the fragment in a broken loading state.

  6. Use requestJira in Jira fragments and requestConfluence in Confluence fragments. Both globals are always injected, but cross-product calls will fail because the authenticated session is product-specific. Do not call requestJira from a Confluence fragment.

  7. Do not mutate without a confirmation or a refresh button. Destructive operations (deleting links, updating fields) happen immediately. Add a visible outcome message and a "Refresh window" button that calls view.refresh() so users can confirm the effect in the host page.

  8. Max 20 fragments per instance. Exceeding the limit at creation time is enforced server-side. The admin UI shows a warning badge. Disable or delete unused fragments before adding new ones.

  9. Scope fragments to specific projects or spaces when possible. Fragments with an empty scope array ([]) appear on every issue/page in the entire instance. Use scope keys to limit visibility and reduce unnecessary API load.

  10. External API calls use fetch, not requestJira/requestConfluence. The Forge Bridge request functions only work with Atlassian REST APIs. For third-party APIs (quotable, custom services), use the browser's native fetch. No Forge authentication is injected for external calls.


Jira Cloud REST API v3

  • Get issue: GET /rest/api/3/issue/{issueIdOrKey}docs
  • Get issue changelog: GET /rest/api/3/issue/{issueIdOrKey}/changelogdocs
  • Update issue: PUT /rest/api/3/issue/{issueIdOrKey}docs
  • Delete issue link: DELETE /rest/api/3/issueLink/{issueLinkId}docs
  • Search issues (JQL): GET /rest/api/3/search?jql=...docs
  • Get current user: GET /rest/api/3/myselfdocs
  • Get project: GET /rest/api/3/project/{projectIdOrKey}docs

Confluence REST API v2

  • Get page: GET /wiki/api/v2/pages/{id}docs
  • Get page versions: GET /wiki/api/v2/pages/{id}?include-versions=true
  • Get page body (storage format): GET /wiki/api/v2/pages/{id}?body-format=storage
  • Get page body (ADF format): GET /wiki/api/v2/pages/{id}?body-format=atlas_doc_format
  • Create page: POST /wiki/api/v2/pagesdocs
  • Update page: PUT /wiki/api/v2/pages/{id}docs
  • Delete/purge page: DELETE /wiki/api/v2/pages/{id}?purge=true
  • List spaces: GET /wiki/api/v2/spacesdocs
  • Get current user: GET /wiki/rest/api/user/current
  • Get users in bulk: GET /wiki/rest/api/user/bulk?accountId=...&accountId=...

Forge Bridge APIs