Skip to content
Merged
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
14 changes: 14 additions & 0 deletions .changeset/smooth-mails-fall.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@o2s/blocks.article-search': minor
'@o2s/blocks.category-list': minor
'@o2s/configs.integrations': minor
'@o2s/integrations.zendesk': major
'@o2s/blocks.article-list': minor
'@o2s/integrations.mocked': minor
'@o2s/blocks.category': minor
'@o2s/api-harmonization': minor
'@o2s/framework': minor
'@o2s/docs': minor
---

feat(zendesk): remove hardcoded locale base paths from article slugs
46 changes: 21 additions & 25 deletions apps/api-harmonization/src/modules/page/page.mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export const mapArticle = (
article: Articles.Model.Article,
category: Articles.Model.Category,
mainLocale: string,
basePath = '/',
): Page => {
return {
meta: {
Expand Down Expand Up @@ -77,7 +78,7 @@ export const mapArticle = (
},
},
hasOwnTitle: true,
breadcrumbs: mapArticleBreadcrumbs(article, category),
breadcrumbs: mapArticleBreadcrumbs(article, category, basePath),
},
};
};
Expand Down Expand Up @@ -108,30 +109,25 @@ const mapPageBreadcrumbs = (page: CMS.Model.Page.Page): Breadcrumb[] => {
return breadcrumbs.filter((breadcrumb) => breadcrumb.slug);
};

