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
7 changes: 7 additions & 0 deletions portal-template/src/lib/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ export function getPageMarkdownUrl(page: { slugs: string[]; locale?: string }) {
return { segments, url: getDocsUrl(segments, page.locale) };
}

export function slugsFromSplat(splat: string | undefined): string[] {
return (splat ?? '')
.split('/')
.filter((segment) => segment.length > 0)
.map(encodeURI);
}

/** @returns page slugs */
export function decodeMarkdownUrl(segments: string[]) {
if (segments.length === 0) return [];
Expand Down
7 changes: 3 additions & 4 deletions portal-template/src/routes/$.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { createServerFn } from '@tanstack/react-start';
import { docs, generated } from '@/lib/source';
import { source } from '@/lib/source.server';
import { PortalLayout } from '@/lib/layout';
import { getPageMarkdownUrl } from '@/lib/shared';
import { getPageMarkdownUrl, slugsFromSplat } from '@/lib/shared';
import { portal } from '@/lib/portal';
import { absoluteUrl, canonicalLink } from '@/lib/seo';
import { useFumadocsLoader } from 'fumadocs-core/source/client';
Expand All @@ -26,7 +26,7 @@ const collections = { docs, generated };
export const Route = createFileRoute('/$')({
component: Page,
loader: async ({ params }) => {
const slugs = params._splat?.split('/').filter((segment) => segment.length > 0) ?? [];
const slugs = slugsFromSplat(params._splat);
const data = await loadPage(slugs);

if (data.type === 'docs') {
Expand All @@ -37,8 +37,7 @@ export const Route = createFileRoute('/$')({
head: ({ loaderData, params }) => {
const title = loaderData && loaderData.type !== 'home' ? `${loaderData.title} | ${portal.name}` : portal.name;
const description = loaderData?.description ?? portal.description;
const splat = params._splat?.replace(/\/$/, '') ?? '';
const pageUrl = splat.length > 0 ? `/${splat}` : '/';
const pageUrl = `/${slugsFromSplat(params._splat).join('/')}`;
const absolute = absoluteUrl(pageUrl);
return {
// Without the og:* pair, a documentation link pasted into Slack, Teams or LinkedIn
Expand Down
4 changes: 2 additions & 2 deletions portal-template/src/routes/{$}[.]md.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { createFileRoute, notFound } from '@tanstack/react-router';
import { source } from '@/lib/source.server';
import { renderHome, renderPage } from '@/lib/llms.server';
import { decodeMarkdownUrl } from '@/lib/shared';
import { decodeMarkdownUrl, slugsFromSplat } from '@/lib/shared';

const headers = { 'Content-Type': 'text/markdown' };

export const Route = createFileRoute('/{$}.md')({
server: {
handlers: {
GET: async ({ params }) => {
const slugs = decodeMarkdownUrl(params._splat?.split('/') ?? []);
const slugs = decodeMarkdownUrl(slugsFromSplat(params._splat));
const page = source.getPage(slugs);
if (page) return new Response(await renderPage(page), { headers });
// Mirrors the generated landing page served at "/" when there is no index page.
Expand Down
72 changes: 72 additions & 0 deletions test/portal-template/page-slugs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import fs from 'fs';
import os from 'os';
import path from 'path';
import { expect } from 'chai';
import { loader } from 'fumadocs-core/source';
import { openApiSection } from '../../portal-template/src/lib/openapi-section.server';
import { decodeMarkdownUrl, slugsFromSplat } from '../../portal-template/src/lib/shared';

const ok = { 200: { description: 'ok' } };

describe('slugsFromSplat', () => {
it('drops the empty segments a leading or trailing slash leaves behind', () => {
expect(slugsFromSplat('guides/getting-started/')).to.deep.equal(['guides', 'getting-started']);
expect(slugsFromSplat('')).to.deep.equal([]);
expect(slugsFromSplat(undefined)).to.deep.equal([]);
});

it('leaves an ordinary slug exactly as it was', () => {
expect(slugsFromSplat('api/openapi/pets/listPets')).to.deep.equal(['api', 'openapi', 'pets', 'listPets']);
});

it('encodes each segment the way the page slugs were encoded, and no further', () => {
expect(slugsFromSplat('api/List All Pets')).to.deep.equal(['api', 'List%20All%20Pets']);
expect(slugsFromSplat('café')).to.deep.equal(['caf%C3%A9']);
expect(slugsFromSplat('pets&cats')).to.deep.equal(['pets&cats']);
});
});

describe('a page whose operationId needs encoding', () => {
let directory: string;
let source: Awaited<ReturnType<typeof sectionSource>>;
let url: string;

const sectionSource = async () =>
loader({ openapi: await openApiSection('openapi', path.join(directory, 'api.json'), null) }, { baseUrl: '/' });

const decodedSplatOf = (pageUrl: string) => decodeURIComponent(pageUrl).replace(/^\//, '');

before(async () => {
directory = fs.mkdtempSync(path.join(os.tmpdir(), 'page-slugs-'));
fs.writeFileSync(
path.join(directory, 'api.json'),
JSON.stringify({
openapi: '3.1.0',
info: { title: 'Pets', version: '1' },
tags: [{ name: 'pets' }],
paths: { '/pets': { get: { operationId: 'List All Pets', tags: ['pets'], responses: ok } } }
})
);

source = await sectionSource();
const page = source.getPages().find((candidate) => candidate.url.includes('%20'));
expect(page, 'no page slug needed encoding').to.not.equal(undefined);

Check warning on line 53 in test/portal-template/page-slugs.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer "expect(page, 'no page slug needed encoding').to.not.be.undefined" over this generic assertion; dedicated matchers read better and report clearer failures.

See more on https://sonarcloud.io/project/issues?id=apimatic_apimatic-cli&issues=AaDn_6vITYiZeLRPXCzZ&open=AaDn_6vITYiZeLRPXCzZ&pullRequest=396
url = page!.url;
});

after(() => {
fs.rmSync(directory, { recursive: true, force: true });
});

it('is found from the splat the router decoded', () => {
expect(source.getPage(slugsFromSplat(decodedSplatOf(url)))?.url).to.equal(url);
});

it('is found at its Markdown address too', () => {
expect(source.getPage(decodeMarkdownUrl(slugsFromSplat(decodedSplatOf(url))))?.url).to.equal(url);
});

it('is missed when the splat is looked up as it arrives', () => {
expect(source.getPage(decodedSplatOf(url).split('/'))).to.equal(undefined);

Check warning on line 70 in test/portal-template/page-slugs.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer "expect(source.getPage(decodedSplatOf(url).split('/'))).to.be.undefined" over this generic assertion; dedicated matchers read better and report clearer failures.

See more on https://sonarcloud.io/project/issues?id=apimatic_apimatic-cli&issues=AaDoAi8x5cu-9vk0Dz25&open=AaDoAi8x5cu-9vk0Dz25&pullRequest=396
Comment thread
sohail2721 marked this conversation as resolved.
Resolved
});
});
Loading