Skip to content

Commit

Permalink
feat(seo): optimized category URLs with full category path (#1163)
Browse files Browse the repository at this point in the history
* changed category id marker from 'cat' to 'ctg'
* unified base for SEO route generation

BREAKING CHANGES: Changed category routes/URLs (see [Migrations / 2.4 to 3.0](https://github.com/intershop/intershop-pwa/blob/develop/docs/guides/migrations.md#24-to-30) for more details).
  • Loading branch information
Eisie96 authored and shauke committed Jul 19, 2022
1 parent 562c56a commit 2a212bb
Show file tree
Hide file tree
Showing 21 changed files with 162 additions and 115 deletions.
2 changes: 1 addition & 1 deletion e2e/cypress/integration/pages/shopping/family.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ export class FamilyPage {
readonly filterNavigation = new FilterNavigationModule();

static navigateTo(categoryUniqueId: string, page?: number) {
cy.visit(`/cat${categoryUniqueId}${page ? `?page=${page}` : ''}`);
cy.visit(`/ctg${categoryUniqueId}${page ? `?page=${page}` : ''}`);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class ProductDetailPage {

static navigateTo(sku: string, categoryUniqueId?: string) {
if (categoryUniqueId) {
cy.visit(`/sku${sku}-cat${categoryUniqueId}`);
cy.visit(`/sku${sku}-ctg${categoryUniqueId}`);
} else {
cy.visit(`/sku${sku}`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,15 +107,15 @@ describe('Missing Data', () => {
at(HomePage, page => page.header.gotoCategoryPage(_.catalog));
at(CategoryPage, page => page.gotoSubCategory(_.categoryid));
at(NotFoundPage);
cy.url().should('contain', `cat${_.categoryid}`);
cy.url().should('contain', `ctg${_.categoryid}`);
});
});

describe('of category on Category Page', () => {
it('should lead straight to error page', () => {
FamilyPage.navigateTo('ERROAR');
at(NotFoundPage);
cy.url().should('contain', 'catERROAR');
cy.url().should('contain', 'ctgERROAR');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ describe('Page Meta', () => {
at(ProductDetailPage, page => {
page.metaData.check({
title: 'Google Home - Smart Home | Intershop PWA',
url: /.*\/Smart-Home\/Google-Home-sku201807171-catHome-Entertainment.SmartHome$/,
url: /.*\/smart-home\/Google-Home-sku201807171-ctgHome-Entertainment.SmartHome$/,
description: 'Google Home - Hands-free help from the Google Assistant',
'og:image': /.*201807171_front.*/,
});
Expand All @@ -25,7 +25,7 @@ describe('Page Meta', () => {
at(FamilyPage, page => {
page.metaData.check({
title: 'Smart Home - Home Entertainment | Intershop PWA',
url: /.*\/Smart-Home-catHome-Entertainment.SmartHome$/,
url: /.*\/smart-home-ctgHome-Entertainment.SmartHome$/,
description: 'Smart Home, Hands-free help from the Google AssistantSmart HomeHome Entertainment',
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe('Page Meta', () => {
at(ProductDetailPage, page => {
page.metaData.check({
title: 'Kodak Slice - Digital Cameras | Intershop PWA',
url: /.*\/Digital-Cameras\/Kodak-Slice-sku3957284-catCameras-Camcorders.575$/,
url: /.*\/digital-cameras\/Kodak-Slice-sku3957284-ctgCameras-Camcorders.575$/,
description: 'Kodak Slice - Slice - 14MP, 5x optisch, 16:9, nickel',
'og:image': /.*3957284-5640.jpg.*/,
});
Expand Down
10 changes: 5 additions & 5 deletions e2e/test-universal.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,19 @@ echo "Waiting for $waitOn"
npx wait-on "$waitOn"

universalTest 1 "${PWA_BASE_URL}/" "router-outlet><ish-home-page"
universalTest 2 "${PWA_BASE_URL}/catComputers.1835.151" "router-outlet><ish-category-page"
universalTest 2 "${PWA_BASE_URL}/computers/notebooks-and-pcs/notebooks-ctgComputers.1835.151" "router-outlet><ish-category-page"
universalTest 3 "${PWA_BASE_URL}/login" "<ish-loading"
universalTest 4 "${PWA_BASE_URL}/register" "Create Account"
universalTest 5 "${PWA_BASE_URL}/catComputers.1835" "<h1>Notebooks and PCs</h1>"
universalTest 6 "${PWA_BASE_URL}/catComputers.1835" "<h3>PCs</h3>"
universalTest 7 "${PWA_BASE_URL}/catComputers.1835.151" "add-to-compare"
universalTest 5 "${PWA_BASE_URL}/computers/notebooks-and-pcs-ctgComputers.1835" "<h1>Notebooks and PCs</h1>"
universalTest 6 "${PWA_BASE_URL}/computers/notebooks-and-pcs-ctgComputers.1835" "<h3>PCs</h3>"
universalTest 7 "${PWA_BASE_URL}/computers/notebooks-and-pcs/notebooks-ctgComputers.1835.151" "add-to-compare"
universalTest 8 "${PWA_BASE_URL}/home" "intershop-pwa-state"
universalTest 9 "${PWA_BASE_URL}/home" "&q;baseURL&q;:"
universalTest 10 "${PWA_BASE_URL}/home" "<ish-content-include includeid=.include.homepage.content.pagelet2-Include"
universalTest 11 "${PWA_BASE_URL}/home" "<link rel=.canonical. href=.${PWA_CANONICAL_BASE_URL}/home/.>"
universalTest 12 "${PWA_BASE_URL}/home" "<meta property=.og:image. content=./assets/img/og-image-default"
universalTest 13 "${PWA_BASE_URL}/home" "<title>inTRONICS Home | Intershop PWA</title>"
universalTest 14 "${PWA_BASE_URL}/sku6997041" "<link rel=.canonical. href=.${PWA_CANONICAL_BASE_URL}/Notebooks/Asus-Eee-PC-1008P-Karim-Rashid-sku6997041-catComputers.1835.151.>"
universalTest 14 "${PWA_BASE_URL}/sku6997041" "<link rel=.canonical. href=.${PWA_CANONICAL_BASE_URL}/notebooks/Asus-Eee-PC-1008P-Karim-Rashid-sku6997041-ctgComputers.1835.151.>"
universalTest 15 "${PWA_BASE_URL}/sku6997041" "<meta property=.og:image. content=[^>]*6997041"
universalTest 16 "${PWA_BASE_URL}/sku6997041" "<title>Asus Eee PC 1008P .Karim Rashid. [^>]* | Intershop PWA</title>"
universalTest 17 "${PWA_BASE_URL}/home;device=tablet" "class=.header container tablet"
Expand Down
6 changes: 3 additions & 3 deletions src/app/core/facades/product-context.facade.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ describe('Product Context Facade', () => {
});

it('should calculate the url property of the product with default category', () => {
expect(context.get('productURL')).toMatchInlineSnapshot(`"//sku123-catABC"`);
expect(context.get('productURL')).toMatchInlineSnapshot(`"//sku123-ctgABC"`);
});
});

Expand All @@ -416,7 +416,7 @@ describe('Product Context Facade', () => {
});

it('should calculate the url property of the product with context category', () => {
expect(context.get('productURL')).toMatchInlineSnapshot(`"//sku123-catASDF"`);
expect(context.get('productURL')).toMatchInlineSnapshot(`"//sku123-ctgASDF"`);
});
});

Expand All @@ -438,7 +438,7 @@ describe('Product Context Facade', () => {
});

it('should calculate the url property of the product with context category', () => {
expect(context.get('productURL')).toMatchInlineSnapshot(`"//sku123-catASDF"`);
expect(context.get('productURL')).toMatchInlineSnapshot(`"//sku123-ctgASDF"`);
});
});
});
Expand Down
4 changes: 4 additions & 0 deletions src/app/core/models/category-view/category-view.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Category } from 'ish-core/models/category/category.model';
export interface CategoryView extends Category {
children: string[];
hasChildren: boolean;
pathElements: Category[];
}

export function createCategoryView(tree: CategoryTree, uniqueId: string): CategoryView {
Expand All @@ -21,6 +22,9 @@ export function createCategoryView(tree: CategoryTree, uniqueId: string): Catego
...tree.nodes[translateRef(tree, uniqueId)],
children: tree.edges[translateRef(tree, uniqueId)] || [],
hasChildren: !!tree.edges[translateRef(tree, uniqueId)] && !!tree.edges[translateRef(tree, uniqueId)].length,
pathElements: (tree.nodes[translateRef(tree, uniqueId)]?.categoryPath || [])
.map(path => tree.nodes[translateRef(tree, path)])
.filter(x => !!x),
};
}

Expand Down
18 changes: 10 additions & 8 deletions src/app/core/routing/category/category.route.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,10 @@ describe('Category Route', () => {
const category = createCategoryView(categoryTree([{ ...specials, name: undefined }]), specials.uniqueId);

it('should be created', () => {
expect(generateCategoryUrl(category)).toMatchInlineSnapshot(`"/catSpecials"`);
expect(generateCategoryUrl(category)).toMatchInlineSnapshot(`"/ctgSpecials"`);
});

it('should not be a match for matcher', () => {
it('should be a match for matcher', () => {
expect(matchCategoryRoute(wrap(generateCategoryUrl(category)))).toMatchInlineSnapshot(`
Object {
"categoryUniqueId": "Specials",
Expand All @@ -84,10 +84,10 @@ describe('Category Route', () => {
const category = createCategoryView(categoryTree([specials]), specials.uniqueId);

it('should be created', () => {
expect(generateCategoryUrl(category)).toMatchInlineSnapshot(`"/Spezielles-1/2-Preis-Aktion-mehr-catSpecials"`);
expect(generateCategoryUrl(category)).toMatchInlineSnapshot(`"/spezielles-1/2-preis-aktion-mehr-ctgSpecials"`);
});

it('should not be a match for matcher', () => {
it('should be a match for matcher', () => {
expect(matchCategoryRoute(wrap(generateCategoryUrl(category)))).toMatchInlineSnapshot(`
Object {
"categoryUniqueId": "Specials",
Expand All @@ -100,10 +100,12 @@ describe('Category Route', () => {
const category = createCategoryView(categoryTree([specials, topSeller, limitedOffer]), limitedOffer.uniqueId);

it('should be created', () => {
expect(generateCategoryUrl(category)).toMatchInlineSnapshot(`"/Black-Friday-catSpecials.TopSeller.LimitedOffer"`);
expect(generateCategoryUrl(category)).toMatchInlineSnapshot(
`"/spezielles-1/2-preis-aktion-mehr/angebote/black-friday-ctgSpecials.TopSeller.LimitedOffer"`
);
});

it('should not be a match for matcher', () => {
it('should be a match for matcher', () => {
expect(matchCategoryRoute(wrap(generateCategoryUrl(category)))).toMatchInlineSnapshot(`
Object {
"categoryUniqueId": "Specials.TopSeller.LimitedOffer",
Expand Down Expand Up @@ -146,12 +148,12 @@ describe('Category Route', () => {
describe('generateLocalizedCategorySlug', () => {
it('should generate slug for top level category', () => {
const category = createCategoryView(categoryTree([specials]), specials.uniqueId);
expect(generateLocalizedCategorySlug(category)).toMatchInlineSnapshot(`"Spezielles-1/2-Preis-Aktion-mehr"`);
expect(generateLocalizedCategorySlug(category)).toMatchInlineSnapshot(`"spezielles-1/2-preis-aktion-mehr"`);
});

it('should generate slug for deep category', () => {
const category = createCategoryView(categoryTree([specials, topSeller, limitedOffer]), limitedOffer.uniqueId);
expect(generateLocalizedCategorySlug(category)).toMatchInlineSnapshot(`"Black-Friday"`);
expect(generateLocalizedCategorySlug(category)).toMatchInlineSnapshot(`"black-friday"`);
});

it('should return empty string when category is unavailable', () => {
Expand Down
48 changes: 35 additions & 13 deletions src/app/core/routing/category/category.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,43 @@ import { UrlMatchResult, UrlSegment } from '@angular/router';
import { MonoTypeOperatorFunction } from 'rxjs';
import { filter } from 'rxjs/operators';

import { CategoryView } from 'ish-core/models/category-view/category-view.model';
import { Category } from 'ish-core/models/category/category.model';
import { CoreState } from 'ish-core/store/core/core-store';
import { selectRouteParam, selectRouteParamAorB } from 'ish-core/store/core/router';
import { reservedCharactersRegEx } from 'ish-core/utils/routing';
import { sanitizeSlugData } from 'ish-core/utils/routing';

/**
* generate a localized category slug
*
* @param category category element for slug
* @returns localized, formatted category slug
*/
export function generateLocalizedCategorySlug(category: Category) {
return (
category?.name
?.replace(reservedCharactersRegEx, '-')
.replace(/-+/g, '-')
.replace(/-+$/, '')
.replace('-cat', '-Cat') || ''
);
return sanitizeSlugData(category?.name);
}

const categoryRouteFormat = /^\/(?!category|categoryref\/.*$)(.*-)?cat(.*)$/;
// matcher to check if a given url is a category route
const categoryRouteFormat = /^\/(?!category|categoryref\/.*$)(.*?)-?ctg(.*)$/;

/**
* check if given url is a category route
*
* @param segments current url segments
* @returns match result if given url is a category route or not
*/
export function matchCategoryRoute(segments: UrlSegment[]): UrlMatchResult {
// compatibility to old routes
if (segments && segments.length === 2 && (segments[0].path === 'category' || segments[0].path === 'categoryref')) {
return { consumed: [] };
}

// generate complete url path
const url = `/${segments.map(s => s.path).join('/')}`;

// check that complete url path is a category route
if (categoryRouteFormat.test(url)) {
// select categoryUniqueId to render a category component
const match = categoryRouteFormat.exec(url);
const posParams: { [id: string]: UrlSegment } = {};
if (match[2]) {
Expand All @@ -40,19 +52,29 @@ export function matchCategoryRoute(segments: UrlSegment[]): UrlMatchResult {
return;
}

export function generateCategoryUrl(category: Category): string {
/**
* generate a localized category url from a category view
*
* @param category category view
* @returns localized category url
*/
export function generateCategoryUrl(category: CategoryView): string {
if (!category) {
return '/';
}
let route = '/';

route += generateLocalizedCategorySlug(category);
// generate for each path element from the given category view a category slug and join them together to a complete route
let route = `/${category.pathElements
?.filter(x => !!x)
.map(el => generateLocalizedCategorySlug(el))
.join('/')}`;

// add to category route the category identifier
if (route !== '/') {
route += '-';
}

route += `cat${category.uniqueId}`;
route += `ctg${category.uniqueId}`;

return route;
}
Expand Down
16 changes: 8 additions & 8 deletions src/app/core/routing/product/product.route.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ describe('Product Route', () => {
const product = createProductView({ sku: 'A' } as Product, specials);

it('should be created', () => {
expect(generateProductUrl(product, category)).toMatchInlineSnapshot(`"/Spezielles/skuA-catSpecials"`);
expect(generateProductUrl(product, category)).toMatchInlineSnapshot(`"/spezielles/skuA-ctgSpecials"`);
});

it('should be a match for matcher', () => {
Expand All @@ -141,7 +141,7 @@ describe('Product Route', () => {

it('should be created', () => {
expect(generateProductUrl(product, category)).toMatchInlineSnapshot(
`"/Spezielles/Das-neue-Surface-Pro-7-skuA-catSpecials"`
`"/spezielles/Das-neue-Surface-Pro-7-skuA-ctgSpecials"`
);
});

Expand All @@ -161,7 +161,7 @@ describe('Product Route', () => {
const product = createProductView({ sku: 'A' } as Product, specials);

it('should be created', () => {
expect(generateProductUrl(product)).toMatchInlineSnapshot(`"/Spezielles/skuA-catSpecials"`);
expect(generateProductUrl(product)).toMatchInlineSnapshot(`"/spezielles/skuA-ctgSpecials"`);
});

it('should be a match for matcher', () => {
Expand All @@ -179,7 +179,7 @@ describe('Product Route', () => {

it('should be created', () => {
expect(generateProductUrl(product)).toMatchInlineSnapshot(
`"/Spezielles/Das-neue-Surface-Pro-7-skuA-catSpecials"`
`"/spezielles/Das-neue-Surface-Pro-7-skuA-ctgSpecials"`
);
});

Expand All @@ -205,7 +205,7 @@ describe('Product Route', () => {

it('should be created', () => {
expect(generateProductUrl(product, category)).toMatchInlineSnapshot(
`"/Black-Friday/skuA-catSpecials.TopSeller.LimitedOffer"`
`"/black-friday/skuA-ctgSpecials.TopSeller.LimitedOffer"`
);
});

Expand All @@ -224,7 +224,7 @@ describe('Product Route', () => {

it('should be created', () => {
expect(generateProductUrl(product, category)).toMatchInlineSnapshot(
`"/Black-Friday/Das-neue-Surface-Pro-7-skuA-catSpecials.TopSeller.LimitedOffer"`
`"/black-friday/Das-neue-Surface-Pro-7-skuA-ctgSpecials.TopSeller.LimitedOffer"`
);
});

Expand All @@ -245,7 +245,7 @@ describe('Product Route', () => {

it('should be created', () => {
expect(generateProductUrl(product)).toMatchInlineSnapshot(
`"/Black-Friday/skuA-catSpecials.TopSeller.LimitedOffer"`
`"/black-friday/skuA-ctgSpecials.TopSeller.LimitedOffer"`
);
});

Expand All @@ -264,7 +264,7 @@ describe('Product Route', () => {

it('should be created', () => {
expect(generateProductUrl(product)).toMatchInlineSnapshot(
`"/Black-Friday/Das-neue-Surface-Pro-7-skuA-catSpecials.TopSeller.LimitedOffer"`
`"/black-friday/Das-neue-Surface-Pro-7-skuA-ctgSpecials.TopSeller.LimitedOffer"`
);
});

Expand Down
5 changes: 3 additions & 2 deletions src/app/core/routing/product/product.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ function generateProductSlug(product: ProductView) {
return slug.replace('-cat', '-Cat');
}

const productRouteFormat = new RegExp('/(?!cat)((?!.*-cat.*-sku).*-)?sku(.*?)(-cat(.*))?$');
// matcher to check if a given url is a product url
const productRouteFormat = /\/(?!ctg)(?!.*-ctg.*-sku)(.*?)-?sku(.*?)(-ctg(.*))?$/;

export function matchProductRoute(segments: UrlSegment[]): UrlMatchResult {
// compatibility to old routes
Expand Down Expand Up @@ -78,7 +79,7 @@ export function generateProductUrl(product: ProductView, category?: Category): s
route += `sku${product.sku}`;

if (contextCategory) {
route += `-cat${contextCategory.uniqueId}`;
route += `-ctg${contextCategory.uniqueId}`;
}

return route;
Expand Down
Loading

0 comments on commit 2a212bb

Please sign in to comment.