How to Check description and transcript Fields in a Join Container's jsonLd
This FAQ explains how to check, for a Join widget present on a page, that each video exposes both a description and a transcript in its jsonLd. These fields are used for SEO, since Google reads them.
1. Retrieve the jsonLd
Once you have the widget’s alias and teamId, call Join’s public API. No authentication is required:
https://api.stories.studio/v3/teams/{teamId}/containers/{alias}?isPublished=true&jsonLd=true&language={language}
Example — you can simply paste the URL into a browser:
https://api.stories.studio/v3/teams/YOUR_TEAM_ID/containers/YOUR_ALIAS?isPublished=true&jsonLd=true&language=LANG
2. Where are description and transcript?
description and transcript?They are in jsonLd.itemListElement[]. Each entry has an item object, which represents one video:
{
"jsonLd": {
"itemListElement": [
{
"position": 1,
"item": {
"name": "Story name",
"description": "Video description...",
"transcript": "Audio transcript...",
"contentUrl": "https://stories.join-stories.com/.../story.mp4"
}
}
]
}
}
description→ video descriptiontranscript→ audio transcript
3. How do you know if a key is “properly filled”?
A key is OK if the key exists AND its value contains real visible text.
Gotcha: a value such as
""," "only spaces, , an empty HTML tag like<p></p>, or invisible characters counts as empty, even if the cell appears to contain “something” visually. Always check that there is actual readable text.
4. A video without a transcript — is that a problem?
transcript — is that a problem?Not always. If the story contains only images and no video with audio, the absence of a transcript is normal.
You can check this in stories[].pages[].elements.mainAsset.contentType:
contentType | Meaning | Transcript expected? |
|---|---|---|
video/... | the story is a video | ✅ yes |
image/... | the story is image-only | ❌ no |
➡️ Only report an issue for video stories without a transcript.
5. What about languages, such as en / fr?
Each language must be requested separately using the &language= parameter. The same story may exist in en and fr, or only in one of the two.
- Normal response → the language exists.
- 404 error → this language does not exist for this widget.
So you need to make one API call per language you want to check.
6. Checklist
- For each language, open the API URL.
- In
jsonLd.itemListElement[], for eachitem, check whetherdescriptionandtranscriptcontain real text. - If
transcriptis empty, check instories[]that the story is actually a video before concluding that there is an issue.
7. Example JS script
This script runs as-is in the browser console with F12 or with Node 18+, with no dependencies:
// --- Parameters to fill in ---
const teamId = "YOUR_TEAM_ID" // e.g. extracted from the widget subdomain
const alias = "YOUR_ALIAS" // e.g. data-join-widget-alias
const languages = ["LANG_1", "LANG_2"] // e.g. ["en", "fr"]
// --- Checks whether a value contains real visible text ---
function hasContent(value) {
if (typeof value !== "string") return false
const stripped = value
.replace(/<[^>]*>/g, "") // HTML tags
.replace(/&[a-z0-9#]+;/gi, "") // HTML entities ( ...)
.replace(/[-]/g, "") // invisible characters
.replace(/\s/g, "") // spaces
return stripped.length > 0
}
// --- Story media type (video / images_only / mixed) ---
function mediaKind(story) {
const pages = story?.pages ?? []
let hasVideo = false, hasImage = false
for (const p of pages) {
const ct = p?.elements?.mainAsset?.contentType ?? ""
if (ct.startsWith("video/")) hasVideo = true
else if (ct.startsWith("image/")) hasImage = true
}
if (hasVideo && hasImage) return "mixed"
if (hasVideo) return "video"
if (hasImage) return "images_only"
return "unknown"
}
// --- Extracts the story ID from contentUrl ---
function storyId(item) {
const m = (item?.contentUrl ?? "").match(
/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i,
)
return m ? m[1].toLowerCase() : ""
}
// --- Check ---
for (const lang of languages) {
const url = 'https://api.stories.studio/v3/teams/' + teamId
+ '/containers/' + alias
+ '?isPublished=true&jsonLd=true&language=' + lang
const res = await fetch(url)
if (res.status === 404) {
console.log('[' + lang + '] ❌ language missing for this widget')
continue
}
const data = await res.json()
const stories = new Map((data.stories ?? []).map((s) => [s._id?.toLowerCase(), s]))
const items = data.jsonLd?.itemListElement ?? []
for (const li of items) {
const item = li.item ?? {}
const id = storyId(item)
const kind = mediaKind(stories.get(id))
const hasDesc = hasContent(item.description)
const hasTransc = hasContent(item.transcript)
// missing transcript = issue ONLY if the story is a video
const transcProblem = !hasTransc && kind === "video"
const descIcon = hasDesc ? "✅" : "❌"
const transcIcon = hasTransc ? "✅" : (transcProblem ? "❌ MISSING" : "— (normal)")
console.log(
'[' + lang + '] pos=' + li.position + ' "' + (item.name ?? "") + '" '
+ '(' + kind + ') description=' + descIcon + ' transcript=' + transcIcon
)
}
}
Example output:
[LANG_1] pos=1 "Story name" (video) description=✅ transcript=✅
[LANG_1] pos=2 "Another story" (images_only) description=✅ transcript=— (normal)
[LANG_2] ❌ language missing for this widget
Updated about 15 hours ago