const mapArticleBreadcrumbs = (article: Articles.Model.Article, category: Articles.Model.Category): Breadcrumb[] => {
const breadcrumbs: Breadcrumb[] = [];

function extractFromParent(parent: Articles.Model.Category['parent']): void {
if (!parent) return;

if (parent.parent) {
extractFromParent(parent.parent);
}

breadcrumbs.push({
slug: parent.slug,
label: parent.title,
});
}

extractFromParent(category);

breadcrumbs.push({
slug: article.slug,
label: article.title,
});

return breadcrumbs.filter((breadcrumb) => breadcrumb.slug);
const mapArticleBreadcrumbs = (
article: Articles.Model.Article,
category: Articles.Model.Category,
basePath = '/',
): Breadcrumb[] => {
// Build full URL paths for breadcrumbs (normalize to avoid double slashes)
const categoryUrl = `${basePath}/${category.slug}`.replace(/\/+/g, '/');
const articleUrl = `${categoryUrl}/${article.slug}`.replace(/\/+/g, '/');

return [
{
slug: categoryUrl,
label: category.title,
},
{
slug: articleUrl,
label: article.title,
},
];
};

export const mapInit = (
Expand Down
13 changes: 11 additions & 2 deletions apps/api-harmonization/src/modules/page/page.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export class PageService {

private processArticle = (
article: Articles.Model.Article,
_query: GetPageQuery,
query: GetPageQuery,
headers: Models.Headers.AppHeaders,
) => {
if (!article.category) {
Expand All @@ -126,6 +126,15 @@ export class PageService {

const category = this.articlesService.getCategory({ id: article.category.id, locale: headers['x-locale'] });

return forkJoin([category]).pipe(map(([category]) => mapArticle(article, category, headers['x-locale'])));
return forkJoin([category]).pipe(
map(([category]) => {
// Extract base path from URL: /basePath/categorySlug/articleSlug -> /basePath
const slugParts = query.slug.split('/').filter(Boolean);
// Remove article slug (last segment) and category slug (second to last)
const basePath = slugParts.length > 2 ? '/' + slugParts.slice(0, -2).join('/') : '/';

return mapArticle(article, category, headers['x-locale'], basePath);
}),
);
};
}
81 changes: 46 additions & 35 deletions apps/docs/docs/integrations/articles/zendesk/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ The Zendesk Help Center integration provides:

The following table shows which methods from the base ArticleService are currently supported by the Zendesk integration:

| Method | Description | Supported |
| --------------- | --------------------------------------------- | --------- |
| getArticle | Retrieve a single article by slug/ID | ✓ |
| getArticleList | Retrieve a list of articles with filtering | ✓ |
| getCategory | Retrieve a single category by ID or slug | ✓ |
| getCategoryList | Retrieve a list of categories | ✓ |
| searchArticles | Search articles with query and filters | ✓ |
| Method | Description | Supported |
| --------------- | ------------------------------------------ | --------- |
| getArticle | Retrieve a single article by slug/ID | ✓ |
| getArticleList | Retrieve a list of articles with filtering | ✓ |
| getCategory | Retrieve a single category by ID or slug | ✓ |
| getCategoryList | Retrieve a list of categories | ✓ |
| searchArticles | Search articles with query and filters | ✓ |

## Module Structure

Expand Down Expand Up @@ -118,48 +118,58 @@ The integration maps Zendesk article data to the standard article model with the

### Field Mapping

| Zendesk Field | Normalized Field | Notes |
| ----------------- | ---------------- | ------------------------------------------ |
| id | id | Converted to string |
| created_at | createdAt | ISO date string |
| updated_at | updatedAt | ISO date string |
| title | title | Article title |
| body | sections | Parsed into ArticleSectionText |
| body (excerpt) | lead | First 300 characters of plain text |
| label_names | tags | Article labels/tags |
| html_url | slug | Extracted and combined with category slug |
| author_id | author | Fetched separately with avatar |
| section_id | category | Resolved via section → category lookup |
| Zendesk Field | Normalized Field | Notes |
| -------------- | ---------------- | -------------------------------------- |
| id | id | Converted to string |
| created_at | createdAt | ISO date string |
| updated_at | updatedAt | ISO date string |
| title | title | Article title |
| body | sections | Parsed into ArticleSectionText |
| body (excerpt) | lead | First 300 characters of plain text |
| label_names | tags | Article labels/tags |
| html_url | slug | Article segment extracted from URL |
| author_id | author | Fetched separately with avatar |
| section_id | category | Resolved via section → category lookup |

### Category Field Mapping

| Zendesk Field | Normalized Field | Notes |
| ------------- | ---------------- | --------------------------------- |
| id | id | Converted to string |
| created_at | createdAt | ISO date string |
| updated_at | updatedAt | ISO date string |
| name | title | Category name |
| description | description | Category description |
| html_url | slug | Full path with locale base |
| Zendesk Field | Normalized Field | Notes |
| ------------- | ---------------- | ------------------------------------ |
| id | id | Converted to string |
| created_at | createdAt | ISO date string |
| updated_at | updatedAt | ISO date string |
| name | title | Category name |
| description | description | Category description |
| html_url | slug | Category segment only (no base path) |

### Slug Generation

Article slugs are generated following this pattern:
The Zendesk integration returns article and category slugs as segments extracted from Zendesk URLs:

**Article slug format:**

```
/{locale-base}/{category-id}-{category-name}/{article-id}-{article-title}
{category-id}-{category-name}/{article-id}-{article-title}
```

**Locale bases:**
- English: `/help-and-support`
- German: `/hilfe-und-support`
- Polish: `/pomoc-i-wsparcie`
**Category slug format:**

**Example:**
```
/help-and-support/12345-Maintenance/67890-Tool-Care-Guide
{category-id}-{category-name}
```

The base path (e.g., `/help-and-support`) is **not** included in the slug returned by the integration. Instead, it's configured in the CMS block configuration via `parent.slug` property. This allows for flexible URL structures without hardcoding locale-specific paths in the integration code.

**Example slugs returned by the integration:**

- Article: `12345-Maintenance/67890-Tool-Care-Guide`
- Category: `12345-Maintenance`

**Full URL construction happens in:**

- CMS blocks (ArticleList, CategoryList) using `cms.parent.slug`
- Page mapper for article detail pages using extracted base path from URL

Comment thread
michnowak marked this conversation as resolved.
### Article Sections

Article body content is converted into sections:
Expand Down Expand Up @@ -196,6 +206,7 @@ Help Center
```

The integration:

1. Fetches articles with their section IDs
2. Resolves section → category relationship
3. Builds proper slugs with category information
71 changes: 36 additions & 35 deletions apps/docs/docs/integrations/articles/zendesk/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ Retrieve a list of articles with optional filtering and pagination.

**Query Parameters:**

| Parameter | Type | Description | Required |
| --------- | ------ | --------------------------------------------------- | -------- |
| locale | string | Language code (en, de, pl) | Yes |
| category | string | Filter by category ID or slug | No |
| offset | number | Pagination offset | No |
| limit | number | Number of articles per page (default: 10) | No |
| sortBy | string | Sort field | No |
| sortOrder | string | Sort direction (asc, desc) | No |
| Parameter | Type | Description | Required |
| --------- | ------ | ----------------------------------------- | -------- |
| locale | string | Language code (en, de, pl) | Yes |
| category | string | Filter by category ID or slug | No |
| offset | number | Pagination offset | No |
| limit | number | Number of articles per page (default: 10) | No |
| sortBy | string | Sort field | No |
| sortOrder | string | Sort direction (asc, desc) | No |

**Example Request:**

Expand All @@ -41,7 +41,7 @@ GET /articles?locale=en&limit=10&offset=0
"data": [
{
"id": "12345",
"slug": "/help-and-support/67890-Maintenance/12345-Tool-Care-Guide",
"slug": "67890-Maintenance/12345-Tool-Care-Guide",
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-16T14:20:00Z",
"title": "Tool Care Guide",
Expand Down Expand Up @@ -72,8 +72,8 @@ Retrieve a specific article by slug with full content.

**Path Parameters:**

| Parameter | Type | Description | Required |
| --------- | ------ | ---------------------------------------------- | -------- |
| Parameter | Type | Description | Required |
| --------- | ------ | ------------------------------------------------ | -------- |
| slug | string | Article slug or ID (e.g., "12345-article-title") | Yes |

**Query Parameters:**
Expand All @@ -93,7 +93,7 @@ GET /articles/12345-tool-care-guide?locale=en
```json
{
"id": "12345",
"slug": "/help-and-support/67890-Maintenance/12345-Tool-Care-Guide",
"slug": "67890-Maintenance/12345-Tool-Care-Guide",
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-16T14:20:00Z",
"title": "Tool Care Guide",
Expand Down Expand Up @@ -160,7 +160,7 @@ GET /articles/categories?locale=en
"data": [
{
"id": "67890",
"slug": "/help-and-support/67890-Maintenance",
"slug": "67890-Maintenance",
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-10T00:00:00Z",
"title": "Maintenance",
Expand All @@ -178,9 +178,9 @@ Retrieve a specific category by ID or slug.

**Path Parameters:**

| Parameter | Type | Description | Required |
| --------- | ------ | --------------------- | -------- |
| id | string | Category ID or slug | Yes |
| Parameter | Type | Description | Required |
| --------- | ------ | ------------------- | -------- |
| id | string | Category ID or slug | Yes |

**Query Parameters:**

Expand All @@ -199,7 +199,7 @@ GET /articles/categories/67890?locale=en
```json
{
"id": "67890",
"slug": "/help-and-support/67890-Maintenance",
"slug": "67890-Maintenance",
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-10T00:00:00Z",
"title": "Maintenance",
Expand All @@ -215,15 +215,15 @@ Search articles with full-text query.

**Body Parameters:**

| Parameter | Type | Description | Required |
| --------- | ------ | -------------------------------------- | -------- |
| locale | string | Language code (en, de, pl) | Yes |
| query | string | Search query | No |
| category | string | Filter by category ID | No |
| dateFrom | string | Filter articles created after (ISO) | No |
| dateTo | string | Filter articles created before (ISO) | No |
| sortBy | string | Sort field | No |
| sortOrder | string | Sort direction (asc, desc) | No |
| Parameter | Type | Description | Required |
| --------- | ------ | ------------------------------------ | -------- |
| locale | string | Language code (en, de, pl) | Yes |
| query | string | Search query | No |
| category | string | Filter by category ID | No |
| dateFrom | string | Filter articles created after (ISO) | No |
| dateTo | string | Filter articles created before (ISO) | No |
| sortBy | string | Sort field | No |
| sortOrder | string | Sort direction (asc, desc) | No |

**Example Request:**

Expand All @@ -246,7 +246,7 @@ Content-Type: application/json
"data": [
{
"id": "12345",
"slug": "/help-and-support/67890-Maintenance/12345-Tool-Care-Guide",
"slug": "67890-Maintenance/12345-Tool-Care-Guide",
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-16T14:20:00Z",
"title": "Tool Care Guide",
Expand All @@ -271,19 +271,20 @@ You should always use the short locale format (en, de, pl) in your API requests.

## Slug Format

Article slugs follow a hierarchical pattern that includes the category:
Article slugs returned by the Zendesk integration contain the category and article segments:

```
/{locale-base}/{category-id}-{category-name}/{article-id}-{article-title}
{category-id}-{category-name}/{article-id}-{article-title}
```

**Examples:**
**Example slugs returned by the integration:**

| Locale | Slug Example |
| ------ | ----------------------------------------------------------- |
| en | `/help-and-support/67890-Maintenance/12345-Tool-Care-Guide` |
| de | `/hilfe-und-support/67890-Wartung/12345-Werkzeugpflege` |
| pl | `/pomoc-i-wsparcie/67890-Konserwacja/12345-Pielegnacja` |
| Type | Slug Example |
| -------- | ----------------------------------------- |
| Article | `67890-Maintenance/12345-Tool-Care-Guide` |
| Category | `67890-Maintenance` |

The base path (e.g., `/help-and-support`, `/hilfe-und-support`) is configured separately in CMS block configuration via `parent.slug` property, not hardcoded in the integration. This allows the same integration to work with different URL structures.

## Filtering Examples

Expand Down
Loading