Study Docusaurus Code: Static Assets

mingming-ma

Mingming Ma

Posted on October 31, 2023

Study Docusaurus Code: Static Assets

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} />;
Enter fullscreen mode Exit fullscreen mode

2.

<img src={require('@site/static/img/docusaurus.png').default} />
Enter fullscreen mode Exit fullscreen mode

3.

import useBaseUrl from '@docusaurus/useBaseUrl';

<img src={useBaseUrl('/img/docusaurus.png')} />;
Enter fullscreen mode Exit fullscreen mode

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);
}

Enter fullscreen mode Exit fullscreen mode

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

  1. 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;
  }
Enter fullscreen mode Exit fullscreen mode

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');
  });
});

Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode
    expect(withBaseUrl('//reactjs.org')).toBe('//reactjs.org');
Enter fullscreen mode Exit fullscreen mode
    expect(withBaseUrl('#hello')).toBe('#hello');
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
mingming-ma
Mingming Ma

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