-
Notifications
You must be signed in to change notification settings - Fork 87
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(seo): optimized content page URLs with full content page path (#…
…1161) * introduced content page route generation similar to categoryies * added content page id marker 'pg' BREAKING CHANGES: Changed content page 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
Showing
16 changed files
with
571 additions
and
43 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
12 changes: 12 additions & 0 deletions
12
src/app/core/routing/content-page/content-page-route.pipe.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { Pipe, PipeTransform } from '@angular/core'; | ||
|
||
import { ContentPageTreeView } from 'ish-core/models/content-page-tree-view/content-page-tree-view.model'; | ||
|
||
import { generateContentPageUrl } from './content-page.route'; | ||
|
||
@Pipe({ name: 'ishContentPageRoute', pure: true }) | ||
export class ContentPageRoutePipe implements PipeTransform { | ||
transform(page: ContentPageTreeView): string { | ||
return generateContentPageUrl(page); | ||
} | ||
} |
112 changes: 112 additions & 0 deletions
112
src/app/core/routing/content-page/content-page-route.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import { TestBed } from '@angular/core/testing'; | ||
import { Router, UrlMatchResult, UrlSegment } from '@angular/router'; | ||
import { RouterTestingModule } from '@angular/router/testing'; | ||
|
||
import { createContentPageTreeView } from 'ish-core/models/content-page-tree-view/content-page-tree-view.model'; | ||
import { ContentPageTreeHelper } from 'ish-core/models/content-page-tree/content-page-tree.helper'; | ||
import { ContentPageTreeElement } from 'ish-core/models/content-page-tree/content-page-tree.model'; | ||
|
||
import { generateContentPageUrl, matchContentRoute } from './content-page.route'; | ||
|
||
describe('Content Page Route', () => { | ||
const topElement: ContentPageTreeElement = { contentPageId: 'top', name: 'top layer', path: ['top'] }; | ||
const secondElement: ContentPageTreeElement = { | ||
contentPageId: 'second', | ||
name: 'second layer', | ||
path: ['top', 'second'], | ||
}; | ||
const bottomElement: ContentPageTreeElement = { | ||
contentPageId: 'bottom', | ||
name: 'bottom layer', | ||
path: ['top', 'second', 'bottom'], | ||
}; | ||
|
||
expect.addSnapshotSerializer({ | ||
test: val => val?.consumed && val.posParams, | ||
print: (val: UrlMatchResult, serialize) => | ||
serialize( | ||
Object.keys(val.posParams) | ||
.map(key => ({ [key]: val.posParams[key].path })) | ||
.reduce((acc, v) => ({ ...acc, ...v }), {}) | ||
), | ||
}); | ||
|
||
let wrap: (url: string) => UrlSegment[]; | ||
|
||
beforeEach(() => { | ||
TestBed.configureTestingModule({ imports: [RouterTestingModule] }); | ||
const router = TestBed.inject(Router); | ||
wrap = url => { | ||
const primary = router.parseUrl(url).root.children.primary; | ||
return primary ? primary.segments : []; | ||
}; | ||
}); | ||
|
||
describe('without anything', () => { | ||
it('should be created', () => { | ||
expect(generateContentPageUrl(undefined)).toMatchInlineSnapshot(`"/"`); | ||
}); | ||
|
||
it('should not be a match for matcher', () => { | ||
expect(matchContentRoute(wrap(generateContentPageUrl(undefined)))).toMatchInlineSnapshot(`undefined`); | ||
}); | ||
}); | ||
|
||
describe('with top level content page without name', () => { | ||
const topLevelView = createContentPageTreeView( | ||
ContentPageTreeHelper.single({ ...topElement, name: undefined }), | ||
'top', | ||
'top' | ||
); | ||
|
||
it('should be created', () => { | ||
expect(generateContentPageUrl(topLevelView)).toMatchInlineSnapshot(`"/pgtop"`); | ||
}); | ||
|
||
it('should be a match for matcher', () => { | ||
expect(matchContentRoute(wrap(generateContentPageUrl(topLevelView)))).toMatchInlineSnapshot(` | ||
Object { | ||
"contentPageId": "top", | ||
} | ||
`); | ||
}); | ||
}); | ||
|
||
describe('with top level content page', () => { | ||
const topLevelView = createContentPageTreeView(ContentPageTreeHelper.single(topElement), 'top', 'top'); | ||
|
||
it('should be created', () => { | ||
expect(generateContentPageUrl(topLevelView)).toMatchInlineSnapshot(`"/top-layer-pgtop"`); | ||
}); | ||
|
||
it('should not be a match for matcher', () => { | ||
expect(matchContentRoute(wrap(generateContentPageUrl(topLevelView)))).toMatchInlineSnapshot(` | ||
Object { | ||
"contentPageId": "top", | ||
} | ||
`); | ||
}); | ||
}); | ||
|
||
describe('with deep content page hierachy', () => { | ||
const topLevelTree = ContentPageTreeHelper.single(topElement); | ||
const secondLevelTree = ContentPageTreeHelper.add(topLevelTree, secondElement); | ||
const completeTree = ContentPageTreeHelper.add(secondLevelTree, bottomElement); | ||
|
||
const bottomLevelView = createContentPageTreeView(completeTree, 'bottom', 'bottom'); | ||
|
||
it('should be created', () => { | ||
expect(generateContentPageUrl(bottomLevelView)).toMatchInlineSnapshot( | ||
`"/top-layer/second-layer/bottom-layer-pgbottom"` | ||
); | ||
}); | ||
|
||
it('should not be a match for matcher', () => { | ||
expect(matchContentRoute(wrap(generateContentPageUrl(bottomLevelView)))).toMatchInlineSnapshot(` | ||
Object { | ||
"contentPageId": "bottom", | ||
} | ||
`); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import { UrlMatchResult, UrlSegment } from '@angular/router'; | ||
import { MonoTypeOperatorFunction, filter } from 'rxjs'; | ||
|
||
import { ContentPageTreeView } from 'ish-core/models/content-page-tree-view/content-page-tree-view.model'; | ||
import { ContentPageTreeElement } from 'ish-core/models/content-page-tree/content-page-tree.model'; | ||
import { CoreState } from 'ish-core/store/core/core-store'; | ||
import { selectRouteParam } from 'ish-core/store/core/router'; | ||
import { sanitizeSlugData } from 'ish-core/utils/routing'; | ||
|
||
/** | ||
* generate a localized content page slug | ||
* | ||
* @param page content page element for slug | ||
* @returns localized, formatted content page slug | ||
*/ | ||
function generateLocalizedContentPageSlug(page: ContentPageTreeElement) { | ||
return sanitizeSlugData(page?.name); | ||
} | ||
|
||
// matcher to check if a given url is a content page route | ||
const contentRouteFormat = /^\/(?!page\/.*$)(.*-)?pg(.*)$/; | ||
|
||
/** | ||
* check if given url is a content page route | ||
* | ||
* @param segments current url segments | ||
* @returns match result if given url is a content page route or not | ||
*/ | ||
export function matchContentRoute(segments: UrlSegment[]): UrlMatchResult { | ||
// compatibility to old routes | ||
if (segments && segments.length === 2 && segments[0].path === 'page') { | ||
return { | ||
consumed: [], | ||
}; | ||
} | ||
|
||
const url = `/${segments.map(s => s.path).join('/')}`; | ||
if (contentRouteFormat.test(url)) { | ||
const match = contentRouteFormat.exec(url); | ||
const posParams: { [id: string]: UrlSegment } = {}; | ||
if (match[2]) { | ||
posParams.contentPageId = new UrlSegment(match[2], {}); | ||
} | ||
return { | ||
consumed: [], | ||
posParams, | ||
}; | ||
} | ||
return; | ||
} | ||
|
||
/** | ||
* generate a localized content page url from a content page | ||
* | ||
* @param page content page | ||
* @returns localized content page url | ||
*/ | ||
export function generateContentPageUrl(page: ContentPageTreeView): string { | ||
if (!page) { | ||
return '/'; | ||
} | ||
|
||
let route = '/'; | ||
|
||
// generate for each path element from the given content page hierarchy a content page slug and join them together to a complete route | ||
route += page.pathElements?.map(p => generateLocalizedContentPageSlug(p)).join('/'); | ||
|
||
// add to content page route the content page identifier | ||
if (route !== '/') { | ||
route += '-'; | ||
} | ||
route += `pg${page.contentPageId}`; | ||
|
||
return route; | ||
} | ||
|
||
export function ofContentPageUrl(): MonoTypeOperatorFunction<{}> { | ||
return source$ => source$.pipe(filter((state: CoreState) => !!selectRouteParam('contentPageId')(state))); | ||
} |
Oops, something went wrong.