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
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import {useBaseUrlUtils, type BaseUrlUtils} from '@docusaurus/useBaseUrl';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import {useBlogMetadata} from '@docusaurus/plugin-content-blog/client';
import {applyTrailingSlash} from '@docusaurus/utils-common';
import type {Props as BlogListPageStructuredDataProps} from '@theme/BlogListPage/StructuredData';
import {useBlogPost} from './contexts';

Expand All @@ -26,6 +27,17 @@ import type {DocusaurusConfig} from '@docusaurus/types';

const convertDate = (dateMs: number) => new Date(dateMs).toISOString();

function getAbsoluteUrl(
permalink: string,
siteConfig: DocusaurusConfig,
): string {
const absoluteUrl = `${siteConfig.url}${permalink}`;
return applyTrailingSlash(absoluteUrl, {
trailingSlash: siteConfig.trailingSlash,
baseUrl: siteConfig.baseUrl,
});
}

function getBlogPost(
blogPostContent: PropBlogPostContent,
siteConfig: DocusaurusConfig,
Expand All @@ -37,7 +49,7 @@ function getBlogPost(
const image = assets.image ?? frontMatter.image;
const keywords = frontMatter.keywords ?? [];

const blogUrl = `${siteConfig.url}${metadata.permalink}`;
const blogUrl = getAbsoluteUrl(metadata.permalink, siteConfig);

const dateModified = lastUpdatedAt ? convertDate(lastUpdatedAt) : undefined;

Expand Down Expand Up @@ -92,7 +104,7 @@ export function useBlogListPageStructuredData(
metadata: {blogDescription, blogTitle, permalink},
} = props;

const url = `${siteConfig.url}${permalink}`;
const url = getAbsoluteUrl(permalink, siteConfig);

// details on structured data support: https://schema.org/Blog
return {
Expand Down Expand Up @@ -121,7 +133,7 @@ export function useBlogPostStructuredData(): WithContext<BlogPosting> {

const dateModified = lastUpdatedAt ? convertDate(lastUpdatedAt) : undefined;

const url = `${siteConfig.url}${metadata.permalink}`;
const url = getAbsoluteUrl(metadata.permalink, siteConfig);

// details on structured data support: https://schema.org/BlogPosting
// BlogPosting is one of the structured data types that Google explicitly
Expand All @@ -142,7 +154,7 @@ export function useBlogPostStructuredData(): WithContext<BlogPosting> {
...(keywords ? {keywords} : {}),
isPartOf: {
'@type': 'Blog',
'@id': `${siteConfig.url}${blogMetadata.blogBasePath}`,
'@id': getAbsoluteUrl(blogMetadata.blogBasePath, siteConfig),
name: blogMetadata.blogTitle,
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,28 @@
*/

import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import {applyTrailingSlash} from '@docusaurus/utils-common';
import type {PropSidebarBreadcrumbsItem} from '@docusaurus/plugin-content-docs';
import type {WithContext, BreadcrumbList} from 'schema-dts';
import type {DocusaurusConfig} from '@docusaurus/types';

export function useBreadcrumbsStructuredData({
type Params = Pick<DocusaurusConfig, 'url' | 'baseUrl' | 'trailingSlash'>;

function getAbsoluteUrl(permalink: string, params: Params): string {
const absoluteUrl = `${params.url}${permalink}`;
return applyTrailingSlash(absoluteUrl, {
trailingSlash: params.trailingSlash,
baseUrl: params.baseUrl,
});
}

function getBreadcrumbsStructuredData({
breadcrumbs,
params,
}: {
breadcrumbs: PropSidebarBreadcrumbsItem[];
params: Params;
}): WithContext<BreadcrumbList> {
const {siteConfig} = useDocusaurusContext();
return {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
Expand All @@ -26,7 +39,16 @@ export function useBreadcrumbsStructuredData({
'@type': 'ListItem',
position: index + 1,
name: breadcrumb.label,
item: `${siteConfig.url}${breadcrumb.href}`,
item: getAbsoluteUrl(breadcrumb.href!, params),
})),
};
}

export function useBreadcrumbsStructuredData({
breadcrumbs,
}: {
breadcrumbs: PropSidebarBreadcrumbsItem[];
}): WithContext<BreadcrumbList> {
const {siteConfig} = useDocusaurusContext();
return getBreadcrumbsStructuredData({breadcrumbs, params: siteConfig});
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,15 @@ describe('applyTrailingSlash', () => {
expect(applyTrailingSlash('/abc/', params(undefined))).toBe('/abc/');
});

it('keeps dollar signs in the pathname', () => {
// Regression test for edge case bug
// see https://github.com/facebook/docusaurus/pull/12219
expect(applyTrailingSlash('/docs/a$$b', params(true))).toBe('/docs/a$$b/');
expect(applyTrailingSlash('/docs/a$&b', params(true))).toBe('/docs/a$&b/');
expect(applyTrailingSlash("/docs/a$'b", params(true))).toBe("/docs/a$'b/");
expect(applyTrailingSlash('/docs/a$$b/', params(false))).toBe('/docs/a$$b');
});

it('applies to path with #anchor', () => {
expect(applyTrailingSlash('/abc#anchor', params(true))).toBe(
'/abc/#anchor',
Expand Down
8 changes: 6 additions & 2 deletions packages/docusaurus-utils-common/src/applyTrailingSlash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ export function addTrailingSlash(str: string): string {
return str.endsWith('/') ? str : `${str}/`;
}

// Trailing slash handling depends in some site configuration options
/**
* Apply/remove a trailing slash on an URL path according to site config options
* Usually applied on permalinks / URL paths, but also works with absolute URL
*/
export default function applyTrailingSlash(
path: string,
options: ApplyTrailingSlashParams,
Expand All @@ -40,6 +43,7 @@ export default function applyTrailingSlash(

// The trailing slash should be handled before the ?search#hash !
const [pathname] = path.split(/[#?]/) as [string, ...string[]];
const queryHash = path.slice(pathname.length);

// Never transform '/' to ''
// Never remove the baseUrl trailing slash!
Expand All @@ -51,7 +55,7 @@ export default function applyTrailingSlash(
? pathname
: handleTrailingSlash(pathname, trailingSlash);

return path.replace(pathname, newPathname);
return `${newPathname}${queryHash}`;
}

/** Appends a leading slash to `str`, if one doesn't exist. */
Expand Down
2 changes: 1 addition & 1 deletion website/docs/api/plugins/plugin-content-blog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ Accepted fields:
| `keywords` | `string[]` | `undefined` | Keywords meta tag, which will become the `<meta name="keywords" content="keyword1,keyword2,..."/>` in `<head>`, used by search engines. |
| `description` | `string` | The first line of Markdown content | The description of your document, which will become the `<meta name="description" content="..."/>` and `<meta property="og:description" content="..."/>` in `<head>`, used by search engines. |
| `image` | `string` | `undefined` | Cover or thumbnail image that will be used as the `<meta property="og:image" content="..."/>` in the `<head>`, enhancing link previews on social media and messaging platforms. |
| `slug` | `string` | File path | Allows to customize the blog post URL (`/<routeBasePath>/<slug>`). Support multiple patterns: `slug: my-blog-post`, `slug: /my/path/to/blog/post`, slug: `/`. |
| `slug` | `string` | File path | Allows to customize the blog post URL (`/<routeBasePath>/<slug>`). Supports multiple patterns: `slug: my-blog-post`, `slug: /my/path/to/blog/post`, slug: `/`. |
| `last_update` | `FrontMatterLastUpdate` | `undefined` | Allows overriding the last update author/date. Date can be any [parsable date string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse). |

```mdx-code-block
Expand Down
2 changes: 1 addition & 1 deletion website/docs/api/plugins/plugin-content-docs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ Accepted fields:
| `keywords` | `string[]` | `undefined` | Keywords meta tag for the document page, for search engines. |
| `description` | `string` | The first line of Markdown content | The description of your document, which will become the `<meta name="description" content="..."/>` and `<meta property="og:description" content="..."/>` in `<head>`, used by search engines. |
| `image` | `string` | `undefined` | Cover or thumbnail image that will be used as the `<meta property="og:image" content="..."/>` in the `<head>`, enhancing link previews on social media and messaging platforms. |
| `slug` | `string` | File path | Allows to customize the document URL (`/<routeBasePath>/<slug>`). Support multiple patterns: `slug: my-doc`, `slug: /my/path/myDoc`, `slug: /`. |
| `slug` | `string` | File path | Allows to customize the document URL (`/<routeBasePath>/<slug>`). Supports multiple patterns: `slug: my-doc`, `slug: /my/path/myDoc`, `slug: /`. |
| `tags` | `Tag[]` | `undefined` | A list of strings or objects of two string fields `label` and `permalink` to tag to your docs. Strings can be a reference to keys of a [tags file](#tags-file) (usually `tags.yml`) |
| `draft` | `boolean` | `false` | Draft documents will only be available during development. |
| `unlisted` | `boolean` | `false` | Unlisted documents will be available in both development and production. They will be "hidden" in production, not indexed, excluded from sitemaps, and can only be accessed by users having a direct link. |
Expand Down
2 changes: 1 addition & 1 deletion website/docs/api/plugins/plugin-content-pages.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ Accepted fields:
| `description` | `string` | The first line of Markdown content | The description of your page, which will become the `<meta name="description" content="..."/>` and `<meta property="og:description" content="..."/>` in `<head>`, used by search engines. |
| `keywords` | `string[]` | `undefined` | Keywords meta tag, which will become the `<meta name="keywords" content="keyword1,keyword2,..."/>` in `<head>`, used by search engines. |
| `image` | `string` | `undefined` | Cover or thumbnail image that will be used as the `<meta property="og:image" content="..."/>` in the `<head>`, enhancing link previews on social media and messaging platforms. |
| `slug` | `string` | File path | Allows to customize the page URL (`/<routeBasePath>/<slug>`). Support multiple patterns: `slug: my-page`, `slug: /my/page`, slug: `/`. |
| `slug` | `string` | File path | Allows to customize the page URL (`/<routeBasePath>/<slug>`). Supports multiple patterns: `slug: my-page`, `slug: /my/page`, slug: `/`. |
| `wrapperClassName` | `string` | | Class name to be added to the wrapper element to allow targeting specific page content. |
| `hide_table_of_contents` | `boolean` | `false` | Whether to hide the table of contents to the right. |
| `draft` | `boolean` | `false` | Draft pages will only be available during development. |
Expand Down
2 changes: 1 addition & 1 deletion website/docs/api/themes/theme-configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ Our [main themes](./overview.mdx) offer additional theme configuration options f

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `versionPersistence` | `'localStorage' \| 'none'` | `undefined` | Defines the browser persistence of the preferred docs version. |
| `versionPersistence` | `'localStorage' \| 'none'` | `'localStorage'` | Defines the browser persistence of the preferred docs version. |
| `sidebar.hideable` | `boolean` | `false` | Show a hide button at the bottom of the sidebar. |
| `sidebar.autoCollapseCategories` | `boolean` | `false` | Automatically collapse all sibling categories of the one you navigate to. |

Expand Down
Loading