-
Notifications
You must be signed in to change notification settings - Fork 3
feat: add hlx6 support for api.aem.live content routing #242
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
f057144
bf254ed
1eb95c9
d67853a
3449993
f266c20
309cc2d
e5d5146
91c565e
237e555
df3c112
e52569b
65c2cd0
7b65a91
cbba3ba
29a7757
3d12ed8
0222c9f
a70838a
ecf9410
6f5fc11
74b1399
0cc671e
12e6198
42d21ba
a6b7579
d769ad6
339fb37
d25d4c1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,6 +28,53 @@ import { BRANCH_NOT_FOUND_HTML_MESSAGE, DEFAULT_HTML_TEMPLATE, UNAUTHORIZED_HTML | |
| import { getSiteConfig } from '../storage/config.js'; | ||
| import { restoreAbsoluteImages } from '../render/rewrite-images.js'; | ||
|
|
||
| const AEM_API = 'https://api.aem.live'; | ||
| const HLX_ADMIN = 'https://admin.hlx.page'; | ||
| const UPGRADE_PROBE_TIMEOUT = 2 * 1000; | ||
|
|
||
| function aemApiSourceUrl(org, site, path) { | ||
| return `${AEM_API}/${org}/sites/${site}/source${path}`; | ||
| } | ||
|
|
||
| async function probeHlx6(org, site) { | ||
| const pingUrl = `${HLX_ADMIN}/ping/${org}/${site}`; | ||
| try { | ||
| const response = await fetch(pingUrl, { | ||
| signal: AbortSignal.timeout(UPGRADE_PROBE_TIMEOUT), | ||
| }); | ||
| if (response.status !== 200) { | ||
| return undefined; | ||
| } | ||
| return response.headers.get('x-api-upgrade-available') === 'true'; | ||
| } catch { | ||
| return undefined; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Resolves the write backend after the site probe fails. | ||
| * Both backends are HEADed concurrently. A source-bus 200 selects api.aem.live. | ||
| * Every other result selects da-admin, including a missing document or failed HEAD. | ||
| * | ||
| * @returns {Promise<boolean>} true for api.aem.live, false for da-admin | ||
| */ | ||
| async function resolveUncertainWriteBackend({ | ||
| org, site, sourcePath, authToken, env, | ||
| }) { | ||
| const headers = new Headers({ Authorization: authToken }); | ||
| const aemUrl = aemApiSourceUrl(org, site, sourcePath); | ||
| const daUrl = new URL(`/source/${org}/${site}${sourcePath}`, env.DA_ADMIN); | ||
|
|
||
| const [aemResult] = await Promise.allSettled([ | ||
| fetch(aemUrl, { method: 'HEAD', headers }), | ||
| env.daadmin.fetch(daUrl, { method: 'HEAD', headers }), | ||
| ]); | ||
| if (aemResult.status === 'fulfilled' && aemResult.value.status === 200) { | ||
| return true; | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| async function getFileBody(data) { | ||
| const text = await data.text(); | ||
| return { body: text, type: data.type }; | ||
|
|
@@ -90,11 +137,21 @@ export async function daSourceGet({ req, env, daCtx }) { | |
| const headers = new Headers(); | ||
| headers.set('Authorization', authToken); | ||
|
|
||
| const hlx6Promise = probeHlx6(org, site); | ||
|
|
||
| if (ext !== 'html') { | ||
| /* | ||
| for non-HTML files, simply proxy the request without processing | ||
| and ensure that extensions are not duplicated | ||
| */ | ||
| const hlx6 = await hlx6Promise; | ||
| if (hlx6) { | ||
| const sourceUrl = aemApiSourceUrl(org, site, path); | ||
| console.log(`-> ${sourceUrl}`); | ||
| const response = await fetch(sourceUrl, { headers }); | ||
| console.log(`<- ${sourceUrl}. ${response.status} ${response.statusText}`, { status: response.status, statusText: response.statusText }); | ||
| return response; | ||
| } | ||
| const adminUrl = new URL(`/source/${org}/${site}${path}`, env.DA_ADMIN); | ||
| console.log(`-> ${adminUrl.toString()}`); | ||
| const response = await env.daadmin.fetch(adminUrl, { method: 'GET', headers }); | ||
|
|
@@ -104,7 +161,10 @@ export async function daSourceGet({ req, env, daCtx }) { | |
|
|
||
| // get the AEM parts (head.html) | ||
| const aemCtx = getAemCtx(env, daCtx); | ||
| const headHtml = await getAEMHtml(aemCtx, '/head.html'); | ||
| const [headHtml, hlx6] = await Promise.all([ | ||
| getAEMHtml(aemCtx, '/head.html'), | ||
|
benpeter marked this conversation as resolved.
|
||
| hlx6Promise, | ||
| ]); | ||
| if (!headHtml) { | ||
| // quick-edit still needs a working shell (with the import map) so the editor | ||
| // can load into this page, even when the AEM branch doesn't exist yet. | ||
|
|
@@ -114,24 +174,30 @@ export async function daSourceGet({ req, env, daCtx }) { | |
| return get404(BRANCH_NOT_FOUND_HTML_MESSAGE); | ||
| } | ||
|
benpeter marked this conversation as resolved.
|
||
|
|
||
| // get the content from DA admin | ||
| const adminUrl = new URL( | ||
| `/source/${org}/${site}${path}.${ext}`, | ||
| env.DA_ADMIN, | ||
| ); | ||
| let sourceResp; | ||
| if (hlx6) { | ||
| const sourceUrl = aemApiSourceUrl(org, site, `${path}.${ext}`); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. but |
||
| console.log(`-> ${sourceUrl}`); | ||
| sourceResp = await fetch(sourceUrl, { headers }); | ||
| console.log(`<- ${sourceUrl}. ${sourceResp.status} ${sourceResp.statusText}`, { status: sourceResp.status, statusText: sourceResp.statusText }); | ||
| } else { | ||
| const adminUrl = new URL( | ||
| `/source/${org}/${site}${path}.${ext}`, | ||
| env.DA_ADMIN, | ||
| ); | ||
| console.log(`-> ${adminUrl.toString()}`); | ||
| const daAdminResp = await env.daadmin.fetch(adminUrl, { headers }); | ||
| console.log(`<- ${adminUrl.toString()}. ${daAdminResp.status} ${daAdminResp.statusText}`, { status: daAdminResp.status, statusText: daAdminResp.statusText }); | ||
| sourceResp = daAdminResp; | ||
| } | ||
|
|
||
| // eslint-disable-next-line no-param-reassign | ||
| req = new Request(adminUrl, { | ||
| method: 'GET', | ||
| headers, | ||
| }); | ||
| console.log(`-> ${adminUrl.toString()}`); | ||
| const daAdminResp = await env.daadmin.fetch(req); | ||
| console.log(`<- ${adminUrl.toString()}. ${daAdminResp.status} ${daAdminResp.statusText}`, { status: daAdminResp.status, statusText: daAdminResp.statusText }); | ||
| if (sourceResp.status !== 200 && sourceResp.status !== 404) { | ||
| return sourceResp; | ||
| } | ||
|
|
||
| // use the stored content when available, otherwise fall back to a template | ||
| const bodyHtml = daAdminResp && daAdminResp.status === 200 | ||
| ? await daAdminResp.text() | ||
| const bodyHtml = sourceResp.status === 200 | ||
| ? await sourceResp.text() | ||
| : await getPageTemplate(env, daCtx, aemCtx, headHtml); | ||
|
|
||
| // compose the page the same way for every request type | ||
|
|
@@ -174,6 +240,15 @@ export async function daSourceHead({ env, daCtx }) { | |
| headers.set('Authorization', authToken); | ||
|
|
||
| const adminPath = ext !== 'html' ? path : `${path}.${ext}`; | ||
| const hlx6 = await probeHlx6(org, site); | ||
| if (hlx6) { | ||
| const sourceUrl = aemApiSourceUrl(org, site, adminPath); | ||
| console.log(`-> HEAD ${sourceUrl}`); | ||
| const response = await fetch(sourceUrl, { method: 'HEAD', headers }); | ||
| console.log(`<- HEAD ${sourceUrl}. ${response.status} ${response.statusText}`, { status: response.status, statusText: response.statusText }); | ||
| return response; | ||
| } | ||
|
|
||
| const adminUrl = new URL(`/source/${org}/${site}${adminPath}`, env.DA_ADMIN); | ||
| console.log(`-> HEAD ${adminUrl.toString()}`); | ||
| const response = await env.daadmin.fetch(adminUrl, { method: 'HEAD', headers }); | ||
|
|
@@ -205,14 +280,42 @@ export async function daSourcePost({ req, env, daCtx }) { | |
|
|
||
| minifyWhitespace(bodyNode); | ||
|
|
||
| const bodyContent = toHtml(bodyNode); | ||
| // daCtx.path contains explicit file extensions; append only inferred HTML. | ||
| const sourcePath = ext !== 'html' ? path : `${path}.${ext}`; | ||
| let hlx6 = await probeHlx6(org, site); | ||
| if (hlx6 === undefined) { | ||
| hlx6 = await resolveUncertainWriteBackend({ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand this check. if it's not hlx6, then it's not hlx6...
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. but if the probe fails, why should the HEAD request then be a good alternative? and for a new file, |
||
| org, | ||
| site, | ||
| sourcePath, | ||
| authToken, | ||
| env, | ||
| }); | ||
| } | ||
| if (hlx6) { | ||
| const sourceUrl = aemApiSourceUrl(org, site, sourcePath); | ||
| const headers = { | ||
| Authorization: authToken, | ||
| 'Content-Type': 'text/html', | ||
| }; | ||
| console.log(`-> ${sourceUrl}`); | ||
| const response = await fetch(sourceUrl, { | ||
| method: 'POST', | ||
| body: bodyContent, | ||
| headers, | ||
| }); | ||
| console.log(`<- ${sourceUrl}. ${response.status} ${response.statusText}`, { status: response.status, statusText: response.statusText }); | ||
| return response; | ||
| } | ||
|
|
||
| // create new POST request with the body content | ||
| const body = new FormData(); | ||
| const bodyContent = toHtml(bodyNode); | ||
| const data = new Blob([bodyContent], { type: 'text/html' }); | ||
| body.set('data', data); | ||
| const headers = { Authorization: authToken }; | ||
| const adminUrl = new URL( | ||
| `/source/${org}/${site}${path}.${ext}`, | ||
| `/source/${org}/${site}${sourcePath}`, | ||
| env.DA_ADMIN, | ||
| ); | ||
| // eslint-disable-next-line no-param-reassign | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.