From c0127be365b48039ad3de356ed86b93ba54ee2bc Mon Sep 17 00:00:00 2001 From: James Johnson Date: Fri, 3 Oct 2025 16:02:46 +1000 Subject: [PATCH 01/54] TCLOUD-4780: Created workflows for automatic previews --- .github/workflows/preview_create.yml | 68 +++++++++++++++++++ .github/workflows/preview_delete.yml | 49 +++++++++++++ .../workflows/scripts/branch-to-subdomain.js | 11 +++ 3 files changed, 128 insertions(+) create mode 100644 .github/workflows/preview_create.yml create mode 100644 .github/workflows/preview_delete.yml create mode 100644 .github/workflows/scripts/branch-to-subdomain.js diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml new file mode 100644 index 0000000000..abfc9c6260 --- /dev/null +++ b/.github/workflows/preview_create.yml @@ -0,0 +1,68 @@ +name: Preview Create/Update + +on: + create: + branches-ignore: + - 'main' + - 'develop' + - 'tinymce/**' + - 'release/**' + - 'archived/**' + + push: + branches-ignore: + - 'main' + - 'develop' + - 'tinymce/**' + - 'release/**' + - 'archived/**' + +jobs: + + build: + name: Update Docs Preview + + runs-on: ubuntu-latest + + defaults: + run: + shell: bash + working-directory: ./.github/workflows/scripts + + steps: + - name: Checkout branch + uses: actions/checkout@v5 + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + cache: 'yarn' + node-version: 24 + + - name: Install dependencies + run: yarn install + + - name: Build Website + run: yarn antora ./antora-playbook.yml + + - name: Rename sitemap.xml + run: | + mv ./build/site/sitemap.xml ./build/site/antora-sitemap.xml + + - name: Convert branch into subdomain + id: get-subdomain + run: | + node ./branch-to-subdomain.js | { read s; echo "subdomain=$s" } >> $GITHUB_OUTPUT + + - name: configure aws credentials + uses: aws-actions/configure-aws-credentials@v5.0.0 + with: + role-to-assume: arn:aws:iam::327995277200:role/staging-docs-preview-update + role-session-name: docs-preview-update + aws-region: us-east-2 + + - name: Upload website preview to S3 + run: | + aws s3 sync --delete ./build/site s3://tiny-cloud-antora-docs-preview/${SUBDOMAIN}/docs + env: + SUBDOMAIN: ${{ steps.get-subdomain.outputs.subdomain }} diff --git a/.github/workflows/preview_delete.yml b/.github/workflows/preview_delete.yml new file mode 100644 index 0000000000..88d18ec874 --- /dev/null +++ b/.github/workflows/preview_delete.yml @@ -0,0 +1,49 @@ +name: Preview Delete + +on: + delete: + branches-ignore: + - 'main' + - 'develop' + - 'tinymce/**' + - 'release/**' + - 'archived/**' + +jobs: + cleanup: + name: Cleanup Docs Preview + + runs-on: ubuntu-latest + + defaults: + run: + shell: bash + working-directory: ./.github/workflows/scripts + + steps: + - name: Checkout branch + uses: actions/checkout@v5 + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + cache: 'yarn' + node-version: 24 + + - name: Convert branch into subdomain + id: get-subdomain + run: | + node ./branch-to-subdomain.js | { read s; echo "subdomain=$s" } >> $GITHUB_OUTPUT + + - name: configure aws credentials + uses: aws-actions/configure-aws-credentials@v5.0.0 + with: + role-to-assume: arn:aws:iam::327995277200:role/staging-docs-preview-update + role-session-name: docs-preview-delete + aws-region: us-east-2 + + - name: Remove website preview from S3 + run: | + aws s3 rm s3://tiny-cloud-antora-docs-preview/${SUBDOMAIN}/docs + env: + SUBDOMAIN: ${{ steps.get-subdomain.outputs.subdomain }} diff --git a/.github/workflows/scripts/branch-to-subdomain.js b/.github/workflows/scripts/branch-to-subdomain.js new file mode 100644 index 0000000000..bf252a9996 --- /dev/null +++ b/.github/workflows/scripts/branch-to-subdomain.js @@ -0,0 +1,11 @@ + +const branch = process.argv[2] ?? process.env.GITHUB_REF_NAME; +const subdomain = ( + branch + .toLowerCase() // convert to lowercase + .replace(/[/\s]/g, '-') // convert spaces and slashes to dash + .replace(/[^0-9a-z-]/g, '') // remove non alphanumeric+dash + .replace(/-$/g, '') // remove trailing dash + .slice(0, 63) // limit to 63 characters +); +console.log(subdomain); \ No newline at end of file From ff18535664807bc71a3384116de05b578a234210 Mon Sep 17 00:00:00 2001 From: James Johnson Date: Fri, 3 Oct 2025 16:11:55 +1000 Subject: [PATCH 02/54] TCLOUD-4780: Set working directory only for the one step --- .github/workflows/preview_create.yml | 2 +- .github/workflows/preview_delete.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index abfc9c6260..c54fdf0a79 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -27,7 +27,6 @@ jobs: defaults: run: shell: bash - working-directory: ./.github/workflows/scripts steps: - name: Checkout branch @@ -51,6 +50,7 @@ jobs: - name: Convert branch into subdomain id: get-subdomain + working-directory: ./.github/workflows/scripts run: | node ./branch-to-subdomain.js | { read s; echo "subdomain=$s" } >> $GITHUB_OUTPUT diff --git a/.github/workflows/preview_delete.yml b/.github/workflows/preview_delete.yml index 88d18ec874..ff16e4ed54 100644 --- a/.github/workflows/preview_delete.yml +++ b/.github/workflows/preview_delete.yml @@ -18,7 +18,6 @@ jobs: defaults: run: shell: bash - working-directory: ./.github/workflows/scripts steps: - name: Checkout branch @@ -32,6 +31,7 @@ jobs: - name: Convert branch into subdomain id: get-subdomain + working-directory: ./.github/workflows/scripts run: | node ./branch-to-subdomain.js | { read s; echo "subdomain=$s" } >> $GITHUB_OUTPUT From 3bc12d6d632267ba51070830f81681eebda2babf Mon Sep 17 00:00:00 2001 From: James Johnson Date: Fri, 3 Oct 2025 16:18:43 +1000 Subject: [PATCH 03/54] TCLOUD-4780: Replace shell code that failed with nodejs --- .github/workflows/preview_create.yml | 2 +- .github/workflows/preview_delete.yml | 2 +- .github/workflows/scripts/branch-to-subdomain.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index c54fdf0a79..b383dcb404 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -52,7 +52,7 @@ jobs: id: get-subdomain working-directory: ./.github/workflows/scripts run: | - node ./branch-to-subdomain.js | { read s; echo "subdomain=$s" } >> $GITHUB_OUTPUT + node ./branch-to-subdomain.js >> $GITHUB_OUTPUT - name: configure aws credentials uses: aws-actions/configure-aws-credentials@v5.0.0 diff --git a/.github/workflows/preview_delete.yml b/.github/workflows/preview_delete.yml index ff16e4ed54..631110bb83 100644 --- a/.github/workflows/preview_delete.yml +++ b/.github/workflows/preview_delete.yml @@ -33,7 +33,7 @@ jobs: id: get-subdomain working-directory: ./.github/workflows/scripts run: | - node ./branch-to-subdomain.js | { read s; echo "subdomain=$s" } >> $GITHUB_OUTPUT + node ./branch-to-subdomain.js >> $GITHUB_OUTPUT - name: configure aws credentials uses: aws-actions/configure-aws-credentials@v5.0.0 diff --git a/.github/workflows/scripts/branch-to-subdomain.js b/.github/workflows/scripts/branch-to-subdomain.js index bf252a9996..25c13b188f 100644 --- a/.github/workflows/scripts/branch-to-subdomain.js +++ b/.github/workflows/scripts/branch-to-subdomain.js @@ -8,4 +8,4 @@ const subdomain = ( .replace(/-$/g, '') // remove trailing dash .slice(0, 63) // limit to 63 characters ); -console.log(subdomain); \ No newline at end of file +console.log(`subdomain=${subdomain}`); \ No newline at end of file From a11cada1b2334f021f7d557da73696b4d80defe7 Mon Sep 17 00:00:00 2001 From: James Johnson Date: Fri, 3 Oct 2025 16:25:29 +1000 Subject: [PATCH 04/54] TCLOUD-4780: Setting permissions for id-token to write --- .github/workflows/preview_create.yml | 4 ++++ .github/workflows/preview_delete.yml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index b383dcb404..8d3b922e59 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -17,6 +17,10 @@ on: - 'release/**' - 'archived/**' +# Need ID token write permission to use OIDC +permissions: + id-token: write + jobs: build: diff --git a/.github/workflows/preview_delete.yml b/.github/workflows/preview_delete.yml index 631110bb83..74278636df 100644 --- a/.github/workflows/preview_delete.yml +++ b/.github/workflows/preview_delete.yml @@ -9,6 +9,10 @@ on: - 'release/**' - 'archived/**' +# Need ID token write permission to use OIDC +permissions: + id-token: write + jobs: cleanup: name: Cleanup Docs Preview From 71fe97ff4ed80d2efc376d87098347552449e43f Mon Sep 17 00:00:00 2001 From: James Johnson Date: Tue, 7 Oct 2025 12:23:09 +1000 Subject: [PATCH 05/54] TCLOUD-4780: Switch to using PR number instead of branch name --- .github/workflows/preview_create.yml | 26 +++++++------------ .github/workflows/preview_delete.yml | 17 +++++------- .../workflows/scripts/branch-to-subdomain.js | 11 -------- 3 files changed, 16 insertions(+), 38 deletions(-) delete mode 100644 .github/workflows/scripts/branch-to-subdomain.js diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index 8d3b922e59..bb654b0fdb 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -1,7 +1,11 @@ name: Preview Create/Update on: - create: + pull_request: + types: + - opened + - reopened + - synchronize branches-ignore: - 'main' - 'develop' @@ -9,18 +13,14 @@ on: - 'release/**' - 'archived/**' - push: - branches-ignore: - - 'main' - - 'develop' - - 'tinymce/**' - - 'release/**' - - 'archived/**' # Need ID token write permission to use OIDC permissions: id-token: write +env: + PR_NUMBER: ${{ github.event.number }} + jobs: build: @@ -52,12 +52,6 @@ jobs: run: | mv ./build/site/sitemap.xml ./build/site/antora-sitemap.xml - - name: Convert branch into subdomain - id: get-subdomain - working-directory: ./.github/workflows/scripts - run: | - node ./branch-to-subdomain.js >> $GITHUB_OUTPUT - - name: configure aws credentials uses: aws-actions/configure-aws-credentials@v5.0.0 with: @@ -67,6 +61,4 @@ jobs: - name: Upload website preview to S3 run: | - aws s3 sync --delete ./build/site s3://tiny-cloud-antora-docs-preview/${SUBDOMAIN}/docs - env: - SUBDOMAIN: ${{ steps.get-subdomain.outputs.subdomain }} + aws s3 sync --delete ./build/site s3://tiny-cloud-antora-docs-preview/pr${PR_NUMBER}/docs diff --git a/.github/workflows/preview_delete.yml b/.github/workflows/preview_delete.yml index 74278636df..d4e6cb8417 100644 --- a/.github/workflows/preview_delete.yml +++ b/.github/workflows/preview_delete.yml @@ -1,7 +1,9 @@ name: Preview Delete on: - delete: + pull_request: + types: + - closed branches-ignore: - 'main' - 'develop' @@ -13,6 +15,9 @@ on: permissions: id-token: write +env: + PR_NUMBER: ${{ github.event.number }} + jobs: cleanup: name: Cleanup Docs Preview @@ -33,12 +38,6 @@ jobs: cache: 'yarn' node-version: 24 - - name: Convert branch into subdomain - id: get-subdomain - working-directory: ./.github/workflows/scripts - run: | - node ./branch-to-subdomain.js >> $GITHUB_OUTPUT - - name: configure aws credentials uses: aws-actions/configure-aws-credentials@v5.0.0 with: @@ -48,6 +47,4 @@ jobs: - name: Remove website preview from S3 run: | - aws s3 rm s3://tiny-cloud-antora-docs-preview/${SUBDOMAIN}/docs - env: - SUBDOMAIN: ${{ steps.get-subdomain.outputs.subdomain }} + aws s3 rm s3://tiny-cloud-antora-docs-preview/pr${PR_NUMBER} diff --git a/.github/workflows/scripts/branch-to-subdomain.js b/.github/workflows/scripts/branch-to-subdomain.js deleted file mode 100644 index 25c13b188f..0000000000 --- a/.github/workflows/scripts/branch-to-subdomain.js +++ /dev/null @@ -1,11 +0,0 @@ - -const branch = process.argv[2] ?? process.env.GITHUB_REF_NAME; -const subdomain = ( - branch - .toLowerCase() // convert to lowercase - .replace(/[/\s]/g, '-') // convert spaces and slashes to dash - .replace(/[^0-9a-z-]/g, '') // remove non alphanumeric+dash - .replace(/-$/g, '') // remove trailing dash - .slice(0, 63) // limit to 63 characters -); -console.log(`subdomain=${subdomain}`); \ No newline at end of file From e1c8f85169304bd95e13fa5519572ac4e2d50059 Mon Sep 17 00:00:00 2001 From: James Johnson Date: Tue, 7 Oct 2025 12:34:03 +1000 Subject: [PATCH 06/54] TCLOUD-4780: Removed branch restriction --- .github/workflows/preview_create.yml | 6 ------ .github/workflows/preview_delete.yml | 6 ------ 2 files changed, 12 deletions(-) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index bb654b0fdb..b75e601358 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -6,12 +6,6 @@ on: - opened - reopened - synchronize - branches-ignore: - - 'main' - - 'develop' - - 'tinymce/**' - - 'release/**' - - 'archived/**' # Need ID token write permission to use OIDC diff --git a/.github/workflows/preview_delete.yml b/.github/workflows/preview_delete.yml index d4e6cb8417..ed1c5822df 100644 --- a/.github/workflows/preview_delete.yml +++ b/.github/workflows/preview_delete.yml @@ -4,12 +4,6 @@ on: pull_request: types: - closed - branches-ignore: - - 'main' - - 'develop' - - 'tinymce/**' - - 'release/**' - - 'archived/**' # Need ID token write permission to use OIDC permissions: From 5706df1a6d36ccb351da27cf4140b91c010fda07 Mon Sep 17 00:00:00 2001 From: James Johnson Date: Fri, 17 Oct 2025 15:17:40 +1000 Subject: [PATCH 07/54] TCLOUD-4780: Avoid creating the docs subdirectory --- .github/workflows/preview_create.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index b75e601358..92f00a5dfe 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -55,4 +55,4 @@ jobs: - name: Upload website preview to S3 run: | - aws s3 sync --delete ./build/site s3://tiny-cloud-antora-docs-preview/pr${PR_NUMBER}/docs + aws s3 sync --delete ./build/site s3://tiny-cloud-antora-docs-preview/pr${PR_NUMBER} From 3a8e7292fc218f9f9d9a41768f9d8da530bcfa8b Mon Sep 17 00:00:00 2001 From: James Johnson Date: Tue, 21 Oct 2025 11:13:32 +1000 Subject: [PATCH 08/54] TCLOUD-4780: Re-add the docs folder segment for redirects Some older redirects go from the docs level (for example docs-4x) so the folder is needed to properly model the redirects. --- .github/workflows/preview_create.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index 92f00a5dfe..016558831d 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -53,6 +53,6 @@ jobs: role-session-name: docs-preview-update aws-region: us-east-2 - - name: Upload website preview to S3 + - name: Upload website preview to S3 # put in the docs subfolder as we rely on that path in redirects run: | - aws s3 sync --delete ./build/site s3://tiny-cloud-antora-docs-preview/pr${PR_NUMBER} + aws s3 sync --delete ./build/site s3://tiny-cloud-antora-docs-preview/pr${PR_NUMBER}/docs From 07c1510ea9ebe316e33936f8f36cdf991faa1110 Mon Sep 17 00:00:00 2001 From: James Johnson Date: Fri, 31 Oct 2025 16:03:13 +1000 Subject: [PATCH 09/54] TCLOUD-4780: Generating redirects --- .../workflows/scripts/generate_redirects.mjs | 242 ++ redirects.json | 3361 +++++++++++++++++ 2 files changed, 3603 insertions(+) create mode 100644 .github/workflows/scripts/generate_redirects.mjs create mode 100644 redirects.json diff --git a/.github/workflows/scripts/generate_redirects.mjs b/.github/workflows/scripts/generate_redirects.mjs new file mode 100644 index 0000000000..96c7a70baa --- /dev/null +++ b/.github/workflows/scripts/generate_redirects.mjs @@ -0,0 +1,242 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import child_process from 'node:child_process'; + + +/** + * Check if an S3 object exists + */ +function checkS3ObjectExists(_dryRun, _bucket, _prefix, subPath) { + // it's too slow to talk to s3, so just check the local files we just uploaded... + if (subPath.startsWith('docs/')) { + return fs.existsSync(path.join(import.meta.dirname, '../../../build/site', subPath.slice('docs/'.length))); + } else { + return false; + } +} + +const metadataArgs = (metadata) => { + // Build metadata string in the format key1=value1,key2=value2 + const metadataString = Object.entries(metadata) + .map(([key, value]) => `${key}=${value}`) + .join(','); + + return metadataString ? ['--metadata', metadataString] : []; +} + +/** + * Copy existing S3 object to itself with new metadata + */ +function copyS3ObjectWithMetadata(dryRun, bucket, prefix, subPath, metadata) { + const fullPath = `${prefix}/${subPath}`; + const cmd = [ + 'aws', 's3api', 'copy-object', + '--bucket', bucket, + '--copy-source', `${bucket}/${fullPath}`, + '--key', subPath, + '--metadata-directive', 'REPLACE', + '--content-type', 'text/html', + ...metadataArgs(metadata) + ]; + + console.log(`Updating existing S3 object with metadata: ${fullPath}`); + console.log(`Command: ${cmd.join(' ')}`); + + const result = dryRun ? { status: 0 } : child_process.spawnSync('aws', cmd.slice(1), { + stdio: 'inherit', + encoding: 'utf8' + }); + + if (result.error) { + console.error(`Error copying S3 object ${fullPath}:`, result.error); + return false; + } else if (result.status !== 0) { + console.error(`AWS CLI copy command failed for ${fullPath} with exit code:`, result.status); + return false; + } else { + console.log(`Successfully updated S3 object metadata: ${fullPath}`); + return true; + } +} + +/** + * Create new S3 object with generated content and metadata + */ +function createNewS3Object(dryRun, bucket, prefix, subPath, metadata, newFileTemplate) { + const fullPath = `${prefix}/${subPath}`; + // AWS CLI command to put object with metadata + const cmd = [ + 'aws', 's3api', 'put-object', + '--bucket', bucket, + '--key', fullPath, + '--body', newFileTemplate, + '--content-type', 'text/html', + ...metadataArgs(metadata) + ]; + + console.log(`Creating new S3 object: ${fullPath}`); + console.log(`Command: ${cmd.join(' ')}`); + + const result = dryRun ? { status: 0 } : child_process.spawnSync('aws', cmd.slice(1), { + stdio: 'inherit', + encoding: 'utf8' + }); + + if (result.error) { + console.error(`Error creating S3 object ${fullPath}:`, result.error); + return false; + } else if (result.status !== 0) { + console.error(`AWS CLI command failed for ${fullPath} with exit code:`, result.status); + return false; + } else { + console.log(`Successfully created S3 object: ${fullPath}`); + return true; + } +} + +/** + * Create or update S3 object with metadata, reusing existing content if available + */ +function createOrUpdateS3Object(dryRun, bucket, prefix, subPath, metadata, newFileTemplate) { + console.log(`\nProcessing: ${subPath}`); + + // Check if object already exists + if (checkS3ObjectExists(dryRun, bucket, prefix, subPath)) { + console.log(`Object exists, updating metadata...`); + return copyS3ObjectWithMetadata(dryRun, bucket, prefix, subPath, metadata); + } else { + console.log(`Object doesn't exist, creating new one...`); + return createNewS3Object(dryRun, bucket, prefix, subPath, metadata, newFileTemplate); + } +} + +/** + * Generate S3 objects for all redirects + */ +function generateRedirectObjects(dryRun, bucket, prefix, redirectsByLocation) { + console.log(`Processing ${redirectsByLocation.size} unique locations`); + + let successCount = 0; + let errorCount = 0; + + // Create empty index.html content for the redirect + const htmlContent = ` + + + + Redirecting... + + +

Redirecting...

