Skip to content
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

Spike: SEO-friendly URLs (ISREST-925,#33) #11

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
37 changes: 37 additions & 0 deletions src/app/core/custom-routes/NEW-category.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Route, UrlSegment, UrlSegmentGroup } from '@angular/router';

import { Category } from 'ish-core/models/category/category.model';

import { CustomRoute } from './custom-route';

export function generateCategoryRoute(category: Category) {
return `/${category.uniqueId}-c`;
}

/**
* UrlMatcher for category route
* Defines a specific URL format for the category page
*/
export function categoryRouteMatcher(url: UrlSegment[], _: UrlSegmentGroup, route: Route) {
if (!route.data) {
route.data = {};
}
route.data.format = '<categoryUniqueId>-c';

// Format: /<categoryUniqueId>-c
if (url.length === 1 && url[0].path.endsWith('-c')) {
const categoryUniqueId = url[0].path.slice(0, -2);
return {
posParams: {
categoryUniqueId: new UrlSegment(categoryUniqueId, {}),
},
consumed: url,
};
}
}

export const categoryRoute: CustomRoute = {
matcher: categoryRouteMatcher,
generateUrl: generateCategoryRoute,
formats: ['<categoryUniqueId>-c'],
};
96 changes: 96 additions & 0 deletions src/app/core/custom-routes/NEW-product.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Route, UrlSegment, UrlSegmentGroup } from '@angular/router';

import { Category } from 'ish-core/models/category/category.model';
import { Product } from 'ish-core/models/product/product.model';

import { CustomRoute } from './custom-route';

export function generateProductSlug(product: Product) {
return product && product.name ? product.name.replace(/[^a-zA-Z0-9-]+/g, '-').replace(/-+$/g, '') : undefined;
}

/**
* Generate a product detail route with optional category context.
* @param product The Product to genereate the route for
* @param category The optional Category that should be used as context for the product route
* @returns Product route string
*/
export function generateProductRoute(product: Product, category?: Category): string {
if (!(product && product.sku)) {
return '/';
}
const productSlug = generateProductSlug(product);
let route = `p-${product.sku}.html`;
if (productSlug) {
route = `${productSlug}-${route}`;
}

if (category) {
route = `${category.uniqueId}-c/${route}`;
}

return '/' + route;
}

/**
* UrlMatcher for product route
* Defines a specific URL format for the product page
*/

export function productRouteMatcher(url: UrlSegment[], _: UrlSegmentGroup, route: Route) {
if (url.length && !url[url.length - 1].path.endsWith('.html')) {
return;
}

if (!route.data) {
route.data = {};
}

let productPath = '';
let categoryUniqueId;

if (url.length === 1) {
productPath = url[0].path;
} else if (url.length === 2) {
// Format: <categoryUniqueId>-c/<productSlug>-p-<sku>.html
route.data.format = '<categoryUniqueId>-c/<productSlug>-p-<sku>.html';
productPath = url[1].path;
categoryUniqueId = url[0].path.slice(0, -2);
}

const productParts = productPath.slice(0, -5).split('-'); // remove '.html'
let sku;

// Format: p-<sku>.html
if (productParts.length === 2 && productParts[0] === 'p') {
route.data.format = 'p-<sku>.html';
sku = productParts[1];
}

// Format: <productSlug>-p-<sku>.html
if (productParts.length >= 3 && [...productParts].splice(-2)[0] === 'p') {
if (!categoryUniqueId) {
route.data.format = '<productSlug>-p-<sku>.html';
}
sku = [...productParts].splice(-1)[0];
}

const posParams: { [key: string]: UrlSegment } = {
sku: new UrlSegment(sku, {}),
};

if (categoryUniqueId) {
posParams.categoryUniqueId = new UrlSegment(categoryUniqueId, {});
}

return {
posParams,
consumed: url,
};
}

export const productRoute: CustomRoute = {
matcher: productRouteMatcher,
generateUrl: generateProductRoute,
formats: ['<categoryUniqueId>-c/<productSlug>-p-<sku>.html', 'p-<sku>.html', '<productSlug>-p-<sku>.html'],
};
15 changes: 15 additions & 0 deletions src/app/core/custom-routes/category.route.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Category } from 'ish-core/models/category/category.model';

import { categoryRoute } from './category.route';

describe('Category Route', () => {
let cat: Category;

beforeEach(() => {
cat = { uniqueId: 'cate' } as Category;
});

it('should generate category route for category', () => {
expect(categoryRoute.generateUrl(cat)).toEqual('/category/cate');
});
});
36 changes: 36 additions & 0 deletions src/app/core/custom-routes/category.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Route, UrlSegment, UrlSegmentGroup } from '@angular/router';

import { Category } from 'ish-core/models/category/category.model';

import { CustomRoute } from './custom-route';

export function generateCategoryRoute(category: Category) {
return '/category/' + category.uniqueId;
}

/**
* UrlMatcher for category route
* Defines a specific URL format for the category page
*/
export function categoryRouteMatcher(url: UrlSegment[], _: UrlSegmentGroup, route: Route) {
if (!route.data) {
route.data = {};
}
route.data.format = 'category/:categoryUniqueId';

// Format: category/:categoryUniqueId
if (url[0].path === 'category') {
return {
posParams: {
categoryUniqueId: url[1],
},
consumed: url,
};
}
}

export const categoryRoute: CustomRoute = {
matcher: categoryRouteMatcher,
generateUrl: generateCategoryRoute,
formats: ['category/:categoryUniqueId'],
};
7 changes: 7 additions & 0 deletions src/app/core/custom-routes/custom-route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { UrlMatcher } from '@angular/router';

