Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove double-linking of theme CSS #598

Merged
merged 5 commits into from Mar 10, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Expand Up @@ -4,5 +4,5 @@ Contributions are very welcome! Installing the development version, building
the demo docs and developing the css/js of the theme, etc, is explained in
more detail in the contributing section of the documentation:

- [Contributing source files](docs/contributing.md)
- [Contributing rendered docs](https://pydata-sphinx-theme.readthedocs.io/en/latest/contributing.html)
- [Contributing source files](docs/contribute/index.md)
- [Contributing rendered docs](https://pydata-sphinx-theme.readthedocs.io/en/latest/contribute/index.html)
2 changes: 0 additions & 2 deletions docs/contribute/index.md
Expand Up @@ -36,8 +36,6 @@ The CSS and JS for this theme are built for the browser from `src/pydata_sphinx_

- the main part of the theme assets
- customizes [Bootstrap](https://getbootstrap.com/) with [Sass](https://sass-lang.com)
- points to the `font-face` of vendored web fonts, but does not include their
CSS `@font-face` declaration

- JS: `src/pydata_sphinx_theme/assets/scripts/index.js`

Expand Down
110 changes: 49 additions & 61 deletions docs/contribute/topics.md
Expand Up @@ -75,21 +75,62 @@ $ pre-commit run --all-files
$ pre-commit run -a
```

## Web asset compiling and bundling
## Web assets (CSS/JS/Fonts)

All of the CSS and JS assets stored in `src/pydata_sphinx_theme/assets` will be compiled and bundled with the theme when you build it locally.
These bundled assets will be placed in `src/pydata_sphinx_theme/theme/pydata_sphinx_theme/static`.
This theme includes several web assets to ease development and design.
The configuration for our asset compilation is in `webpack.config.js`.

The configuration that defines what happens upon compilation is at `webpack.config.js`.
### Compile and bundle assets

When assets are compiled, a `<hash>` is generated for each, and appended to the end of the asset's reference in the HTML templates of the theme.
When assets are compiled, static versions are placed in various places in the theme's static folder:

```
src/pydata_sphinx_theme/theme/pydata_sphinx_theme/static
```

For many assets, a `<hash>` is generated and appended to the end of its reference in the HTML templates of the theme.
This ensures the correct asset versions are served when viewers return to your
site after upgrading the theme.

Web fonts, and their supporting CSS, are copied into
`src/pydata_sphinx_theme/theme/pydata_sphinx_theme/static/vendor/<font name>/<font version>/`.
To compile the assets and bundle them with the theme, run this command:

```console
$ nox -s compile
```

### Styles (SCSS) and Scripts (JS)

There are two relevant places for CSS/JS assets:

- `src/pydata_sphinx_theme/assets/styles` has source files for SCSS assets. These will be compiled to CSS.
- `src/pydata_sphinx_theme/assets/scripts` has source files for JS assets. These will be compiled to JS and import several vendored libraries (like Bootstrap).
- `src/pydata_sphinx_theme/theme/pydata_sphinx_theme/static` has compiled versions of these assets (e.g. CSS files). This folder is not tracked in `.git` history, but it is bundled with the theme's distribution.

The links to these unique file names are captured as Jinja2 macros that are defined in HTML templates bundled with the theme.
### Vendored scripts

We vendor several packages in addition to our own CSS and JS.
For example, Bootstrap, JQuery, and Popper.
This is configured in the `webpack.config.js` file, and imported in the respective `SCSS` or `JS` file in our assets folder.

### FontAwesome icons

Three "styles" of the [FontAwesome 5 Free](https://fontawesome.com/icons?m=free)
icon font are used for {ref}`icon links <icon-links>` and admonitions, and is
the only `vendored` font.

- It is managed as a dependency in `package.json`
- Copied directly into the site statics at compilation, including licenses
- Partially preloaded to reduce flicker and artifacts of early icon renders
- Configured in `webpack.config.js`

### Jinja macros

Our Webpack build generates a collection of [Jinja macros](https://jinja.palletsprojects.com/en/3.0.x/templates/#macros) in the `static/webpack-macros.html` file.

These macros are imported in the main `layout.html` file, and then inserted at various places in the page to link the static assets.

Some of the assets [are "preloaded"](https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/preload), meaning that the browser begins requesting these resources before they're actually needed.
In particular, our JavaScript assets are preloaded in `<head>`, and the scripts are actually loaded at the end of `<body>`.

## Accessibility checks

Expand Down Expand Up @@ -171,59 +212,6 @@ The output of the last command includes:
- a short summary of the current state of the accessibility rules we are trying to maintain
- local paths to JSON and HTML reports which contain all of the issues found

## Change fonts

Three "styles" of the [FontAwesome 5 Free](https://fontawesome.com/icons?m=free)
icon font are used for {ref}`icon links <icon-links>` and admonitions, and is
the only `vendored` font. Further font choices are described in the {ref}`customizing`
section of the user guide, and require some knowledge of HTML and CSS.

The remaining vendored font selection is:

- managed as a dependency in `package.json`

- allowing the version to be managed centrally

- copied directly into the site statics, including licenses

- allowing the chosen font to be replaced (or removed entirely) with minimal
templating changes: practically, changing the icon font is difficult at this
point.

- partially preloaded

- reducing flicker and re-layout artifacts of early icon renders

- mostly managed in `webpack.js`

- allowing upgrades to be handled in a relatively sane, manageable way, to
ensure the most recent icons

### Upgrade a font

If _only_ the version of the `existing` font must change, for example to enable
new icons, edit the appropriate font version in `package.json`.
Then re-compile the theme with:

```console
$ nox -s compile
```

### Change a font

If the above doesn't work, for example if file names for an existing font change,
or a new font variant altogether is being added, hand-editing of `webpack.config.js`
is required. The steps are roughly:

- install the new font, as above
- in `webpack.config.js`:
- add the new font to `vendorVersions` and `vendorPaths`
- add new `link` tags to the appropriate macro in `macroTemplate`
- add the new font files (including the license) to `CopyPlugin`
- remove references to the font being replaced/removed, if applicable
- see the `font-awesome` sections of this configuration to see what the end-result configuration looks like.
- re-compile the theme's assets: `nox -s compile`

## Update our kitchen sink documents

The [kitchen sink reference](../demo/kitchen-sink/index.rst) is for demonstrating as much syntax and style for Sphinx builds as possible.
Expand Down
12 changes: 10 additions & 2 deletions src/pydata_sphinx_theme/__init__.py
Expand Up @@ -45,7 +45,8 @@ def update_config(app, env):


def update_templates(app, pagename, templatename, context, doctree):
"""Update template names for page build."""
"""Update template names and assets for page build."""
# Allow for more flexibility in template names
template_sections = [
"theme_navbar_start",
"theme_navbar_center",
Expand All @@ -55,7 +56,6 @@ def update_templates(app, pagename, templatename, context, doctree):
"theme_left_sidebar_end",
"sidebars",
]

for section in template_sections:
if context.get(section):
# Break apart `,` separated strings so we can use , in the defaults
Expand All @@ -69,6 +69,14 @@ def update_templates(app, pagename, templatename, context, doctree):
if not os.path.splitext(template)[1]:
context[section][ii] = template + ".html"

# Remove a duplicate entry of the theme CSS. This is because it is in both:
# - theme.conf
# - manually linked in `webpack-macros.html`
if "css_files" in context:
theme_css_name = "_static/styles/pydata-sphinx-theme.css"
if theme_css_name in context["css_files"]:
context["css_files"].remove(theme_css_name)


def add_toctree_functions(app, pagename, templatename, context, doctree):
"""Add functions so Jinja templates can add toctree objects."""
Expand Down
Expand Up @@ -2,11 +2,8 @@
{%- import "static/webpack-macros.html" as _webpack with context %}

{%- block css %}
{{ _webpack.head_pre_bootstrap() }}
{{ _webpack.head_pre_assets() }}
{{ _webpack.head_pre_icons() }}
{% block fonts %}
damianavila marked this conversation as resolved.
Show resolved Hide resolved
{{ _webpack.head_pre_fonts() }}
{% endblock %}
{{- css() }}
{{ _webpack.head_js_preload() }}
{%- endblock %}
Expand Down
2 changes: 2 additions & 0 deletions src/pydata_sphinx_theme/theme/pydata_sphinx_theme/theme.conf
@@ -1,5 +1,7 @@
[theme]
inherit = basic
# Note that we don't link the CSS file via Sphinx
# instead we manually link it in `webpack-macros.html`
stylesheet = styles/pydata-sphinx-theme.css
pygments_style = tango
sidebars = search-field.html, sidebar-nav-bs.html
Expand Down
37 changes: 24 additions & 13 deletions webpack.config.js
@@ -1,7 +1,13 @@
// Webpack configuration for pydata-sphinx-theme.
//
// There's a decent amount of complexity here, arising from the fact that we
// have a fairly non-standard "JS application" here.
/**
* Webpack configuration for pydata-sphinx-theme.
*
* This script does a few primary things:
*
* - Generates a `webpack-macros.html` file that defines macros used
* to insert CSS / JS at various places in the main `layout.html` template.
* - Compiles our SCSS and JS and places them in the _static/ folder
* - Downloads and links FontAwesome and some JS libraries (Bootstrap, jQuery, etc)
*/

const { resolve } = require("path");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
Expand Down Expand Up @@ -31,26 +37,31 @@ const vendorPaths = {
//
function macroTemplate({ compilation }) {
const hash = compilation.hash;
// We load these files into the theme via HTML templates
const css_files = ["styles/theme.css", "styles/pydata-sphinx-theme.css"];
const js_files = ["scripts/pydata-sphinx-theme.js"];

// Load a CSS script with a digest for cache busting.
function stylesheet(css) {
return `<link href="{{ pathto('_static/${css}', 1) }}?digest=${hash}" rel="stylesheet">`;
}

// Pre-load a JS script (script will need to be loaded later on in the page)
function preload(js) {
return `<link rel="preload" as="script" href="{{ pathto('_static/${js}', 1) }}?digest=${hash}">`;
}

// Load a JS script with a digest for cache busting.
function script(js) {
return `<script src="{{ pathto('_static/${js}', 1) }}?digest=${hash}"></script>`;
}

return dedent(`\
<!--
All these macros are auto-generated and must **NOT** be edited by hand.
See the webpack.config.js file, to learn more about how this is generated.
AUTO-GENERATED from webpack.config.js, do **NOT** edit by hand.
These are re-used in layout.html
-->
{# Load FontAwesome icons #}
{% macro head_pre_icons() %}
<link rel="stylesheet"
href="{{ pathto('_static/vendor/fontawesome/${
Expand All @@ -66,19 +77,19 @@ function macroTemplate({ compilation }) {
}/webfonts/fa-brands-400.woff2', 1) }}">
{% endmacro %}

{% macro head_pre_fonts() %}
{% endmacro %}

{% macro head_pre_bootstrap() %}
${css_files.map(stylesheet).join("\n ")}
{% macro head_pre_assets() %}
<!-- Loaded before other Sphinx assets -->
${css_files.map(stylesheet).join("\n")}
{% endmacro %}

{% macro head_js_preload() %}
${js_files.map(preload).join("\n ")}
<!-- Pre-loaded scripts that we'll load fully later -->
${js_files.map(preload).join("\n")}
{% endmacro %}

{% macro body_post() %}
${js_files.map(script).join("\n ")}
<!-- Scripts loaded after <body> so the DOM is not blocked -->
${js_files.map(script).join("\n")}
{% endmacro %}
`);
}
Expand Down