+ +`; + + // Write temporary file + const newFileTemplate = `/tmp/redirect-${Date.now()}.html`; + fs.writeFileSync(newFileTemplate, htmlContent); + + try { + for (const [location, locationRedirects] of redirectsByLocation) { + // Create S3 object path by appending index.html to location + const locationIndexHtml = location.endsWith('/') + ? `${location}index.html` + : `${location}/index.html`; + + // Remove leading slash from location + const subPath = locationIndexHtml.startsWith('/') ? locationIndexHtml.slice(1) : locationIndexHtml; + + // Build metadata headers + const metadata = {}; + + locationRedirects.forEach((redirect, index) => { + const i = index + 1; // 1-based indexing as requested + + // Add redirect location header + metadata[`redirect-location-${i}`] = redirect.redirect; + + // Add pattern header if it exists + if (redirect.pattern !== undefined) { + metadata[`redirect-pattern-${i}`] = redirect.pattern; + } + }); + + // Create or update the S3 object + if (createOrUpdateS3Object(dryRun, bucket, prefix, subPath, metadata, newFileTemplate)) { + successCount++; + } else { + errorCount++; + } + } + } finally { + // Clean up temporary file + if (fs.existsSync(newFileTemplate)) { + fs.unlinkSync(newFileTemplate); + } + } + + console.log(`\nSummary:`); + console.log(`Successfully processed: ${successCount} objects`); + console.log(`Errors: ${errorCount} objects`); +} + + +const usage = () => ` +generate-redirects [--dry-run] + Generate redirects in s3. + + Options: + --dry-run only output the commands that will be run +` + +const main = async () => { + + const args = process.argv.slice(2); + const dryRun = (() => { + const idx = args.findIndex((arg) => arg === '--dry-run'); + if (idx !== -1) { + args.splice(idx, 1); + return true; + } + return false; + })(); + if (args.length !== 2) { + return Promise.reject(`Expected 2 values, got ${args.length}`); + } + const [bucket, prefix] = args; + if (!/^[a-z0-9][a-z0-9\.-]{1,61}[a-z0-9]$/.test(bucket) || + /\.\./.test(bucket) || /^\d+\.\d+\.\d+\.\d+$/.test(bucket) || + /^xn--/.test(bucket) || /^sthree-/.test(bucket) || /^amzn-s3-demo-/.test(bucket) || + /-s3alias$/.test(bucket) || /--ol-s3$/.test(bucket) || /\.mrap$/.test(bucket) || + /--x-s3$/.test(bucket) || /--table-s3$/.test(bucket)) { + return Promise.reject(`Invalid bucket name, got ${bucket}`); + } + + if (!/^[a-z0-9\.-]+$/.test(prefix)) { + return Promise.reject(`Invalid prefix, got ${prefix}`); + } + + const redirects = JSON.parse(fs.readFileSync(path.join(import.meta.dirname, '../../../redirects.json'), 'utf-8')); + + // Group redirects by location to handle multiple redirects for the same location + const redirectsByLocation = new Map(); + + redirects.forEach((redirect) => { + const location = redirect.location; + if (!redirectsByLocation.has(location)) { + redirectsByLocation.set(location, []); + } + redirectsByLocation.get(location).push(redirect); + }); + + // Generate all redirect objects + generateRedirectObjects(dryRun, bucket, prefix, redirectsByLocation); + + console.log('Redirect object generation completed.'); +}; + + +main().catch((err) => { + console.error(err); + console.error(usage()); + process.exit(1); +}) \ No newline at end of file diff --git a/redirects.json b/redirects.json new file mode 100644 index 0000000000..d015c49a62 --- /dev/null +++ b/redirects.json @@ -0,0 +1,3361 @@ +[ + { + "location": "/", + "pattern": "^/docs%-beta(.*)$", + "redirect": "/docs$1" + }, + { + "location": "/", + "pattern": "^/docs%-preview(.*)$", + "redirect": "/docs$1" + }, + { + "location": "/docs-3x/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs-4x/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/advanced/", + "redirect": "/docs/tinymce/latest/how-to-guides" + }, + { + "location": "/docs/advanced/accessibility/", + "redirect": "/docs/tinymce/latest/tinymce-and-screenreaders/" + }, + { + "location": "/docs/advanced/annotations/", + "redirect": "/docs/tinymce/latest/annotations/" + }, + { + "location": "/docs/advanced/available-menu-items/", + "redirect": "/docs/tinymce/latest/available-menu-items/" + }, + { + "location": "/docs/advanced/available-toolbar-buttons/", + "redirect": "/docs/tinymce/latest/available-toolbar-buttons/" + }, + { + "location": "/docs/advanced/boilerplate-content-css/", + "redirect": "/docs/tinymce/latest/editor-content-css/" + }, + { + "location": "/docs/advanced/cdn-details/", + "redirect": "/docs/tinymce/latest/tinydrive-introduction/#securityperformance" + }, + { + "location": "/docs/advanced/configuring-comments-callbacks/", + "redirect": "/docs/tinymce/latest/comments-callback-mode/" + }, + { + "location": "/docs/advanced/contributing-docs/", + "redirect": "https://github.com/tinymce/tinymce-docs/blob/staging/docs-7/CONTRIBUTING.md" + }, + { + "location": "/docs/advanced/contributing-to-open-source/", + "redirect": "https://github.com/tinymce/tinymce/blob/main/CONTRIBUTING.md" + }, + { + "location": "/docs/advanced/creating-a-custom-button/", + "redirect": "/docs/tinymce/latest/custom-toolbarbuttons/" + }, + { + "location": "/docs/advanced/creating-a-plugin/", + "redirect": "/docs/tinymce/latest/creating-a-plugin/" + }, + { + "location": "/docs/advanced/creating-a-sidebar/", + "redirect": "/docs/tinymce/latest/customsidebar/" + }, + { + "location": "/docs/advanced/creating-a-skin/", + "redirect": "/docs/tinymce/latest/creating-a-skin/" + }, + { + "location": "/docs/advanced/creating-an-icon-pack/", + "redirect": "/docs/tinymce/latest/creating-an-icon-pack/" + }, + { + "location": "/docs/advanced/creating-custom-dialogs/", + "redirect": "/docs/tinymce/latest/dialog/" + }, + { + "location": "/docs/advanced/creating-custom-notifications/", + "redirect": "/docs/tinymce/latest/creating-custom-notifications/" + }, + { + "location": "/docs/advanced/editor-command-identifiers/", + "redirect": "/docs/tinymce/latest/editor-command-identifiers/" + }, + { + "location": "/docs/advanced/editor-context-menu-identifiers/", + "redirect": "/docs/tinymce/latest/editor-context-menu-identifiers/" + }, + { + "location": "/docs/advanced/editor-icon-identifiers/", + "redirect": "/docs/tinymce/latest/editor-icon-identifiers/" + }, + { + "location": "/docs/advanced/events/", + "redirect": "/docs/tinymce/latest/events/" + }, + { + "location": "/docs/advanced/generate-rsa-key-pairs/", + "redirect": "/docs/tinymce/latest/generate-rsa-key-pairs/" + }, + { + "location": "/docs/advanced/handle-async-image-uploads/", + "redirect": "/docs/tinymce/latest/upload-images/" + }, + { + "location": "/docs/advanced/keyboard-shortcuts/", + "redirect": "/docs/tinymce/latest/keyboard-shortcuts/" + }, + { + "location": "/docs/advanced/php-upload-handler/", + "redirect": "/docs/tinymce/latest/php-upload-handler/" + }, + { + "location": "/docs/advanced/plugin-reference/", + "redirect": "/docs/tinymce/latest/plugins/" + }, + { + "location": "/docs/advanced/security/", + "redirect": "/docs/tinymce/latest/security/" + }, + { + "location": "/docs/advanced/usage-with-module-loaders/", + "redirect": "/docs/tinymce/latest/introduction-to-bundling-tinymce/" + }, + { + "location": "/docs/advanced/usage-with-module-loaders/browserify/", + "redirect": "/docs/tinymce/latest/browserify/" + }, + { + "location": "/docs/advanced/usage-with-module-loaders/browserify/browserify_cjs_download/", + "redirect": "/docs/tinymce/latest/browserify-cjs-download/" + }, + { + "location": "/docs/advanced/usage-with-module-loaders/browserify/browserify_cjs_npm/", + "redirect": "/docs/tinymce/latest/browserify-cjs-npm/" + }, + { + "location": "/docs/advanced/usage-with-module-loaders/introduction_to_bundling_tinymce/", + "redirect": "/docs/tinymce/latest/introduction-to-bundling-tinymce/" + }, + { + "location": "/docs/advanced/usage-with-module-loaders/reference/", + "redirect": "/docs/tinymce/latest/bundling-reference/" + }, + { + "location": "/docs/advanced/usage-with-module-loaders/reference/content-css/", + "redirect": "/docs/tinymce/latest/bundling-content-css/" + }, + { + "location": "/docs/advanced/usage-with-module-loaders/reference/icons/", + "redirect": "/docs/tinymce/latest/bundling-icons/" + }, + { + "location": "/docs/advanced/usage-with-module-loaders/reference/localization/", + "redirect": "/docs/tinymce/latest/bundling-localization/" + }, + { + "location": "/docs/advanced/usage-with-module-loaders/reference/plugins/", + "redirect": "/docs/tinymce/latest/bundling-plugins/" + }, + { + "location": "/docs/advanced/usage-with-module-loaders/reference/skins/", + "redirect": "/docs/tinymce/latest/bundling-skins/" + }, + { + "location": "/docs/advanced/usage-with-module-loaders/reference/themes/", + "redirect": "/docs/tinymce/latest/bundling-themes/" + }, + { + "location": "/docs/advanced/usage-with-module-loaders/rollup/", + "redirect": "/docs/tinymce/latest/rollup-es6-npm/" + }, + { + "location": "/docs/advanced/usage-with-module-loaders/rollup/rollup_es6_download/", + "redirect": "/docs/tinymce/latest/rollup-es6-download/" + }, + { + "location": "/docs/advanced/usage-with-module-loaders/rollup/rollup_es6_npm/", + "redirect": "/docs/tinymce/latest/rollup-es6-npm/" + }, + { + "location": "/docs/advanced/usage-with-module-loaders/webpack/", + "redirect": "/docs/tinymce/latest/webpack-es6-npm/" + }, + { + "location": "/docs/advanced/usage-with-module-loaders/webpack/webpack_cjs_download/", + "redirect": "/docs/tinymce/latest/webpack-cjs-download/" + }, + { + "location": "/docs/advanced/usage-with-module-loaders/webpack/webpack_cjs_npm/", + "redirect": "/docs/tinymce/latest/webpack-cjs-npm/" + }, + { + "location": "/docs/advanced/usage-with-module-loaders/webpack/webpack_es6_download/", + "redirect": "/docs/tinymce/latest/webpack-es6-download/" + }, + { + "location": "/docs/advanced/usage-with-module-loaders/webpack/webpack_es6_npm/", + "redirect": "/docs/tinymce/latest/webpack-es6-npm/" + }, + { + "location": "/docs/advanced/yeoman-generator/", + "redirect": "/docs/tinymce/latest/yeoman-generator/" + }, + { + "location": "/docs/api-reference/core/editor/editor/", + "redirect": "/docs/" + }, + { + "location": "/docs/api/", + "redirect": "/docs/tinymce/latest/apis/tinymce.root/" + }, + { + "location": "/docs/api/class/tinymce/", + "redirect": "/docs/tinymce/latest/apis/tinymce.root/" + }, + { + "location": "/docs/api/tinymce-react/", + "redirect": "/docs/tinymce/latest/react-ref/" + }, + { + "location": "/docs/api/tinymce.dom/tinymce.dom.bookmarkmanager/", + "redirect": "/docs/tinymce/latest/apis/tinymce.dom.bookmarkmanager/" + }, + { + "location": "/docs/api/tinymce.dom/tinymce.dom.controlselection/", + "redirect": "/docs/tinymce/latest/apis/tinymce.dom.selection/" + }, + { + "location": "/docs/api/tinymce.dom/tinymce.dom.domquery/", + "redirect": "/docs/tinymce/latest/migration-from-5x/#previously-deprecated-items-now-removed" + }, + { + "location": "/docs/api/tinymce.dom/tinymce.dom.domutils/", + "redirect": "/docs/tinymce/latest/apis/tinymce.dom.domutils/" + }, + { + "location": "/docs/api/tinymce.dom/tinymce.dom.eventutils/", + "redirect": "/docs/tinymce/latest/apis/tinymce.dom.eventutils/" + }, + { + "location": "/docs/api/tinymce.dom/tinymce.dom.rangeutils/", + "redirect": "/docs/tinymce/latest/apis/tinymce.dom.rangeutils/" + }, + { + "location": "/docs/api/tinymce.dom/tinymce.dom.scriptloader/", + "redirect": "/docs/tinymce/latest/apis/tinymce.dom.scriptloader/" + }, + { + "location": "/docs/api/tinymce.dom/tinymce.dom.selection/", + "redirect": "/docs/tinymce/latest/apis/tinymce.dom.selection/" + }, + { + "location": "/docs/api/tinymce.dom/tinymce.dom.serializer/", + "redirect": "/docs/tinymce/latest/apis/tinymce.dom.serializer/" + }, + { + "location": "/docs/api/tinymce.dom/tinymce.dom.stylesheetloader/", + "redirect": "/docs/tinymce/latest/apis/tinymce.dom.stylesheetloader/" + }, + { + "location": "/docs/api/tinymce.dom/tinymce.dom.textseeker/", + "redirect": "/docs/tinymce/latest/apis/tinymce.dom.textseeker/" + }, + { + "location": "/docs/api/tinymce.dom/tinymce.dom.treewalker/", + "redirect": "/docs/tinymce/latest/apis/tinymce.dom.treewalker/" + }, + { + "location": "/docs/api/tinymce.editor.ui/tinymce.editor.ui.registry/", + "redirect": "/docs/tinymce/latest/apis/tinymce.editor.ui.registry/" + }, + { + "location": "/docs/api/tinymce.editor.ui/tinymce.editor.ui.ui/", + "redirect": "/docs/tinymce/latest/apis/tinymce.editor.ui.ui/" + }, + { + "location": "/docs/api/tinymce.editor/", + "redirect": "/docs/" + }, + { + "location": "/docs/api/tinymce.geom/tinymce.geom.rect/", + "redirect": "/docs/tinymce/latest/apis/tinymce.geom.rect/" + }, + { + "location": "/docs/api/tinymce.html/tinymce.html.domparser/", + "redirect": "/docs/tinymce/latest/apis/tinymce.html.domparser/" + }, + { + "location": "/docs/api/tinymce.html/tinymce.html.entities/", + "redirect": "/docs/tinymce/latest/apis/tinymce.html.entities/" + }, + { + "location": "/docs/api/tinymce.html/tinymce.html.node/", + "redirect": "/docs/tinymce/latest/apis/tinymce.html.node/" + }, + { + "location": "/docs/api/tinymce.html/tinymce.html.saxparser/", + "redirect": "/docs/tinymce/latest/6.0-release-notes-core-changes/#changed-plugins-media" + }, + { + "location": "/docs/api/tinymce.html/tinymce.html.schema/", + "redirect": "/docs/tinymce/latest/apis/tinymce.html.schema/" + }, + { + "location": "/docs/api/tinymce.html/tinymce.html.serializer/", + "redirect": "/docs/tinymce/latest/apis/tinymce.html.serializer/" + }, + { + "location": "/docs/api/tinymce.html/tinymce.html.styles/", + "redirect": "/docs/tinymce/latest/apis/tinymce.html.styles/" + }, + { + "location": "/docs/api/tinymce.html/tinymce.html.writer/", + "redirect": "/docs/tinymce/latest/apis/tinymce.html.writer/" + }, + { + "location": "/docs/api/tinymce.ui/tinymce.ui.browsebutton/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/api/tinymce.ui/tinymce.ui.button/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/api/tinymce.ui/tinymce.ui.buttongroup/", + "redirect": "/docs/" + }, + { + "location": "/docs/api/tinymce.ui/tinymce.ui.checkbox/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/api/tinymce.ui/tinymce.ui.collection/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/api/tinymce.ui/tinymce.ui.colorpicker/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/api/tinymce.ui/tinymce.ui.control/", + "redirect": "/docs/" + }, + { + "location": "/docs/api/tinymce.ui/tinymce.ui.draghelper/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/api/tinymce.ui/tinymce.ui.factory/", + "redirect": "/docs/" + }, + { + "location": "/docs/api/tinymce.ui/tinymce.ui.filepicker/", + "redirect": "/docs/" + }, + { + "location": "/docs/api/tinymce.ui/tinymce.ui.flexlayout/", + "redirect": "/docs/" + }, + { + "location": "/docs/api/tinymce.ui/tinymce.ui.form/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/api/tinymce.ui/tinymce.ui.formitem/", + "redirect": "/docs/" + }, + { + "location": "/docs/api/tinymce.ui/tinymce.ui.iframe/", + "redirect": "/docs/" + }, + { + "location": "/docs/api/tinymce.ui/tinymce.ui.listbox/", + "redirect": "/docs/" + }, + { + "location": "/docs/api/tinymce.ui/tinymce.ui.menubar/", + "redirect": "/docs/" + }, + { + "location": "/docs/api/tinymce.ui/tinymce.ui.menuitem/", + "redirect": "/docs/" + }, + { + "location": "/docs/api/tinymce.ui/tinymce.ui.notification/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/api/tinymce.ui/tinymce.ui.panelbutton/", + "redirect": "/docs/" + }, + { + "location": "/docs/api/tinymce.ui/tinymce.ui.path/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/api/tinymce.ui/tinymce.ui.progress/", + "redirect": "/docs/" + }, + { + "location": "/docs/api/tinymce.ui/tinymce.ui.resizehandle/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/api/tinymce.ui/tinymce.ui.scrollable/", + "redirect": "/docs/" + }, + { + "location": "/docs/api/tinymce.ui/tinymce.ui.slider/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/api/tinymce.ui/tinymce.ui.splitbutton/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/api/tinymce.ui/tinymce.ui.tabpanel/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/api/tinymce.ui/tinymce.ui.throbber/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/api/tinymce.ui/tinymce.ui.tooltip/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/api/tinymce.ui/tinymce.ui.window/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/api/tinymce.util/tinymce.util.color/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/api/tinymce.util/tinymce.util.delay/", + "redirect": "/docs/tinymce/latest/apis/tinymce.util.delay/" + }, + { + "location": "/docs/api/tinymce.util/tinymce.util.eventdispatcher/", + "redirect": "/docs/tinymce/latest/apis/tinymce.util.eventdispatcher/" + }, + { + "location": "/docs/api/tinymce.util/tinymce.util.i18n/", + "redirect": "/docs/tinymce/latest/apis/tinymce.util.i18n/" + }, + { + "location": "/docs/api/tinymce.util/tinymce.util.imageuploader/", + "redirect": "/docs/tinymce/latest/apis/tinymce.util.imageuploader/" + }, + { + "location": "/docs/api/tinymce.util/tinymce.util.json/", + "redirect": "/docs/tinymce/latest/6.0-release-notes-core-changes/#previously-deprecated-items-now-removed" + }, + { + "location": "/docs/api/tinymce.util/tinymce.util.jsonrequest/", + "redirect": "/docs/tinymce/latest/6.0-release-notes-core-changes/#previously-deprecated-items-now-removed" + }, + { + "location": "/docs/api/tinymce.util/tinymce.util.observable/", + "redirect": "/docs/tinymce/latest/apis/tinymce.util.observable/" + }, + { + "location": "/docs/api/tinymce.util/tinymce.util.tools/", + "redirect": "/docs/tinymce/latest/apis/tinymce.util.tools/" + }, + { + "location": "/docs/api/tinymce.util/tinymce.util.uri/", + "redirect": "/docs/tinymce/latest/apis/tinymce.util.uri/" + }, + { + "location": "/docs/api/tinymce.util/tinymce.util.xhr/", + "redirect": "/docs/tinymce/latest/6.0-release-notes-core-changes/#previously-deprecated-items-now-removed" + }, + { + "location": "/docs/api/tinymce/root_tinymce/", + "redirect": "/docs/tinymce/latest/apis/tinymce.root/" + }, + { + "location": "/docs/api/tinymce/tinymce.addonmanager/", + "redirect": "/docs/tinymce/latest/apis/tinymce.addonmanager/" + }, + { + "location": "/docs/api/tinymce/tinymce.annotator/", + "redirect": "/docs/tinymce/latest/apis/tinymce.annotator/" + }, + { + "location": "/docs/api/tinymce/tinymce.contentevent/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/api/tinymce/tinymce.editor/", + "redirect": "/docs/tinymce/latest/apis/tinymce.editor/" + }, + { + "location": "/docs/api/tinymce/tinymce.editormode/", + "redirect": "/docs/tinymce/latest/apis/tinymce.editormode/" + }, + { + "location": "/docs/api/tinymce/tinymce.env/", + "redirect": "/docs/tinymce/latest/apis/tinymce.env/" + }, + { + "location": "/docs/api/tinymce/tinymce.event/", + "redirect": "/docs/tinymce/latest/apis/tinymce.event/" + }, + { + "location": "/docs/api/tinymce/tinymce.formatter/", + "redirect": "/docs/tinymce/latest/apis/tinymce.formatter/" + }, + { + "location": "/docs/api/tinymce/tinymce.notificationmanager/", + "redirect": "/docs/tinymce/latest/apis/tinymce.notificationmanager/" + }, + { + "location": "/docs/api/tinymce/tinymce.plugin/", + "redirect": "/docs/tinymce/latest/apis/tinymce.plugin/" + }, + { + "location": "/docs/api/tinymce/tinymce.progressstateevent/", + "redirect": "/docs/" + }, + { + "location": "/docs/api/tinymce/tinymce.shortcuts/", + "redirect": "/docs/tinymce/latest/apis/tinymce.shortcuts/" + }, + { + "location": "/docs/api/tinymce/tinymce.theme/", + "redirect": "/docs/tinymce/latest/apis/tinymce.theme/" + }, + { + "location": "/docs/api/tinymce/tinymce.undomanager/", + "redirect": "/docs/tinymce/latest/apis/tinymce.undomanager/" + }, + { + "location": "/docs/api/tinymce/tinymce.windowmanager/", + "redirect": "/docs/tinymce/latest/apis/tinymce.windowmanager/" + }, + { + "location": "/docs/browser-compatibility/", + "redirect": "/docs/" + }, + { + "location": "/docs/browser-extensions/", + "redirect": "/docs/" + }, + { + "location": "/docs/changelog/", + "redirect": "/docs/tinymce/latest/changelog/" + }, + { + "location": "/docs/cloud-deployment-guide/", + "redirect": "/docs/tinymce/latest/cloud-deployment-guide/" + }, + { + "location": "/docs/cloud-deployment-guide/cloud-troubleshooting/", + "redirect": "/docs/tinymce/latest/cloud-troubleshooting/" + }, + { + "location": "/docs/cloud-deployment-guide/editor-and-features/", + "redirect": "/docs/tinymce/latest/editor-and-features/" + }, + { + "location": "/docs/cloud-deployment-guide/editor-plugin-version/", + "redirect": "/docs/tinymce/latest/editor-plugin-version/" + }, + { + "location": "/docs/cloud-deployment-guide/features-only/", + "redirect": "/docs/tinymce/latest/features-only/" + }, + { + "location": "/docs/cloud-deployment-guide/plugin-editor-version-compatibility/", + "redirect": "/docs/tinymce/latest/plugin-editor-version-compatibility/" + }, + { + "location": "/docs/cms-web-application-settings/", + "redirect": "/docs/" + }, + { + "location": "/docs/configure/", + "redirect": "/docs/tinymce/latest/initial-configuration/" + }, + { + "location": "/docs/configure/accessibility/", + "redirect": "/docs/tinymce/latest/accessibility/" + }, + { + "location": "/docs/configure/advanced-editing-behavior/", + "redirect": "/docs/tinymce/latest/content-behavior-options/" + }, + { + "location": "/docs/configure/content-appearance/", + "redirect": "/docs/tinymce/latest/add-css-options/" + }, + { + "location": "/docs/configure/content-filtering/", + "redirect": "/docs/tinymce/latest/content-filtering/" + }, + { + "location": "/docs/configure/content-formatting/", + "redirect": "/docs/tinymce/latest/content-formatting/" + }, + { + "location": "/docs/configure/contributing-docs/", + "redirect": "https://github.com/tinymce/tinymce-docs/blob/staging/docs-7/CONTRIBUTING.md" + }, + { + "location": "/docs/configure/contributing-to-open-source/", + "redirect": "https://github.com/tinymce/tinymce/blob/main/CONTRIBUTING.md" + }, + { + "location": "/docs/configure/editor-appearance/", + "redirect": "/docs/tinymce/latest/user-formatting-options/" + }, + { + "location": "/docs/configure/editor-settings/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/configure/editor/", + "redirect": "/docs/" + }, + { + "location": "/docs/configure/file-image-link-tools/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/configure/file-image-pload/", + "redirect": "/docs/" + }, + { + "location": "/docs/configure/file-image-upload/", + "redirect": "/docs/tinymce/latest/file-image-upload/" + }, + { + "location": "/docs/configure/integration-and-setup/", + "redirect": "/docs/tinymce/latest/editor-important-options/" + }, + { + "location": "/docs/configure/integration-and-setup/content-filtering/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/configure/integration/", + "redirect": "/docs/" + }, + { + "location": "/docs/configure/jwt-authentication/", + "redirect": "/docs/" + }, + { + "location": "/docs/configure/localization/", + "redirect": "/docs/tinymce/latest/ui-localization/" + }, + { + "location": "/docs/configure/spelling/", + "redirect": "/docs/tinymce/latest/spelling/" + }, + { + "location": "/docs/configure/url%20handling/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/configure/url-handling/", + "redirect": "/docs/tinymce/latest/url-handling/" + }, + { + "location": "/docs/demo/", + "redirect": "/docs/tinymce/latest/examples/" + }, + { + "location": "/docs/demo/a11ychecker/", + "redirect": "/docs/" + }, + { + "location": "/docs/demo/advcode/", + "redirect": "/docs/" + }, + { + "location": "/docs/demo/basic-example/", + "redirect": "/docs/tinymce/latest/basic-example/" + }, + { + "location": "/docs/demo/casechange/", + "redirect": "/docs/tinymce/latest/casechange/" + }, + { + "location": "/docs/demo/checklist/", + "redirect": "/docs/tinymce/latest/checklist/" + }, + { + "location": "/docs/demo/classic/", + "redirect": "/docs/tinymce/latest/classic-demo/" + }, + { + "location": "/docs/demo/comments-2/", + "redirect": "/docs/tinymce/latest/introduction-to-tiny-comments/" + }, + { + "location": "/docs/demo/custom-menu-item/", + "redirect": "/docs/tinymce/latest/creating-custom-menu-items/#interactive-example" + }, + { + "location": "/docs/demo/custom-toolbar-button/", + "redirect": "/docs/tinymce/latest/custom-toggle-toolbar-button/#toggle-button-example-and-explanation" + }, + { + "location": "/docs/demo/custom-toolbar-listbox/", + "redirect": "/docs/tinymce/latest/examples/" + }, + { + "location": "/docs/demo/custom-toolbar-menu-button/", + "redirect": "/docs/tinymce/latest/custom-menu-toolbar-button/#menu-button-example-and-explanation" + }, + { + "location": "/docs/demo/custom-toolbar-split-button/", + "redirect": "/docs/tinymce/latest/custom-split-toolbar-button/#split-button-example-and-explanation" + }, + { + "location": "/docs/demo/editor-dfree/", + "redirect": "/docs/tinymce/latest/distraction-free-demo/" + }, + { + "location": "/docs/demo/export/", + "redirect": "/docs/" + }, + { + "location": "/docs/demo/file-picker/", + "redirect": "/docs/tinymce/latest/tinydrive-pick/" + }, + { + "location": "/docs/demo/format-custom/", + "redirect": "/docs/tinymce/latest/content-formatting/#example-of-usage-of-the-formats-option" + }, + { + "location": "/docs/demo/format-html5/", + "redirect": "/docs/tinymce/latest/user-formatting-options/#interactive-examples" + }, + { + "location": "/docs/demo/formatpainter/", + "redirect": "/docs/tinymce/latest/formatpainter/" + }, + { + "location": "/docs/demo/full-fateud/", + "redirect": "/docs/" + }, + { + "location": "/docs/demo/full-featured/", + "redirect": "/docs/tinymce/latest/full-featured-premium-demo/" + }, + { + "location": "/docs/demo/image-tools/", + "redirect": "/docs/tinymce/latest/editimage/" + }, + { + "location": "/docs/demo/inline/", + "redirect": "/docs/tinymce/latest/inline-demo/" + }, + { + "location": "/docs/demo/linkchecker/", + "redirect": "/docs/" + }, + { + "location": "/docs/demo/local-upload/", + "redirect": "/docs/tinymce/latest/upload-images/" + }, + { + "location": "/docs/demo/mediaembed/", + "redirect": "/docs/" + }, + { + "location": "/docs/demo/mentions/", + "redirect": "/docs/" + }, + { + "location": "/docs/demo/pageembed/", + "redirect": "/docs/tinymce/latest/pageembed/" + }, + { + "location": "/docs/demo/permanentpen/", + "redirect": "/docs/tinymce/latest/permanentpen/" + }, + { + "location": "/docs/demo/powerpaste/", + "redirect": "/docs/" + }, + { + "location": "/docs/demo/tiny-drive-demo/demo_files.json", + "redirect": "/docs/" + }, + { + "location": "/docs/demo/tiny-drive/", + "redirect": "/docs/tinymce/latest/tinydrive-introduction/" + }, + { + "location": "/docs/demo/tinydrive/", + "redirect": "/docs/tinymce/latest/tinydrive-introduction/" + }, + { + "location": "/docs/demo/upload-image/", + "redirect": "/docs/tinymce/latest/examples/" + }, + { + "location": "/docs/demo/url-conversion/", + "redirect": "/docs/tinymce/latest/url-handling/" + }, + { + "location": "/docs/demo/valid-elements/", + "redirect": "/docs/tinymce/latest/content-filtering/#interactive-example" + }, + { + "location": "/docs/demo/whats-new/", + "redirect": "/docs/tinymce/latest/release-notes/" + }, + { + "location": "/docs/enterprise/", + "redirect": "/docs/tinymce/latest/plugins/" + }, + { + "location": "/docs/enterprise/accessibility/", + "redirect": "/docs/tinymce/latest/a11ychecker/" + }, + { + "location": "/docs/enterprise/advanced-tables/", + "redirect": "/docs/tinymce/latest/advtable/" + }, + { + "location": "/docs/enterprise/advcode/", + "redirect": "/docs/tinymce/latest/advcode/" + }, + { + "location": "/docs/enterprise/casechange/", + "redirect": "/docs/tinymce/latest/casechange/" + }, + { + "location": "/docs/enterprise/check-links/", + "redirect": "/docs/tinymce/latest/linkchecker/" + }, + { + "location": "/docs/enterprise/check-spelling/", + "redirect": "/docs/tinymce/latest/introduction-to-tiny-spellchecker/" + }, + { + "location": "/docs/enterprise/check-spelling/custom/", + "redirect": "/docs/tinymce/latest/custom-dictionaries-for-tiny-spellchecker/" + }, + { + "location": "/docs/enterprise/check-spelling/modifying/", + "redirect": "/docs/tinymce/latest/introduction-to-tiny-spellchecker/" + }, + { + "location": "/docs/enterprise/checklist/", + "redirect": "/docs/tinymce/latest/checklist/" + }, + { + "location": "/docs/enterprise/embed-media/", + "redirect": "/docs/tinymce/latest/introduction-to-mediaembed/" + }, + { + "location": "/docs/enterprise/embed-media/mediaembed-server-config/", + "redirect": "/docs/tinymce/latest/mediaembed-server-config/" + }, + { + "location": "/docs/enterprise/embed-media/mediaembed-server-integration/", + "redirect": "/docs/tinymce/latest/mediaembed-server-integration/" + }, + { + "location": "/docs/enterprise/export/", + "redirect": "/docs/tinymce/latest/export/" + }, + { + "location": "/docs/enterprise/formatpainter/", + "redirect": "/docs/tinymce/latest/formatpainter/" + }, + { + "location": "/docs/enterprise/get-tinymce-bugs-fixed/", + "redirect": "/docs/tinymce/latest/support/" + }, + { + "location": "/docs/enterprise/manage-files-and-images/", + "redirect": "/docs/tinymce/latest/moxiemanager/#fileandimagemanagementusingmoxiemanager" + }, + { + "location": "/docs/enterprise/mentions/", + "redirect": "/docs/tinymce/latest/mentions/" + }, + { + "location": "/docs/enterprise/moxiemanager/", + "redirect": "/docs/tinymce/latest/moxiemanager/" + }, + { + "location": "/docs/enterprise/pageembed/", + "redirect": "/docs/tinymce/latest/pageembed/" + }, + { + "location": "/docs/enterprise/paste-from-word/", + "redirect": "/docs/tinymce/latest/introduction-to-powerpaste/" + }, + { + "location": "/docs/enterprise/permanentpen/", + "redirect": "/docs/tinymce/latest/permanentpen/" + }, + { + "location": "/docs/enterprise/premium-skins-and-icon-packs/", + "redirect": "/docs/tinymce/latest/enhanced-skins-and-icon-packs/" + }, + { + "location": "/docs/enterprise/premium-skins-and-icon-packs/bootstrap-demo/", + "redirect": "/docs/tinymce/latest/bootstrap-demo/" + }, + { + "location": "/docs/enterprise/premium-skins-and-icon-packs/borderless-demo/", + "redirect": "/docs/tinymce/latest/borderless-demo/" + }, + { + "location": "/docs/enterprise/premium-skins-and-icon-packs/fabric-demo/", + "redirect": "/docs/tinymce/latest/fabric-demo/" + }, + { + "location": "/docs/enterprise/premium-skins-and-icon-packs/fluent-demo/", + "redirect": "/docs/tinymce/latest/fluent-demo/" + }, + { + "location": "/docs/enterprise/premium-skins-and-icon-packs/jam-demo/", + "redirect": "/docs/tinymce/latest/jam-demo/" + }, + { + "location": "/docs/enterprise/premium-skins-and-icon-packs/material-classic-demo/", + "redirect": "/docs/tinymce/latest/material-classic-demo/" + }, + { + "location": "/docs/enterprise/premium-skins-and-icon-packs/material-outline-demo/", + "redirect": "/docs/tinymce/latest/material-outline-demo/" + }, + { + "location": "/docs/enterprise/premium-skins-and-icon-packs/naked-demo/", + "redirect": "/docs/tinymce/latest/naked-demo/" + }, + { + "location": "/docs/enterprise/premium-skins-and-icon-packs/outside-demo/", + "redirect": "/docs/tinymce/latest/outside-demo/" + }, + { + "location": "/docs/enterprise/premium-skins-and-icon-packs/small-demo/", + "redirect": "/docs/tinymce/latest/small-demo/" + }, + { + "location": "/docs/enterprise/premium-skins-and-icon-packs/snow-demo/", + "redirect": "/docs/tinymce/latest/snow-demo/" + }, + { + "location": "/docs/enterprise/server/", + "redirect": "/docs/tinymce/latest/introduction-to-premium-selfhosted-services/" + }, + { + "location": "/docs/enterprise/server/configure/", + "redirect": "/docs/tinymce/latest/configure-required-services/" + }, + { + "location": "/docs/enterprise/server/docker/", + "redirect": "/docs/" + }, + { + "location": "/docs/enterprise/server/docker/docker-compose.yml", + "redirect": "/docs/" + }, + { + "location": "/docs/enterprise/server/docker/docker-compose.yml/openapi.json", + "redirect": "/docs/" + }, + { + "location": "/docs/enterprise/server/docker/setup/", + "redirect": "/docs/" + }, + { + "location": "/docs/enterprise/server/docker/setup/aspnet.core/", + "redirect": "/docs/" + }, + { + "location": "/docs/enterprise/server/docker/setup/java/", + "redirect": "/docs/" + }, + { + "location": "/docs/enterprise/server/docker/setup/nodejs/", + "redirect": "/docs/" + }, + { + "location": "/docs/enterprise/server/docker/setup/php/", + "redirect": "/docs/" + }, + { + "location": "/docs/enterprise/server/dockerservices/", + "redirect": "/docs/tinymce/latest/bundle-intro-setup/" + }, + { + "location": "/docs/enterprise/server/php/", + "redirect": "/docs/" + }, + { + "location": "/docs/enterprise/server/self-hosting-hunspell/", + "redirect": "/docs/tinymce/latest/self-hosting-hunspell/" + }, + { + "location": "/docs/enterprise/server/troubleshoot/", + "redirect": "/docs/tinymce/latest/troubleshoot-server/" + }, + { + "location": "/docs/enterprise/support/", + "redirect": "/docs/tinymce/latest/support/" + }, + { + "location": "/docs/enterprise/system-requirements/", + "redirect": "/docs/tinymce/latest/support/" + }, + { + "location": "/docs/enterprise/tiny-comments/", + "redirect": "/docs/tinymce/latest/introduction-to-tiny-comments/" + }, + { + "location": "/docs/enterprise/tinydrive/", + "redirect": "/docs/tinymce/latest/tinydrive-introduction/" + }, + { + "location": "/docs/general-configuration-guide/", + "redirect": "/docs/tinymce/latest/getting-started/" + }, + { + "location": "/docs/general-configuration-guide/advanced-install/", + "redirect": "/docs/tinymce/latest/installation/" + }, + { + "location": "/docs/general-configuration-guide/attribution-requirements/", + "redirect": "https://www.tiny.cloud/legal/attribution-requirements/" + }, + { + "location": "/docs/general-configuration-guide/basic-setup/", + "redirect": "/docs/tinymce/latest/basic-setup/" + }, + { + "location": "/docs/general-configuration-guide/boilerplate-content-css/", + "redirect": "/docs/tinymce/latest/editor-content-css/" + }, + { + "location": "/docs/general-configuration-guide/customize-ui/", + "redirect": "/docs/tinymce/latest/customize-ui/" + }, + { + "location": "/docs/general-configuration-guide/filter-content/", + "redirect": "/docs/tinymce/latest/filter-content/" + }, + { + "location": "/docs/general-configuration-guide/get-support/", + "redirect": "/docs/tinymce/latest/support/" + }, + { + "location": "/docs/general-configuration-guide/localize-your-language/", + "redirect": "/docs/tinymce/latest/localize-your-language/" + }, + { + "location": "/docs/general-configuration-guide/mobile/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/general-configuration-guide/multiple-editors/", + "redirect": "/docs/tinymce/latest/multiple-editors/" + }, + { + "location": "/docs/general-configuration-guide/spell-checking/", + "redirect": "/docs/tinymce/latest/spell-checking/" + }, + { + "location": "/docs/general-configuration-guide/system-requirements/", + "redirect": "/docs/tinymce/latest/support/" + }, + { + "location": "/docs/general-configuration-guide/upgrading/", + "redirect": "/docs/tinymce/latest/upgrading/" + }, + { + "location": "/docs/general-configuration-guide/upload-images/", + "redirect": "/docs/tinymce/latest/upload-images/" + }, + { + "location": "/docs/general-configuration-guide/use-tinymce-classic/", + "redirect": "/docs/tinymce/latest/use-tinymce-classic/" + }, + { + "location": "/docs/general-configuration-guide/use-tinymce-distraction-free/", + "redirect": "/docs/tinymce/latest/use-tinymce-distraction-free/" + }, + { + "location": "/docs/general-configuration-guide/use-tinymce-inline/", + "redirect": "/docs/tinymce/latest/use-tinymce-inline/" + }, + { + "location": "/docs/general-configuration-guide/work-with-plugins/", + "redirect": "/docs/tinymce/latest/work-with-plugins/" + }, + { + "location": "/docs/get-started-cloud/", + "redirect": "/docs/tinymce/latest/editor-and-features/" + }, + { + "location": "/docs/get-started-cloud/editor-and-features/", + "redirect": "/docs/tinymce/latest/editor-and-features/" + }, + { + "location": "/docs/get-started/", + "redirect": "/docs/tinymce/latest/editor-and-features/" + }, + { + "location": "/docs/get-started/advanced-install/", + "redirect": "/docs/tinymce/latest/installation/" + }, + { + "location": "/docs/get-started/customize-ui/", + "redirect": "/docs/tinymce/latest/customize-ui/" + }, + { + "location": "/docs/get-started/first-steps/", + "redirect": "/docs/tinymce/latest/editor-and-features/" + }, + { + "location": "/docs/get-started/get-support/", + "redirect": "/docs/tinymce/latest/support/" + }, + { + "location": "/docs/get-started/multiple-editors/", + "redirect": "/docs/" + }, + { + "location": "/docs/get-started/system-requirements/", + "redirect": "/docs/tinymce/latest/support/" + }, + { + "location": "/docs/get-started/upload-images/", + "redirect": "/docs/tinymce/latest/upload-images/" + }, + { + "location": "/docs/get-started/use-tinymce-inline/", + "redirect": "/docs/tinymce/latest/use-tinymce-inline/" + }, + { + "location": "/docs/get-started/work-with-plugins/", + "redirect": "/docs/tinymce/latest/work-with-plugins/" + }, + { + "location": "/docs/integrations/", + "redirect": "/docs/tinymce/latest/installation/" + }, + { + "location": "/docs/integrations/angular/", + "redirect": "/docs/tinymce/latest/angular-ref/" + }, + { + "location": "/docs/integrations/angular2/", + "redirect": "/docs/tinymce/latest/angular-ref/" + }, + { + "location": "/docs/integrations/angularjs/", + "redirect": "/docs/tinymce/latest/angular-pm/" + }, + { + "location": "/docs/integrations/blazor/", + "redirect": "/docs/tinymce/latest/blazor-cloud/" + }, + { + "location": "/docs/integrations/bootstrap/", + "redirect": "/docs/tinymce/latest/bootstrap-cloud/" + }, + { + "location": "/docs/integrations/django/", + "redirect": "/docs/tinymce/latest/django-cloud/" + }, + { + "location": "/docs/integrations/dojo/", + "redirect": "/docs/" + }, + { + "location": "/docs/integrations/expressjs/", + "redirect": "/docs/tinymce/latest/expressjs-pm/" + }, + { + "location": "/docs/integrations/jquery/", + "redirect": "/docs/tinymce/latest/jquery-cloud/" + }, + { + "location": "/docs/integrations/knockout/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/integrations/laravel/laravel-composer-install/", + "redirect": "/docs/tinymce/latest/laravel-composer-install/" + }, + { + "location": "/docs/integrations/laravel/laravel-introduction/", + "redirect": "/docs/tinymce/latest/laravel-tiny-cloud/" + }, + { + "location": "/docs/integrations/laravel/laravel-tiny-cloud/", + "redirect": "/docs/tinymce/latest/laravel-tiny-cloud/" + }, + { + "location": "/docs/integrations/laravel/laravel-zip-install/", + "redirect": "/docs/tinymce/latest/laravel-zip-install/" + }, + { + "location": "/docs/integrations/python/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/integrations/rails/", + "redirect": "/docs/tinymce/latest/rails-cloud/" + }, + { + "location": "/docs/integrations/react/", + "redirect": "/docs/tinymce/latest/react-cloud/" + }, + { + "location": "/docs/integrations/svelte/", + "redirect": "/docs/tinymce/latest/svelte-cloud/" + }, + { + "location": "/docs/integrations/swing/", + "redirect": "/docs/tinymce/latest/swing/" + }, + { + "location": "/docs/integrations/vue/", + "redirect": "/docs/tinymce/latest/vue-cloud/" + }, + { + "location": "/docs/integrations/webcomponent/", + "redirect": "/docs/tinymce/latest/webcomponent-cloud/" + }, + { + "location": "/docs/integrations/wordpress/", + "redirect": "/docs/tinymce/latest/wordpress/" + }, + { + "location": "/docs/js-tutorial/understanding-the-api/", + "redirect": "/docs/" + }, + { + "location": "/docs/migration-from-4x/", + "redirect": "/docs/tinymce/latest/migration-from-6x/" + }, + { + "location": "/docs/migration-from-5x/", + "redirect": "/docs/" + }, + { + "location": "/docs/migration-from-froala/", + "redirect": "/docs/tinymce/latest/migration-from-froala/" + }, + { + "location": "/docs/mobile/", + "redirect": "/docs/tinymce/latest/tinymce-for-mobile/" + }, + { + "location": "/docs/php-integration/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/plugin-module-settings/", + "redirect": "/docs/" + }, + { + "location": "/docs/plugins/", + "redirect": "/docs/tinymce/latest/plugins/" + }, + { + "location": "/docs/plugins/a11ychecker/", + "redirect": "/docs/tinymce/latest/a11ychecker/" + }, + { + "location": "/docs/plugins/advcode/", + "redirect": "/docs/tinymce/latest/advcode/" + }, + { + "location": "/docs/plugins/advlist/", + "redirect": "/docs/tinymce/latest/advlist/" + }, + { + "location": "/docs/plugins/advtable/", + "redirect": "/docs/tinymce/latest/advtable/" + }, + { + "location": "/docs/plugins/anchor/", + "redirect": "/docs/tinymce/latest/anchor/" + }, + { + "location": "/docs/plugins/autocorrect/", + "redirect": "/docs/tinymce/latest/autocorrect/" + }, + { + "location": "/docs/plugins/autolink/", + "redirect": "/docs/tinymce/latest/autolink/" + }, + { + "location": "/docs/plugins/autoresize/", + "redirect": "/docs/tinymce/latest/autoresize/" + }, + { + "location": "/docs/plugins/autosave/", + "redirect": "/docs/tinymce/latest/autosave/" + }, + { + "location": "/docs/plugins/bbcode/", + "redirect": "/docs/tinymce/6/migration-from-5x/#removed-plugins" + }, + { + "location": "/docs/plugins/casechange/", + "redirect": "/docs/tinymce/latest/casechange/" + }, + { + "location": "/docs/plugins/charmap/", + "redirect": "/docs/tinymce/latest/charmap/" + }, + { + "location": "/docs/plugins/checklist/", + "redirect": "/docs/tinymce/latest/checklist/" + }, + { + "location": "/docs/plugins/code/", + "redirect": "/docs/tinymce/latest/code/" + }, + { + "location": "/docs/plugins/codesample/", + "redirect": "/docs/tinymce/latest/codesample/" + }, + { + "location": "/docs/plugins/colorpicker/", + "redirect": "/docs/tinymce/latest/add-css-options/#textcoloroptions" + }, + { + "location": "/docs/plugins/comments/", + "redirect": "/docs/tinymce/latest/introduction-to-tiny-comments/" + }, + { + "location": "/docs/plugins/compat3x/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/plugins/contextmenu/", + "redirect": "/docs/tinymce/latest/contextmenu/" + }, + { + "location": "/docs/plugins/directionality/", + "redirect": "/docs/tinymce/latest/directionality/" + }, + { + "location": "/docs/plugins/drive/", + "redirect": "/docs/tinymce/latest/tinydrive-introduction/" + }, + { + "location": "/docs/plugins/emoticons/", + "redirect": "/docs/tinymce/latest/emoticons/" + }, + { + "location": "/docs/plugins/example/", + "redirect": "/docs/tinymce/latest/plugins/" + }, + { + "location": "/docs/plugins/exampledependency/", + "redirect": "/docs/tinymce/latest/plugins/" + }, + { + "location": "/docs/plugins/export/", + "redirect": "/docs/tinymce/latest/exportpdf/" + }, + { + "location": "/docs/plugins/formatpainter/", + "redirect": "/docs/tinymce/latest/formatpainter/" + }, + { + "location": "/docs/plugins/fullpage/", + "redirect": "/docs/tinymce/6/migration-from-5x/#removed-plugins" + }, + { + "location": "/docs/plugins/fullscreen/", + "redirect": "/docs/tinymce/latest/fullscreen/" + }, + { + "location": "/docs/plugins/help/", + "redirect": "/docs/tinymce/latest/help/" + }, + { + "location": "/docs/plugins/hr/", + "redirect": "/docs/tinymce/6/migration-from-5x/#removed-plugins" + }, + { + "location": "/docs/plugins/image/", + "redirect": "/docs/tinymce/latest/image/" + }, + { + "location": "/docs/plugins/imagetools/", + "redirect": "/docs/tinymce/latest/editimage/" + }, + { + "location": "/docs/plugins/importcss/", + "redirect": "/docs/tinymce/latest/importcss/" + }, + { + "location": "/docs/plugins/insertdatetime/", + "redirect": "/docs/tinymce/latest/insertdatetime/" + }, + { + "location": "/docs/plugins/layer/", + "redirect": "/docs/tinymce/latest/plugins/" + }, + { + "location": "/docs/plugins/legacyoutput/", + "redirect": "/docs/tinymce/6/migration-from-5x/#removed-plugins" + }, + { + "location": "/docs/plugins/link/", + "redirect": "/docs/tinymce/latest/link/" + }, + { + "location": "/docs/plugins/linkchecker/", + "redirect": "/docs/tinymce/latest/linkchecker/" + }, + { + "location": "/docs/plugins/lists/", + "redirect": "/docs/tinymce/latest/lists/" + }, + { + "location": "/docs/plugins/media/", + "redirect": "/docs/tinymce/latest/media/" + }, + { + "location": "/docs/plugins/mediaembed/", + "redirect": "/docs/tinymce/latest/introduction-to-mediaembed/" + }, + { + "location": "/docs/plugins/mentions/", + "redirect": "/docs/tinymce/latest/mentions/" + }, + { + "location": "/docs/plugins/moxiemanager/", + "redirect": "/docs/tinymce/latest/moxiemanager/" + }, + { + "location": "/docs/plugins/nonbreaking/", + "redirect": "/docs/tinymce/latest/nonbreaking/" + }, + { + "location": "/docs/plugins/noneditable/", + "redirect": "/docs/tinymce/6/migration-from-5x/#removed-plugins" + }, + { + "location": "/docs/plugins/obscenewords/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/plugins/opensource/", + "redirect": "/docs/tinymce/latest/plugins/" + }, + { + "location": "/docs/plugins/opensource/accordion/", + "redirect": "/docs/" + }, + { + "location": "/docs/plugins/opensource/advlist/", + "redirect": "/docs/tinymce/latest/advlist/" + }, + { + "location": "/docs/plugins/opensource/anchor/", + "redirect": "/docs/tinymce/latest/anchor/" + }, + { + "location": "/docs/plugins/opensource/autolink/", + "redirect": "/docs/tinymce/latest/autolink/" + }, + { + "location": "/docs/plugins/opensource/autoresize/", + "redirect": "/docs/tinymce/latest/autoresize/" + }, + { + "location": "/docs/plugins/opensource/autosave/", + "redirect": "/docs/tinymce/latest/autosave/" + }, + { + "location": "/docs/plugins/opensource/bbcode/", + "redirect": "/docs/tinymce/6/migration-from-5x/#removed-plugins" + }, + { + "location": "/docs/plugins/opensource/charmap/", + "redirect": "/docs/tinymce/latest/charmap/" + }, + { + "location": "/docs/plugins/opensource/code/", + "redirect": "/docs/tinymce/latest/code/" + }, + { + "location": "/docs/plugins/opensource/codesample/", + "redirect": "/docs/tinymce/latest/codesample/" + }, + { + "location": "/docs/plugins/opensource/directionality/", + "redirect": "/docs/tinymce/latest/directionality/" + }, + { + "location": "/docs/plugins/opensource/emoticons/", + "redirect": "/docs/tinymce/latest/emoticons/" + }, + { + "location": "/docs/plugins/opensource/fullpage/", + "redirect": "/docs/tinymce/6/migration-from-5x/#removed-plugins" + }, + { + "location": "/docs/plugins/opensource/fullscreen/", + "redirect": "/docs/tinymce/latest/fullscreen/" + }, + { + "location": "/docs/plugins/opensource/help/", + "redirect": "/docs/tinymce/latest/help/" + }, + { + "location": "/docs/plugins/opensource/hr/", + "redirect": "/docs/tinymce/6/migration-from-5x/#removed-plugins" + }, + { + "location": "/docs/plugins/opensource/image/", + "redirect": "/docs/tinymce/latest/image/" + }, + { + "location": "/docs/plugins/opensource/imagetools/", + "redirect": "/docs/tinymce/latest/editimage/" + }, + { + "location": "/docs/plugins/opensource/importcss/", + "redirect": "/docs/tinymce/latest/importcss/" + }, + { + "location": "/docs/plugins/opensource/insertdatetime/", + "redirect": "/docs/tinymce/latest/insertdatetime/" + }, + { + "location": "/docs/plugins/opensource/legacyoutput/", + "redirect": "/docs/tinymce/6/migration-from-5x/#removed-plugins" + }, + { + "location": "/docs/plugins/opensource/link/", + "redirect": "/docs/tinymce/latest/link/" + }, + { + "location": "/docs/plugins/opensource/lists/", + "redirect": "/docs/tinymce/latest/lists/" + }, + { + "location": "/docs/plugins/opensource/media/", + "redirect": "/docs/tinymce/latest/media/" + }, + { + "location": "/docs/plugins/opensource/nonbreaking/", + "redirect": "/docs/tinymce/latest/nonbreaking/" + }, + { + "location": "/docs/plugins/opensource/noneditable/", + "redirect": "/docs/tinymce/6/migration-from-5x/#removed-plugins" + }, + { + "location": "/docs/plugins/opensource/pagebreak/", + "redirect": "/docs/tinymce/latest/pagebreak/" + }, + { + "location": "/docs/plugins/opensource/paste/", + "redirect": "/docs/tinymce/6/migration-from-5x/#removed-plugins" + }, + { + "location": "/docs/plugins/opensource/preview/", + "redirect": "/docs/tinymce/latest/preview/" + }, + { + "location": "/docs/plugins/opensource/print/", + "redirect": "/docs/tinymce/6/migration-from-5x/#removed-plugins" + }, + { + "location": "/docs/plugins/opensource/quickbars/", + "redirect": "/docs/tinymce/latest/quickbars/" + }, + { + "location": "/docs/plugins/opensource/save/", + "redirect": "/docs/tinymce/latest/save/" + }, + { + "location": "/docs/plugins/opensource/searchreplace/", + "redirect": "/docs/tinymce/latest/searchreplace/" + }, + { + "location": "/docs/plugins/opensource/spellchecker/", + "redirect": "/docs/tinymce/latest/spelling/" + }, + { + "location": "/docs/plugins/opensource/tabfocus/", + "redirect": "/docs/tinymce/6/migration-from-5x/#tabfocus" + }, + { + "location": "/docs/plugins/opensource/table/", + "redirect": "/docs/tinymce/latest/table/" + }, + { + "location": "/docs/plugins/opensource/template/", + "redirect": "/docs/tinymce/latest/migration-from-6x/#removed-plugins" + }, + { + "location": "/docs/plugins/opensource/textcolor/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/plugins/opensource/textpattern/", + "redirect": "/docs/tinymce/6/migration-from-5x/#text-patterns" + }, + { + "location": "/docs/plugins/opensource/toc/", + "redirect": "/docs/tinymce/latest/tableofcontents/" + }, + { + "location": "/docs/plugins/opensource/visualblocks/", + "redirect": "/docs/tinymce/latest/visualblocks/" + }, + { + "location": "/docs/plugins/opensource/visualchars/", + "redirect": "/docs/tinymce/latest/visualchars/" + }, + { + "location": "/docs/plugins/opensource/wordcount/", + "redirect": "/docs/tinymce/latest/wordcount/" + }, + { + "location": "/docs/plugins/pagebreak/", + "redirect": "/docs/tinymce/latest/pagebreak/" + }, + { + "location": "/docs/plugins/pageembed/", + "redirect": "/docs/tinymce/latest/pageembed/" + }, + { + "location": "/docs/plugins/paste/", + "redirect": "/docs/tinymce/6/migration-from-5x/#removed-plugins" + }, + { + "location": "/docs/plugins/permanentpen/", + "redirect": "/docs/tinymce/latest/permanentpen/" + }, + { + "location": "/docs/plugins/powerpaste/", + "redirect": "/docs/tinymce/latest/introduction-to-powerpaste/" + }, + { + "location": "/docs/plugins/premium/", + "redirect": "/docs/tinymce/latest/plugins/" + }, + { + "location": "/docs/plugins/premium/a11ychecker/", + "redirect": "/docs/tinymce/latest/a11ychecker/" + }, + { + "location": "/docs/plugins/premium/advcode/", + "redirect": "/docs/tinymce/latest/advcode/" + }, + { + "location": "/docs/plugins/premium/advtable/", + "redirect": "/docs/tinymce/latest/advtable/" + }, + { + "location": "/docs/plugins/premium/casechange/", + "redirect": "/docs/tinymce/latest/casechange/" + }, + { + "location": "/docs/plugins/premium/checklist/", + "redirect": "/docs/tinymce/latest/checklist/" + }, + { + "location": "/docs/plugins/premium/comments/", + "redirect": "/docs/tinymce/latest/introduction-to-tiny-comments/" + }, + { + "location": "/docs/plugins/premium/comments/comments_callback_mode/", + "redirect": "/docs/tinymce/latest/comments-callback-mode/" + }, + { + "location": "/docs/plugins/premium/comments/comments_commands_events_apis/", + "redirect": "/docs/tinymce/latest/comments-commands-events-apis/" + }, + { + "location": "/docs/plugins/premium/comments/comments_embedded_mode/", + "redirect": "/docs/tinymce/latest/comments-embedded-mode/" + }, + { + "location": "/docs/plugins/premium/comments/comments_toolbars_menus/", + "redirect": "/docs/tinymce/latest/comments-toolbars-menus/" + }, + { + "location": "/docs/plugins/premium/comments/comments_using_comments/", + "redirect": "/docs/tinymce/latest/comments-using-comments/" + }, + { + "location": "/docs/plugins/premium/comments/introduction_to_tiny_comments/", + "redirect": "/docs/tinymce/latest/introduction-to-tiny-comments/" + }, + { + "location": "/docs/plugins/premium/export/", + "redirect": "/docs/tinymce/latest/export/" + }, + { + "location": "/docs/plugins/premium/formatpainter/", + "redirect": "/docs/tinymce/latest/formatpainter/" + }, + { + "location": "/docs/plugins/premium/linkchecker/", + "redirect": "/docs/tinymce/latest/linkchecker/" + }, + { + "location": "/docs/plugins/premium/mediaembed/", + "redirect": "/docs/tinymce/latest/introduction-to-mediaembed/" + }, + { + "location": "/docs/plugins/premium/mentions/", + "redirect": "/docs/tinymce/latest/mentions/" + }, + { + "location": "/docs/plugins/premium/moxiemanager/", + "redirect": "/docs/tinymce/latest/moxiemanager/" + }, + { + "location": "/docs/plugins/premium/pageembed/", + "redirect": "/docs/tinymce/latest/pageembed/" + }, + { + "location": "/docs/plugins/premium/permanentpen/", + "redirect": "/docs/tinymce/latest/permanentpen/" + }, + { + "location": "/docs/plugins/premium/powerpaste/", + "redirect": "/docs/tinymce/latest/introduction-to-powerpaste/" + }, + { + "location": "/docs/plugins/premium/rtc/", + "redirect": "/docs/tinymce/6/rtc-introduction/" + }, + { + "location": "/docs/plugins/premium/tinydrive/", + "redirect": "/docs/tinymce/latest/tinydrive-introduction/" + }, + { + "location": "/docs/plugins/premium/tinymcespellchecker/", + "redirect": "/docs/tinymce/latest/introduction-to-tiny-spellchecker/" + }, + { + "location": "/docs/plugins/preview/", + "redirect": "/docs/tinymce/latest/preview/" + }, + { + "location": "/docs/plugins/print/", + "redirect": "/docs/tinymce/6/migration-from-5x/#removed-plugins" + }, + { + "location": "/docs/plugins/quickbars/", + "redirect": "/docs/tinymce/latest/quickbars/" + }, + { + "location": "/docs/plugins/save/", + "redirect": "/docs/tinymce/latest/save/" + }, + { + "location": "/docs/plugins/searchreplace/", + "redirect": "/docs/tinymce/latest/searchreplace/" + }, + { + "location": "/docs/plugins/spellchecker/", + "redirect": "/docs/tinymce/latest/introduction-to-tiny-spellchecker/" + }, + { + "location": "/docs/plugins/tabfocus/", + "redirect": "/docs/tinymce/6/migration-from-5x/#tabfocus" + }, + { + "location": "/docs/plugins/table/", + "redirect": "/docs/tinymce/latest/table/" + }, + { + "location": "/docs/plugins/template/", + "redirect": "/docs/tinymce/latest/template/" + }, + { + "location": "/docs/plugins/textcolor/", + "redirect": "/docs/tinymce/latest/add-css-options/#textcoloroptions" + }, + { + "location": "/docs/plugins/textpattern/", + "redirect": "/docs/tinymce/6/migration-from-5x/#text-patterns" + }, + { + "location": "/docs/plugins/tinydrive/", + "redirect": "/docs/tinymce/latest/tinydrive-introduction/" + }, + { + "location": "/docs/plugins/tinymcespellchecker/", + "redirect": "/docs/tinymce/latest/introduction-to-tiny-spellchecker/" + }, + { + "location": "/docs/plugins/toc/", + "redirect": "/docs/tinymce/latest/tableofcontents/" + }, + { + "location": "/docs/plugins/visualblocks/", + "redirect": "/docs/tinymce/latest/visualblocks/" + }, + { + "location": "/docs/plugins/visualchars/", + "redirect": "/docs/tinymce/latest/visualchars/" + }, + { + "location": "/docs/plugins/wordcount/", + "redirect": "/docs/tinymce/latest/wordcount/" + }, + { + "location": "/docs/plugins/xulrunner/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/quick-start/", + "redirect": "/docs/tinymce/latest/editor-and-features/" + }, + { + "location": "/docs/release-notes/", + "redirect": "/docs/tinymce/latest/release-notes/" + }, + { + "location": "/docs/release-notes/6.0-upcoming-changes/", + "redirect": "/docs/tinymce/5/6.0-upcoming-changes/" + }, + { + "location": "/docs/release-notes/premium-plugins-errata509/", + "redirect": "/docs/tinymce/latest/release-notes/" + }, + { + "location": "/docs/release-notes/release-notes/", + "redirect": "/docs/tinymce/5/release-notes/" + }, + { + "location": "/docs/release-notes/release-notes50/", + "redirect": "/docs/tinymce/5/release-notes50/" + }, + { + "location": "/docs/release-notes/release-notes501/", + "redirect": "/docs/tinymce/5/release-notes501/" + }, + { + "location": "/docs/release-notes/release-notes5013/", + "redirect": "/docs/tinymce/5/release-notes5013/" + }, + { + "location": "/docs/release-notes/release-notes5014/", + "redirect": "/docs/tinymce/5/release-notes5014/" + }, + { + "location": "/docs/release-notes/release-notes502/", + "redirect": "/docs/tinymce/5/release-notes502/" + }, + { + "location": "/docs/release-notes/release-notes503/", + "redirect": "/docs/tinymce/5/release-notes503/" + }, + { + "location": "/docs/release-notes/release-notes504/", + "redirect": "/docs/tinymce/5/release-notes504/" + }, + { + "location": "/docs/release-notes/release-notes505/", + "redirect": "/docs/tinymce/5/release-notes505/" + }, + { + "location": "/docs/release-notes/release-notes506/", + "redirect": "/docs/tinymce/5/release-notes506/" + }, + { + "location": "/docs/release-notes/release-notes507/", + "redirect": "/docs/tinymce/5/release-notes507/" + }, + { + "location": "/docs/release-notes/release-notes509/", + "redirect": "/docs/tinymce/5/release-notes509/" + }, + { + "location": "/docs/release-notes/release-notes51/", + "redirect": "/docs/tinymce/5/release-notes51/" + }, + { + "location": "/docs/release-notes/release-notes510/", + "redirect": "/docs/tinymce/5/release-notes510/" + }, + { + "location": "/docs/release-notes/release-notes5101/", + "redirect": "/docs/tinymce/5/release-notes5101/" + }, + { + "location": "/docs/release-notes/release-notes5102/", + "redirect": "/docs/tinymce/5/release-notes5102/" + }, + { + "location": "/docs/release-notes/release-notes5103/", + "redirect": "/docs/tinymce/5/release-notes5103/" + }, + { + "location": "/docs/release-notes/release-notes5104/", + "redirect": "/docs/tinymce/5/release-notes5104/" + }, + { + "location": "/docs/release-notes/release-notes5105/", + "redirect": "/docs/tinymce/5/release-notes5105/" + }, + { + "location": "/docs/release-notes/release-notes5106/", + "redirect": "/docs/tinymce/5/release-notes5106/" + }, + { + "location": "/docs/release-notes/release-notes5107/", + "redirect": "/docs/tinymce/5/release-notes5107/" + }, + { + "location": "/docs/release-notes/release-notes5108/", + "redirect": "/docs/tinymce/5/release-notes5108/" + }, + { + "location": "/docs/release-notes/release-notes5109/", + "redirect": "/docs/tinymce/5/release-notes5109/" + }, + { + "location": "/docs/release-notes/release-notes514/", + "redirect": "/docs/tinymce/5/release-notes514/" + }, + { + "location": "/docs/release-notes/release-notes515/", + "redirect": "/docs/tinymce/5/release-notes515/" + }, + { + "location": "/docs/release-notes/release-notes516/", + "redirect": "/docs/tinymce/5/release-notes516/" + }, + { + "location": "/docs/release-notes/release-notes52/", + "redirect": "/docs/tinymce/5/release-notes52/" + }, + { + "location": "/docs/release-notes/release-notes521/", + "redirect": "/docs/tinymce/5/release-notes521/" + }, + { + "location": "/docs/release-notes/release-notes522/", + "redirect": "/docs/tinymce/5/release-notes522/" + }, + { + "location": "/docs/release-notes/release-notes53/", + "redirect": "/docs/tinymce/5/release-notes53/" + }, + { + "location": "/docs/release-notes/release-notes54/", + "redirect": "/docs/tinymce/5/release-notes54/" + }, + { + "location": "/docs/release-notes/release-notes542/", + "redirect": "/docs/tinymce/5/release-notes542/" + }, + { + "location": "/docs/release-notes/release-notes55/", + "redirect": "/docs/tinymce/5/release-notes55/" + }, + { + "location": "/docs/release-notes/release-notes56/", + "redirect": "/docs/tinymce/5/release-notes56/" + }, + { + "location": "/docs/release-notes/release-notes562/", + "redirect": "/docs/tinymce/5/release-notes562/" + }, + { + "location": "/docs/release-notes/release-notes57/", + "redirect": "/docs/tinymce/5/release-notes57/" + }, + { + "location": "/docs/release-notes/release-notes571/", + "redirect": "/docs/tinymce/5/release-notes571/" + }, + { + "location": "/docs/release-notes/release-notes58/", + "redirect": "/docs/tinymce/5/release-notes58/" + }, + { + "location": "/docs/release-notes/release-notes581/", + "redirect": "/docs/tinymce/5/release-notes581/" + }, + { + "location": "/docs/release-notes/release-notes582/", + "redirect": "/docs/tinymce/5/release-notes582/" + }, + { + "location": "/docs/release-notes/release-notes59/", + "redirect": "/docs/tinymce/5/release-notes59/" + }, + { + "location": "/docs/rtc/", + "redirect": "/docs/tinymce/6/rtc-introduction/" + }, + { + "location": "/docs/rtc/configuration/rtc-options-optional/", + "redirect": "/docs/tinymce/6/rtc-options-optional/" + }, + { + "location": "/docs/rtc/configuration/rtc-options-overview/", + "redirect": "/docs/tinymce/6/rtc-options-overview/" + }, + { + "location": "/docs/rtc/configuration/rtc-options-required/", + "redirect": "/docs/tinymce/6/rtc-options-required/" + }, + { + "location": "/docs/rtc/encryption/", + "redirect": "/docs/tinymce/6/rtc-encryption/" + }, + { + "location": "/docs/rtc/events/", + "redirect": "/docs/tinymce/6/rtc-events/" + }, + { + "location": "/docs/rtc/getting-started/", + "redirect": "/docs/tinymce/6/rtc-getting-started/" + }, + { + "location": "/docs/rtc/how-the-rtc-plugin-encrypts-content/", + "redirect": "/docs/tinymce/6/how-the-rtc-plugin-encrypts-content/" + }, + { + "location": "/docs/rtc/introduction/", + "redirect": "/docs/tinymce/6/rtc-introduction/" + }, + { + "location": "/docs/rtc/jwt-authentication/", + "redirect": "/docs/tinymce/6/rtc-jwt-authentication/" + }, + { + "location": "/docs/rtc/rtc-supported-functionality/", + "redirect": "/docs/tinymce/6/rtc-supported-functionality/" + }, + { + "location": "/docs/rtc/rtc-troubleshooting/", + "redirect": "/docs/tinymce/6/rtc-troubleshooting/" + }, + { + "location": "/docs/textbox-io/apis/editor/editorconfig/editor.html", + "redirect": "/docs/" + }, + { + "location": "/docs/textbox-io/apis/editor/editorconfig/toolbar.html", + "redirect": "/docs/" + }, + { + "location": "/docs/themes/modern/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/tinydrive/", + "redirect": "/docs/tinymce/latest/tinydrive-introduction/" + }, + { + "location": "/docs/tinydrive/api/", + "redirect": "/docs/tinymce/latest/introduction-to-tinydrive-apis/" + }, + { + "location": "/docs/tinydrive/changelog/", + "redirect": "/docs/tinymce/latest/tinydrive-changelog/" + }, + { + "location": "/docs/tinydrive/configuration/", + "redirect": "/docs/tinymce/latest/tinydrive-setup-options/" + }, + { + "location": "/docs/tinydrive/get-help/", + "redirect": "/docs/tinymce/latest/tinydrive-dotnet/#additional-resources" + }, + { + "location": "/docs/tinydrive/getting-started/", + "redirect": "/docs/tinymce/latest/tinydrive-getting-started/" + }, + { + "location": "/docs/tinydrive/integrations/", + "redirect": "/docs/tinymce/latest/tinydrive-dropbox-integration/" + }, + { + "location": "/docs/tinydrive/integrations/dropbox-integration/", + "redirect": "/docs/tinymce/latest/tinydrive-dropbox-integration/" + }, + { + "location": "/docs/tinydrive/integrations/googledrive-integration/", + "redirect": "/docs/tinymce/latest/tinydrive-googledrive-integration/" + }, + { + "location": "/docs/tinydrive/introduction/", + "redirect": "/docs/tinymce/latest/tinydrive-introduction/" + }, + { + "location": "/docs/tinydrive/jwt-authentication/", + "redirect": "/docs/tinymce/latest/tinydrive-jwt-authentication/" + }, + { + "location": "/docs/tinydrive/libraries/", + "redirect": "/docs/tinymce/latest/tinydrive-dotnet/" + }, + { + "location": "/docs/tinydrive/libraries/dotnet/", + "redirect": "/docs/tinymce/latest/tinydrive-dotnet/" + }, + { + "location": "/docs/tinydrive/libraries/java/", + "redirect": "/docs/tinymce/latest/tinydrive-java/" + }, + { + "location": "/docs/tinydrive/libraries/nodejs/", + "redirect": "/docs/tinymce/latest/tinydrive-nodejs/" + }, + { + "location": "/docs/tinydrive/libraries/php/", + "redirect": "/docs/tinymce/latest/tinydrive-php/" + }, + { + "location": "/docs/tinydrive/tinydrive-api/plugin/", + "redirect": "/docs/tinymce/latest/introduction-to-tinydrive-apis/" + }, + { + "location": "/docs/tinydrive/tinydrive-api/standalone/", + "redirect": "/docs/tinymce/latest/introduction-to-tinydrive-apis/" + }, + { + "location": "/docs/tinymce-for-swing/", + "redirect": "/docs/" + }, + { + "location": "/docs/tinymce/5/advanced/annotations/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/advanced/available-toolbar-buttons/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/advanced/editor-command-identifiers/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/advanced/events/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/advanced/generate-rsa-key-pairs/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/advanced/keyboard-shortcuts/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/advanced/security/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/advanced/usage-with-module-loaders/reference/icons/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/advanced/usage-with-module-loaders/reference/plugins/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/advanced/usage-with-module-loaders/reference/skins/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/advanced/usage-with-module-loaders/reference/themes/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/advanced/usage-with-module-loaders/rollup/rollup_es6_download/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/advanced/usage-with-module-loaders/rollup/rollup_es6_npm/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/advanced/usage-with-module-loaders/webpack/webpack_cjs_download/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/advanced/usage-with-module-loaders/webpack/webpack_cjs_npm/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/advanced/usage-with-module-loaders/webpack/webpack_es6_download/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/api/tinymce.dom/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/api/tinymce.dom/tinymce.dom.domutils/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/api/tinymce.dom/tinymce.dom.rangeutils/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/api/tinymce.dom/tinymce.dom.scriptloader/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/api/tinymce.dom/tinymce.dom.stylesheetloader/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/api/tinymce.dom/tinymce.dom.textseeker/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/api/tinymce.dom/tinymce.dom.treewalker/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/api/tinymce.editor.ui/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/api/tinymce.editor.ui/tinymce.editor.ui.registry/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/api/tinymce.editor.ui/tinymce.editor.ui.ui/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/api/tinymce.html/tinymce.html.entities/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/api/tinymce.html/tinymce.html.node/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/api/tinymce.html/tinymce.html.saxparser/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/api/tinymce.html/tinymce.html.serializer/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/api/tinymce.html/tinymce.html.styles/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/api/tinymce.util/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/api/tinymce.util/tinymce.util.imageuploader/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/api/tinymce.util/tinymce.util.jsonrequest/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/api/tinymce.util/tinymce.util.observable/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/api/tinymce.util/tinymce.util.tools/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/api/tinymce.util/tinymce.util.xhr/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/api/tinymce/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/api/tinymce/root_tinymce/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/api/tinymce/tinymce.editor/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/api/tinymce/tinymce.env/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/api/tinymce/tinymce.event/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/api/tinymce/tinymce.formatter/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/api/tinymce/tinymce.plugin/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/api/tinymce/tinymce.shortcuts/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/api/tinymce/tinymce.theme/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/cloud-deployment-guide/cloud-troubleshooting/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/cloud-deployment-guide/editor-and-features/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/cloud-deployment-guide/editor-plugin-version/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/cloud-deployment-guide/features-only/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/cloud-deployment-guide/plugin-editor-version-compatibility/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/configure/advanced-editing-behavior/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/configure/content-filtering/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/configure/contributing-docs/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/configure/editor-appearance/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/configure/integration-and-setup/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/configure/localization/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/demo/basic-example/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/demo/checklist/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/demo/comments-2/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/demo/custom-toolbar-button/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/demo/custom-toolbar-menu-button/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/demo/custom-toolbar-split-button/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/demo/editor-dfree/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/demo/file-picker/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/demo/format-custom/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/demo/formatpainter/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/demo/full-featured/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/demo/image-tools/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/demo/inline/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/demo/local-upload/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/demo/tiny-drive-demo/demo_files.json", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/demo/tinydrive/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/demo/url-conversion/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/demo/valid-elements/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/demos/tiny-drive-demo/demo_files.json", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/enterprise/advanced-tables/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/enterprise/advcode/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/enterprise/check-links/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/enterprise/check-spelling/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/enterprise/check-spelling/custom/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/enterprise/checklist/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/enterprise/embed-media/mediaembed-server-config/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/enterprise/embed-media/mediaembed-server-integration/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/enterprise/export/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/enterprise/formatpainter/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/enterprise/moxiemanager/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/enterprise/permanentpen/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/enterprise/premium-skins-and-icon-packs/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/enterprise/premium-skins-and-icon-packs/borderless-demo/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/enterprise/premium-skins-and-icon-packs/fabric-demo/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/enterprise/premium-skins-and-icon-packs/naked-demo/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/enterprise/premium-skins-and-icon-packs/small-demo/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/enterprise/premium-skins-and-icon-packs/snow-demo/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/enterprise/server/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/enterprise/server/configure/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/enterprise/server/dockerservices/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/enterprise/server/self-hosting-hunspell/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/enterprise/tinydrive/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/general-configuration-guide/advanced-install/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/general-configuration-guide/attribution-requirements/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/general-configuration-guide/basic-setup/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/general-configuration-guide/customize-ui/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/general-configuration-guide/filter-content/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/general-configuration-guide/get-support/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/general-configuration-guide/localize-your-language/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/general-configuration-guide/multiple-editors/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/general-configuration-guide/spell-checking/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/general-configuration-guide/system-requirements/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/general-configuration-guide/use-tinymce-inline/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/general-configuration-guide/work-with-plugins/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/integrations/angular/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/integrations/angularjs/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/integrations/blazor/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/integrations/bootstrap/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/integrations/laravel/laravel-composer-install/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/integrations/laravel/laravel-introduction/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/integrations/laravel/laravel-tiny-cloud/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/integrations/laravel/laravel-zip-install/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/integrations/react/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/integrations/svelte/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/integrations/swing/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/integrations/vue/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/integrations/webcomponent/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/opensource/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/opensource/anchor/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/opensource/autoresize/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/opensource/autosave/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/opensource/charmap/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/opensource/code/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/opensource/codesample/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/opensource/emoticons/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/opensource/fullscreen/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/opensource/help/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/opensource/image/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/opensource/imagetools/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/opensource/importcss/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/opensource/insertdatetime/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/opensource/media/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/opensource/noneditable/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/opensource/paste/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/opensource/preview/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/opensource/print/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/opensource/quickbars/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/opensource/searchreplace/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/opensource/spellchecker/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/opensource/tabfocus/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/opensource/template/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/opensource/textpattern/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/premium/a11ychecker/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/premium/advcode/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/premium/casechange/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/premium/checklist/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/premium/comments/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/premium/comments/comments_commands_events_apis/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/premium/comments/comments_embedded_mode/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/premium/comments/comments_toolbars_menus/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/premium/comments/comments_using_comments/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/premium/comments/introduction_to_tiny_comments/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/premium/export/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/premium/linkchecker/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/premium/mediaembed/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/premium/mentions/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/premium/pageembed/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/premium/powerpaste/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/plugins/premium/tinydrive/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/release-notes/6.0-upcoming-changes/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/release-notes/release-notes503/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/release-notes/release-notes504/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/release-notes/release-notes505/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/release-notes/release-notes506/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/release-notes/release-notes507/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/release-notes/release-notes509/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/release-notes/release-notes51/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/release-notes/release-notes510/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/release-notes/release-notes5101/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/release-notes/release-notes5102/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/release-notes/release-notes5106/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/release-notes/release-notes5107/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/release-notes/release-notes5108/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/release-notes/release-notes5109/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/release-notes/release-notes514/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/release-notes/release-notes515/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/release-notes/release-notes516/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/release-notes/release-notes521/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/release-notes/release-notes522/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/release-notes/release-notes53/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/release-notes/release-notes542/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/release-notes/release-notes55/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/release-notes/release-notes56/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/release-notes/release-notes562/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/release-notes/release-notes58/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/release-notes/release-notes581/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/release-notes/release-notes582/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/rtc/configuration/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/rtc/configuration/rtc-options-optional/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/rtc/configuration/rtc-options-overview/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/rtc/getting-started/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/rtc/jwt-authentication/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/rtc/rtc-troubleshooting/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/tinydrive/get-help/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/tinydrive/getting-started/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/tinydrive/integrations/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/tinydrive/libraries/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/tinydrive/libraries/dotnet/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/tinydrive/libraries/java/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/tinydrive/tinydrive-api/plugin/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/ui-components/autocompleter/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/ui-components/contextform/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/ui-components/contextmenu/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/ui-components/contexttoolbar/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/ui-components/customsidebar/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/ui-components/dialog/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/ui-components/dialogcomponents/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/ui-components/typesoftoolbarbuttons/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/5/ui-components/urldialog/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/6/api/", + "redirect": "/docs/tinymce/6/" + }, + { + "location": "/docs/tinymce/6/basic-setup.html", + "redirect": "/docs/tinymce/6/" + }, + { + "location": "/docs/tinymce/6/cloud-quick-start.html", + "redirect": "/docs/tinymce/6/" + }, + { + "location": "/docs/tinymce/6/code-plugin/", + "redirect": "/docs/tinymce/6/code/" + }, + { + "location": "/docs/tinymce/6/file-picker/", + "redirect": "/docs/tinymce/6/" + }, + { + "location": "/docs/tinymce/6/get-started/", + "redirect": "/docs/tinymce/6/getting-started/" + }, + { + "location": "/docs/tinymce/6/integrations.html", + "redirect": "/docs/tinymce/6/" + }, + { + "location": "/docs/tinymce/6/integrations/", + "redirect": "/docs/tinymce/6/installation/" + }, + { + "location": "/docs/tinymce/6/npm-projects.html", + "redirect": "/docs/tinymce/6/" + }, + { + "location": "/docs/tinymce/6/premium-full-featured/", + "redirect": "/docs/tinymce/6/full-featured-premium-demo/" + }, + { + "location": "/docs/tinymce/6/premium-skins-and-icons/", + "redirect": "/docs/tinymce/6/enhanced-skins-and-icon-packs/" + }, + { + "location": "/docs/tinymce/6/react-pm/", + "redirect": "/docs/tinymce/6/react-pm-host/" + }, + { + "location": "/docs/tinymce/6/react-zip/", + "redirect": "/docs/tinymce/6/react-zip-host/" + }, + { + "location": "/docs/tinymce/7/export/", + "redirect": "/docs/tinymce/7/exportpdf/" + }, + { + "location": "/docs/tinymce/8/", + "pattern": "^/docs/tinymce/8/(.*)$", + "redirect": "/docs/tinymce/latest/$1" + }, + { + "location": "/docs/tinymce/demo/url-dialog-demo/external-page.html", + "redirect": "/docs/" + }, + { + "location": "/docs/tinymce/latest/6.0-release-notes-core-changes/", + "redirect": "/docs/tinymce/6/" + }, + { + "location": "/docs/tinymce/latest/6.0-release-notes-known-issues/", + "redirect": "/docs/tinymce/6/" + }, + { + "location": "/docs/tinymce/latest/6.0-release-notes-overview/", + "redirect": "/docs/tinymce/6/" + }, + { + "location": "/docs/tinymce/latest/6.0-release-notes-premium-changes/", + "redirect": "/docs/tinymce/6/" + }, + { + "location": "/docs/tinymce/latest/6.0.2-release-notes/", + "redirect": "/docs/tinymce/6/" + }, + { + "location": "/docs/tinymce/latest/6.0.3-release-notes/", + "redirect": "/docs/tinymce/6/" + }, + { + "location": "/docs/tinymce/latest/6.1-release-notes/", + "redirect": "/docs/tinymce/6/" + }, + { + "location": "/docs/tinymce/latest/6.1.2-release-notes/", + "redirect": "/docs/tinymce/6/" + }, + { + "location": "/docs/tinymce/latest/6.2-release-notes/", + "redirect": "/docs/tinymce/6/" + }, + { + "location": "/docs/tinymce/latest/6.3-release-notes/", + "redirect": "/docs/tinymce/6/" + }, + { + "location": "/docs/tinymce/latest/6.3.2-release-notes/", + "redirect": "/docs/tinymce/6/" + }, + { + "location": "/docs/tinymce/latest/6.4.1-release-notes/", + "redirect": "/docs/tinymce/6/" + }, + { + "location": "/docs/tinymce/latest/6.4.2-release-notes/", + "redirect": "/docs/tinymce/6/" + }, + { + "location": "/docs/tinymce/latest/6.5.1-release-notes/", + "redirect": "/docs/tinymce/6/" + }, + { + "location": "/docs/tinymce/latest/6.6-release-notes/", + "redirect": "/docs/tinymce/6/" + }, + { + "location": "/docs/tinymce/latest/6.6.2-release-notes/", + "redirect": "/docs/tinymce/6/" + }, + { + "location": "/docs/tinymce/latest/6.7-release-notes/", + "redirect": "/docs/tinymce/6/" + }, + { + "location": "/docs/tinymce/latest/6.7.1-release-notes/", + "redirect": "/docs/tinymce/6/" + }, + { + "location": "/docs/tinymce/latest/6.7.2-release-notes/", + "redirect": "/docs/tinymce/6/" + }, + { + "location": "/docs/tinymce/latest/6.7.3-release-notes/", + "redirect": "/docs/tinymce/6/" + }, + { + "location": "/docs/tinymce/latest/6.8.1-release-notes/", + "redirect": "/docs/tinymce/6/" + }, + { + "location": "/docs/tinymce/latest/6.8.2-release-notes/", + "redirect": "/docs/tinymce/6/" + }, + { + "location": "/docs/tinymce/latest/6.8.3-release-notes/", + "redirect": "/docs/tinymce/6/" + }, + { + "location": "/docs/tinymce/latest/7.0-relea", + "redirect": "/docs/tinymce/6/" + }, + { + "location": "/docs/tinymce/latest/bundle-hyperlinking-container/", + "redirect": "/docs/" + }, + { + "location": "/docs/tinymce/latest/bundle-imageproxy-container/", + "redirect": "/docs/" + }, + { + "location": "/docs/tinymce/latest/bundle-spelling-container/", + "redirect": "/docs/" + }, + { + "location": "/docs/tinymce/latest/editor-premium-upgrade-promotion/", + "redirect": "/docs/" + }, + { + "location": "/docs/tinymce/latest/export-to-pdf-with-jwt-authentication-with-php/", + "redirect": "/docs/" + }, + { + "location": "/docs/tinymce/latest/export-to-word-with-jwt-authentication-with-php/", + "redirect": "/docs/" + }, + { + "location": "/docs/tinymce/latest/export/", + "redirect": "/docs/tinymce/latest/exportpdf/" + }, + { + "location": "/docs/tinymce/latest/how-the-rtc-plugin-encrypts-content/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/tinymce/latest/import-word-with-jwt-authentication-with-php/", + "redirect": "/docs/" + }, + { + "location": "/docs/tinymce/latest/importword-with-jwt-authentication-nodejs/", + "redirect": "/docs/" + }, + { + "location": "/docs/tinymce/latest/integrations/", + "redirect": "/docs/tinymce/latest/installation/" + }, + { + "location": "/docs/tinymce/latest/markdown/r/", + "redirect": "/docs/tinymce/5/" + }, + { + "location": "/docs/tinymce/latest/mathequations/", + "redirect": "/docs/tinymce/latest/math/" + }, + { + "location": "/docs/tinymce/latest/premium-full-featured/", + "redirect": "/docs/tinymce/latest/full-featured-premium-demo/" + }, + { + "location": "/docs/tinymce/latest/premium-skins-and-icons/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/tinymce/latest/react-pm/", + "redirect": "/docs/tinymce/latest/react-pm-host/" + }, + { + "location": "/docs/tinymce/latest/react-zip/", + "redirect": "/docs/tinymce/latest/react-zip-host/" + }, + { + "location": "/docs/tinymce/latest/rtc-encryption/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/tinymce/latest/rtc-events/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/tinymce/latest/rtc-getting-started/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/tinymce/latest/rtc-introduction/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/tinymce/latest/rtc-jwt-authentication/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/tinymce/latest/rtc-options-optional/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/tinymce/latest/rtc-options-overview/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/tinymce/latest/rtc-options-required/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/tinymce/latest/rtc-supported-functionality/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/tinymce/latest/rtc-troubleshooting/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/tinymce/latest/template/", + "redirect": "/docs/tinymce/latest/advanced-templates/" + }, + { + "location": "/docs/tinymce/latest/templates/", + "redirect": "/docs/tinymce/latest/advanced-templates/" + }, + { + "location": "/docs/tinymce/latest/tinymce-and-csp/", + "redirect": "/docs/" + }, + { + "location": "/docs/tinymce/latest/yeoman-generator/", + "redirect": "/docs/" + }, + { + "location": "/docs/ui-components/", + "redirect": "/docs/tinymce/latest/ui-components/" + }, + { + "location": "/docs/ui-components/autocompleter/", + "redirect": "/docs/tinymce/latest/autocompleter/" + }, + { + "location": "/docs/ui-components/button/", + "redirect": "/docs/tinymce/latest/" + }, + { + "location": "/docs/ui-components/contextform/", + "redirect": "/docs/tinymce/latest/contextform/" + }, + { + "location": "/docs/ui-components/contextmenu/", + "redirect": "/docs/tinymce/latest/contextmenu/" + }, + { + "location": "/docs/ui-components/contexttoolbar/", + "redirect": "/docs/tinymce/latest/contexttoolbar/" + }, + { + "location": "/docs/ui-components/customsidebar/", + "redirect": "/docs/tinymce/latest/customsidebar/" + }, + { + "location": "/docs/ui-components/dialog/", + "redirect": "/docs/tinymce/latest/dialog/" + }, + { + "location": "/docs/ui-components/dialogc/", + "redirect": "/docs/tinymce/5/dialog/" + }, + { + "location": "/docs/ui-components/dialogcomponents/", + "redirect": "/docs/tinymce/latest/dialog-components/" + }, + { + "location": "/docs/ui-components/menuitems/", + "redirect": "/docs/tinymce/latest/creating-custom-menu-items/" + }, + { + "location": "/docs/ui-components/toolbarbuttons/", + "redirect": "/docs/tinymce/latest/custom-toolbarbuttons/" + }, + { + "location": "/docs/ui-components/typesoftoolbarbuttons/", + "redirect": "/docs/tinymce/latest/custom-toolbarbuttons/" + }, + { + "location": "/docs/ui-components/urldialog/", + "redirect": "/docs/tinymce/latest/urldialog/" + }, + { + "location": "/docs/usage-limits/", + "redirect": "/docs/tinymce/latest/" + } +] From 14d90286d4f919e8a1ca1d7231f899489b3b9071 Mon Sep 17 00:00:00 2001 From: James Johnson Date: Mon, 3 Nov 2025 18:04:52 +1000 Subject: [PATCH 10/54] TCLOUD-4780: Make script parallel to speed up object creation --- .../workflows/scripts/generate_redirects.mjs | 354 ++++++++++++------ 1 file changed, 249 insertions(+), 105 deletions(-) diff --git a/.github/workflows/scripts/generate_redirects.mjs b/.github/workflows/scripts/generate_redirects.mjs index 96c7a70baa..ceddbb62fe 100644 --- a/.github/workflows/scripts/generate_redirects.mjs +++ b/.github/workflows/scripts/generate_redirects.mjs @@ -1,6 +1,46 @@ import fs from 'node:fs'; import path from 'node:path'; -import child_process from 'node:child_process'; +import { spawn } from 'node:child_process'; + +/** + * + * @param {string[]} cmd + * @returns {Promise<{ cmd: string[], stdout: string, stderr: string, status: number | null, signal: NodeJS.Signals | null, error?: Error}>} + */ +const spawnAsync = (cmd) => new Promise((resolve) => { + const p = spawn(cmd[0], cmd.slice(1), { stdio: ['inherit', 'pipe', 'pipe'] }); + p.stdout.setEncoding('utf-8'); + let out = ''; + let err = ''; + p.stdout.on('data', (chunk) => { + out += chunk; + }); + p.stderr.on('data', (chunk) => { + err += chunk; + }); + p.on('exit', (code, signal) => { + const failed = code !== 0 || signal; + const data = { + cmd, + stdout: out, + stderr: err, + status: code, + signal: signal, + ...(failed ? { error: new Error(err) } : {}), + }; + resolve(data); + }); +}); + +/** + * + * @param {string[]} cmd + * @returns {Promise<{ cmd: string[], stdout: string, stderr: string, status: number | null, signal: NodeJS.Signals | null, error?: Error}>} + */ +const dryRunSuccess = (cmd) => new Promise((resolve) => { + console.log('Starting ' + cmd.join(' ')); + setTimeout(() => resolve({ cmd, stdout: '', stderr: '', status: 0, signal: null }), Math.floor(Math.random() * 100)); +}); /** @@ -15,19 +55,30 @@ function checkS3ObjectExists(_dryRun, _bucket, _prefix, subPath) { } } +/** + * + * @param {Record} metadata + * @returns + */ const metadataArgs = (metadata) => { // Build metadata string in the format key1=value1,key2=value2 const metadataString = Object.entries(metadata) .map(([key, value]) => `${key}=${value}`) .join(','); - + return metadataString ? ['--metadata', metadataString] : []; } /** - * Copy existing S3 object to itself with new metadata + * + * @param {boolean} dryRun + * @param {string} bucket + * @param {string} prefix + * @param {string} subPath + * @param {Record} metadata + * @returns */ -function copyS3ObjectWithMetadata(dryRun, bucket, prefix, subPath, metadata) { +const copyS3ObjectWithMetadataAsync = async (dryRun, bucket, prefix, subPath, metadata) => { const fullPath = `${prefix}/${subPath}`; const cmd = [ 'aws', 's3api', 'copy-object', @@ -38,87 +89,70 @@ function copyS3ObjectWithMetadata(dryRun, bucket, prefix, subPath, metadata) { '--content-type', 'text/html', ...metadataArgs(metadata) ]; - - console.log(`Updating existing S3 object with metadata: ${fullPath}`); - console.log(`Command: ${cmd.join(' ')}`); - - const result = dryRun ? { status: 0 } : child_process.spawnSync('aws', cmd.slice(1), { - stdio: 'inherit', - encoding: 'utf8' - }); - - if (result.error) { - console.error(`Error copying S3 object ${fullPath}:`, result.error); - return false; - } else if (result.status !== 0) { - console.error(`AWS CLI copy command failed for ${fullPath} with exit code:`, result.status); - return false; - } else { - console.log(`Successfully updated S3 object metadata: ${fullPath}`); - return true; - } -} + const result = await (dryRun ? dryRunSuccess(cmd) : spawnAsync(cmd)); + return { + ...result, + copied: true, + subPath, + }; +}; /** - * Create new S3 object with generated content and metadata + * + * @param {boolean} dryRun + * @param {string} bucket + * @param {string} prefix + * @param {string} subPath + * @param {Record} metadata + * @param {string} newFileTemplate + * @returns */ -function createNewS3Object(dryRun, bucket, prefix, subPath, metadata, newFileTemplate) { +const createNewS3ObjectAsync = async (dryRun, bucket, prefix, subPath, metadata, newFileTemplate) => { const fullPath = `${prefix}/${subPath}`; - // AWS CLI command to put object with metadata - const cmd = [ - 'aws', 's3api', 'put-object', - '--bucket', bucket, - '--key', fullPath, - '--body', newFileTemplate, - '--content-type', 'text/html', - ...metadataArgs(metadata) - ]; - - console.log(`Creating new S3 object: ${fullPath}`); - console.log(`Command: ${cmd.join(' ')}`); - - const result = dryRun ? { status: 0 } : child_process.spawnSync('aws', cmd.slice(1), { - stdio: 'inherit', - encoding: 'utf8' - }); - - if (result.error) { - console.error(`Error creating S3 object ${fullPath}:`, result.error); - return false; - } else if (result.status !== 0) { - console.error(`AWS CLI command failed for ${fullPath} with exit code:`, result.status); - return false; - } else { - console.log(`Successfully created S3 object: ${fullPath}`); - return true; - } + // AWS CLI command to put object with metadata + const cmd = [ + 'aws', 's3api', 'put-object', + '--bucket', bucket, + '--key', fullPath, + '--body', newFileTemplate, + '--content-type', 'text/html', + ...metadataArgs(metadata) + ]; + const result = await (dryRun ? dryRunSuccess(cmd) : spawnAsync(cmd)); + return { + ...result, + copied: false, + subPath, + } } /** - * Create or update S3 object with metadata, reusing existing content if available + * + * @param {boolean} dryRun + * @param {string} bucket + * @param {string} prefix + * @param {string} subPath + * @param {Record} metadata + * @param {string} newFileTemplate + * @returns */ -function createOrUpdateS3Object(dryRun, bucket, prefix, subPath, metadata, newFileTemplate) { - console.log(`\nProcessing: ${subPath}`); - +const createOrUpdateS3ObjectAsync = (dryRun, bucket, prefix, subPath, metadata, newFileTemplate) => { // Check if object already exists if (checkS3ObjectExists(dryRun, bucket, prefix, subPath)) { - console.log(`Object exists, updating metadata...`); - return copyS3ObjectWithMetadata(dryRun, bucket, prefix, subPath, metadata); + return copyS3ObjectWithMetadataAsync(dryRun, bucket, prefix, subPath, metadata); } else { - console.log(`Object doesn't exist, creating new one...`); - return createNewS3Object(dryRun, bucket, prefix, subPath, metadata, newFileTemplate); + return createNewS3ObjectAsync(dryRun, bucket, prefix, subPath, metadata, newFileTemplate); } } /** - * Generate S3 objects for all redirects + * + * @param {boolean} dryRun + * @param {string} bucket + * @param {string} prefix + * @param {Map} redirectsByLocation */ -function generateRedirectObjects(dryRun, bucket, prefix, redirectsByLocation) { - console.log(`Processing ${redirectsByLocation.size} unique locations`); - - let successCount = 0; - let errorCount = 0; - +function* generateRedirectObjectsAsync(dryRun, bucket, prefix, redirectsByLocation) { // Create empty index.html content for the redirect const htmlContent = ` @@ -135,11 +169,13 @@ function generateRedirectObjects(dryRun, bucket, prefix, redirectsByLocation) { const newFileTemplate = `/tmp/redirect-${Date.now()}.html`; fs.writeFileSync(newFileTemplate, htmlContent); + let allSettled = Promise.resolve(); + try { for (const [location, locationRedirects] of redirectsByLocation) { // Create S3 object path by appending index.html to location - const locationIndexHtml = location.endsWith('/') - ? `${location}index.html` + const locationIndexHtml = location.endsWith('/') + ? `${location}index.html` : `${location}/index.html`; // Remove leading slash from location @@ -147,13 +183,13 @@ function generateRedirectObjects(dryRun, bucket, prefix, redirectsByLocation) { // Build metadata headers const metadata = {}; - + locationRedirects.forEach((redirect, index) => { const i = index + 1; // 1-based indexing as requested - + // Add redirect location header metadata[`redirect-location-${i}`] = redirect.redirect; - + // Add pattern header if it exists if (redirect.pattern !== undefined) { metadata[`redirect-pattern-${i}`] = redirect.pattern; @@ -161,31 +197,138 @@ function generateRedirectObjects(dryRun, bucket, prefix, redirectsByLocation) { }); // Create or update the S3 object - if (createOrUpdateS3Object(dryRun, bucket, prefix, subPath, metadata, newFileTemplate)) { - successCount++; - } else { - errorCount++; - } + const task = createOrUpdateS3ObjectAsync(dryRun, bucket, prefix, subPath, metadata, newFileTemplate); + yield task; + // throw away task success/failure info, just keep task settled status + const taskSettled = task.then(() => { }, () => { }); + allSettled = allSettled.then(() => taskSettled); } } finally { - // Clean up temporary file - if (fs.existsSync(newFileTemplate)) { - fs.unlinkSync(newFileTemplate); + allSettled.then(() => { + // Clean up temporary file + if (fs.existsSync(newFileTemplate)) { + fs.unlinkSync(newFileTemplate); + } + }) + } +} + +/** + * @template T + * @param {number} max + * @param {Generator, void, unknown>} source + * @returns {AsyncGenerator} + */ +async function* parallelGenerator(max, source) { + /** + * @param {number} i + * @param {IteratorResult, void>} task + * @returns {Promise<[number, IteratorResult]>} + */ + const wrap = (i, task) => new Promise((resolve) => { + if (task.done) { + resolve([i, { done: true }]); + } else { + task.value.then((v) => resolve([i, { done: false, value: v }])) + } + }) + /** @type {(Promise<[number, IteratorResult]>)[]} */ + let tasks = []; + for (let i = 0; i < max; i++) { + tasks.push(wrap(i, source.next())) + } + /** @type {(Promise<[number, IteratorResult]> | null)[]} */ + let tasksAndNull; + while (true) { + const [i, v] = await Promise.race(tasks); + if (v.done) { + // move the tasks over to the nullable list + tasksAndNull = tasks.splice(0, tasks.length); + tasksAndNull[i] = null; + break; + } else { + tasks[i] = wrap(i, source.next()); + yield v.value; + } + } + let filteredTasks = tasksAndNull.filter((v) => v !== null); + while (filteredTasks.length > 0) { + const [i, v] = await Promise.race(filteredTasks); + tasksAndNull[i] = null; + if (!v.done) { + yield v.value; + } + filteredTasks = tasksAndNull.filter((v) => v !== null); + } +} + +/** + * + * @param {boolean} dryRun + * @param {string} bucket + * @param {string} prefix + * @param {number} parallel + * @param {{location: string, pattern?: string, redirect: string}[]} redirects + */ +const makeRedirectObjects = async (dryRun, bucket, prefix, parallel, redirects) => { + let successCount = 0; + let errorCount = 0; + // Group redirects by location to handle multiple redirects for the same location + /** @type {Map} */ + const redirectsByLocation = new Map(); + + redirects.forEach((redirect) => { + const location = redirect.location; + if (!redirectsByLocation.has(location)) { + redirectsByLocation.set(location, []); + } + redirectsByLocation.get(location).push(redirect); + }); + + const tasks = parallelGenerator(parallel, generateRedirectObjectsAsync(dryRun, bucket, prefix, redirectsByLocation)); + let processedCount = 0; + for await (const taskResult of tasks) { + const { subPath, cmd, error } = taskResult; + processedCount++; + if (error) { + errorCount++; + } else { + successCount++; + } + console.log(`\nProcessed ${processedCount} of ${redirectsByLocation.size}: ${taskResult.subPath}`); + const fullPath = `${prefix}/${subPath}`; + if (taskResult.copied) { + console.log(`Updating S3 object: ${fullPath}`); + console.log(`Command: ${cmd.join(' ')}`); + if (taskResult.error) { + console.error(`Error copying S3 object ${fullPath}:`, error); + } else { + console.log(`Successfully updated S3 object ${fullPath}`); + } + } else { + console.log(`Created new S3 object: ${fullPath}`); + console.log(`Command: ${cmd.join(' ')}`); + if (taskResult.error) { + console.error(`Error creating S3 object ${fullPath}:`, error); + } else { + console.log(`Successfully created S3 object ${fullPath}`); + } } } console.log(`\nSummary:`); console.log(`Successfully processed: ${successCount} objects`); console.log(`Errors: ${errorCount} objects`); -} - + console.log('Redirect object generation completed.'); +}; const usage = () => ` generate-redirects [--dry-run] Generate redirects in s3. Options: - --dry-run only output the commands that will be run + --parallel number of s3 command to run in parallel; default: 5 + --dry-run only output the commands that will be run ` const main = async () => { @@ -199,15 +342,27 @@ const main = async () => { } return false; })(); + const parallelValue = (() => { + const idx = args.findIndex((arg) => arg === '--parallel'); + if (idx !== -1) { + const [_flag, num] = args.splice(idx, 2); + return num; + } + return null; + })(); + if (parallelValue != null && !/^\d+$/.test(parallelValue)) { + return Promise.reject(`Invalid value for --parallel, got ${parallelValue}`); + } + const parallel = parallelValue != null ? parseInt(parallelValue, 10) : 5; if (args.length !== 2) { return Promise.reject(`Expected 2 values, got ${args.length}`); } const [bucket, prefix] = args; - if (!/^[a-z0-9][a-z0-9\.-]{1,61}[a-z0-9]$/.test(bucket) || - /\.\./.test(bucket) || /^\d+\.\d+\.\d+\.\d+$/.test(bucket) || - /^xn--/.test(bucket) || /^sthree-/.test(bucket) || /^amzn-s3-demo-/.test(bucket) || - /-s3alias$/.test(bucket) || /--ol-s3$/.test(bucket) || /\.mrap$/.test(bucket) || - /--x-s3$/.test(bucket) || /--table-s3$/.test(bucket)) { + if (!/^[a-z0-9][a-z0-9\.-]{1,61}[a-z0-9]$/.test(bucket) || + /\.\./.test(bucket) || /^\d+\.\d+\.\d+\.\d+$/.test(bucket) || + /^xn--/.test(bucket) || /^sthree-/.test(bucket) || /^amzn-s3-demo-/.test(bucket) || + /-s3alias$/.test(bucket) || /--ol-s3$/.test(bucket) || /\.mrap$/.test(bucket) || + /--x-s3$/.test(bucket) || /--table-s3$/.test(bucket)) { return Promise.reject(`Invalid bucket name, got ${bucket}`); } @@ -215,23 +370,12 @@ const main = async () => { return Promise.reject(`Invalid prefix, got ${prefix}`); } + /** + * @type {{location: string, pattern?: string, redirect: string}[]} + */ const redirects = JSON.parse(fs.readFileSync(path.join(import.meta.dirname, '../../../redirects.json'), 'utf-8')); - // Group redirects by location to handle multiple redirects for the same location - const redirectsByLocation = new Map(); - - redirects.forEach((redirect) => { - const location = redirect.location; - if (!redirectsByLocation.has(location)) { - redirectsByLocation.set(location, []); - } - redirectsByLocation.get(location).push(redirect); - }); - - // Generate all redirect objects - generateRedirectObjects(dryRun, bucket, prefix, redirectsByLocation); - - console.log('Redirect object generation completed.'); + makeRedirectObjects(dryRun, bucket, prefix, parallel, redirects); }; From 985696c49123368eeddf3767d8f685b649e1822e Mon Sep 17 00:00:00 2001 From: James Johnson Date: Mon, 3 Nov 2025 18:13:38 +1000 Subject: [PATCH 11/54] TCLOUD-4780: Increased parallel s3 operations to 20 --- .github/workflows/scripts/generate_redirects.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scripts/generate_redirects.mjs b/.github/workflows/scripts/generate_redirects.mjs index ceddbb62fe..1a1140068e 100644 --- a/.github/workflows/scripts/generate_redirects.mjs +++ b/.github/workflows/scripts/generate_redirects.mjs @@ -353,7 +353,7 @@ const main = async () => { if (parallelValue != null && !/^\d+$/.test(parallelValue)) { return Promise.reject(`Invalid value for --parallel, got ${parallelValue}`); } - const parallel = parallelValue != null ? parseInt(parallelValue, 10) : 5; + const parallel = parallelValue != null ? parseInt(parallelValue, 10) : 20; if (args.length !== 2) { return Promise.reject(`Expected 2 values, got ${args.length}`); } From 1f7bd30a383297c67583a208d10104b751b98abb Mon Sep 17 00:00:00 2001 From: James Johnson Date: Mon, 3 Nov 2025 18:23:27 +1000 Subject: [PATCH 12/54] TCLOUD-4780: Create redirects in github action --- .github/workflows/preview_create.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index 016558831d..525d724dba 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -55,4 +55,8 @@ jobs: - name: Upload website preview to S3 # put in the docs subfolder as we rely on that path in redirects run: | - aws s3 sync --delete ./build/site s3://tiny-cloud-antora-docs-preview/pr${PR_NUMBER}/docs + aws s3 rm s3://tiny-cloud-antora-docs-preview/pr${PR_NUMBER} + aws s3 sync ./build/site s3://tiny-cloud-antora-docs-preview/pr${PR_NUMBER}/docs + + - name: Create redirects on S3 + run: node ./.github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview pr${PR_NUMBER} \ No newline at end of file From 28a707fbb1277cd198ccdf56d769b10ba7d1bd03 Mon Sep 17 00:00:00 2001 From: James Johnson Date: Tue, 4 Nov 2025 12:01:23 +1000 Subject: [PATCH 13/54] TCLOUD-4780: Ensure the clean up of redirects --- .github/workflows/preview_create.yml | 9 ++++++--- .github/workflows/scripts/generate_redirects.mjs | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index 525d724dba..377f1d2aa8 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -42,9 +42,13 @@ jobs: - name: Build Website run: yarn antora ./antora-playbook.yml + - name: Rename site + run: | + mv ./build/site ./build/docs + - name: Rename sitemap.xml run: | - mv ./build/site/sitemap.xml ./build/site/antora-sitemap.xml + mv ./build/docs/sitemap.xml ./build/docs/antora-sitemap.xml - name: configure aws credentials uses: aws-actions/configure-aws-credentials@v5.0.0 @@ -55,8 +59,7 @@ jobs: - name: Upload website preview to S3 # put in the docs subfolder as we rely on that path in redirects run: | - aws s3 rm s3://tiny-cloud-antora-docs-preview/pr${PR_NUMBER} - aws s3 sync ./build/site s3://tiny-cloud-antora-docs-preview/pr${PR_NUMBER}/docs + aws s3 sync --delete ./build s3://tiny-cloud-antora-docs-preview/pr${PR_NUMBER} - name: Create redirects on S3 run: node ./.github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview pr${PR_NUMBER} \ No newline at end of file diff --git a/.github/workflows/scripts/generate_redirects.mjs b/.github/workflows/scripts/generate_redirects.mjs index 1a1140068e..96a76f542a 100644 --- a/.github/workflows/scripts/generate_redirects.mjs +++ b/.github/workflows/scripts/generate_redirects.mjs @@ -49,7 +49,7 @@ const dryRunSuccess = (cmd) => new Promise((resolve) => { function checkS3ObjectExists(_dryRun, _bucket, _prefix, subPath) { // it's too slow to talk to s3, so just check the local files we just uploaded... if (subPath.startsWith('docs/')) { - return fs.existsSync(path.join(import.meta.dirname, '../../../build/site', subPath.slice('docs/'.length))); + return fs.existsSync(path.join(import.meta.dirname, '../../../build', subPath)); } else { return false; } @@ -84,7 +84,7 @@ const copyS3ObjectWithMetadataAsync = async (dryRun, bucket, prefix, subPath, me 'aws', 's3api', 'copy-object', '--bucket', bucket, '--copy-source', `${bucket}/${fullPath}`, - '--key', subPath, + '--key', fullPath, '--metadata-directive', 'REPLACE', '--content-type', 'text/html', ...metadataArgs(metadata) From 15a05fffad34ce94465be4c21a2a2772cc7882ff Mon Sep 17 00:00:00 2001 From: James Johnson Date: Tue, 4 Nov 2025 16:38:16 +1000 Subject: [PATCH 14/54] TCLOUD-4780: Use Lua replacment string "%1" instead of "$1" --- redirects.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/redirects.json b/redirects.json index d015c49a62..44db0c2e20 100644 --- a/redirects.json +++ b/redirects.json @@ -2,12 +2,12 @@ { "location": "/", "pattern": "^/docs%-beta(.*)$", - "redirect": "/docs$1" + "redirect": "/docs%1" }, { "location": "/", "pattern": "^/docs%-preview(.*)$", - "redirect": "/docs$1" + "redirect": "/docs%1" }, { "location": "/docs-3x/", @@ -3072,7 +3072,7 @@ { "location": "/docs/tinymce/8/", "pattern": "^/docs/tinymce/8/(.*)$", - "redirect": "/docs/tinymce/latest/$1" + "redirect": "/docs/tinymce/latest/%1" }, { "location": "/docs/tinymce/demo/url-dialog-demo/external-page.html", From fae80ade52d5b5b11d49f16df8a264477640ed9a Mon Sep 17 00:00:00 2001 From: James Johnson Date: Wed, 5 Nov 2025 10:21:26 +1000 Subject: [PATCH 15/54] TCLOUD-4780: Fixed missing semicolon --- .github/workflows/scripts/generate_redirects.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scripts/generate_redirects.mjs b/.github/workflows/scripts/generate_redirects.mjs index 96a76f542a..8223206c4c 100644 --- a/.github/workflows/scripts/generate_redirects.mjs +++ b/.github/workflows/scripts/generate_redirects.mjs @@ -209,7 +209,7 @@ function* generateRedirectObjectsAsync(dryRun, bucket, prefix, redirectsByLocati if (fs.existsSync(newFileTemplate)) { fs.unlinkSync(newFileTemplate); } - }) + }); } } From 7f0f6b06784c8775d00c0ec21ad033146fc5dacf Mon Sep 17 00:00:00 2001 From: James Johnson Date: Wed, 5 Nov 2025 11:01:31 +1000 Subject: [PATCH 16/54] TCLOUD-4780: Use crypto to make the temporary file name unpredictable Note that this doesn't prevent the file being rewriten after it has been created but as we have to pass the file to other processes that can't be prevented. --- .github/workflows/scripts/generate_redirects.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/scripts/generate_redirects.mjs b/.github/workflows/scripts/generate_redirects.mjs index 8223206c4c..310408dc8d 100644 --- a/.github/workflows/scripts/generate_redirects.mjs +++ b/.github/workflows/scripts/generate_redirects.mjs @@ -1,4 +1,6 @@ +import crypto from 'node:crypto'; import fs from 'node:fs'; +import os from 'node:os'; import path from 'node:path'; import { spawn } from 'node:child_process'; @@ -166,7 +168,7 @@ function* generateRedirectObjectsAsync(dryRun, bucket, prefix, redirectsByLocati `; // Write temporary file - const newFileTemplate = `/tmp/redirect-${Date.now()}.html`; + const newFileTemplate = path.join(os.tmpdir(), `redirect-${crypto.randomBytes(16).toString('hex')}.html`); fs.writeFileSync(newFileTemplate, htmlContent); let allSettled = Promise.resolve(); From 88f5f2084adf6a6778d7163e437ce22eeeb1be83 Mon Sep 17 00:00:00 2001 From: James Johnson Date: Wed, 5 Nov 2025 12:44:26 +1000 Subject: [PATCH 17/54] TCLOUD-4780: Use preexisting file as redirect file template This avoids problems with creating temporary files. --- .github/workflows/scripts/empty.html | 1 + .../workflows/scripts/generate_redirects.mjs | 72 ++++++------------- 2 files changed, 22 insertions(+), 51 deletions(-) create mode 100644 .github/workflows/scripts/empty.html diff --git a/.github/workflows/scripts/empty.html b/.github/workflows/scripts/empty.html new file mode 100644 index 0000000000..09ce0846fd --- /dev/null +++ b/.github/workflows/scripts/empty.html @@ -0,0 +1 @@ +? \ No newline at end of file diff --git a/.github/workflows/scripts/generate_redirects.mjs b/.github/workflows/scripts/generate_redirects.mjs index 310408dc8d..d9b62e8236 100644 --- a/.github/workflows/scripts/generate_redirects.mjs +++ b/.github/workflows/scripts/generate_redirects.mjs @@ -1,6 +1,4 @@ -import crypto from 'node:crypto'; import fs from 'node:fs'; -import os from 'node:os'; import path from 'node:path'; import { spawn } from 'node:child_process'; @@ -155,63 +153,35 @@ const createOrUpdateS3ObjectAsync = (dryRun, bucket, prefix, subPath, metadata, * @param {Map} redirectsByLocation */ function* generateRedirectObjectsAsync(dryRun, bucket, prefix, redirectsByLocation) { - // Create empty index.html content for the redirect - const htmlContent = ` - - - - Redirecting... - - -

Redirecting...

- -`; + // create path to empty template + const newFileTemplate = path.join(import.meta.dirname, 'empty.html'); - // Write temporary file - const newFileTemplate = path.join(os.tmpdir(), `redirect-${crypto.randomBytes(16).toString('hex')}.html`); - fs.writeFileSync(newFileTemplate, htmlContent); + for (const [location, locationRedirects] of redirectsByLocation) { + // Create S3 object path by appending index.html to location + const locationIndexHtml = location.endsWith('/') + ? `${location}index.html` + : `${location}/index.html`; - let allSettled = Promise.resolve(); + // Remove leading slash from location + const subPath = locationIndexHtml.startsWith('/') ? locationIndexHtml.slice(1) : locationIndexHtml; - try { - for (const [location, locationRedirects] of redirectsByLocation) { - // Create S3 object path by appending index.html to location - const locationIndexHtml = location.endsWith('/') - ? `${location}index.html` - : `${location}/index.html`; + // Build metadata headers + const metadata = {}; - // Remove leading slash from location - const subPath = locationIndexHtml.startsWith('/') ? locationIndexHtml.slice(1) : locationIndexHtml; + locationRedirects.forEach((redirect, index) => { + const i = index + 1; // 1-based indexing as requested - // Build metadata headers - const metadata = {}; + // Add redirect location header + metadata[`redirect-location-${i}`] = redirect.redirect; - locationRedirects.forEach((redirect, index) => { - const i = index + 1; // 1-based indexing as requested - - // Add redirect location header - metadata[`redirect-location-${i}`] = redirect.redirect; - - // Add pattern header if it exists - if (redirect.pattern !== undefined) { - metadata[`redirect-pattern-${i}`] = redirect.pattern; - } - }); - - // Create or update the S3 object - const task = createOrUpdateS3ObjectAsync(dryRun, bucket, prefix, subPath, metadata, newFileTemplate); - yield task; - // throw away task success/failure info, just keep task settled status - const taskSettled = task.then(() => { }, () => { }); - allSettled = allSettled.then(() => taskSettled); - } - } finally { - allSettled.then(() => { - // Clean up temporary file - if (fs.existsSync(newFileTemplate)) { - fs.unlinkSync(newFileTemplate); + // Add pattern header if it exists + if (redirect.pattern !== undefined) { + metadata[`redirect-pattern-${i}`] = redirect.pattern; } }); + + // Create or update the S3 object + yield createOrUpdateS3ObjectAsync(dryRun, bucket, prefix, subPath, metadata, newFileTemplate); } } From d4c631fc01e4cf018333e7d52bb7ed838ccccc12 Mon Sep 17 00:00:00 2001 From: James Johnson Date: Wed, 5 Nov 2025 12:46:36 +1000 Subject: [PATCH 18/54] TCLOUD-4780: Add additional metadata header to indicate 404s This solves a problem when a file created to indicate conditional redirects does not trigger a redirect but instead returns the dummy content. --- .github/workflows/scripts/generate_redirects.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/scripts/generate_redirects.mjs b/.github/workflows/scripts/generate_redirects.mjs index d9b62e8236..e68a951069 100644 --- a/.github/workflows/scripts/generate_redirects.mjs +++ b/.github/workflows/scripts/generate_redirects.mjs @@ -116,7 +116,8 @@ const createNewS3ObjectAsync = async (dryRun, bucket, prefix, subPath, metadata, '--key', fullPath, '--body', newFileTemplate, '--content-type', 'text/html', - ...metadataArgs(metadata) + '--if-none-match', fullPath, + ...metadataArgs({...metadata, 'redirect-failure': 'not-found'}) ]; const result = await (dryRun ? dryRunSuccess(cmd) : spawnAsync(cmd)); return { From 8f8e95777691197737572674ca23bb1962560244 Mon Sep 17 00:00:00 2001 From: James Johnson Date: Wed, 5 Nov 2025 12:47:26 +1000 Subject: [PATCH 19/54] TCLOUD-4780 Support atomic updates --- .github/workflows/preview_create.yml | 9 +++++++-- .github/workflows/scripts/generate_redirects.mjs | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index 377f1d2aa8..e345470a64 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -59,7 +59,12 @@ jobs: - name: Upload website preview to S3 # put in the docs subfolder as we rely on that path in redirects run: | - aws s3 sync --delete ./build s3://tiny-cloud-antora-docs-preview/pr${PR_NUMBER} + aws s3 sync ./build s3://tiny-cloud-antora-docs-preview/pr${PR_NUMBER}/${GITHUB_RUN_ATTEMPT} - name: Create redirects on S3 - run: node ./.github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview pr${PR_NUMBER} \ No newline at end of file + run: | + node .github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview pr${PR_NUMBER}/${GITHUB_RUN_ATTEMPT} + + - name: Update pointer to current run output # put in the docs subfolder as we rely on that path in redirects + run: | + aws s3api put-object --bucket tiny-cloud-antora-docs-preview --key pr${PR_NUMBER}/index.html --body .github/workflows/scripts/empty.html --content-type text/html --metadata pointer=${GITHUB_RUN_ATTEMPT} \ No newline at end of file diff --git a/.github/workflows/scripts/generate_redirects.mjs b/.github/workflows/scripts/generate_redirects.mjs index e68a951069..e8fd55b317 100644 --- a/.github/workflows/scripts/generate_redirects.mjs +++ b/.github/workflows/scripts/generate_redirects.mjs @@ -339,7 +339,7 @@ const main = async () => { return Promise.reject(`Invalid bucket name, got ${bucket}`); } - if (!/^[a-z0-9\.-]+$/.test(prefix)) { + if (!/^[a-z0-9\.-]+(\/[a-z0-9\.-]+)*$/.test(prefix)) { return Promise.reject(`Invalid prefix, got ${prefix}`); } From 3a992a93423f190190664ecb31ac13185d3dd1a3 Mon Sep 17 00:00:00 2001 From: James Johnson Date: Wed, 5 Nov 2025 12:58:35 +1000 Subject: [PATCH 20/54] TCLOUD-4780: Remove `--if-none-match` as it seems unsupported --- .github/workflows/scripts/generate_redirects.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/scripts/generate_redirects.mjs b/.github/workflows/scripts/generate_redirects.mjs index e8fd55b317..363b54bbfc 100644 --- a/.github/workflows/scripts/generate_redirects.mjs +++ b/.github/workflows/scripts/generate_redirects.mjs @@ -116,7 +116,6 @@ const createNewS3ObjectAsync = async (dryRun, bucket, prefix, subPath, metadata, '--key', fullPath, '--body', newFileTemplate, '--content-type', 'text/html', - '--if-none-match', fullPath, ...metadataArgs({...metadata, 'redirect-failure': 'not-found'}) ]; const result = await (dryRun ? dryRunSuccess(cmd) : spawnAsync(cmd)); From da882858e53c7309b0a4e383727f183a5989d685 Mon Sep 17 00:00:00 2001 From: James Johnson Date: Wed, 5 Nov 2025 12:59:52 +1000 Subject: [PATCH 21/54] TCLOUD-4780: Prefix the run number so it's more readable --- .github/workflows/preview_create.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index e345470a64..4a2460ea79 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -14,6 +14,7 @@ permissions: env: PR_NUMBER: ${{ github.event.number }} + RUN_NUMBER: ${{ github.run_attempt }} jobs: @@ -59,12 +60,12 @@ jobs: - name: Upload website preview to S3 # put in the docs subfolder as we rely on that path in redirects run: | - aws s3 sync ./build s3://tiny-cloud-antora-docs-preview/pr${PR_NUMBER}/${GITHUB_RUN_ATTEMPT} + aws s3 sync ./build s3://tiny-cloud-antora-docs-preview/pr${PR_NUMBER}/run${RUN_NUMBER} - name: Create redirects on S3 run: | - node .github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview pr${PR_NUMBER}/${GITHUB_RUN_ATTEMPT} + node .github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview pr${PR_NUMBER}/run${RUN_NUMBER} - name: Update pointer to current run output # put in the docs subfolder as we rely on that path in redirects run: | - aws s3api put-object --bucket tiny-cloud-antora-docs-preview --key pr${PR_NUMBER}/index.html --body .github/workflows/scripts/empty.html --content-type text/html --metadata pointer=${GITHUB_RUN_ATTEMPT} \ No newline at end of file + aws s3api put-object --bucket tiny-cloud-antora-docs-preview --key pr${PR_NUMBER}/index.html --body .github/workflows/scripts/empty.html --content-type text/html --metadata pointer=run${RUN_NUMBER} \ No newline at end of file From e611136bab77b5cbdcc974ea11f5cac56102c45c Mon Sep 17 00:00:00 2001 From: James Johnson Date: Wed, 5 Nov 2025 13:11:17 +1000 Subject: [PATCH 22/54] TCLOUD-4780: Improve output prefix --- .github/workflows/preview_create.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index 4a2460ea79..bf91e764b6 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -13,8 +13,8 @@ permissions: id-token: write env: - PR_NUMBER: ${{ github.event.number }} - RUN_NUMBER: ${{ github.run_attempt }} + PR: pr-${{ github.event.number }} + RUN: run-${{ github.run_number }}-${{ github.run_attempt }} jobs: @@ -60,12 +60,12 @@ jobs: - name: Upload website preview to S3 # put in the docs subfolder as we rely on that path in redirects run: | - aws s3 sync ./build s3://tiny-cloud-antora-docs-preview/pr${PR_NUMBER}/run${RUN_NUMBER} + aws s3 sync ./build s3://tiny-cloud-antora-docs-preview/${PR}/{RUN} - name: Create redirects on S3 run: | - node .github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview pr${PR_NUMBER}/run${RUN_NUMBER} + node .github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview ${PR}/${RUN} - name: Update pointer to current run output # put in the docs subfolder as we rely on that path in redirects run: | - aws s3api put-object --bucket tiny-cloud-antora-docs-preview --key pr${PR_NUMBER}/index.html --body .github/workflows/scripts/empty.html --content-type text/html --metadata pointer=run${RUN_NUMBER} \ No newline at end of file + aws s3api put-object --bucket tiny-cloud-antora-docs-preview --key ${PR}/index.html --body .github/workflows/scripts/empty.html --content-type text/html --metadata pointer=${RUN} \ No newline at end of file From 7c036237b26a5f602a32967684d1584fb94118af Mon Sep 17 00:00:00 2001 From: James Johnson Date: Wed, 5 Nov 2025 13:13:32 +1000 Subject: [PATCH 23/54] TCLOUD-4780: Fix typo --- .github/workflows/preview_create.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index bf91e764b6..943d368fe5 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -60,7 +60,7 @@ jobs: - name: Upload website preview to S3 # put in the docs subfolder as we rely on that path in redirects run: | - aws s3 sync ./build s3://tiny-cloud-antora-docs-preview/${PR}/{RUN} + aws s3 sync ./build s3://tiny-cloud-antora-docs-preview/${PR}/${RUN} - name: Create redirects on S3 run: | From dfe1c4ffcfdc28809131636fc3a3433007f722fd Mon Sep 17 00:00:00 2001 From: James Johnson Date: Wed, 5 Nov 2025 14:35:58 +1000 Subject: [PATCH 24/54] TCLOUD-4780: Try redirect without URL encoding --- redirects.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redirects.json b/redirects.json index 44db0c2e20..220137f501 100644 --- a/redirects.json +++ b/redirects.json @@ -670,7 +670,7 @@ "redirect": "/docs/tinymce/latest/spelling/" }, { - "location": "/docs/configure/url%20handling/", + "location": "/docs/configure/url handling/", "redirect": "/docs/tinymce/latest/" }, { From 5a3deb9cbf9c151fded873c0e48e0a39108fe6ef Mon Sep 17 00:00:00 2001 From: James Johnson Date: Wed, 5 Nov 2025 15:18:10 +1000 Subject: [PATCH 25/54] TCLOUD-4780: Reduce logging --- .../workflows/scripts/generate_redirects.mjs | 32 ++++--------------- 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/.github/workflows/scripts/generate_redirects.mjs b/.github/workflows/scripts/generate_redirects.mjs index 363b54bbfc..daa4f2d999 100644 --- a/.github/workflows/scripts/generate_redirects.mjs +++ b/.github/workflows/scripts/generate_redirects.mjs @@ -38,7 +38,6 @@ const spawnAsync = (cmd) => new Promise((resolve) => { * @returns {Promise<{ cmd: string[], stdout: string, stderr: string, status: number | null, signal: NodeJS.Signals | null, error?: Error}>} */ const dryRunSuccess = (cmd) => new Promise((resolve) => { - console.log('Starting ' + cmd.join(' ')); setTimeout(() => resolve({ cmd, stdout: '', stderr: '', status: 0, signal: null }), Math.floor(Math.random() * 100)); }); @@ -260,38 +259,19 @@ const makeRedirectObjects = async (dryRun, bucket, prefix, parallel, redirects) const tasks = parallelGenerator(parallel, generateRedirectObjectsAsync(dryRun, bucket, prefix, redirectsByLocation)); let processedCount = 0; for await (const taskResult of tasks) { - const { subPath, cmd, error } = taskResult; + const { subPath, cmd, error, copied } = taskResult; processedCount++; + console.log(`Processed ${((processedCount / redirectsByLocation.size) * 100).toFixed(1)}%: ${taskResult.subPath}`); if (error) { errorCount++; + console.error(`\nCommand: ${cmd.join(' ')}`); + console.error(`Error ${copied ? 'Updating' : 'Creating'} S3 object ${prefix}/${subPath}:`, error); + console.error(''); } else { successCount++; } - console.log(`\nProcessed ${processedCount} of ${redirectsByLocation.size}: ${taskResult.subPath}`); - const fullPath = `${prefix}/${subPath}`; - if (taskResult.copied) { - console.log(`Updating S3 object: ${fullPath}`); - console.log(`Command: ${cmd.join(' ')}`); - if (taskResult.error) { - console.error(`Error copying S3 object ${fullPath}:`, error); - } else { - console.log(`Successfully updated S3 object ${fullPath}`); - } - } else { - console.log(`Created new S3 object: ${fullPath}`); - console.log(`Command: ${cmd.join(' ')}`); - if (taskResult.error) { - console.error(`Error creating S3 object ${fullPath}:`, error); - } else { - console.log(`Successfully created S3 object ${fullPath}`); - } - } } - - console.log(`\nSummary:`); - console.log(`Successfully processed: ${successCount} objects`); - console.log(`Errors: ${errorCount} objects`); - console.log('Redirect object generation completed.'); + console.log(`\nFinished with ${errorCount} error(s)`); }; const usage = () => ` From 67392231d66bb7e021f7c85a54f813fda73fcaab Mon Sep 17 00:00:00 2001 From: James Johnson Date: Wed, 5 Nov 2025 15:18:28 +1000 Subject: [PATCH 26/54] TCLOUD-4780: Fix a strange redirect --- redirects.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redirects.json b/redirects.json index 220137f501..af7aa83242 100644 --- a/redirects.json +++ b/redirects.json @@ -671,7 +671,7 @@ }, { "location": "/docs/configure/url handling/", - "redirect": "/docs/tinymce/latest/" + "redirect": "/docs/tinymce/latest/url-handling/" }, { "location": "/docs/configure/url-handling/", From b9aa8c7d8fd4855b253b2be3f4896bd7ab4f1997 Mon Sep 17 00:00:00 2001 From: James Johnson Date: Wed, 5 Nov 2025 15:28:19 +1000 Subject: [PATCH 27/54] TCLOUD-4780: Try --parallel 5 --- .github/workflows/preview_create.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index 943d368fe5..928e511b33 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -64,7 +64,7 @@ jobs: - name: Create redirects on S3 run: | - node .github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview ${PR}/${RUN} + node .github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview ${PR}/${RUN} --parallel 5 - name: Update pointer to current run output # put in the docs subfolder as we rely on that path in redirects run: | From 0647c2bf52a11ef790e8bc9920ec84b427267dcc Mon Sep 17 00:00:00 2001 From: James Johnson Date: Wed, 5 Nov 2025 15:34:54 +1000 Subject: [PATCH 28/54] TCLOUD-4780: Try --parallel 2 --- .github/workflows/preview_create.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index 928e511b33..994c8f3b63 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -64,7 +64,7 @@ jobs: - name: Create redirects on S3 run: | - node .github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview ${PR}/${RUN} --parallel 5 + node .github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview ${PR}/${RUN} --parallel 2 - name: Update pointer to current run output # put in the docs subfolder as we rely on that path in redirects run: | From feff106916acb8ad883235cfafee2e2aaea8cc78 Mon Sep 17 00:00:00 2001 From: James Johnson Date: Wed, 5 Nov 2025 15:46:17 +1000 Subject: [PATCH 29/54] TCLOUD-4780: Try --parallel 10 --- .github/workflows/preview_create.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index 994c8f3b63..326d9bb6d6 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -64,7 +64,7 @@ jobs: - name: Create redirects on S3 run: | - node .github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview ${PR}/${RUN} --parallel 2 + node .github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview ${PR}/${RUN} --parallel 10 - name: Update pointer to current run output # put in the docs subfolder as we rely on that path in redirects run: | From f0cbfbe6febea495bfba6dccccc105692f1f793a Mon Sep 17 00:00:00 2001 From: James Johnson Date: Wed, 5 Nov 2025 15:53:30 +1000 Subject: [PATCH 30/54] TCLOUD-4780: Try --parallel 8 --- .github/workflows/preview_create.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index 326d9bb6d6..c30ce6be56 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -64,7 +64,7 @@ jobs: - name: Create redirects on S3 run: | - node .github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview ${PR}/${RUN} --parallel 10 + node .github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview ${PR}/${RUN} --parallel 8 - name: Update pointer to current run output # put in the docs subfolder as we rely on that path in redirects run: | From 374943b99591da491f5dc86c2bed198a84aa201a Mon Sep 17 00:00:00 2001 From: James Johnson Date: Wed, 5 Nov 2025 16:01:01 +1000 Subject: [PATCH 31/54] TCLOUD-4780: Try --parallel 15 --- .github/workflows/preview_create.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index c30ce6be56..b1238ae3c0 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -64,7 +64,7 @@ jobs: - name: Create redirects on S3 run: | - node .github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview ${PR}/${RUN} --parallel 8 + node .github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview ${PR}/${RUN} --parallel 15 - name: Update pointer to current run output # put in the docs subfolder as we rely on that path in redirects run: | From c422864fe47bf56f9dca33ff2d25d75d342a7be8 Mon Sep 17 00:00:00 2001 From: James Johnson Date: Wed, 5 Nov 2025 16:08:35 +1000 Subject: [PATCH 32/54] TCLOUD-4780: Try --parallel 12 --- .github/workflows/preview_create.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index b1238ae3c0..9958c14da1 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -64,7 +64,7 @@ jobs: - name: Create redirects on S3 run: | - node .github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview ${PR}/${RUN} --parallel 15 + node .github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview ${PR}/${RUN} --parallel 12 - name: Update pointer to current run output # put in the docs subfolder as we rely on that path in redirects run: | From ec76654c79b24d9ccc093221e579d788a8ece2ff Mon Sep 17 00:00:00 2001 From: James Johnson Date: Wed, 5 Nov 2025 16:16:22 +1000 Subject: [PATCH 33/54] TCLOUD-4780: Try --parallel 11 --- .github/workflows/preview_create.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index 9958c14da1..43476a96c8 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -64,7 +64,7 @@ jobs: - name: Create redirects on S3 run: | - node .github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview ${PR}/${RUN} --parallel 12 + node .github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview ${PR}/${RUN} --parallel 11 - name: Update pointer to current run output # put in the docs subfolder as we rely on that path in redirects run: | From c2e357560ebf250f306df1292f98fecf6a24e827 Mon Sep 17 00:00:00 2001 From: James Johnson Date: Wed, 5 Nov 2025 16:23:56 +1000 Subject: [PATCH 34/54] TCLOUD-4780: Try --parallel 9 --- .github/workflows/preview_create.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index 43476a96c8..943f4e0798 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -64,7 +64,7 @@ jobs: - name: Create redirects on S3 run: | - node .github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview ${PR}/${RUN} --parallel 11 + node .github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview ${PR}/${RUN} --parallel 9 - name: Update pointer to current run output # put in the docs subfolder as we rely on that path in redirects run: | From 6e4abdd72c5cf05a8c593560419b1ea578fda0ff Mon Sep 17 00:00:00 2001 From: James Johnson Date: Wed, 5 Nov 2025 16:37:08 +1000 Subject: [PATCH 35/54] TCLOUD-4780: Try --parallel 10 again --- .github/workflows/preview_create.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index 943f4e0798..326d9bb6d6 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -64,7 +64,7 @@ jobs: - name: Create redirects on S3 run: | - node .github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview ${PR}/${RUN} --parallel 9 + node .github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview ${PR}/${RUN} --parallel 10 - name: Update pointer to current run output # put in the docs subfolder as we rely on that path in redirects run: | From 0d5d153e3c7be733643d84b0ee7e9e5c6bd83180 Mon Sep 17 00:00:00 2001 From: James Johnson Date: Wed, 5 Nov 2025 16:45:35 +1000 Subject: [PATCH 36/54] TCLOUD-4780: Try --parallel 4 --- .github/workflows/preview_create.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index 326d9bb6d6..6d5f9f2008 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -64,7 +64,7 @@ jobs: - name: Create redirects on S3 run: | - node .github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview ${PR}/${RUN} --parallel 10 + node .github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview ${PR}/${RUN} --parallel 4 - name: Update pointer to current run output # put in the docs subfolder as we rely on that path in redirects run: | From 3616f8394e56f484bcf7922ec670cb13777ebb76 Mon Sep 17 00:00:00 2001 From: James Johnson Date: Wed, 5 Nov 2025 16:55:37 +1000 Subject: [PATCH 37/54] TCLOUD-4780: Try --parallel 8 again --- .github/workflows/preview_create.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index 6d5f9f2008..c30ce6be56 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -64,7 +64,7 @@ jobs: - name: Create redirects on S3 run: | - node .github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview ${PR}/${RUN} --parallel 4 + node .github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview ${PR}/${RUN} --parallel 8 - name: Update pointer to current run output # put in the docs subfolder as we rely on that path in redirects run: | From e5dfdb83b9e3054e1463ecd7d8f776ce5153bdba Mon Sep 17 00:00:00 2001 From: James Johnson Date: Wed, 5 Nov 2025 17:04:23 +1000 Subject: [PATCH 38/54] TCLOUD-4780: Try --parallel 6 --- .github/workflows/preview_create.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index c30ce6be56..3fab5fdb92 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -64,7 +64,7 @@ jobs: - name: Create redirects on S3 run: | - node .github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview ${PR}/${RUN} --parallel 8 + node .github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview ${PR}/${RUN} --parallel 6 - name: Update pointer to current run output # put in the docs subfolder as we rely on that path in redirects run: | From 217c872854afa52a4edb37c8d2423a37e828cf80 Mon Sep 17 00:00:00 2001 From: James Johnson Date: Wed, 5 Nov 2025 17:15:21 +1000 Subject: [PATCH 39/54] TCLOUD-4780: Try --parallel 7 --- .github/workflows/preview_create.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index 3fab5fdb92..0ee8c6c3e5 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -64,7 +64,7 @@ jobs: - name: Create redirects on S3 run: | - node .github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview ${PR}/${RUN} --parallel 6 + node .github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview ${PR}/${RUN} --parallel 7 - name: Update pointer to current run output # put in the docs subfolder as we rely on that path in redirects run: | From 36b3fe42a4aa4781c789b379d5044a8a6e7968b1 Mon Sep 17 00:00:00 2001 From: James Johnson Date: Wed, 5 Nov 2025 17:23:36 +1000 Subject: [PATCH 40/54] TCLOUD-4780: Try --parallel 5 again --- .github/workflows/preview_create.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index 0ee8c6c3e5..928e511b33 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -64,7 +64,7 @@ jobs: - name: Create redirects on S3 run: | - node .github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview ${PR}/${RUN} --parallel 7 + node .github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview ${PR}/${RUN} --parallel 5 - name: Update pointer to current run output # put in the docs subfolder as we rely on that path in redirects run: | From 44b6dbc916eeff5868c7e3a552d429ff2e70cbef Mon Sep 17 00:00:00 2001 From: James Johnson Date: Mon, 10 Nov 2025 11:02:09 +1000 Subject: [PATCH 41/54] TCLOUD-4780: Move the generation of redirects to an action --- .github/workflows/preview_create.yml | 13 +- .github/workflows/preview_delete.yml | 4 +- .github/workflows/scripts/empty.html | 1 - .../workflows/scripts/generate_redirects.mjs | 338 ------------------ 4 files changed, 11 insertions(+), 345 deletions(-) delete mode 100644 .github/workflows/scripts/empty.html delete mode 100644 .github/workflows/scripts/generate_redirects.mjs diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index 928e511b33..7c5ce62950 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -43,11 +43,11 @@ jobs: - name: Build Website run: yarn antora ./antora-playbook.yml - - name: Rename site + - name: Rename site folder to docs run: | mv ./build/site ./build/docs - - name: Rename sitemap.xml + - name: Rename sitemap.xml to antora-sitemap.xml run: | mv ./build/docs/sitemap.xml ./build/docs/antora-sitemap.xml @@ -63,8 +63,13 @@ jobs: aws s3 sync ./build s3://tiny-cloud-antora-docs-preview/${PR}/${RUN} - name: Create redirects on S3 - run: | - node .github/workflows/scripts/generate_redirects.mjs tiny-cloud-antora-docs-preview ${PR}/${RUN} --parallel 5 + uses: tinymce/tinymce-docs-generate-redirects-action@feature/TCLOUD-4780 + with: + build: ./build/ + redirects: ./redirects.json + bucket: tiny-cloud-antora-docs-preview + prefix: ${PR}/${RUN} + parallel: 10 - name: Update pointer to current run output # put in the docs subfolder as we rely on that path in redirects run: | diff --git a/.github/workflows/preview_delete.yml b/.github/workflows/preview_delete.yml index ed1c5822df..eb68a734d9 100644 --- a/.github/workflows/preview_delete.yml +++ b/.github/workflows/preview_delete.yml @@ -10,7 +10,7 @@ permissions: id-token: write env: - PR_NUMBER: ${{ github.event.number }} + PR: pr-${{ github.event.number }} jobs: cleanup: @@ -41,4 +41,4 @@ jobs: - name: Remove website preview from S3 run: | - aws s3 rm s3://tiny-cloud-antora-docs-preview/pr${PR_NUMBER} + aws s3 rm s3://tiny-cloud-antora-docs-preview/${PR} diff --git a/.github/workflows/scripts/empty.html b/.github/workflows/scripts/empty.html deleted file mode 100644 index 09ce0846fd..0000000000 --- a/.github/workflows/scripts/empty.html +++ /dev/null @@ -1 +0,0 @@ -? \ No newline at end of file diff --git a/.github/workflows/scripts/generate_redirects.mjs b/.github/workflows/scripts/generate_redirects.mjs deleted file mode 100644 index daa4f2d999..0000000000 --- a/.github/workflows/scripts/generate_redirects.mjs +++ /dev/null @@ -1,338 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; -import { spawn } from 'node:child_process'; - -/** - * - * @param {string[]} cmd - * @returns {Promise<{ cmd: string[], stdout: string, stderr: string, status: number | null, signal: NodeJS.Signals | null, error?: Error}>} - */ -const spawnAsync = (cmd) => new Promise((resolve) => { - const p = spawn(cmd[0], cmd.slice(1), { stdio: ['inherit', 'pipe', 'pipe'] }); - p.stdout.setEncoding('utf-8'); - let out = ''; - let err = ''; - p.stdout.on('data', (chunk) => { - out += chunk; - }); - p.stderr.on('data', (chunk) => { - err += chunk; - }); - p.on('exit', (code, signal) => { - const failed = code !== 0 || signal; - const data = { - cmd, - stdout: out, - stderr: err, - status: code, - signal: signal, - ...(failed ? { error: new Error(err) } : {}), - }; - resolve(data); - }); -}); - -/** - * - * @param {string[]} cmd - * @returns {Promise<{ cmd: string[], stdout: string, stderr: string, status: number | null, signal: NodeJS.Signals | null, error?: Error}>} - */ -const dryRunSuccess = (cmd) => new Promise((resolve) => { - setTimeout(() => resolve({ cmd, stdout: '', stderr: '', status: 0, signal: null }), Math.floor(Math.random() * 100)); -}); - - -/** - * Check if an S3 object exists - */ -function checkS3ObjectExists(_dryRun, _bucket, _prefix, subPath) { - // it's too slow to talk to s3, so just check the local files we just uploaded... - if (subPath.startsWith('docs/')) { - return fs.existsSync(path.join(import.meta.dirname, '../../../build', subPath)); - } else { - return false; - } -} - -/** - * - * @param {Record} metadata - * @returns - */ -const metadataArgs = (metadata) => { - // Build metadata string in the format key1=value1,key2=value2 - const metadataString = Object.entries(metadata) - .map(([key, value]) => `${key}=${value}`) - .join(','); - - return metadataString ? ['--metadata', metadataString] : []; -} - -/** - * - * @param {boolean} dryRun - * @param {string} bucket - * @param {string} prefix - * @param {string} subPath - * @param {Record} metadata - * @returns - */ -const copyS3ObjectWithMetadataAsync = async (dryRun, bucket, prefix, subPath, metadata) => { - const fullPath = `${prefix}/${subPath}`; - const cmd = [ - 'aws', 's3api', 'copy-object', - '--bucket', bucket, - '--copy-source', `${bucket}/${fullPath}`, - '--key', fullPath, - '--metadata-directive', 'REPLACE', - '--content-type', 'text/html', - ...metadataArgs(metadata) - ]; - const result = await (dryRun ? dryRunSuccess(cmd) : spawnAsync(cmd)); - return { - ...result, - copied: true, - subPath, - }; -}; - -/** - * - * @param {boolean} dryRun - * @param {string} bucket - * @param {string} prefix - * @param {string} subPath - * @param {Record} metadata - * @param {string} newFileTemplate - * @returns - */ -const createNewS3ObjectAsync = async (dryRun, bucket, prefix, subPath, metadata, newFileTemplate) => { - const fullPath = `${prefix}/${subPath}`; - // AWS CLI command to put object with metadata - const cmd = [ - 'aws', 's3api', 'put-object', - '--bucket', bucket, - '--key', fullPath, - '--body', newFileTemplate, - '--content-type', 'text/html', - ...metadataArgs({...metadata, 'redirect-failure': 'not-found'}) - ]; - const result = await (dryRun ? dryRunSuccess(cmd) : spawnAsync(cmd)); - return { - ...result, - copied: false, - subPath, - } -} - -/** - * - * @param {boolean} dryRun - * @param {string} bucket - * @param {string} prefix - * @param {string} subPath - * @param {Record} metadata - * @param {string} newFileTemplate - * @returns - */ -const createOrUpdateS3ObjectAsync = (dryRun, bucket, prefix, subPath, metadata, newFileTemplate) => { - // Check if object already exists - if (checkS3ObjectExists(dryRun, bucket, prefix, subPath)) { - return copyS3ObjectWithMetadataAsync(dryRun, bucket, prefix, subPath, metadata); - } else { - return createNewS3ObjectAsync(dryRun, bucket, prefix, subPath, metadata, newFileTemplate); - } -} - -/** - * - * @param {boolean} dryRun - * @param {string} bucket - * @param {string} prefix - * @param {Map} redirectsByLocation - */ -function* generateRedirectObjectsAsync(dryRun, bucket, prefix, redirectsByLocation) { - // create path to empty template - const newFileTemplate = path.join(import.meta.dirname, 'empty.html'); - - for (const [location, locationRedirects] of redirectsByLocation) { - // Create S3 object path by appending index.html to location - const locationIndexHtml = location.endsWith('/') - ? `${location}index.html` - : `${location}/index.html`; - - // Remove leading slash from location - const subPath = locationIndexHtml.startsWith('/') ? locationIndexHtml.slice(1) : locationIndexHtml; - - // Build metadata headers - const metadata = {}; - - locationRedirects.forEach((redirect, index) => { - const i = index + 1; // 1-based indexing as requested - - // Add redirect location header - metadata[`redirect-location-${i}`] = redirect.redirect; - - // Add pattern header if it exists - if (redirect.pattern !== undefined) { - metadata[`redirect-pattern-${i}`] = redirect.pattern; - } - }); - - // Create or update the S3 object - yield createOrUpdateS3ObjectAsync(dryRun, bucket, prefix, subPath, metadata, newFileTemplate); - } -} - -/** - * @template T - * @param {number} max - * @param {Generator, void, unknown>} source - * @returns {AsyncGenerator} - */ -async function* parallelGenerator(max, source) { - /** - * @param {number} i - * @param {IteratorResult, void>} task - * @returns {Promise<[number, IteratorResult]>} - */ - const wrap = (i, task) => new Promise((resolve) => { - if (task.done) { - resolve([i, { done: true }]); - } else { - task.value.then((v) => resolve([i, { done: false, value: v }])) - } - }) - /** @type {(Promise<[number, IteratorResult]>)[]} */ - let tasks = []; - for (let i = 0; i < max; i++) { - tasks.push(wrap(i, source.next())) - } - /** @type {(Promise<[number, IteratorResult]> | null)[]} */ - let tasksAndNull; - while (true) { - const [i, v] = await Promise.race(tasks); - if (v.done) { - // move the tasks over to the nullable list - tasksAndNull = tasks.splice(0, tasks.length); - tasksAndNull[i] = null; - break; - } else { - tasks[i] = wrap(i, source.next()); - yield v.value; - } - } - let filteredTasks = tasksAndNull.filter((v) => v !== null); - while (filteredTasks.length > 0) { - const [i, v] = await Promise.race(filteredTasks); - tasksAndNull[i] = null; - if (!v.done) { - yield v.value; - } - filteredTasks = tasksAndNull.filter((v) => v !== null); - } -} - -/** - * - * @param {boolean} dryRun - * @param {string} bucket - * @param {string} prefix - * @param {number} parallel - * @param {{location: string, pattern?: string, redirect: string}[]} redirects - */ -const makeRedirectObjects = async (dryRun, bucket, prefix, parallel, redirects) => { - let successCount = 0; - let errorCount = 0; - // Group redirects by location to handle multiple redirects for the same location - /** @type {Map} */ - const redirectsByLocation = new Map(); - - redirects.forEach((redirect) => { - const location = redirect.location; - if (!redirectsByLocation.has(location)) { - redirectsByLocation.set(location, []); - } - redirectsByLocation.get(location).push(redirect); - }); - - const tasks = parallelGenerator(parallel, generateRedirectObjectsAsync(dryRun, bucket, prefix, redirectsByLocation)); - let processedCount = 0; - for await (const taskResult of tasks) { - const { subPath, cmd, error, copied } = taskResult; - processedCount++; - console.log(`Processed ${((processedCount / redirectsByLocation.size) * 100).toFixed(1)}%: ${taskResult.subPath}`); - if (error) { - errorCount++; - console.error(`\nCommand: ${cmd.join(' ')}`); - console.error(`Error ${copied ? 'Updating' : 'Creating'} S3 object ${prefix}/${subPath}:`, error); - console.error(''); - } else { - successCount++; - } - } - console.log(`\nFinished with ${errorCount} error(s)`); -}; - -const usage = () => ` -generate-redirects [--dry-run] - Generate redirects in s3. - - Options: - --parallel number of s3 command to run in parallel; default: 5 - --dry-run only output the commands that will be run -` - -const main = async () => { - - const args = process.argv.slice(2); - const dryRun = (() => { - const idx = args.findIndex((arg) => arg === '--dry-run'); - if (idx !== -1) { - args.splice(idx, 1); - return true; - } - return false; - })(); - const parallelValue = (() => { - const idx = args.findIndex((arg) => arg === '--parallel'); - if (idx !== -1) { - const [_flag, num] = args.splice(idx, 2); - return num; - } - return null; - })(); - if (parallelValue != null && !/^\d+$/.test(parallelValue)) { - return Promise.reject(`Invalid value for --parallel, got ${parallelValue}`); - } - const parallel = parallelValue != null ? parseInt(parallelValue, 10) : 20; - if (args.length !== 2) { - return Promise.reject(`Expected 2 values, got ${args.length}`); - } - const [bucket, prefix] = args; - if (!/^[a-z0-9][a-z0-9\.-]{1,61}[a-z0-9]$/.test(bucket) || - /\.\./.test(bucket) || /^\d+\.\d+\.\d+\.\d+$/.test(bucket) || - /^xn--/.test(bucket) || /^sthree-/.test(bucket) || /^amzn-s3-demo-/.test(bucket) || - /-s3alias$/.test(bucket) || /--ol-s3$/.test(bucket) || /\.mrap$/.test(bucket) || - /--x-s3$/.test(bucket) || /--table-s3$/.test(bucket)) { - return Promise.reject(`Invalid bucket name, got ${bucket}`); - } - - if (!/^[a-z0-9\.-]+(\/[a-z0-9\.-]+)*$/.test(prefix)) { - return Promise.reject(`Invalid prefix, got ${prefix}`); - } - - /** - * @type {{location: string, pattern?: string, redirect: string}[]} - */ - const redirects = JSON.parse(fs.readFileSync(path.join(import.meta.dirname, '../../../redirects.json'), 'utf-8')); - - makeRedirectObjects(dryRun, bucket, prefix, parallel, redirects); -}; - - -main().catch((err) => { - console.error(err); - console.error(usage()); - process.exit(1); -}) \ No newline at end of file From 14a88e36c86599fdc435acfd0919825c98fdbac8 Mon Sep 17 00:00:00 2001 From: James Johnson Date: Mon, 10 Nov 2025 11:21:09 +1000 Subject: [PATCH 42/54] TCLOUD-4780: Try using a tag to reference the action --- .github/workflows/preview_create.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index 7c5ce62950..fe640fd02f 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -63,7 +63,7 @@ jobs: aws s3 sync ./build s3://tiny-cloud-antora-docs-preview/${PR}/${RUN} - name: Create redirects on S3 - uses: tinymce/tinymce-docs-generate-redirects-action@feature/TCLOUD-4780 + uses: tinymce/tinymce-docs-generate-redirects-action@v0.1 with: build: ./build/ redirects: ./redirects.json From e94a692ae0ca245ca3f4981ef16ebf3e359752be Mon Sep 17 00:00:00 2001 From: James Johnson Date: Mon, 10 Nov 2025 15:33:55 +1000 Subject: [PATCH 43/54] TCLOUD-4780: Maybe enable debug logs --- .github/workflows/preview_create.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index fe640fd02f..38d4a17c80 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -70,6 +70,8 @@ jobs: bucket: tiny-cloud-antora-docs-preview prefix: ${PR}/${RUN} parallel: 10 + env: + ACTIONS_STEP_DEBUG: true - name: Update pointer to current run output # put in the docs subfolder as we rely on that path in redirects run: | From 94464f7571264e4328f885a48c5a521b7a5e7b9e Mon Sep 17 00:00:00 2001 From: James Johnson Date: Mon, 10 Nov 2025 16:33:08 +1000 Subject: [PATCH 44/54] TCLOUD-4780: Fix calculation of prefix --- .github/workflows/preview_create.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index 38d4a17c80..5a9f01c188 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -68,10 +68,8 @@ jobs: build: ./build/ redirects: ./redirects.json bucket: tiny-cloud-antora-docs-preview - prefix: ${PR}/${RUN} + prefix: ${{ vars.PR }}/${{ vars.RUN }} parallel: 10 - env: - ACTIONS_STEP_DEBUG: true - name: Update pointer to current run output # put in the docs subfolder as we rely on that path in redirects run: | From 7aeb001cde372902d2640ad1ab1ce3e7906887da Mon Sep 17 00:00:00 2001 From: James Johnson Date: Mon, 10 Nov 2025 16:45:01 +1000 Subject: [PATCH 45/54] TCLOUD-4780: Try referring to env vars just by their name --- .github/workflows/preview_create.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index 5a9f01c188..4198a40c28 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -68,7 +68,7 @@ jobs: build: ./build/ redirects: ./redirects.json bucket: tiny-cloud-antora-docs-preview - prefix: ${{ vars.PR }}/${{ vars.RUN }} + prefix: ${{ PR }}/${{ RUN }} parallel: 10 - name: Update pointer to current run output # put in the docs subfolder as we rely on that path in redirects From 91f1cef1b4948c881ad95b332cab8382cbf5c1e5 Mon Sep 17 00:00:00 2001 From: James Johnson Date: Mon, 10 Nov 2025 16:46:11 +1000 Subject: [PATCH 46/54] TCLOUD-4780: Try prefixing env vars with "env." --- .github/workflows/preview_create.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index 4198a40c28..ea06248ab6 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -68,7 +68,7 @@ jobs: build: ./build/ redirects: ./redirects.json bucket: tiny-cloud-antora-docs-preview - prefix: ${{ PR }}/${{ RUN }} + prefix: ${{ env.PR }}/${{ env.RUN }} parallel: 10 - name: Update pointer to current run output # put in the docs subfolder as we rely on that path in redirects From 6dd03bf8fa6bb56886dd4424fe4e9bdd9ac3999e Mon Sep 17 00:00:00 2001 From: James Johnson Date: Mon, 10 Nov 2025 16:56:24 +1000 Subject: [PATCH 47/54] TCLOUD-4780: Change AWS region --- .github/workflows/preview_create.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index ea06248ab6..8a4093e69d 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -56,7 +56,7 @@ jobs: with: role-to-assume: arn:aws:iam::327995277200:role/staging-docs-preview-update role-session-name: docs-preview-update - aws-region: us-east-2 + aws-region: us-east-1 - name: Upload website preview to S3 # put in the docs subfolder as we rely on that path in redirects run: | From d3c0b047d87f595a435cf8aab19ba42a22b06f80 Mon Sep 17 00:00:00 2001 From: James Johnson Date: Mon, 10 Nov 2025 17:04:09 +1000 Subject: [PATCH 48/54] TCLOUD-4780: Fix creation of pointer file --- .github/workflows/preview_create.yml | 2 +- .github/workflows/resources/empty.html | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/resources/empty.html diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index 8a4093e69d..3e9a8cbee7 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -73,4 +73,4 @@ jobs: - name: Update pointer to current run output # put in the docs subfolder as we rely on that path in redirects run: | - aws s3api put-object --bucket tiny-cloud-antora-docs-preview --key ${PR}/index.html --body .github/workflows/scripts/empty.html --content-type text/html --metadata pointer=${RUN} \ No newline at end of file + aws s3api put-object --bucket tiny-cloud-antora-docs-preview --key ${PR}/index.html --body .github/workflows/resources/empty.html --content-type text/html --metadata pointer=${RUN} \ No newline at end of file diff --git a/.github/workflows/resources/empty.html b/.github/workflows/resources/empty.html new file mode 100644 index 0000000000..09ce0846fd --- /dev/null +++ b/.github/workflows/resources/empty.html @@ -0,0 +1 @@ +? \ No newline at end of file From caefc9374f30ff244dec4e790f0b3ba434c77d78 Mon Sep 17 00:00:00 2001 From: James Johnson Date: Mon, 10 Nov 2025 17:52:26 +1000 Subject: [PATCH 49/54] TCLOUD-4780: Try loading redirects from a URL --- .github/workflows/preview_create.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index 3e9a8cbee7..8eec33d0f7 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -66,7 +66,7 @@ jobs: uses: tinymce/tinymce-docs-generate-redirects-action@v0.1 with: build: ./build/ - redirects: ./redirects.json + redirects: https://raw.githubusercontent.com/tinymce/tinymce-docs/refs/heads/feature/TCLOUD-4780/redirects.json bucket: tiny-cloud-antora-docs-preview prefix: ${{ env.PR }}/${{ env.RUN }} parallel: 10 From 6ba55367b419302dde26b86c037e66e71aeaa393 Mon Sep 17 00:00:00 2001 From: James Johnson Date: Mon, 10 Nov 2025 18:00:03 +1000 Subject: [PATCH 50/54] TCLOUD-4780: Revert to using local file path for redirects --- .github/workflows/preview_create.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index 8eec33d0f7..3e9a8cbee7 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -66,7 +66,7 @@ jobs: uses: tinymce/tinymce-docs-generate-redirects-action@v0.1 with: build: ./build/ - redirects: https://raw.githubusercontent.com/tinymce/tinymce-docs/refs/heads/feature/TCLOUD-4780/redirects.json + redirects: ./redirects.json bucket: tiny-cloud-antora-docs-preview prefix: ${{ env.PR }}/${{ env.RUN }} parallel: 10 From f700f813f3dde02ba004373c52cb272e463938bc Mon Sep 17 00:00:00 2001 From: James Johnson Date: Wed, 19 Nov 2025 14:37:19 +1000 Subject: [PATCH 51/54] TCLOUD-4780: Use v1.0 of the tinymce-docs-generate-redirects-action --- .github/workflows/preview_create.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index 3e9a8cbee7..e4d0f88845 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -63,7 +63,7 @@ jobs: aws s3 sync ./build s3://tiny-cloud-antora-docs-preview/${PR}/${RUN} - name: Create redirects on S3 - uses: tinymce/tinymce-docs-generate-redirects-action@v0.1 + uses: tinymce/tinymce-docs-generate-redirects-action@v1.0 with: build: ./build/ redirects: ./redirects.json From ebbe9493b6a496955fde2e649b404134223e48ce Mon Sep 17 00:00:00 2001 From: James Johnson Date: Wed, 19 Nov 2025 14:37:33 +1000 Subject: [PATCH 52/54] TCLOUD-4780: Remove out of date comments --- .github/workflows/preview_create.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index e4d0f88845..f6ffe16b8b 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -58,7 +58,7 @@ jobs: role-session-name: docs-preview-update aws-region: us-east-1 - - name: Upload website preview to S3 # put in the docs subfolder as we rely on that path in redirects + - name: Upload website preview to S3 run: | aws s3 sync ./build s3://tiny-cloud-antora-docs-preview/${PR}/${RUN} @@ -71,6 +71,6 @@ jobs: prefix: ${{ env.PR }}/${{ env.RUN }} parallel: 10 - - name: Update pointer to current run output # put in the docs subfolder as we rely on that path in redirects + - name: Update pointer to current run output run: | aws s3api put-object --bucket tiny-cloud-antora-docs-preview --key ${PR}/index.html --body .github/workflows/resources/empty.html --content-type text/html --metadata pointer=${RUN} \ No newline at end of file From 5ba383632b6ac02294d3093fe905beab2c65c644 Mon Sep 17 00:00:00 2001 From: James Johnson Date: Wed, 19 Nov 2025 14:42:32 +1000 Subject: [PATCH 53/54] TCLOUD-4780: Update assumed role The service was renamed to tinymce-docs and so the role was updated to match. --- .github/workflows/preview_create.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/preview_create.yml b/.github/workflows/preview_create.yml index f6ffe16b8b..03c3a7760e 100644 --- a/.github/workflows/preview_create.yml +++ b/.github/workflows/preview_create.yml @@ -54,8 +54,8 @@ jobs: - name: configure aws credentials uses: aws-actions/configure-aws-credentials@v5.0.0 with: - role-to-assume: arn:aws:iam::327995277200:role/staging-docs-preview-update - role-session-name: docs-preview-update + role-to-assume: arn:aws:iam::327995277200:role/staging-tinymce-docs-update + role-session-name: tinymce-docs-update aws-region: us-east-1 - name: Upload website preview to S3 From 6d5df99f8acca253102b3c295fe483ca8db9d2f4 Mon Sep 17 00:00:00 2001 From: James Johnson Date: Wed, 19 Nov 2025 15:25:46 +1000 Subject: [PATCH 54/54] TCLOUD-4780: Fix codeql's complaint about no Javascript --- .github/workflows/resources/unused.js | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .github/workflows/resources/unused.js diff --git a/.github/workflows/resources/unused.js b/.github/workflows/resources/unused.js new file mode 100644 index 0000000000..8befc6da26 --- /dev/null +++ b/.github/workflows/resources/unused.js @@ -0,0 +1,2 @@ +// Give CodeQL something to scan so it doesn't complain there is no Javascript... +console.log('Hello world'); \ No newline at end of file