Study Docusaurus Code: Static Assets
Mingming Ma
Posted on October 31, 2023
Docusaurus is an open-source static-site generator that builds a single-page application with fast client-side navigation, leveraging the full power of React to make sites interactive. I started learning from the Docusaurus source code a few days ago and would like to share my findings in this blog about Static Assets.
Static Assets
Static assets are the non-code files that are directly copied to the build output. They include images, stylesheets, favicons, fonts, etc. By default, as the name implies, it is the static
folder that used to store these file.
Docusaurus implementation
There are three ways showed in the Docusaurus document for the static assets.
1.
import DocusaurusImageUrl from '@site/static/img/docusaurus.png';
<img src={DocusaurusImageUrl} />;
2.
<img src={require('@site/static/img/docusaurus.png').default} />
3.
import useBaseUrl from '@docusaurus/useBaseUrl';
<img src={useBaseUrl('/img/docusaurus.png')} />;
The first one is using the import() method which is a dynamic import statement introduced in ECMAScript and is commonly used in modern JavaScript for loading modules on-demand. The second one is using the require() function which is core part of the Node.js runtime. The third one is introduced by Docusaurus. Let's look at it.
useBaseUrl
function
We can find how useBaseUrl
function implemented in the source code docusaurus/packages/docusaurus/src/client/exports/useBaseUrl.ts
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {useCallback} from 'react';
import useDocusaurusContext from './useDocusaurusContext';
import {hasProtocol} from './isInternalUrl';
import type {BaseUrlOptions, BaseUrlUtils} from '@docusaurus/useBaseUrl';
function addBaseUrl(
siteUrl: string,
baseUrl: string,
url: string,
{forcePrependBaseUrl = false, absolute = false}: BaseUrlOptions = {},
): string {
// It never makes sense to add base url to a local anchor url, or one with a
// protocol
if (!url || url.startsWith('#') || hasProtocol(url)) {
return url;
}
if (forcePrependBaseUrl) {
return baseUrl + url.replace(/^\//, '');
}
// /baseUrl -> /baseUrl/
// https://github.com/facebook/docusaurus/issues/6315
if (url === baseUrl.replace(/\/$/, '')) {
return baseUrl;
}
// We should avoid adding the baseurl twice if it's already there
const shouldAddBaseUrl = !url.startsWith(baseUrl);
const basePath = shouldAddBaseUrl ? baseUrl + url.replace(/^\//, '') : url;
return absolute ? siteUrl + basePath : basePath;
}
export function useBaseUrlUtils(): BaseUrlUtils {
const {
siteConfig: {baseUrl, url: siteUrl},
} = useDocusaurusContext();
const withBaseUrl = useCallback(
(url: string, options?: BaseUrlOptions) =>
addBaseUrl(siteUrl, baseUrl, url, options),
[siteUrl, baseUrl],
);
return {
withBaseUrl,
};
}
export default function useBaseUrl(
url: string,
options: BaseUrlOptions = {},
): string {
const {withBaseUrl} = useBaseUrlUtils();
return withBaseUrl(url, options);
}
The useBaseUrlUtils
function provides a utility to use the addBaseUrl
function with the site's baseUrl
and siteUrl
. It returns an object with a withBaseUrl
function that can be used to generate absolute or relative URLs. The useDocusaurusContext
function to access Docusaurus site configuration, hasProtocol
to check if a URL has a protocol (like "http" or "https"), and types related to BaseUrlOptions
and BaseUrlUtils
. The main useBaseUrl
function takes a url
and optional options
and delegates the work to the withBaseUrl
function provided by useBaseUrlUtils
.
Other insights from the code
- local anchor url
// It never makes sense to add base url to a local anchor url, or one with a
// protocol
if (!url || url.startsWith('#') || hasProtocol(url)) {
return url;
}
I realized I had been neglecting this special case
Unit test
we can see the unit test code from docusaurus/packages/docusaurus/src/client/exports/__tests__/useBaseUrl.test.tsx
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React from 'react';
import {renderHook} from '@testing-library/react-hooks';
import useBaseUrl, {useBaseUrlUtils} from '../useBaseUrl';
import {Context} from '../../docusaurusContext';
import type {DocusaurusContext} from '@docusaurus/types';
import type {BaseUrlOptions} from '@docusaurus/useBaseUrl';
const forcePrepend = {forcePrependBaseUrl: true};
describe('useBaseUrl', () => {
const createUseBaseUrlMock =
(context: DocusaurusContext) => (url: string, options?: BaseUrlOptions) =>
renderHook(() => useBaseUrl(url, options), {
wrapper: ({children}) => (
<Context.Provider value={context}>{children}</Context.Provider>
),
}).result.current;
it('works with empty base URL', () => {
const mockUseBaseUrl = createUseBaseUrlMock({
siteConfig: {
baseUrl: '/',
url: 'https://docusaurus.io',
},
} as DocusaurusContext);
expect(mockUseBaseUrl('hello')).toBe('/hello');
expect(mockUseBaseUrl('/hello')).toBe('/hello');
expect(mockUseBaseUrl('hello/')).toBe('/hello/');
expect(mockUseBaseUrl('/hello/')).toBe('/hello/');
expect(mockUseBaseUrl('hello/foo')).toBe('/hello/foo');
expect(mockUseBaseUrl('/hello/foo')).toBe('/hello/foo');
expect(mockUseBaseUrl('hello/foo/')).toBe('/hello/foo/');
expect(mockUseBaseUrl('/hello/foo/')).toBe('/hello/foo/');
expect(mockUseBaseUrl('https://github.com')).toBe('https://github.com');
expect(mockUseBaseUrl('//reactjs.org')).toBe('//reactjs.org');
expect(mockUseBaseUrl('//reactjs.org', forcePrepend)).toBe('//reactjs.org');
expect(mockUseBaseUrl('https://site.com', forcePrepend)).toBe(
'https://site.com',
);
expect(mockUseBaseUrl('/hello/foo', {absolute: true})).toBe(
'https://docusaurus.io/hello/foo',
);
expect(mockUseBaseUrl('#hello')).toBe('#hello');
});
it('works with non-empty base URL', () => {
const mockUseBaseUrl = createUseBaseUrlMock({
siteConfig: {
baseUrl: '/docusaurus/',
url: 'https://docusaurus.io',
},
} as DocusaurusContext);
expect(mockUseBaseUrl('')).toBe('');
expect(mockUseBaseUrl('hello')).toBe('/docusaurus/hello');
expect(mockUseBaseUrl('/hello')).toBe('/docusaurus/hello');
expect(mockUseBaseUrl('hello/')).toBe('/docusaurus/hello/');
expect(mockUseBaseUrl('/hello/')).toBe('/docusaurus/hello/');
expect(mockUseBaseUrl('hello/foo')).toBe('/docusaurus/hello/foo');
expect(mockUseBaseUrl('/hello/foo')).toBe('/docusaurus/hello/foo');
expect(mockUseBaseUrl('hello/foo/')).toBe('/docusaurus/hello/foo/');
expect(mockUseBaseUrl('/hello/foo/')).toBe('/docusaurus/hello/foo/');
expect(mockUseBaseUrl('https://github.com')).toBe('https://github.com');
expect(mockUseBaseUrl('//reactjs.org')).toBe('//reactjs.org');
expect(mockUseBaseUrl('//reactjs.org', forcePrepend)).toBe('//reactjs.org');
expect(mockUseBaseUrl('/hello', forcePrepend)).toBe('/docusaurus/hello');
expect(mockUseBaseUrl('https://site.com', forcePrepend)).toBe(
'https://site.com',
);
expect(mockUseBaseUrl('/hello/foo', {absolute: true})).toBe(
'https://docusaurus.io/docusaurus/hello/foo',
);
expect(mockUseBaseUrl('/docusaurus')).toBe('/docusaurus/');
expect(mockUseBaseUrl('/docusaurus/')).toBe('/docusaurus/');
expect(mockUseBaseUrl('/docusaurus/hello')).toBe('/docusaurus/hello');
expect(mockUseBaseUrl('#hello')).toBe('#hello');
});
});
describe('useBaseUrlUtils().withBaseUrl()', () => {
const mockUseBaseUrlUtils = (context: DocusaurusContext) =>
renderHook(() => useBaseUrlUtils(), {
wrapper: ({children}) => (
<Context.Provider value={context}>{children}</Context.Provider>
),
}).result.current;
it('empty base URL', () => {
const {withBaseUrl} = mockUseBaseUrlUtils({
siteConfig: {
baseUrl: '/',
url: 'https://docusaurus.io',
},
} as DocusaurusContext);
expect(withBaseUrl('hello')).toBe('/hello');
expect(withBaseUrl('/hello')).toBe('/hello');
expect(withBaseUrl('hello/')).toBe('/hello/');
expect(withBaseUrl('/hello/')).toBe('/hello/');
expect(withBaseUrl('hello/foo')).toBe('/hello/foo');
expect(withBaseUrl('/hello/foo')).toBe('/hello/foo');
expect(withBaseUrl('hello/foo/')).toBe('/hello/foo/');
expect(withBaseUrl('/hello/foo/')).toBe('/hello/foo/');
expect(withBaseUrl('https://github.com')).toBe('https://github.com');
expect(withBaseUrl('//reactjs.org')).toBe('//reactjs.org');
expect(withBaseUrl('//reactjs.org', forcePrepend)).toBe('//reactjs.org');
expect(withBaseUrl('https://site.com', forcePrepend)).toBe(
'https://site.com',
);
expect(withBaseUrl('/hello/foo', {absolute: true})).toBe(
'https://docusaurus.io/hello/foo',
);
expect(withBaseUrl('#hello')).toBe('#hello');
});
it('non-empty base URL', () => {
const {withBaseUrl} = mockUseBaseUrlUtils({
siteConfig: {
baseUrl: '/docusaurus/',
url: 'https://docusaurus.io',
},
} as DocusaurusContext);
expect(withBaseUrl('hello')).toBe('/docusaurus/hello');
expect(withBaseUrl('/hello')).toBe('/docusaurus/hello');
expect(withBaseUrl('hello/')).toBe('/docusaurus/hello/');
expect(withBaseUrl('/hello/')).toBe('/docusaurus/hello/');
expect(withBaseUrl('hello/foo')).toBe('/docusaurus/hello/foo');
expect(withBaseUrl('/hello/foo')).toBe('/docusaurus/hello/foo');
expect(withBaseUrl('hello/foo/')).toBe('/docusaurus/hello/foo/');
expect(withBaseUrl('/hello/foo/')).toBe('/docusaurus/hello/foo/');
expect(withBaseUrl('https://github.com')).toBe('https://github.com');
expect(withBaseUrl('//reactjs.org')).toBe('//reactjs.org');
expect(withBaseUrl('//reactjs.org', forcePrepend)).toBe('//reactjs.org');
expect(withBaseUrl('https://site.com', forcePrepend)).toBe(
'https://site.com',
);
expect(withBaseUrl('/hello/foo', {absolute: true})).toBe(
'https://docusaurus.io/docusaurus/hello/foo',
);
expect(withBaseUrl('/docusaurus')).toBe('/docusaurus/');
expect(withBaseUrl('/docusaurus/')).toBe('/docusaurus/');
expect(withBaseUrl('/docusaurus/hello')).toBe('/docusaurus/hello');
expect(withBaseUrl('#hello')).toBe('#hello');
});
});
From the test code, I find the well written test covered lots of cases. For me, it serves as an excellent TDD example to emulate. Such as the handling double slashes and the local anchor url situations.
expect(withBaseUrl('/hello')).toBe('/docusaurus/hello');
expect(withBaseUrl('//reactjs.org')).toBe('//reactjs.org');
expect(withBaseUrl('#hello')).toBe('#hello');
Conclusion
I've gained a deeper understanding of the inner workings of Docusaurus's useBaseUrl
function. The comprehensive unit tests have underscored the reliability of the codebase. The special cases have proven to be very helpful to me in the development of similar features.
Posted on October 31, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024