export interface CustomRoute {
matcher: UrlMatcher;
formats: string[];
generateUrl(...args): string;
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,8 @@
import { TestBed } from '@angular/core/testing';
import * as using from 'jasmine-data-provider';

import { ProductRoutePipe } from './product-route.pipe';

describe('Product Route Pipe', () => {
let productRoutePipe: ProductRoutePipe;

beforeEach(() => {
TestBed.configureTestingModule({
providers: [ProductRoutePipe],
});
productRoutePipe = TestBed.get(ProductRoutePipe);
});

it('should be created', () => {
expect(productRoutePipe).toBeTruthy();
});
import { productRoute } from './product.route';

describe('Product Route', () => {
function dataProvider() {
return [
{
Expand Down Expand Up @@ -44,7 +30,7 @@ describe('Product Route Pipe', () => {
it(`should return ${dataSlice.expected} when supplying product '${JSON.stringify(
dataSlice.product
)}' and category '${JSON.stringify(dataSlice.category)}'`, () => {
expect(productRoutePipe.transform(dataSlice.product, dataSlice.category)).toEqual(dataSlice.expected);
expect(productRoute.generateUrl(dataSlice.product, dataSlice.category)).toEqual(dataSlice.expected);
});
});
});
74 changes: 74 additions & 0 deletions src/app/core/custom-routes/product.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Route, UrlSegment, UrlSegmentGroup } from '@angular/router';

import { Category } from 'ish-core/models/category/category.model';
import { Product } from 'ish-core/models/product/product.model';

import { CustomRoute } from './custom-route';

export function generateProductSlug(product: Product) {
return product && product.name ? product.name.replace(/[^a-zA-Z0-9-]+/g, '-').replace(/-+$/g, '') : undefined;
}

/**
* Generate a product detail route with optional category context.
* @param product The Product to genereate the route for
* @param category The optional Category that should be used as context for the product route
* @returns Product route string
*/
export function generateProductRoute(product: Product, category?: Category): string {
if (!(product && product.sku)) {
return '/';
}
let route = '/product/' + product.sku;
const productSlug = generateProductSlug(product);
if (productSlug) {
route += '/' + productSlug;
}

if (category) {
route = `/category/${category.uniqueId}${route}`;
} else {
// TODO: add defaultCategory to route once this information is available with the products REST call
}
return route;
}

/**
* UrlMatcher for product route
* Defines a specific URL format for the product page
*/

export function productRouteMatcher(url: UrlSegment[], _: UrlSegmentGroup, route: Route) {
if (!route.data) {
route.data = {};
}

// Format: product/:sku/:productSlug
if (url[0].path === 'product') {
route.data.format = 'product/:sku/:productSlug';
return {
posParams: {
sku: url[1],
},
consumed: url,
};
}

// Format: category/:categoryUniqueId/product/:sku/:productSlug
if (url.length >= 4 && url[0].path === 'category' && url[2].path === 'product') {
route.data.format = 'category/:categoryUniqueId/product/:sku/:productSlug';
return {
posParams: {
categoryUniqueId: url[1],
sku: url[3],
},
consumed: url,
};
}
}

export const productRoute: CustomRoute = {
matcher: productRouteMatcher,
generateUrl: generateProductRoute,
formats: ['product/:sku/:productSlug', 'category/:categoryUniqueId/product/:sku/:productSlug'],
};
26 changes: 0 additions & 26 deletions src/app/core/pipes/category-route.pipe.spec.ts

This file was deleted.

3 changes: 2 additions & 1 deletion src/app/core/pipes/category-route.pipe.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Pipe, PipeTransform } from '@angular/core';

import { categoryRoute } from 'ish-core/custom-routes/category.route';
import { Category } from 'ish-core/models/category/category.model';

@Pipe({ name: 'ishCategoryRoute', pure: true })
export class CategoryRoutePipe implements PipeTransform {
transform(category: Category): string {
return '/category/' + category.uniqueId;
return categoryRoute.generateUrl(category);
}
}
32 changes: 2 additions & 30 deletions src/app/core/pipes/product-route.pipe.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,12 @@
import { Pipe, PipeTransform } from '@angular/core';

import { productRoute } from 'ish-core/custom-routes/product.route';
import { Category } from 'ish-core/models/category/category.model';
import { Product } from 'ish-core/models/product/product.model';

function generateProductSlug(product: Product) {
return product && product.name ? product.name.replace(/[^a-zA-Z0-9-]+/g, '-').replace(/-+$/g, '') : undefined;
}

/**
* Generate a product detail route with optional category context.
* @param product The Product to genereate the route for
* @param category The optional Category that should be used as context for the product route
* @returns Product route string
*/

export function generateProductRoute(product: Product, category?: Category): string {
if (!(product && product.sku)) {
return '/';
}
let productRoute = '/product/' + product.sku;
const productSlug = generateProductSlug(product);
if (productSlug) {
productRoute += '/' + productSlug;
}

if (category) {
productRoute = `/category/${category.uniqueId}${productRoute}`;
} else {
// TODO: add defaultCategory to route once this information is available with the products REST call
}
return productRoute;
}

@Pipe({ name: 'ishProductRoute', pure: true })
export class ProductRoutePipe implements PipeTransform {
transform(product: Product, category?: Category): string {
return generateProductRoute(product, category);
return productRoute.generateUrl(product, category);
}
}
Loading