Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions libs/blocks/preflight/checks/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ export const STATUS = {
EMPTY: 'empty',
};

export const STATUS_TO_ICON_MAP = {
[STATUS.PASS]: 'green',
[STATUS.FAIL]: 'red',
[STATUS.LIMBO]: 'orange',
[STATUS.EMPTY]: 'empty',
};

export const SEVERITY = {
CRITICAL: 'critical',
WARNING: 'warning',
Expand Down
82 changes: 82 additions & 0 deletions libs/blocks/preflight/checks/localization.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { STATUS } from './constants.js';
import { getConfig, getLocale } from '../../../utils/utils.js';

async function getStatus(url) {
const tryHead = await fetch(url, { method: 'HEAD', cache: 'no-store' }).catch(() => null);
if (tryHead?.ok || tryHead?.status === 404) return tryHead.status;
const res = await fetch(url, { method: 'GET', cache: 'no-store' }).catch(() => null);
return res?.status || 0;
}

function removeLocale(pathname, locales) {
const { prefix } = getLocale(locales, pathname);
if (!prefix) return pathname;
if (pathname === prefix || pathname === `${prefix}/`) return '/';
return pathname.startsWith(`${prefix}/`) ? pathname.slice(prefix.length) : pathname;
}

function addLocale(basePath, prefix) {
const cleanBase = basePath.startsWith('/') ? basePath : `/${basePath}`;
if (!prefix) return cleanBase;
const cleanPrefix = prefix.startsWith('/') ? prefix : `/${prefix}`;
return `${cleanPrefix}${cleanBase === '/' ? '' : cleanBase}`;
}

function normalizePath(path) {
if (!path) return '/';
if (path === '/') return path;
return path.replace(/\/+$/, '');
}

export async function runChecks({ area = document } = {}) {
const { locales } = getConfig();
const locale = getLocale(locales, window.location.pathname);
const links = Array.from(area.querySelectorAll('a[href]'));
const seen = new Set();
const violations = (await Promise.all(links.map(async (linkEl) => {
const href = linkEl.getAttribute('href');
if (!href || href.startsWith('#')) return null;
const url = new URL(href, window.location.origin);
const basePath = removeLocale(url.pathname, locales);
if (url.hash && normalizePath(basePath) === '/') return null;
const key = `${url.origin}${normalizePath(url.pathname)}`;
if (seen.has(key)) return null;
seen.add(key);

const currentLocalePath = addLocale(basePath, locale.prefix);
const isCurrentLocaleLink = normalizePath(url.pathname)
=== normalizePath(currentLocalePath);

const [localizedStatus, usStatus] = await Promise.all([
getStatus(`${url.origin}${currentLocalePath}`),
getStatus(`${url.origin}${basePath}`),
]);

const shouldFlag = (!isCurrentLocaleLink && [200, 404].includes(localizedStatus))
|| (isCurrentLocaleLink && localizedStatus === 404);

if (shouldFlag) {
return {
url: url.href,
isLocalized: isCurrentLocaleLink,
usStatus,
localizedStatus,
};
}

return null;
}))).filter(Boolean);

const violationsCount = violations.length;
return [{
id: 'link-localization',
title: 'Links',
status: violationsCount === 0 ? STATUS.PASS : STATUS.FAIL,
description: violationsCount === 0
? 'All links are localized or valid.'
: `${violationsCount} link${violationsCount > 1 ? 's' : ''} potentially not localized or invalid.`,
details: { violations },
}];
}

export default { runChecks };
77 changes: 64 additions & 13 deletions libs/blocks/preflight/panels/general.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { html, signal, useEffect } from '../../../deps/htm-preact.js';
import { STATUS, STRUCTURE_TITLES } from '../checks/constants.js';
import { STATUS_TO_ICON_MAP, STRUCTURE_TITLES } from '../checks/constants.js';
import { runChecks as runStructureChecks } from '../checks/structure.js';
import userCanPublishPage from '../../../tools/utils/publish.js';
import { runChecks as runLocalizationChecks } from '../checks/localization.js';

const DEF_NOT_FOUND = 'Not found';
const DEF_NEVER = 'Never';
Expand All @@ -20,6 +21,9 @@ const footerResult = signal({ icon: 'purple', title: STRUCTURE_TITLES.footer, de
const regionSelectorResult = signal({ icon: 'purple', title: STRUCTURE_TITLES.regionSelector, description: 'Checking...' });
const georoutingResult = signal({ icon: 'purple', title: STRUCTURE_TITLES.georouting, description: 'Checking...' });
const breadcrumbsResult = signal({ icon: 'purple', title: STRUCTURE_TITLES.breadcrumbs, description: 'Checking...' });
const localizationResult = signal({ icon: 'purple', title: 'Links', description: 'Checking...' });
const localizationIssues = signal([]);
const localizationClosed = signal(false);

async function getStructureResults() {
const signals = [
Expand All @@ -31,16 +35,9 @@ async function getStructureResults() {
];
const checks = runStructureChecks({ area: document });

const statusToIconMap = {
[STATUS.PASS]: 'green',
[STATUS.FAIL]: 'red',
[STATUS.LIMBO]: 'orange',
[STATUS.EMPTY]: 'empty',
};

await Promise.all(checks.map((result, index) => Promise.resolve(result)
.then((res) => {
const icon = statusToIconMap[res.status] || 'orange';
const icon = STATUS_TO_ICON_MAP[res.status] || 'orange';
signals[index].value = {
icon,
title: res.title,
Expand All @@ -56,6 +53,24 @@ async function getStructureResults() {
})));
}

async function getLocalizationResults() {
try {
const [res] = await runLocalizationChecks({ area: document });
localizationResult.value = {
icon: STATUS_TO_ICON_MAP[res.status] || 'orange',
title: res.title,
description: res.description,
};
localizationIssues.value = res.details?.violations || [];
} catch (error) {
localizationResult.value = {
icon: 'red',
title: 'Links',
description: `Error: ${error.message}`,
};
}
}

function getAdminUrl(url, type) {
if (!(/adobecom\.(hlx|aem)./.test(url.hostname))) return false;
const project = url.hostname === 'localhost' ? 'main--milo--adobecom' : url.hostname.split('.')[0];
Expand Down Expand Up @@ -275,17 +290,45 @@ function ContentGroup({ name, group }) {
</div>`;
}

export default function General() {
useEffect(() => { setContent(); getStructureResults(); }, []);

const StructureItem = ({ icon, title, description }) => html`
function StructureItem({ icon, title, description }) {
return html`
<div class="preflight-item">
<div class="result-icon ${icon}"></div>
<div class="preflight-item-text">
<p class="preflight-item-title">${title}</p>
<p class="preflight-item-description">${description}</p>
</div>
</div>`;
}

function LocalizationIssuesList({ issues }) {
return html`
${issues.length > 0 && html`
<div class="preflight-content-group${localizationClosed.value ? ' is-closed' : ''}">
<div class="preflight-group-row preflight-group-heading" onClick=${() => { localizationClosed.value = !localizationClosed.value; }}>
<div class="preflight-group-expand"></div>
<p class=preflight-content-heading>Faulty links</p>
<p class="preflight-content-heading">Loc</p>
<p class="preflight-content-heading">US status</p>
<p class="preflight-content-heading">Loc status</p>
</div>
<div class=preflight-group-items>
${issues.map((v) => html`
<div class="preflight-group-row preflight-group-detail">
<p><a href=${v.url} target=_blank>${v.url}</a></p>
<p>${v.isLocalized ? 'Yes' : 'No'}</p>
<p>${v.usStatus}</p>
<p>${v.localizedStatus}</p>
</div>
`)}
</div>
</div>
`}
`;
}

export default function General() {
useEffect(() => { setContent(); getStructureResults(); getLocalizationResults(); }, []);

const allChecked = Object.values(content.value)
.flatMap((item) => item.items).filter((item) => item.checked);
Expand Down Expand Up @@ -313,6 +356,14 @@ export default function General() {
<${StructureItem} ...${breadcrumbsResult.value} />
</div>
</div>
<p class="preflight-structure-title">Localization</p>
<div class=preflight-structure-columns>
<div class=preflight-column>
<${StructureItem} ...${localizationResult.value} />
</div>
</div>
<${LocalizationIssuesList} issues=${localizationIssues.value} />
<p class="preflight-structure-title">Content</p>
${Object.keys(content.value).map((key) => html`<${ContentGroup} name=${key} group=${content.value[key]} />`)}
</div>

Expand Down
9 changes: 2 additions & 7 deletions libs/blocks/preflight/panels/performance.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { html, signal, useEffect } from '../../../deps/htm-preact.js';
import preflightApi from '../checks/preflightApi.js';
import { STATUS } from '../checks/constants.js';
import { STATUS_TO_ICON_MAP } from '../checks/constants.js';

const { getLcpEntry, runChecks } = preflightApi.performance;

Expand Down Expand Up @@ -34,12 +34,7 @@ async function getResults() {
const signalResult = signals[index];
return Promise.resolve(resultOrPromise)
.then((result) => {
const statusToIconMap = {
[STATUS.PASS]: 'green',
[STATUS.FAIL]: 'red',
[STATUS.EMPTY]: 'empty',
};
const icon = statusToIconMap[result.status] ?? 'orange';
const icon = STATUS_TO_ICON_MAP[result.status] ?? 'orange';
signalResult.value = {
icon,
title: result.title.replace('Performance - ', ''),
Expand Down
4 changes: 2 additions & 2 deletions libs/blocks/preflight/preflight.css
Original file line number Diff line number Diff line change
Expand Up @@ -333,15 +333,15 @@ span.preflight-time {

/* GENERAL */
.preflight-structure-title {
margin: 0;
margin: 0 0 24px;
font-weight: 700;
font-size: 32px;
line-height: 1;
padding-left: 24px;
}

.preflight-structure-columns {
margin: 24px 48px;
margin: 0 48px 24px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 48px;
Expand Down
Loading