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-levelawaitsupport (required for calling async Forge Bridge APIs directly). - Globals are on
window— callview,requestJira,setHeight, etc. directly without any import orwindow.prefix. - No
returnstatement — fragments render HTML; there is no output panel. Use DOM manipulation to display results. - Errors are silent by default — add
window.onerrorto 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 is100%. - 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.
| Global | Type | Description |
|---|---|---|
view | Forge view object | Access 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) => Flag | Show a notification flag in the Atlassian product UI |
router | Forge router object | Navigate to Jira or Confluence pages programmatically |
events | Forge events object | Publish and subscribe to Forge UI events |
Modal | Forge Modal object | Open Forge modal dialogs |
theme | { colorMode: 'light' | 'dark' } | Current Atlaskit color mode for dark/light theme support |
setHeight | (height: string) => void | Resize the fragment iframe (e.g. setHeight('300px')) |
fetch | Web fetch | Call any external HTTP endpoint — no authentication injected |
console | Browser console | console.log(), console.error(), etc. (visible in DevTools) |
Note:
requestJirais always injected but will fail in Confluence contexts because the current user is not authenticated against Jira. UserequestConfluencein Confluence fragments andrequestJirain 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— standardfetchRequestInitoptions (method, headers, body)- Returns — a standard
Responseobject
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— standardfetchoptions- Returns — a standard
Responseobject
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.
Modal
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>
Jira: Delete all issue links
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 = ``;
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
-
Use
<script type="module">for any script that calls Forge Bridge APIs. Only module scripts support top-levelawait. Plain<script>tags cannot useawaitat the top level. -
Always call
setHeight()after rendering content. The iframe defaults to100%height of its container, which is often too small or too large. CallsetHeight(document.querySelector(':root').scrollHeight + 'px')after all DOM mutations. -
Add
window.onerrorto surface errors. Fragments run in an iframe — thrown errors are silent in the host page. Add<div id="errors" style="color:red"></div>andwindow.onerror = (e) => document.getElementById('errors').textContent = e.toString();to every fragment. -
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-modeattribute and load the correct token stylesheet. Copy the three-line theme block from the Code Patterns section above. -
Check
response.statusbefore 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. -
Use
requestJirain Jira fragments andrequestConfluencein Confluence fragments. Both globals are always injected, but cross-product calls will fail because the authenticated session is product-specific. Do not callrequestJirafrom a Confluence fragment. -
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. -
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.
-
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. -
External API calls use
fetch, notrequestJira/requestConfluence. The Forge Bridge request functions only work with Atlassian REST APIs. For third-party APIs (quotable, custom services), use the browser's nativefetch. No Forge authentication is injected for external calls.
Useful REST API Links
Jira Cloud REST API v3
- Get issue:
GET /rest/api/3/issue/{issueIdOrKey}— docs - Get issue changelog:
GET /rest/api/3/issue/{issueIdOrKey}/changelog— docs - 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/myself— docs - 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/pages— docs - 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/spaces— docs - Get current user:
GET /wiki/rest/api/user/current - Get users in bulk:
GET /wiki/rest/api/user/bulk?accountId=...&accountId=...